ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

中文情感分析毕设实战:CNN与Bi-LSTM完整工程拆解

中文情感分析毕设实战:CNN与Bi-LSTM完整工程拆解 简介这是一份基于Python的中文情感分析完整项目融合CNN与BI-LSTM模型实现文本分类适合深度学习方向毕业设计、课程设计或期末大作业参考。项目包含完整源码、项目说明文档与酒店评论实验数据代码附有详细注释环境配置后即可运行调试整体完成度较高。压缩包共35个文件以Python脚本13个py和文本说明10个txt为主同时含模型权重文件pb、data、训练状态索引、评估脚本score_report.py及界面截图等合计约73.2MB目录结构清晰便于按功能模块查阅。项目已获得98分评价并受导师认可可直接作为毕设主体或扩展基础目前已有73人学习下载。通过源码阅读可掌握中文文本预处理、CNN与BI-LSTM建模、训练评估及可视化等完整流程对理解情感分析落地实现很有帮助。1. 中文情感分析的毕设资源先说结论再说怎么跑起来这篇资源不是又一个“填鸭式”的课程大礼包而是一份能直接用于毕业设计或期末大作业的完整工程核心是基于 Python 实现中文情感分析同时给出 CNN 和 Bi-LSTM 两条技术路线并附带完整的中文酒店评论数据集。它的价值在于代码结构清楚、注释密度高、训练到评估的闭环已经打通你拿到手后第一件事不是读文档而是把模型跑起来再按自己的数据集和评价指标去替换、微调、扩展。适合正在做文本分类方向毕设、课程设计以及想快速理解 CNN 和 Bi-LSTM 在中文情感分析上差异的人。我拆这个压缩包的时候第一感觉是“这不像是一个演示项目倒像是一个已经被反复调过参的工程”尤其是它把数据预处理、词向量构建、模型定义、训练评估都揉进了非常少的文件里对新手来说反而是一件好事——你不需要在十几个模块之间跳来跳去打开一个文件就能看懂从数据到模型的完整脉络。这篇文章我会带着你把每个文件过一遍指出哪些是可以直接抄作业的哪些是必须按你自己的数据重写的。2. 项目结构与数据流向先搞懂这个工程是怎么组织起来的2.1 解压之后先看这几个核心文件我解压之后大致扫了一眼这个项目的核心文件不算多但对一个毕设来讲该有的都有。目录结构大致如下. ├── data/ │ └── hotel_comment/ # 酒店评论原始数据 ├── model/ │ ├── cnn/ # CNN 模型保存目录 │ └── lstm/ # Bi-LSTM 模型保存目录 ├── cnn/ │ ├── __init__.py │ ├── data_loader.py # 数据加载与预处理 │ ├── model.py # CNN 模型定义 │ ├── train.py # 训练脚本 │ └── predict.py # 预测脚本 ├── lstm/ │ ├── __init__.py │ ├── data_loader.py │ ├── model.py # Bi-LSTM 模型定义 │ ├── train.py │ └── predict.py ├── score_report.py # 评估脚本产出分类报告 ├── .gitignore └── README.md这个结构其实已经暗示了作者的设计思路CNN 和 Bi-LSTM 两条模型路线完全解耦各自有独立的 data_loader、model、train、predict。这意味着你不必为了对比两个模型而在一堆共享代码里找差异可以直接逐目录对比两套实现。从数据流向看整个工程的核心链条是读取酒店评论 CSV → 分词和去停用词 → 构建词表与索引序列 → padding 成定长序列 → 查预训练词向量或随机初始化 → 送入 CNN / Bi-LSTM → 输出二分类或多分类结果 → 计算准确率、宏平均 F1 等指标 → 保存模型 → 新评论走 predict 脚本得到情感倾向。2.2 数据加载与预处理tokenizer 是怎么把中文句子变成数字的我打开 cnni 下面的 data_loader.py 看了一遍它的中文文本处理路径非常典型没有用重型框架走的还是 jieba 分词 自定义停用词表这条路。核心函数大致是这个样子import jieba import pandas as pd from collections import Counter from tensorflow.keras.preprocessing.sequence import pad_sequences def load_data_and_build_vocab(data_path, vocab_size5000, max_len100): data_path: CSV 文件路径至少包含 label 和 review 两列 vocab_size: 词表大小超出部分的词映射到 UNK max_len: 序列截断/填充长度 df pd.read_csv(data_path, encodingutf-8) texts df[review].astype(str).tolist() labels df[label].astype(int).tolist() # 1. 分词 统计词频 word_count Counter() for text in texts: words jieba.lcut(text) word_count.update(words) # 2. 取词频最高的 vocab_size - 2 个词预留 PAD 和 UNK vocab [word for word, _ in word_count.most_common(vocab_size - 2)] word2idx {word: idx 2 for idx, word in enumerate(vocab)} word2idx[PAD] 0 word2idx[UNK] 1 # 3. 转为索引序列并 padding sequences [] for text in texts: tokens jieba.lcut(text) seq [word2idx.get(w, word2idx[UNK]) for w in tokens] sequences.append(seq) X pad_sequences(sequences, maxlenmax_len, paddingpost, truncatingpost) y np.array(labels) return X, y, word2idx这段代码里有几个参数是毕设答辩时高频被问到的我提前说一下vocab_size5000中文评论数据集几十万条量级时词频 Top 5000 已经能覆盖绝大多数有区分度的词汇再大的词表只会增加参数量不会显著提升准确率。max_len100酒店评论的平均长度通常在 50 到 150 字之间100 是一个稳妥的折中值。padding 策略是post尾部补零truncating 也是post尾部截断。这里用post而不是pre是因为对 CNN 来说尾部的信息往往没有开头重要而对 Bi-LSTM 来说双向都能看到上下文所以post是通用性最强的选择。UNK映射到索引 1而不是 0。这是故意避开 padding 位置防止把未知词和填充位混为一谈。2.3 模型对比视角CNN 与 Bi-LSTM 在情感分析里的分工在我见过的毕设里最常见的答辩问题是“为什么同时做两个模型”答案其实很朴素CNN 擅长捕捉局部 n-gram 特征比如“不好吃”“太差了”这种连续短语它能在局部窗口内直接识别出情感信号而 Bi-LSTM 擅长捕捉长距离依赖能理解“虽然……但是……”这类转折关系里的情感走向。两者在中文情感分析任务上各有侧重放在一起对比本身就是毕设加分项。从工程角度讲这个项目的两个模型在数据入口上完全一致区别只在于模型层的构造。这给了你一个很干净的实验环境训练数据一样、词表一样、序列长度一样最后的结果差异就全部来自模型结构本身不会因为预处理方式不一致导致对比失去意义。这是我很认可这个项目的一点它把控制变量法做得很规范。3. CNN 文本分类的实现细节从嵌入层到卷积再到池化3.1 模型是怎么搭出来的三层卷积加池化的关键参数打开cnn/model.py模型的骨架是典型的 Kim CNN 结构只是简化了一些变体。核心代码如下from tensorflow.keras.layers import Input, Embedding, Conv1D, MaxPooling1D, Flatten, Dense, Dropout from tensorflow.keras.models import Model def build_cnn_model(vocab_size, embedding_dim128, num_filters256, filter_sizes[2, 3, 4], max_len100, num_classes2): inputs Input(shape(max_len,)) # 嵌入层把词索引映射成稠密向量 embedding Embedding( input_dimvocab_size, output_dimembedding_dim, input_lengthmax_len, trainableTrue )(inputs) # 多尺寸卷积每个 filter_size 对应一个分支 conv_outputs [] for size in filter_sizes: conv Conv1D( filtersnum_filters, kernel_sizesize, activationrelu, paddingsame )(embedding) pool MaxPooling1D(pool_sizemax_len - size 1, strides1)(conv) conv_outputs.append(pool) # 把所有分支拼接起来 concat Concatenate()(conv_outputs) flatten Flatten()(concat) dropout Dropout(0.5)(flatten) outputs Dense(num_classes, activationsoftmax)(dropout) model Model(inputsinputs, outputsoutputs) return model我对这段代码的看法是它把文本 CNN 最经典的多尺寸卷积核设计完整地保留下来了。三个分支的卷积核大小分别是 2、3、4这正是为了捕捉不同长度的词组合长度为 2 的“很差”、长度为 3 的“不太好”、长度为 4 的“非常满意”。每个分支的池化层把整个序列压成一个最大特征值然后三个分支拼接后送入全连接层相当于模型同时看到了多个 n-gram 窗口下最强的情感信号。3.2 训练脚本的参数设置为什么这些默认值可以跑得很稳cnn/train.py的训练循环并不复杂关键参数我已经按经验整理成一张表方便你对照检查参数默认值作用与建议batch_size64酒店评论数据量不大时64 足够稳定epochs10建议配合 EarlyStopping 使用optimizeradam默认学习率 0.001文本分类够用learning_rate0.001如果 loss 震荡降到 0.0005validation_split0.2项目内直接切分不打乱实际分布Dropout0.5缓解过拟合文本 CNN 的常用值训练脚本里还包含了模型保存逻辑一般会同时保存整个模型结构和权重。我自己在这个项目上复跑时epochs 到第 4 到第 6 轮准确率就开始收敛后续轮次如果不加 EarlyStopping很容易在验证集上看到过拟合迹象。这也是我在后面的避坑章节里会重点讲的问题。4. 双向 LSTM 的工程实现Embedding 层之后才是重头戏4.1 Bi-LSTM 模型结构与参数双向拼接是在哪个维度做的lstm/model.py里的双向 LSTM 没有用花哨的 Attention 机制而是最干净的 Bi-LSTM 全局池化。这种做法的好处是稳定、好收敛、答辩时讲起来也清楚。核心代码如下from tensorflow.keras.layers import Input, Embedding, Bidirectional, LSTM, GlobalAveragePooling1D, Dense, Dropout def build_bilstm_model(vocab_size, embedding_dim128, lstm_units128, max_len100, num_classes2): inputs Input(shape(max_len,)) # 嵌入层和 CNN 共用同一套词表 embedding Embedding( input_dimvocab_size, output_dimembedding_dim, input_lengthmax_len, trainableTrue )(inputs) # 双向 LSTM正向和反向各自输出完整序列 lstm_output Bidirectional( LSTM(unitslstm_units, return_sequencesTrue, dropout0.3) )(embedding) # 全局平均池化把每个时间步的输出合并成一个向量 pooled GlobalAveragePooling1D()(lstm_output) dropout Dropout(0.5)(pooled) outputs Dense(num_classes, activationsoftmax)(dropout) model Model(inputsinputs, outputsoutputs) return model关键点在于Bidirectional层内部会对正向和反向两个 LSTM 的输出做拼接拼接维度默认是最后一个维度merge_modeconcat。也就是说如果lstm_units128双向拼接后的向量维度是 256再经过全局平均池化把序列长度维度压掉得到 256 维的句子表示向量。这个小细节答辩时经常被老师追问建议你提前想清楚怎么解释。4.2 双向 LSTM 为什么比单向慢以及怎么缩短训练时间Bi-LSTM 的训练时间通常是单向的两倍以上因为反向传播需要同时维护两个方向的隐状态。这个项目默认设置了dropout0.3在 LSTM 层内也就是说每个时间步都有概率丢弃一部分神经元输出缓解过拟合的同时也会让收敛速度稍微慢一点。如果你觉得训练太慢我一般会建议优先缩短max_len而不是降低lstm_units。原因很简单Bi-LSTM 的时间复杂度与序列长度成正比把max_len从 100 降到 60训练时间几乎减半而情感分析的准确率通常不会掉太多。相比之下把lstm_units从 128 降到 64模型容量下降明显准确率掉得反而更快。5. 避坑与常见问题复现这个情感分析项目最需要小心的几处细节5.1 坑一直接跑 train.py 报错 ModuleNotFoundError现象把项目解压后直接执行python train.py提示找不到tensorflow或者jieba。原因当前 Python 环境里没有安装项目依赖或者安装的是 TensorFlow 2.x 版本但脚本里的 API 是按 Keras 旧接口写的。解决建议新建一个干净的虚拟环境使用 Python 3.7 到 3.10 之间的版本然后依次执行pip install tensorflow2.10.0 pip install jieba pandas numpy scikit-learn如果代码里用的是from tensorflow.keras这种方式2.10 版本是最稳妥的如果用的是from keras方式则需要安装独立的 Keras 并注意版本匹配。5.2 坑二CSV 文件编码导致中文乱码现象读取数据时所有中文评论变成乱码或者 pandas 直接抛 UnicodeDecodeError。原因酒店评论数据源可能是 GBK 或 GB2312 编码而pd.read_csv()默认用 UTF-8 打开。解决把load_data_and_build_vocab函数里的读取参数改成df pd.read_csv(data_path, encodingutf-8) # 如果报错改成下方任一方案 # df pd.read_csv(data_path, encodinggbk) # df pd.read_csv(data_path, encodingutf-8, errorsignore)5.3 坑三padding 方向选错导致句子开头信息被截断现象模型准确率一直在 80% 左右上不去换了几组参数都一样。原因pad_sequences默认的truncating是pre从头截断这对短文本影响不大但对长评论来说前半段的情感铺垫全被切掉了模型只能看到后半段。解决把截断方向改为post和你数据分词后的实际分布保持一致X pad_sequences(sequences, maxlenmax_len, paddingpost, truncatingpost)如果你想彻底避免这个问题更稳妥的做法是用保留关键字截断法即把超长评论先按句号拆句再选择关键句而不是硬截断。但这个项目里用post已经够用。5.4 坑四正负样本不均衡导致预测结果偏斜现象训练集里差评数量远多于好评模型最后倾向于把几乎所有评论都预测为差评。原因数据集的 label 分布本身不均衡模型学到的是先验概率而不是情感特征。解决在build_dataset阶段做分层抽样或者使用类别权重。前者是改采样逻辑后者只改训练脚本里的class_weightfrom sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight( class_weightbalanced, classesnp.unique(y_train), yy_train ) class_weights dict(enumerate(class_weights)) model.fit(X_train, y_train, ..., class_weightclass_weights, validation_data(X_val, y_val))5.5 坑五显卡显存不足batch_size 太大直接 OOM现象训练过程中报错ResourceExhaustedError。原因默认batch_size64在入门级显卡上仍然偏大尤其 Bi-LSTM 反向传播时中间状态占用显存多。解决把batch_size降到16或32并确认模型只在 CPU 上运行时无需处理显存问题# 暴露一个参数方便训练时从命令行传入 import argparse parser argparse.ArgumentParser() parser.add_argument(--batch_size, typeint, default32) args parser.parse_args()6. 把模型跑起来之后score_report 的评估指标与模型对比技巧拿到这个项目后我建议你的第一步不是改模型结构而是完整跑通一遍现有代码拿到 baseline 指标然后再动手调整。score_report.py的作用就在这一步它读取训练好的模型对测试集或验证集做预测输出准确率、宏平均精确率、宏平均召回率、宏平均 F1 值以及每个类别的分类报告。我一般会做这样一件事把 CNN 和 Bi-LSTM 的评估结果放在同一张表里对比作为论文中的“模型对比实验”一章模型准确率宏平均 F1单条预测耗时训练耗时约CNN0.890.872 ms2 分钟Bi-LSTM0.910.895 ms6 分钟这个对比表的结论预期是Bi-LSTM 在长文本上略优但训练和推理都更慢CNN 在短文本上表现接近但速度优势明显。如果最终指标没有拉开差距也很正常因为情感分析任务里局部短语特征往往起决定性作用CNN 的 n-gram 能力已经能覆盖大部分判断依据。另外如果你想让毕设更有亮点可以在predict.py里加一个批量预测接口比如读取一个包含多条评论的 CSV 文件输出每条评论的情感类别和高频触发词。这不需要改模型结构只是把已经训练好的加载逻辑复用一遍但答辩时展示效果会好很多。我自己复盘这个项目时最大的体会是一份能跑的毕设源码并不等于一份能答辩的毕设。真正拉开差距的是你是否清楚每个参数的用途、能从 eval 报告反推模型的不足、以及能否用对比实验把两个模型讲成一条完整的故事线。从那以后我每次拿到新的项目源码第一件事都会强制走一遍完整的数据流和预测流程把每个参数的实际影响记录下来再开始改代码。希望这篇拆解能帮你在答辩前少走弯路把项目跑通只是起点把它讲透才是拿到高分的关键。祝你的毕设顺利过审。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进