Supervised Learning

Unsupervised Learning

Vanilla Recurrent Neural Network

Recurrent neural network is a type of network architecture that accepts variable inputs and variable outputs, which contrasts with the vanilla feed-forward neural networks. We can also consider input with variable length, such as video frames and we want to make a decision along every frame of that video.

Process Sequences

sequence

- One-to-one
- This is the classic feed forward neural network architecture, with one input and we expect oneoutput.

- One-to-many
- This can be thought of as image captioning. We have one image as a fixed size input and theoutput can be words or sentences which are variable in length.

- Many-to-one
- This is used for sentiment classification. The input is expected to be a sequence of words oreven paragraphs of words. The output can be a regression output with continuous values whichrepresent the likelihood of having a positive sentiment.

- Many-to-many
- This model is ideal for machine translation like the one we see on Google translate. The inputcould an English sentence which has variable length and the output will be the same sentence ina different language which also has variable length. The last many to many model can be used forvideo classification on frame level. Feed every frame of a video into the neural network andexpect an output right away. However, since frames are generally dependent on each other, it isnecessary for the network to propagate its hidden state from the previous to the next. Thus, weneed recurrent neural network for this kind of task.

Computational Graph

Instead of imagining that hidden state is being *recurrently* fed back into the network, it's easier to visualize the process if we unroll the operation into a computational graph that is composed to many time steps. (*The concept of hidden state and mathematical formulation will be explained in the next section.*)

For example, we begin with a zero'ed vector as our hidden state on the left. We feed it into the network along with our first input. When we receive the next input, we take the new hidden state and feed it into the network again with the second input. The procoess goes on until the point we wish to compute the final output of the network.

computational-graph-1

We use the same set of weight for every time step of the computation.

computational-graph-2

Many-to-many

For the many-to-many case, we compute a

`y[t]`

and the loss for every time step. At the end we simply sum up the loss of all the time steps and count that as our total loss of the network.When we think about the back propagation for this model, we will have a separate gradient for W flowing from each of those time steps and then the final gradient for W will be the sum of all those individual time step gradients. *Imagine that we have some sort of ground-truth label for every step of the sequence*:

computational-graph-many-to-many

Many-to-one

If we have this many to one situation, we make the decision based on the final hidden state of this network. This final hidden state summarizes all of the context from the entire sequence.

computational-graph-many-to-one

One-to-many

If we have this one to many situation, where we want to receive a fixed size input and produce a variable length output, then you would commonly use that fixed size input to initialize the hidden state and then let the network to propagate and evolve the hidden state forward.

computational-graph-one-to-many

Squence to Sequence

For the sequence to sequence models where you might want to do something like machine translation, this is a combination of **many-to-one** and **one-to-many** architecture. We proceed in two stages, (1) the encoder receives a variably sized input like an english sentence and performs encoding into a hidden state vector, (2) the decoder receives the hidden state vector and produces a variably sized output. The motivation of using this architecture is modularity. We can easily swap out encoder and decoder for different type of language translation.

Mathematical Formulation

We can process a sequence of vectors **x** applying a recurrence formula at every time step:

$h_{t} = f_{W}(h_{t - 1}, x_{t})$

Time step of an input vector is represented by **W**.

`x[t]`

and time step of a hidden state is represented by `h[t]`

. Thus we can think of `h[t - 1]`

as the previous hidden state. The production of hidden state is simply a matrix muplitication of input and hidden state by some weights Forward Propagation Example

Here's a simple one-to-many vanilla recurrent neural network example in functional form. If we were to produce

`h[t]`

, we need some weight matrices, `h[t-1]`

, `x[t]`

and a non-linearity `tanh`

.$h_{t} = tanh(W_{hh}h_{t-1} + W_{xh}x_{t} + B_{h})$

Since this is a **one-to-many** network, we'd want to produce an output

`y[t]`

at every timestep, thus, we need another weight matrix that accepts a hidden state and project it to an output.$y_{t} = W_{hy}h_{t} + B_{y}$

1

import numpy as np

2

