
π§ Lesson 4 of 12 Β· Build Your Own LLM course
In this article
In Lesson 3 our model was a table of counts. Now we get the same behaviour from a neural network trained with gradient descent β the exact training loop that scales up to GPT. Install PyTorch first (pip install torch; the CPU version is fine).
Prepare the data
“`python
import torch
import torch.nn as nn
import torch.nn.functional as F
text = open(“input.txt”, encoding=”utf-8″).read()
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for c, i in stoi.items()}
data = torch.tensor([stoi[c] for c in text])
V = len(chars)
n = int(0.9 * len(data))
train_data, val_data = data[:n], data[n:]
“`
A bigram model as a neural network
nn.Embedding(V, V) is a trainable VΓV table. Row i holds scores (logits) for which character follows character i β the neural version of our count table.
“`python
class Bigram(nn.Module):
def __init__(self, V):
super().__init__()
self.table = nn.Embedding(V, V)
def forward(self, idx):
return self.table(idx) # (batch, V) logits
def batch(split, size=256):
d = train_data if split == “train” else val_data
ix = torch.randint(0, len(d) – 1, (size,))
return d[ix], d[ix + 1] # inputs, next characters
model = Bigram(V)
opt = torch.optim.AdamW(model.parameters(), lr=1e-2)
for step in range(3001):
x, y = batch(“train”)
loss = F.cross_entropy(model(x), y)
opt.zero_grad()
loss.backward()
opt.step()
if step % 500 == 0:
print(step, round(loss.item(), 3))
“`
The loss starts near log(V) (random) and settles around the same value the counting model reached. That is the point: gradient descent rediscovered the count table. F.cross_entropy is the exact loss we computed by hand in Lesson 3.
What one training step does
- Forward: look up logits for 256 random characters.
- Loss: how much probability went to the true next character.
- Backward:
loss.backward()computes, for every number in the table, which direction would reduce the loss. - Update: the optimiser nudges each number a little in that direction.
Adding real embeddings
Now the important step. Instead of jumping straight from a character to V scores, we first map each character to a short vector β its embedding β then use a linear layer to produce scores.
“`python
class EmbedModel(nn.Module):
def __init__(self, V, d=32):
super().__init__()
self.emb = nn.Embedding(V, d) # each token -> 32 numbers
self.head = nn.Linear(d, V) # 32 numbers -> V scores
def forward(self, idx):
return self.head(self.emb(idx))
“`
Why bother? Because tokens that behave alike end up with similar vectors. The model can then share what it learns: if it learns something about a, it partly applies to e. In a real LLM, the embedding for “Monday” ends up near “Tuesday”, and “VLOOKUP” near “XLOOKUP”.
Looking inside the embeddings
“`python
emb_model = EmbedModel(V)
# … train it with the same loop as above (swap `model` for `emb_model`) …
E = F.normalize(emb_model.emb.weight.detach(), dim=1)
def similar(c, k=5):
sims = E @ E[stoi[c]]
best = sims.argsort(descending=True)[1:k + 1]
return [(itos[i.item()], round(sims[i].item(), 2)) for i in best]
print(similar(“a”)) # often other vowels
print(similar(“5”)) # often other digits
“`
Nobody told the model what a vowel or a digit is. It discovered that these characters appear in similar places, so their vectors drifted together.
Generate text
“`python
@torch.no_grad()
def generate(model, start=”T”, n=300):
idx = torch.tensor([stoi[start]])
out = [start]
for _ in range(n):
probs = F.softmax(model(idx)[-1], dim=-1)
idx = torch.multinomial(probs, 1)
out.append(itos[idx.item()])
return “”.join(out)
print(generate(emb_model))
“`
Still gibberish β each prediction sees only one character. To use more context, tokens need a way to look at each other. That mechanism is attention, next lesson.
Check your understanding
- Validation loss: compute
F.cross_entropyon abatch("val")insidetorch.no_grad(). It should be close to training loss; if it is much higher, the model is memorising. - Parameter count:
sum(p.numel() for p in emb_model.parameters()). With V=65 and d=32 it is only a few thousand. GPT-3 has 175 billion.