Predicting the Next Character
The corpus, the shifted target, and the loss you have to beat
A language model does one thing: given some text, produce a probability distribution over what comes next. Everything else people do with them is built on that one operation repeated.
The corpus
def build_corpus(seed=0, sentences=500):
"""A small language with real grammar, generated from a seed.
Written into the page rather than downloaded, so the week runs
offline and everybody trains on identical characters. It is
regular enough that a model can genuinely learn it, and long
enough that counting pairs of characters cannot."""
g = torch.Generator().manual_seed(seed)
def pick(xs):
return xs[int(torch.randint(len(xs), (1,), generator=g))]
subject = ['the model', 'the network', 'the optimiser',
'the gradient', 'the loss', 'attention',
'the encoder', 'the decoder', 'each layer',
'the batch', 'the learning rate', 'dropout']
verb = ['reads', 'predicts', 'updates', 'normalises',
'averages', 'scores', 'reduces', 'controls',
'stabilises', 'copies']
obj = ['the sequence', 'the next token', 'every position',
'the weights', 'the hidden state', 'the input',
'the residual path', 'the attention weights',
'the training data', 'the validation set']
tail = ['during training', 'at every step', 'in a single pass',
'before the softmax', 'after the residual connection',
'without a mask', 'across the batch', 'once per epoch']
joiner = [', and ', ', so ', ', because ', '. ', '. ', '. ']
out = []
for _ in range(sentences):
out.append('%s %s %s %s%s'
% (pick(subject), pick(verb), pick(obj),
pick(tail), pick(joiner)))
return ''.join(out)
CORPUS = build_corpus()
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
text = CORPUS.strip()
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for c, i in stoi.items()}
VOCAB = len(chars)
def encode(s):
return torch.tensor([stoi[c] for c in s], dtype=torch.long)
def decode(t):
return ''.join(itos[int(i)] for i in t)
data = encode(text)
BLOCK_SIZE = 32
def windows(seq, block):
"""Every position is a training example: x is block characters,
y is the same block shifted one to the right."""
starts = range(0, len(seq) - block - 1, 4) # stride 4
x = torch.stack([seq[i:i + block] for i in starts])
y = torch.stack([seq[i + 1:i + block + 1] for i in starts])
return x, y
split = int(len(data) * 0.9)
X_tr, Y_tr = windows(data[:split], BLOCK_SIZE)
X_va, Y_va = windows(data[split:], BLOCK_SIZE)
train_loader = DataLoader(TensorDataset(X_tr, Y_tr), batch_size=64,
shuffle=True)
print('characters %d' % len(text))
print('vocabulary %d: %s' % (VOCAB, ''.join(chars)))
print()
print(text[:180])
print()
print('encoded:', encode('the model')[:12].tolist())
print('decoded:', decode(encode('the model')))
vocabulary 27: ,.abcdefghiklmnopqrstuvwxy
each layer copies the weights during training, so the gradient controls the weights at every step. attention reduces the residual path once per epoch. each layer reduces the traini
encoded: [21, 10, 7, 0, 14, 16, 6, 7, 13]
decoded: the model
Character level, and why it is the right place to start
A vocabulary of a few dozen characters means no tokeniser to explain, no unknown tokens, and a model small enough to train in seconds. Everything about the training loop, the mask and the sampling is identical to a model with a fifty thousand token vocabulary. Day 5 covers what changes when you move to subword tokens, and the answer is less than you would expect.
Every position is a training example
def build_corpus(seed=0, sentences=500):
"""A small language with real grammar, generated from a seed.
Written into the page rather than downloaded, so the week runs
offline and everybody trains on identical characters. It is
regular enough that a model can genuinely learn it, and long
enough that counting pairs of characters cannot."""
g = torch.Generator().manual_seed(seed)
def pick(xs):
return xs[int(torch.randint(len(xs), (1,), generator=g))]
subject = ['the model', 'the network', 'the optimiser',
'the gradient', 'the loss', 'attention',
'the encoder', 'the decoder', 'each layer',
'the batch', 'the learning rate', 'dropout']
verb = ['reads', 'predicts', 'updates', 'normalises',
'averages', 'scores', 'reduces', 'controls',
'stabilises', 'copies']
obj = ['the sequence', 'the next token', 'every position',
'the weights', 'the hidden state', 'the input',
'the residual path', 'the attention weights',
'the training data', 'the validation set']
tail = ['during training', 'at every step', 'in a single pass',
'before the softmax', 'after the residual connection',
'without a mask', 'across the batch', 'once per epoch']
joiner = [', and ', ', so ', ', because ', '. ', '. ', '. ']
out = []
for _ in range(sentences):
out.append('%s %s %s %s%s'
% (pick(subject), pick(verb), pick(obj),
pick(tail), pick(joiner)))
return ''.join(out)
CORPUS = build_corpus()
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
text = CORPUS.strip()
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for c, i in stoi.items()}
VOCAB = len(chars)
def encode(s):
return torch.tensor([stoi[c] for c in s], dtype=torch.long)
def decode(t):
return ''.join(itos[int(i)] for i in t)
data = encode(text)
BLOCK_SIZE = 32
def windows(seq, block):
"""Every position is a training example: x is block characters,
y is the same block shifted one to the right."""
starts = range(0, len(seq) - block - 1, 4) # stride 4
x = torch.stack([seq[i:i + block] for i in starts])
y = torch.stack([seq[i + 1:i + block + 1] for i in starts])
return x, y
split = int(len(data) * 0.9)
X_tr, Y_tr = windows(data[:split], BLOCK_SIZE)
X_va, Y_va = windows(data[split:], BLOCK_SIZE)
train_loader = DataLoader(TensorDataset(X_tr, Y_tr), batch_size=64,
shuffle=True)
x, y = X_tr[0], Y_tr[0]
print('input :', repr(decode(x[:32])))
print('target:', repr(decode(y[:32])))
print('\nthe target is the input shifted one place left.')
print()
for i in range(5):
print('given %-22r predict %r'
% (decode(x[:i + 1]), decode(y[i:i + 1])))
print('\n%d windows of %d characters, so %d predictions per epoch'
% (len(X_tr), BLOCK_SIZE, len(X_tr) * BLOCK_SIZE))
target: 'ach layer copies the weights dur'
the target is the input shifted one place left.
given 'e' predict 'a'
given 'ea' predict 'c'
given 'eac' predict 'h'
given 'each' predict ' '
given 'each ' predict 'l'
6413 windows of 32 characters, so 205216 predictions per epoch
What a good loss looks like
def build_corpus(seed=0, sentences=500):
"""A small language with real grammar, generated from a seed.
Written into the page rather than downloaded, so the week runs
offline and everybody trains on identical characters. It is
regular enough that a model can genuinely learn it, and long
enough that counting pairs of characters cannot."""
g = torch.Generator().manual_seed(seed)
def pick(xs):
return xs[int(torch.randint(len(xs), (1,), generator=g))]
subject = ['the model', 'the network', 'the optimiser',
'the gradient', 'the loss', 'attention',
'the encoder', 'the decoder', 'each layer',
'the batch', 'the learning rate', 'dropout']
verb = ['reads', 'predicts', 'updates', 'normalises',
'averages', 'scores', 'reduces', 'controls',
'stabilises', 'copies']
obj = ['the sequence', 'the next token', 'every position',
'the weights', 'the hidden state', 'the input',
'the residual path', 'the attention weights',
'the training data', 'the validation set']
tail = ['during training', 'at every step', 'in a single pass',
'before the softmax', 'after the residual connection',
'without a mask', 'across the batch', 'once per epoch']
joiner = [', and ', ', so ', ', because ', '. ', '. ', '. ']
out = []
for _ in range(sentences):
out.append('%s %s %s %s%s'
% (pick(subject), pick(verb), pick(obj),
pick(tail), pick(joiner)))
return ''.join(out)
CORPUS = build_corpus()
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
text = CORPUS.strip()
chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for c, i in stoi.items()}
VOCAB = len(chars)
def encode(s):
return torch.tensor([stoi[c] for c in s], dtype=torch.long)
def decode(t):
return ''.join(itos[int(i)] for i in t)
data = encode(text)
BLOCK_SIZE = 32
def windows(seq, block):
"""Every position is a training example: x is block characters,
y is the same block shifted one to the right."""
starts = range(0, len(seq) - block - 1, 4) # stride 4
x = torch.stack([seq[i:i + block] for i in starts])
y = torch.stack([seq[i + 1:i + block + 1] for i in starts])
return x, y
split = int(len(data) * 0.9)
X_tr, Y_tr = windows(data[:split], BLOCK_SIZE)
X_va, Y_va = windows(data[split:], BLOCK_SIZE)
train_loader = DataLoader(TensorDataset(X_tr, Y_tr), batch_size=64,
shuffle=True)
import math
from collections import Counter
counts = Counter(text)
total = sum(counts.values())
uniform = math.log(VOCAB)
unigram = -sum((n / total) * math.log(n / total) for n in counts.values())
print('loss if the model guesses uniformly %.4f' % uniform)
print('loss from character frequencies alone %.4f' % unigram)
print('\nperplexity is exp(loss), the effective number of choices:')
print(' uniform %.1f' % math.exp(uniform))
print(' unigram %.1f' % math.exp(unigram))
print('\nanything above the unigram number means the model has not')
print('learned even which letters are common.')
loss from character frequencies alone 2.8138
perplexity is exp(loss), the effective number of choices:
uniform 27.0
unigram 16.7
anything above the unigram number means the model has not
learned even which letters are common.