
简介面向毕业设计场景的深度学习虚假评论检测系统源码围绕评论文本真伪识别构建完整流程适合计算机相关专业学生直接运行也适合作为算法模型与Web工程结合的毕业设计参考。压缩包共二十四个文件主体为二十个Python脚本涵盖后端逻辑、推理模型、数据接口等模块另含数据库文件、配置说明及版本管理忽略文件资源总大小约707KB便于下载与快速部署。目前已有二百三十七人学习下载项目经过调试确保可运行并配有前端页面与后端管理功能可体验从评论数据入库、深度模型预测到前端结果展示的完整链路。借助源码可系统学习文本预处理、模型训练推理以及前后端联调的实现细节也能复用目录结构与工程组织方式提升独立完成毕业设计项目的效率。1. 为什么毕设和入门都绕不开它这套虚假评论检测源码到底装着什么同样一条五星好评你能分辨它是真实消费后的反馈还是商家批量制造的刷单吗这个问题的工程化版本就是“基于深度学习的虚假评论检测系统源码”。我拆过不少毕设资源多数打包的只是半成品但这份源码把数据清洗、文本建模、模型训练到单条预测全流程串在了一条能跑通的管线上对毕业设计和深度学习入门者都算友好。它解决的问题很聚焦输入一批评论文本输出每条评论是“真实”还是“虚假”的概率。适合两类人一类是需要交毕设项目的学生另一类是手里有评论数据、想做风控预筛选的从业者。前者能照着改参数完成自己的系统后者能直接借用管线做数据探查。不过它也不是开箱即用数据格式、词表构建、损失函数这些旋钮都得自己摸一遍。下面按数据、模型、训练、部署的顺序把源码里的关键位置给你标出来。2. 数据管线先行源数据怎样变成可训练的样本2.1 先摸清数据格式字段、标签与正负样本分布打开项目里的数据文件常见做法是用 CSV 或 JSON 存储。不管哪种核心字段就两个一条评论文本一个类别标签。标签的取值一般是 0 表示真实、1 表示虚假也有用 deceptive/genuine 命名的版本。第一次拿到数据我会先用 pandas 把结构和分布打印出来别急着跑训练。import pandas as pd df pd.read_csv(data/reviews.csv) print(df.head()) print(df.shape) print(df[label].value_counts())这段代码的作用是确认三件事列名是否和后续代码预期一致、数据量大概什么量级、正负样本是否严重失衡。如果虚假评论只占 5%后面就得考虑类别加权或者采样策略否则模型很容易学成“全判真实”。参数上value_counts()输出的比例比总数更重要先记住这个比例后面训练时对照着看有没有改善。2.2 清洗与序列化从原始文本到 token id 的全过程字符级噪声比如 HTML 标签、引号转义和不可见字符会直接干扰分词。常见做法是先用正则和html.unescape做一轮粗清洗再按空格分词。import re import html def clean_text(text: str) - str: text html.unescape(text) text re.sub(r[^], , text) text re.sub(r[/\\*\[\](){}|], , text) text re.sub(r\s, , text) return text.strip()逻辑不复杂但有个细节值得注意这里刻意保留标点和数字不移除。深度学习文本分类里像“!!!”“????”这类标点堆叠本身就是虚假评论的弱信号一律清掉反而丢信息。所以清洗的原则是去格式噪声而不是去内容。清洗完接着建词表词表大小直接影响 embedding 参数量和训练速度。from collections import Counter import torch from torch.nn.utils.rnn import pad_sequence def build_vocab(texts, max_vocab50000): all_words [] for t in texts: all_words.extend(t.split()) counter Counter(all_words) most_common [w for w, _ in counter.most_common(max_vocab - 2)] vocab {w: i 2 for i, w in enumerate(most_common)} vocab[pad] 0 vocab[unk] 1 return vocab def encode(texts, vocab, max_len256): seqs [] for t in texts: ids [vocab.get(w, vocab[unk]) for w in t.split()] ids ids[:max_len] seqs.append(torch.tensor(ids, dtypetorch.long)) return pad_sequence(seqs, batch_firstTrue, padding_valuevocab[pad])build_vocab用词频截断而不是保留全部词防止出现只在一条评论里冒出来一次的噪声词。max_vocab50000对公开的中英文评论语料都够用。encode里ids[:max_len]是硬截断超过 256 个 token 的评论直接切掉如果显存足够可以试 512但绝大多数评论的语义在前面几十个词就完整表达了。pad_sequence自动对齐到 batch 内最长句padding_value0正好对应pad。注意unk和pad必须占 0 和 1否则 embedding 层的padding_idx会指错位置。2.3 划分数据集随机切分还是按来源分组切分这是整个数据环节里最影响说服力的一步。随机切分在学术 benchmark 上常见但在虚假评论场景会埋一颗雷同一个店铺的几十条刷单评论可能同时被切到训练集和验证集模型等于提前见过了答案验证指标虚高提交测试或上业务后立刻打回原形。我一般会这样做如果数据里带店铺名、商品 ID 或评论者 ID 这类分组字段就按分组字段划分 train/val/test而不是按行随机划分。让同一批刷单的评论要么全在训练里要么全在验证里才能测出模型在未知评论源上的真实泛化能力。from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(df, df[label], groupsdf[merchant_id])) train_df, val_df df.iloc[train_idx], df.iloc[val_idx]groups参数传的是分组字段这里用merchant_id表示商家 ID。GroupShuffleSplit会保证同一个商家的所有评论都落在同一侧。这一步不需要改模型只是分组方式不同但对最终分数的可信度影响非常大。毕业设计答辩时也很容易讲清楚评审问“你的验证集划分合理吗”你直接说“同一商家的评论没有跨集合”已经比大部分同题项目站得住。提示如果数据里没有商家或评论者字段至少按评论发送的时间窗口分桶避免同一天批量刷的评论被散到两边。3. 模型主路径TextCNN、BiLSTM 的源码结构与训练脚本3.1 为什么先选 TextCNN 当第一个模型评论文本是典型的短文本核心语义往往藏在几个关键短语里而不是整篇的全局依赖关系。TextCNN 用一组不同宽度的卷积核在词向量序列上滑动等价于固定窗口的 n-gram 特征提取器窗口 3 提取相邻三个词的组合信号窗口 4、5 再往上叠一层。这正好对着虚假评论里常见的高频套路堆砌感叹词、大段通用好评模板、同一语气反复出现。网络结构本身简单训练也快CPU 上都能在十几分钟内看到趋势。对毕业设计或第一次跑深度学习模型的读者我建议先把 TextCNN 跑通再上 BiLSTM 做对比实验这是最容易出成绩的路径。3.2 TextCNN 核心代码与参数选择import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embedding_dim100, num_filters256, kernel_sizes(3, 4, 5), num_classes2, dropout0.5): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.convs nn.ModuleList([ nn.Conv1d(embedding_dim, num_filters, k) for k in kernel_sizes ]) self.dropout nn.Dropout(dropout) self.fc nn.Linear(len(kernel_sizes) * num_filters, num_classes) def forward(self, x): emb self.embedding(x).transpose(1, 2) conved [torch.relu(conv(emb)) for conv in self.convs] pooled [torch.max(c, dim2).values for c in conved] cat torch.cat(pooled, dim1) return self.fc(self.dropout(cat))几个参数逐个说。embedding_dim100是词向量维度文本量不大就别激进上调100 在精度和内存之间比较平衡。num_filters256表示每种卷积核给 256 个输出通道特征多了模型容量大但需要更多数据喂饱它如果训练集只有几千条降到 128 更稳。kernel_sizes(3,4,5)对应三组卷积核每组关注的短语长度不同最后把三个池化结果拼接接一个全连接层输出 2 类 logits。forward里emb.transpose(1,2)把维度从(batch, seq_len, embed_dim)换成(batch, embed_dim, seq_len)因为Conv1d默认卷积输入通道在第二维很多人第一次写就在这翻车。torch.max(..., dim2)取时间维最大值是 max-pooling比 average-pooling 更能保留“这个短语是否出现过”的强信号对虚假评论识别也更合适。3.3 BiLSTM双向结构的价值在哪hidden_size 怎么定TextCNN 的弱点是只看局部组合顺序信息用得不充分。BiLSTM 补的正是这一块正向传播捕获从前到后的上下文反向传播捕获从后到前的上下文两个方向的隐状态拼起来代表当前位置的完整语义。class BiLSTMClassifier(nn.Module): def __init__(self, vocab_size, embedding_dim100, hidden_size128, num_layers2, num_classes2, dropout0.3): super().__init__() self.embedding nn.Embedding(vocab_size, embedding_dim, padding_idx0) self.lstm nn.LSTM(embedding_dim, hidden_size, num_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout if num_layers 1 else 0.0) self.dropout nn.Dropout(dropout) self.fc nn.Linear(hidden_size * 2, num_classes) def forward(self, x): emb self.embedding(x) out, _ self.lstm(emb) last out[:, -1, :] return self.fc(self.dropout(last))hidden_size128是隐状态维度。bidirectionalTrue时每个时间步的输出是前向和后向隐状态的拼接所以全连接层输入要写hidden_size*2。num_layers2是两层堆叠第二层能学到更抽象的句子模式超过 2 层在小数据集上反而容易过拟合。这里的out[:, -1, :]是直接取序列最后一个时间步的输出。这个写法很常见但有个隐患如果评论 padding 过长最后时间步可能落在大量pad上。更稳的做法是记录每条样本的真实长度取对应的最后有效时间步或者把out做 masked average pooling。源码包里如果只用了out[:, -1, :]我建议至少把max_len控制在 256 以内把 padding 带来的误差压到最低。3.4 训练脚本的关键设置loss、优化器、早停与存储训练部分的代码几乎决定了模型最终能不能用。第一个要点是类别加权。CrossEntropyLoss的weight参数直接给少数类别更高权重比后处理阈值更省事。class_weight torch.tensor([1.0, 5.0]) # 虚假类样本少就给更高权重 criterion nn.CrossEntropyLoss(weightclass_weight) optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemax, factor0.5, patience2)class_weight的具体值要根据 2.1 节看过的样本比例来设比如真实评论 2000 条、虚假评论 400 条5 倍权重是合理起点。优化器选 Adam 而不是 SGD短文本分类任务上 Adam 收敛稳、对学习率不敏感省去大量调参时间。ReduceLROnPlateau监控验证 F1连续两个 epoch 不涨就把学习率减半避免后期在局部最优周围震荡。第二个要点是早停和 checkpoint。训练中途过拟合是常态尤其是 epoch 超过 10 以后训练 loss 还在降验证指标已经不再动。常见做法是每轮验证后只保留验证 F1 最高的权重文件连续 3 轮没有新高就终止训练。best_f1 0.0 patience 0 for epoch in range(20): train_one_epoch(model, train_loader, criterion, optimizer) val_f1 evaluate(model, val_loader) if val_f1 best_f1: torch.save(model.state_dict(), checkpoints/best_model.pt) best_f1 val_f1 patience 0 else: patience 1 if patience 3: print(fearly stop at epoch {epoch}) break不保存最后一轮、只保存 best_f1 那轮是我踩过最痛的坑之一。曾经训练 BiLSTM 到第 12 轮验证分数明显回落评估时却直接 load 了最后一轮的权重线下结果比第 8 轮的 best 低了近 10 个百分点。从那以后我的训练脚本里必挂best_model.pt最后评估永远先加载这份文件。另外 BiLSTM 的反向传播容易梯度爆炸训练循环里加一行torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)会更稳这是这类模型的常规操作。4. 训练避坑过拟合、类别失衡与数据泄漏的排查记录4.1 验证集 F1 很高线下测试或换一批数据立刻变脸现象训练时验证 F1 到了 0.92以为模型已经很好拿到新店铺的评论一测F1 直接跌到 0.6。原因数据是按行随机切的同一商家刷出来的几十条评论被拆到了训练集和验证集两边模型实际见过这些评论的“同源答案”验证分数是虚的。这种问题在虚假评论场景里比一般文本分类更隐蔽因为刷单评论本身就是批量生成的随机切分几乎必然泄漏。解决改成分组划分即 2.3 节说的GroupShuffleSplit按商家 ID 或评论者 ID 切分保证同一来源的评论只在一边。这一步改完验证分数会“变丑”但那个丑分数才是真实的。验证分组是否生效也很简单取训练集和验证集各自的merchant_id打印交集是否为空空集才算通过。4.2 训练 loss 不降准确率一直卡在 50% 左右现象模型在训练集上半天不动loss 曲线近似水平输出基本在两类之间随机摇摆。原因最常见的是标签和文本对不上比如读 CSV 时 columns 顺序写反把评论 ID 当成 label其次是严重类别失衡时没做任何处理模型学成“全预测真实”也能有高准确率loss 表面低但任务没有进展。解决先把 dataframe 抽样打印出来人工核对index、text、label 三列肉眼扫一遍再做序列化。第二步用torch.unique统计train_loader里每个 batch 的标签分布确认 shuffle 之后两类都出现。这些都是“先查数据再查模型”的常规排查顺序。4.3 预测结果几乎全是“真实”虚假类被模型完全忽略现象验证集里虚假评论本来占 30%模型预测出来却只有 2% 的评论被判成虚假precision 看着很高recall 一塌糊涂。原因loss 里两类贡献本来就按样本数量加权虚假类样本少、对梯度的贡献占比低模型只要把边界推得偏保守就能把整体 loss 压得很低。解决第一层先给CrossEntropyLoss加 weight让虚假类的错分代价变大第二层如果还不行考虑对真实类做欠采样或者对虚假类做过采样。还有个更简单的经验把判定阈值从默认的 0.5 往下调比如设 0.4 或 0.3跑一遍验证集看 F1 变化有时候不重训就能救回来。阈值调整要记录对应的验证结果别凭感觉定死。4.4 显存溢出或训练速度慢到没法忍现象batch_size 设 128max_len 设 512GPU 6G 显存跑 BiLSTM 直接 OOM。原因PyTorch 的 embedding 和 LSTM 在长序列上显存开销按序列长度线性增长max_len 翻倍backward 记录的大量中间激活也跟着翻倍。解决先按 max_len128、batch_size32 跑通整个管线确认模型收敛趋势后再逐步往上加。BiLSTM 不要一上来就设 hidden_size256 和 num_layers31282 层在绝大多数评论分类场景已经够用。如果只有 CPUTextCNN 跑一轮可能几分钟BiLSTM 多出一倍多耐心等是可以接受的但调参时不要总做全量训练拿一个小的子集先验证管线不报错再上全量。4.5 模型加载时报 embedding 维度不匹配现象加载保存的 checkpoint 时RuntimeError 提示 weight shape 不匹配比如报 embedding.weight 的 size 是[45000, 100]和[40000, 100]不一致。原因训练时用的词表是 50000评估脚本里重新 build vocab 时因为 max_vocab 参数或清洗函数不同词表大小不一样state_dict 的 key 对不上。解决把 vocab、模型参数这类对象一并存成同一个 checkpoint 文件评估时只用 checkpoint 里的 vocab而不是重新生成。torch.save({ model: model.state_dict(), vocab: vocab, label_map: {0: 真实, 1: 虚假} }, checkpoints/best_model.pt) # 加载时 ckpt torch.load(checkpoints/best_model.pt) model build_model(vocab_sizelen(ckpt[vocab])) model.load_state_dict(ckpt[model])这个习惯能顺手解决几十种类似的序列化问题不只是 embedding 维度这一个。单独存一个 vocab.json 也是可以的但和权重放同一个文件里最省心评估脚本不会因为少读一个文件而报错。5. 把模型用起来单条预测、指标可视化与最小 Flask 接口5.1 单条预测脚本模型权重加载与预处理一致性训练和推理最大的坑在预处理不一致。训练时先clean_text再encode推理时如果直接把原始文本丢给模型unk比例飙升结果基本失真。比较稳的做法是把清洗和词表编码封装成公共函数训练和预测都调同一个入口。def predict_single(text: str, model, vocab, max_len256): model.eval() cleaned clean_text(text) ids encode([cleaned], vocab, max_len) with torch.no_grad(): logits model(ids) probs torch.softmax(logits, dim1) return probs[0, 1].item() # 虚假类概率encode返回的 ids 仍然是 batch 形式所以这里传入[text]再取probs[0]。返回的浮点数就是模型认为这条评论是虚假的概率后续根据业务自主定阈值。注意model.eval()不能省它关掉 dropout 和 batch norm 的训练行为推理输出才稳定。还要注意中英文预处理的差别。英文按空格切分没有问题中文则要先经过 jieba 分词或按字符切分否则训练和预测的分词结果对不上。如果你手头是中文评论数据把clean_text里的t.split()换成 .join(jieba.cut(t))再走同一个encode流程词表照样能建起来。5.2 用混淆矩阵和 ROC 曲线盯住指标而不是只看 accuracy毕业设计最常见的评审发问是“你这个模型效果到底怎么样”只报 accuracy 很容易被一句“样本均衡吗”问倒。虚假评论检测天然是样本不均衡任务要同时看 recall 和 precision。from sklearn.metrics import classification_report, roc_auc_score, confusion_matrix y_true, y_pred, y_prob [], [], [] for texts, labels in test_loader: outputs model(texts) _, preds torch.max(outputs, dim1) y_true.extend(labels.tolist()) y_pred.extend(preds.tolist()) y_prob.extend(torch.softmax(outputs, dim1)[:, 1].tolist()) print(classification_report(y_true, y_pred, target_names[真实, 虚假])) print(AUC:, roc_auc_score(y_true, y_prob)) print(confusion_matrix(y_true, y_pred))classification_report会同时输出每个类别的 precision、recall、f1-score一眼看出模型对虚假类的识别能力。AUC 是阈值无关的排序指标适合在答辩 PPT 里放一个数值代表模型的排序能力。如果只看准确率模型把所有评论都判成真实accuracy 也能到 70%但 Auc 和虚假类的 recall 会立刻暴露问题。如果要细调阈值可以循环扫一遍验证集for th in [0.3, 0.4, 0.5, 0.6, 0.7]: preds_th [1 if p th else 0 for p in y_prob] f1 f1_score(y_true, preds_th, pos_label1) print(fthreshold{th}, f1{f1:.4f})f1_score的pos_label1指定把虚假类当作正类这套循环能直接画出阈值和 F1 的关系答辩时顺手贴一张小表格也很有说服力。5.3 Flask 接口与答辩演示的验证方式演示环节没人看终端输出把一个输入框加提交按钮换成 POST 接口前端传一条评论文本接口返回概率 JSON演示效果比命令行好得多。from flask import Flask, request, jsonify app Flask(__name__) model, vocab load_model_and_vocab(checkpoints/best_model.pt) app.route(/predict, methods[POST]) def predict(): data request.get_json() text data.get(text, ) prob predict_single(text, model, vocab) return jsonify({fake_prob: round(prob, 4), is_fake: prob 0.5}) app.run(host0.0.0.0, port8080)load_model_and_vocab对应 4.5 节说的把 vocab 和权重存在一起的 checkpoint 格式否则单独 load state_dict 还会遇到词表对不上的问题。接口返回is_fake用的是硬阈值 0.5正式场景建议把阈值作为可配置项因为业务对不同漏报和误报的容忍度不一样。host0.0.0.0让局域网内的其他机器也能访问答辩演示时可以在手机上输入地址测试。接口验证用 curl 就够了不必先写前端页面curl -X POST http://127.0.0.1:8080/predict \ -H Content-Type: application/json \ -d {text: 这家店的东西质量太差了客服态度也很糟糕}返回的 JSON 里会带fake_prob和is_fake两个字段。如果返回异常先检查predict_single里有没有跑model.eval()再看vocab是不是 checkpoint 里那份。演示级接口不要处理高并发答辩场景一个人点两三次问题不大真要并发至少把模型加载放到全局变量避免每个请求重复读权重。6. 换一组数据重训从原始文本到验收的完整走查清单最后这一步是这套源码真正变成“你自己的系统”的分水岭。把官方示例数据换成你自己的语料不是侦探小说按固定套路走一遍半小时内能跑完。我的走查清单一般长这样步骤检查项预期结果1新数据的 text 和 label 字段是否存在且匹配每行一条评论标签值只有两类2label 分布是否均衡记录少数类占比决定 class_weight3有无分组字段有则用 GroupShuffleSplit无则随机划分4vocab 大小观察词表上限是否触及 max_vocab500005先用子集跑一遍1000 条样本能正常过 forward 和 backward6全量训练日志里验证 F1 持续上升早停正常触发7测试集报告confusion_matrix 中虚假类 recall 不要太低实际操作时先复制一个干净的入口脚本比如train_new_data.py把数据路径、列名、max_len、class_weight 这几个变量改成新数据集对应的值其他不动。训练跑起来后盯紧验证 F1 的走势如果连续几个 epoch 不涨按第 4 章的排查顺序从数据分布开始查而不是盲目调模型结构。这套流程走完新数据的结果没有大问题这份基于深度学习的虚假评论检测源码才算真正在你手里跑通了。这些年拆项目下来我习惯在每次换数据重训前强制做一遍第 1 和第 2 步的核对花不了两分钟但避免过“模型训了三个小时才发现标签列错位”这种极其浪费时间的翻车。希望帮到你。本文还有配套的精品资源点击获取