GPT implementado em 500 linhas de SQL
(explainextended.com)- Reproduz o fluxo de inferência do GPT-2 em SQL usando apenas PostgreSQL e pgvector, da tokenização aos blocos Transformer e à geração do próximo token
- Um LLM generativo é mais próximo de uma função determinística que produz as mesmas probabilidades de tokens candidatos para a mesma entrada; o ponto em que a resposta muda é a etapa probabilística de seleção do token candidato
- A implementação expressa em consultas SQL e tabelas o tokenizador BPE do GPT-2, 50.257 tokens, embeddings de 768 dimensões, contexto de 1.024 tokens, 12 blocos, 12 cabeças de attention e feedforward baseado em GELU
- Foram necessárias adaptações práticas ao ambiente de banco de dados, como a falta de suporte a propriedades Unicode em regex do PostgreSQL e a limitação do
EXPcom valores muito pequenos - No exemplo, a partir de
"Happy New Year! I wish you", são gerados 10 tokens e a saída é"Happy New Year! I wish you all the best in your new year!"; no ambiente do autor, levou 2 minutos e 44 segundos
Criando um pipeline de inferência do GPT-2 em SQL
- O ChatGPT respondeu que SQL não seria adequado para implementar um modelo de linguagem de grande porte, mas foi implementado um pipeline de inferência do GPT-2 em PostgreSQL SQL
- A explicação de implementação usada como referência é GPT in 60 Lines of NumPy, de Jay Mody, e os mesmos componentes foram transpostos para tabelas e consultas de banco de dados
- Um LLM generativo pode ser visto como uma função do tipo
llm(prompt: str) -> list[tuple[str, float]]- A entrada é um prompt de texto
- A saída é um array de candidatos à próxima string e suas probabilidades
- Se a matemática interna e os parâmetros forem os mesmos, a mesma entrada retorna o mesmo resultado
- O motivo de produtos como o ChatGPT poderem dar respostas diferentes à mesma pergunta está menos no modelo em si e mais na seleção probabilística da próxima palavra/token
Loop de geração de texto
- O processo de geração tem a estrutura de transformar o prompt em um array de tokens, chamar o modelo repetidamente para escolher o próximo token e anexá-lo ao final do prompt
- O fluxo básico é composto pelas seguintes etapas
- Converter a string em um array de IDs de token com
tokenize(prompt) gpt2(tokens)calcula as probabilidades para 50.257 tokensselect_next_token(candidates)escolhe o próximo token- Adicionar o token escolhido ao array
- Parar em condições como número fixo de tokens, timeout ou stopword
- Restaurar o array de tokens para string com
detokenize(tokens)
- Converter a string em um array de IDs de token com
- A sequência acumulada de tokens assim pode se tornar um texto em linguagem natural com propriedades que parecem gramática, sintaxe, significado e raciocínio
Implementando o tokenizador BPE em SQL
- Antes de entrar na rede neural, o texto precisa virar uma lista de números, mas usar diretamente code points Unicode torna o espaço de tokens e o comprimento ineficientes
- O GPT-2 usa uma variação de Byte pair encoding
- O vocabulário de tokens usa 50.257 code points
- Inclui sequências de bytes UTF-8 e o token “end of text”
- Começa com 256 tokens de byte e depois adiciona novos tokens a partir de pares adjacentes frequentes
- Essa fusão é repetida 50.000 vezes para chegar a 50.256 tokens, e por fim é adicionado o token end-of-text
- O tokenizador do GPT-2 tem uma camada extra que mapeia bytes para caracteres de string, e esse mapeamento está definido em
encoder.pydo OpenAI GPT-2 - Na implementação em SQL, o vocabulário de tokens baixado da OpenAI foi colocado na tabela
tokenizer, e o mapeamento byte-caractere foi armazenado na tabelaencoder - O exemplo
"Mississippilessly"começa com bytes individuais em um recursive CTE e faz repetidamente a melhor fusão possível entre pares adjacentes- No exemplo, o número de tokens cai de 17 para 5
- Em vez de usar o espaço de cerca de 150 mil code points do Unicode, usa o espaço de cerca de 50 mil tokens do GPT-2
- Ao processar várias palavras, o GPT-2 divide o texto com regex e faz as fusões dentro de cada palavra
- O PostgreSQL não oferece suporte a propriedades de caracteres Unicode em regex, então a regex original do GPT-2 foi parcialmente modificada
- Essa modificação pode ter prejudicado o suporte adequado a Unicode
"PostgreSQL is great"é convertido pelo tokenizador SQL em[6307, 47701, 318, 1049]- Os clusters de tokens são
Post,greSQL,Ġis,Ġgreat Ġrepresenta um espaço
- Os clusters de tokens são
Embeddings e janela de contexto
- IDs de token não são usados diretamente nos cálculos do modelo; eles são convertidos em vetores de embedding
- O GPT-2 faz embedding separado de token e de posição
WTEé o word token embedding e é uma matriz 50257×768WPEé o word position embedding e é uma matriz 1024×768
- Para cada posição do token, soma-se o vetor de
WTEcom o vetor deWPEpara formar o vetor de entrada da etapa seguinte - Como
WPEtem apenas 1.024 posições, o número máximo de tokens que pode ser usado em um prompt do GPT-2 é 1.024- Esse número corresponde à janela de contexto do LLM
- É um hiperparâmetro definido no projeto do modelo e não muda com treinamento
- A implementação em SQL usa pgvector
- Também seria possível em SQL puro definindo operações vetoriais diretamente sobre arrays, mas o desempenho seria pior
- A versão inicial funcionava com funções puramente SQL, mas era lenta
Expandindo self-attention em consultas SQL
- O núcleo do Transformer é o mecanismo de self-attention, baseado no artigo de 2017 Attention is all you need
- A attention faz com que os vetores de token influenciem uns aos outros, permitindo que informações do início do prompt cheguem ao vetor final
- A implementação do GPT-2 usa 12 conjuntos de matrizes
Q,K,V- Cada conjunto é uma attention head
- Cada head tem 64 dimensões
c_attné uma transformação linear 768×2304, e o resultado é um vetor de 2304 dimensões comQ,K,Vempilhados horizontalmente- Os pesos e bias ficam armazenados nas tabelas
c_attn_wec_attn_b
- Antes do cálculo de attention, é aplicada layer normalization
- Os parâmetros de scale e shift ficam nas tabelas
ln_1_geln_1_b
- Os parâmetros de scale e shift ficam nas tabelas
- Em causal self-attention, aplica-se uma causal mask para impedir que tokens posteriores influenciem tokens anteriores
- Os candidatos ao próximo token do modelo são determinados no fim a partir do último embedding
- O fluxo de informação precisa apontar para o último vetor, e os valores intermediários do último vetor não podem influenciar vetores anteriores
- Na implementação em SQL, para evitar o problema de o
EXPdo PostgreSQL falhar com números muito pequenos no cálculo de softmax, entradas menores que -745.13 são tratadas como 0 - Graças à causal mask, mesmo que um novo token seja anexado ao prompt, os resultados já calculados para os tokens anteriores não mudam
- A implementação original do GPT-2 aproveita essa propriedade
- A implementação em SQL não reutiliza isso por simplicidade
Multi-head attention e conexões residuais
- Os resultados de attention das 12 heads têm 64 dimensões cada e são empilhados horizontalmente para voltar a 768 dimensões
- Em seguida, a saída de attention é projetada por uma transformação linear treinada armazenada em
c_proj_wec_proj_b - O resultado da multi-headed attention recebe novamente a soma da entrada original
- Essa conexão residual é uma técnica incluída no artigo original do Transformer
- Foi apresentada como um desenho para aliviar problemas de vanishing gradient e exploding gradient durante o treinamento
Etapa feedforward e bloco Transformer
- Depois da attention, vem uma rede neural feedforward
- A etapa feedforward do GPT-2 é composta por um perceptron multicamadas de 3 camadas
- As dimensões são 768 → 3072 → 768
- A função de ativação usada é GELU
- Os parâmetros de transformação linear são armazenados nas seguintes tabelas
mlp_c_fc_w,mlp_c_fc_bmlp_c_proj_w,mlp_c_proj_b
- A entrada do feedforward também é normalizada antes com os parâmetros de
ln_2ln_2_geln_2_barmazenam scale e shift
- Uma conexão residual que soma de volta a entrada também é aplicada à saída do feedforward
- Essa combinação de attention + feedforward forma um bloco, e o GPT-2 encadeia 12 blocos como um pipeline
- Cada bloco tem seu próprio conjunto de parâmetros treinados
- Em SQL, os blocos são conectados com um recursive CTE
- A saída do último bloco é normalizada novamente com os parâmetros
ln_f
Voltando ao próximo token
- No resultado final, o vetor de 768 dimensões da última posição é o embedding do próximo token
- Para transformá-lo de volta em token, reutiliza-se a matriz
WTEusada no embedding inicial dos tokens - Em geral, a inversão exata não é possível
- O embedding previsto pode não ser exatamente igual a uma linha específica de
WTE - Por isso, calcula-se o dot product com cada embedding de token para encontrar os tokens mais próximos
- O embedding previsto pode não ser exatamente igual a uma linha específica de
- O resultado do dot product entre
WTEe o embedding previsto gera 50.257 pontuações, ou seja, logits - Essas pontuações passam por softmax e viram probabilidades
top_né o número de candidatos do topo- temperature é o valor que controla a distribuição de probabilidades
- Quanto maior a temperature, maior a chance de um token fora do primeiro lugar ser escolhido, e menos previsível fica a inferência
- No exemplo
"PostgreSQL is great", os 5 principais candidatos ao próximo token são os seguintesĠfor,.ĠatĠto
- Quando a temperature muda para 0.5, 1 e 2, a distribuição softmax de probabilidade dos mesmos candidatos muda
Resultado real da inferência e código
- O SQL final repete o processo de escolher tokens segundo a probabilidade e anexá-los ao prompt
- O modelo em si é determinístico; o único elemento não determinístico está no sorteio do token
- A configuração do exemplo é a seguinte
- Prompt:
"Happy New Year! I wish you" - Número de tokens gerados: 10
- Temperature: 2
- top_n: 1
- Uso de
SETSEED(0.20231231)
- Prompt:
- No ambiente do autor, a consulta levou 2 minutos e 44 segundos para executar
- O resultado de saída foi
"Happy New Year! I wish you all the best in your new year!" - A consulta e o código de instalação estão no repositório GitHub quassnoi/explain-extended-2024
1 comentários
Comentários do Hacker News
Lindo. Eu também estava cavando uma toca de coelho parecida com SQLite, mas ainda não tinha chegado ao ponto de puxar até redes neurais
Foi inspirado pela série de aulas makemore[0], e depois de mais ou menos 1 hora ela passa do método de contagem para redes neurais, que foi mais ou menos até onde eu cheguei
Dividir isso em um modelo relacional é um exercício realmente ótimo
[0] https://www.youtube.com/watch?v=PaCmpygFfXo
É uma boa demo, mas a explicação de mascaramento causal no texto parece misturar treinamento e inferência
O mascaramento causal serve para impedir que se “espie” tokens futuros durante o treinamento e, em arquiteturas do tipo GPT, também para impor autorregressão durante a inferência
Na inferência, de qualquer forma, só o último token é usado, então esse token presta atenção à sequência inteira de entrada; portanto, o próximo token não é determinado apenas pelo embedding do último token
Fico me perguntando se isso representa com precisão o loop driver do GPT: tokenizar o prompt, obter as probabilidades de 50257 tokens com
gpt2(tokens), escolher o próximo token, anexá-lo à lista de tokens, verificar a condição de parada e, no fim, destokenizarMas isso faz a máquina de estados parecer estar implementando o algoritmo do pintor Shlemiel, o que me faz questionar o custo computacional intrínseco do trabalho de geração
Essa janela é uma janela deslizante
gpt2É mais ou menos só mostrar como os tokens gerados são reunidos em uma frase
Material relacionado: A GPT in 60 Lines of NumPy - https://news.ycombinator.com/item?id=34726115 - fevereiro de 2023, 146 comentários
Em um contexto parecido, implementei o GPT inteiro usando funções de planilha, e também fiz um tutorial em vídeo para acompanhar
https://spreadsheets-are-all-you-need.ai/
Eu acho LLMs bem legais, mas nunca precisei aprender profissionalmente como elas funcionam de fato, e esse vídeo de 10 minutos me ensinou mais do que anos lendo comentários obscuros no HN e textos rasos da mídia tradicional
Ver aquela quantidade absurda de números de ponto flutuante empilhados esperando cálculo também tornou muito mais intuitivo entender por que essa tecnologia consome tanta GPU
Também parece que daria para explicar bem o processo de treinamento calculando a derivada de cada parâmetro em cada exemplo de treino e mostrando explicitamente como isso se mapeia para aquele parâmetro
Muito bom. Há 1 ano isso ainda parecia uma espécie de mágica, e agora está sendo explicado tão bem, quase a ponto de até uma criança conseguir acompanhar
O modelo explicado no texto é o GPT-2, lançado no começo de 2019
Para entender este texto direito, é preciso ter uma base sólida em ciência da computação, e o próprio título já é pouco acessível para 99% da humanidade
Eu vinha evitando completamente GPT e LLMs, e esse método parece conseguir produzir certa fluência na saída de texto, mas não parece ir até a capacidade de interpretar perguntas e respondê-las
Queria saber se existe algum post de blog simples ou curso que explique como isso realmente funciona, ou que mostre um motor de brinquedo em Python
O material educacional que vi até agora tende a focar em como usar as plataformas, e não tanto em como elas funcionam por dentro
Especialmente [0], [1], [2]
[0] http://jalammar.github.io/illustrated-transformer/
[1] http://jalammar.github.io/illustrated-gpt2/
[2] https://jalammar.github.io/visualizing-neural-machine-transl...
Curiosamente, o aprendizado de máquina moderno não exige completude de Turing
Mesmo assim estamos considerando a possibilidade de AGI, então seria bem interessante se a conclusão fosse que completude de Turing não é necessária
Por um motivo simples: eu consigo acompanhar mentalmente a execução de um código Turing-completo
O texto foi excelente, e a explicação de cada componente estava clara e bem completa, então foi uma ótima leitura
Mas apertei por engano “+ expand source” e, depois de ver aquele monstro impressionante, passei a concordar com o ChatGPT quando diz que “SQL não é adequado para implementar modelos de linguagem grandes”
A afirmação de que “Unicode comum não combina bem com redes neurais” não é verdadeira. Basta olhar para ByT5
O que o texto chama de “alfabeto” normalmente é chamado de vocabulário, e, se você usar bytes UTF-8 como vocabulário, passa a ter 256 tokens em vez de 149186
O ByT5 faz exatamente isso
O fato de que os modelos com melhor desempenho do mercado todos usam tokenização é a prova disso
Não é segredo que tokenização é, no fundo, algo meio hacky, e que o ideal seria conseguir eliminá-la de alguma forma no futuro (https://twitter.com/karpathy/status/1657949234535211009)
Em princípio, dá para compensar as limitações da tokenização em nível de bytes com modelos maiores e contexto maior, mas, na prática, treinar um modelo com o mesmo nível de inteligência exige muito mais recursos
Claro, também existem tarefas específicas em que a tokenização acaba até prejudicando a inteligência, como contar o número de letras em uma palavra