0Pricing
Machine Learning Academy · 课时

序列到单值:使用 LSTM 进行情感分析

您将构建端到端的情感分类器:对评论进行分词,使用 nn.Embedding 嵌入词语,通过 LSTM 处理,并对最终隐藏状态进行分类。

序列到单值:使用 LSTM 进行情感分析 是 CoddyKit 上的免费 Machine Learning Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Machine Learning Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Machine Learning Academy 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Sequence-to-One Classification Explained

In sequence-to-one tasks, the model reads an entire sequence and produces a single output label. Sentiment analysis is the classic example: given a full movie review ('I loved every minute of this film'), predict a single label — positive or negative.

This contrasts with sequence-to-sequence (translation, where every input token produces an output token) and one-to-sequence (image captioning). In sequence-to-one, we discard intermediate LSTM outputs and use only the final hidden state h_T, which theoretically encodes the meaning of the entire input sequence.

Dataset: IMDB Movie Reviews

The IMDB dataset contains 50,000 movie reviews (25,000 train, 25,000 test) labelled as positive or negative. Each review is a variable-length string of text. Before feeding it to an LSTM, we need three preprocessing steps: tokenisation (split text into words), vocabulary building (map words to integer indices), and padding (make all sequences the same length).

We also need to handle reviews that are very long — LSTM training slows quadratically with sequence length, so truncating to 256 or 512 tokens is common practice without significantly hurting accuracy.

from torchtext.datasets import IMDB
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator

tokenizer = get_tokenizer('basic_english')

def yield_tokens(data_iter):
    for _, text in data_iter:
        yield tokenizer(text)

train_iter = IMDB(split='train')
vocab = build_vocab_from_iterator(yield_tokens(train_iter),
                                   specials=['<unk>', '<pad>'])
vocab.set_default_index(vocab['<unk>'])
print('Vocabulary size:', len(vocab))

Tokenising and Padding Sequences

Padding is necessary because PyTorch DataLoaders require tensors of the same shape within a batch. We pad shorter sequences with a special <pad> token (usually index 0) and truncate longer ones to a maximum length.

The key insight is that the LSTM should ignore padding tokens. PyTorch's pack_padded_sequence and pad_packed_sequence utilities let you efficiently skip padding during the LSTM computation, which is important for correctness — otherwise the model will update its hidden state based on meaningless padding, distorting the final h_T.

import torch
from torch.nn.utils.rnn import pad_sequence

def text_pipeline(text):
    return vocab(tokenizer(text))[:256]  # Truncate to 256 tokens

def label_pipeline(label):
    return 1 if label == 'pos' else 0

def collate_batch(batch):
    labels, texts, lengths = [], [], []
    for label, text in batch:
        labels.append(label_pipeline(label))
        processed = torch.tensor(text_pipeline(text), dtype=torch.long)
        texts.append(processed)
        lengths.append(len(processed))
    texts = pad_sequence(texts, batch_first=True, padding_value=1)  # 1 = <pad>
    return torch.tensor(labels), texts, torch.tensor(lengths)

Embedding Layer: Words to Dense Vectors

Integer word indices cannot be fed directly into an LSTM — we need to convert them to continuous vectors. An embedding layer nn.Embedding(vocab_size, embedding_dim) is a lookup table that maps each word index to a dense vector of floats.

Common embedding dimensions are 100, 200, or 300. The embeddings are learnable parameters that get updated during training alongside the LSTM weights. Alternatively, you can initialise them from pre-trained word vectors like GloVe or Word2Vec, which dramatically speeds up training on small datasets by providing pre-learned semantic relationships between words.

import torch.nn as nn

vocab_size = 25000
embedding_dim = 100
hidden_size = 128
output_dim = 1  # Binary classification: positive/negative

embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=1)

# Optional: load pre-trained GloVe vectors
# from torchtext.vocab import GloVe
# glove = GloVe(name='6B', dim=100)
# embedding.weight.data.copy_(glove.vectors)

print('Embedding weight shape:', embedding.weight.shape)  # (25000, 100)

Building the SentimentLSTM Model

The complete model architecture has three parts: an embedding layer, an LSTM, and a fully connected head. The embedding converts token indices to vectors, the LSTM processes the sequence, and the linear layer maps the final hidden state to a single logit.

