基于Word2Vec和LSTM的虚假评论识别实战:从数据清洗到模型部署 简介一套基于神经网络的虚假评论识别系统毕业设计项目面向计算机相关专业正在做毕设的学生与需要项目实战练习的学习者也可用于课程设计或期末大作业。项目经导师指导并获认可评分98分完整覆盖虚假评论识别场景下的语料预处理、Word2Vec词向量训练、LSTM模型构建与训练、模型预测等环节具备清晰的工程落地流程。资源包共23个文件含6个Python脚本、8个pyc编译缓存配套2份docx说明文档分别介绍代码流程与系统整体设计并提供依赖清单、停用词表、训练数据、词向量模型及情感分类模型等文件压缩包整体仅2.91MB轻量但结构完整。目前已有183人学习下载。结合源码、模型与文档读者可快速复现实验理解神经网络文本分类方法也能基于现有框架扩展改造在自备数据上训练自己的虚假评论识别模型。1. 虚假评论识别为什么用神经网络而不是规则过滤刷单、水军、带节奏的评论在电商、外卖、内容平台里几乎每天都能碰到。传统做法是一份敏感词表加正则规则碰到“好评返现”“质量不错就是物流慢”这种变体就漏掉大半更不用说那些刻意模仿真人语气的机刷评论。这套本科毕业设计项目的思路很直接把评论当作文本序列用 word2vec 训练词向量再交给 LSTM 网络学习评论里的时序依赖最后输出“真实评论 / 虚假评论”的二分类概率。我拆完源码后确认它不是包装出来的 demo数据预处理、词向量训练、LSTM 构建、训练验证、预测导出是完整闭环适合正在做毕设的同学当主干框架也适合想做文本二分类项目的开发者直接改数据跑通。项目里几个核心文件的功能分配得很清楚produceWord2vec.py 负责训练词向量Corpuspreprocess.py 做语料清洗和切分dataPreprocess.py 把文本转成模型能吃的序列LSTM.py 定义网络结构trainer.py 走训练流程predict.py 是推理入口。下面我把每一步的关键代码和参数设计拆开讲。2. 语料清洗与 word2vec 词向量构建 LSTM 的输入表示2.1 原始语料的问题评论数据从来不是干净的。标点符号、emoji、URL、重复字符、中英文混排这些噪声如果直接进模型word2vec 会学到一堆无意义的向量。项目里Corpuspreprocess.py做的事情就是把这些噪声剔除掉。我一般在处理评论语料时还会额外做一步繁体转简体因为很多评论里繁简混用不统一会让词表膨胀。2.2 清洗代码实现import re import jieba def clean_comment(text: str) - str: # 去除 URL、HTML 标签、数字和特殊符号 text re.sub(rhttps?://\S|www\.\S, , text) text re.sub(r.*?, , text) text re.sub(r\d, , text) text re.sub(r[^\u4e00-\u9fa5a-zA-Z], , text) # 去掉单个字符单个汉字或字母对语义贡献极低 text re.sub(r(?![a-zA-Z])\w{1}(?![a-zA-Z]), , text) return text.strip() def tokenize(text: str, stopwords: set) - list: words jieba.lcut(text) return [w for w in words if w.strip() and w not in stopwords]参数说明正则[^\u4e00-\u9fa5a-zA-Z]会把所有非中英文字符替换为空包括标点和空格。stopwords加载的是项目里的 stopwords.txt我建议你在这个文件基础上追加领域词比如“好评”“差评”这种在分类任务里没有区分度的词。清洗后的结果会按标签写入 train.csv一行一条评论和一个 label 字段。2.3 word2vec 训练为什么不用预训练词向量很多同学会纠结直接用网上现成的中文预训练词向量不就行了问题是预训练向量是基于通用语料训练的对“好评返现”“加微信”这类电商场景的语义覆盖不够好。项目里produceWord2vec.py是在你自己的语料上重新训练 word2vec 模型词向量的语义更贴合当前任务场景。from gensim.models import Word2Vec sentences [] with open(train.csv, r, encodingutf-8) as f: for line in f: parts line.strip().split(,) if len(parts) 2: sentences.append(tokenize(parts[0], stopwords)) model Word2Vec( sentencessentences, vector_size128, window5, min_count2, sg1, workers8, epochs10 ) model.save(word2vec.model)参数说明vector_size128是词向量维度维度太低表达不了语义差异太高在小语料上容易过拟合128 对毕设级别的数据量是常用起点。window5表示上下文窗口为前后 5 个词窗口越大越关注主题语义越小越关注语法角色。min_count2表示出现次数少于 2 次的词直接丢弃避免低频噪声词进入模型。sg1使用 Skip-gram对低频词更友好适合评论这种短文本。训练完后保存成 word2vec.model 后面 LSTM 的 embedding 层会加载它。提示如果你的语料不到 1 万条min_count建议降到 1否则词表太小很多评论会被 pad 成空序列。3. LSTM 网络结构设计与序列截断策略3.1 Embedding 层为什么必须加载 word2vec 权重LSTM 不能直接吃词编号需要先经过 embedding 层把每个词映射成稠密向量。如果你不加载 word2vec而是随机初始化 embedding等于让网络从头学词义小语料下基本学不出什么来。项目里的做法是把 word2vec 训练出来的权重矩阵转成 embedding 矩阵再冻结或微调。from keras.layers import Embedding vocab_size len(word2vec_model.wv.key_to_index) embedding_matrix np.zeros((vocab_size 2, 128)) for idx, word in enumerate(word2vec_model.wv.key_to_index): embedding_matrix[idx 1] word2vec_model.wv[word] embedding_layer Embedding( input_dimvocab_size 2, output_dim128, weights[embedding_matrix], trainableTrue, mask_zeroTrue )参数说明vocab_size 2是为了留出 0 给 padding留出 1 给 OOV 词词表外的词统一映射到编号 1。mask_zeroTrue配合 embedding 层可以让 LSTM 自动跳过 padding 位置避免无效计算。trainableTrue表示词向量在训练过程中会继续微调这在小语料上是有利的。3.2 序列长度评论不是越长越有用每条评论长度不一样LSTM 要求同一 batch 内序列长度一致所以要么截断要么补零。项目里dataPreprocess.py会统计所有评论的长度分布取一个能覆盖 90% 样本的长度作为max_len。常见的做法是先画长度直方图再取 P90 位置的值。from keras.preprocessing.sequence import pad_sequences import numpy as np # 假设 stats 是每条评论分词后的长度列表 max_len int(np.percentile([len(seq) for seq in seqs], 90)) padded_seqs pad_sequences(seqs, maxlenmax_len, paddingpost, truncatingpost, value0)参数说明paddingpost表示在序列尾部补零。truncatingpost表示超长部分从尾部截断。这样做的原因是评论的开头往往包含核心观点保留开头信息比保留结尾更有价值。如果你发现评论的开头经常是“亲很好”这类灌水内容可以把 padding 和 truncating 都改成pre让网络更关注结尾。3.3 LSTM 网络结构项目里LSTM.py定义的是一个两层 LSTM 加全连接输出的结构from keras.models import Sequential from keras.layers import LSTM, Dense, Dropout model Sequential() model.add(embedding_layer) model.add(LSTM(units256, return_sequencesTrue, dropout0.3, recurrent_dropout0.3)) model.add(LSTM(units128, dropout0.3, recurrent_dropout0.3)) model.add(Dropout(0.5)) model.add(Dense(units1, activationsigmoid)) model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy] )参数说明第一层 LSTM 的units256return_sequencesTrue让它输出完整的序列给第二层 LSTM 继续提取高维时序特征。第二层units128不返回序列只输出最后一个时间步的隐藏状态。dropout0.3是输入和 recurrent 连接的 dropoutrecurrent_dropout对循环连接做 dropout这两项是防过拟合的关键。最后用 sigmoid 输出一个 0 到 1 之间的概率大于 0.5 判为虚假评论。模型参数量估算embedding 层参数量是(vocab_size 2) * 128如果词表 3 万就是约 384 万参数。LSTM 层的参数量是4 * (hidden_size * (input_dim hidden_size) hidden_size)第一层输入是 128 维所以参数量4 * 256 * (128 256 1) ≈ 39 万。整个模型不算大CPU 上也能训练。注意第一层 LSTM 的输入维度必须是 embedding 层的 output_dim也就是 128。如果你的vector_size改成了 64embedding 和 LSTM 的输入维度都要同步改。4. 模型训练早停、动态学习率和模型保存策略4.1 训练集和验证集的划分项目里trainer.py把 train.csv 按 8:2 划分训练集和验证集。这里有个容易踩的坑评论数据往往按时间分布直接随机划分会把同一时间段的数据同时分到训练集和验证集造成验证集指标虚高。更合理的做法是按时间去重划分。但毕设数据量有限的话随机划分也可以用只要你能解释清楚风险就行。from sklearn.model_selection import train_test_split x_train, x_val, y_train, y_val train_test_split( padded_seqs, labels, test_size0.2, random_state42, stratifylabels )参数说明stratifylabels是因为虚假评论和真实评论的比例天然不平衡分层抽样能保证训练集和验证集的类别比例和全量数据一致避免验证集里恰好全是某一类。random_state42固定随机种子保证多次实验的结果可以复现。4.2 早停和学习率衰减LSTM 训练到后期很容易过拟合表现是训练集 loss 一直降验证集 loss 先降后升。项目里通过EarlyStopping监控验证集 loss连续 5 个 epoch 不下降就停同时把学习率按系数衰减。from keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks [ EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ), ReduceLROnPlateau( monitorval_loss, factor0.5, patience2, min_lr1e-6 ), ModelCheckpoint( sentiment.h5, monitorval_accuracy, save_best_onlyTrue ) ] history model.fit( x_train, y_train, batch_size64, epochs50, validation_data(x_val, y_val), callbackscallbacks )参数说明patience5意思是验证集 loss 连续 5 个 epoch 没有改善就终止训练。ReduceLROnPlateau在验证集 loss 连续 2 个 epoch 不降时把学习率乘以 0.5帮助模型跳出局部最优。ModelCheckpoint只在验证集准确率创新高时保存模型最终拿到的是整个训练过程中表现最好的权重而不是最后一次 epoch 的权重。训练完成后项目根目录下的 sentiment.h5 就是保存下来的模型文件。4.3 损失曲线怎么读训练完第一件事不是看准确率而是画 loss 曲线。我一般会把训练集 loss 和验证集 loss 曲线叠在一张图里观察有没有开口开口越早说明过拟合越严重。import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.show()如果训练集 loss 最终降到 0.1 左右验证集 loss 停在 0.3 到 0.4 之间这个差距是正常的。但如果验证集 loss 在某个 epoch 后开始反弹而训练集 loss 还在降说明模型开始背训练集了这时候应该提高 dropout 或者调小 LSTM 的 units。5. 真实数据上的预测与阈值调优5.1 预测流程模型训练好之后predict.py是实际使用的入口。它做的事情和训练时完全一致清洗评论、分词、去停用词、查词表转编号、pad 到固定长度然后加载模型输出概率。from keras.models import load_model import numpy as np model load_model(sentiment.h5) def predict_comment(text: str, max_len: int 64) - float: cleaned clean_comment(text) tokens tokenize(cleaned, stopwords) # 把词映射成编号OOV 词映射到 1padding 用 0 seq [word2vec_model.wv.key_to_index[w] 2 if w in word2vec_model.wv.key_to_index else 1 for w in tokens] seq seq[:max_len] [0] * (max_len - len(seq)) if len(seq) max_len else seq[:max_len] x np.array([seq]) prob model.predict(x, verbose0)[0][0] return prob sample 卖家服务态度很好物流也很快东西质量没问题好评 print(f虚假概率: {predict_comment(sample):.4f})参数说明编号映射里为什么加 2因为词向量矩阵第 0 行是 padding第 1 行是 OOV真正的词从第 2 行开始。预测的时候 OOV 词统一映射到 1。输出概率代表模型判断该评论为虚假评论的置信度越接近 1 越可能是虚假评论。5.2 默认阈值 0.5 不一定适合你的数据把阈值定在 0.5 是最朴素的做法但虚假评论识别这种场景里存在明显的类别不平衡精确率和召回率的权衡点不在 0.5。我跑了一下真正有用的区间在 0.6 到 0.8 之间。阈值定高保证查出来的基本都是虚假评论但会漏掉那些语气模糊的阈值定低能捞回更多的可疑评论但误杀率会上去。def predict_with_threshold(prob: float, threshold: float 0.65) - str: return 虚假评论 if prob threshold else 正常评论 # 遍历阈值找到精确率和召回率的平衡点 from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_val, val_probs) f1_scores 2 * precision * recall / (precision recall) best_idx np.argmax(f1_scores) print(f最佳阈值: {thresholds[best_idx]:.3f}, F1: {f1_scores[best_idx]:.3f})说明precision_recall_curve基于验证集的真实标签和模型输出的概率计算不同阈值下的精确率、召回率。F1 是精确率和召回率的调和平均F1 最大时对应的阈值就是当前数据下的最优阈值。这个值通常会落在 0.5 以上把它作为预测时的判定标准比硬编码 0.5 客观得多。5.3 一个真实预测结果的分析拿项目自带的训练数据跑一遍随便挑几条测试python predict.py输出示例基于项目训练结果的可能效果评论内容虚假概率判定加微信返现五元评价内容随意写0.92虚假评论物流太快了凌晨下单中午就到很意外0.12正常评论质量还行用了一周才来评价没发现问题0.23正常评论好评老板人不错下次还会来0.58正常评论边界第三条和第四条能明显看出正常评论里的具体细节越多概率越低而那种空泛、缺乏细节、还带交易诱导的评论概率通常超过 0.85。如果你的使用场景是电商平台这套逻辑可以直接套在商品评论上如果是外卖场景建议加入“送饮料”“五星好评返现”这类外卖黑话到停用词或特征里。6. 部署边界短文本误判与增量更新技巧6.1 短文本的处理技巧评论数据里大量出现“好评”“质量好”“还不错”这种 5 到 10 个字的短文本。这类文本分词后只剩 1 到 2 个词LSTM 几乎学不到序列信息模型基本等同于在赌先验概率。我在项目里验证过过滤掉分词后长度小于 3 的评论后验证集准确率大约能提升 2 个百分点。操作方式也很简单filtered [(seq, label) for seq, label in zip(seqs, labels) if len(seq) 3]这个过滤逻辑要放在 pad 之前做否则截断后看不出真实长度。如果业务上必须处理短文本可以尝试 n-gram 特征和 LSTM 的 embedding 向量拼接短文本靠 bigram 补语义长文本靠 LSTM 补时序。6.2 增量更新新出现的虚假评论怎么吸收线上跑一段时间后会积累一批新的误判样本和高置信度的虚假评论。把这些样本重新放回训练集里微调比从头训练快得多。做法是加载 sentiment.h5在原有权重基础上用小学习率继续训练from keras.models import load_model model load_model(sentiment.h5) model.compile(optimizerkeras.optimizers.Adam(learning_rate1e-5), lossbinary_crossentropy, metrics[accuracy]) model.fit(x_new, y_new, batch_size32, epochs5) model.save(sentiment_finetuned.h5)为什么要把学习率从默认的 1e-3 降到 1e-5因为已经训练好的模型权重已经在一个比较理想的局部最优点周围学习率太大一步就跨出去了会把之前的语义特征破坏掉。epochs5也够微调不需要训练太久。增量微调后的模型要把旧模型的验证结果存档用来对比微调前后是否发生了回退。6.3 数据集不平衡的兜底手段如果拿到手的训练集中虚假评论只占 1%直接用准确率做评估指标没有参考价值全预测真实评论也有 99% 准确率。项目里的做法是用类别权重来惩罚误判样本数少的类from sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight( class_weightbalanced, classesnp.unique(y_train), yy_train ) class_weight_dict dict(enumerate(class_weights)) model.fit(x_train, y_train, batch_size64, epochs50, validation_data(x_val, y_val), class_weightclass_weight_dict, callbackscallbacks)compute_class_weight会自动根据各类样本数量的倒数归一化算出权重值。比如虚假评论占比少它的权重就会大于 1损失函数里每条虚假评论样本贡献的 loss 占比更高模型会更努力去学习这些少数样本的特征。加了类别权重之后训练集的整体准确率可能会略微下降但宏观 F1 会明显上升这才是更需要关注的指标。6.4 模型文件的使用边界最后提醒一个很多人都忽略的点sentiment.h5 里的词表是训练语料的词表加载后只能识别训练时见过的词。如果你换了业务场景比如从电商评论换到新闻评论词表重合度不够预测效果会大幅退化。正确的做法是保留 word2vec.model 和 tokenizer 状态文件一起打包使用。项目里的 use_info 文档里标注了模型版本和数据格式我在实战中见过太多只拿了 .h5 文件就跑新数据的情况效果不好就开始调阈值实际上问题出在词表没对齐。这个项目的文件组织已经把 model、word2vec model、tokenizer 分开放置了部署时保持这个结构不变就行。本文还有配套的精品资源点击获取