Logo Passei Direto
Buscar
Material
páginas com resultados encontrados.
páginas com resultados encontrados.
details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

details

Libere esse material sem enrolação!

Craque NetoCraque Neto

Ao continuar, você aceita os Termos de Uso e Política de Privacidade

Prévia do material em texto

Andrej Karpathy Acabou de Criar um GPT Completo em 243 Linhas de Python
Sem PyTorch. Sem TensorFlow. Apenas Python puro e matemática básica.
Sumit Pandey
15 de fevereiro de 2026
Li muitas implementações de transformadores durante o doutorado. Densas bases de código. Milhares de arquivos. Dependências empilhadas sobre dependências. Você abre um repositório, executa pip install -r requirements.txt e observa 400 pacotes serem baixados antes mesmo de poder ver seu modelo em funcionamento (sem contar erros, problemas de dependência etc.).
Então, em 11 de fevereiro de 2026, Andrej Karpathy lançou um único arquivo Python que treina e roda um GPT do zero. 243 linhas. Zero dependências.
Se você não conseguir ler o artigo, clique aqui
Suas únicas importações? os, math, random‚ e argparse. É isso. Esse é todo o LLM. Ele chamou de “projeto de arte.” Eu chamo isso de a melhor educação em IA que existe na internet atualmente. Deixe-me explicar cada parte desse código como se estivesse explicando para um amigo enquanto tomamos um café.
Primeiro, O Que Essa Coisa Realmente Faz?
Antes de tocarmos no código, sejamos claros sobre o que microGPT faz. Ele baixa uma lista de nomes de bebês. Ele aprende os padrões nesses nomes. Depois gera novos nomes, falsos, que som real mas nunca existiu.
É isso. O mesmo conceito do ChatGPT, só que em menor escala. O ChatGPT aprendeu com toda a internet. Este modelo aprende com um arquivo de texto de nomes. Mas o algoritmo é idêntico. O mecanismo de atenção, o loop de treinamento, a maneira como ele prevê o próximo token: é a mesma matemática executada dentro do GPT-4. Só que em microescala.
Pense nisso desta forma: um carro de brinquedo e um Tesla têm ambos motores, rodas, direção e freios. O carro de brinquedo não vai ganhar nenhuma corrida, mas se você quiser entender como um carro funciona, o carro de brinquedo é perfeito.
Parte 1: O tokenizador (as linhas que você vai esquecer são importantes)
chars = ['', ''] + sorted(list(set(''.join(docs))))
vocab_size = len(chars)
stoi = { ch:i for i, ch in enumerate(chars) }
itos = { i:ch for i, ch in enumerate(chars) }
Computadores não entendem letras. Eles entendem números.
Portanto, a primeira tarefa é converter cada caractere em um número. A letra a se torna 2, b se torna 3 e assim por diante. Há também dois tokens especiais: (Beginning Of Sequence, “Ei, modelo, um novo nome está começando!”) e (End Of Sequence, “Este nome está concluído!”).
stoi converte strings em inteiros. itos converte inteiros de volta para strings. Esse é todo o seu tokenizador.
O ChatGPT usa um tokenizador muito mais sofisticado chamado BPE (Byte Pair Encoding, codificação de pares de bytes), que agrupa combinações comuns de letras em tokens únicos. Mas a ideia é idêntica: texto entra, números saem.
Parte 2: O Motor Autograd (O Coração de Tudo)
É aqui que fica bonito.
class Value:
 def __init__(self, data, _children=(), _op=''):
 self.data = data
 self.grad = 0
 self._backward = lambda: None
 self._prev = set(_children)
Karpathy constrói uma versão mini do mecanismo autograd do PyTorch em cerca de 40 linhas.
Deixe-me explicar por que isso é importante.
Quando uma rede neural faz uma previsão, geralmente ela está errada no início. Precisamos descobrir: quais números (pesos) devemos ajustar, e em quanto, para torná-la menos errada?
É isso que a retropropagação faz. E a retropropagação precisa de gradientes — basicamente, ela precisa saber “se eu mexer neste peso um pouquinho, quanto o erro final mudará?”
A classe Value envolve cada único número na rede e rastreia:
· data: o número real
· grad: quanto a perda final muda quando você altera esse número
· _backward: instruções para calcular esse gradiente
· _prev: quais outros Values foram usados para criar este
Sempre que você faz cálculos matemáticos (adição, multiplicação, potência, etc.), a classe Value constrói silenciosamente um gráfico de operações nos bastidores. Quando o treinamento é concluído para um exemplo, você chama .backward()e os gradientes fluem de volta através deste gráfico automaticamente.
Aqui está a adição:
def __add__(self, other):
 out = Value(self.data + other.data, (self, other), '+')
 def _backward():
 self.grad += out.grad
 other.grad += out.grad
 out._backward = _backward
 return out
