0%
Processamento de Linguagem Natural (NLP)

NLP

Do texto bruto aos grandes modelos de linguagem.

10-15 min

Processamento de Linguagem Natural (NLP)

Um modelo só sabe manipular números. O texto, por sua vez, é uma sequência de caracteres cujo sentido depende da ordem, do contexto, do implícito e, às vezes, do que não está escrito. Entre esses dois mundos, é preciso construir uma ponte — e a forma como essa ponte é construída determina quase tudo o que o sistema saberá fazer, e tudo o que ele errará.

É isso que explica comportamentos desconcertantes. Um modelo de linguagem capaz de redigir uma dissertação cheia de nuances pode errar ao contar as letras de uma palavra. Ele pode afirmar, com aplomb perfeito, uma referência bibliográfica que nunca existiu. Essas falhas parecem aleatórias; não são. Cada uma se deduz de uma escolha de projeto precisa, situada em algum ponto da ponte entre os caracteres e os números.

Esta lição percorre essa ponte em ordem: dividir o texto, transformar os pedaços em vetores, fazer com que cada pedaço leve os outros em conta. Esse terceiro ponto é a atenção, a ideia que mudou o rumo da área em 2017 e sobre a qual se apoia tudo o que hoje se chama de « grande modelo de linguagem ». Vamos codificá-la em cinco linhas de NumPy.

Terminaremos com o mecanismo da alucinação. Não para lamentá-la, mas para mostrar que ela é a consequência direta e previsível do objetivo dado a esses modelos.

Dividir o texto: a tokenização

Primeira decisão: qual é a unidade elementar? A resposta óbvia — a palavra, separada por espaços — logo revela seus limites.

texte = "Le chat dort. Les chats dorment, le chaton dort aussi !"

mots = texte.lower().replace(".", " ").replace(",", " ").replace("!", " ").split()
print(mots)
# ['le', 'chat', 'dort', 'les', 'chats', 'dorment', 'le', 'chaton', 'dort', 'aussi']
print(len(mots), len(set(mots)))   # 10 8

Para a máquina, chat, chats e chaton são três símbolos sem nenhuma relação entre si — tão estranhos um ao outro quanto chat e aussi. O mesmo vale para dort e dorment. Cada forma flexionada precisará ser aprendida separadamente, o que multiplica o número de exemplos necessários.

O segundo problema é o vocabulário fora da lista. Uma palavra ausente do vocabulário de treinamento — nome próprio, neologismo, erro de digitação — não tem nenhuma representação e acaba substituída por um símbolo genérico de desconhecido.

É por isso que os modelos atuais dividem em subpalavras. O algoritmo mais difundido, o Byte Pair Encoding, parte dos caracteres e funde iterativamente os pares mais frequentes do corpus. O resultado é um meio-termo: as palavras comuns permanecem em um único pedaço, as palavras raras se decompõem em fragmentos conhecidos.

Divisão em palavras      : ["anticonstitutionnellement"]  → desconhecida se ausente
Divisão em subpalavras   : ["anti", "constitution", "nelle", "ment"]  → tudo é conhecido

Essa escolha tem uma consequência direta e muitas vezes mal compreendida: um modelo de linguagem não vê as letras. Ele vê identificadores de fragmentos. Pedir a ele quantas vezes a letra « r » aparece em uma palavra equivale a pedir uma informação que ele não tem diante dos olhos, o que explica a fragilidade bem documentada desses modelos em tarefas de contagem de caracteres, anagramas ou rimas.

Nas abordagens clássicas, cada documento é então representado por um vetor de contagens:

from sklearn.feature_extraction.text import CountVectorizer

corpus = [
    "le chat dort sur le canape",
    "le chien dort sur le tapis",
    "le chat mange une souris",
    "le chien mange une croquette",
    "la souris mange du fromage",
]

v = CountVectorizer()
M = v.fit_transform(corpus)
print(M.shape)                    # (5, 14)
print(v.get_feature_names_out())
# ['canape' 'chat' 'chien' 'croquette' 'dort' 'du' 'fromage' 'la' 'le'
#  'mange' 'souris' 'sur' 'tapis' 'une']

Cinco frases muito curtas já produzem 14 colunas. Em um corpus real, esse vetor tem dezenas de milhares de dimensões, quase todas com valor zero para um dado documento. E, sobretudo, essa representação ignora completamente a ordem: « le chat mange la souris » e « la souris mange le chat » são idênticas nela.

Das palavras aos vetores: os embeddings

A ideia que destrava a situação é linguística antes de ser matemática: palavras que aparecem nos mesmos contextos têm sentidos próximos. É a hipótese distribucional, e ela se verifica por meio do cálculo.

import numpy as np

vocab = v.get_feature_names_out().tolist()

# Matriz de coocorrência: quantas vezes duas palavras aparecem
# na mesma frase.
C = np.zeros((len(vocab), len(vocab)))
for phrase in corpus:
    presents = [vocab.index(m) for m in phrase.split() if m in vocab]
    for i in presents:
        for j in presents:
            if i != j:
                C[i, j] += 1

