JA EN
LearnHow Transformers Work
·★ MEMBER·PAPER·12 min read

Build Your Own Mini GPT — A Language Model in 300 Lines

Write a character-level GPT in PyTorch from an empty file: tokenizer, causally masked self-attention, training loop, and temperature sampling — then watch Shakespeare's formatting emerge from nothing but next-character prediction.

ModalitytextTasknlp

Attention Is All You Need

Primary source — what this article is built on

undefined2017-06-12undefined2026-08-279y 2mo later

Attention Is All You NeedAshish Vaswani, Noam Shazeer, Niki Parmar et al. · 2017-06-12 · v7arXiv:1706.03762Paper page·PDF
undefined

The dominant sequence transduction models are based on complex recurrent or convolutional neural networks in an encoder-decoder configuration. The best performing models also connect the encoder and decoder through an attention mechanism. We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely. Experiments on two machine translation tasks show these models to be superior in quality while being more parallelizable and requiring significantly less time to train. Our model achieves 28.4 BLEU on the WMT 2014 English-to-German translation task, improving over the existing best results, including ensembles by over 2 BLEU. On the WMT 2014 English-to-French translation task, our model establishes a new single-model state-of-the-art BLEU score of 41.8 after training for 3.5 days on eight GPUs, a small fraction of the training costs of the best models from the literature. We show that the Transformer generalizes well to other tasks by applying it successfully to English constituency parsing both with large and limited training data.


What fits in 300 lines

"Building a GPT" sounds like a story about thousands of GPUs and trillions of tokens. For training a commercial model, it is. But the architecture itself is startlingly small. A tokenizer that turns characters into numbers, embeddings, self-attention, a feed-forward block, a training loop, a generation loop — all of it fits in roughly 300 lines of PyTorch.

What we build here is a character-level GPT: it predicts one character at a time instead of one word. The vocabulary is only a few dozen symbols, so the output layer stays small and the tokenizer takes twenty lines. That leaves all the attention on the Transformer itself. For data we use the complete works of Shakespeare flattened into a single text file — the familiar tiny-shakespeare corpus, on the order of a million characters with about 65 distinct symbols in it.

The goal of this article is not to copy the code. It is to be able to say what lives on each of those 300 lines, and what breaks if you delete any one of them. That is the exact boundary between calling model.generate() from a library and choosing your own settings with reasons behind them.

A GPT is a probability distribution over the next character

Strip everything away and a GPT is one function: it takes the text so far and returns a probability distribution over the next single character. That's all.

Show it "To be or not to b" and it should give "e" a high probability and "z" a low one — a number for every symbol in the vocabulary. Generating text is nothing more than calling that function, sampling one character, appending it, and calling again. The model is not "writing a sentence"; it is being re-invoked once per roll of the dice.

Training means adjusting the weights so that the character that actually came next gets as much probability as possible. The whole objective is one cross-entropy term.

L=1Tt=1Tlogpθ(xtx<t)\mathcal{L} = -\frac{1}{T}\sum_{t=1}^{T} \log p_\theta(x_t \mid x_{<t})
(1)

In words: at every position tt, take the probability the model assigned to the character that actually appeared, take its logarithm, flip the sign, and average. Here x<tx_{<t} means "every character before position tt", θ\theta is the full set of model parameters, and TT is how many characters we look at in one go. Assign probability 1 to the truth and log1=0\log 1 = 0 costs nothing; the more the model underrates the truth, the faster the number climbs.

One efficiency note worth internalising. Pushing a length-TT passage through the model once gives you predictions and losses at all TT positions simultaneously. You are not training one character at a time. That is why next-character prediction can consume enormous amounts of text — and the causal mask we meet later is precisely the trick that makes this simultaneous computation legitimate.

The raw numbers the model emits are called logits, and softmax turns them into probabilities. At generation time we slip a temperature τ\tau into that step:

pi=exp(zi/τ)jexp(zj/τ)p_i = \frac{\exp(z_i/\tau)}{\sum_j \exp(z_j/\tau)}

In words, this just divides each logit ziz_i by τ\tau before the softmax. A small τ\tau exaggerates the largest logits and makes the distribution spiky; a large τ\tau flattens it. That single division is the dial between "safe but dull" and "inventive but incoherent". Try it:

FIG 1Lower the temperature and the distribution collapses onto one bar (the same character every time); raise it and everything flattens into noise. The temperature argument in a generate() call is doing exactly this to the bar chart

The parts list — where the 300 lines go

Component Approx. lines Job
Character tokenizer 20 Map characters ↔ integers
Batch sampler 10 Cut out two tensors offset by one character
Self-attention head 25 Causally masked Q, K, V
Multi-head + MLP 30 Concatenate heads, two-layer feed-forward
Block 15 Residual connections and LayerNorm
The GPT itself 40 Embeddings, stacking, output layer, loss
Training loop 30 AdamW, gradient clipping, evaluation
Generation loop 20 Temperature, top-k, sampling

The rest is imports and configuration. No new concepts appear from here on. What follows is simply filling in that table from the top.

Turning characters into integers

Neural networks only handle numbers, so the first job is assigning an integer to each character. Collect the unique characters from the corpus, sort them, and build two dictionaries.

import torch, torch.nn as nn
from torch.nn import functional as F

text  = open('input.txt', encoding='utf-8').read()
chars = sorted(set(text))                      # about 65 symbols
stoi  = {c: i for i, c in enumerate(chars)}
itos  = {i: c for c, i in stoi.items()}
encode = lambda s: [stoi[c] for c in s]
decode = lambda ids: ''.join(itos[i] for i in ids)

data = torch.tensor(encode(text), dtype=torch.long)
n = int(0.9 * len(data))
train_data, val_data = data[:n], data[n:]

A real GPT swaps this for subword splitting such as BPE, pushing the vocabulary into the tens of thousands. The difference is purely a trade between vocabulary size and sequence length; everything downstream is identical. Character-level keeps the vocabulary tiny, but the same passage becomes a much longer sequence, so the model has to reach further back. How to choose a splitting scheme is covered in Tokenizers from Scratch.

The input is two copies offset by one

This is the part of the implementation people most often misread. The input x and the target y are the same string shifted by a single character.

def get_batch(data, block_size, batch_size):
    ix = torch.randint(len(data) - block_size - 1, (batch_size,))
    x = torch.stack([data[i:i + block_size]         for i in ix])
    y = torch.stack([data[i + 1:i + block_size + 1] for i in ix])
    return x, y

If `x` is "To be or not to b" then `y` is "o be or not to be". At position 0 the model must predict "o" from "T", at position 1 a space from "To", and so on — one batch manufactures `block_size` prediction problems at once. Nobody has to label anything, so any text at all is an endless supply of exercises. That is what

What's behind this

§

Members-only from here

371 walkthroughs, 26 textbook chapters, 48 student units and 6 close readings — all included for $4.99/mo, with three new explainers every day. Cancel any time; access runs to the end of the period.

Already a member? Sign in to keep reading

References

  1. Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit et al.. (2017-06-12) Attention Is All You Need. arXiv:1706.03762Paper page·PDF

This article is written from the source paper above. Where they differ, the original is authoritative.

Comments

Sign in to comment