Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to our next lesson.
In our previous lessons, we've built a strong foundation for sequence modeling. We learned how to process raw text through tokenization, and then how to convert those tokens into meaningful, dense vectors using embedding layers. You now understand the what and the why of preparing text data for a neural network.
Today, we'll take the final and most exciting step in this part of our journey: assembling these components into a complete, functional LSTM-based model for sequence classification using PyTorch. We will focus on defining the model's architecture itself.
By the end of this lesson, you will be able to:
- Structure a sequence model in PyTorch by subclassing
nn.Module. - Define the necessary layers in the
__init__method: an embedding layer, an LSTM layer, and a final linear layer for classification. - Implement the
forwardpass to correctly pipe data through the network. - Extract the appropriate LSTM output for a many-to-one classification task.
This lesson is all about constructing the blueprint of our classifier. In the next lesson, we will use this blueprint to train the model on a real dataset.
The Anatomy of an LSTM Classifier
Before we write any code, let's visualize the architecture of a typical LSTM-based text classifier. It follows a "many-to-one" pattern: it processes a sequence of many inputs (the words in a sentence) to produce a single output (a class label, like "positive" or "negative" sentiment).
This architecture consists of three main stages:
- Embedding Layer (
nn.Embedding): As we covered in the last lesson, this layer acts as a lookup table. It takes the integer IDs of the tokens in our input sentence and converts each one into a dense vector representation. - LSTM Layer (
nn.LSTM): This is the core of our model. It processes the sequence of embedding vectors one by one, updating its internal hidden state and cell state at each step to capture contextual information from the sequence. - Classifier Head (
nn.Linear): This is a standard fully-connected layer. It takes the final, information-rich output from the LSTM layer and projects it down to the number of output classes we want to predict.
Let's briefly review the LSTM's mechanism for handling sequences.
NLP with PyTorch: A Comprehensive Guide
The following article, 'NLP with PyTorch' from DataCamp, provides a concise summary of how an LSTM processes a sequence and uses its internal gates to manage information flow. This will serve as a quick refresher.
Read the section 'NLP Model Architecture', focusing on the paragraphs under the 'LSTMs' subheading. Pay attention to the description of the input, forget, and output gates and how the final state of the network is used for prediction.
As the article mentions, the key is that the LSTM maintains a memory of the sequence, and we use its final state to make our prediction. Now, let's translate this architecture into PyTorch code.
Implementing the Model with nn.Module
In PyTorch, all neural network models are created by creating a class that inherits from torch.nn.Module. This base class provides all the necessary functionality for tracking parameters, moving them between CPU/GPU, etc.
To create our custom model, we need to implement two essential methods:
__init__(): The constructor, where we define and initialize all the layers our network will use (e.g., embedding, LSTM, linear).forward(): This method defines the "forward pass"—how an input tensor flows through the layers we defined in__init__to produce an output.
Let's see how this is done with a practical example from the DataCamp article.
NLP with PyTorch: A Comprehensive Guide
This section of the DataCamp article provides a clear and complete implementation of an LSTM-based sentiment analysis model. We will study it carefully to understand how the __init__, forward, and an important helper method, init_hidden, are constructed.
Read the section '5. Define the LSTM model'. Study the SentimentRNN class definition closely. We will break down each part of this code in the following sections.
1. The __init__ Method: Defining the Layers
In the __init__ method, we instantiate the layers we'll need and assign them as attributes of the class (e.g., self.lstm).
Here's a typical __init__ block for an LSTM classifier:
import torch
import torch.nn as nn
class LSTMClassifier(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, dropout_prob=0.5):
super(LSTMClassifier, self).__init__()
self.hidden_dim = hidden_dim
self.n_layers = n_layers
# 1. Embedding Layer
self.embedding = nn.Embedding(vocab_size, embedding_dim)
# 2. LSTM Layer
self.lstm = nn.LSTM(input_size=embedding_dim,
hidden_size=hidden_dim,
num_layers=n_layers,
batch_first=True, # Crucial for intuitive tensor shapes
dropout=dropout_prob if n_layers > 1 else 0)
# 3. Dropout Layer for regularization
self.dropout = nn.Dropout(dropout_prob)
# 4. Fully Connected Layer (Classifier Head)
self.fc = nn.Linear(hidden_dim, output_dim)
# 5. Sigmoid activation for binary classification
self.sig = nn.Sigmoid()
Let's break down the nn.LSTM parameters:
input_size: The size of the features at each time step. For us, this is theembedding_dim, since we feed embedding vectors into the LSTM.hidden_size: The number of features in the hidden stateh. This is a hyperparameter you choose.num_layers: The number of LSTM layers to stack on top of each other.batch_first=True: This is a very important argument. It tells PyTorch that our input tensors will have the batch dimension first, i.e., their shape will be(batch_size, sequence_length, feature_size). This is generally more intuitive to work with than the default(sequence_length, batch_size, feature_size).
2. The init_hidden Helper Method
The LSTM requires an initial hidden state (h_0) and cell state (c_0) to start its sequential processing. It's a common practice to create a helper method to initialize these states as zero-tensors. The SentimentRNN class from the DataCamp article does this cleanly.
def init_hidden(self, batch_size):
# Use the current device of the model
device = next(self.parameters()).device
# Create zero tensors for the hidden and cell states
h0 = torch.zeros((self.n_layers, batch_size, self.hidden_dim)).to(device)
c0 = torch.zeros((self.n_layers, batch_size, self.hidden_dim)).to(device)
# The LSTM layer expects the hidden state as a tuple (h_0, c_0)
hidden = (h0, c0)
return hidden
This method takes the batch_size as input because the hidden state dimensions depend on it.
3. The forward Method: Defining the Data Flow
This method orchestrates the entire process, from input tokens to final prediction scores. The video below provides a fantastic walkthrough of a similar model's forward pass, highlighting how to handle the shapes and extract the correct output.
PyTorch Tutorial - RNN & LSTM & GRU - Recurrent Neural Nets
In this video by Patrick Loeber, he implements a recurrent network for image classification, but the PyTorch mechanics are identical to our text case. Pay close attention to how he initializes the hidden state and processes the output of the RNN/LSTM layer.
Watch from 04:33 to 12:20 and then from 13:12 to 15:18. Focus on: The use of batch_first=True (05:46). Initializing the hidden state with torch.zeros (09:15). The shape of the LSTM output (10:39). How to select the output of the last time step for classification using slicing: out[:, -1, :] (11:42). How the nn.LSTM layer requires a tuple for its initial state (h0, c0) (15:03).
Synthesizing what you saw in the video and the SentimentRNN example, a forward pass for our classifier looks like this:
def forward(self, x, hidden):
batch_size = x.size(0)
# 1. Pass through embedding layer
# Input x shape: (batch_size, seq_length)
# Output embeds shape: (batch_size, seq_length, embedding_dim)
embeds = self.embedding(x)
# 2. Pass through LSTM layer
# lstm_out shape: (batch_size, seq_length, hidden_dim)
# hidden is a tuple of the final hidden and cell states
lstm_out, hidden = self.lstm(embeds, hidden)
# 3. Stack up LSTM outputs and apply dropout
# We only need the output of the last time step
# lstm_out shape becomes: (batch_size, hidden_dim)
lstm_out = lstm_out[:, -1, :]
out = self.dropout(lstm_out)
# 4. Pass through the fully-connected layer
# out shape: (batch_size, output_dim)
out = self.fc(out)
# 5. Pass through the sigmoid activation function
# sig_out shape: (batch_size, 1) for binary classification
sig_out = self.sig(out)
return sig_out, hidden
The most critical step here is lstm_out = lstm_out[:, -1, :]. This selects the hidden state outputs from the very last time step for all sequences in the batch, which is exactly what we need for our "many-to-one" classification.
A Complete Example
To see all these pieces work together in an end-to-end text classification context, let's look at one more practical example.
Creating a Text Classifier With LSTM! PyTorch Deep Learning Tutorial
This video by Luke Ditria builds a text classifier from scratch. We will focus only on the model definition part, which provides another excellent example of the architecture we've been discussing.
Watch from 26:04 to 28:12. Notice how he defines the nn.Embedding, nn.LSTM, and nn.Linear layers. He also discusses the model's parameters, highlighting how the embedding layer often contains the vast majority of them, which reinforces the importance of vocabulary size management.
The video effectively shows how to instantiate the model and highlights a key practical aspect: the nn.Embedding layer can account for millions of parameters, making vocabulary size an important consideration.
Conclusion
Congratulations! You have now learned how to define a complete LSTM-based neural network for sequence classification in PyTorch. You've assembled the building blocks from our previous lessons into a coherent and functional model architecture.
Key Takeaways:
- PyTorch models are defined as classes inheriting from
nn.Module. - The
__init__method is where you declare and instantiate all the layers your model will use (e.g.,nn.Embedding,nn.LSTM,nn.Linear). - The
forwardmethod dictates the flow of data through these layers, from input to output. - The
nn.LSTMlayer requires an initial hidden state and cell state, typically provided as a tuple of zero-tensors. - For many-to-one sequence classification, you typically use the LSTM's output from the final time step to feed into the classification layer.
Preview of the Next Lesson
We've built the car, but we haven't started the engine yet. In our next lesson, we will do just that. We will take the LSTMClassifier class we designed today, write a complete training loop, and train it on a real sentiment analysis dataset to see it learn and make predictions.