简介基于中文电影评论的情感分析项目资源面向自然语言处理初学者与研究者围绕MLP、CNN与LSTM三种主流神经网络模型实现对中文影评的情感极性自动判别并系统对比不同架构在短文本分类任务上的表现。压缩包共包含18个文件大小约6.15MB3个Python脚本分别对应预处理、模型实现与运行流程3个txt文件提供训练集和验证集6张png呈现各模型的准确率曲线与混淆矩阵等对比图表另有README及配置文件说明环境依赖与使用方式。已有67人浏览学习。从资源中可直接获得完整的可运行代码、已划分的训练与验证数据、分词与去停用词等预处理步骤以及三组模型训练过程中的历史指标记录。读者既能复现实验也能进一步调整嵌入维度、卷积核、隐藏层等超参数透过可视化结果理解MLP、CNN与LSTM在捕捉中文文本局部特征与长距离依赖时的差异为后续情感分析任务提供可扩展的调优思路与代码基础。1. 中文情感分析这份电影评论数据集与 NN(MLP)、CNN、LSTM 三类模型实现到底能拿来做什么做 NLP 的人应该都有同感情感分析是最适合练手的任务数据好找、标注清晰、效果可感知。但如果只在网上下个现成的英文 IMDb 情感分析跑一遍换到中文场景就会发现处处卡壳——中文分词、停用词、字 vs 词、词向量怎么对齐全是问题。这份基于中文电影评论的情感分析资源核心就干一件事把「中文数据清洗 → 文本向量化 → MLP/CNN/LSTM 三类模型训练与对比」这条完整链路一次打通。它适合两种人——刚入门深度学习、想在 NLP 上做第一个完整项目的初学者以及已经在做文本分类、想快速拿到一份可直接跑通的中文情感基线代码的老手。资源里既有标注好的中文电影评论数据也有三类模型的可复现代码解决了中文场景下「有数据没代码、有代码跑不通」的常见痛点。2. 先把地基打牢中文评论数据预处理与文本向量化的四个关键环节2.1 原始数据长什么样清洗时要处理哪些脏数据中文电影评论数据集常见的原始格式是 CSV 或 JSON每条样本包含评论文本和一个情感标签通常 0 表示负面1 表示正面也有数据用 2 分类的「差评/好评」。下载这份资源后先用 pandas 载入看一眼分布import pandas as pd df pd.read_csv(movie_reviews.csv, encodingutf-8) print(df.head()) print(df[label].value_counts()) print(df.isnull().sum())逻辑说明head()用于确认字段名和文本格式value_counts()看标签是否均衡——如果正负样本差得太多后面对模型效果评估会有影响isnull()则是查空值。我见过不少人在这一步直接跳过结果后续train_test_split报错或者模型 loss 跳成 NaN其实源头就是空文本。再往下就是清洗。中文评论文本里最常见的噪声有三类HTML 标签抓取时留下的、表情符号、以及大量无意义的重复标点。处理方式一般是这样import re def clean_text(text): if not isinstance(text, str): return text re.sub(r[^], , text) # 去 HTML 标签 text re.sub(r[0-9a-zA-Z], , text) # 去英文和数字 text re.sub(r[\u4e00-\u9fa5], , text) # 只保留中文 return text.strip()参数说明第二个正则删掉英文单词和数字是因为电影评论里夹杂的英文多数是片名或无关信息第三个正则的意图是反过来剔除标点、表情只留中文。实际用的时候要注意第三行正则写的是「把连续中文字符替换成空格」不是「只保留中文」——想要后者应该用re.sub(r[^\u4e00-\u9fa5], , text)。这种细节最容易翻车建议动手改代码前先想清楚要的效果是对齐还是剔除。另外评论里常见的「哈哈哈哈哈哈哈哈」「好看好看好看好看」这类重复词清洗时如果不做长度去重会在后面向量化时产生大量无意义的重复 n-gram 特征。我一般会加一个简单的连续重复压缩re.sub(r(.)\1{3,}, r\1, text)把连续重复 3 次以上的字符压缩成一个。2.2 分词工具怎么选jieba 的两种模式与停用词表的作用中文文本向量化前必须先分词jieba 是事实标准。资源里大概率已经内置了分词代码但理解它在做什么比能跑更重要。jieba 有两种常用模式精确模式和搜索引擎模式。情感分析用精确模式就够搜索引擎模式把长词再拆出子词反而引入噪声。import jieba def tokenize(text, stopwords): words jieba.lcut(text) # 精确模式分词 words [w for w in words if w not in stopwords] return words参数说明jieba.lcut返回的是 list比jieba.cut返回生成器更直观适合后面做向量化。停用词表建议直接用哈工大停用词表扩充版资源里通常会附一份。要注意的是情感分析场景下「不」「没」「太」这类否定词不能被当停用词删掉——「不好看」删掉「不」就变成「好看」了语义完全反转。很多第一次做的人在这步踩坑准确率上不去的其中原因就出在这里。所以停用词表选好后一定要手动检查一遍是否包含否定词和程度副词有的话必须移除。2.3 向量化的两条路线词级 TF-IDF 与字/词级序列编码文本向量化是决定模型上限的环节。这份资源里MLP 用的是词级 TF-IDF 向量CNN 和 LSTM 用的是序列化的词索引。两条路线要分清因为它们的输入维度完全不一样。TF-IDF 的做法是构建词表然后给每篇评论生成一个固定长度的稀疏向量from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer(max_features8000) X_tfidf vectorizer.fit_transform([ .join(tokens) for tokens in all_tokens])参数说明max_features8000是只保留出现频率最高的 8000 个词目的是控制维度防止稀疏矩阵过大拖慢 MLP 训练。如果机器内存小可以压到 5000但不要低于 3000否则信息损失太明显。「注意」TF-IDF 的语料统计一定要在训练集上做 fit然后 transform 验证集和测试集不能在整个数据集上 fit——否则验证集的词分布信息提前泄漏给模型评估结果会虚高。序列化编码的典型做法是先按出现频率给每个词编一个从 1 开始的 ID0 保留给 padding然后把每条评论转成等长的 ID 序列from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences tokenizer Tokenizer(num_words20000) tokenizer.fit_on_texts([ .join(tokens) for tokens in all_tokens]) sequences tokenizer.texts_to_sequences([ .join(tokens) for tokens in all_tokens]) X_padded pad_sequences(sequences, maxlen120, paddingpost, truncatingpost)参数说明num_words20000是词表上限maxlen120是每条评论固定的序列长度——评论超过 120 个词截断不足的用 0 补齐。paddingpost是尾部补零truncatingpost是尾部截断。这两个参数对 LSTM 的影响非常大尾部截断会直接丢掉评论的后半段内容如果你觉得电影评论的结尾往往是「总结性评价」那truncatingpre保留尾部可能更合理。这类参数没有绝对标准我的习惯是先在验证集上比较两种方案。2.4 训练集划分与标签编码数据划分要保证三点标签分布一致、不混入重复样本、验证集至少 1000 条。from sklearn.model_selection import train_test_split X_train, X_val, X_test, y_train, y_val, y_test train_test_split( X_padded, y, test_size0.2, stratifyy, random_state42 )逻辑说明stratifyy保证切分后正负样本比例与原数据集一致random_state42固定随机种子让实验可复现。这里有一点容易被忽略——评论区里偶尔有完全相同的复制评论如果不先去重就直接划分同一条文本可能同时出现在训练集和测试集模型评估的准确率会虚高几个点。稳妥做法是在清洗后先做df.drop_duplicates(subset[text])。3. 用 MLP 跑通基线全连接网络做情感分析的原理、结构与调参3.1 为什么先用全连接网络而不是直接上深度模型很多入门者一上来就试 LSTM结果训练慢、调参难、效果还不见得比简单模型好。MLP 在情感分析里的价值是建立基线——它告诉你「在不考虑词序信息的前提下模型能到多少准确率」。如果一个 MLP 能到 82%那 CNN/LSTM 做不到 85% 以上就是有问题的趁早检查数据或者模型实现。全连接网络在文本上的思路其实是把每条评论的向量当成一个高维点让模型学一个分类面。它的局限是很明显的——词与词的相对位置信息在展开成向量时全部丢失「很不好」和「不好很」在词袋向量里几乎是同一个表示。所以 MLP 的上限是被信息表示卡住的它跟数据量关系不大。3.2 MLP 的模型架构与关键参数说明用 TF-IDF 向量做输入的 MLP 结构通常长这样from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout model Sequential() model.add(Dense(256, activationrelu, input_shape(X_tfidf.shape[1],))) model.add(Dropout(0.5)) model.add(Dense(128, activationrelu)) model.add(Dropout(0.5)) model.add(Dense(1, activationsigmoid)) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy])逻辑说明第一层和第二层的relu激活函数用来让网络具备非线性表达能力Dropout(0.5)是对抗过拟合——全连接层参数量巨大不做 dropout 的话训练集准确率很容易冲上 98%验证集只有 80%。最后一层sigmoid输出一个 0 到 1 的打分大于 0.5 判为正面。参数说明input_shape的维度取决于 TF-IDF 的max_features改前面那个值后这里必须同步改。隐藏层 256/128 是常见的中等规模配置如果你的机器是 CPU 跑可以降到 128/64速度提升但精度可能略降。优化器用 adam 就行学习率用默认的 0.001 在多数场景下已经够稳。3.3 训练策略与过拟合的判别标准训练 MLP 时关注两个曲线训练准确率和验证准确率。两者差距超过 5 个百分点就是过拟合信号优先调大Dropout而不是加数据——因为清洗后的电影评论数据就那么多加数据通常不现实。history model.fit( X_tfidf_train, y_train, validation_data(X_tfidf_val, y_val), batch_size64, epochs20, verbose1 )参数说明batch_size64是每轮梯度更新用的样本数内存小就降到 32。epochs20是训练轮数我习惯设大一点然后用 EarlyStopping 在验证集不再提升时提前停既省时间又能避免后面几个 epoch 纯粹在拟合噪声。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue) model.fit(..., callbacks[early_stop])逻辑说明patience3表示连续 3 轮验证集 loss 没有下降就停止训练restore_best_weightsTrue会把模型权重回退到验证集最优的那一轮。这个机制对 MLP、CNN、LSTM 三套模型通用建议直接写进公共代码模块里。4. CNN 与 LSTM 的中文文本分类复现核心区别、实现要点与超参数对照4.1 为什么 CNN 能抓局部特征LSTM 能抓序列依赖MLP 丢掉了词序信息而电影评论里情感表达高度依赖局部短语「演技炸裂」「剧情拖沓」「全程无尿点」——这些 2 到 4 个词的组合是情感判断的最强信号。CNN 在文本上做的就是用一个固定宽度的卷积核滑过词序列每个窗口提取一组局部 n-gram 特征。举个例子窗口大小为 3 的卷积核看到的是连续三个词组成的局部片段多核并行就等于同时关注多种长度的短语组合。LSTM 走的是另一条路按照词出现的先后顺序逐个处理每个时间步的隐状态携带之前所有词的累计信息。它天然适合捕捉「前面的转折词如何影响后面的评价」——比如「虽然前半段很精彩但结局莫名其妙」这类带转折的句子LSTM 有机会学到前半段的正面信号被后半段的负面信号覆盖。这也是 LSTM 在情感分析上通常略强于 CNN 的原因。4.2 文本 CNN 的搭建卷积核宽度、数量与池化方式文本 CNN 是 Kim 在 2014 年提出的经典结构资源里的实现基本遵循这个框架from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, Conv1D, GlobalMaxPooling1D, Dense, Dropout model Sequential() model.add(Embedding(input_dim20000, output_dim128, input_length120)) model.add(Conv1D(filters128, kernel_size3, activationrelu)) model.add(GlobalMaxPooling1D()) model.add(Dense(64, activationrelu)) model.add(Dropout(0.5)) model.add(Dense(1, activationsigmoid)) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy])逻辑说明Embedding层把每个词的 ID 映射成一个 128 维的稠密向量Conv1D在词向量序列上做卷积——这里的kernel_size3是每次覆盖 3 个词filters128表示同时学习 128 种不同的局部模式。GlobalMaxPooling1D在每个特征图上取最大值相当于从整条评论里提取每个卷积核最激发的那个位置的特征。参数说明input_dim20000必须和上一章 Tokenizer 的num_words一致input_length必须和pad_sequences的maxlen一致。想提升效果最常见的做法是并排多个不同kernel_size的卷积核比如 2/3/4 各来 128 个——资源里如果只给了单卷积核版本可以自己改造这属于性价比最高的进阶改动。4.3 LSTM 的搭建嵌入层、循环层与过拟合控制from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding, LSTM, Dense, Dropout model Sequential() model.add(Embedding(input_dim20000, output_dim128, input_length120, mask_zeroTrue)) model.add(LSTM(units128, dropout0.3, recurrent_dropout0.3)) model.add(Dense(1, activationsigmoid)) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy])逻辑说明mask_zeroTrue是关键配置——告诉 LSTM 跳过 padding 出来的 0 位置不做无效计算。units128是隐状态维度越大模型容量越高但越容易过拟合。dropout0.3作用于输入到隐层的连接recurrent_dropout0.3作用于隐层之间的循环连接两者同时开是 LSTM 防过拟合的标准做法。参数说明LSTM 的训练速度比 CNN 慢得多在同样的数据量下建议先把units从 128 降到 64 跑通流程再逐步加大。另外一个很实际的建议——如果评论平均长度已经接近 120LSTM 处理长序列的负担会比较大。可以考虑用双向 LSTMBidirectional 包裹一层来增强效果代价是参数量和训练时间翻倍。初学阶段先跑通单向版本记录效果再决定要不要加双向。4.4 三类模型的效果对比表与选型结论模型关键特点训练速度预期准确率区间适用场景MLP忽略词序全连接拟合最快78% - 82%基线/快速验证数据质量CNN捕捉局部 n-gram 特征较快82% - 86%短评/短语为主的情感表达LSTM建模序列依赖捕捉转折语义较慢83% - 88%长评/含转折结构的表达表格里的准确率区间是这类中文数据集的常见水平不代表资源里能直接跑出这个值——你的分词质量、清洗强度、序列长度设置都会影响最终数字。选型结论其实很直接如果做线上实时推理CNN 更快更稳如果追求极致准确率且推理时延不敏感LSTM 更合适MLP 不是拿来上线的是拿来做数据 sanity check 的。5. 避坑排查训练与评估阶段最常见的六个翻车现场5.1 训练集准确率极高但测试集一塌糊涂现象训练三个 epoch 后训练集准确率到 95%测试集只有 70%而且还在下降。原因这是典型的过拟合叠加信息泄漏。信息泄漏部分多出在 TF-IDF 的fit步骤用到了全部数据过拟合部分则是模型容量太大隐藏层神经元太多而数据量不足。解决先把TfidfVectorizer改成先在训练集上做fit再transform验证集然后把Dropout从 0.3 提到 0.5同时把隐藏层神经元减半。5.2 LSTM 训练 loss 永远是 NaN现象训练第一个 epoch loss 就成了 NaN之后一直不变。原因多数是嵌入层没有做合理的参数初始化或者学习率太大导致梯度爆炸。也有人把未清洗的文本直接送进 Tokenizer产生了异常的 token 序列。解决确认输入序列中所有值都是非负整数索引且小于num_words把优化器换成Adam并把clipnorm设为 1.0限制梯度范数。常见做法是Adam(learning_rate0.0005, clipnorm1.0)对 LSTM 这类循环网络非常有效。5.3 分词后的文本为空或者长度全部为 1现象pad_sequences之前打印每条序列的长度发现大量为 1 或 0。原因清洗时把非中文的字符全部删掉后有些评论原本就以英文为主或者标签符号居多剩下的就是空文本。分词后这些空文本的 token 列表为空Tokenizer 会把它编成空序列。解决清洗后加一步过滤凡len(tokens) 3的样本直接丢弃。同时检查停用词表是否误删了「不」「别」这类词——如果删了很多格式为「不推荐」「别浪费时间」的评论分词后只剩「推荐」「浪费」长度确实保存了但语义完全反转。5.4 验证集准确率波动幅度极大现象验证集准确率不是稳定上升而是在 70% 到 85% 之间疯狂横跳训练集倒是很平滑。原因验证集样本量太小。如果总数据只有两三千条train_test_split切出 20% 验证集不足千条评估方差很大每个 epoch 抽到的验证批次不同导致分数剧烈波动。解决先用stratifyy保证标签分布一致然后把验证集比例从 0.2 提到 0.3或者干脆用 5 折交叉验证评估最终效果。训练过程仍然用单一验证集做 early stopping最后再交叉验证一次取均值。5.5 预测结果一直是同一个标签现象模型能正常训练准确率也还行但用model.predict()对新评论推理时全部输出 0.5 附近或者全部输出同一个标签。原因最大可能是预测时用的向量化器和训练时不一致——比如训练用 TF-IDF预测时重新定义了一个没fit过的向量化器特征空间根本对不上。另一种可能是推理时文本没有走清洗流程评论文本里的一个字被映射成全零向量。解决把向量化器用pickle.dump保存下来和模型权重放在同一个目录预测前原样加载。这步我在实际项目中翻过车——训练时写了个preprocess()函数预测时以为它会把原始文本自动处理结果函数内部依赖的停用词表路径写的是相对路径部署环境一换就找不到文件。从那以后我每次做完实验都会强制把 tokenizer 和清洗函数一起导出再写一个单独的 inference 脚本用真实清洗后的文本走一遍模型预测确认输出分布的合理性再算实验收工。希望这份资源的复现过程也能帮你少走几次这些弯路把时间花在调模型而不是调 bug 上。本文还有配套的精品资源点击获取