← início
Nível 1 · model/BigramModel.java

Bigrama

O modelo mais simples que ainda merece o nome. Ele estabelece a linha de base: qualquer modelo mais sofisticado precisa bater a perda do bigrama para justificar a própria complexidade.

Contexto: 1 caractereTreino: instantâneoPerda esperada: ~2–3 nats
$ java -jar target/mini-gpt-java.jar train --model bigram

O nível por dentro

A hipótese

A regra do produto diz que a probabilidade de um texto é o produto de p(x_t | x₁ … x_{t−1}) sobre todas as posições. O problema é que esse condicional depende de um passado que cresce sem parar: não há como tabelá-lo. O bigrama resolve isso com a hipótese mais agressiva possível.

p(x_t | x₁ … x_{t−1})   ≈   p(x_t | x_{t−1})
x_{t−1}
o caractere imediatamente anterior, o único que o modelo enxerga
uma aproximação que sabemos ser falsa, e não uma igualdade

Com essa hipótese o condicional vira uma tabela V × V: uma linha por caractere de contexto, uma coluna por caractere possível. Com V = 96, são 9.216 números, que cabem na memória e podem ser contados numa passada.

A ideia central desta páginaUm modelo de linguagem é uma tabela de probabilidades condicionais. Tudo o que os níveis 2 e 3 fazem é comprimir essa tabela, que seria grande demais para o contexto real, dentro de uma função com parâmetros. Mas o objeto que se quer estimar continua sendo este.

Estimar contando

Como estimar p(b | a) a partir de um corpus? Contando. Se a apareceu 1.000 vezes e em 300 delas foi seguido de b, a estimativa é 0,3. Isso tem nome: estimativa de máxima verossimilhança. É, demonstravelmente, a tabela que torna o corpus observado o mais provável possível.

N[a][b]  =  quantas vezes 'b' apareceu logo depois de 'a'
p(b | a)  =  ( N[a][b] + α )  /  ( Σ_c N[a][c] + α·V )
N[a][b]
a contagem crua do par, preenchida numa varredura só
Σ_c N[a][c]
quantas vezes a apareceu no total: o denominador sem suavização
α
a constante de suavização (o projeto usa α = 1)
V
o tamanho do vocabulário; α·V é a massa total que a suavização acrescenta à linha

O treino inteiro do nível 1 são estas cinco linhas, copiadas sem corte do corpo de BigramModel.fit().

for (int i = 0; i + 1 < ids.length; i++) {
int a = ids[i];
int b = ids[i + 1];
counts[a][b] += 1.0;
rowSums[a] += 1.0;
}
model/BigramModel.java — o método fit() completo

Complexidade O(N) no tamanho do corpus, uma passada, sem gradiente. Um megabyte de texto é contado antes de você tirar a mão do teclado.

Por que α não pode ser zero

Suponha que o par "zq" nunca apareceu no corpus de treino. Sem suavização, p(q | z) = 0. Agora esse par aparece uma única vez na validação, e a perda daquela posição é −ln 0 = +∞. A média de qualquer coisa com um infinito dentro é infinito: uma ocorrência isolada destrói a métrica inteira.

Probabilidade zero é uma afirmação forte demaisDizer p = 0 é afirmar que aquilo é impossível, não apenas raro. Nenhum corpus finito autoriza essa conclusão. A suavização de Laplace (add-α) soma α a toda contagem, o que dá massa mínima a todo par e mantém a perda finita, ao custo de tirar um pouco de probabilidade dos pares que de fato ocorreram.

É por isso que o construtor recusa α ≤ 0 com uma exceção, em vez de aceitar e falhar de forma obscura mais tarde. Na demonstração da página inicial você pode arrastar o α e ver a perda subir dos dois lados: pouco demais deixa a cauda perigosa, muito demais afoga as contagens reais em ruído uniforme.

O que a perda significa

L  =  −(1/N) Σ ln p(x_t | x_{t−1})
L_uniforme  =  ln V  ≈  4,56   (para V = 96)
L
a entropia cruzada média, em nats por caractere
ln V
a perda de um modelo que chuta uniformemente: o teto que qualquer modelo tem que furar

Um bigrama num corpus real de português chega a algo entre 2 e 3 nats. Isso significa que, sabendo apenas o caractere anterior, a incerteza sobre o próximo caiu de 96 opções equivalentes para o equivalente a cerca de e^2,4 ≈ 11.

Se você preferir pensar em perplexidade, é só exponenciar: PP = e^L. Ela responde à pergunta "entre quantas opções igualmente prováveis o modelo está efetivamente escolhendo?", com a mesma informação numa escala mais intuitiva.

O que esperar do texto

  • A textura do português aparece: a proporção certa de vogais, acentos em posições plausíveis, palavras com tamanho verossímil.
  • Palavras reais quase não aparecem, e as que aparecem são curtas e frequentes: de, a, que.
  • Não há concordância, não há sintaxe, não há memória: depois de escrever menin, o modelo já esqueceu que estava escrevendo uma palavra.
É esse esquecimento que motiva o nível 2O bigrama não falha por falta de dados nem por má estimativa: a estimativa dele é ótima dado o que ele olha. Ele falha porque olha um caractere só. A pergunta natural passa a ser: e se olhasse oito?

Para fazer com as mãos

Nenhum destes exercícios pede uma biblioteca nova, e todos cabem numa sessão. O terceiro e o quarto de cada nível são os que mais ensinam, porque quebram alguma coisa de propósito.

E1Meça a linha de base

Rode train --model bigram e anote a perda de validação. Compare com ln V (o V aparece no cabeçalho da saída). Quanto do caminho até a certeza um único caractere de contexto já percorreu?

E2Quebre a suavização

Em App.java, troque o 1.0 passado ao construtor do BigramModel por 1e-9 e depois por 50. Explique, sem rodar, o que cada extremo faz com a perda, e depois confira.

E3Conte o que nunca aconteceu

Escreva um laço que conte quantas das células de counts continuam em zero depois do fit(). Essa fração é exatamente a superfície que a suavização está cobrindo.

E4Encontre o τ que engana

Gere com --temp 0.4, 1.0 e 1.6. Existe uma temperatura em que o bigrama parece mais inteligente do que é? Por que ela não melhora a perda?

O arquivo deste nívelTudo o que esta página explica está em model/BigramModel.java. O Javadoc da classe traz a mesma matemática, ao lado da linha que a implementa.