%matplotlib inline
from d2l import tensorflow as d2l
import math
import random
import time
import tensorflow as tfAn RNN language model on The Time Machine, over the 1,024-token BPE vocabulary, built twice: from raw tensor ops, then with the framework’s recurrent layer. Four pieces:
Parameters: \mathbf{W}_{xh}, \mathbf{W}_{hh}, \mathbf{b}. Initialize randomly, scaled to keep activations sensible:
class RNNScratch(d2l.Module):
"""The RNN model implemented from scratch."""
def __init__(self, num_inputs, num_hiddens, sigma=0.01):
super().__init__()
self.save_hyperparameters()
self.W_xh = tf.Variable(d2l.normal(
(num_inputs, num_hiddens)) * sigma)
self.W_hh = tf.Variable(d2l.normal(
(num_hiddens, num_hiddens)) * sigma)
self.b_h = tf.Variable(d2l.zeros(num_hiddens))Walk a length-T input one step at a time, carrying the hidden state forward:
@d2l.add_to_class(RNNScratch)
def forward(self, inputs, state=None):
if state is None:
# Initial state with shape: (batch_size, num_hiddens)
state = tf.zeros((tf.shape(inputs)[1], self.num_hiddens))
outputs = []
for X in tf.unstack(inputs): # Shape: (num_steps, batch_size, num_inputs)
state = d2l.tanh(d2l.matmul(X, self.W_xh) +
d2l.matmul(state, self.W_hh) + self.b_h)
outputs.append(state)
return outputs, stateSanity check on output shapes:
def check_len(a, n):
"""Check the length of a list."""
assert len(a) == n, f'list\'s length {len(a)} != expected length {n}'
def check_shape(a, shape):
"""Check the shape of a tensor."""
assert a.shape == shape, \
f'tensor\'s shape {a.shape} != expected shape {shape}'
check_len(outputs, num_steps)
check_shape(outputs[0], (batch_size, num_hiddens))
check_shape(state, (batch_size, num_hiddens))With |\mathcal{V}| = 1{,}024, one-hot inputs waste a 1,024-wide multiply per step on a vector of zeros.
Embedding in, vocab-sized projection out; plot perplexity instead of loss:
class RNNLMScratch(d2l.Classifier):
"""The RNN-based language model implemented from scratch."""
def __init__(self, rnn, vocab_size, lr=0.01):
super().__init__()
self.save_hyperparameters()
self.init_params()
def init_params(self):
self.W_e = tf.Variable(d2l.normal(
(self.vocab_size, self.rnn.num_inputs)))
self.W_hq = tf.Variable(d2l.normal(
(self.rnn.num_hiddens, self.vocab_size)) * self.rnn.sigma)
self.b_q = tf.Variable(d2l.zeros(self.vocab_size))
def _report_train(self, loss):
self.plot('ppl', d2l.exp(loss), train=True)
def _report_val(self, y_hat, batch):
self.plot('ppl', d2l.exp(self.loss(y_hat, batch[-1])), train=False)Project every hidden state through the shared head, then a shape smoke test: (batch, steps) ids in, (batch, steps, vocab) logits out:
@d2l.add_to_class(RNNLMScratch)
def output_layer(self, rnn_outputs):
outputs = [d2l.matmul(H, self.W_hq) + self.b_q for H in rnn_outputs]
return d2l.stack(outputs, 1)
@d2l.add_to_class(RNNLMScratch)
def forward(self, X, state=None):
embs = self.embedding(X)
rnn_outputs, _ = self.rnn(embs, state)
return self.output_layer(rnn_outputs)Backprop through T steps multiplies T Jacobians, one explosion-prone product. Clip the gradient onto a ball of radius \theta before each update:
\mathbf{g} \leftarrow \min\!\left(1, \frac{\theta}{\|\mathbf{g}\|}\right)\mathbf{g}.
@d2l.add_to_class(d2l.Trainer)
def clip_gradients(self, grad_clip_val, grads):
grad_clip_val = tf.constant(grad_clip_val, dtype=tf.float32)
new_grads = [tf.convert_to_tensor(grad) if isinstance(
grad, tf.IndexedSlices) else grad for grad in grads]
norm = tf.math.sqrt(sum((tf.reduce_sum(grad ** 2)) for grad in new_grads))
scale = tf.minimum(1.0, grad_clip_val / norm)
return [grad * scale for grad in new_grads](PyTorch/MXNet: called by fit_epoch; TF: inside the compiled step; JAX: optax.clip_by_global_norm does it inside fit.)
50k windows of 32 BPE tokens, batch 1024, 10 epochs, clip at 1. Fresh zero state per window = truncated BPTT:
validation perplexity 97.5
Val ppl ~90–100 over 1,024 tokens vs. char-level ppl ~7 over 27: not comparable. Convert to bits per byte:
2.78 bytes/token, 2.38 bits per byte
~2.4 bpb beats the char-trigram baseline’s 2.68 bpb: the “worse” perplexity is the better language model.
Warm up on the prefix, then feed each chosen token back in. Greedy (T=0) or temperature sampling:
@d2l.add_to_class(RNNLMScratch)
def predict(self, prefix, num_tokens, tok, device=None, temperature=0.0,
rng=None):
outputs, state = tok.encode(prefix), None
for i in range(len(outputs) - 1): # Warm up on the prefix
X = d2l.tensor([[outputs[i]]])
_, state = self.rnn(self.embedding(X), state)
rng = random.Random() if rng is None else rng
for _ in range(num_tokens): # Generate num_tokens continuation tokens
X = d2l.tensor([[outputs[-1]]])
rnn_outputs, state = self.rnn(self.embedding(X), state)
logits = d2l.numpy(self.output_layer(rnn_outputs))[0, 0]
if temperature == 0:
outputs.append(int(logits.argmax()))
else:
weights = [math.exp(l) for l in
(logits - logits.max()) / temperature]
outputs.append(rng.choices(range(len(weights)), weights)[0])
return tok.decode(outputs)'the time traveller pastesture of the faint and purtening, and I was not seem, and I was not seem, and I was the little people were of the Time Traveller, ...
Greedy is fluent, then circles. Sampling breaks the loop at the price of stranger choices:
the time traveller ' kns. It was very utentationsly and dis Thisationsausereadies. Then Filby, and hisib dist
the time travellerly premipes, and I could not see I had a long'sion had seen in this faster thannesset
Doing better = decoding strategies, later in this chapter.
Same interface as RNNScratch, one fused call:
class RNN(d2l.Module):
"""The RNN model implemented with high-level APIs."""
def __init__(self, num_inputs, num_hiddens):
super().__init__()
self.save_hyperparameters()
self.rnn = tf.keras.layers.SimpleRNN(
num_hiddens, return_sequences=True, return_state=True)
def forward(self, inputs, H=None):
# The keras layer is batch-major: transpose in and out
outputs, H = self.rnn(tf.transpose(inputs, perm=[1, 0, 2]), H)
return tf.transpose(outputs, perm=[1, 0, 2]), HThe LM wrapper is inherited: swap in framework embedding and dense layers:
class RNNLM(d2l.RNNLMScratch):
"""The RNN-based language model implemented with high-level APIs."""
def init_params(self):
self.emb = tf.keras.layers.Embedding(
self.vocab_size, self.rnn.num_inputs,
embeddings_initializer=tf.keras.initializers.RandomNormal(
stddev=1))
self.linear = tf.keras.layers.Dense(self.vocab_size)
def embedding(self, X):
return self.emb(tf.transpose(X))
def output_layer(self, hiddens):
return d2l.transpose(self.linear(hiddens), (1, 0, 2))Untrained model generates byte soup, but the wiring (tokenizer to model and back) is sound:
'it hasaceull�ywardspp doatedyC any so beganhingtain�ars\x08 could'
Same trainer, same data:
perplexity 109.5, "the time travellerre I\nwas decay.\n\n'In a breathing of the little people were sensible. I was not a"
model time (s) val ppl
scratch 57.4 97.5
concise 42.1 109.5
SimpleRNN has no fused GPU kernel; the compiled scratch loop matches it.num_steps tokens.