基于深度学习的IMDB电影评论情感分析完整实现

0 阅读14分钟

基于GRU+Attention的IMDB电影评论情感分析完整实现

本文完整实现了一个基于双向 GRU + Attention 机制的 IMDB 电影评论情感分类模型,使用 PyTorch 框架,在 25,000 条测试评论上达到约 87% 的准确率。项目涵盖数据探索分析(EDA)、模型定义、训练调优、测试预测全流程,所有代码均可直接运行。

代码加数据: pan.baidu.com/s/1zBFmkjj1… 提取码: hasb

什么是情感分析

情感分析(Sentiment Analysis)是自然语言处理(NLP)的一个分支任务,目标是从文本中自动识别和提取主观情感信息,通常将文本分类为正向、负向或中性。在电影评论场景中,情感分析将用户评论自动判别为"好评"或"差评",帮助平台聚合用户口碑、辅助推荐决策。

情感分析的核心挑战在于:人类语言中的情感表达高度依赖上下文。例如"this movie was anything but good"包含正面词汇"good",但整体语义为负面。传统基于词典规则的方法难以处理这类情况,而基于深度学习的序列模型能够捕获上下文依赖关系,显著提升分类效果。

为什么选择 GRU 而非 LSTM

GRU(Gated Recurrent Unit,门控循环单元)是一种循环神经网络变体,由 Cho 等人于 2014 年提出,通过更新门和重置门控制信息流动,用于处理序列数据中的长期依赖关系。

GRU 与 LSTM 的关键区别在于门控结构的设计:

特性GRULSTM
门控数量2 个(更新门、重置门)3 个(输入门、遗忘门、输出门)
单元状态无独立单元状态,合并到隐藏状态有独立的单元状态(Cell State)
参数量较少(约为 LSTM 的 3/4)较多
训练速度较快较慢
长序列表现中等较优
短中等序列表现优秀优秀

在 IMDB 情感分析任务中,评论文本平均长度约 237 词,截断后固定为 200 个 token,属于中等长度序列。GRU 在此场景下的性能与 LSTM 接近,但参数更少、训练更快,是性价比更高的选择。

注意力机制的作用

注意力机制(Attention Mechanism)是一种让模型在处理序列时动态分配关注权重的方法,核心思想是让模型学会"关注"输入序列中对当前任务最重要的部分。

在情感分析中,一条评论可能长达数百词,但决定情感倾向的往往只是少数关键短语,如"absolutely amazing"、"complete waste of time"。传统 GRU 仅取最后一个时间步的隐藏状态作为句子的全局表示,这会导致前面出现的关键情感词被稀释。注意力机制对所有时间步的输出进行加权求和,让模型自动学习哪些词更重要,从而生成更精准的句子表示。

本项目的注意力机制还引入了 mask 机制:由于输入序列经过 padding 对齐,注意力计算时需要屏蔽 padding 位置的权重,防止模型将注意力分配给无意义的填充 token。

项目整体架构

项目采用模块化设计,共 5 个 Python 脚本,各文件职责清晰:

文件功能运行方式
download_imdb.py从 Stanford 官方地址下载并解压 IMDB 数据集python download_imdb.py
data_analysis_eda.py探索性数据分析,生成 5 类统计图表python data_analysis_eda.py
model.pyBiGRU + Attention 模型定义被其他脚本自动导入
train.py模型训练主脚本,含数据加载与可视化python train.py
test.py模型评估与预测展示python test.py

数据流向如下:

download_imdb.py → 下载 imdb/aclImdb/ 数据集
        ↓
data_analysis_eda.py → 生成 img/ 下 EDA 图表
        ↓
train.py → 读取数据 → 训练模型 → 保存 pth/best_model.pth + 生成训练曲线图
        ↓
test.py → 加载模型 → 评估指标 + 预测展示

环境配置

依赖安装

pip install torch torchvision scikit-learn matplotlib numpy

本项目已移除 torchtext 依赖,数据直接从本地文件读取,无需额外安装。

验证 PyTorch 安装

import torch
print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA是否可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
    print(f"GPU设备名: {torch.cuda.get_device_name(0)}")

环境要求:Python 3.8+,GPU 可选(有 CUDA 可加速训练,无 GPU 也可正常运行)。

IMDB 数据集介绍

本项目使用的是 Stanford Large Movie Review Dataset(aclImdb),由 Andrew Maas 等人从 IMDb 网站采集并标注,是情感分析领域的标准基准数据集。

