Time traveling to the past to implement a simple character-level RNN. We'll write the forward pass, backpropagation through time (BPTT), and gradient descent algorithm from scratch without the help of Pytorch. Yes, you'll have to manually calculate the gradients. It will be worth it. After this, we'll train it on some small text datasets like quotes from Steve Jobs :).
The only packages we need are numpy and tqdm.
poetry init
poetry shell
poetry installTo build a dataset, make a txt file. Every sequence will be seperated by a newline.
Snippet from ./data/stevejobs.txt. In the example below, there are 2 items.
"Remembering that you are going to die is the best way I know to avoid the trap of thinking you have something to lose. You are already naked. There is no reason not to follow your heart."
― Steve Jobs
"Being the richest man in the cemetery doesn't matter to me. Going to bed at night saying we've done something wonderful... that's what matters to me."
― Steve Jobs
...Train and sample from RNN.
Training on Steve Jobs dataset for 100000 iterations at lr = 1e-1 with sequence length s. Weights will be saved to ./weights.pkl. Checkpoints every 1000 steps and generates sequences unconditionally at temperature 0.5.
python3 train.py \
-d "./data/stevejobs.txt" \
-i 100000 \
-lr 1e-1 \
-s 25 \
-sp "./weights.pkl" \
-vs 1000 \
-vt 0.5 \Load weights and generate 3 samples unconditionally.
python3 inference.py -w ./weights.pkl -n 3For those interested in how the forward and backward pass works.
RNNs are a type of neural net that operate on sequences. They use a hidden state vector and an input vector to predict an output vector (ie. probabilities over tokens) and the next hidden state. The next hidden state is used to predict the next token, so on and so forth. The recurrence relationship, where the next hidden state depends on the previous hidden state gives them the name Recurrent Neural Nets.
RNNs have a simple API. They take in a hidden state vector and an input vector to produce an output vector and the next hidden state.
Weights and Bias Matrices
Input Vector
Hidden State Vector
Foward Pass
We have all the ingredients for the forward pass! Our choice of activation is tanh and softmax. Tanh squeezes the activations between -1 and 1 and softmax gives us output probabilities.
After the forward pass, we'll compute the loss and gradients of the weight and bias matrices. Then do gradient descent.
We use cross entropy loss between the predicted token probability and the target token across all time steps.
Source: https://phillipi.github.io/6.882/2020/notes/6.036_notes.pdf
The hardest part is keeping track of matrix shapes. Do multiply the shapes of the partials to check if the shapes make sense. Don't memorize. You can derive everything from first principles just by following the RNN section of the textbook above. Phillip Isola does an amazing job at explaining how to implement BPTT and where the gradients come from.
Hopefully you'll come to the same result! My derivation is slightly different because I use row vectors instead of column vectors
Just peel back each layer and apply the chain rule.
https://cs231n.github.io/neural-networks-case-study/#grad
Gradient of the
Gradient of the
This is the most tricky layer. Let's define some terms which will be useful.
We'll be taking gradients of the future loss with respect to a hidden state. The future loss is defined as follows. Note the recurrence in the definition (we can write
Gradient of Future Loss w.r.t hidden state
Note that
Also note that
Gradient of Loss w.r.t hidden state
Gradient of hidden state w.r.t to its input before activation $z_t^h$
Really this is a
Gradient of $F_{t-1}$ w.r.t hidden state
Everything is going to come together nicely now. This is just the sum of 2 gradients we defined above.
Gradient of hidden state weight matrices
Let's calculate them now!
Gradient of hidden state bias vector
At timestep
Let's apply the chain rule to
Simplify.
We now have everything we need to implement backprop. In rnn.py we will translate all of this to code.
We'll implement a version of gradient descent called Adagrad. One of the common problems training RNNs are the exploding vanishing gradients. Adagrad adapts our learning rate so that we take smaller steps when the gradients are big and bigger steps when gradients are small. This improves our training stability significantly. In fact, using vanilla stochastic gradient descent, training does not converge.
We implement this in train.py.
Would not have made it without these. Read them.
- https://phillipi.github.io/6.882/2020/notes/6.036_notes.pdf
- https://stanford.edu/~shervine/teaching/cs-230/cheatsheet-recurrent-neural-networks
- https://karpathy.github.io/2015/05/21/rnn-effectiveness/
- https://gist.github.com/karpathy/d4dee566867f8291f086
- https://cs231n.github.io/neural-networks-case-study/#grad
- https://explained.ai/matrix-calculus/
- https://www.youtube.com/watch?v=0XdPIqi0qpg
