Transformers with PyTorch
Transformers with PyTorch
The goal of today’s practical is to train a mini-transformer to generate sentences and answer questions about Shakespeare texts. The exercises are based on the original transformers paper: https://proceedings.neurips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf
%pip install datasetsimport datasets
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F1. Tiny Shakespeare
Load the tiny_shakespeare dataset from Hugging Face and tokenize the characters https://huggingface.co/datasets/karpathy/tiny_shakespeare
import torch
from torch.utils.data import TensorDataset, DataLoader
# ---------------------------------------------------------------- load text
# The Hugging Face copy is the primary source. If it is unavailable (no
# network, or a datasets version mismatch) we fall back to the raw .txt file
# so that the rest of the practical still works.
try:
from datasets import load_dataset
ds = load_dataset('karpathy/tiny_shakespeare')
train_text = ds['train'][0]['text']
val_text = ds['validation'][0]['text']
if len(train_text) < 100_000:
raise ValueError('unexpected dataset structure')
print('Loaded from Hugging Face.')
except Exception as e:
print(f'Hugging Face load failed ({type(e).__name__}: {e}).')
print('Falling back to the raw text file.')
import urllib.request
url = ('https://raw.githubusercontent.com/karpathy/char-rnn/'
'master/data/tinyshakespeare/input.txt')
with urllib.request.urlopen(url) as r:
full_text = r.read().decode('utf-8')
split = int(0.9 * len(full_text))
train_text, val_text = full_text[:split], full_text[split:]
print(f'train: {len(train_text):,} characters, validation: {len(val_text):,} characters')
print(train_text[:250])
# ------------------------------------------------------------ character vocab
# The vocabulary is built from the train split only.
vocabulary = sorted(set(train_text))
vocab_size = len(vocabulary)
stoi = {ch: i for i, ch in enumerate(vocabulary)}
itos = {i: ch for i, ch in enumerate(vocabulary)}
unseen = set(val_text) - set(vocabulary)
if unseen:
print(f'Warning: dropping {len(unseen)} character(s) unseen in train: {unseen}')
val_text = ''.join(c for c in val_text if c in stoi)
def encode(s):
"""string -> list of integer token ids"""
return [stoi[c] for c in s]
def decode(ids):
"""iterable of token ids -> string"""
return ''.join(itos[int(i)] for i in ids)
print(f'vocab_size = {vocab_size}')
print('vocabulary:', ''.join(vocabulary).replace('\n', '\\n'))
# ------------------------------------------------- (cur_char, next_char) pairs
seq_len = 100
batch_size = 32
def make_sequences(text, seq_len):
"""Cut `text` into non-overlapping (cur_char, next_char) sequence pairs.
next_char is cur_char shifted one position to the left, so position t of
next_char holds the character the model must predict from position t of
cur_char. Both come back with shape (n_sequences, seq_len).
"""
data = torch.tensor(encode(text), dtype=torch.long)
n_seq = (len(data) - 1) // seq_len # -1 leaves room for the shift
cur_char = data[:n_seq * seq_len].view(n_seq, seq_len)
next_char = data[1:n_seq * seq_len + 1].view(n_seq, seq_len)
return cur_char, next_char
train_x, train_y = make_sequences(train_text, seq_len)
val_x, val_y = make_sequences(val_text, seq_len)
train_loader = DataLoader(TensorDataset(train_x, train_y),
batch_size=batch_size, shuffle=True, drop_last=True)
val_loader = DataLoader(TensorDataset(val_x, val_y),
batch_size=batch_size, shuffle=False)
print(f'train sequences: {tuple(train_x.shape)}, validation: {tuple(val_x.shape)}')
xb, yb = next(iter(train_loader))
print(f'one batch -> cur_char {tuple(xb.shape)}, next_char {tuple(yb.shape)}')
print('cur_char [0][:60]:', repr(decode(xb[0][:60])))
print('next_char[0][:60]:', repr(decode(yb[0][:60])))2. Positional encoding
Fill in the sinusoidal_positional_encoding function as described in the Attention Is All You Need paper
def sinusoidal_positional_encoding(max_len, d_model):
# TODO: create a (max_len, d_model) matrix
# TODO: generate positions 0,1,...,max_len-1
# TODO: compute div_term (the exponential denominator)
# TODO: assign sin to even positions and cos to odd positions
pass3. Scaled Dot-Product Attention
Fill in the dot-product function that is used to compute attention in a transformer
# TODO: Implement scaled dot-product attention
def scaled_dot_product_attention(Q, K, V, mask=None):
# TODO: implement attention
raise NotImplementedError("Implement the scaled dot-product attention")4. Multi-Head Attention
Implement a Multi-Head attention block as described in the Attention Is All You Need paper
class MultiHeadAttention(nn.Module):
def __init__(self, d_model=128, num_heads=8):
super().__init__()
assert d_model % num_heads == 0
self.d_k = d_model // num_heads
self.num_heads = num_heads
# TODO: define linear layers W_q, W_k, W_v, W_o
def forward(self, x):
# TODO: implement multi-head attention forward pass
raise NotImplementedError("Implement the MultiHeadAttention class")5. Transformer Encoder Block
Define a transformer Encoder block that uses the attention layers you defined earlier
class TransformerEncoderBlock(nn.Module):
def __init__(self, d_model=128, num_heads=8, dim_ff=512):
super().__init__()
# TODO: instantiate attention, layer norms, feed-forward block
def forward(self, x):
# TODO: implement encoder block logic
raise NotImplementedError("Implement the TransformerEncoderBlock")6. Mini Transformer
Build a transformer with all the parts you have defined in the previous exercises
class MiniTransformer(nn.Module):
def __init__(self, vocab_size, d_model=128, n_layers=2):
super().__init__()
# TODO: define embeddings, positional encodings, encoder layers, final linear layer
def forward(self, x):
# TODO: implement forward pass
raise NotImplementedError("Implement MiniTransformer")7. Training
Train the transformer with the Shakespeare dataset that you loaded at the beginning
# TODO: write the training code8. Testing the Transformer
Write the code that allows the transformer to give answers based on a string from the user