L'implémentation la plus atomique d'un GPT en Python pur, sans dépendance, par Andrej Karpathy. Ci-dessous : le code original et ses portages en Java et Ruby réalisés par la communauté.
import os, math, random
random.seed(42)
if not os.path.exists('input.txt'):
import urllib.request
urllib.request.urlretrieve(
'https://raw.githubusercontent.com/karpathy/makemore/988aa59/names.txt',
'input.txt')
docs = [line.strip() for line in open('input.txt') if line.strip()]
random.shuffle(docs)
print(f"num docs: {len(docs)}")
uchars = sorted(set(''.join(docs)))
BOS = len(uchars)
vocab_size = len(uchars) + 1
print(f"vocab size: {vocab_size}")
class Value:
__slots__ = ('data', 'grad', '_children', '_local_grads')
def __init__(self, data, children=(), local_grads=()):
self.data = data
self.grad = 0
self._children = children
self._local_grads = local_grads
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data + other.data, (self, other), (1, 1))
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
return Value(self.data * other.data, (self, other), (other.data, self.data))
def __pow__(self, other): return Value(self.data**other, (self,), (other * self.data**(other-1),))
def log(self): return Value(math.log(self.data), (self,), (1/self.data,))
def exp(self): return Value(math.exp(self.data), (self,), (math.exp(self.data),))
def relu(self): return Value(max(0, self.data), (self,), (float(self.data > 0),))
def __neg__(self): return self * -1
def __radd__(self, other): return self + other
def __sub__(self, other): return self + (-other)
def __rsub__(self, other): return other + (-self)
def __rmul__(self, other): return self * other
def __truediv__(self, other): return self * other**-1
def __rtruediv__(self, other): return other * self**-1
def backward(self):
topo, visited = [], set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._children: build_topo(child)
topo.append(v)
build_topo(self)
self.grad = 1
for v in reversed(topo):
for child, local_grad in zip(v._children, v._local_grads):
child.grad += local_grad * v.grad
n_layer, n_embd, block_size, n_head = 1, 16, 16, 4
head_dim = n_embd // n_head
matrix = lambda nout, nin, std=0.08: [[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), 'lm_head': matrix(vocab_size, n_embd)}
for i in range(n_layer):
state_dict[f'layer{i}.attn_wq'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.attn_wk'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.attn_wv'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.attn_wo'] = matrix(n_embd, n_embd)
state_dict[f'layer{i}.mlp_fc1'] = matrix(4 * n_embd, n_embd)
state_dict[f'layer{i}.mlp_fc2'] = matrix(n_embd, 4 * n_embd)
params = [p for mat in state_dict.values() for row in mat for p in row]
print(f"num params: {len(params)}")
def linear(x, w):
return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w]
def softmax(logits):
max_val = max(val.data for val in logits)
exps = [(val - max_val).exp() for val in logits]
total = sum(exps)
return [e / total for e in exps]
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]
def gpt(token_id, pos_id, keys, values):
x = [t + p for t, p in zip(state_dict['wte'][token_id], state_dict['wpe'][pos_id])]
x = rmsnorm(x)
for li in range(n_layer):
x_residual = x
x = rmsnorm(x)
q = linear(x, state_dict[f'layer{li}.attn_wq'])
k = linear(x, state_dict[f'layer{li}.attn_wk'])
v = linear(x, state_dict[f'layer{li}.attn_wv'])
keys[li].append(k); values[li].append(v)
x_attn = []
for h in range(n_head):
hs = h * head_dim
q_h = q[hs:hs+head_dim]
k_h = [ki[hs:hs+head_dim] for ki in keys[li]]
v_h = [vi[hs:hs+head_dim] for vi in values[li]]
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)]
x_attn.extend(head_out)
x = linear(x_attn, state_dict[f'layer{li}.attn_wo'])
x = [a + b for a, b in zip(x, x_residual)]
x_residual = x
x = rmsnorm(x)
x = linear(x, state_dict[f'layer{li}.mlp_fc1'])
x = [xi.relu() for xi in x]
x = linear(x, state_dict[f'layer{li}.mlp_fc2'])
x = [a + b for a, b in zip(x, x_residual)]
return linear(x, state_dict['lm_head'])
learning_rate, beta1, beta2, eps_adam = 0.01, 0.85, 0.99, 1e-8
m = [0.0] * len(params)
v = [0.0] * len(params)
num_steps = 1000
for step in range(num_steps):
doc = docs[step % len(docs)]
tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
n = min(block_size, len(tokens) - 1)
keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
losses = []
for pos_id in range(n):
token_id, target_id = tokens[pos_id], tokens[pos_id + 1]
logits = gpt(token_id, pos_id, keys, values)
probs = softmax(logits)
losses.append(-probs[target_id].log())
loss = (1 / n) * sum(losses)
loss.backward()
lr_t = learning_rate * (1 - step / num_steps)
for i, p in enumerate(params):
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)
p.grad = 0
print(f"step {step+1:4d} / {num_steps:4d} | loss {loss.data:.4f}", end='\r')
temperature = 0.5
print("\n--- inference ---")
for sample_idx in range(20):
keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
token_id, sample = BOS, []
for pos_id in range(block_size):
logits = gpt(token_id, pos_id, keys, values)
probs = softmax([l / temperature for l in logits])
token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
if token_id == BOS: break
sample.append(uchars[token_id])
print(f"sample {sample_idx+1:2d}: {''.join(sample)}")
Value)package com.anirudhology.microgpt;
import com.anirudhology.microgpt.data.NGramDatasetBuilder;
import com.anirudhology.microgpt.data.TextCorpus;
import com.anirudhology.microgpt.data.TrainingExample;
import com.anirudhology.microgpt.model.BaselineBigramModel;
import com.anirudhology.microgpt.model.GPTLanguageModel;
import com.anirudhology.microgpt.model.MLPLanguageModel;
import com.anirudhology.microgpt.model.NeuralBigramAutogradModel;
import com.anirudhology.microgpt.model.NeuralBigramModel;
import com.anirudhology.microgpt.optimizer.AdamOptimizer;
import com.anirudhology.microgpt.tokenizer.CharacterTokenizer;
import java.util.Collections;
import java.util.List;
import java.util.Random;
/**
* Runs each model in sequence, from simplest to most powerful.
*
* Step 1: Statistical Bigram - pure counting, no neural network
* Step 2: Neural Bigram - same task, learned weights, manual gradients
* Step 3: Neural Bigram (Autograd) - same model, automatic differentiation via Value
* Step 4: MLP Language Model - wider context window, embeddings, hidden layer
* Step 5: GPT Transformer - multi-head attention, transformer blocks, Adam
*/
public class Runner {
static void main() {
TextCorpus textCorpus = new TextCorpus();
final List<String> docs = textCorpus.readCorpus("input.txt");
CharacterTokenizer tokenizer = new CharacterTokenizer();
tokenizer.buildVocabulary(docs);
int split = (int) (docs.size() * 0.9);
final List<String> trainDocs = docs.subList(0, split);
final List<String> validationDocs = docs.subList(split, docs.size());
printStep(1, "Statistical Bigram",
"Count character pair frequencies. No learning, no gradients. Pure statistics.");
runBaselineBigram(tokenizer, docs);
printStep(2, "Neural Bigram (manual gradients)",
"Replace the count table with a learned weight matrix.\n" +
" Gradients computed by hand: dL/dlogit = p - 1_correct.");
runNeuralBigram(tokenizer, trainDocs, validationDocs);
printStep(3, "Neural Bigram (autograd)",
"Same model, but gradients computed automatically by the Value graph.");
runNeuralAutogradBigram(tokenizer, trainDocs, validationDocs);
printStep(4, "MLP Language Model",
"Wider context window (3 chars). Token + positional embeddings flattened\n" +
" into a hidden layer with tanh activation.");
runMLPLanguageModel(tokenizer, docs);
printStep(5, "GPT Transformer (single-head attention)",
"CausalSelfAttention: one attention head over the full embedding dimension.");
runGPTLanguageModel(tokenizer, docs, false);
printStep(6, "GPT Transformer (multi-head attention)",
"MultiHeadCausalSelfAttention: splits embedding into 4 heads.");
runGPTLanguageModel(tokenizer, docs, true);
}
private static void runBaselineBigram(CharacterTokenizer tokenizer, List<String> docs) {
BaselineBigramModel model = new BaselineBigramModel(tokenizer.getVocabularySize(), tokenizer.getBOSId(), 42L);
model.fit(docs, tokenizer, 1.0);
double nll = model.averageNegativeLogLikelihood(docs, tokenizer);
System.out.printf("Avg NLL: %.4f%n", nll);
for (int i = 0; i < 10; i++)
System.out.printf("Sample %2d: %s%n", i + 1, model.sample(tokenizer, 16));
}
private static void runNeuralBigram(CharacterTokenizer tokenizer,
List<String> trainDocs, List<String> validationDocs) {
NeuralBigramModel model = new NeuralBigramModel(tokenizer.getVocabularySize(), tokenizer.getBOSId(), 42L);
double learningRate = 0.5;
for (int e = 1; e <= 30; e++) {
double trainNll = model.trainEpoch(trainDocs, tokenizer, learningRate, 1000L + e);
double valNll = model.averageNegativeLogLikelihood(validationDocs, tokenizer);
System.out.printf("Epoch %2d | Train NLL: %.4f | Val NLL: %.4f%n", e, trainNll, valNll);
learningRate *= 0.98;
}
for (int i = 0; i < 10; i++)
System.out.printf("Sample %2d: %s%n", i + 1, model.sample(tokenizer, 16, 0.9));
}
private static void runNeuralAutogradBigram(CharacterTokenizer tokenizer,
List<String> trainDocs, List<String> validationDocs) {
NeuralBigramAutogradModel model = new NeuralBigramAutogradModel(
tokenizer.getVocabularySize(), tokenizer.getBOSId(), 42L);
double learningRate = 0.5;
for (int e = 0; e < 20; e++) {
double trainNll = model.train(trainDocs, tokenizer, learningRate, 1000L + e);
double valNll = model.averageNegativeLogLikelihood(validationDocs, tokenizer);
System.out.printf("Epoch %2d | Train NLL: %.4f | Val NLL: %.4f%n", e, trainNll, valNll);
learningRate *= 0.98;
}
for (int i = 0; i < 10; i++)
System.out.printf("Sample %2d: %s%n", i + 1, model.sample(tokenizer, 16, 0.9));
}
private static void runMLPLanguageModel(CharacterTokenizer tokenizer, List<String> documents) {
final int blockSize = 3;
final List<TrainingExample> examples = NGramDatasetBuilder.build(documents, tokenizer, blockSize, true);
final MLPLanguageModel model = new MLPLanguageModel(
tokenizer.getVocabularySize(), blockSize, 10, 100, 42L);
double learningRate = 0.01;
for (int epoch = 0; epoch < 10; epoch++) {
double totalLoss = 0.0;
Collections.shuffle(examples, new Random(42L + epoch));
for (int i = 0; i < examples.size(); i++) {
totalLoss += model.trainStep(examples.get(i), learningRate, true);
if ((i + 1) % 1000 == 0)
System.out.printf("Epoch %d, Step %d/%d, Avg Loss: %.4f%n",
epoch + 1, i + 1, examples.size(), totalLoss / (i + 1));
}
learningRate *= 0.9;
}
for (int i = 0; i < 10; i++)
System.out.printf("Sample %2d: %s%n", i + 1, model.generate(tokenizer, 20, 1.0));
}
private static void runGPTLanguageModel(CharacterTokenizer tokenizer,
List<String> documents, boolean useMultiHead) {
final int blockSize = 16, embeddingDimension = 16, numHeads = 4, numberOfLayers = 1;
final List<TrainingExample> examples = NGramDatasetBuilder.build(documents, tokenizer, blockSize, true);
final GPTLanguageModel model = new GPTLanguageModel(
tokenizer.getVocabularySize(), blockSize, embeddingDimension,
numHeads, numberOfLayers, useMultiHead, 42L);
final AdamOptimizer optimizer = new AdamOptimizer(model.parameters());
double initialLearningRate = 0.01;
for (int step = 0; step < 1000; step++) {
double learningRate = initialLearningRate * (1.0 - (double) step / 1000);
optimizer.zeroGradient(model.parameters());
double loss = model.trainStep(examples.get(step % examples.size()));
optimizer.step(model.parameters(), learningRate);
if ((step + 1) % 100 == 0)
System.out.printf("Step %4d / 1000 | Loss: %.4f | LR: %.6f%n", step + 1, loss, learningRate);
}
for (int i = 0; i < 10; i++)
System.out.printf("Sample %2d: %s%n", i + 1, model.generate(tokenizer, 20, 0.5));
}
private static void printStep(int step, String title, String description) {
System.out.println("\n" + "=".repeat(70));
System.out.printf(" Step %d: %s%n", step, title);
System.out.println("=".repeat(70));
System.out.println(" " + description);
System.out.println("-".repeat(70));
}
}
#!/usr/bin/env ruby
# frozen_string_literal: true
# nanogpt — A Ruby port of Karpathy's nanoGPT
# Source: https://github.com/khasinski/nanogpt-rb
require "nano_gpt"
class NanoGPTCLI
COMMANDS = %w[prepare train sample bench version help].freeze
def initialize(args)
@command = args.shift
@args = args
end
def run
case @command
when "prepare" then prepare
when "train" then train
when "sample" then sample
when "bench" then bench
when "version", "-v", "--version" then version
when "help", "-h", "--help", nil then help
else
puts "Unknown command: #{@command}"
help; exit 1
end
end
private
def train
config = NanoGPT::TrainConfig.load(@args)
config[:device] = NanoGPT::Device.auto if config[:device] == "auto"
data_dir = File.join("data", config[:dataset])
tokenizer = NanoGPT::Tokenizer.for_dataset(data_dir)
model_config = NanoGPT::GPTConfig.new(
block_size: config[:block_size],
vocab_size: tokenizer.vocab_size,
n_layer: config[:n_layer],
n_head: config[:n_head],
n_embd: config[:n_embd],
dropout: config[:dropout],
bias: config[:bias]
)
model = NanoGPT::GPT.new(model_config)
model.to(config[:device]) if config[:device] != "cpu"
data_loader = NanoGPT::DataLoader.new(
data_dir: data_dir,
block_size: config[:block_size],
batch_size: config[:batch_size],
device: config[:device]
)
NanoGPT::Trainer.new(model: model, data_loader: data_loader, config: config.to_h).train
puts "\nTraining complete! Checkpoint saved to #{config[:out_dir]}/ckpt.pt"
end
def sample
config = NanoGPT::SampleConfig.load(@args)
config[:device] = NanoGPT::Device.auto if config[:device] == "auto"
Torch.manual_seed(config[:seed])
checkpoint = Torch.load(File.join(config[:out_dir], "ckpt.pt"))
model_config = NanoGPT::GPTConfig.new(**checkpoint["model_args"].transform_keys(&:to_sym))
model = NanoGPT::GPT.new(model_config)
model.load_state_dict(checkpoint["model"])
model.to(config[:device]) if config[:device] != "cpu"
model.eval
tokenizer = NanoGPT::Tokenizer.for_dataset(File.join("data", config[:dataset]))
start_text = config[:start]
start_text = File.read(start_text[5..]) if start_text.start_with?("FILE:")
x = Torch.tensor([tokenizer.encode(start_text)], dtype: :long, device: config[:device])
config[:num_samples].times do
y = model.generate(x, config[:max_new_tokens], temperature: config[:temperature], top_k: config[:top_k])
output = tokenizer.decode(y[0].to_a)
puts output
puts "-" * 50
end
end
def bench
config = NanoGPT::BenchConfig.load(@args)
config[:device] = NanoGPT::Device.auto if config[:device] == "auto"
Torch.manual_seed(config[:seed])
model_config = NanoGPT::GPTConfig.new(
block_size: config[:block_size], vocab_size: 50304,
n_layer: config[:n_layer], n_head: config[:n_head], n_embd: config[:n_embd],
dropout: config[:dropout], bias: config[:bias]
)
model = NanoGPT::GPT.new(model_config)
model.to(config[:device]) if config[:device] != "cpu"
optimizer = model.configure_optimizers(
weight_decay: 1e-2, learning_rate: 1e-4, betas: [0.9, 0.95],
device_type: NanoGPT::Device.type(config[:device])
)
get_batch = lambda do
x = Torch.randint(50304, [config[:batch_size], config[:block_size]], dtype: :long)
y = Torch.randint(50304, [config[:batch_size], config[:block_size]], dtype: :long)
[x, y]
end
[{ name: "burn-in", steps: 10 }, { name: "benchmark", steps: 20 }].each do |phase|
puts "\nPhase: #{phase[:name]}"
x, y = get_batch.call
t0 = Time.now
phase[:steps].times do |k|
_logits, loss = model.forward(x, targets: y)
x, y = get_batch.call
optimizer.zero_grad; loss.backward; optimizer.step
puts " #{k}/#{phase[:steps]} loss: #{format('%.4f', loss.item)}"
end
if phase[:name] == "benchmark"
dt = Time.now - t0
mfu = model.estimate_mfu(config[:batch_size] * phase[:steps], dt)
puts "\nTime/iter: #{format('%.2f', dt / phase[:steps] * 1000)}ms | MFU: #{format('%.2f', mfu * 100)}%"
end
end
end
def prepare
dataset = @args.first || (puts "Usage: nanogpt prepare <dataset>"; exit 1)
load File.join(File.dirname(__FILE__), "..", "data", dataset, "prepare.rb")
end
def version
puts "nanogpt #{NanoGPT::VERSION}"
end
def help
puts <<~HELP
nanogpt — Ruby port of Karpathy's nanoGPT
https://github.com/khasinski/nanogpt-rb
Commands:
prepare <dataset> Download and prepare a dataset
train Train a GPT model
sample Generate text from a trained model
bench Run performance benchmarks
version Show version
help Show this help
HELP
end
end
NanoGPTCLI.new(ARGV).run
nanogpt)