Se c = a + b‚ e alguém lhe disser “o gradiente de C é 5,” então o gradiente de a e de b também é 5. Porque se você aumentar a em 1, c também aumenta em 1. Essa é a regra da cadeia do cálculo, implementada em 4 linhas.
A multiplicação é semelhante, mas usa a regra da “troca” regra:
def __mul__(self, other):
 out = Value(self.data * other.data, (self, other), '*')
 def _backward():
 self.grad += other.data * out.grad
 other.grad += self.data * out.grad
 out._backward = _backward
 return out
Se c = a * b, então o gradiente de a é b * (gradient of c) e o gradiente de b é a * (gradient of c). É isso.
O backward completo usa classificação topológica: ele apenas garante que você calcule os gradientes na ordem correta (saídas primeiro, entradas por último):
def backward(self):
 topo = []
 visited = set()
 def build_topo(v):
 if v not in visited:
 visited.add(v)
 for child in v._prev:
 build_topo(child)
 topo.append(v)
 build_topo(self)
 self.grad = 1
 for v in reversed(topo):
 v._backward()
Este é literalmente o mesmo algoritmo que é executado no PyTorch quando você chama loss.backward(). Karpathy simplesmente o escreveu do zero usando listas Python e recursão.
Parte 3: Parâmetros do Modelo (O “Cérebro” do GPT)
matrix = lambda nout, nin, std=0.02: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)]
state_dict = {'wte': matrix(vocab_size, n_embd), 'wpe': matrix(block_size, n_embd)}
Cada rede neural é apenas uma coleção de números (chamados pesos ou parâmetros). Antes do treinamento, eles são aleatórios. Após o treinamento, eles codificam os padrões aprendidos.
Veja o que cada matriz de pesos faz:
· wte (Embedding do Word Token): converte cada ID de token em um vetor de 16 números. Pense nisso como dar a cada caractere uma “personalidade”: uma posição em um espaço de 16 dimensões onde caracteres semelhantes ficam próximos uns dos outros.
· wpe (Word Position Embedding): informa ao modelo onde cada caractere se encontra na sequência. O “a” na posição 1 deve ser tratado de maneira diferente do “a” na posição 5.
Para cada camada do transformador, há:
· attn_wq, attn_wk, attn_wv: As matrizes Query, Key e Value para atenção (mais detalhes abaixo)
· attn_wo: A projeção de saída da atenção
· mlp_fc1, mlp_fc2: A rede feedforward que processa as informações atendidas
Configuração padrão: 16 dimensões de incorporação, 4 cabeças de atenção, 1 camada, comprimento de sequência de 8. Isso resulta em aproximadamente 4.000 parâmetros. O GPT-4 tem mais de um trilhão. Mesma arquitetura, escala totalmente diferente.
Parte 4: A Arquitetura GPT (Onde a Mágica Acontece)
Este é o núcleo. Vou explicar a função gpt() passo a passo.
Passo 1: Embedding
tok_emb = state_dict['wte'][token_id]
pos_emb = state_dict['wpe'][pos_id % block_size]
x = [t + p for t, p in zip(tok_emb, pos_emb)]
Pegue a incorporação do caractere. Pegue a incorporação da posição. Someas. Agora, o modelo sabe tanto o que é o caractere quanto onde ele está localizado.
Passo 2: RMSNorm
def rmsnorm(x):
 ms = sum(xi * xi for xi in x) / len(x)
 scale = (ms + 1e-5) ** -0.5
 return [xi * scale for xi in x]
