基于 LSTM 的中文电商评论情感分类:从数据处理到 91% 准确率实战

基于 LSTM 的中文电商评论情感分类:从数据处理到 91% 准确率实战

项目地址(Gitee):https://gitee.com/Touari/lstm_sentiment_2class.git

本文整合自项目《构建文档》与《技术文档》,既讲复现步骤,也讲设计原理。


1. 项目简介

构建一个中文电商评论情感分类模型 :给定一条评论文本,判定其情感倾向为正向(好评)负向(差评) 。在工程上等价于基于句子的二分类任务(Binary Text Classification)。

复制代码
输入:这个商品质量非常好,物流也快,很满意
输出:正向(概率 0.9997)

与上一个项目(RNN 中文输入法,预测下一词)的演进对比

RNN_TextInput(上项目) LSTM_Sentiment_2class(本项目)
任务 前 5 词预测下一词 整句判定正/负情感
任务本质 多分类(21,138 类) 二分类(2 类)
循环层 nn.RNN(简单循环单元) nn.LSTM(三门控机制)
输入 定长窗口 5 词 变长句子 → 定长 128(填充/截断)
输出层 Linear(hidden → 21138) Linear(hidden → 1)(单个 logit)
损失函数 CrossEntropyLoss BCEWithLogitsLoss

2. 整体架构与数据流

复制代码
data/raw/online_shopping_10_cats.csv(62774 条电商评论,10 品类,0/1 标签)
        │  process.py:读取 → 分层划分 8:2 → jieba 分词 → 建词表 → 定长编码
        ▼
data/processed/train.jsonl(50218 条)+ test.jsonl(12555 条)
        │  每条:{"label": 0/1, "review": [128 个词 id]}
        │  dataset.py:SentimentDataset + DataLoader(batch=64)
        ▼
src/train.py:SentimentModel(Embedding→LSTM→Linear(1))+ BCEWithLogitsLoss + Adam
        │
        ├──→ models/best.pt(loss 最优权重,约 32 MB)
        ├──→ logs/(TensorBoard 曲线)
        ▼
evaluate.py(测试集准确率 91.48%)    predict.py(交互式预测)

核心思想:把「判断情感」转化为「二分类问题」------模型对整句输出一个 logit(正负得分),经 sigmoid 变成 0~1 的概率,>0.5 判正向。


3. 数据处理详解

3.1 原始数据

online_shopping_10_cats.csv,62,774 条京东/携程电商评论,列:cat(品类)、label(0 差评 / 1 好评)、review(评论文本)。

  • 10 个品类:书籍、平板、手机、水果、洗发水、热水器、蒙牛、衣服、计算机、酒店
  • 标签高度均衡:好评 31,728(50.5%)/ 差评 31,046(49.5%)------ 二分类理想状态,可直接用准确率作为可信指标
  • 评论长度:中位数 34 字,>200 字仅 3.4%,最长 2876 字 ------ 长短差异极大,是变长处理问题的根源

3.2 划分数据集

  • train_test_split(test_size=0.2, random_state=42, stratify=df['label'])
  • stratify 分层抽样:保证训练/测试集正负比例一致(相比上项目是改进点)
  • 训练 50,218 条 / 测试 12,555 条

3.3 分词与词表

  • jieba 分词jieba.lcut
  • 词表 = ['<pad>', '<unk>'] + 训练集出现过的词,共 60,973
  • <pad>(索引 0) :填充占位符,nn.Embedding(padding_idx=0) 使其向量恒为 0、不参与梯度
  • <unk>(索引 1):未登录词统一映射,避免 OOV 崩溃
  • 只使用训练集建词表 ------ 防止测试集信息泄漏(标准做法)

3.4 定长序列化(核心设计决策)

句子长短不一(1~1795 个词),而批量训练要求矩阵等长,因此统一到 SEQ_LEN = 128

复制代码
分词后: [w1, w2, ..., wN]
  N > 128 → 截断尾部:保留前 128 个词
  N < 128 → 尾部填充:补 (128 - N) 个 <pad>