â€‹

3

â€‹

4

np.random.seed(0)

5

class RecurrentNetwork(object):

6

"""When we say W_hh, it means a weight matrix that accepts a hidden state and produce a new hidden state.

7

Similarly, W_xh represents a weight matrix that accepts an input vector and produce a new hidden state. This

8

notation can get messy as we get more variables later on with LSTM and I simplify the notation a little bit in

9

LSTM notes.

10

"""

11

def __init__(self):

12

self.hidden_state = np.zeros((3, 3))

13

self.W_hh = np.random.randn(3, 3)

14

self.W_xh = np.random.randn(3, 3)

15

self.W_hy = np.random.randn(3, 3)

16

self.Bh = np.random.randn(3,)

17

self.By = np.random.rand(3,)

18

â€‹

19

def forward_prop(self, x):

20

# The order of which you do dot product is entirely up to you. The gradient updates will take care itself

21

# as long as the matrix dimension matches up.

22

self.hidden_state = np.tanh(np.dot(self.hidden_state, self.W_hh) + np.dot(x, self.W_xh) + self.Bh)

23

â€‹

24

return self.W_hy.dot(self.hidden_state) + self.By

Copied!

1

input_vector = np.ones((3, 3))

2

silly_network = RecurrentNetwork()

3

â€‹

4

# Notice that same input, but leads to different ouptut at every single time step.

5

print silly_network.forward_prop(input_vector)

6

print silly_network.forward_prop(input_vector)

7

print silly_network.forward_prop(input_vector)

Copied!

1

[[-1.73665315 -2.40366542 -2.72344361]

2

[ 1.61591482 1.45557046 1.13262256]

3

[ 1.68977504 1.54059305 1.21757531]]

4

[[-2.15023381 -2.41205828 -2.71701457]

5

[ 1.71962883 1.45767515 1.13101034]

6

[ 1.80488553 1.542929 1.21578594]]

7

[[-2.15024751 -2.41207375 -2.720968 ]

8

[ 1.71963227 1.45767903 1.13200175]

9

[ 1.80488935 1.54293331 1.21688628]]

Copied!

Back Propagation Example

Using softmax loss and gradient of softmax loss for every time step, we can derive

`grad_y`

. Now we are tasked with calculating the following gradients:$\frac{\partial L}{\partial W_{hy}} \;, \frac{\partial L}{\partial W_{By}} \;,
\frac{\partial L}{\partial h_{t}} \;, \frac{\partial L}{\partial B_{h}} \;
\frac{\partial L}{\partial W_{hh}} \;, \frac{\partial L}{\partial W_{xh}} \;$

Please look at Character-level Language Model below for detailed backprop example

For recurrent neural network, we are essentially backpropagation through time, which means that we are forwarding through entire sequence to compute losses, then backwarding through entire sequence to compute gradients.

However, this becomes problematic when we want to train a sequence that is very long. For example, if we were to train a a paragraph of words, we have to iterate through many layers before we can compute one simple gradient step. In practice, what people do is an approximation called **truncated backpropagation** through time. Run forward and backward through chunks of the sequence instead of the whole sequence.

Even though our input sequence can potentially be very long or even infinite, when we are training our model, we will step forward for some number of steps and compute a loss only over this sub sequence of the data. Then backpropagate through this sub-sequence and make a gradient step on the weights. When we move to the next batch, we still have this hidden state from the previous batch of data, we will carry this hidden state forward. The forward pass is unaffected but we will only backpropgate again through this second batch.

truncated-backprop

Character-level Language Model

Training Time

Suppose that we have a character-level language model, the list of possible *vocabularies* is

`['h', 'e', 'l', 'o']`

. An example training sequence is `hello`

. The same output from hidden layer is being fed to output layer and the next hidden layer, as noted below that `y[t]`

is a product of `W_hy`

and `h[t]`

. Since we know what we are expecting, we can backpropagate the cost and update weights.The

`y[t]`

is a prediction for which letter is most likely to come next. For example, when we feed `h`

into the network, `e`