Antes de cada cálculo importante, normalizamos os valores para que eles não explodam ou encolham até zero. O RMSNorm é um primo mais simples do LayerNorm (que o GPT-2 original usa). Ele divide cada valor pela raiz quadrada média de todos os valores.
Pense nisso como manter o volume de todos em um nível razoável antes de uma discussão em grupo.
Passo 3: Atenção (A Estrela do Show)
Esta é a parte que todos falam. “Attentionis All You Need”: o famoso artigo de 2017. Aqui está ele, puro e simples, em Python:
q = linear(x, state_dict[f'layer{li}.attn_wq'])
k = linear(x, state_dict[f'layer{li}.attn_wk'])
val = linear(x, state_dict[f'layer{li}.attn_wv'])
Para o token atual, calcule três coisas:
· Query (Q): “O que estou procurando?”
· Key (K): “O que eu contenho?”
· Value (V): “Que informação eu carrego?”
Então, para cada cabeça de atenção:
attn_logits = [sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5 
 for t in range(len(k_h))]
attn_weights = softmax(attn_logits)
head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) 
 for j in range(head_dim)]
Eis o que está acontecendo em linguagem simples:
1. A Consulta do token atual pergunta: “Quais tokens anteriores são relevantes para mim?”
2. Ele calcula um produto escalar com a chave de cada token anterior (como uma pontuação de compatibilidade)
3. Essas pontuações são passadas pelo softmax (fazendo com que somem 1, como probabilidades)
4. Os vetores de valor de todos os tokens são então misturados, ponderados por essas pontuações
Então, se o modelo está gerando o nome “Micha” e estiver prestes a prever a próxima letra, o mecanismo de atenção permite que a posição atual “olhe para trás” para M, i, c, h, a, e decida quais são mais importantes para prever o que vem a seguir.
A divisão por head_dim**0.5 é chamada de atenção dimensionada: ela evita que os produtos dot (produtos escalares) fiquem muito grandes, o que faria com que a saída do softmax apresentasse probabilidades extremas (basicamente todos os 0s e um 1).
Atenção multi-head significa que executamos este processo 4 vezes em paralelo (com 4 projeções Q, K, V diferentes), cada uma analisando uma fatia diferente de 4 dimensões da incorporação de 16 dimensões. Uma head pode aprender a se concentrar nas vogais. Outra pode se concentrar nos caracteres recentes. Todas elas trazem perspectivas diferentes.
Passo 4: Conexão Residual
x = [a + b for a, b in zip(x, x_residual)]
Após a atenção, adicionamos novamente a entrada original. Isso é chamado de conexão residual (ou conexão de salto).
Por quê? Porque cria um “autoestrada” para gradientes durante o treinamento. Sem ela, redes profundas tornam-se quase impossíveis de treinar. É como dizer: “Tudo o que você aprendeu com a atenção, basta adicionar ao que você já sabia”.
Passo 5: MLP (Rede Feed-Forward)
x = linear(x, state_dict[f'layer{li}.mlp_fc1'])
x = [xi.relu() ** 2 for xi in x]
x = linear(x, state_dict[f'layer{li}.mlp_fc2'])
Depois que a atenção decide a quais tokens prestar atenção, o MLP processa essas informações.
O MLP expande o vetor de 16 dimensões para 64 dimensões (4 * n_embd), aplica uma não linearidade (ReLU quadrado), e o comprime de volta para 16.
Por que expandir e comprimir? Pense nisso como um brainstorming. Primeiro você gera muitas ideias (expande para 64), filtra as ruins (ReLU elimina as negativas, a quadratura amplifica sinais fortes) e, em seguida, resume (comprimir de volta para 16).
ReLU ao quadrado em vez de GELU é uma das simplificações de Karpathy. ReLU regular diz: “se negativo, torne 0.” Ao Quadrado ReLU diz: “se negativo, torne 0; se positivo, eleve ao quadrado.” Isso proporciona uma não linearidade mais forte.
Passo 6: Projete para o Vocabulário
logits = linear(x, state_dict['wte'])
Por fim, o modelo usa a mesma matriz de incorporação (wte) para projetar de volta ao tamanho do vocabulário. Isso é chamado weight tying (vinculação de pesos): os mesmos pesos que convertem IDs de tokens em embeddings são reutilizados para converter incorporações de volta em previsões.
A saída é uma pontuação para cada caractere seguinte possível. Pontuação mais alta = modelo considera que esse caractere é mais provável de aparecer a seguir.
Parte 5: O Loop de Treinamento (Como Ele Aprende)
for step in range(args.num_steps):
 doc = docs[step % len(docs)]
 tokens = [BOS] + [stoi[ch] for ch in doc] + [EOS]
 tokens = tokens[:block_size]