最终:     [w1, ..., w128](全部等长)
  • 上限怎么定的 :先对训练集做长度分布统计,quantile(0.96) 约 116 词 → 取 128 覆盖 96% 的句子,只截断最长 4% 的极端评论
  • 为什么选定长而非动态填充 :「先跑通」路线------process 阶段一次性定长,dataset 零负担(DataLoader 默认 stack 即可,无需 collate_fn)。进阶路线(pad_sequence 动态填充 + pack_padded_sequence)可在此基础上升级
  • 填充方向 :统一尾部填充(pad 在句尾),这是后面「取最后一个真实词」策略的前提

4. 模型结构详解

4.1 网络结构

python 复制代码
SentimentModel:
  Embedding(60,973 → 128, padding_idx=0)   # 每个词映射为 128 维向量
  LSTM(input=128, hidden=256)              # 单层单向,batch_first=True
  Linear(256 → 1)                          # 隐状态 → 单个 logit

4.2 前向传播形状流转

复制代码
输入 x:              [batch, 128]              # 128 个词 id(定长)
  → Embedding:       [batch, 128, 128]         # 每个词 → 128 维向量
  → LSTM:            [batch, 128, 256]         # 每时间步输出 256 维隐状态
  → 取最后真实词:     [batch, 256]              # output[batch_idx, lengths-1]
  → Linear:          [batch, 1]                # 单个 logit
  → squeeze(-1):     [batch]                   # 形状契约:每个样本一个 logit
  → sigmoid(在预测时): [batch]                  # 概率(损失函数内部自带)

4.3 关键设计点(最有含金量的三处)

① 取「最后一个真实词」的隐状态,而不是最后一步

复制代码
实际句子: [好, 用, 很, 快, <pad>, <pad>, <pad>]
          ↑ lengths-1 = 3(最后一个真实词)   ↑ -1 = 6(pad 位置)

由于 pad 在尾部,output[:, -1, :] 取到的是 pad 位置的隐状态(无意义)。正确做法:

python 复制代码
lengths = (x != self.embedding.padding_idx).sum(dim=1)   # 每个样本真实长度
last_hidden = output[batch_indexes, lengths - 1]         # 精确取最后一个真实词

注意:LSTM 返回的 (h_n, c_n)h_n 虽也是"最后时间步"隐状态,但同样位于 pad 位置,不可直接使用

padding_idx=0

告诉 Embedding 层 0 号是 pad:pad 位置的词向量恒为 0,且不参与梯度更新。漏掉此参数是定长填充模型最常见的隐性错误。

③ 输出层 squeeze(-1) 的形状契约

模型统一输出 [batch](每个样本一个 logit),与 BCEWithLogitsLoss 的输入要求(input 与 target 形状一致)对齐。若保留 [batch, 1] 会报 Target size must be the same as input size

4.4 参数量计算(为什么是 820 万)

计算 参数量
Embedding 60,973 × 128 7,804,544
LSTM 权重 ih 4 × 256 × 128 131,072
LSTM 权重 hh 4 × 256 × 256 262,144
LSTM 偏置 4 × 256 × 2 2,048
Linear 256 × 1 + 1 257
合计 8,200,065 ≈ 820 万

要点:

  • LSTM 有 4 组权重 (输入门/遗忘门/细胞门/输出门),因此参数量是同等规模 nn.RNN 的 4 倍------这就是「门控」的物理代价
  • 参数大头在 Embedding(95%);FP32 训练态显存约数十 MB,普通显卡毫无压力

4.5 为什么 LSTM 优于 RNN(换层的意义)

nn.RNN 每个时间步 h_t = tanh(W·x_t + U·h_{t-1}),链式相乘导致梯度随序列长度指数级衰减/爆炸(长距离信息记不住)。LSTM 引入三个门控制信息流:

  • 遗忘门:决定丢弃多少旧记忆
  • 输入门:决定写入多少新信息
  • 输出门:决定暴露多少记忆给输出

门控使梯度可以"绕行"细胞状态(c_t)直接传播,缓解了长序列的梯度消失,这是 LSTM 自 1997 年提出后统治 NLP 十余年的核心原因。


5. 超参数设计