def cosinus(u, w):
    return float(u @ w / (np.linalg.norm(u) * np.linalg.norm(w)))

i_chat = vocab.index("chat")
i_chien = vocab.index("chien")
i_fromage = vocab.index("fromage")

print(round(cosinus(C[i_chat], C[i_chien]), 3))     # 0.867
print(round(cosinus(C[i_chat], C[i_fromage]), 3))   # 0.258

Com apenas cinco frases, chat e chien obtêm uma similaridade de 0,867, contra 0,258 entre chat e fromage. O cálculo não recebeu nenhuma informação sobre animais: ele simplesmente constatou que chat e chien são cercados pelas mesmas palavras (le, dort, sur, mange), enquanto fromage frequenta outra vizinhança.

Os embeddings (word embeddings) industrializam essa ideia. Em vez de vetores esparsos do tamanho do vocabulário, aprende-se para cada palavra um vetor denso de algumas centenas de dimensões, otimizado para prever seu contexto. Esse é o princípio do word2vec (Mikolov et al., 2013) e do GloVe (Pennington et al., 2014).

A medida de similaridade, por sua vez, continua a mesma:

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

T = TfidfVectorizer().fit_transform(corpus)
print(cosine_similarity(T[0:1], T).round(2))
# [[1.   0.61 0.38 0.18 0.  ]]

A frase 0 (« le chat dort sur le canapé ») é próxima da frase 1 (« le chien dort sur le tapis ») com 0,61 e totalmente estranha à frase 4 (« la souris mange du fromage »), com 0,00. O ponderador TF-IDF corrige de passagem um defeito da contagem bruta: ele reduz o peso das palavras presentes em todo lugar, como le, que não trazem nenhuma informação discriminante.

Resta uma limitação, e ela é considerável: um embedding clássico atribui um único vetor por palavra. A palavra francesa « avocat » (que significa tanto « advogado » quanto « abacate ») tem, portanto, a mesma representação em « un avocat au barreau » (um advogado inscrito na ordem) e em « un avocat bien mûr » (um abacate bem maduro). Resolver isso exige fazer a representação depender do contexto, que é exatamente a contribuição do mecanismo seguinte.

A atenção: não olhar todas as palavras do mesmo jeito

A atenção responde a uma pergunta simples: para representar uma determinada palavra, quais outras palavras da frase é preciso levar em conta, e em que proporção?

O cálculo usa três objetos por palavra: uma consulta (query, o que essa palavra procura), uma chave (key, o que cada palavra oferece) e um valor (value, a informação a recuperar). Compara-se a consulta a todas as chaves, transformam-se os scores em proporções que somam 1 e calcula-se com elas uma média ponderada dos valores.

import numpy as np
np.set_printoptions(suppress=True)

mots = ["chat", "souris", "dort"]

# Chaves e valores ilustrativos, escolhidos para deixar o cálculo legível.
K = np.array([[4.0, 0.0, 0.0, 0.0],     # chat
              [3.0, 2.0, 0.0, 0.0],     # souris
              [0.0, 0.0, 4.0, 0.0]])    # dort
V = np.array([[10.0], [6.0], [1.0]])

Q = np.array([[4.0, 0.0, 0.0, 0.0]])    # a consulta busca um sujeito animado

d = K.shape[1]
scores = Q @ K.T / np.sqrt(d)           # divisão: evita scores muito dispersos
poids = np.exp(scores - scores.max())
poids = poids / poids.sum()             # softmax

print(scores.round(2))       # [[8. 6. 0.]]
print(poids.round(3))        # [[0.881 0.119 0.   ]]
print((poids @ V).round(2))  # [[9.52]]

A leitura é direta: 88,1 % da atenção vai para chat, 11,9 % para souris e praticamente nada para dort. A representação produzida, 9,52, é quase a de chat (10,0), levemente puxada em direção à de souris (6,0).

Três propriedades explicam o sucesso desse mecanismo:

  • Os pesos são calculados, não fixados. A mesma palavra obterá uma distribuição diferente em outra frase. A representação se torna contextual, o que resolve o problema do « avocat ».
  • A distância não custa nada. Uma palavra na posição 1 pode receber toda a atenção de uma palavra na posição 400 tão facilmente quanto a de seu vizinho imediato. As arquiteturas recorrentes que vieram antes, processando o texto palavra por palavra, perdiam progressivamente a informação distante.
  • Tudo é paralelizável. Os scores de todas as palavras são calculados em um produto matricial, portanto em GPU. É essa propriedade, tanto quanto a qualidade dos resultados, que permitiu a mudança de escala.

Os transformers e os grandes modelos de linguagem

A arquitetura que empilha camadas de atenção se chama transformer, introduzida em 2017 no artigo « Attention Is All You Need », de Vaswani et al. Como a atenção sozinha ignora as posições, acrescenta-se uma codificação de posição, além de camadas densas clássicas e normalizações.

