Python+LSTM三分类情感分析实战:从数据到部署

发布时间:2026/9/28 2:23:28
Python+LSTM三分类情感分析实战:从数据到部署 简介这份资源是Python基于LSTM三分类的文本情感分析完整项目面向计算机相关专业正在做课程设计、期末大作业的学生以及需要NLP项目实战练习的学习者。项目将文本情感划分为三类采用LSTM网络建模配套数据预处理、词向量训练与模型评估流程代码经导师指导并获99分评审认可小白也能按文档顺利跑通。压缩包共13个文件约11.63MB包含3个py脚本、2个ipynb笔记本、3个csv情感语料、1个pkl词向量模型、1个h5训练权重、1个yml配置、1个txt依赖清单及1个md说明文档覆盖从数据到模型落地的完整链路。目前已有307人学习下载。读者可获得可直接运行的源码、逐模块文档说明与清晰的目录结构便于快速理解LSTM三分类的实现思路并在此基础上完成自己的课程设计或二次开发。1. 三分类情感分析从二分类的“差不多”到三分类的“差在哪”做文本情感分析的人大多是从二分类起步的好评/差评、正面/负面跑个朴素贝叶斯或者 LSTM 就能到 90% 以上的准确率看起来很美。但真正落到业务里你会发现二分类根本不够用——电商评论里大量“东西还行但物流太慢”这种中性偏负的句子硬塞进正面或负面都会误导决策。三分类正面/中性/负面才是大多数场景的真实需求也是这个项目标题里“三分类”三个字的分量所在。这个方案用 Python LSTM 做三分类情感分析附带完整源码和文档说明适合有 Python 基础、想跑通一个端到端 NLP 项目的人。它解决的核心问题是把一段中文文本映射到三个情感极性之一并且让你能看懂每一步在干什么、参数怎么调、哪里容易翻车。下面从数据准备一路讲到模型调优中间该踩的坑我都会标出来。2. 数据准备与标注三分类的语料从哪来、怎么洗2.1 三分类语料的来源与标注口径二分类语料遍地都是三分类的公开中文语料却少得多。常见做法是拿二分类语料做基础再补一批中性样本。我一般会从三个渠道凑一是电商评论带星级3 星可视为中性二是酒店/餐饮评价三是自己爬一批社交媒体短文本人工标。这里要强调标注口径必须先定死否则三个人标出来的“中性”能差出十万八千里。标注时建议用这套规则正面明确表达满意或推荐负面明确表达不满或劝阻中性陈述事实、无明显倾向、或正负抵消。比如“收到了还没用”算中性“质量不错但客服态度差”算中性偏负如果业务需要可以单独设“混合”类但三分类项目里就归中性。标注一致性用 Kappa 系数卡一下低于 0.7 就重新对齐标准。数据量方面三分类比二分类更吃数据每类至少 2000 条起步中性类往往最难凑可以考虑用回译或同义替换做增强但别过度否则模型学到的是模板不是语义。2.2 中文文本清洗与 jieba 分词的参数中文不像英文有空格分词是绕不开的一步。常见做法是 jieba 自定义词典把领域词加进去比如“续航”“性价比”“翻车”这类词如果被切碎情感信号就丢了。import jieba import re # 加载自定义词典每行一个词比如续航能力、客服态度 jieba.load_userdict(user_dict.txt) def clean_text(text): # 去掉 URL、用户、话题标签、多余空白 text re.sub(rhttp\S|\S|#\S#, , text) text re.sub(r\s, , text).strip() # 只保留中文、英文、数字和基本标点 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , text) return text def tokenize(text): text clean_text(text) # 精确模式适合情感分析这种需要保留完整词义的场景 words jieba.lcut(text, cut_allFalse) # 去掉单字和停用词单字往往噪声大 stopwords set(open(stopwords.txt, encodingutf-8).read().split()) words [w for w in words if len(w) 1 and w not in stopwords] return wordsclean_text里正则的顺序有讲究先去 URL 和 再去特殊符号最后统一空白。jieba.lcut用精确模式而不是全模式全模式会把“不错”切成“不”“错”情感直接反转。len(w) 1过滤单字是个经验阈值像“好”“差”这种单字其实有情感但噪声太大我一般靠模型从上下文里学不单独保留。分词后要建词表。三分类任务词表别开太大10000 到 20000 足够太大会导致 embedding 矩阵稀疏、训练慢。低频词统一映射到UNK词频阈值一般设 2 或 3。2.3 标签编码与数据集划分的坑标签用 0/1/2 编码别用 one-hot 存文件浪费空间。划分比例 8:1:1但要注意分层采样保证三个类在训练/验证/测试里比例一致否则中性类少的时候测试集可能一条都没有。from sklearn.model_selection import train_test_split import numpy as np texts [...] # 分词后的词列表 labels [...] # 0/1/2 # stratify 保证三类比例一致random_state 固定方便复现 X_train, X_temp, y_train, y_temp train_test_split( texts, labels, test_size0.2, stratifylabels, random_state42) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size0.5, stratifyy_temp, random_state42)这里stratify是关键参数少了它中性类样本少的时候验证集可能全是正面验证准确率虚高。random_state固定是为了复现调参阶段别改。另外别在划分前做数据增强否则增强样本会泄漏到验证集指标好看但上线就崩这是血泪经验。3. LSTM 模型搭建从 embedding 到三分类输出层3.1 为什么选 LSTM 而不是 TextCNN 或 BERT三分类情感分析里LSTM 的优势是能捕捉长距离依赖和语序信息比如“不是不好”这种双重否定CNN 的局部窗口容易漏掉。BERT 效果通常更好但参数量大、推理慢如果项目要求轻量部署或者你还在学习阶段LSTM 是性价比很高的选择。这个项目标题明确用 LSTM那我们就把它吃透。LSTM 的核心是三个门遗忘门决定丢多少旧信息输入门决定存多少新信息输出门决定输出什么。情感分析里遗忘门帮模型忽略无关词输入门记住情感词输出门在句尾给出极性判断。双向 LSTM 再叠加一层反向扫描能同时利用前后文对“虽然…但是…”这种转折句特别有效。3.2 PyTorch 版 LSTM 三分类模型代码下面是一个可直接跑的双向 LSTM 模型输入是词索引序列输出三个类别的 logits。import torch import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim128, num_layers2, num_classes3, dropout0.5, pad_idx0): super().__init__() # padding_idx 让 PAD 的 embedding 不参与梯度更新 self.embedding nn.Embedding(vocab_size, embed_dim, padding_idxpad_idx) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layersnum_layers, bidirectionalTrue, batch_firstTrue, dropoutdropout if num_layers 1 else 0) self.dropout nn.Dropout(dropout) # 双向所以是 hidden_dim * 2 self.fc nn.Linear(hidden_dim * 2, num_classes) def forward(self, x): # x: (batch, seq_len) emb self.dropout(self.embedding(x)) # out: (batch, seq_len, hidden*2), (h, c) 各 (num_layers*2, batch, hidden) out, (h, c) self.lstm(emb) # 取最后一层正向和反向的 hidden 拼接 h_forward h[-2] # 正向最后一层 h_backward h[-1] # 反向最后一层 h_cat torch.cat([h_forward, h_backward], dim1) h_cat self.dropout(h_cat) logits self.fc(h_cat) return logitsembed_dim128是中文三分类的常用起点语料大可以加到 256。hidden_dim128配合双向就是 256 维表示再大容易过拟合。num_layers2时 dropout 才生效单层 LSTM 加 dropout 没意义。取h[-2]和h[-1]是因为 PyTorch 把双向的 hidden 按层叠在一起最后一层正向在倒数第二个反向在最后一个这个索引写错是常见翻车点模型直接学不动。3.3 序列填充、mask 与 batch 构造变长序列要 padding 到同一长度。常见做法是取训练集 95 分位长度作为 max_len别直接取全局最大否则一条超长文本让所有 batch 都变慢。from torch.nn.utils.rnn import pad_sequence from torch.utils.data import Dataset, DataLoader class TextDataset(Dataset): def __init__(self, sequences, labels): self.sequences [torch.tensor(s, dtypetorch.long) for s in sequences] self.labels torch.tensor(labels, dtypetorch.long) def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.sequences[idx], self.labels[idx] def collate_fn(batch): seqs, labels zip(*batch) # padding 到 batch 内最大长度pad_idx0 seqs_padded pad_sequence(seqs, batch_firstTrue, padding_value0) return seqs_padded, torch.stack(labels) train_loader DataLoader(TextDataset(X_train_idx, y_train), batch_size64, shuffleTrue, collate_fncollate_fn)pad_sequence按 batch 内最大长度填充比全局填充省显存。padding_value0要和 embedding 的pad_idx一致。注意这里没做 mask因为取的是最后时刻 hiddenpadding 在句尾会影响反向 LSTM 的最终状态。更严谨的做法是用pack_padded_sequence但那样取 hidden 要额外处理新手可以先跑通再优化。如果发现验证集指标波动大优先检查 padding 方向。4. 训练、评估与调参让三分类真正跑起来4.1 训练循环与类别权重处理三分类常见问题是中性类样本少模型偏向多数类。解决办法是在损失函数里加类别权重权重和样本数成反比。from sklearn.utils.class_weight import compute_class_weight import numpy as np # 计算类别权重 class_weights compute_class_weight(balanced, classesnp.array([0,1,2]), yy_train) class_weights torch.tensor(class_weights, dtypetorch.float) criterion nn.CrossEntropyLoss(weightclass_weights) optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(10): model.train() total_loss 0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() print(fEpoch {epoch}, Loss: {total_loss/len(train_loader):.4f})compute_class_weight(balanced)自动按样本数反比给权重中性类少就权重大。clip_grad_norm_的max_norm5.0是 LSTM 训练的后悔药梯度爆炸时能救回来。学习率 1e-3 是 Adam 的常用值如果 loss 震荡就降到 5e-4。4.2 评估指标别只看准确率三分类不平衡时准确率会骗人。必须看每类的 precision、recall、F1以及混淆矩阵。from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, loader): model.eval() preds, trues [], [] with torch.no_grad(): for x, y in loader: x x.to(device) logits model(x) pred torch.argmax(logits, dim1).cpu().numpy() preds.extend(pred) trues.extend(y.numpy()) print(classification_report(trues, preds, target_names[负面,中性,正面])) print(confusion_matrix(trues, preds)) return preds, trues重点看中性类的 F1如果低于 0.6说明模型没学到中性特征要么加数据要么检查标注口径。混淆矩阵里如果正面和中性互相混得多说明边界模糊可以考虑合并或重新定义中性标准。4.3 超参数怎么调一份可对照的参数表参数常用范围调大效果调小效果embed_dim128-256表达强易过拟合欠拟合训练快hidden_dim128-256容量大需更多数据欠拟合num_layers1-2表达强梯度难训简单可能欠拟合dropout0.3-0.5抗过拟合可能欠拟合易过拟合batch_size32-128稳定显存高噪声大可能不收敛lr1e-4-1e-3收敛快易震荡收敛慢调参顺序建议先定 embed_dim 和 hidden_dim再调 dropout最后调 lr。每次只动一个记录验证集 F1。如果训练 loss 降但验证 loss 升就是过拟合加 dropout 或减层数。5. 避坑与排查三分类 LSTM 最常见的五个翻车点5.1 现象验证集准确率 95%上线后一塌糊涂原因数据泄漏。划分数据集前做了增强或者测试集样本混进了训练集。解决先划分再增强增强只对训练集做用哈希或 ID 去重确保同一条原文只出现在一个集合。5.2 现象模型永远预测多数类原因类别不平衡且没加权重或者学习率太大导致模型直接躺平。解决加class_weight检查 loss 是否在下降如果 loss 不动降 lr 到 1e-4 再试。5.3 现象中性类 F1 极低正面负面还行原因中性标注口径不一致或者中性样本太少。解决重新对齐标注标准补中性数据也可以先把中性拆成“偏正”“偏负”再合并看模型是否能学到中间态。5.4 现象训练 loss 震荡剧烈甚至出现 nan原因梯度爆炸LSTM 常见。解决加clip_grad_norm_max_norm 设 5 或 1检查输入里有没有超长序列截断到 max_len。5.5 现象推理时同一句话每次结果不一样原因模型没切到 eval 模式dropout 还在生效。解决推理前调model.eval()并用torch.no_grad()包住。这个坑新手必踩记住 dropout 是训练期技巧推理必须关。6. 进阶技巧用注意力机制和错误分析把三分类再提一档LSTM 跑通后想再提点效果最划算的是加注意力机制。原理很简单让模型对情感词加权比如“质量好但价格贵”里“好”和“贵”权重都高模型能同时看到正负信号中性判断更准。class AttentionLSTM(nn.Module): def __init__(self, vocab_size, embed_dim128, hidden_dim128, num_classes3): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, bidirectionalTrue, batch_firstTrue) # 注意力打分层 self.attn nn.Linear(hidden_dim * 2, 1) self.fc nn.Linear(hidden_dim * 2, num_classes) def forward(self, x): emb self.embedding(x) out, _ self.lstm(emb) # (batch, seq_len, hidden*2) scores self.attn(out) # (batch, seq_len, 1) weights torch.softmax(scores, dim1) context torch.sum(weights * out, dim1) # 加权求和 return self.fc(context)attn是一个线性层把每个时刻的 hidden 映射成标量分数softmax 归一化成权重再对 hidden 加权求和。这样句尾的 padding 影响也被权重压下去了。加注意力后通常能提 1-3 个点尤其是中性类。另一个提效手段是错误分析。把验证集预测错的样本导出来人工看 50 条归类错误原因是标注错、分词错、还是模型没学到。我自己的习惯是每次调完参都跑一遍错误分析比盲目调参快得多。比如发现“不推荐”被分成正面多半是分词把“不”和“推荐”切开了加自定义词典就能救。最后说个部署上的小技巧推理时把模型转成torch.jit或者 ONNX速度能快 2-3 倍尤其是 batch 推理。但转之前一定确认model.eval()且输入维度固定否则转出来结果对不上。这个方案值不值得做如果你需要一个轻量、可解释、能自己改的中文三分类情感分析LSTM 加注意力是很好的起点源码和文档都在照着跑一遍比看十篇综述管用。希望帮到你。本文还有配套的精品资源点击获取