超参 设计考量
SEQ_LEN 128 句长上限,quantile(0.96) 统计确定,覆盖 96% 句子
BATCH_SIZE 64 序列短、模型小,64 提升 GPU 吞吐
EMBEDDING_DIM 128 词向量维度,6 万词表下 128~256 常见区间
HIDDEN_DIM 256 LSTM 隐状态维度,与嵌入维同量级
LEARNING_RATE 0.001 Adam 常用默认学习率
EPOCHS 10 loss 仍在下降未到平台期,可适当增加(见第 8 节)

6. 训练细节

  • 损失函数BCEWithLogitsLoss ------ 内部融合 sigmoid 与二元交叉熵,数值更稳定。二分类专用;输入 logits(未过 sigmoid)与 0/1 标签
  • 优化器Adam(lr=0.001)
  • 设备cuda 优先
  • 数据加载DataLoader(batch_size=64, shuffle=True)
  • 日志SummaryWriter 每轮记录 loss 到 logs/<时间戳>/
  • 模型保存 :每轮后若 loss 优于历史最优则覆盖保存 models/best.pt(state_dict)
  • 设备一致性torch.arange(..., device=output.device) ------ 索引张量与模型同设备,避免 CUDA 下崩溃(本项目踩过并修复的坑)

实测训练曲线(10 epochs)

复制代码
0.3932 → 0.2474 → 0.1852 → 0.1419 → 0.1105 → 0.0823 → 0.0560 → 0.0378 → 0.0281 → 0.0229

初始 loss ≈ 0.693(= ln2,二分类随机猜测的理论交叉熵),说明初始化不偏不倚;逐轮下降说明学习正常。


7. 实验结果

7.1 测试集评估(12,555 条全量)

指标 数值 基线
准确率 91.48% 随机猜测 50%

解读:91.48% 意味着模型真正学到了好评/差评的语言规律("质量差劲"、"再也不买" vs "很满意"、"值得购买"),远非瞎猜。二分类场景下 90%+ 已属良好水平。

7.2 预测示例

输入 判断 概率
这个商品质量非常好,物流也快,很满意 正向 0.9997
东西太差了,用了两天就坏了 负向 0.0000
太好了 正向 0.9859
不好看 负向 0.9933

7.3 置信度解读

预测概率普遍落在 0 或 1 附近(如 0.9997 / 0.0000),这是 sigmoid 饱和的正常现象------训练充分的二分类模型对分布内样本非常自信。概率落在中间(0.4~0.6)反而意味着模型不确定。


8. 快速开始(复现指南)

8.1 环境准备

  • Python 3.11
  • 依赖:torch 2.7.1+cu118、pandas 3.0.2、jieba 0.42.1、scikit-learn 1.8.0、tqdm 4.68.2、tensorboard 2.21.0
bash 复制代码
python -m venv .venv
# Windows 激活
.venv\Scripts\activate
pip install torch pandas jieba scikit-learn tqdm tensorboard

命令行运行注意:所有脚本需 cd src 后运行(模块间使用 import config 平级导入),且建议 python -E 启动防止 PYTHONPATH 污染。

8.2 目录结构

复制代码
LSTM_Sentiment_2class/
├── data/
│   ├── raw/online_shopping_10_cats.csv   原始数据(62774 条电商评论)
│   └── processed/train.jsonl, test.jsonl 预处理产物(定长 128 的 id 序列 + 标签)
├── logs/                                  训练日志(TensorBoard)
├── models/
│   ├── vocab.txt                          词表(60973 词,<pad>=0, <unk>=1)
│   └── best.pt                            最优模型权重(约 32 MB)
├── src/
│   ├── config.py                          路径与超参唯一来源
│   ├── process.py                         数据预处理流水线
│   ├── tokenizer.py                       词表构建与编解码(含定长填充)
│   ├── dataset.py                         数据集与 DataLoader
│   ├── model.py                           情感分类模型(Embedding+LSTM+Linear)
│   ├── train.py                           训练循环
│   ├── evaluate.py                        测试集评估
│   └── predict.py                         交互式预测
├── .gitignore
└── main.py

8.3 全流程四步

第 1 步:数据预处理

bash 复制代码
cd src
python -E process.py

