ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LSTM中文短文本情感分析:基于PyTorch的完整源码与训练调参实战

LSTM中文短文本情感分析:基于PyTorch的完整源码与训练调参实战 简介一份基于长短期记忆网络LSTM的中文短文本情感分析项目源码专门面向需要完成期末大作业或课程设计的计算机相关专业学生也适合作为入门深度学习文本分类的练手项目。项目使用Python语言编写完整覆盖中文语料预处理、分词、词向量构建、模型训练与情感预测等环节代码结构清晰运行环境配置简单下载后无需修改即可直接运行并得到结果。压缩包共14个文件主要包含Python脚本负责模型训练与推理、文本格式的正负样本数据、CSV标注文件、训练好的模型权重文件以及说明文档整体大小仅1.96MB轻量易用且便于二次开发。目前已有244人学习下载项目稳定可靠附带的系统说明和README文档能帮助使用者快速理解每个模块的作用。通过该资源读者既能收获一个可直接提交的高分作业原型也能在现有代码基础上自由调整参数、更换数据集深入理解长短期记忆网络在情感分析任务中的实际应用为课程答辩或后续研究打下扎实基础。1. 短文本情感分析为什么这份 LSTM 源码值得拆开看中文短文本情感分析是 NLP 情感分析里最容易见效的入门任务也是 Python 课程设计与期末大作业中反复出现的选题。多数人交上去的版本是先用 jieba 分词再统计正负样本里词频最后拿朴素贝叶斯或者 SVM 交差能跑通但讲不出什么模型深度。这份源码的价值在于它把问题提到了序列建模层面——用 LSTM 把“词序”也当成特征让文本分类从词袋模型跨到深度学习在答辩时既讲得了原理也拿得出训练曲线和预测样例。本文会沿着源码包里的 load_data.py、sentiment_analysis.py、test.py 三条主线把数据处理、模型结构、训练调参完整拆一遍适合正在做期末大作业、课程设计或者想快速搭一套中文情感分类基线系统的读者。2. 数据准备从 CSV 原始语料到可训练的 index 序列2.1 源码包里每个文件是干什么的先看清包里有什么再说怎么跑。下面这张表是我按工程用途重新归类的和 zip 里原始的散落结构不完全一致但对应关系很清楚文件类型作用1170300418.csv原始语料带标签的中文短文本数据一般包含 label 和 text 两列load_data.py脚本读取 CSV、分词、构建词表、生成训练/测试用张量cut.txt中间产物分词后的文本缓存方便排查分词异常label_data.pt数据文件PyTorch 序列化后的样本数据包含 input_ids、labels、词表等sample.positive.txt / sample.negative.txt人工标注样本正负例各若干条用来快速验证模型方向是否反了test_data / test.txt测试集做最终评估用的短文本sentiment_analysis.py主脚本定义 LSTM 模型执行训练与评估test.py预测脚本加载训练好的模型对单条文本做预测readme-情感分类系统说明.txt文档项目说明与运行步骤这里label_data.pt是整个数据管线的核心产物。既然包里已经有它理论上可以直接训练但课程设计答辩一定会被追问“文本是怎么变成张量的”所以 load_data.py 才是真正要讲清楚的部分。2.2 文本索引化的标准流程我拆过不少类似的课程设计常见做法是下面这条链路读取 CSV → jieba 分词 → 过滤停用词与单字 → 构建 word2idx 词表 → 按固定长度截断/补齐 → 保存为.pt文件。load_data.py 里最重要的逻辑可以用下面这段代码概括import jieba import torch from torch.utils.data import TensorDataset, DataLoader from collections import Counter STOP_WORDS {的, 了, 是, 在, 和, 有, 就, 不} # 常用停用词集合 def build_vocab(texts, min_count2): 统计词频保留出现次数 min_count 的词 counter Counter() for text in texts: words jieba.lcut(text) # 精确模式分词 counter.update([w for w in words if w not in STOP_WORDS and len(w) 1]) word2idx {w: i 2 for i, (w, _) in enumerate(counter.most_common())} word2idx[PAD] 0 word2idx[UNK] 1 return word2idx def text_to_sequence(text, word2idx, max_len64): 把单条文本转成定长 index 序列超出截断不足补 PAD words [w for w in jieba.lcut(text) if w in word2idx] ids [word2idx.get(w, word2idx[UNK]) for w in words[:max_len]] if len(ids) max_len: ids [word2idx[PAD]] * (max_len - len(ids)) return ids这段代码解决了三个问题。第一min_count2把只出现过一次的生僻词过滤掉能显著压缩词表大小减少 Embedding 层参数量第二用len(w) 1再滤掉一部分单字——中文单字在很多评论文本里是语气助词比如“啊”“呢”对情感判别贡献低第三PAD和UNK分别占用 0 和 1 号位置保证后续 Embedding 层的 padding_idx 有固定出口。2.3 生成 .pt 文件时的版本坑label_data.pt是torch.save()的产物加载时用torch.load(label_data.pt)。这里有一个很多人踩过的坑PyTorch 的.pt文件本质是 pickle 序列化Python 大版本不兼容会直接报UnicodeDecodeError或者AttributeError。这份源码包里出现了load_data.cpython-36.pyc说明原始环境是 Python 3.6如果你本机是 Python 3.8 以上加载时偶尔会碰到pickle协议层面的报错。提示.pt加载报错第一反应不要删文件重跑先看报错是 pickle 版本问题还是张量维度问题。前者可直接在这台机器上重新执行 load_data.py 生成新 .pt后者要去检查文本截断长度是否一致。加载后建议立刻打印结构确认data torch.load(label_data.pt) if isinstance(data, dict): print(data.keys()) # 期望看到 input_ids / labels / word2idx 等键 print(data[input_ids].shape) elif hasattr(data, tensors): print(data.tensors[0].shape) # TensorDataset 的写法这一步能帮你快速判断数据是否已经截断成定长张量也决定了后面 DataLoader 的 batch 参数怎么设置。3. 模型结构Embedding、双层 LSTM 与最后一帧信息提取3.1 为什么短文本情感分析用 LSTM 而不是普通 RNN对于一句话里隐藏的情感倾向关键词的语境价值远高于词频。以“这家店真不算好吃”为例词袋模型会把“好吃”当成正向证据但“不算”在它前面把语义反转成了负向。普通 RNN 在反向传播时梯度要沿时间步连乘长距离信息几乎传不回去LSTM 通过输入门、遗忘门、输出门三个门控结构让梯度有一条“高速公路”信息可以在记忆单元里保留多个时间步。短文本虽然句子不长但 LSTM 的结构解释性对答辩非常有用这也是这份源码选它的核心原因。模型定义部分在 sentiment_analysis.py 里核心结构可以拆成四层Embedding 层把词索引映射成稠密向量Dropout防止小数据下过拟合双层 LSTM第二层在第一层的输出基础上继续建模Linear Softmax把最后一步的 hidden state 映射成类别概率3.2 LSTMClassifier 的 PyTorch 实现import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim128, hidden_size256, num_layers2, num_classes2, dropout0.3, pad_idx0): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idxpad_idx) self.lstm nn.LSTM(embedding_dim, hidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size, num_classes) def forward(self, x): # x: [batch_size, seq_len] emb self.embedding(x) # [batch, seq_len, embedding_dim] outputs, (h_n, c_n) self.lstm(emb) # outputs: 所有时间步输出 last_hidden h_n[-1] # 取最后一层的 final hidden state logits self.fc(self.dropout(last_hidden)) return logits这里有一个关键设计outputs保留了每个时间步的隐层输出但情感分类用的是整句话的语义所以只取h_n[-1]——也就是 LSTM 读完整句话之后记忆单元里的最终状态。对中文短文本来说这个设计的直觉是读完最后一个词时模型已经把所有信息压缩进了这一个向量里。如果你更谨慎后期可以让模型只用“最后一个位置”的 hidden state而不是整个序列的平均值这两者在答辩时常被问区别。3.3 模型参数如何对应这份数据集课程设计的数据量通常在几千条上下vocab_size 一般在 5000 到 20000 这个区间。下面这套参数是我验证过比较稳的组合也基本覆盖这份源码的默认选择参数取值选型依据embedding_dim128短文本词表小128 维足够表达语义再大只会增加训练时间hidden_size256容量适中能承接 128 维的词向量输入num_layers2单层欠拟合常见三层以上在几千条数据上容易过拟合dropout0.3训练时随机丢弃 30% 神经元抑制过拟合padding_idx0与词表里PAD的索引一致Embedding 对 PAD 输出零向量batch_firstTrue输入张量形状是 [batch, seq_len]符合直觉方便调试打印batch_firstTrue这个参数看起来不起眼但它决定了你x的维度是[batch, seq_len]还是[seq_len, batch]。用错的话训练时不会报错但准确率会不稳定排查起来很费时间。第一次搭模型建议先打印一遍每层输出的 shape确认从[64, 64]到[64, 256]再到[64, 2]的走向。4. 训练与评估损失函数、优化器选择和过拟合信号判断4.1 训练循环的骨架有了模型和数据训练部分的核心是三个选择交叉熵损失函数、Adam 优化器、固定随机种子。PyTorch 的实现可以直接套下面这套循环import torch.optim as optim from sklearn.metrics import accuracy_score, f1_score model LSTMClassifier(vocab_sizelen(word2idx)) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) for epoch in range(15): model.train() total_loss 0 for batch_x, batch_y in train_loader: optimizer.zero_grad() logits model(batch_x) # [batch, 2] loss criterion(logits, batch_y) # batch_y 必须是 LongTensor loss.backward() optimizer.step() total_loss loss.item() # 每轮结束评估一次验证集 model.eval() with torch.no_grad(): preds, trues [], [] for batch_x, batch_y in val_loader: logits model(batch_x) preds.extend(logits.argmax(dim1).tolist()) trues.extend(batch_y.tolist()) print(fepoch {epoch} | loss {total_loss:.4f} | acc {accuracy_score(trues, preds):.4f})选择损失函数时二分类任务经常有人问要不要用BCEWithLogitsLoss。就这个项目而言输出层是两个节点所以CrossEntropyLoss是更直接的选择它内部整合了 Softmax 和 NLLLoss预测时直接argmax(dim1)就能得到类别索引。注意标签batch_y必须是torch.long类型这也是label_data.pt里标签张量定义时要确认的一点。4.2 训练日志怎么看训练日志不是只看 acc。第 1 个 epoch 大概会看到 loss 在 0.6 到 0.7 附近acc 在 50% 上下徘徊这说明模型还在“学词表”没有明显倾向。到第 3 到第 5 个 epochacc 会跳到 80% 以上loss 也会稳步下降。如果第 2 个 epoch 就出现 acc 90% 以上的情况不要高兴太早大概率是样本分布失衡或者验证集与训练集有重复。我一般会用下面几行代码同时记录损失和指标把每个 epoch 的结果落盘log {epoch: epoch, loss: total_loss / len(train_loader), val_acc: accuracy_score(trues, preds), val_f1: f1_score(trues, preds, averagebinary)} with open(train_log.csv, a) as f: f.write(f{log[epoch]},{log[loss]:.4f},{log[val_acc]:.4f},{log[val_f1]:.4f}\n)4.3 过拟合的三个信号几千条中文短文本在双层 LSTM 上很容易过拟合最常见的三个信号是训练 loss 持续下降但验证 acc 停滞训练 acc 接近 1.0 而验证 acc 掉头向下以及同一句话在训练集和测试集上预测结果完全相反。遇到这些情况按优先级依次尝试增大 dropout从 0.3 调到 0.5减小 hidden_size从 256 降到 128在训练循环里判断验证 acc 是否连续 3 轮不升是则提前终止提示不要一开始就用早停先确认 baseline 能收敛再加限制条件。早停的 patience 设 3 到 5 都比较合理。这份源码的另一个可看点是用sample.positive.txt和sample.negative.txt做方向性验证。有时候模型 acc 很高但预测结果全是负向——比如标签 0 和 1 的定义在 CSV 里跟模型输出反了。这时别急着调参先拿两三个明显正面的样本跑一遍预测脚本确认方向再回到训练循环里找问题。5. 答辩高频问题与一条新文本预测的完整验证5.1 单条文本预测test.py 的完整逻辑test.py做的事是“把训练好的模型加载回来对一条新评论做预测”。下面这个版本兼容了模型保存格式不确定的情况import torch import jieba def predict(text, model, word2idx, max_len64): model.eval() words [w for w in jieba.lcut(text) if w in word2idx] ids [word2idx.get(w, 1) for w in words[:max_len]] ids [0] * (max_len - len(ids)) x torch.tensor([ids], dtypetorch.long) with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim1) pred torch.argmax(logits, dim1).item() return pred, prob[0][pred].item() # 加载时兼容两种保存格式 checkpoint torch.load(best_model.pt) if isinstance(checkpoint, dict) and model_state in checkpoint: model.load_state_dict(checkpoint[model_state]) else: model.load_state_dict(checkpoint) pred, score predict(外卖送得慢而且包装还破了, model, word2idx) print(pred, round(score, 4)) # 期望输出类别 0负向参数说明max_len64必须和训练时保持一致否则 LSTM 读到的序列长度不同prob[0][pred]取的是预测类别的置信度答辩时可以把它解释成模型对判断的“确定程度”。5.2 课程设计答辩经常追问的四个问题第一个是“为什么取h_n[-1]而不是outputs[:, -1, :]”。两者在单层 LSTM 里结果几乎一样但h_n[-1]在多层时取的是最后一层的最终隐状态语义上是“模型读完整句话后各层压缩出的最终表示”回答思路更干净。第二个是“双向 LSTM 会不会更强”。对短文本会略微提升因为它能看到每个词前后两侧的上下文但模型参数量翻倍几千条数据上容易过拟合这份源码用单向 LSTM 是正确的稳妥选择。第三个是“类别不平衡怎么处理”。如果是 9:1 的正负样本先看 CSV 里有没有原始分布统计再考虑给CrossEntropyLoss传weight参数正类数量除以负类数量得到一个权重比用它放大少数类的损失。第四个是“为什么用 LSTM 不用 BERT”。标准答案是推理代价低、参数少、几千条数据就能训起来BERT 微调在这份数据量级上优势不明显。5.3 最后一个实操技巧跑完评估之后补一组人工回归训练结束后不要急着交付写一段最短的回归脚本把包里的sample.positive.txt和sample.negative.txt逐行送进模型python test.py --input sample.positive.txt --checkpoint best_model.pt每个文件里只要有一条预测方向和文件命名相反就说明数据方向定义或者标签映射有问题直接重新整理 CSV 比调参效率高。把这条命令作为最终验证手段整个项目从数据到模型再到预测链路就真正闭环了。本文还有配套的精品资源点击获取
返回列表