基于LSTM的中文文本情感分析Python毕设源码实战解析
简介面向计算机相关专业毕业设计场景这是一套基于LSTM的中文文本情感分析完整实现包含积极与消极评论各约8000条的训练数据集、jieba分词预处理、Word2Vec词向量训练与LSTM模型构建代码并附有训练好的模型文件及说明文档适合需要从数据预处理到模型评估全流程参考的学生或开发者。压缩包共8个文件以Python脚本、txt数据、yml配置、h5/pkl模型文件及md说明为主整体仅6.62MB结构简洁且便于直接运行与二次修改。目前已有379人学习下载。资源内代码均测试通过除模型与数据外还包含README说明及项目结构示意可支持答辩演示、课程设计或情感分析任务的方法对比下载后可按文档快速复现训练流程并在此基础上替换语料、调整网络参数以适配其他文本分类需求。1. 基于 LSTM 的文本情感分析这套 Python 毕设源码到底值不值得用中文文本情感分析是 NLP 入门最常碰到的任务LSTM 又是深度学习课程里必讲的网络所以拿 LSTM 做情感分析几乎成了计算机专业毕业设计的高频选题。这套 Python 源码正好把这条链路整体打包了数据、分词、词向量、LSTM 模型训练、预测脚本全都有积极和消极文本各 8000 条模型也提前训练好了。适合三类人做毕业设计或课程设计的学生需要快速跑通 LSTM 情感分析全流程的初学者以及想在已有代码上改成自己数据集的工程师。下载后先读 README再动代码能省掉大半调试时间。2. 拆压缩包先读懂 data、model、code 三个目录再动手拿到压缩包后第一件事不是双击运行 lstm.py而是先把目录结构看一遍。这种项目最容易翻车的不是模型训练而是文件路径对不上、格式没搞清楚就跑最后报一堆 FileNotFoundError。README.md 里通常写了环境依赖和运行顺序我建议先把它读完再碰代码。作者把代码、数据和模型分三个目录放其实已经暗示了运行顺序先处理数据再加载或训练模型最后是预测验证。2.1 文件清单拆解每个文件在整条链路里的角色压缩包里一共两层目录核心是 data、model、code 三个文件夹外加 README.md 和一张说明图。先用表格把文件职责理清楚。路径文件职责datapos.txt积极情感语料每行一条文本约 8000 条dataneg.txt消极情感语料每行一条文本约 8000 条modelWord2Vec.pkl训练好的词向量模型pickle 序列化modellstm.ymlLSTM 网络结构定义YAML 格式modellstm.h5训练好的网络权重HDF5 格式codelstm.py从数据读取到训练保存的完整脚本根目录README.md / LSTM-Sentiment_analysis.png环境说明、运行步骤与结果图示从表格能看出作者的保存习惯网络结构用 YAML权重用 HDF5词向量用 pickle。这种分开保存的方式比直接model.save(lstm.h5)要稳因为预测阶段可以只加载结构和权重不需要重新编译训练配置Keras 版本波动时也很好排查。提示每次拿到这类资源我先做一件事把 README 里的运行顺序记下来。作者在打包说明中写了“下载后请首先打开 README.md”这不是例行公事而是因为代码里多个文件存在依赖关系训练前要读 data/pos.txt 和 data/neg.txt训练后要把权重和结构分别写到 model/ 目录任何一个相对路径被改动代码都要跟着调。按 README 的顺序走一遍流程能避开文件找不到、编码错乱、模型加载失败这三类最常见的报错。2.2 数据集结构与标注约定先确认正负样本怎么组织data 目录下两个 txt 文件是全部训练语料先打开看几行确认格式。# 在 data 目录下查看前 3 行并统计总行数 head -n 3 pos.txt wc -l pos.txt neg.txthead -n 3只打印每个文件的前 3 行用于快速目测文本格式wc -l输出两个文件的总行数用来核对是不是真的各 8000 条左右。pos.txt 里每条文本都是正面评价neg.txt 里都是负面评价文件的每一行是一条样本没有表头也没有标签列。训练时的标签完全由文件位置决定读 pos.txt 时标签记为 1读 neg.txt 时标签记为 0。这个设计很简单但容易踩坑比如追加语料的时候两个文件行数没对齐模型就会学到错误映射。我会在读完两个文件后立刻打印行数和平均长度做校验。import codecs pos_path data/pos.txt neg_path data/neg.txt with codecs.open(pos_path, r, encodingutf-8) as f: pos_lines [line.strip() for line in f if line.strip()] with codecs.open(neg_path, r, encodingutf-8) as f: neg_lines [line.strip() for line in f if line.strip()] print(neg 样本数:, len(neg_lines)) # 约 8000 print(pos 样本数:, len(pos_lines)) # 约 8000 print(neg 平均字数:, sum(len(line) for line in neg_lines) / len(neg_lines)) print(pos 平均字数:, sum(len(line) for line in pos_lines) / len(pos_lines))这里用codecs.open并明确指定 utf-8 编码是为了防止 Windows 下默认 GBK 读取导致乱码。过滤掉空行是为了防止文件末尾的换行符被当作一条空样本。打印样本数的目的是确认两个文件的行数没有明显差距平均字数则是后面定max_len的依据如果两类平均字数差异过大截断策略就不应该一刀切否则长文本信息会被大量截掉。这套数据里积极和消极各约 8000 条分布基本均衡训练时不需要额外设置 class_weight。2.3 训练产物Word2Vec.pkl 与 lstm.h5 是给预测准备的model 目录里已经放好了训练产物这意味着下载下来以后即使完全不训练也能直接对文本做情感预测。Word2Vec.pkl保存的是 gensim 的 Word2Vec 模型里面包含训练时用到的全部词表以及每个词的向量表示lstm.h5是训练收敛后的权重文件lstm.yml保存的是网络结构。预测阶段的标准加载流程是先用 YAML 恢复网络结构再加载权重再用 Word2Vec 里的词表把输入文本转成索引序列。之所以说这份资源对毕设友好是因为省去了最耗时的训练步骤答辩演示时可以现场预测几条文本也可以重训看训练曲线。如果只是写课程报告直接加载已有产物就足够了。另外Word2Vec.pkl不只是保存向量数值还会把训练语料里出现的每个词连同向量一起序列化。预测新文本时需要先查这个词是否在词表里词表外的新词统一走 UNK 分支。如果没有这份词表纯靠一个向量矩阵做映射就得自己维护一份额外的单词列表这也就是Word2Vec.pkl必须原样保留的原因。我在第一次替换语料时把Word2Vec.pkl删掉后直接加载旧权重结果预测结果完全随机最后才发现是词表映射断了。注意加载lstm.h5时如果报 h5py 相关错误请先检查 Keras 和 TensorFlow 的版本最常见的原因是 Keras 2.x 与 1.x 的 h5 保存格式差异。3. 文本预处理jieba 分词之外还有清洗和序列化两个脏活跑通整个项目之前先把预处理链路讲清楚。中文文本不能像英文那样按空格切词必须分词或分字这一步直接决定模型输入质量也是整个项目里最花时间的环节。代码里这部分集中在lstm.py的开头替换成自己的数据集时改动最多的也是这段。3.1 jieba 分词为什么够用中文分词的两个方向中文文本建模前需要把句子变成词序列或字序列常见两个方向一是用 jieba 做分词二是直接按字切分。这两种方案适用场景不一样对比下来各有取舍。方案优点缺点适用场景jieba 分词保留语义单元模型输入维度低新词、网络用语识别弱常规评论、新闻、商品评价按字切分无词典依赖不会切错序列变长丢失词组信息专业术语多、文本噪声大这套项目选用 jieba 分词因为情感分析面对的是日常评论文本词汇层面的特征对情感判断更直接。例如“这部电影真难看”切词后是“这 / 部 / 电影 / 真 / 难看”“难看”作为整体进入词表模型很容易学习到负面倾向。如果按字切分“难”和“看”单独进网络上下文依赖会更重一些训练数据不足时反而不容易收敛。做毕设时如果想在报告里加一组对比实验可以保留分字的写法跟 jieba 分词跑同一份数据但那样需要把训练语料重新编码一遍工程量和收益要自己权衡。3.2 清洗与停用词训练前的必要步骤分词之前先做工整的文本清洗否则标点、数字、特殊符号会全部变成无意义词占词表位置。常见的处理步骤是去空白、去数字和特殊字符、按 jieba.cut 切分、再过滤停用词。import re import jieba stopwords set(open(stopwords.txt, encodingutf-8).read().split()) def clean_text(text): # 去掉 URL、数字、英文只保留中文字符和常用标点 text re.sub(rhttps?://\S|www\.\S, , text) text re.sub(r[0-9], , text) text re.sub(r[a-zA-Z], , text) return text.strip() def tokenize(text): text clean_text(text) words jieba.cut(text, cut_allFalse) return [w for w in words if w and w not in stopwords]clean_text里的三个正则分别处理 URL、数字、英文这三类字符对情感判断贡献很小留在词表里只会增加噪声。tokenize中cut_allFalse是精确模式也是情感分析任务默认选项全模式虽然召回率高但会产生大量无效词组比如把“这部电影”同时切成“这部”和“电影”不适合直接作为模型输入。停用词表可以用网上的中文停用词表也可以自己统计高频无意义词像“的”“了”“吗”“就”这类词删掉后词表能缩小 20% 以上。3.3 序列化与 padding把中文词变成模型能吃的数字分词完成后模型拿到的还是一串字符串需要映射成整数索引并且让所有句子长度一致才能送入 LSTM。from collections import Counter import numpy as np word_count Counter() for words in tokenized_corpus: word_count.update(words) # 按词频构建词表低频词计入 UNK vocab {word: i 1 for i, (word, _) in enumerate(word_count.most_common(50000))} UNK_ID len(vocab) 1 def encode(words, max_len80): ids [vocab.get(w, UNK_ID) for w in words] if len(ids) max_len: ids ids[:max_len] # 超长截断 else: ids ids [0] * (max_len - len(ids)) # 短则补零 return np.array(ids, dtypeint32)词表用词频排序高频词编号小这样 Embedding 层的输入空间比较紧凑。max_len决定了句子的统一长度这套语料平均字数在几十字左右取 80 是稳妥默认值。短句补 0、长句截断是 LSTM 任务里的常规做法补 0 不会影响训练梯度因为 Embedding 会把索引 0 映射成全零向量。提示vocab.get(w, UNK_ID)会把训练时没见过的词统一映射成 UNK 的编号这份资源预测新文本时也会走这个分支。如果新文本里生僻词太多整条句子会被 UNK 刷屏预测结果基本失去意义。4. 训练与保存Embedding LSTM Dropout 的结构和关键参数预处理做完下一步就是词向量和网络训练。这一章的参数直接影响最终精度同时也是后端运行时最常改动的部分。复现项目时我习惯把lstm.py里的参数全部打印出来和 README 说明对照一遍再训练。4.1 先训练 Word2Vec词向量维度与窗口怎么定模型输入是词索引但真正送进网络的是词向量。项目用 gensim 的 Word2Vec 把语料里的每个词训练成稠密向量再嵌入到 LSTM 里。from gensim.models import Word2Vec sentences tokenized_corpus # 词序列列表 w2v_model Word2Vec( sentencessentences, vector_size100, # 每个词的向量维度 window5, # 上下文窗口大小 min_count1, # 词频不少于 1 的词进入词表 workers4, # 并行线程数 epochs5 ) w2v_model.save(Word2Vec.pkl) # gensim 底层是 pickle 格式vector_size100是中小型语料的常见选择。向量维度太小词组信息不够维度太大训练参数暴涨8000 条样本容易过拟合。window5是 gensim 默认值表示每个词只看前后 5 个词以内的上下文对评论文本够用。min_count1意味着所有出现过至少一次的词都保留对这个小语料是合理的因为删词会严重缩小词表导致很多文本无法编码。save到 pkl 后预测阶段用同一个 gensim 版本加载即可后续接网络时只需要w2v_model.wv里的词向量。4.2 网络结构LSTM 层的神经元数不是越大越好词向量拿到以后模型结构可以分成四段Embedding 层把词索引换成正真向量LSTM 层抽取时序特征Dropout 层抑制过拟合最后 Dense 层输出二分类概率。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout vocab_size len(w2v_model.wv) 1 # 词表大小 embedding_dim 100 max_len 80 model Sequential([ Embedding(input_dimvocab_size, output_dimembedding_dim, mask_zeroTrue, input_lengthmax_len), LSTM(units64, dropout0.5, recurrent_dropout0.2), Dense(64, activationrelu), Dropout(0.3), Dense(1, activationsigmoid) ])mask_zeroTrue是 padding 补零时非常关键的一个参数它让 Embedding 层跳过索引 0 的位置避免补零参与梯度更新否则 LSTM 会把大量无意义的填充位置看成“空词”反复学习。LSTM units64对 8000 条样本就够了128 单元虽然能提高表达能力但在这个数据量下极易过拟合训练曲线会出现 val_loss 快速反弹。dropout和recurrent_dropout分别作用于输入和循环内部两个都开训练稳定性会好很多。最后接一个带 sigmoid 的 Dense 输出数值就是积极概率。4.3 训练参数batch_size、epochs、validation_split 怎么配合训练阶段用二分类交叉熵优化器用 Adam然后按固定比例切出验证集来观察泛化情况。from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) callbacks [ EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue), ModelCheckpoint(lstm.h5, monitorval_loss, save_best_onlyTrue) ] model.fit( X_train, y_train, batch_size64, epochs20, validation_split0.2, callbackscallbacks, verbose1 )batch_size64是折中值太大收敛慢且占显存太小梯度震荡明显8000 条样本下 64 是稳妥起步。epochs20给了模型足够的拟合空间但真正决定训练时长的是 EarlyStopping 的patience3连续 3 个 epoch 验证损失不下降就提前结束。validation_split0.2会在内部把训练集最后 20% 划成验证集不参与权重更新只用来观察过拟合。验证集这个参数很少有人改但我会明确建议如果你的语料不是按时间顺序的评论0.2 没问题如果数据有明显时间趋势务必改成手动按时间切分否则验证集精度会虚高。训练结束后ModelCheckpoint只会保存验证损失最小的那次权重这也是lstm.h5能保持较好泛化能力的原因。4.4 保存结构lstm.yml 与 lstm.h5 为什么分开存训练结束后的产物是lstm.yml和lstm.h5这两份文件在项目里是分开保存的。from tensorflow.keras.models import model_from_yaml # 保存网络结构 yaml_str model.to_yaml() with open(lstm.yml, w, encodingutf-8) as f: f.write(yaml_str) # 加载时先恢复结构再加载权重 with open(lstm.yml, r, encodingutf-8) as f: model model_from_yaml(f.read()) model.load_weights(lstm.h5)model.to_yaml()只存结构不含权重这让结构文件很小修改网络结构后也能直接对比差异。load_weights的前提是模型结构和保存时完全一致比如 LSTM 的 units 从 64 改成 128加载旧权重就会报维度不匹配。所以如果你看 README 时发现模型是提前训好的千万不要随意改结构再接旧权重这是最容易白忙活半天的操作。5. 避坑笔记六个场景的排查与解决这份资源跑下来作者踩过的坑基本集中在文本编码、版本差异和维度不匹配三类。下面按使用顺序整理成排查清单遇到对应报错可以直接对照。5.1 数据预处理阶段空文本与切词不干净坑 1jieba 切出空列表整条样本被丢弃。现象训练时打印日志发现样本数比原文件行数少很多或者 Word2Vec 训练时报警告Empty vocabulary。原因部分文本清洗后只剩标点或停用词剩余词数为 0被停用词过滤逻辑直接删掉了。解决在 tokenize 函数里统计序列长度删除空序列同时同步删除对应标签。我一般会在分词后做一次过滤再拼词表而不是等到 Word2Vec 训练才暴露问题。valid_indices [i for i, ws in enumerate(tokenized_corpus) if len(ws) 0] tokenized_corpus [tokenized_corpus[i] for i in valid_indices] labels [labels[i] for i in valid_indices]坑 2Windows 打开 txt 文件乱码。现象在 Windows 里用open(data/neg.txt)读文件打印出来是乱码或直接 UnicodeDecodeError。原因txt 文件是 UTF-8 编码而 Windows 默认以 GBK 读取。解决统一用codecs.open(path, r, encodingutf-8)不要依赖系统默认编码。这个坑在 Mac 上不容易出现但到了 Windows 环境基本必现代码里提前写死编码能省不少事。5.2 模型加载阶段Keras 版本与 pickle 版本坑 3加载 lstm.h5 报 h5py 相关错误。现象model.load_weights(model/lstm.h5)抛出属性错误或者 h5py 直接报版本冲突。原因项目训练环境与当前环境的 TensorFlow、Keras、h5py 版本不一致h5 文件格式在某些版本间不兼容。解决先按 README 标注的版本装环境如果还想留在现有环境就把.h5重新保存成 Keras 3 支持的.keras格式但要注意lstm.yml可能需要跟着重新生成。这类问题属于环境黑匣子不看版本来回折腾通常浪费一晚上。坑 4加载 Word2Vec.pkl 报 AttributeError。现象word2vec pickle.load(open(model/Word2Vec.pkl, rb))报AttributeError: Cant get attribute Word2Vec。原因pickle 是按 Python 全路径寻找类定义的gensim 版本不同或类名被重命名反序列化就找不到对应类。解决优先保证 gensim 版本和训练时一致如果版本跨度太大pkl 基本救不回来只能重新用原始语料训练一份 Word2Vec。这也是为什么 README 里必须写清楚 gensim 版本号遇到这类报错先看版本比看代码有效。5.3 预测阶段序列长度与输出解读坑 5预测单条文本时维度对不上。现象model.predict(np.array([sent]))报维度错误提示 expected shape 与实际 shape 不一致。原因训练时input_length80但预测时只送了一个长度不足 80 的句子。解决走和训练一致的预处理通道先分词、再编码、再 padding 到相同 max_len最后 reshape 成(1, max_len)再预测。def predict_single(text, model, w2v_model, max_len80): words tokenize(text) ids encode(words, max_lenmax_len) return model.predict(ids.reshape(1, -1))[0][0]坑 6输出概率到底哪个是积极。现象输出的数值刚好在 0.4 到 0.6 之间有人直接在单个概率上做 argmax导致误判。原因Dense 层用了 sigmoid输出本身就是 P(积极) 的估计值不是一个二维类别向量。解决设定阈值 0.5大于等于 0.5 判为积极小于 0.5 判为消极如果你的业务场景对误判敏感可以在验证集上重新确定最优阈值。这条最容易被忽略训练集若基本均衡0.5 阈值没问题但真实场景里用户发的偏负向表达往往更多直接套 0.5 会把很多“还行”“一般”当作消极这是产品上线前必须重新标定的。6. 用训练好的模型写一个 predict 脚本单条验证与批量准确率模型文件已经存在最后这一步把整条链路收口到预测脚本上。跑通 predict再回头改自己的语料是效率最高的顺序。6.1 单条文本预测from tensorflow.keras.models import model_from_yaml import pickle with open(model/lstm.yml, r, encodingutf-8) as f: model model_from_yaml(f.read()) model.load_weights(model/lstm.h5) with open(model/Word2Vec.pkl, rb) as f: w2v_model pickle.load(f) text 这部电影的剧情很拖沓演员演技也很尴尬我不推荐。 score predict_single(text, model, w2v_model) print(积极概率:, round(score, 4), 预测结果:, 积极 if score 0.5 else 消极)预测时唯一不能省的是 Word2Vec 词表因为encode函数要依赖它的词表把新词映射成索引。如果词表缺失所有词都会变成 UNK输出会稳定在 0.5 附近。6.2 批量验证准确率correct 0 for text, true_label in test_samples: score predict_single(text, model, w2v_model) pred 1 if score 0.5 else 0 correct (pred true_label) print(准确率:, correct / len(test_samples))我在实际测试时发现模型对明显情感的句子识别得比较准对“还行”“说得过去”这类中性表述稳定性差批量验证时可以单独抽出来看。如果这类样本占比高阈值要往 0.6 或 0.4 方向调整具体看你的场景是更怕误报还是更怕漏报。6.3 再往前一步把预测接到小接口如果做课程展示可以把predict_single封装成函数再包一个简单路由接受 POST 请求前端传文本、后端返回情感概率。这套项目的数据和模型结构都在改成接口只需要动lstm.py的末尾部分模型的训练逻辑可以完全不动。如果想提精度下一步是把 LSTM 换成预训练语言模型做对比实验或者用这份语料训练一个 SVM 基线模型两个模型的对比正好补上毕设里的实验章节。从那以后我每次拿到这类 NLP 项目第一件事都是先跑通 predict 再重训而不是一上来就训练。先确认已有模型的输入输出是正常的再动手改自己的语料这样能省掉大半的调试时间。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →