简介一份面向计算机相关专业学生、教师及企业员工的基于长短期记忆网络LSTM的中文文本情感分析毕业设计资源覆盖从中文分词、特征预处理到模型训练与评估的完整流程。数据集包含积极、消极中文文本各约8000条程序采用jieba分词完成关键预处理项目代码均测试通过、功能完整可直接用于毕业设计、课程设计或日常学习参考。压缩包共11个文件主要包含源码文件、说明文档、模型权重、词向量、配置参数及文本数据等整体大小约6.63MB目录结构清晰便于按需查阅。目前已有79人学习适合希望掌握中文自然语言处理预处理和深度学习建模的入门与进阶学习者。通过该项目可同时获得可运行的工程代码、预训练模型、配套数据集及操作指引便于快速复现实验结果并在此基础上进行改进拓展。1. 基于LSTM的文本情感分析这份毕业设计资源到底能做什么如果你正在为毕业设计选题发愁或者已经定下“文本情感分析”这个方向但不知道从哪下手这份基于LSTM的中文情感分析项目值得你先花十分钟搞明白它解决了什么问题。它不是一个空壳demo而是一套包含Python代码、训练好的模型、正负面各8000条标注好的语料以及配套说明文档的完整工程。拿到手之后你能直接看到一条从原始文本到情感概率输出的完整链路清洗分词、构建词表、搭建LSTM网络、训练评估、保存模型最后用模型对一条新评论做预测。这套资源适合两类人。第一类是把“基于LSTM的文本情感分析”作为毕业设计题目、需要快速产出可运行系统并写出论文的学生第二类是已经会用sklearn做一些简单分类、想用深度学习模型替换文本分类方案的在职开发者。前者能省掉从零收集数据和调通环境的两个星期后者能直接拿训练好的模型做二次开发。有一点我要提前说LSTM本身的原理并不难真正让新手翻车的地方全在数据预处理和训练细节上比如序列怎么填充、词表怎么对齐、梯度爆炸怎么处理。这篇笔记会把这条路上的关键节点和踩过的坑全部拆开讲清楚。2. 数据集与预处理正负面各8000条语料如何变成模型能吃的数值2.1 原始数据结构与标签分布拿到资源后第一步先看数据集的目录结构。常规做法是两种格式一种是一个CSV文件包含label和text两列另一种是pos和neg两个文件夹里面各放8000个txt文件。我拆过的项目里这份资源用的是CSV格式两列字段名分别是label0代表负面1代表正面和text评论文本一共16000行数据。建议先做一次标签分布检查确认数据没有明显倾斜。虽然正负面各8000条已经是平衡数据集但保不齐有人在复制过程中混入了空行或重复样本。我一般会用pandas快速过一遍import pandas as pd df pd.read_csv(data/sentiment_data.csv, encodingutf-8) print(df.head()) print(df[label].value_counts()) # 检查是否有空文本 print(df[text].isnull().sum()) # 检查重复行 print(df.duplicated().sum()) df df.drop_duplicates().reset_index(dropTrue)这段代码解决三件事查看前几行确认字段名是否和预期一致统计标签分布看正负样本数量去掉空文本和重复行。注意encoding参数Windows环境下CSV文件经常用gbk或gb18030编码如果读取报UnicodeDecodeError把编码换成gbk再试一次。这一步出错的比例很高很多人在这里卡了半小时。2.2 中文文本清洗与分词中文情感分析和英文最大的不同在于分词。英文按空格切分就行中文必须用分词工具。目前毕业设计里最常用的还是jieba因为安装简单、词典覆盖度够用。处理流程分两步先做文本清洗把网址、数字、表情符号、非中文字符处理掉再去做分词和停用词过滤。import re import jieba # 加载停用词表常见的中文停用词表网上可下载 stopwords set() with open(data/stopwords.txt, r, encodingutf-8) as f: for line in f: stopwords.add(line.strip()) def clean_text(text): # 去掉URL text re.sub(rhttp\S, , text) # 去掉非中文、非字母、非数字的字符 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) return text def tokenize(text): text clean_text(text) words jieba.cut(text) words [w for w in words if w.strip() and w not in stopwords] return .join(words) df[clean_text] df[text].apply(tokenize) print(df[clean_text].head())清洗这一步决定了模型上限。如果不过滤噪声字符模型会把表情符号和标点也当成语义特征学进去泛化能力大打折扣。[^\u4e00-\u9fa5a-zA-Z0-9]是Unicode范围匹配\u4e00-\u9fa5覆盖所有常用汉字。至于要不要删停用词毕业设计建议删因为“的”“了”“是”这类词对情感判断几乎没有贡献去掉之后词表体积能缩小不少训练速度也会快一些。2.3 词表构建与序列填充分词结束后需要把文本转成数字序列。这一步有两个关键操作构建词表映射和统一序列长度。模型只能吃数字所以每个词要对应一个唯一的整数索引。序列长度必须一致LSTM才能按batch训练这里的核心处理方式是pad_sequences。from collections import Counter import numpy as np # 构建词表 all_words [] for text in df[clean_text]: all_words.extend(text.split()) word_count Counter(all_words) vocab {word: idx 2 for idx, (word, count) in enumerate(word_count.items())} # 预留0给PAD1给UNK vocab[PAD] 0 vocab[UNK] 1 def text_to_sequence(text, vocab, max_len100): words text.split() seq [vocab.get(w, vocab[UNK]) for w in words] # 截断或填充到max_len if len(seq) max_len: return seq[:max_len] else: return seq [vocab[PAD]] * (max_len - len(seq)) X np.array([text_to_sequence(t, vocab) for t in df[clean_text]]) y np.array(df[label]) print(X shape:, X.shape) print(y shape:, y.shape)这里有几个参数需要说清楚。max_len100是最大序列长度它是根据数据集文本长度的分布来定的。我一般会先画一个长度分布直方图选择能覆盖90%文本的分位数如果大部分评论在三五十个字左右max_len设64就行如果文本较长再往128、256上调。idx2把0和1预留给了PAD和UNK这个细节很多人会漏掉导致维度不匹配。UNK用来替代词表外的词保证预测阶段遇到新词不会报错。预处理这块是整套项目里最容易被低估的部分。模型结构可以抄但数据处理的好坏直接决定最终准确率上下浮动五六个点。有一条血泪经验是词表必须在训练集上构建并保存下来预测时加载同一个词表否则会出现训练时好好的、一预测就报错的情况。具体原因后面避坑章节会展开。3. LSTM模型搭建PyTorch实现与核心参数设计3.1 从词的向量表示到LSTM层在PyTorch中搭建文本情感分类的LSTM模型通常由四个部分组成Embedding层、LSTM层、全连接分类头和Dropout。Embedding层的作用是把数字索引还原成稠密词向量它本身是一个可训练的查找表。这里的核心参数是vocab_size和embedding_dim前者是词表大小后者是每个词向量的维度常见取200或300。LSTM层的参数是input_size、hidden_size、num_layers和batch_first。input_size必须等于embedding_dim。每经过一个LSTM单元输出维度就是hidden_size大小。层数堆到两层以上能学到更高阶的语义组合但也更容易过拟合毕业设计场景下一层或两层就够了。import torch import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim, hidden_size, num_layers, num_classes, dropout0.5, bidirectionalTrue): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.lstm nn.LSTM( input_sizeembedding_dim, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, bidirectionalbidirectional, dropoutdropout if num_layers 1 else 0.0 ) # 如果是双向LSTM输出维度翻倍 lstm_output_dim hidden_size * 2 if bidirectional else hidden_size self.fc nn.Linear(lstm_output_dim, num_classes) self.dropout nn.Dropout(dropout) def forward(self, x): # x: [batch_size, seq_len] emb self.embedding(x) # [batch_size, seq_len, embedding_dim] lstm_out, _ self.lstm(emb) # [batch_size, seq_len, lstm_output_dim] # 取最后一个时间步的输出 last_out lstm_out[:, -1, :] # [batch_size, lstm_output_dim] out self.fc(self.dropout(last_out)) return out参数说明padding_idx0告诉Embedding层索引0对应的向量始终是全零并且不参与梯度更新这样填充的PAD位置不会干扰LSTM的学习。batch_firstTrue表示输入张量的形状是[batch, seq_len, embedding_dim]而不是PyTorch默认的[seq_len, batch, embedding_dim]强烈建议打开这个参数否则数据维度转置会把你搞晕。3.2 双向LSTM为什么能提升情感分类准确率双向LSTM是重点要理解的部分。普通LSTM按时间步从左往右读文本到了“太”这个位置它只知道前面有“这家店”不知道后面接的是“难吃了”。而双向LSTM同时跑两个方向前向LSTM保留左边上下文后向LSTM保留右边上下文最后在每一步把两个方向的隐状态拼接起来。这样最后一个时间步的输出实际上融合了整句话的信息对判断“味道差”“服务好”这种依赖语义位置的表达非常有效。用双向LSTM时要注意维度变化。hidden_size128的双向LSTM输出是256维因为前向和后向各128维拼接在一起。全连接层的输入维度必须跟着改成hidden_size * 2。如果不改运行时会直接报维度不匹配的错。3.3 损失函数与优化器选择情感分类是二分类问题损失函数用交叉熵损失。PyTorch的CrossEntropyLoss内部已经包含softmax变换所以模型最后一层不需要额外加softmax激活函数。优化器优先选Adam因为它在大多数场景下不需要手动调整学习率对新手友好。学习率初始值通常设在1e-3到1e-4之间太高会导致训练震荡太低则收敛缓慢。model LSTMClassifier( vocab_sizelen(vocab), embedding_dim200, hidden_size128, num_layers2, num_classes2, dropout0.5, bidirectionalTrue ) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) print(model)dropout0.5是防止过拟合的重要设置。Dropout随机让一半神经元失活迫使网络不依赖单一特征路径。这里有一个参数细节PyTorch的nn.LSTM只有在num_layers 1时才会在层间插入Dropout所以代码里做了条件判断。如果你只设置了一层LSTM还想加Dropout就必须像上面的代码一样在输出后显式调用self.dropout。词表的len(vocab)在预处理阶段已经确定在使用模型前务必确认这个值和保存词表的大小一致。我遇到过有人重新训练了一套词表但加载模型时还在用旧的vocab_size结果Embedding层的维度对不上模型参数加载直接报错。这种问题排查起来非常浪费时间。4. 训练、评估与调参让模型在验证集上稳定跑出90%以上的准确率4.1 数据划分与DataLoader构建训练前要把数据切成训练集、验证集和测试集三个部分。训练集用来更新模型参数验证集用来观察模型是否过拟合、调整超参数测试集只在最后做一次评估。常见比例是8:1:1同时必须保证切分是随机的不能出现正样本全在训练集、负样本全在测试集的情况。from sklearn.model_selection import train_test_split from torch.utils.data import TensorDataset, DataLoader X_train, X_temp, y_train, y_temp train_test_split( X, y, test_size0.2, random_state42, stratifyy ) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size0.5, random_state42, stratifyy_temp ) train_dataset TensorDataset(torch.LongTensor(X_train), torch.LongTensor(y_train)) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) val_dataset TensorDataset(torch.LongTensor(X_val), torch.LongTensor(y_val)) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse)stratifyy这个参数很多人会忽略。它保证切分后的训练集和临时集合里正负样本比例与原始数据一致避免随机切分带来的分布偏移。batch_size64不是拍脑袋定的它取决于显存大小和数据量级。16000条数据训练集大约12800条64的batch size会在训练时更稳定梯度更新方向更平滑。4.2 训练循环与梯度裁剪训练循环的逻辑本身不复杂但有几个细节直接决定了训练是否有效梯度裁剪、学习率调整时机、每个epoch跑完后在验证集上做一次评估。梯度裁剪是LSTM训练中非常重要的操作因为长序列在反向传播时梯度容易指数级增长导致loss变成NaN。epochs 10 best_val_acc 0.0 for epoch in range(epochs): model.train() total_loss 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() # 梯度裁剪防止梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() # 验证阶段 model.eval() correct 0 total 0 with torch.no_grad(): for batch_x, batch_y in val_loader: logits model(batch_x) preds torch.argmax(logits, dim1) correct (preds batch_y).sum().item() total batch_y.size(0) val_acc correct / total print(fEpoch {epoch1}/{epochs} | Loss: {total_loss/len(train_loader):.4f} | Val Acc: {val_acc:.4f}) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth)model.train()和model.eval()是PyTorch里容易踩坑的地方。训练模式启用Dropout和BatchNorm的统计更新评估模式关闭Dropout并使用累积的统计值。如果忘了切换回train()模式就继续训练模型会带着Dropout的随机性更新参数验证准确率就会出现莫名其妙的波动。clip_grad_norm_的max_norm1.0是参数上限超过这个范数的梯度会被等比缩放常见取值范围在0.5到5之间。4.3 学习率调整与提前停止固定学习率训练到后面往往会出现loss在某个平台期反复震荡。我的习惯是第5轮之后把学习率降低一个数量级让模型在原有参数基础上做小范围微调。from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience2)ReduceLROnPlateau会在loss连续patience个epoch没有下降时把学习率乘以factor。这里patience2表示连续两个epoch验证loss不降就触发衰减。实际训练中往往到不了设定的10个epoch就会在验证集上出现过拟合所以要记录最佳验证准确率准确率下降时保存当前最好的模型。代码里每次验证完如果val_acc提升就覆盖保存best_model.pth这个文件是训练结束后的核心产物。4.4 测试集评估与F1值毕业设计论文里不能只放一个准确率测试集上的精确率、召回率和F1值是必写的内容。训练好的模型在测试集上做一次完整评估from sklearn.metrics import classification_report, confusion_matrix model.load_state_dict(torch.load(best_model.pth)) model.eval() all_preds [] all_labels [] with torch.no_grad(): for batch_x, batch_y in test_loader: logits model(batch_x) preds torch.argmax(logits, dim1) all_preds.extend(preds.numpy()) all_labels.extend(batch_y.numpy()) print(classification_report(all_labels, all_preds, target_names[负面, 正面])) print(confusion_matrix(all_labels, all_preds))分类报告里的F1值是精确率和召回率的调和平均在正负样本不平衡时比准确率更能反映模型真实水平。这份资源虽然数据是平衡的但论文里能写出F1值的变化趋势会让评审老师觉得你做了更深入的分析。混淆矩阵可以看出模型更容易把负面误判为正面还是反过来这个结果直接决定了后续是补负面数据还是调低正面的分类阈值。5. LSTM情感分析避坑指南五个最常见的翻车现场5.1 预测时报错“词索引超出词表范围”现象模型训练一切正常但拿一条新评论去预测时Embedding层报错index out of range。原因预测时输入的新文本里出现了训练词表中不存在的词构建序列时又没有做UNK映射兜底导致这个词的索引等于词表长度加1超出了Embedding矩阵的行数范围。解决预处理阶段构建词表时把所有不在词表中的词映射到vocab[UNK]代码在2.3节已经给出。预测时使用的词表字典必须和训练时完全同一个对象也就是要把词表保存成JSON文件预测前加载。我一般会在text_to_sequence里加一个兜底判断如果word不在词典里用vocab.get(word, vocab[UNK])永远不要把生词直接跳过或排到序列尾部。5.2 训练集loss下降但验证集准确率不涨现象训练loss从1.2降到0.3看起来模型学得很好但每轮epoch验证准确率始终在70%到75%之间徘徊。原因过拟合已经发生模型把训练集里的噪声和特定措辞死记硬背下来了没有学到可泛化的情感判断规则。另一层原因可能是序列长度截断不合理max_len100时把关键的转折词丢弃了。解决先从数据入手把max_len调大覆盖更多完整文本观察验证准确率有没有变化。然后增大Dropout比例到0.5甚至0.6。如果两层LSTM仍过拟合降到一层试试。还有一种情况是学习率过大导致参数在小范围内反复震荡把学习率降到1e-4重跑。5.3 训练到一半loss变成NaN现象训练进行到第几轮时loss突然变成nan之后所有梯度更新都无效。原因梯度爆炸。LSTM在反向传播时梯度范数呈指数级放大数值精度溢出变成非数值。解决在loss.backward()之后、optimizer.step()之前加入梯度裁剪在4.2节的代码里已经写了nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。如果裁剪后还有NaN检查数据里是否有极端长文本或全部是PAD的样本那种样本会让LSTM看到全零序列梯度计算不稳定。5.4 每次跑同一个模型结果都不一样现象同一个训练代码、同一个数据两次训练出来的测试集准确率能差两个百分点提交论文时的结果无法稳定复现。原因PyTorch的随机初始化、数据加载时的shuffle顺序、某些CUDA操作的原子性都会引入随机性。解决训练代码开头固定随机种子。Python层面设置random.seed(42)和np.random.seed(42)PyTorch层面设置torch.manual_seed(42)和torch.cuda.manual_seed_all(42)。同时把DataLoader的shuffle开关固定住。这样在同一台机器、同样版本的依赖环境下结果能基本复现。需要说明的是不同机器上因为硬件浮点运算差异结果仍然会略有浮动这属于正常现象。5.5 模型文件很大但加载到内存报错现象torch.load(best_model.pth)之后实例化模型报错提示size mismatch。原因保存模型权重时用的是当前模型的state_dict但加载时创建的模型实例参数维度不一致。最常见的情况是修改了hidden_size或改动了vocab_size之后没有重新保存模型。解决加载模型前后必须保证模型的超参数完全一致。把这个逻辑固定写成一个build_model()函数加载模型前调用它创建实例再调用load_state_dict。如果之前保存的模型来自一份不同的词表那就只能重新训练或者用原项目里配套的config.py里的参数从头创建。6. 模型推理与落地把训练好的模型用起来模型训练完成只是毕业设计的前半程后半程是让模型能对一条新的评论文本输出情感置信度。这里我通常会把推理封装成一个独立的函数输入原始文本输出正负面概率值。这样做的好处是后续无论是接Flask接口还是做GUI界面都只需要调用这一个函数。import json import torch.nn.functional as F # 保存词表 with open(vocab.json, w, encodingutf-8) as f: json.dump(vocab, f, ensure_asciiFalse) def load_model_and_vocab(model_pathbest_model.pth, vocab_pathvocab.json): with open(vocab_path, r, encodingutf-8) as f: saved_vocab json.load(f) model_params { vocab_size: len(saved_vocab), embedding_dim: 200, hidden_size: 128, num_layers: 2, num_classes: 2, dropout: 0.5, bidirectional: True } model LSTMClassifier(**model_params) model.load_state_dict(torch.load(model_path, map_locationcpu)) model.eval() return model, saved_vocab def predict_sentiment(text, model, vocab, max_len100): tokens tokenize(text) seq text_to_sequence(tokens, vocab, max_len) input_tensor torch.LongTensor([seq]) with torch.no_grad(): logits model(input_tensor) probs F.softmax(logits, dim1) pred torch.argmax(probs, dim1).item() return pred, probs[0].tolist() model, vocab load_model_and_vocab() result predict_sentiment(这家店的服务态度很好上菜速度也快下次还会来。, model, vocab) print(预测结果:, result)注意load_state_dict里的map_locationcpu参数如果你在GPU上训练但在本机CPU上做推理不写这个参数会报CUDA不可用的错。F.softmax把logits转成概率分布两类的概率之和为1阈值默认取0.5如果负面概率超过0.5则判定为负。推理速度单条不到10毫秒完全能满足简单的实时对话交互。如果你想把模型接到Web端用Flask包一层HTTP接口请求体传text字段返回JSON格式的预测标签和置信度。前端可以是一个简单的HTML页面用户输入文字点击按钮就能看到正负面判断。这套架构用在毕业设计答辩展示上是完全够用的。最后说一个我做项目时养成的习惯拿到任何训练好的模型第一件事不是直接部署而是永远先跑一次测试集评估把准确率和F1值打出来看一眼。因为模型文件可能来自不同的训练阶段不跑一次根本不知道当前这个权重到底处于什么水平。从那以后我每接手一个模型都会强制走这遍流程包括这个项目里的best_model.pth我建议你也这样做一次确认当前的准确率表现后再往下做界面或接口希望帮到你。本文还有配套的精品资源点击获取