Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Chapter 15 - Recurrent Neural Networks (RNN)

Open in Colab Open in Kaggle

Chapter 15 - Recurrent Neural Networks (RNN)

Hidden-recurrence vs. output-recurrence

Using the torch.nn module, a recurrent layer can be defined via RNN, which is similar to the hidden-to-hidden recurrence. Create the layer and assign the weights and biases for our manual computations:

W_xh shape: torch.Size([2, 5])
W_hh shape: torch.Size([2, 2])
b_xh shape: torch.Size([2])
b_hh shape: torch.Size([2])

The input shape for this layer is (batch_size, sequence_length, 5), which is (1,3,5) in the following code, where the first dimension is the batch dimension (as we set batch_first=True), the second dimension corresponds to the sequence, and the last dimension corresponds to the features. Notice that we will output a sequence, which, for an input sequence of length 3 , will result in the output sequence ⟨o(0),o(1),o(2)⟩\left\langle\boldsymbol{o}^{(0)}, \boldsymbol{o}^{(1)}, \boldsymbol{o}^{(2)}\right\rangle. Also, RNN uses one layer by default, and you can set num_layers to stack multiple RNN layers together to form a stacked RNN.

Now, we will call the forward pass on the rnn_layer and manually compute the outputs at each time step and compare them. The results show that Output (manual) and RNN output are the same.

Time step 0 =>
   Input           : [[1. 1. 1. 1. 1.]]
   Hidden          : [[-0.4701929   0.58639044]]
   Output (manual) : [[-0.3519801   0.52525216]]
   RNN output      : [[-0.35198015  0.52525216]]

Time step 1 =>
   Input           : [[2. 2. 2. 2. 2.]]
   Hidden          : [[-0.88883156  1.2364398 ]]
   Output (manual) : [[-0.68424344  0.76074266]]
   RNN output      : [[-0.68424344  0.76074266]]

Time step 2 =>
   Input           : [[3. 3. 3. 3. 3.]]
   Hidden          : [[-1.3074702  1.8864892]]
   Output (manual) : [[-0.8649416  0.9046636]]
   RNN output      : [[-0.8649416   0.90466356]]




Implementing RNNs for sequence modeling in PyTorch

We are going to apply RNNs to two common problem tasks in project 1 and 2:

  1. Sentiment analysis

  2. Language modeling

