Aqui aparece o gradiente, e aparece sem autodiff: cada derivada é escrita explicitamente, para que você veja de onde ela vem antes de deixar uma biblioteca calculá-la por você.
$ java -jar target/mini-gpt-java.jar train --model mlpO bigrama esquece tudo, exceto um caractere. A correção óbvia seria contar trigramas, tetragramas, e assim por diante, mas a tabela cresce como V^k. Com V = 96 e uma janela de oito, seriam 96⁸ linhas, cerca de sete mil trilhões. E, pior do que o tamanho: a maioria delas ficaria em zero, porque nenhum corpus contém todas as combinações. A tabela gigante não teria nada a dizer justamente sobre as janelas que você mais precisa prever.
"o menin" e "a menin" são duas linhas sem relação nenhuma.MlpModel.java.x = concat( C[id₁], C[id₂], …, C[id_T] ) (T·E) a = tanh( x · W₁ + b₁ ) (H) logits = a · W₂ + b₂ (V) L = entropia cruzada( softmax(logits), alvo )
V × E. A linha C[id] é o vetor que representa aquele caractere, e o treino a aprende em vez de recebê-la pronta--context)--embed)--hidden)É a arquitetura de Bengio (2003), a mesma que popularizou a ideia de embeddings aprendidos. Repare que a concatenação é o ponto fraco: cada posição da janela recebe o seu próprio bloco de pesos em W₁, então o que a rede aprende sobre a posição 3 não vale nada para a posição 4. Guarde isso: é exatamente o que a atenção vai consertar no nível 3.
Derivar a entropia cruzada e o softmax separadamente é trabalhoso e numericamente instável. Derivar os dois juntos faz tudo cancelar e sobra uma linha, a única fórmula deste nível que vale a pena decorar:
∂L / ∂logits = ( P − onehot(y) ) / B
softmax(logits), com uma linha por exemplo do loteLeia em português: o gradiente é o erro. "Quanta probabilidade eu dei" menos "quanta eu deveria ter dado". Se o modelo deu 0,7 ao caractere certo, aquela coordenada recebe 0,7 − 1 = −0,3 e o passo de treino aumenta aquele logit. Todos os outros recebem o próprio P, positivo, e são empurrados para baixo.
A partir de dLogits, cada passo seguinte é uma aplicação mecânica da regra da cadeia, andando de trás para frente pelo caminho de ida. Este bloco é o comentário de classe de MlpModel, copiado, e cada linha dele existe, comentada, no corpo de forwardBackward().
dLogits = (P − onehot(y)) / B (B×V)dW2 = Aᵀ · dLogits db2 = Σ_b dLogits (H×V), (1×V)dA = dLogits · W2ᵀ (B×H)dH1 = dA ⊙ (1 − A²) (derivada da tanh) (B×H)dW1 = Xcatᵀ · dH1 db1 = Σ_b dH1 (T·E×H), (1×H)dXcat = dH1 · W1ᵀ (B×T·E)dC[id] += fatia de dXcat daquela posição (scatter-add)
W₁ é (T·E × H), então dW1 também é. Isso sozinho já determina de que lado cada transposta entra na multiplicação, e elimina a maior parte dos erros de derivação antes mesmo de você derivar.a = tanh(z) ⇒ da/dz = 1 − a²
z, só do resultadoa = 0 e cai a zero quando a → ±1Aí está a saturação: um neurônio empurrado para ±1 tem derivada quase nula, e o gradiente que passa por ele desaparece. Ele para de aprender sem dar nenhum sinal. É por isso que W₁ é inicializado com desvio 1/√fan_in: para as ativações começarem na região central, onde a derivada ainda é grande.
O último passo do backward é o mais fácil de errar. Cada exemplo usou T linhas da tabela C, e o mesmo caractere pode aparecer várias vezes na mesma janela. Pense em "casa da ", com três a. O gradiente de cada uso tem que ser somado na linha correspondente, nunca atribuído.
for (int i = 0; i < b; i++) {for (int t = 0; t < blockSize; t++) {int id = windows[i][t];int base = t * embedDim;for (int e = 0; e < embedDim; e++) {c.grad[id][e] += dXcat[i][base + e]; // +=, nunca =}}}
MlpGradCheckTest existir.Cada uma das derivações acima é conferida contra a definição de derivada, por diferença central, com erro abaixo de 1e-5. O teste perturba um parâmetro de cada vez, mede a perda dos dois lados e compara com o que o backward afirmou. Nenhum gradiente deste nível está no repositório sem ter passado por isso.
mvn test -Dtest=MlpGradCheckTest
q é seguido de u, que nh existe e hn não.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.
Rode com --context 4, 8 e 16, mantendo o resto igual. A perda melhora sempre? A partir de que ponto o ganho não paga o custo, e por que a concatenação torna esse custo linear em T?
Use --embed 2. Cada caractere vira um ponto no plano. Imprima a tabela C ao fim do treino: vogais caem perto de vogais? Onde ficam o espaço e a quebra de linha?
Substitua Math.tanh(...) pela identidade e ajuste o backward (a derivada vira 1). A rede continua treinando, mas prove que ela deixou de ser mais expressiva que uma única camada linear.
Troque c.grad[id][e] += por = e rode mvn test. Qual teste falha, e o que exatamente ele reporta? Depois desfaça e repare que a perda de treino também caía com o bug.
model/MlpModel.java. O Javadoc da classe traz a mesma matemática, ao lado da linha que a implementa.