Para cada etapa, pegue um nome do conjunto de dados. Converta-o em IDs de token. Adicione BOS no início e EOS no final.
for pos_id in range(len(tokens) - 1):
 logits = gpt(tokens[pos_id], pos_id, keys, values)
 probs = softmax(logits)
 loss = -probs[tokens[pos_id + 1]].log()
Para cada posição, o modelo vê o token atual e prevê o próximo. A perda é a log-verossimilhança negativa: se o modelo atribuir alta probabilidade ao próximo caractere correto, a perda é baixa. Se atribuir baixa probabilidade, a perda é alta.
Em seguida, loss.backward() calcula os gradientes para cada parâmetro, e o otimizador Adam os atualiza:
m[i] = beta1 * m[i] + (1 - beta1) * p.grad
v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2
m_hat = m[i] / (1 - beta1 ** (step + 1))
v_hat = v[i] / (1 - beta2 ** (step + 1))
p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)
Adam é como o gradiente descendente com memória. Em vez de apenas seguir o gradiente atual, ele rastreia:
· m : a direção média dos gradientes recentes (momento: como uma bola rolando ladeira abaixo)
· v : a magnitude média dos gradientes recentes (ajuda a normalizar: gradientes grandes não dominam)
Este é o mesmo otimizador usado para treinar o GPT-4, o DALL-E e todos os principais modelos atuais.
Parte 6: Inferência (Gerando Novos Nomes)
for sample_idx in range(5):
 token_id = BOS
 generated = []
 for pos_id in range(block_size):
 logits = gpt(token_id, pos_id, keys, values)
 probs = softmax(logits)
 token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
 if token_id == EOS:
 break
 generated.append(itos[token_id])
Comece com BOS (Broadcast Outcome System - Sistema de Ordenação de Entrada). Pergunte ao modelo "o que vem a seguir?". Faça uma amostragem da distribuição de probabilidade. Alimente o modelo com essa previsão. Repita até que o modelo retorne EOS (End of Outcome System - Sistema de Ordenação de Entrada) ou até que o espaço se esgote.
Isto está geração autorregressiva: a mesma coisa que o ChatGPT faz toda vez que você envia uma mensagem. Um token de cada vez, cada um condicionado a tudo o que veio antes.
O Quadro Geral: Por que isso é importante?
Eis o que me impressionou.
Cada linha de código além dessas 243 linhas: os milhares de arquivos no PyTorch, os kernels CUDA, as estruturas de treinamento distribuídas, os tokenizadores sofisticados, tudo isso é apenas otimização. Tornando-o mais rápido. Tornando-o maior. Fazendo-o rodar em milhares de GPUs.
Mas o algoritmo? A inteligência real? Está bem aqui.
Como Karpathy disse: “Este é todo o conteúdo algorítmico necessário. Todo o resto é apenas para eficiência. Não posso simplificar mais do que isso.”
Esta é a jornada de compressão de 6 anos de Karpathy:
· 2020: micrograd (mecanismo autograd)
· 2020: minGPT (PyTorch GPT)
· 2023: nanoGPT (treinamento em nível de produção)
· 2024: llm.c (C/CUDA puro, sem frameworks)
· 2026: microGPT (o algoritmo e nada mais)
Cada etapa removeu uma camada de abstração. Esta versão final removeu todas elas.
A indústria está gastando US$ 400 bilhões em infraestrutura de IA este ano. Mas e o algoritmo central que alimenta tudo isso? Ele cabe em um arquivo menor do que a maioria dos documentos README.
Se você está tentando entender como os LLMs funcionam, não comece pela documentação do PyTorch. Comece aqui.
Se você achou isso útil, siga-me no Towards Deep Learning ( Rumo à Aprendizagem Profunda ) onde eu analiso as últimas pesquisas em IA em linguagem simples. E se você quiser executar isso sozinho, pegue o código no GitHub Gist da Karpathy e digite python microgpt.py. Não é necessário instalar nada.
Andrej Karpathy Just Built an Entire GPT in 243 Lines of Python
No PyTorch. No TensorFlow. Just pure Python and basic math.
 Sumit Pandey Feb 15, 2026