数据项数量说明
训练集正向评论12,500 条train/pos/*.txt
训练集负向评论12,500 条train/neg/*.txt
测试集正向评论12,500 条test/pos/*.txt
测试集负向评论12,500 条test/neg/*.txt
词表大小89,527 词imdb.vocab
平均文本长度237 词含停用词
文本长度范围10 ~ 2,525 词差异较大

数据集正负样本完全均衡(各 12,500 条),无需处理类别不平衡问题。训练集与测试集各占 50%,共 50,000 条评论。

自动下载数据集

python download_imdb.py

脚本会从 https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz 下载并自动解压到 imdb/aclImdb/ 目录。如需重新下载,添加 --force 参数。

数据探索分析(EDA)

运行 python data_analysis_eda.py 可生成 5 类可视化图表,帮助理解数据分布特征。

文本长度分布

在这里插入图片描述

正负评论的文本长度分布基本一致,均值约 237 词,中位数略低于均值,说明存在少量超长评论拉高了均值。箱线图显示正向评论的长尾略长于负向评论,但整体差异不大。

高频词对比

在这里插入图片描述

正向评论中 "great"、"love"、"best" 等词出现频率显著高于负向评论;负向评论中 "bad"、"worst"、"waste" 等词占主导。这种词汇分布差异为模型提供了清晰的分类信号。

情感特征词对比

在这里插入图片描述

"great" 在正向评论中出现次数远超负向评论,而 "bad"、"worst"、"terrible" 在负向评论中占绝对优势。值得注意的是,部分负面词(如 "bad")也会出现在正向评论中,通常以否定形式出现(如 "not bad"),这体现了上下文理解在情感分析中的重要性。

模型设计:BiGRU + Attention

模型由四个核心组件构成:Embedding 层、双向 GRU、注意力层、分类头。

模型架构图

输入: [batch_size, seq_len] 词索引序列
    │
    ▼
Embedding 层 (vocab_size → 256维)
    │
    ▼
Dropout (0.5)
    │
    ▼
双向 GRU (2层, hidden=128, 输出 256维)
    │  ← 输出: [batch, seq_len, 256]
    ▼
Attention 层 (对 seq_len 维度加权求和)
    │  ← 输出: [batch, 256] 上下文向量
    ▼
全连接层 (256 → 128) + ReLU + LayerNorm
    │
    ▼
Dropout (0.5)
    │
    ▼
全连接层 (128 → 2)  →  分类 logits

Attention 机制实现

class Attention(nn.Module):
    """注意力机制:对GRU所有时间步的输出加权求和"""

    def __init__(self, hidden_size, dropout=0.0):
        super(Attention, self).__init__()
        self.attn = nn.Linear(hidden_size, hidden_size)
        self.v = nn.Linear(hidden_size, 1, bias=False)
        self.dropout = nn.Dropout(dropout)

    def forward(self, encoder_outputs, mask=None):
        # encoder_outputs: [batch, seq_len, hidden*2]
        energy = torch.tanh(self.attn(encoder_outputs))  # [batch, seq_len, hidden*2]
        scores = self.v(energy).squeeze(-1)               # [batch, seq_len]

        # mask掉padding位置,防止注意力分配给pad token
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)

        weights = F.softmax(scores, dim=1)                # [batch, seq_len]
        weights = self.dropout(weights)
        context = torch.bmm(weights.unsqueeze(1), encoder_outputs).squeeze(1)
        return context, weights

注意力计算分三步:先用线性层 + tanh 激活将 GRU 输出投影为注意力能量值,再通过一个线性层映射为标量分数,最后对分数做 softmax 得到归一化权重。mask 操作将 padding 位置的分数设为负无穷(-1e9),使 softmax 后这些位置的权重趋近于零。

完整模型定义

class MyGRU(nn.Module):
    """基于GRU + Attention的IMDB电影评论情感分类模型"""

    def __init__(self, vocab_size, emb_size=256, hidden_size=128, num_classes=2,
                 num_layers=2, dropout=0.5, pad_idx=None):
        super(MyGRU, self).__init__()
        self.pad_idx = pad_idx

        # Embedding层
        self.embedding = nn.Embedding(vocab_size, emb_size, padding_idx=pad_idx)
        self.emb_dropout = nn.Dropout(dropout)

        # 双向GRU
        self.gru = nn.GRU(
            input_size=emb_size,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True,
            bidirectional=True,
            dropout=dropout if num_layers > 1 else 0
        )

        # 注意力层(带dropout)
        self.attention = Attention(hidden_size * 2, dropout=dropout * 0.5)

        # 分类头:两层全连接 + LayerNorm + 正则化
        self.fc1 = nn.Linear(hidden_size * 2, hidden_size)
        self.ln = nn.LayerNorm(hidden_size)
        self.fc2 = nn.Linear(hidden_size, num_classes)
        self.dropout = nn.Dropout(dropout)

    def forward(self, inputs):
        # 构造mask:padding位置为0,有效位置为1
        mask = (inputs != self.pad_idx).float() if self.pad_idx is not None else None

        emb = self.embedding(inputs)                    # [batch, seq_len, emb_size]
        emb = self.emb_dropout(emb)

        output, _ = self.gru(emb)                       # [batch, seq_len, hidden*2]

        context, attn_weights = self.attention(output, mask)  # [batch, hidden*2]

        out = self.fc1(context)                          # [batch, hidden]
        out = F.relu(out)
        out = self.ln(out)
        out = self.dropout(out)
        return self.fc2(out)

模型采用双向 GRU,前向和后向各 128 维,拼接后得到 256 维的隐藏表示。分类头使用两层全连接,中间加入 LayerNorm 和 ReLU 激活,配合 Dropout 抑制过拟合。

训练策略与防过拟合技巧

本项目综合使用了 6 种训练优化技术,共同将验证准确率从基础 GRU 的约 80% 提升至 87%。

关键超参数

参数默认值作用
seq_len200输入序列固定长度(截断/填充)
batch_size64批次大小
epochs15最大训练轮数
learning_rate0.001初始学习率
hidden_size128GRU 隐藏层维度
emb_size256词嵌入维度
dropout0.5Dropout 比例
num_layers2GRU 层数
early_stop_patience3早停耐心值
label_smoothing0.15标签平滑系数
weight_decay1e-4L2 正则化系数
max_grad_norm5.0梯度裁剪阈值
random_delete_prob0.1数据增强:随机删除 token 概率

六大优化技术

1. AdamW 优化器 + L2 正则化

AdamW 相比标准 Adam 在权重衰减的实现上更合理,将 L2 正则化与自适应学习率解耦,正则化效果更稳定:

optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=weight_decay)

2. 学习率动态调度

使用 ReduceLROnPlateau 策略,当验证准确率连续 2 个 epoch 不提升时,学习率自动减半:

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, mode='max', factor=0.5, patience=2, verbose=True
)

3. 标签平滑

标签平滑将硬标签 [0, 1] 软化为 [0.075, 0.925],防止模型对训练样本过度自信,提升泛化能力:

criterion = nn.CrossEntropyLoss(label_smoothing=label_smoothing)

4. 梯度裁剪

限制梯度范数不超过 5.0,防止 GRU 训练中出现梯度爆炸:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm)

5. 早停机制

当验证准确率连续 3 个 epoch 无提升时自动终止训练,避免过拟合:

if no_improve_count >= early_stop_patience:
    print(f"早停触发! 连续 {early_stop_patience} 个epoch验证准确率未提升")
    break

6. 数据增强:随机删除 token

训练时以 10% 的概率随机将 token 替换为 padding,迫使模型不依赖个别关键词,增强鲁棒性:

if self.augment and self.delete_prob > 0:
    mask = np.random.rand(len(sent)) > self.delete_prob
    sent[~mask] = self.pad_id

训练过程与结果分析

运行 python train.py 开始训练。训练过程中每个 epoch 结束后在测试集上评估,仅保存验证准确率最高的模型。

训练曲线

在这里插入图片描述

训练在约第 6-7 个 epoch 触发早停。训练准确率持续上升至约 94%,而验证准确率在第 3 个 epoch 后趋于稳定,维持在 86%-87% 区间。训练损失与验证损失之间存在的差距表明模型存在一定程度的过拟合,但通过 Dropout、标签平滑和数据增强等手段,过拟合已被有效控制。

混淆矩阵

在这里插入图片描述

混淆矩阵显示模型对两类评论的识别能力较为均衡:

指标负向评论正向评论
真阴性/真阳性 (TN/TP)10,94010,647
假阳性/假阴性 (FP/FN)1,5201,853
召回率 (Recall)87.8%85.2%
精确率 (Precision)85.5%87.5%
F1-Score0.8660.863

整体准确率约 86.5%,正负两类的 F1-Score 均在 0.86 以上,说明模型没有偏向某一类别。假阴性(1,853)略多于假阳性(1,520),表明模型在判断正面评论时稍显保守。

模型测试与预测

评估模式

python test.py --model pth/best_model.pth --mode eval

输出分类报告,包含每个类别的 Precision、Recall、F1-Score:

[评估结果] Accuracy: 0.8742, Loss: 0.3521
总样本数: 24992, 正确数: 21842

分类报告:
              precision    recall  f1-score   support
  negative       0.88      0.88      0.88     12492
  positive       0.87      0.87      0.87     12500

预测模式

python test.py --model pth/best_model.pth --mode predict

展示具体样本的预测结果和置信度:

样本 1:
文本: this movie was absolutely amazing ...
真实标签: positive (1) | 预测标签: positive (1) ✓
置信度: Positive=0.9234, Negative=0.0766

命令行参数

参数默认值说明
--modelpth/best_model.pth模型权重文件路径
--modeall运行模式:eval(仅评估)/ predict(仅预测)/ all(全部)

常见问题

GRU 和 LSTM 在情感分析任务中哪个更好

在 IMDB 等中等长度文本(200 token 左右)的情感分析任务中,GRU 和 LSTM 的性能差异通常在 1-2 个百分点以内。GRU 参数量比 LSTM 少约 25%,训练速度更快,在计算资源有限时是更优的选择。对于超长序列(500 token 以上)或需要更精细门控控制的任务,LSTM 可能更有优势。

为什么验证准确率比训练准确率低很多

训练准确率达到 94% 而验证准确率约 87%,这 7 个百分点的差距是过拟合的典型表现。模型在训练数据上学到了部分噪声和特定样本的特征,这些特征在测试集上不适用。本项目通过 Dropout(0.5)、标签平滑(0.15)、数据增强(随机删除 token)、L2 正则化和早停机制五种手段联合抑制过拟合,将差距控制在合理范围内。

没有GPU能否运行本项目

可以。代码中通过 torch.device('cuda' if torch.cuda.is_available() else 'cpu') 自动检测设备。无 GPU 时使用 CPU 训练,单个 epoch 预计耗时 5-15 分钟(取决于 CPU 性能),完整训练约需 1-2 小时。有 CUDA GPU 时单 epoch 约 2-5 分钟。

如何进一步提升模型准确率

按优先级建议如下:

  1. 使用预训练词向量(如 GloVe)替代随机初始化 Embedding,可提升 1-3 个百分点
  2. 增大模型容量:hidden_size 从 128 提升至 256,emb_size 从 256 提升至 300
  3. 使用 BERT 等预训练语言模型替代 GRU,IMDB 数据集上可达 93%+ 准确率
  4. 增加序列长度:seq_len 从 200 提升至 400,保留更多文本信息
  5. 尝试混合精度训练(torch.cuda.amp)加速训练,以便尝试更多超参数组合

报错 "No such file or directory: imdb/aclImdb" 怎么办

原因是未下载 IMDB 数据集或路径不正确。运行 python download_imdb.py 自动下载,或手动将 aclImdb 文件夹放入项目的 imdb/ 目录下。确认目录结构包含 imdb/aclImdb/train/pos/imdb/aclImdb/train/neg/imdb/aclImdb/test/pos/imdb/aclImdb/test/neg/imdb/aclImdb/imdb.vocab

GPU 显存不足怎么办

train.py 中将 batch_size 从 64 减小到 32 或 16:

batch_size = 32  # 或 16

较小的 batch size 会增加训练迭代次数,但不会影响最终模型质量。

完整代码文件结构

imdb情感分析/
├── imdb/
│   └── aclImdb/                    # IMDB数据集
│       ├── imdb.vocab              # 词表文件(89,527词)
│       ├── train/
│       │   ├── pos/                # 正向评论(12,500条)
│       │   └── neg/                # 负向评论(12,500条)
│       └── test/
│           ├── pos/
│           └── neg/
├── img/                            # 可视化图表
│   ├── dataset_overview.png        # 数据集概览
│   ├── text_length_distribution.png
│   ├── word_frequency.png
│   ├── sentiment_word_comparison.png
│   ├── length_vs_sentiment.png
│   ├── training_loss.png
│   ├── training_accuracy.png
│   ├── training_curves.png
│   └── Confusion_Matrix.png
├── pth/
│   └── best_model.pth              # 最佳模型权重
├── model.py                        # 模型定义
├── train.py                        # 训练脚本
├── test.py                         # 测试脚本
├── data_analysis_eda.py            # EDA分析脚本
└── download_imdb.py                # 数据集下载脚本

总结

本项目完整实现了基于双向 GRU + Attention 的 IMDB 电影评论情感分类系统,主要技术要点如下:

  • 模型架构:Embedding(256) → BiGRU(2层, 128维) → Attention → FC + LayerNorm → 分类,参数量适中,适合单卡或 CPU 训练
  • 防过拟合策略:综合运用 Dropout、标签平滑、L2 正则化、梯度裁剪、早停、数据增强六种技术,将验证准确率提升至约 87%
  • 注意力机制:对 GRU 所有时间步输出加权求和,自动聚焦关键情感词,配合 mask 屏蔽 padding 位置
  • 完整流程:从数据下载、EDA 分析、模型训练到测试预测,5 个脚本覆盖 NLP 项目的全生命周期

GRU + Attention 架构在中等长度文本分类任务中兼具效率与效果。若需追求更高准确率,可在此基础上引入预训练词向量或迁移至 BERT 等预训练语言模型。