We add dropout after the embedding and before the linear layer to regularise the model. For the output, we apply a sigmoid to get a probability between 0 and 1. Training uses Binary Cross-Entropy loss since this is binary classification.

import torch
import torch.nn as nn

class SentimentLSTM(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_size, num_layers, dropout):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=1)
        self.lstm = nn.LSTM(embed_dim, hidden_size, num_layers,
                            batch_first=True, dropout=dropout)
        self.dropout = nn.Dropout(dropout)
        self.fc = nn.Linear(hidden_size, 1)

    def forward(self, text):
        embedded = self.dropout(self.embedding(text))  # (B, T, embed_dim)
        output, (h_n, c_n) = self.lstm(embedded)
        last_hidden = self.dropout(h_n[-1])             # Final layer hidden state
        return self.fc(last_hidden).squeeze(1)          # (B,) logits

Training Setup: Loss and Optimizer

For binary classification we use nn.BCEWithLogitsLoss, which combines sigmoid activation and binary cross-entropy loss in a single numerically stable operation. This is preferred over applying sigmoid manually followed by nn.BCELoss.

We use the Adam optimiser with a learning rate of 1e-3. Adam adapts learning rates per parameter, making it well-suited for sparse gradients that arise from embeddings (only the embeddings for words that appear in the batch receive gradient updates). Use gradient clipping with torch.nn.utils.clip_grad_norm_ to prevent exploding gradients, a common issue in RNN training.

import torch
import torch.nn as nn
import torch.optim as optim

model = SentimentLSTM(
    vocab_size=25000, embed_dim=100,
    hidden_size=128, num_layers=2, dropout=0.3
)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.BCEWithLogitsLoss()

# Gradient clipping prevents exploding gradients in RNNs
MAX_GRAD_NORM = 1.0

print('Model parameters:', sum(p.numel() for p in model.parameters()))

The Training Loop for Sentiment LSTM

The training loop for sequence-to-one classification follows the standard PyTorch pattern: forward pass, compute loss, backpropagate, clip gradients, update parameters. We also compute accuracy by comparing predicted class (sigmoid output > 0.5) to ground truth labels.

One important consideration is moving both the model and batch tensors to the same device (CPU or GPU). Using model.to(device) and tensor.to(device) ensures consistent computation. On a GPU, training a two-layer LSTM on IMDB typically takes 2-5 minutes per epoch.

def train_epoch(model, loader, optimizer, criterion, device):
    model.train()
    total_loss, correct = 0.0, 0
    for labels, texts, lengths in loader:
        labels, texts = labels.float().to(device), texts.to(device)
        optimizer.zero_grad()
        predictions = model(texts)                    # Forward pass
        loss = criterion(predictions, labels)         # Compute loss
        loss.backward()                               # Backprop
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)  # Clip
        optimizer.step()                              # Update weights
        total_loss += loss.item()
        preds = (torch.sigmoid(predictions) > 0.5).float()
        correct += (preds == labels).sum().item()
    return total_loss / len(loader), correct / len(loader.dataset)

Evaluation and Monitoring Progress

During evaluation we call model.eval() and wrap forward passes in torch.no_grad() to disable dropout and gradient tracking. This gives us an unbiased view of generalisation performance.

We track four numbers across epochs: train loss, validation loss, train accuracy, and validation accuracy. If train accuracy climbs but validation accuracy stagnates, the model is overfitting — increase dropout or reduce model size. If both are low, the model is underfitting — add LSTM layers or increase hidden size.

def evaluate(model, loader, criterion, device):
    model.eval()
    total_loss, correct = 0.0, 0
    with torch.no_grad():
        for labels, texts, lengths in loader:
            labels, texts = labels.float().to(device), texts.to(device)
            predictions = model(texts)
            loss = criterion(predictions, labels)
            total_loss += loss.item()
            preds = (torch.sigmoid(predictions) > 0.5).float()
            correct += (preds == labels).sum().item()
    return total_loss / len(loader), correct / len(loader.dataset)

# Training run
for epoch in range(5):
    tr_loss, tr_acc = train_epoch(model, train_loader, optimizer, criterion, device)
    vl_loss, vl_acc = evaluate(model, val_loader, criterion, device)
    print(f'Epoch {epoch+1}: Train Acc={tr_acc:.3f}, Val Acc={vl_acc:.3f}')