https://www.towardsdeeplearning.com/andrej-karpathy-just-built-an-entire-gpt-in-243-lines-of-python-7d66cfdfa301
I’ve read many transformer implementations during my PhD.Dense codebases. Thousands of files. Dependencies stacked on top of dependencies. You open a repo, run pip install -r requirements.txt, and watch 400 packages download before you can even see your model train (than errors , dependency issues … etc.).
Then on February 11, 2026, Andrej Karpathy dropped a single Python file that trains and runs a GPT from scratch. 243 lines. Zero dependencies.
His only imports? os, math, random, and argparse. That’s it. That’s the entire LLM. He called it an “art project.” I call it the best AI education that exists on the internet right now. Let me walk you through every piece of this code like I’m explaining it to a friend over coffee.
First, What Is This Thing Actually Doing?
Before we touch the code, let’s be clear about what microGPT does. It downloads a list of baby names. It learns the patterns in those names. Then it generates new, fake names that sound real but never existed.
That’s it. Same concept as ChatGPT, just tiny. ChatGPT learned from the entire internet. This model learns from a text file of names. But the algorithm is identical. The attention mechanism, the training loop, the way it predicts the next token: it’s the same math running inside GPT-4. Just at a micro scale.
Think of it like this: a toy car and a Tesla both have engines, wheels, steering, and brakes. The toy car won’t win any races, but if you want to understand how a car works, the toy car is perfect.
Part 1: The Tokenizer (Lines You’ll Forget Are Important)
chars = ['', ''] + sorted(list(set(''.join(docs))))
vocab_size = len(chars)
stoi = { ch:i for i, ch in enumerate(chars) }
itos = { i:ch for i, ch in enumerate(chars) }
Computers don’t understand letters. They understand numbers.
So the very first job is to convert every character into a number. The letter a becomes 2, b becomes 3, and so on. There are also two special tokens:  (Beginning Of Sequence, "Hey model, a new name is starting!") and  (End Of Sequence "This name is done now!").
stoi converts strings to integers. itos converts integers back to strings. That's your entire tokenizer.
ChatGPT uses a much fancier tokenizer called BPE (Byte Pair Encoding) that groups common letter combinations into single tokens. But the idea is identical: text in, numbers out.
Part 2: The Autograd Engine (The Heart of Everything)
This is where it gets beautiful.
class Value:
 def __init__(self, data, _children=(), _op=''):
 self.data = data
 self.grad = 0
 self._backward = lambda: None
 self._prev = set(_children)
Karpathy builds a mini version of PyTorch’s autograd engine in about 40 lines.
Let me explain why this matters.
When a neural network makes a prediction, it’s usually wrong at first. We need to figure out: which numbers (weights) should we adjust, and by how much, to make it less wrong?
That’s what backpropagation does. And backpropagation needs gradients — basically, it needs to know “if I wiggle this weight a tiny bit, how much does the final error change?”
The Value class wraps every single number in the network and tracks:
· data: the actual number
· grad: how much the final loss changes when you change this number
· _backward: instructions for computing that gradient
· _prev: which other Values were used to create this one
Every time you do math (add, multiply, power, etc.), the Value class quietly builds a graph of operations behind the scenes. When training is done for one example, you call .backward() and the gradients flow back through this graph automatically.
Here’s addition:
def __add__(self, other):
 out = Value(self.data + other.data, (self, other), '+')
 def _backward():
 self.grad += out.grad
 other.grad += out.grad
 out._backward = _backward
 return out
If c = a + b, and someone tells you "c's gradient is 5," then both a's and b's gradient is also 5. Because if you increase a by 1, c also increases by 1. That's the chain rule from calculus, implemented in 4 lines.
Multiplication is similar but uses the “swap” rule:
def __mul__(self, other):
 out = Value(self.data * other.data, (self, other), '*')
 def _backward():
 self.grad += other.data * out.grad
 other.grad += self.data * out.grad
 out._backward = _backward
 return out
If c = a ∗ b, then a's gradient is b ∗ (gradient of c) and b's gradient is a ∗ (gradient of c). That's it.
The full backward pass uses topological sort: it just makes sure you compute gradients in the right order (outputs first, inputs last):
def backward(self):
 topo = []
 visited = set()
 def build_topo(v):
 if v not in visited:
 visited.add(v)
 for child in v._prev:
 build_topo(child)
 topo.append(v)
 build_topo(self)
 self.grad = 1
 for v in reversed(topo):
 v._backward()