Attention: To reproduce the code in the books, please make sure to use torchtext 0.10.0 (see https://pypi.org/project/torchtext/0.10.0/), which is the package I used for this chapter.

There are a few adjustments in this notebooks that also make it compatible to newer versions of torchtext.

For newer versions of torchtext, installing portalocker may be necessary:

Project 1: Predicting the sentiment of IMDb movie reviews

Preparing the movie review data

Each sample of the datasets consists of two elements, the sentiment label representing the target label we want to predict (neg refers to negative sentiment and pos refers to positive sentiment), and the movie review text (the input features). The text component of these movie reviews is sequences of words, and the RNN model classifies each sequence as a positive (1) or negative (0) review.

Vocab-size: 69023

Note that there might be some tokens in the validation or testing data that are not present in the training data and are thus not included in the mapping. If we have q tokens (that is, the size of token_counts passed to Vocab, which in this case is 69,023), then all tokens that haven’t been seen before, and are thus not included in token_counts, will be assigned the integer 1 (a placeholder for the unknown token). In other words, the index 1 is reserved for unknown words. Another reserved value is the integer 0, which serves as a placeholder, a so-called padding token, for adjusting the sequence length. Later, when we are building an RNN model in PyTorch, we will consider this placeholder, 0, in more detail.

[11, 7, 35, 457, 3, 11, 7, 4, 2297]
tensor([[   35,  1739,     7,   449,   721,     6,   301,     4,   787,     9,
             4,    18,    44,     2,  1705,  2460,   186,    25,     7,    24,
           100,  1874,  1739,    25,     7, 34415,  3568,  1103,  7517,   787,
             5,     2,  4991, 12401,    36,     7,   148,   111,   939,     6,
         11598,     2,   172,   135,    62,    25,  3199,  1602,     3,   928,
          1500,     9,     6,  4601,     2,   155,    36,    14,   274,     4,
         42945,     9,  4991,     3,    14, 10296,    34,  3568,     8,    51,
           148,    30,     2,    58,    16,    11,  1893,   125,     6,   420,
          1214,    27, 14542,   940,    11,     7,    29,   951,    18,    17,
         15994,   459,    34,  2480, 15211,  3713,     2,   840,  3200,     9,
          3568,    13,   107,     9,   175,    94,    25,    51, 10297,  1796,
            27,   712,    16,     2,   220,    17,     4,    54,   722,   238,
           395,     2,   787,    32,    27,  5236,     3,    32,    27,  7252,
          5118,  2461,  6390,     4,  2873,  1495,    15,     2,  1054,  2874,
           155,     3,  7015,     7,   409,     9,    41,   220,    17,    41,
           390,     3,  3925,   807,    37,    74,  2858,    15, 10297,   115,
            31,   189,  3506,   667,   163,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0],
        [  216,   175,   724,     5,    11,    18,    10,   226,   110,    14,
           182,    78,     8,    13,    24,   182,    78,     8,    13,   166,
           182,    50,   150,    24,    85,     2,  4031,  5935,   107,    96,
            28,  1867,   602,    19,    52,   162,    21,  1698,     8,     6,
          1181,   367,     2,   351,    10,   140,   419,     4,   333,     5,
          6022,  7136,  5055,  1209, 10892,    32,   219,     9,     2,   405,
          1413,    13,  4031,    13,  1099,     7,    85,    19,     2,    20,
          1018,     4,    85,   565,    34,    24,   807,    55,     5,    68,
           658,    10,   507,     8,     4,   668,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0],
        [   10,   121,    24,    28,    98,    74,   589,     9,   149,     2,
          7372,  3030, 14543,  1012,   520,     2,   985,  2327,     5, 16847,
          5479,    19,    25,    67,    76,  3478,    38,     2,  7372,     3,
            25,    67,    76,  2951,    34,    35, 10893,   155,   449, 29495,
         23725,    10,    67,     2,   554,    12, 14543,    67,    91,     4,
            50,    20,    19,     8,    67,    24,  4228,     2,  2142,    37,
            33,  3478,    87,     3,  2564,   160,   155,    11,   634,   126,
            24,   158,    72,   286,    13,   373,     2,  4804,    19,     2,
          7372,  6794,     6,    30,   128,    73,    48,    10,   886,     8,
            13,    24,     4,    85,    20,    19,     8,    13,    35,   218,
             3,   428,   710,     2,   107,   936,     7,    54,    72,   223,
             3,    10,    96,   122,     2,   103,    54,    72,    82,     2,
           658,   202,     2,   106,   293,   103,     7,  1193,     3,  3031,
           708,  5760,     3,  2918,  3991,   706,  3327,   349,   148,   286,
            13,   139,     6,     2,  1501,   750,    29,  1407,    62,    65,
          2612,    71,    40,    14,     4,   547,     9,    62,     8,  7943,
            71,    14,     2,  5687,     5,  4868,  3111,     6,   205,     2,
            18,    55,  2075,     3,   403,    12,  3111,   231,    45,     5,
           271,     3,    68,  1400,     7,  9774,   932,    10,   102,     2,
            20,   143,    28,    76,    55,  3810,     9,  2723,     5,    12,
            10,   379,     2,  7372,    15,     4,    50,   710,     8,    13,
            24,   887,    32,    31,    19,     8,    13,   428],
        [18923,     7,     4,  4753,  1669,    12,  3019,     6,     4, 13906,
           502,    40,    25,    77,  1588,     9,   115,     6, 21713,     2,
            90,   305,   237,     9,   502,    33,    77,   376,     4, 16848,
           847,    62,    77,   131,     9,     2,  1580,   338,     5, 18923,
            32,     2,  1980,    49,   157,   306, 21713,    46,   981,     6,
         10298,     2, 18924,   125,     9,   502,     3,   453,     4,  1852,
           630,   407,  3407,    34,   277,    29,   242,     2, 20200,     5,
         18923,    77,    95,    41,  1833,     6,  2105,    56,     3,   495,
           214,   528,     2,  3479,     2,   112,     7,   181,  1813,     3,
           597,     5,     2,   156,   294,     4,   543,   173,     9,  1562,
           289, 10038,     5,     2,    20,    26,   841,  1392,    62,   130,
           111,    72,   832,    26,   181, 12402,    15,    69,   183,     6,
            66,    55,   936,     5,     2,    63,     8,     7,    43,     4,
            78, 23726, 15995,    13,    20,    17,   800,     5,   392,    59,
          3992,     3,   371,   103,  2596,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0]],
       device='cuda:0')
tensor([1., 1., 1., 0.], device='cuda:0')
tensor([165,  86, 218, 145], device='cuda:0')
torch.Size([4, 218])

Embedding layers for sentence encoding

Why Not Use One-Hot Encoding?

  1. Large Dimensionality

    • One-hot encoding maps each word to a vector of size equal to the vocabulary size.

    • With vocabulary sizes in the range of 104−10510^4 - 10^5, the number of input features becomes extremely large.

    • This high dimensionality increases computational cost and memory usage.

  2. Curse of Dimensionality

    • Training a model with such high-dimensional input features can lead to inefficiencies.

    • The model may struggle to generalize well due to sparse feature representation.

  3. Sparsity Issue

    • One-hot encoded vectors contain mostly zeros, with only a single one per word.

    • This results in inefficient memory storage and computation.

Thus, alternative embedding methods (e.g., word embeddings) are preferred to reduce dimensionality and capture meaningful relationships between words.

Embedding methods

Word Embeddings: An Alternative to One-Hot Encoding

  1. Concept of Embeddings

    • Instead of using one-hot encoding, each word is mapped to a vector of fixed size with real-valued elements.

    • Unlike one-hot encoded vectors, embeddings can represent an infinite number of real numbers within a finite-sized vector.

    • Theoretically, real numbers can be extracted from a continuous interval (e.g., [−1,1][-1,1]).

  2. Feature Learning with Embeddings

    • Embeddings allow for automatic learning of salient features to represent words in the dataset.

    • The embedding dimension is chosen such that: embedding_dim≪nwords\text{embedding\_dim} \ll n_{\text{words}}, where nwordsn_{\text{words}} is the vocabulary size.

  3. Embedding Matrix Structure

    • Given a token set of size n+2n + 2:

      • nn: Size of the token vocabulary.

      • Index 0: Reserved for padding.

      • Index 1: Used for unknown words.

    • The embedding matrix has a shape: (n+2)×embedding_dim(n + 2) \times \text{embedding\_dim}

    • Each row in this matrix represents the learned numerical features for a token.

    • When an integer index ii is input, it retrieves the corresponding row from the matrix.

  4. Implementation in Practice

    • The embedding matrix serves as the input layer for neural network models.

    • In PyTorch, an embedding layer can be created using:

      nn.Embedding(num_embeddings=n+2, embedding_dim=embedding_dim)
  • input_dim: Total number of unique words (vocabulary size) plus reserved indices (padding and unknown words).

  • output_dim: Size of each word’s embedding vector (also called the embedding dimension). This must be explicitly set and is much smaller than input_dim to reduce dimensionality.

  • input_length: Length of the input sequence (padded if necessary).

    • for example, 'This is an example' -> [0, 0, 0, 0, 0, 0, 3, 1, 8, 9]
      => input_length is 10

  • When calling the layer, takes integer values as input, the embedding layer convert each interger into float vector of size [output_dim]

    • If input shape is [BATCH_SIZE], output shape will be [BATCH_SIZE, output_dim]

    • If input shape is [BATCH_SIZE, input_length], output shape will be [BATCH_SIZE, input_length, output_dim]

<IPython.core.display.Image object>
tensor([[[ 0.7039, -0.8321, -0.4651],
         [-0.3203,  2.2408,  0.5566],
         [-0.4643,  0.3046,  0.7046],
         [-0.7106, -0.2959,  0.8356]],

        [[-0.4643,  0.3046,  0.7046],
         [ 0.0946, -0.3531,  0.9124],
         [-0.3203,  2.2408,  0.5566],
         [ 0.0000,  0.0000,  0.0000]]], grad_fn=<EmbeddingBackward0>)

Building an RNN model

  • RNN layers:

    • nn.RNN(input_size, hidden_size, num_layers=1): A regular RNN layer, that is, a fully connected recurrent layer.

    • nn.LSTM(..): A long short-term memory RNN, which is useful for capturing the long-term dependencies.

    • nn.GRU(..): A recurrent layer with a gated recurrent unit.

    • nn.RNN(input_size, hidden_size, num_layers=1, bidirectional=True)

RNN(
  (rnn): RNN(64, 32, num_layers=2, batch_first=True)
  (fc): Linear(in_features=32, out_features=1, bias=True)
)
tensor([[ 0.3183], [ 0.1230], [ 0.1772], [-0.1052], [-0.1259]], grad_fn=<AddmmBackward0>)

Building an RNN model for the sentiment analysis task

Since we have very long sequences, we are going to use an LSTM layer to account for long-range effects. We will create an RNN model for sentiment analysis. You may change self.rnn from LSTM to regular RNN, you will observe the performance drops because regular RNN might not able to capture well on long-range data.

Epoch 0 accuracy: 0.5908 val_accuracy: 0.6596
Epoch 1 accuracy: 0.7154 val_accuracy: 0.6112
Epoch 2 accuracy: 0.6871 val_accuracy: 0.7304
Epoch 3 accuracy: 0.8082 val_accuracy: 0.8180
Epoch 4 accuracy: 0.8720 val_accuracy: 0.8430
Epoch 5 accuracy: 0.9087 val_accuracy: 0.8508
Epoch 6 accuracy: 0.9288 val_accuracy: 0.8352
Epoch 7 accuracy: 0.9459 val_accuracy: 0.8248
Epoch 8 accuracy: 0.9623 val_accuracy: 0.8678
Epoch 9 accuracy: 0.9698 val_accuracy: 0.8684
test_accuracy: 0.8552

More on the bidirectional RNN

Trying bidirectional recurrent layer

The bidirectional RNN layer makes two passes over each input sequence: a forward pass and a reverse or backward pass (note that this is not to be confused with the forward and backward passes in the context of backpropagation). The resulting hidden states of these forward and backward passes are usually concatenated into a single hidden state. Other merge modes include summation, multiplication (multiplying the results of the two passes), and averaging (taking the average of the two).

Epoch 0 accuracy: 0.6386 val_accuracy: 0.7114
Epoch 1 accuracy: 0.7897 val_accuracy: 0.8082
Epoch 2 accuracy: 0.8706 val_accuracy: 0.8380
Epoch 3 accuracy: 0.8833 val_accuracy: 0.8302
Epoch 4 accuracy: 0.9302 val_accuracy: 0.8580
Epoch 5 accuracy: 0.9496 val_accuracy: 0.8356
Epoch 6 accuracy: 0.9687 val_accuracy: 0.8584
Epoch 7 accuracy: 0.9805 val_accuracy: 0.8552
Epoch 8 accuracy: 0.9777 val_accuracy: 0.8590
Epoch 9 accuracy: 0.9824 val_accuracy: 0.8272
test_accuracy: 0.8566

Project two: character-level language modeling in PyTorch

In the model that we will build now, the input is a text document, and our goal is to develop a model that can generate new text that is similar in style to the input document. Examples of such input are a book or a computer program in a specific programming language.

In character-level language modeling, the input is broken down into a sequence of characters that are fed into our network one character at a time. The network will process each new character in conjunction with the memory of the previously seen characters to predict the next one.

<IPython.core.display.Image object>

Preparing the dataset

To obtain the input data, visit the Project Gutenberg website at https://www.gutenberg.org/, which provides thousands of free e-books. For our example, you can download the book The Mysterious Island, by Jules Verne (published in 1874) in plain text format from https://www.gutenberg.org/files/1268/1268-0.txt. Note that this link will take you directly to the download page. If you are using macOS or a Linux operating system, you can download the file with the following command in the terminal:

Preprocessing the dataset

Total Length: 1112350
Unique Characters: 80

Most NN libraries and RNN implementations cannot deal with input data in string format, which is why we have to convert the text into a numeric format. To do this, we will create a simple Python dictionary that maps each character to an integer, char2int. We will also need a reverse mapping to convert the results of our model back to text. Although the reverse can be done using a dictionary that associates integer keys with character values, using a NumPy array and indexing the array to map indices to those unique characters is more efficient.

<IPython.core.display.Image object>

Building the dictionary to map characters to integers, and reverse mapping via indexing a NumPy array. We can do it with the following code:

Text encoded shape:  (1112350,)
THE MYSTERIOUS       == Encoding ==>  [44 32 29  1 37 48 43 44 29 42 33 39 45 43  1]
[33 43 36 25 38 28]  == Reverse  ==>  ISLAND
44 -> T
32 -> H
29 -> E
1 ->  
37 -> M

Remember our goal is to design a model that can predict the next character of a given input sequence, where the input sequence represents an incomplete text. For example, after seeing “Deep Learn,” the model should predict “i” as the next character. Given that we have 80 unique characters, this problem becomes a multiclass classification task.

<IPython.core.display.Image object>

Starting with a sequence of length 1 (that is, one single letter), we can iteratively generate new text based on this multiclass classification approach:

<IPython.core.display.Image object>

In practice, the sequence length impacts the quality of the generated text.

  • Longer sequences can result in more meaningful sentences.

  • For shorter sequences, however, the model might focus on capturing individual words correctly, while ignoring the context for the most part.

Although longer sequences usually result in more meaningful sentences, as mentioned, for long sequences, the RNN model will have problems capturing long-range dependencies. Thus, in practice, finding a sweet spot and good value for the sequence length is a hyperparameter optimization problem.

[44 32 29  1 37 48 43 44 29 42 33 39 45 43  1 33 43 36 25 38 28  1  6  6
  6  0  0  0  0  0 40 67 64 53 70 52 54 53  1 51]  ->  74
'THE MYSTERIOUS ISLAND ***\n\n\n\n\nProduced b'  ->  'y'

We have already stored the entire encoded text in its original order in text_encoded. We will first create text chunks consisting of 41 characters each. We will further get rid of the last chunk if it is shorter than 41 characters. As a result, the new chunked dataset, named text_chunks, will always contain sequences of size 41 . The 41-character chunks will then be used to construct the sequence xx (that is, the input), as well as the sequence yy (that is, the target), both of which will have 40 elements. For instance, sequence xx will consist of the elements with indices [0,1,…,39][0,1, \ldots, 39]. Furthermore, since sequence yy will be shifted by one position with respect to xx, its corresponding indices will be [1,2,…,40][1,2, \ldots, 40]. Then, we will transform the result into a Dataset object by applving a self-defined Dataset class:

/tmp/ipykernel_595431/2527503007.py:15: UserWarning: Creating a tensor from a list of numpy.ndarrays is extremely slow. Please consider converting the list to a single numpy.ndarray with numpy.array() before converting to a tensor. (Triggered internally at /opt/pytorch/pytorch/torch/csrc/utils/tensor_new.cpp:245.)
  seq_dataset = TextDataset(torch.tensor(text_chunks))
 Input (x): 'THE MYSTERIOUS ISLAND ***\n\n\n\n\nProduced b'
Target (y): 'HE MYSTERIOUS ISLAND ***\n\n\n\n\nProduced by'

 Input (x): 'HE MYSTERIOUS ISLAND ***\n\n\n\n\nProduced by'
Target (y): 'E MYSTERIOUS ISLAND ***\n\n\n\n\nProduced by '

Building a character-level RNN model

RNN( (embedding): Embedding(80, 256) (rnn): LSTM(256, 512, batch_first=True) (fc): Linear(in_features=512, out_features=80, bias=True) )
Epoch 0 loss: 4.3722
Epoch 500 loss: 1.3905
Epoch 1000 loss: 1.3410
Epoch 1500 loss: 1.2183
Epoch 2000 loss: 1.2261
Epoch 2500 loss: 1.1993
Epoch 3000 loss: 1.1510
Epoch 3500 loss: 1.1376
Epoch 4000 loss: 1.1863
Epoch 4500 loss: 1.1514
Epoch 5000 loss: 1.0877
Epoch 5500 loss: 1.1253
Epoch 6000 loss: 1.1545
Epoch 6500 loss: 1.1262
Epoch 7000 loss: 1.1061
Epoch 7500 loss: 1.1627
Epoch 8000 loss: 1.1322
Epoch 8500 loss: 1.1490
Epoch 9000 loss: 1.1052
Epoch 9500 loss: 1.1162

Evaluation phase: generating new text passages

The RNN model we trained in the previous section returns the logits of size 80 for each unique character. These logits can be readily converted to probabilities, via the softmax function, that a particular character will be encountered as the next character. To predict the next character in the sequence, we can simply select the element with the maximum logit value, which is equivalent to selecting the character with the highest probability. However, instead of always selecting the character with the highest likelihood, we want to (randomly) sample from the outputs; otherwise, the model will always produce the same text. PyTorch already provides a class, torch.distributions.categorical.Categorical, which we can use to draw random samples from a categorical distribution.

To see how this works, let’s generate some random samples from three categories [0,1,2][0, 1, 2], with input logits [1,1,1][1, 1, 1]: As you can see, with the given logits, the categories have the same probabilities (that is, equiprobable categories). Therefore, if we use a large sample size (num_samples →∞\rightarrow \infty), we would expect the number of occurrences of each category to reach ≈1/3\approx 1 / 3 of the sample size.

Probabilities: [0.33333334 0.33333334 0.33333334]
[[0]
 [0]
 [0]
 [0]
 [1]
 [0]
 [1]
 [2]
 [1]
 [1]]

If we change the logits to [1,1,3][1,1,3], then we would expect to observe more occurrences for category 2 (when a very large number of examples are drawn from this distribution):

Probabilities: [0.10650698 0.10650698 0.78698605]
[[0]
 [2]
 [2]
 [1]
 [2]
 [1]
 [2]
 [2]
 [2]
 [2]]

We then define a function, sample(), that receives a short starting string, starting_str, and generate a new string, generated_str, which is initially set to the input string. starting_str is encoded to a sequence of integers, encoded_input. encoded_input is passed to the RNN model one character at a time to update the hidden states. The last character of encoded_input is passed to the model to generate a new character. Note that the output of the RNN model represents the logits (here, a vector of size 80 , which is the total number of possible characters) for the next character after observing the input sequence by the model.

Here, we only use the logits output (that is, o(7)o^{(7)} ), which is passed to the Categorical class to generate a new sample. This new sample is converted to a character, which is then appended to the end of the generated string, generated_text, increasing its length by 1 . Then, this process is repeated until the length of the generated string reaches the desired value. The process of consuming the generated sequence as input for generating new elements is called autoregression.

The island had been neged to reward from them with denies he giving these gigant of some taking two years from the question has been employed without
consequences in his depth. As
to the notice beate can be vapor when there we must forgot.

“Shat’s likely,” said Pencroft.

“What a
region of Neption!” Cyrus Harding would have
been pushed their return to the shored him, to do in the dockyar and formation of the settlers’ animal which would be better dead of freshest.

Cyrus, was well presented by means of t

As you can see, the model generates mostly correct words, and, in some cases, the sentences are partially meaningful. You can further tune the training parameters, such as the length of input sequences for training, and the model architecture.

Predictibility vs. randomness

Furthermore, to control the predictability of the generated samples (that is, generating text following the learned patterns from the training text versus adding more randomness), the logits computed by the RNN model can be scaled before being passed to Categorical for sampling. The scaling factor, α\alpha, can be interpreted as an analog to the temperature in physics.

  • Higher temperatures result in more entropy or randomness versus more predictable behavior at lower temperatures.

  • By scaling the logits with α<1\alpha<1, the probabilities computed by the softmax function become more uniform, as shown in the following code:

Scaling Factor in Text Generation

Logit Scaling vs. Temperature Scaling

In text generation using RNNs, controlling randomness in generated text is crucial. This is typically done by adjusting the logits before applying softmax.

  1. Standard Temperature Scaling (Typical ML Approach)

    • Temperature (TT) is applied inside softmax as:

      P(yi)=exp⁡(logiti/T)∑jexp⁡(logitj/T)P(y_i) = \frac{\exp(\text{logit}_i / T)}{\sum_j \exp(\text{logit}_j / T)}
    • Higher TT makes probabilities more uniform (more randomness).

    • Lower TT sharpens the probability distribution (more predictability).

  2. Logit Scaling in This Implementation (Textbook)

    • Instead of using temperature scaling inside softmax, the scale_factor directly multiplies logits:

      scaled_logits = logits * scale_factor
    • This follows:

      P(yi)=exp⁡(logiti⋅α)∑jexp⁡(logitj⋅α)P(y_i) = \frac{\exp(\text{logit}_i \cdot \alpha)}{\sum_j \exp(\text{logit}_j \cdot \alpha)}
    • Larger α\alpha (e.g., scale_factor = 2.0) → Expands logits → Higher confidence → More predictable output.

    • Smaller α\alpha (e.g., scale_factor = 0.5) → Compresses logits → More uniform probabilities → More randomness.

Key Takeaways

  • Unlike traditional temperature scaling, scale_factor works inversely:

    • Increasing scale_factor (>1) makes the output more deterministic.

    • Decreasing scale_factor (<1) makes the output more random.

  • The author’s use of scale_factor = 2.0 (more structured) and scale_factor = 0.5 (more random) is correct.

  • This distinction is important to avoid confusion with traditional softmax temperature scaling.

Example Effect of scale_factor

Scale Factor (α\alpha)Effect on LogitsEffect on SamplingOutput Type
2.0Logits spread apartMore confident choicesStructured, repetitive text
1.0No changeDefault randomnessMix of structure & variation
0.5Logits compressedMore uniform probabilitiesNonsensical, high variation
Probabilities before scaling:         [0.10650698 0.10650698 0.78698605]
Probabilities after scaling with 0.5: [0.21194156 0.21194156 0.57611686]
Probabilities after scaling with 0.1: [0.3104238  0.3104238  0.37915248]

Now, compare with α=2.0\alpha=2.0 (more predictable) and α=0.5\alpha=0.5 (more randomness).

The island on the shore, and thought the island was finished, and the stranger had constituted to this precipitation of the corral, which he suffered in the depths of the cone of a ship was only a long time was a sort of the passage there to the sea, he was ready to
do anything to the south and to the shore, the settlers did not think the convicts were to be on the corral, and the engineer then to hope of the settlers, and shelter at the corral, the colonists had not happened to the settlers had only to t
The island deep ising side; his”not
awoke-prtaugh? Gideon Spilett too?,”

Helofe, and ihos eyes; “re” helm usn’t produtiuso,
at Tajow him, somucn!”

Above Verishy, silene;
ote fat wire, notwiths, and
bank pludes
attackleep, plaions fraitr byll,
you ironaliers thefee
groat, however
parcht mad. Would wideney twontashmonest.

A fugitors plan ascererver; Thousand-shwark orhrankheaded abope had
let us alwayberrusible, withwh.
Blacaneara,moss, Net susnid! 6

I- Poingin’s new kay was “Muldhidge?”
-!

“It phincip