Um grande modelo de linguagem (LLM) é um transformer treinado em uma tarefa de simplicidade desconcertante: dados os tokens anteriores, prever o seguinte. Nenhuma anotação humana é necessária, o texto é seu próprio rótulo, o que permite treinar em corpora gigantescos.

Algumas ordens de grandeza documentadas, úteis para situar as escalas:

ModeloAnoNúmero de parâmetros
BERT-base2018cerca de 110 milhões
BERT-large2018cerca de 340 milhões
GPT-32020175 bilhões

Esses números vêm dos respectivos artigos de publicação. Os modelos comerciais recentes não publicam mais sistematicamente essa informação: na ausência de fonte, é melhor não afirmar nada.

O fato notável, e que não era previsto, é que esse objetivo estreito — prever a próxima palavra — produz capacidades que não foram programadas: traduzir, resumir, reformular, seguir uma instrução. É preciso, porém, ser preciso sobre o que está estabelecido. O que está estabelecido é o desempenho medido em tarefas de avaliação. O que continua em debate é a interpretação: saber se essas capacidades decorrem de uma forma de generalização ou de uma interpolação muito fina a partir de um corpus imenso é uma questão em aberto, e qualquer afirmação categórica em um sentido ou no outro vai além do que os dados permitem estabelecer.

Por que um modelo de linguagem alucina

O mecanismo pode ser demonstrado em um modelo minúsculo. Um modelo de bigramas retém apenas uma coisa: para cada palavra, a lista das palavras que a seguiram no corpus. Depois, ele gera texto sorteando ao acaso nessa lista.

import random
from collections import defaultdict

texte = ("le chat dort sur le canape le chien dort sur le tapis "
         "le chat mange une souris le chien mange une croquette")
mots = texte.split()

suivants = defaultdict(list)
for a, b in zip(mots, mots[1:]):
    suivants[a].append(b)

random.seed(3)
mot = "le"
phrase = [mot]
for _ in range(11):
    if mot not in suivants:
        break
    mot = random.choice(suivants[mot])
    phrase.append(mot)

print(" ".join(phrase))
# le canape le chien mange une souris le chien dort sur le

O resultado é quase francês. As sequências locais são irrepreensíveis: « le chien mange une souris » (o cachorro come um camundongo) é uma frase perfeitamente bem formada. Ela também é falsa em relação ao corpus, no qual o cachorro come uma croquete e é o gato que come o camundongo. O modelo não mentiu e não se enganou no sentido de um bug: ele produziu uma continuação estatisticamente plausível, que é exatamente, e unicamente, o que lhe foi pedido.

Eis a alucinação, na escala de vinte palavras de vocabulário. Um modelo de vários bilhões de parâmetros faz a mesma coisa com uma fluidez incomparavelmente superior, o que torna o erro muito mais difícil de identificar. A diferença de tamanho muda a qualidade da superfície, não a natureza do objetivo.

Quatro consequências práticas decorrem disso, todas verificáveis:

  • A fluidez não é um indício de confiabilidade. Uma referência inventada é formulada com a mesma segurança que uma referência exata, porque ambas são produzidas pelo mesmo processo.
  • Não há fonte. O modelo não copia uma base de conhecimento consultável; ele produz uma sequência de tokens. Perguntar « de onde vem essa informação? » obtém uma resposta gerada, não um rastro.
  • A janela de contexto é finita. Além de certo comprimento, os tokens antigos saem do contexto considerado.
  • As correções atenuam, mas não eliminam. Fornecer documentos no contexto e exigir citações verificáveis — o que se chama de geração aumentada por recuperação (RAG) — reduz fortemente o fenômeno ao ancorar a resposta em textos reais. Até hoje, nenhuma técnica publicada o elimina completamente.

A boa postura de engenharia decorre daí: tratar a saída de um modelo de linguagem como uma proposta a verificar, e não como um resultado. Em todo lugar em que a exatidão é contratual, a verificação deve ser externa ao modelo.

Pontos-chave

  • A tokenização é a primeira escolha de projeto e tem efeitos duradouros. A divisão em subpalavras resolve o problema das palavras desconhecidas, mas faz com que um modelo nunca veja as letras — daí sua fragilidade na contagem de caracteres.
  • A hipótese distribucional se verifica por meio do cálculo: com cinco frases, chat e chien atingem 0,867 de similaridade contra 0,258 entre chat e fromage, sem que nenhum conhecimento zoológico tenha sido fornecido.
  • A atenção calcula, para cada palavra, a proporção de atenção a conceder às demais. Ela torna a representação dependente do contexto, elimina o custo da distância e é calculada em paralelo.
  • Um grande modelo de linguagem é um transformer treinado para prever o próximo token. Esse objetivo único produz capacidades não programadas; sua interpretação teórica, porém, continua em debate.
  • A alucinação não é uma falha, mas a consequência do objetivo: o modelo otimiza a plausibilidade, não a verdade. Um modelo de bigramas de vinte palavras já a reproduz. Toda saída destinada a um uso sério deve ser verificada fora do modelo.

Comentários

Os comentários são alimentados pelo GitHub Discussions

Conecte-se com o GitHub para participar da discussão

Link copiado!