This is literally the same algorithm that runs inside PyTorch when you call loss.backward(). Karpathy just wrote it from scratch using Python lists and recursion.
Part 3: Model Parameters (The “Brain” of the GPT)
matrix = lambda nout, nin, std=0.02: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)]
state_dict = {'wte': matrix(vocab_size, n_embd), 'wpe': matrix(block_size, n_embd)}
Every neural network is just a collection of numbers (called weights or parameters). Before training, these are random. After training, they encode learned patterns.
Here’s what each weight matrix does:
· wte (Word Token Embedding): Converts each token ID into a vector of 16 numbers. Think of it as giving each character a "personality": a position in a 16-dimensional space where similar characters end up close together.
· wpe (Word Position Embedding): Tells the model where in the sequence each character sits. "a" at position 1 should be treated differently than "a" at position 5.
For each transformer layer, there are:
· attn_wq, attn_wk, attn_wv: The Query, Key, and Value matrices for attention (more on this below)
· attn_wo: The output projection of attention
· mlp_fc1, mlp_fc2: The feedforward network that processes the attended information
Default configuration: 16 embedding dimensions, 4 attention heads, 1 layer, sequence length of 8. This gives you roughly 4,000 parameters. GPT-4 has over a trillion. Same architecture, wildly different scale.
Part 4: The GPT Architecture (Where the Magic Happens)
This is the core. Let me break down the gpt() function piece by piece.
Step 1: Embedding
tok_emb = state_dict['wte'][token_id]
pos_emb = state_dict['wpe'][pos_id % block_size]
x = [t + p for t, p in zip(tok_emb, pos_emb)]
Take the character’s embedding. Take the position’s embedding. Add them together. Now the model knows both what the character is and where it sits.
Step 2: RMSNorm
def rmsnorm(x):
 ms = sum(xi * xi for xi in x) / len(x)
 scale = (ms + 1e-5) ** -0.5
 return [xi * scale for xi in x]
Before each major computation, we normalize the values so they don’t explode or shrink to zero. RMSNorm is a simpler cousin of LayerNorm (which original GPT-2 uses). It divides each value by the root-mean-square of all values.
Think of it as keeping everyone’s volume at a reasonable level before a group discussion.
Step 3: Attention (The Star of the Show)
This is the part everyone talks about. “Attention is All You Need”: the famous 2017 paper. Here it is, naked, in pure Python:
q = linear(x, state_dict[f'layer{li}.attn_wq'])
k = linear(x, state_dict[f'layer{li}.attn_wk'])
val = linear(x, state_dict[f'layer{li}.attn_wv'])
For the current token, compute three things:
· Query (Q): “What am I looking for?”
· Key (K): “What do I contain?”
· Value (V): “What information do I carry?”
Then for each attention head:
attn_logits = [sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5 
 for t in range(len(k_h))]
attn_weights = softmax(attn_logits)
head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) 
 for j in range(head_dim)]
Here’s what’s happening inplain English:
1. The current token’s Query asks: “Which previous tokens are relevant to me?”
2. It computes a dot product with every previous token’s Key (like a compatibility score)
3. These scores are passed through softmax (making them add up to 1, like probabilities)
4. The Value vectors of all tokens are then mixed together, weighted by these scores
So if the model is generating the name “Micha” and it’s about to predict the next letter, the attention mechanism lets the current position “look back” at M, i, c, h, and a, and decide which ones matter most for predicting what comes next.
The division by head_dim**0.5 is called scaled attention: it prevents the dot products from getting too large, which would make softmax output extreme probabilities (basically all 0s and one 1).
Multi-head attention means we run this process 4 times in parallel (with 4 different Q, K, V projections), each looking at a different 4-dimensional slice of the 16-dimensional embedding. One head might learn to focus on vowels. Another might focus on recent characters. They all bring different perspectives.
Step 4: Residual Connection
x = [a + b for a, b in zip(x, x_residual)]
After attention, we add back the original input. This is called a residual connection (or skip connection).
Why? Because it creates a “highway” for gradients during training. Without it, deep networks become nearly impossible to train. It’s like saying: “Whatever you learned from attention, just add it to what you already knew.”
Step 5: MLP (Feed-Forward Network)
x = linear(x, state_dict[f'layer{li}.mlp_fc1'])
x = [xi.relu() ** 2 for xi in x]
x = linear(x, state_dict[f'layer{li}.mlp_fc2'])
After attention decides which tokens to pay attention to, the MLP processes that information.
The MLP expands the 16-dimensional vector to 64 dimensions (4 * n_embd), applies a non-linearity (squared ReLU), and squishes it back to 16.
Why expand and compress? Think of it like brainstorming. You first generate lots of ideas (expand to 64), filter out the bad ones (ReLU kills negatives, squaring amplifies strong signals), then summarize (compress back to 16).
Squared ReLU instead of GELU is one of Karpathy’s simplifications. Regular ReLU says: “if negative, make it 0.” Squared ReLU says: “if negative, make it 0; if positive, square it.” This gives stronger non-linearity.
Step 6: Project to Vocabulary
logits = linear(x, state_dict['wte'])
Finally, the model uses the same embedding matrix (wte) to project back to vocabulary size. This is called weight tying ,the same weights that convert token IDs into embeddings are reused to convert embeddings back into predictions.
The output is a score for each possible next character. Higher score = model thinks this character is more likely to come next.
Part 5: The Training Loop (How It Learns)
for step in range(args.num_steps):
 doc = docs[step % len(docs)]
 tokens = [BOS] + [stoi[ch] for ch in doc] + [EOS]
 tokens = tokens[:block_size]