预期输出:打印 词表大小: 60973数据处理完成。产物:train.jsonl(50218 条)、test.jsonl(12555 条)、vocab.txt

第 2 步:训练

bash 复制代码
python -E train.py

每轮打印 ========== Epoch N ==========Loss: x.xxxx,loss 从约 0.39 逐轮下降;每轮若 loss 优于历史最优则覆盖 models/best.pt。RTX 5080 上 10 epochs 约 5~10 分钟。

训练曲线可用 TensorBoard 查看:

bash 复制代码
tensorboard --logdir logs

第 3 步:评估

bash 复制代码
python -E evaluate.py

预期输出:

复制代码
词表加载成功
模型加载成功
评估结果
acc: 0.9148

第 4 步:交互式预测

bash 复制代码
python -E predict.py
复制代码
> 这个商品质量非常好,物流也快,很满意
正向(置信度:0.9997)
> 不好看
负向(置信度:0.9933)
> q
欢迎下次再来

9. 已知边界与改进方向

已知边界

  1. 分布外(OOD)输入不可靠 :模型学的是电商评论语言分布。测试集(分布内)91.48%,但训练分布外的表达(如夸奖人的口语"你真棒")会误判。这是所有 NLP 模型的共性,不是 bug
  2. 无中性类:二分类强制所有输入二选一,"一般般吧"这类中性表达会被迫归入某一类
  3. 单层单向 LSTM:只能利用左侧上下文,无法看到"不过......但是......"这类转折后置的信息
  4. 定长截断:超过 128 词的句子尾部被截掉,长句关键信息可能丢失(仅影响最长 4%)

改进方向(按性价比排序)

  1. 训练循环内加测试集评估 :每 epoch 打印 val_acc,观察过拟合拐点,配合早停(Early Stopping)
  2. 继续训练:loss 降至 0.023 仍未到平台期,可增加 EPOCHS 或调大 HIDDEN_DIM
  3. 双向 LSTMnn.LSTM(bidirectional=True),输出维度翻倍(hidden 取 128 保持参数量),利用右侧上下文
  4. 动态填充collate_fn + pad_sequence + pack_padded_sequence,避免 pad 位置的无效计算
  5. 升级架构:TextCNN、Transformer/BERT ------ 属后续项目范畴

10. 小结

本项目用 Embedding → LSTM → Linear(1) 的经典结构,在 6 万词表的中文电商评论上实现了测试集准确率 91.48% 的情感二分类。相比上一项目(RNN 输入法),本项目的进阶点清晰可见:LSTM 门控替代简单 RNN、分层抽样、定长序列化 + 精确取真实词隐状态、BCEWithLogitsLoss 二分类损失。项目代码结构清晰、全流程可复现,是 NLP 序列建模从「预测」走向「理解」的典型练习。

完整代码与数据见项目仓库:https://gitee.com/Touari/lstm_sentiment_2class.git

相关推荐
用户298698530141 小时前
HTML 转 Word 指南:新手入门教程
人工智能·后端·python
冬哥聊AI1 小时前
淘天一面:Prefix Caching 原理是什么?Agent 框架怎么保证不破坏缓存?
人工智能
agent8971 小时前
实战升级|SpringBoot WebSocket实现多轮对话AI流式问答(上下文记忆+自动重连+会话隔离)
人工智能·spring boot·websocket
染指11101 小时前
80.高级RAG-LLamaIndex实际应用-金融助手
人工智能·rag·llama_index·llamaindex
Clipp_Huang1 小时前
光学跟踪系统标定
人工智能·计算机视觉·重构·机器视觉
阿图灵1 小时前
Agentic AI 架构入门(三):Agent 的七大组件与 PRAL 循环
人工智能·架构·llm·rag·ai agent·智能体·agentic ai
weixin_468466851 小时前
目标检测精度上限与影响因素分析
图像处理·人工智能·目标检测·计算机视觉·图像分类·coco·检测精度
宋哥转AI1 小时前
深入理解 AI Agent · MCP 子系列 #01:MCP 协议全解—从消息格式到传输层的完整拆解
人工智能·agent·mcp
看山先生1 小时前
凌晨两点,我把一块开发板接进了自己的世界
人工智能·agent