- Um experimento que constrói um transformer somente decodificador parecido com o GPT-2, projetando manualmente apenas os pesos, sem treinamento, para prever o padrão
(aab)* - A previsão de
"aabaabaabaab..."precisa olhar para os dois tokens anteriores, então é melhor para revelar o funcionamento da atenção do que um padrão alternado simples - O modelo é pequeno, com
N_CTX=5,N_VOCAB=2eN_EMBED=8, usando tokenizaçãoa=0,b=1e embeddings one-hot - Um único head de atenção distribui 0,5 de atenção para cada um dos dois tokens mais recentes, e calcula o próximo token usando o cancelamento da codificação
a=1,b=-1 - Em contextos não ambíguos, mostrou 100,0% (27/27) de acerto, mas precisa de cerca de 4.000 FLOPs para uma única previsão com contexto de 5 tokens, sendo muito menos eficiente do que uma regra implementada diretamente
Um mini GPT-2 com pesos definidos à mão, sem treinamento
- O objetivo é entender de forma intuitiva o que cada componente do transformer e da atenção realmente faz
- O modelo não é treinado nem usa pesos pré-treinados; ele é construído em uma noite, definindo manualmente cada peso
- A estrutura é um transformer somente decodificador parecido com o GPT-2, e a implementação foi simplificada a partir da implementação picoGPT de jaymody
- remoção do layer norm
- uso de um único head em vez de multi-head attention
- remoção da camada feed-forward
mlpdo bloco transformer
Por que escolher a sequência (aab)*
- No começo, a ideia era prever uma sequência como
"ababababab", mas isso ficou fácil demais porque o transformer prevê uma sequência deslocada- Bastaria prever
bse fossea, eacaso contrário, sem precisar usar embedding posicional
- Bastaria prever
- A tarefa final foi definida como prever
"aabaabaabaab...", ou seja, a sequência(aab)*- se os dois tokens anteriores forem
abouba, o próximo token éa - se os dois tokens anteriores forem
aa, o próximo token éb bbé tratado como um caso fora do escopo da tarefa
- se os dois tokens anteriores forem
- A tokenização é simples e lida com apenas dois símbolos
aé0bé1
Dimensões do modelo e fluxo de cálculo
- Foram escolhidos três parâmetros para o modelo
N_CTX = 5: tamanho máximo de contexto que o modelo vê de uma vezN_VOCAB = 2: os dois tokensaebN_EMBED = 8: tamanho do embedding que guarda espaço para token, posição e cálculo
- Embora a tarefa real só precise dos dois tokens anteriores, usar
N_CTX=5inclui a situação em que é preciso ignorar tokens irrelevantes - A função
gptfunciona na seguinte ordem- soma embeddings de token e posição com
wte[inputs] + wpe[range(len(inputs))] - passa por um único bloco transformer
- ao final, cria os logits no espaço do vocabulário com
x @ wte.T
- soma embeddings de token e posição com
Embeddings com posição e token em one-hot
wpeé o embedding posicional, usando as primeiras 5 dimensões do embedding como one-hot de posição- posição 0 é
[1, 0, 0, 0, 0] - posição 4 é
[0, 0, 0, 0, 1]
- posição 0 é
wteé o embedding de token, usando as 2 dimensões seguintes como one-hot de token- o token
aé[1, 0]nessas dimensões - o token
bé[0, 1]
- o token
- A 8ª posição do embedding não é usada no início e vira um scratch space dentro do bloco transformer
- Por exemplo,
"aabaa"é representado como uma matriz de embeddings5 x 8, em que cada linha contém tanto o one-hot de posição quanto o one-hot de token
Como o head de atenção escolhe os dois tokens mais recentes
- O bloco transformer é composto por um único head de atenção e por uma camada linear
c_projque projeta o resultado da atenção de volta ao espaço de embedding c_attné uma camada linear de tamanhoembed_size x (embed_size * 3), que transforma o embedding de entrada em uma matrizqkv, depois dividida emq,kevksepara o embedding posicional para representar a informação de posição de cada tokenqrepresenta a faixa de posições que cada posição quer buscar, eq @ k.Tproduz a matriz de scores de atenção- Depois do softmax e da causal mask, a matriz de atenção tem as seguintes propriedades
- a primeira linha dá 100% de atenção apenas ao primeiro token
- as linhas seguintes dão
0,5de atenção para cada um dos dois tokens mais recentes acessíveis
- A causal mask impede olhar para tokens futuros somando um valor muito pequeno nas posições futuras; no código real, algo como
-1e10- este modelo artesanal não foi projetado para espionar o futuro, mas a máscara foi mantida para ficar mais próximo da estrutura do GPT-2
- O escalonamento por
np.sqrt(q.shape[-1])ajuda a melhorar gradientes no treinamento real, mas não afeta este modelo feito à mão
Codificação em v e previsão por soma com cancelamento
vtransforma o one-hot do token em uma codificaçãoa=1,b=-1- Como o resultado da atenção faz a média dos dois tokens mais recentes com peso 0,5 para cada um, essa codificação calcula a seguinte regra
a, b→0.5 * 1 + 0.5 * (-1) = 0b, a→0.5 * (-1) + 0.5 * 1 = 0a, a→0.5 * 1 + 0.5 * 1 = 1
- Como resultado, a 7ª posição da linha passa a conter o seguinte valor
0quando se deve prevera1quando se deve preverb
- Na entrada
"aabaa", a primeira linha pode gerar uma previsãobpor falta de informação, mas as previsões seguintes batem com a regra de(aab)*
Enviando a previsão de volta ao espaço do vocabulário
c_projconverte o valor da 7ª posição do resultado da atenção de volta para o formato one-hot do token- Em vez de simplesmente criar
[..., 1, 0, ...]ou[..., 0, 1, ...], ele cria um one-hot escalado por1024embedding[row, 5] = 1024 + (-1024) * predictionembedding[row, 6] = 0 + 1024 * prediction
- O bloco transformer tem uma residual connection, então
x = x + causal_self_attention(...)soma o embedding original de volta - Como o sinal residual sobraria sem necessidade, usa-se a escala
1024para sobrepujá-lo - Por fim, calcula-se
x @ wte.Tpara gerar os logits e aplica-se softmax- no contexto
"aabaa", a linha final da previsão aponta parab - no treinamento, as previsões de todas as linhas são úteis, mas na inferência só a última linha é necessária
- no contexto
Resultado da geração e acurácia
- A função
completepassa os últimos até 5 tokens ao modelo e escolhe o próximo token comargmaxna última linha do resultado do softmax - Exemplos de geração
complete("a")→a :: baabaabaabcomplete("ba")→ba :: abaabaabaacomplete("abaab")→abaab :: aabaabaaba
- Mesmo com entradas fora do escopo, às vezes o modelo recupera o padrão repetitivo
complete("ababa")→ababa :: abaabaabaacomplete("bbbbb")→bbbbb :: aabaabaaba
- No teste com
"aab" * 10, avaliando apenas contextos não ambíguos, a acurácia foi de 100,0% (27/27)
A diferença entre 4.000 FLOPs e 8 instruções
- Usando o contexto completo de 5 tokens, este modelo precisa de cerca de 4.000 operações de ponto flutuante para prever um único token
- a maior parte é gasta no cálculo da atenção
- isso pode ser reduzido com janela de contexto menor, fused multiply-add, cache de
kve outras técnicas - ainda assim, prever um único token exigiria centenas de instruções de máquina
- A mesma regra
(aab)*, escrita diretamente em assembly x64, calcula o próximo token em 8 instruções - Fica a pergunta se é possível treinar um modelo de linguagem 1000 vezes mais eficiente do que os modelos atuais para geração de linguagem natural
1 comentários
Comentários do Hacker News
Há um trabalho relacionado chamado "Thinking Like Transformers"
Ele apresenta uma linguagem de programação primitiva chamada RASP, composta por operações que podem ser modeladas com componentes de Transformer, e mostra que é possível escrever programas como histogramas ou ordenação
Também há excelentes posts de blog de Sasha Rush e Gail Weiss, e pesquisas posteriores mostraram que programas do tipo RASP podem ser compilados diretamente para pesos de modelo reais, sem treinamento
[1] https://arxiv.org/abs/2106.06981
[2] https://srush.github.io/raspy/
[3] https://arxiv.org/abs/2301.05062
Se essa área parecer interessante, talvez valha dar uma olhada também no meu trabalho HandCrafted Transformers, em que escolhi manualmente os pesos de um modelo Transformer para fazê-lo realizar adição longa, de uma forma parecida com a que as pessoas aprendem no ensino fundamental
[1] https://colab.research.google.com/github/newhouseb/handcraft...
A direção oposta, de redes neurais para código, também parece muito interessante do ponto de vista da explicabilidade
Eu achava que entendia bem Transformers, mas nunca tinha implementado um por conta própria
Um dia implementei, e ele não funcionava nem treinava tão bem quanto o Transformer padrão do PyTorch; no fim, percebi que o motivo era eu ter ignorado o dropout
Eu estava treinando adição de números e nunca mostrava o mesmo par duas vezes, então achava que overfitting seria impossível, mas o papel do dropout foi muito maior do que eu esperava
Em resumo, é bom simplesmente implementar um Transformer por conta própria, e quanto mais do zero, melhor
Todos que fizeram isso aprenderam algo inesperado, e cada pessoa percebeu coisas diferentes, desde paralelizar o treinamento em nível de tokens até como a retropropagação realmente funciona
Também gosto dos materiais do Karpathy, mas o que finalmente fez Transformer fazer sentido para mim foi este vídeo: https://youtu.be/kWLed8o5M2Y?si=SJT5_lCJ0hSR7Z_k
Venho pensando em algo parecido há algum tempo
Será que não dá para criar uma interface intuitiva para os pesos de um modelo, que um especialista de domínio possa ajustar manualmente para acelerar o treinamento?
Por exemplo, em um modelo de visão, ao detectar cones de trânsito, aumentar um conjunto de pesos correspondente a "alaranjadez"
Assim, em vez de exigir milhares ou milhões de exemplos adicionais para calibrar corretamente a "alaranjadez", uma pessoa poderia acelerar o processo
Claro que a dificuldade é que essa interface teria de ser mapeada para conjuntos de pesos com significados diferentes, e fico curioso se há algum motivo técnico para isso ser impossível
[1] https://www.youtube.com/watch?v=8SF_h3xF3cE&t=1358s
Em poucas palavras, formas de humanos ajudarem a IA quase sempre têm pior custo-benefício do que usar mais poder computacional
Enquanto uma pessoa calibra uma camada de pesos para detectar cones de trânsito laranja, um cluster de GPUs treina a IA para detectar cones de trânsito, semáforos, árvores, outros carros e até cones de trânsito em tons de laranja ligeiramente diferentes
Mesmo que você ajuste a imagem para enxergar melhor o laranja, se não puder monitorar ao mesmo tempo a precisão em todas as outras cores, é provável que crie problemas em outras cores sem perceber
Não é como se um cluster específico de neurônios correspondesse a um conceito específico; em geral, tudo faz um pouco de tudo
Há parâmetros demais envolvidos
O artigo dos Transformers é técnico demais, então eu sempre quis entendê-lo, mesmo que superficialmente, mas era difícil
Este texto ajudou muito a entender como eles funcionam, e pelo menos o exemplo ficou muito claro
Graças a ele, também consegui relembrar as matrizes que aprendi na faculdade
Não seria uma espécie de máquina abstrata, como uma máquina de Turing ou uma máquina que faz parsing de expressões regulares?
Primeiro você define uma lista de tokens; por exemplo, para facilitar, digamos que sejam 24 caracteres
Essa máquina recebe uma sequência de tokens de entrada, executa operações matriciais determinísticas e então gera uma lista de probabilidades para todos os tokens
"Aprendizado" é apenas o processo de definir alguns dos números dentro das matrizes usadas nessas operações
Vale notar que no código final há apenas um
if, e ele serve para avaliar a precisão do resultadoToda a "lógica" vem dos resultados das operações matriciais
Em redes neurais, tudo em geral é meio nebuloso, e quase não há coisas como
if/else, embora existam casos em que valores são "mascarados" como 0 ou -∞, como no exemplo de TransformerA saída também é quase sempre um conjunto de pontuações ou probabilidades; então, se um modelo que distingue fotos de cães e gatos produz algo como
dog:0.95 cat:0.05, dizemos que ele previu cachorro porque a pontuação de cachorro é maiorO mecanismo de atenção, que é o núcleo do Transformer, se baseia em uma espécie de operação de consulta suave
Num sistema não nebuloso, você percorreria cada token da sequência, verificaria se ele é relevante para o token atual e, se fosse, faria alguma ação; mas, no Transformer, a relevância não é uma decisão binária
Em vez disso, ele calcula uma pontuação contínua de relevância entre todos os pares de tokens da sequência e usa essa pontuação para decidir o próximo passo
Só que algumas coisas não são fáceis de generalizar diretamente a partir de sistemas baseados em decisões binárias
Por exemplo, essas pontuações de relevância são usadas como pesos para calcular uma média ponderada dos tokens do vocabulário, obtendo um "token médio" para a posição atual
Não parece haver uma maneira fácil de interpretar isso como uma extensão de um processo baseado em lógica de ramificação
Vale ver este artigo, que explica que Linear Transformers são, na verdade, Fast Weight Programmers: https://arxiv.org/abs/2102.11174
Se você definir os pesos cuidadosamente, consegue fazê-las executar qualquer computação
Só seria bom ter um compilador que não dependesse de aproximações
Fico curioso sobre para que serviria a frase "talvez você fique com vontade de criar seu próprio modelo", além de ser um exercício de aprendizado para satisfazer a curiosidade
Estou começando a sentir que modelos de machine learning complexos são impraticáveis para alguém lendo blogs em casa
Ele cria muitas palavras em inglês antigo que parecem plausíveis e aprende o básico da gramática inglesa e o formato de peças teatrais, entre outras coisas
Achei bem surpreendente chegar a esse ponto em tão pouco tempo
Treinar vários modelos localmente até um nível de fidelidade tipo Shakespeare-from-Wish.com poderia ajudar a julgar se você encontrou uma boa arquitetura e se chegou a hora de tentar escalá-la
Ele diz que queria entender melhor Transformers e atenção, e que tinha lido The Illustrated Transformer, mas não conseguia formar uma intuição sobre o que as várias partes da atenção realmente faziam
Algo como a diferença entre
qek, sem falar emvVai além de simplesmente satisfazer a curiosidade: ajuda a construir e aprofundar o entendimento
Seria bom se o título pudesse incluir uma expressão como neural network
Isto é sobre a arquitetura "Transformer" de machine learning, não sobre um conjunto de bobinas que acopla eletromagneticamente dois circuitos