For each step, grab one name from the dataset. Convert it to token IDs. Add BOS at the start and EOS at the end.
for pos_id in range(len(tokens) - 1):
 logits = gpt(tokens[pos_id], pos_id, keys, values)
 probs = softmax(logits)
 loss = -probs[tokens[pos_id + 1]].log()
For each position, the model sees the current token and predicts the next one. The loss is negative log likelihood: if the model assigns high probability to the correct next character, the loss is low. If it assigns low probability, the loss is high.
Then loss.backward() computes gradients for every parameter, and the Adam optimizer updates them:
m[i] = beta1 * m[i] + (1 - beta1) * p.grad
v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2
m_hat = m[i] / (1 - beta1 ** (step + 1))
v_hat = v[i] / (1 - beta2 ** (step + 1))
p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)
Adam is like gradient descent with memory. Instead of just following the current gradient, it tracks:
· m : the average direction of recent gradients (momentum: like a ball rolling downhill)
· v : the average magnitude of recent gradients (helps normalize: big gradients don't dominate)
This is the same optimizer used to train GPT-4, DALL-E, and every major model today.
Part 6: Inference (Generating New Names)
for sample_idx in range(5):
 token_id = BOS
 generated = []
 for pos_id in range(block_size):
 logits = gpt(token_id, pos_id, keys, values)
 probs = softmax(logits)
 token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
 if token_id == EOS:
 break
 generated.append(itos[token_id])
Start with BOS. Ask the model “what comes next?” Sample from the probability distribution. Feed that prediction back in. Repeat until the model outputs EOS or we run out of space.
This is autoregressive generation: the same thing ChatGPT does every time you send it a message. One token at a time, each conditioned on everything before it.
The Big Picture: Why This Matters
Here’s the thing that blew my mind.
Every line of code beyond these 243 lines: the thousands of files in PyTorch, the CUDA kernels, the distributed training frameworks, the fancy tokenizers, all of that is just optimization. Making it faster. Making it bigger. Making it run across thousands of GPUs.
But the algorithm? The actual intelligence? It’s right here.
As Karpathy put it: “This is the full algorithmic content of what is needed. Everything else is just for efficiency. I cannot simplify this any further.”
This is Karpathy’s 6-year compression journey:
· 2020: micrograd (autograd engine)
· 2020: minGPT (PyTorch GPT)
· 2023: nanoGPT (production-grade training)
· 2024: llm.c (raw C/CUDA, no frameworks)
· 2026: microGPT (the algorithm and nothing else)
Each step peeled away a layer of abstraction. This final version removed all of them.
The industry is spending $400 billion on AI infrastructure this year. But the core algorithm that powers all of it? It fits in a file smaller than most README documents.
If you’re trying to understand how LLMs work, don’t start with the PyTorch documentation. Start here.
If you found this useful, follow me on Towards Deep Learning where I break down the latest AI research into plain English. And if you want to run this yourself, grab the code from Karpathy’s GitHub Gist and just type python microgpt.py. No installs needed.
image1.jpeg
image2.png

Mais conteúdos dessa disciplina