Expected Performance and Common Mistakes

A well-tuned LSTM sentiment classifier on IMDB typically achieves 85-90% test accuracy. Common mistakes that hurt performance include: not setting model.eval() during evaluation (dropout remains active), not zeroing gradients before backprop, and training for too few epochs before the embeddings converge.

Another pitfall is using the same vocabulary index for unknown words and padding. Separate <unk> (index 0) and <pad> (index 1) tokens and pass padding_idx=1 to the embedding layer so pad embeddings are always zero and their gradients are suppressed during training.

# Common mistakes checklist:
# 1. Missing model.eval() before evaluation
# 2. Missing optimizer.zero_grad() before backward
# 3. Applying sigmoid twice (using BCELoss instead of BCEWithLogitsLoss)
# 4. Not handling unknown words (<unk> token)
# 5. Padding and unknown sharing the same index

# Correct: separate special tokens
vocab = build_vocab_from_iterator(
    yield_tokens(train_iter),
    specials=['<unk>', '<pad>'],  # 0 and 1
    min_freq=2                   # Discard rare words
)

Making Predictions on New Text

To classify new reviews at inference time, we apply the same preprocessing pipeline used during training: tokenise, convert to indices, truncate, and add a batch dimension. The model returns a logit; applying sigmoid converts it to a probability.

A prediction score above 0.5 indicates positive sentiment. You can adjust this threshold: lowering it to 0.3 increases recall for positive reviews at the cost of precision. Always apply model.eval() and torch.no_grad() during inference to disable dropout and avoid unnecessary gradient computation.

def predict_sentiment(model, text, vocab, tokenizer, device, max_len=256):
    model.eval()
    tokens = vocab(tokenizer(text))[:max_len]
    tensor = torch.tensor(tokens, dtype=torch.long).unsqueeze(0).to(device)
    with torch.no_grad():
        logit = model(tensor)
        prob = torch.sigmoid(logit).item()
    return 'positive' if prob > 0.5 else 'negative', prob

label, score = predict_sentiment(
    model,
    'This film was absolutely brilliant. Loved every scene!',
    vocab, tokenizer, device
)
print(f'Sentiment: {label} ({score:.3f})')

LSTM vs BERT for Sentiment Analysis

While LSTMs were state-of-the-art for NLP until around 2018, transformer-based models like BERT now dominate most NLP benchmarks. BERT achieves ~93-95% on IMDB, compared to LSTM's ~87-90%.

However, LSTMs remain relevant for several reasons: they are far more computationally efficient for very long sequences (transformers scale quadratically with sequence length), they work well on time-series data where the sequential nature is intrinsic, and they have a much smaller memory footprint. Understanding LSTMs also provides the conceptual foundation needed to understand how attention mechanisms improve upon them.

Quick Check

Test your understanding of LSTM-based sequence classification from this lesson.

Lesson Recap

In this lesson you learned: sequence-to-one architecture reads an entire input sequence and maps it to a single label using the final LSTM hidden state, text preprocessing pipeline requires tokenisation, vocabulary building, and padding before data enters the model, and embedding layers convert discrete word indices into dense learnable vectors that capture semantic meaning. Next up we explore transfer learning, where pre-trained model weights replace training from scratch.

常见问题解答

「序列到单值:使用 LSTM 进行情感分析」课时是免费的吗?

是的 — 「序列到单值:使用 LSTM 进行情感分析」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Machine Learning Academy 课程的其余内容,请升级到 CoddyKit PRO。 Machine Learning Academy 课程共包含 4 节课。

「序列到单值:使用 LSTM 进行情感分析」这节课中我会学到什么?

您将构建端到端的情感分类器:对评论进行分词,使用 nn.Embedding 嵌入词语,通过 LSTM 处理,并对最终隐藏状态进行分类。 你通过在浏览器中直接运行的动手代码来练习 Machine Learning Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Machine Learning Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Machine Learning Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「序列到单值:使用 LSTM 进行情感分析」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Machine Learning Academy 课中编写并运行代码吗?

能。每节 Machine Learning Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 基础 RNN:隐藏状态与序列展开
  2. 深层时间步中的梯度消失问题
  3. LSTM 单元:输入门、遗忘门与输出门
  4. 序列到单值:使用 LSTM 进行情感分析
← 返回 Machine Learning Academy