is the expected output of the network because the only training example we have is `hello`

.language-model

Test Time

At test time, we sample characters one at a time and feed it back to the model to produce a whole sequence of characters (which makes up a word.) We seed the word with a prefix like the letter **h** in this case. The output is a softmax vector which represents probability. We can use it as a probability distribution and perform sampling.

language-model-test-time

Implementation: Minimal character-level Vanilla RNN model

Let's use the same

`tanh`

example we had up there to implement a single layer recurrent nerual network. The forward pass is quite easy. Assuming the input is a list of character index, i.e. `a => 0`

, `b => 1`

, etc..., the target is a list of character index that represents the next letter in the sequence. For example, the target is characters of the word `ensorflow`

and the input is `tensorflo`

. Given a letter `t`

, it should predict that next letter is `e`

.ForwardProp

1

# Encode input state in 1-of-k representation

2

input_states[t] = np.zeros((self.input_dim, 1))

3

input_states[t][input_list[t]] = 1

4

â€‹

5

# Compute hidden state

6

hidden_states[t] = tanh(dot(self.params['Wxh'], input_states[t]) +

7

dot(self.params['Whh'], hidden_states[t-1]) +

8

self.params['Bh'])

9

â€‹

10

# Compute output state a.k.a. unnomralized log probability using current hidden state

11

output_states[t] = dot(self.params['Why'], hidden_states[t]) + self.params['By']

12

â€‹

13

# Compute softmax probability state using the output state

14

prob_states[t] = exp(output_states[t]) / np.sum(exp(output_states[t]))

Copied!

BackProp

Now here's the fun part, computing the gradients for backpropagation. First of all, let's remind ourself what our model is.

$h_{t} = tanh(W_{hh}h_{t-1} + W_{xh}x_{t} + B_{h})$

$y_{t} = W_{hy}h_{t} + B_{y}$

First compute the gradient of loss with respect to output vector

`y`

:$\frac{\partial L}{\partial y_{t}}$

1

# Softmax gradient

2

grad_output = np.copy(prob_states[t])

3

grad_output[target_list[t]] -= 1

Copied!

Then gradient of loss with respect to

`Why`

, `h`

, and the bias:$\frac{\partial L}{\partial W_{hy}} = \frac{\partial L}{\partial y_{t}} \cdot \frac{\partial y_{t}}{\partial W_{hy}}$

$\frac{\partial L}{\partial B_{y}} = \frac{\partial L}{\partial y_{t}} \cdot \frac{\partial y_{t}}{\partial B_{y}}$

$\frac{\partial L}{\partial h_{t}} = \frac{\partial L}{\partial y_{t}} \cdot \frac{\partial y_{t}}{\partial h_{t}}$

1

grads['Why'] += dot(grad_output, hidden_states[t].T)

2

grads['By'] += grad_output

3

grad_h = dot(self.params['Why'].T, grad_output) + grad_prev_h # (H, O)(O, H) => (H, H)

Copied!

We need to perform a little u-substitution here to simplify our derivatives.

$h_{t} = tanh(u) + B_{h}$

So we find the gradient of loss with respect to

`u`

and then use that to find rest of the gradients.$\frac{\partial L}{\partial u} = \frac{\partial L}{\partial h_{t}} \cdot \frac{\partial h_{t}}{\partial u}$

$\frac{\partial L}{\partial B_{h}} = \frac{\partial L}{\partial h_{t}} \cdot \frac{\partial h_{t}}{\partial B_{h}}$

1

grad_u = (1 - hidden_states[t] * hidden_states[t]) * grad_h

2

grads['Bh'] += grad_u

Copied!

Finally, we can compute the gradients for the last two parameters:

$\frac{\partial L}{\partial W_{xh}} = \frac{\partial L}{\partial u} \cdot \frac{\partial u}{\partial W_{xh}}$

$\frac{\partial L}{\partial W_{hh}} = \frac{\partial L}{\partial u} \cdot \frac{\partial u}{\partial W_{hh}}$

