☰
基于LSTM的中文文本情感分析Python毕设源码实战解析
2026/10/1 4:19:30 网站建设 项目流程

简介:面向计算机相关专业毕业设计场景,这是一套基于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.txt

head -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', encoding='utf-8') as f: pos_lines = [line.strip() for line in f if line.strip()] with codecs.open(neg_path, 'r', encoding='utf-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', encoding='utf-8').read().split()) def clean_text(text): # 去掉 URL、数字、英文,只保留中文字符和常用标点 text = re.sub(r'https?://\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_all=False) return [w for w in words if w and w not in stopwords]

clean_text里的三个正则分别处理 URL、数字、英文,这三类字符对情感判断贡献很小,留在词表里只会增加噪声。tokenize中cut_all=False是精确模式,也是情感分析任务默认选项;全模式虽然召回率高,但会产生大量无效词组,比如把“这部电影”同时切成“这部”和“电影”,不适合直接作为模型输入。停用词表可以用网上的中文停用词表,也可以自己统计高频无意义词,像“的”“了”“吗”“就”这类词删掉后,词表能缩小 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_len=80): 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, dtype='int32')

词表用词频排序,高频词编号小,这样 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( sentences=sentences, vector_size=100, # 每个词的向量维度 window=5, # 上下文窗口大小 min_count=1, # 词频不少于 1 的词进入词表 workers=4, # 并行线程数 epochs=5 ) w2v_model.save('Word2Vec.pkl') # gensim 底层是 pickle 格式

vector_size=100是中小型语料的常见选择。向量维度太小,词组信息不够;维度太大,训练参数暴涨,8000 条样本容易过拟合。window=5是 gensim 默认值,表示每个词只看前后 5 个词以内的上下文,对评论文本够用。min_count=1意味着所有出现过至少一次的词都保留,对这个小语料是合理的,因为删词会严重缩小词表,导致很多文本无法编码。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_dim=vocab_size, output_dim=embedding_dim, mask_zero=True, input_length=max_len), LSTM(units=64, dropout=0.5, recurrent_dropout=0.2), Dense(64, activation='relu'), Dropout(0.3), Dense(1, activation='sigmoid') ])

mask_zero=True是 padding 补零时非常关键的一个参数,它让 Embedding 层跳过索引 0 的位置,避免补零参与梯度更新,否则 LSTM 会把大量无意义的填充位置看成“空词”反复学习。LSTM units=64对 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(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) callbacks = [ EarlyStopping(monitor='val_loss', patience=3, restore_best_weights=True), ModelCheckpoint('lstm.h5', monitor='val_loss', save_best_only=True) ] model.fit( X_train, y_train, batch_size=64, epochs=20, validation_split=0.2, callbacks=callbacks, verbose=1 )

batch_size=64是折中值:太大收敛慢且占显存,太小梯度震荡明显,8000 条样本下 64 是稳妥起步。epochs=20给了模型足够的拟合空间,但真正决定训练时长的是 EarlyStopping 的patience=3,连续 3 个 epoch 验证损失不下降就提前结束。validation_split=0.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', encoding='utf-8') as f: f.write(yaml_str) # 加载时先恢复结构,再加载权重 with open('lstm.yml', 'r', encoding='utf-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 数据预处理阶段:空文本与切词不干净

坑 1:jieba 切出空列表,整条样本被丢弃。

现象:训练时打印日志发现样本数比原文件行数少很多,或者 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]

坑 2:Windows 打开 txt 文件乱码。

现象:在 Windows 里用open('data/neg.txt')读文件,打印出来是乱码或直接 UnicodeDecodeError。

原因:txt 文件是 UTF-8 编码,而 Windows 默认以 GBK 读取。

解决:统一用codecs.open(path, 'r', encoding='utf-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: Can't get attribute 'Word2Vec'。

原因:pickle 是按 Python 全路径寻找类定义的,gensim 版本不同或类名被重命名,反序列化就找不到对应类。

解决:优先保证 gensim 版本和训练时一致;如果版本跨度太大,pkl 基本救不回来,只能重新用原始语料训练一份 Word2Vec。这也是为什么 README 里必须写清楚 gensim 版本号,遇到这类报错先看版本比看代码有效。

5.3 预测阶段:序列长度与输出解读

坑 5:预测单条文本时维度对不上。

现象:model.predict(np.array([sent]))报维度错误,提示 expected shape 与实际 shape 不一致。

原因:训练时input_length=80,但预测时只送了一个长度不足 80 的句子。

解决:走和训练一致的预处理通道,先分词、再编码、再 padding 到相同 max_len,最后 reshape 成(1, max_len)再预测。

def predict_single(text, model, w2v_model, max_len=80): words = tokenize(text) ids = encode(words, max_len=max_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', encoding='utf-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 再重训,而不是一上来就训练。先确认已有模型的输入输出是正常的,再动手改自己的语料,这样能省掉大半的调试时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询