$\frac{\partial L}{\partial h_{t-1}} = \frac{\partial L}{\partial u} \cdot \frac{\partial u}{\partial h_{t-1}}$

1

grads['Wxh'] += dot(grad_u, input_states[t].T)

2

grads['Whh'] += dot(grad_u, hidden_states[t-1].T)

3

grad_prev_h = dot(self.params['Whh'].T, grad_u)

Copied!

1

import numpy as np

2

from rnn.adagrad import AdaGradOptimizer

3

from rnn.data_util import *

4

from rnn.recurrent_model import VanillaRecurrentModel

5

â€‹

6

hidden_dim = 100

7

seq_length = 50

8

learning_rate = 1e-1

9

text_data, char_to_idx, idx_to_char = load_dictionary("rnn/datasets/random_text.txt")

10

model = VanillaRecurrentModel(len(char_to_idx), hidden_dim)

11

optimizer = AdaGradOptimizer(model, learning_rate)

Copied!

1

text document contains 727 characters and has 40 unique characters

Copied!

1

curr_iter, pointer, epoch_size, total_iters = 0, 0, 100, 20000

2

â€‹

3

steps, losses = [], []

4

while curr_iter < total_iters:

5

if curr_iter == 0 or pointer + seq_length + 1 >= len(text_data):

6

prev_hidden_state = np.zeros((hidden_dim, 1)) # Reset RNN memory

7

pointer = 0 # Reset the pointer

8

â€‹

9

# Since we are trying to predict next letter in the sequence, the target is simply pointer + 1

10

input_list = [char_to_idx[ch] for ch in text_data[pointer:pointer+seq_length]]

11

target_list = [char_to_idx[ch] for ch in text_data[pointer+1: pointer+seq_length+1]]

12

loss, grads, prev_hidden_state = model.loss(input_list, target_list, prev_hidden_state)

13

if curr_iter % epoch_size == 0:

14

steps.append(curr_iter)

15

losses.append(loss)

16

â€‹

17

optimizer.update_param(grads)

18

curr_iter += 1

19

pointer += seq_length

Copied!

1

%matplotlib inline

2

import matplotlib

3

import matplotlib.pyplot as plt

4

â€‹

5

plt.plot(steps, losses)

6

plt.show()

Copied!

png

1

# Pick a random character and sample a 100 characters long sequence (i.e. a sentence.)

2

letter = 'T'

3

hidden_state = np.zeros_like((hidden_dim, 1))

4

_, sampled_indices = model.sample_chars(prev_hidden_state, char_to_idx[letter], 500)

5

predicted_text = ''.join(idx_to_char[idx] for idx in sampled_indices)

6

print "-------------\n%s\n-------------" % predicted_text

Copied!

1

ds ond wea, leaves no step had trodden black.

2

Oh, I kept the first for another day!

3

Yet keowrodde it waves no step had trodden black.

4

Oh, I kept the first for another day!

5

Yet knowing how way leads on to way,

6

I doubted if I should ever come backourd

7

Two roand I-

8

I took the undergrooked bow,

9

And how oway lay

10

In leaves no step had trodden black.

11

Oh, I kept the first for another day!

12

Yet knowing how way leads on to way,

13

I doubted if I should ever come back.

14

I shall be telliegh

15

Somewher come batk.

16

I

17

-------------

Copied!

Multi-layer RNN

We can construct a multi-layer recurrent neural network by stacking layers of RNN together. That is simply taking the output hidden state and feed it into another hidden layer as an input sequence and repeat that process. However, in general RNN does not go very deep due to the exploding gradient problem from long sequence of data. Also for most natural language problems, there isn't a lot of incentive to go deep for every time step. The key thing is long sequence data.

multi-layer-rnn

$h^{layer}_{t} = tanh \begin{pmatrix} W^{layer} \begin{pmatrix} h^{layer - 1}_{t} \\ h^{layer}_{t-1} \end{pmatrix} \end{pmatrix}$

In this case, the

`W[l]`

is a `(hidden_dim, 2 * hidden_dim)`

matrix.Last modified 1yr ago

Copy link