简介:这份资源是面向计算机相关专业学生与项目实战学习者的Python毕业设计完整方案,主题为基于神经网络的虚假评论识别系统,经导师指导并通过答辩,获得98分评价,也可用于课程设计或期末大作业。压缩包共23个文件,约2.91MB,包含6个py源码文件、8个pyc编译文件、2个docx说明文档、2个txt文本、1个xlsx表格、1个csv数据集、1个model模型文件与1个h5权重文件,覆盖数据预处理、语料处理、词向量训练、LSTM建模、预测与训练流程等模块。资源已有183人学习下载,说明其参考价值获得一定认可。读者可获得从数据清洗、word2vec词向量构建到LSTM模型训练与预测的完整代码链路,配套代码流程说明文档与需求清单,便于快速理解项目结构、复现实验并在此基础上完成二次开发或论文撰写。
1. 从一份 zip 说起:虚假评论识别到底在识别什么
电商评论区里那条“用了三天,效果惊艳,强烈推荐”大概率不是真人写的。虚假评论识别要做的,就是给每条评论打一个概率分,判断它属于真实体验还是营销灌水。这个方向在毕业设计里出现频率极高,因为它同时踩中了文本分类、神经网络和可演示系统三个点,工作量可控,答辩时也讲得清楚。你拿到的这份 zip 通常包含三块:一份带标签的评论数据集、一套基于神经网络的训练与推理代码、一份把模型跑起来的使用说明。它解决的不是“识别水军账号”这种需要图关系的任务,而是单条文本的二分类问题。适合谁?适合已经装好 Python、能看懂pip install、但还没把神经网络真正跑通过一次的人。下面我按“数据怎么进、模型怎么搭、结果怎么验”的顺序,把这份东西拆开讲。
2. 数据先过关:虚假评论语料的清洗与向量化
2.1 为什么虚假评论不能直接丢进神经网络
原始评论里混着大量对分类无用的东西:HTML 标签、表情符号、重复标点、平台水印、以及“此用户未填写评价内容”这类系统占位文本。更麻烦的是类别不平衡——真实评论往往远多于虚假评论,如果直接训练,模型会把所有样本都判成“真实”,准确率看着有 90%,但虚假评论一条都抓不出来。所以第一步不是搭网络,而是把数据变成“干净且平衡”的输入。
常见做法是先把标签列和文本列分离,标签统一成 0/1,然后做三件事:去重(同一模板刷出来的评论高度相似)、去噪(正则清掉非中文字符和多余空白)、分层采样(让正负样本比例接近 1:1)。这里有个容易被忽略的点:去重不能只按完全相等判断,很多虚假评论只改了几个词,需要用相似度做近似去重,否则训练集和验证集会互相“泄漏”,指标虚高。
2.2 用 jieba 加分词表做中文切分
中文没有空格,必须先分词。下面这段是我一般会先跑的预处理脚本,作用是读入原始 CSV,清洗后输出分词结果和标签。
import re import jieba import pandas as pd # 读取原始评论数据,假设两列:content 文本,label 标签 df = pd.read_csv("raw_comments.csv", encoding="utf-8") df = df.dropna(subset=["content", "label"]) # 去掉完全重复的评论,避免训练验证集泄漏 df = df.drop_duplicates(subset=["content"]) def clean_text(text): # 去掉 HTML 标签、网址、@ 提及和多余空白 text = re.sub(r"<.*?>", "", str(text)) text = re.sub(r"http\S+|www\.\S+", "", text) text = re.sub(r"@\S+", "", text) text = re.sub(r"\s+", " ", text) # 只保留中文、英文和数字,其余符号统一丢弃 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9]", "", text) return text.strip() df["clean"] = df["content"].apply(clean_text) df = df[df["clean"].str.len() > 3] # 太短的评论没有分类价值 # 加载自定义词典,把领域词加进去,避免被切碎 jieba.load_userdict("user_dict.txt") df["tokens"] = df["clean"].apply(lambda x: " ".join(jieba.lcut(x))) df[["tokens", "label"]].to_csv("clean_comments.csv", index=False, encoding="utf-8") print(df["label"].value_counts())逻辑说明:drop_duplicates处理完全重复,clean_text负责去噪,jieba.load_userdict是关键——像“好评返现”“刷单”这类词如果不在词典里,会被切成单字,模型学不到有效特征。参数上,str.len() > 3这个阈值可以调,评论太短时噪声占比高,我一般设在 3 到 5 之间。跑完看value_counts(),如果两类差距超过 3 倍,就要在训练时加类别权重或做欠采样。
2.3 向量化:从词袋到词嵌入的取舍
分词之后要把文本变成数字。毕业设计里常见两种做法:TF-IDF 加分类器,或者词嵌入加神经网络。既然标题写的是神经网络,就用嵌入层。但嵌入层有两种来源:一种是自己训练一个Embedding层,随机初始化;另一种是加载预训练词向量。前者简单、不依赖外部文件,适合数据量几千到几万条的场景;后者在数据量小的时候能明显提升效果,但需要额外下载词向量文件。
我一般会先跑随机初始化的版本,确认整条链路通了,再考虑换预训练。词表大小按实际词频截断,比如保留出现次数大于等于 2 的词,其余归为<UNK>。序列长度取 95 分位数,不要直接取最大值,否则大量填充会拖慢训练。下面这段把分词结果转成定长索引序列。
from collections import Counter import numpy as np MAX_LEN = 128 # 覆盖绝大多数评论长度 MIN_FREQ = 2 # 低频词归入 UNK texts = df["tokens"].tolist() labels = df["label"].astype(int).tolist() # 统计词频,构建词表 counter = Counter() for t in texts: counter.update(t.split()) vocab = {"<PAD>": 0, "<UNK>": 1} for word, freq in counter.items(): if freq >= MIN_FREQ: vocab[word] = len(vocab) def encode(text): ids = [vocab.get(w, vocab["<UNK>"]) for w in text.split()] if len(ids) < MAX_LEN: ids += [vocab["<PAD>"]] * (MAX_LEN - len(ids)) return ids[:MAX_LEN] X = np.array([encode(t) for t in texts]) y = np.array(labels) np.save("X.npy", X) np.save("y.npy", y) print("词表大小:", len(vocab), "样本数:", len(X))逻辑说明:MIN_FREQ控制词表规模,太小会引入大量只出现一次的词,模型记不住;太大则很多词变成<UNK>,信息丢失。MAX_LEN取 128 是中文评论的常见经验值,短评论多的话可以降到 64。<PAD>必须放在索引 0,后面嵌入层要设置padding_idx=0,否则填充位也会参与梯度更新。
3. 模型搭起来:TextCNN 与 BiLSTM 两条路怎么选
3.1 前馈网络为什么在文本上不够用
热词里常出现“bp 神经网络拟合曲线”“前馈神经网络”,这类全连接网络处理表格数据没问题,但直接用在文本上会丢掉词序。虚假评论的一个典型特征是“语序异常”或“模板化表达”,比如“质量很好很好很好”这种重复,全连接网络把每个词独立看待,学不到这种局部模式。所以文本分类更常用卷积或循环结构。TextCNN 用不同尺寸的卷积核抓 n-gram 特征,训练快、参数少;BiLSTM 按顺序读,能捕捉长距离依赖,但训练慢。毕业设计里我一般先上 TextCNN,因为它在小数据集上不容易过拟合,而且推理速度快,演示时体验好。
3.2 TextCNN 的完整实现与关键参数
下面是一个可以直接跑的 TextCNN,输入是上一节生成的X.npy和y.npy。
import torch import torch.nn as nn import numpy as np from torch.utils.data import TensorDataset, DataLoader class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim=128, num_filters=100, kernel_sizes=(2,3,4), num_classes=2): super().__init__() # padding_idx=0 保证填充位不参与训练 self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) # 多尺寸卷积核,分别抓二元、三元、四元词组特征 self.convs = nn.ModuleList([ nn.Conv1d(embed_dim, num_filters, k) for k in kernel_sizes ]) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(num_filters * len(kernel_sizes), num_classes) def forward(self, x): x = self.embedding(x) # [B, L, E] x = x.permute(0, 2, 1) # [B, E, L] 卷积要求通道在前 x = [torch.relu(conv(x)) for conv in self.convs] x = [torch.max_pool1d(f, f.size(2)).squeeze(2) for f in x] x = torch.cat(x, dim=1) x = self.dropout(x) return self.fc(x) X = np.load("X.npy") y = np.load("y.npy") vocab_size = int(X.max()) + 1 dataset = TensorDataset(torch.LongTensor(X), torch.LongTensor(y)) loader = DataLoader(dataset, batch_size=64, shuffle=True) model = TextCNN(vocab_size) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() for epoch in range(10): model.train() total_loss = 0 for bx, by in loader: optimizer.zero_grad() out = model(bx) loss = criterion(out, by) loss.backward() optimizer.step() total_loss += loss.item() print(f"epoch {epoch+1}, loss {total_loss/len(loader):.4f}") torch.save(model.state_dict(), "textcnn.pt")逻辑说明:embed_dim=128是嵌入维度,数据量小的时候可以降到 64;num_filters=100是每个卷积核的输出通道数,三组核共 300 维特征;kernel_sizes=(2,3,4)对应二元到四元词组,这是 TextCNN 的经典配置。padding_idx=0必须和前面词表的<PAD>索引一致。dropout=0.5是防过拟合的关键,如果训练 loss 下降但验证 loss 上升,先调这个。batch_size=64在几千条数据上比较稳,显存不够就降到 32。
3.3 BiLSTM 版本与两者对比
如果答辩老师追问“为什么不用循环网络”,你得能说清楚。BiLSTM 的实现把卷积部分换掉即可:
class BiLSTM(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=128, num_classes=2): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True, bidirectional=True) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(hidden_dim * 2, num_classes) def forward(self, x): x = self.embedding(x) out, _ = self.lstm(x) # 取最后一个时间步的正反向拼接 x = out[:, -1, :] x = self.dropout(x) return self.fc(x)对比一下:TextCNN 训练一个 epoch 通常几秒,BiLSTM 要慢三到五倍;在五千条以内的数据上,两者准确率差距往往在 1 到 2 个百分点内,TextCNN 甚至更稳。所以我的习惯是先用 TextCNN 出基线,如果准确率卡在某个值上不去,再换 BiLSTM 看是否有提升。hidden_dim=128配合双向就是 256 维输出,数据量再小就降到 64。
4. 训练与评估:别被 99% 的准确率骗了
4.1 划分数据集时最容易犯的错
很多人把数据随机打乱后按 8:2 切分,然后报告一个很高的准确率。问题在于:如果同一条评论的变体同时出现在训练集和测试集,模型只是记住了模板,不是学会了判断。正确做法是按“评论模板”或“用户”分组切分,保证同一来源的样本只出现在一边。毕业设计里如果数据没有用户 ID,至少要做近似去重后再切分。另外,验证集要用来调超参数,测试集只在最后跑一次,不要反复用测试集调参。
4.2 评估指标:准确率之外必须看 F1
虚假评论识别是类别不平衡问题,准确率会骗人。假设 100 条里只有 10 条虚假,全判真实也有 90% 准确率。所以要同时看精确率、召回率和 F1。精确率高说明判为虚假的确实大多是虚假,召回率高说明虚假评论被抓得全。毕业设计答辩时,老师大概率会问“你的召回率是多少”,提前算好。
from sklearn.metrics import classification_report, confusion_matrix import torch model.load_state_dict(torch.load("textcnn.pt")) model.eval() # 这里用测试集,实际项目中测试集应单独留出 with torch.no_grad(): logits = model(torch.LongTensor(X)) preds = torch.argmax(logits, dim=1).numpy() print(confusion_matrix(y, preds)) print(classification_report(y, preds, target_names=["真实", "虚假"]))逻辑说明:confusion_matrix看四个数——真阳、假阳、真阴、假阴。如果假阴很高,说明虚假评论漏判多,可以调低判定阈值或加类别权重。classification_report直接给出 F1,写论文时用这个数比准确率有说服力。注意这里为了演示用了全量数据,实际要换成独立的测试集。
4.3 推理脚本:把模型变成能演示的工具
毕业设计需要现场演示,所以得有一个接收输入、输出结果的脚本。下面这个把训练好的模型封装成函数,输入一条评论返回判定和概率。
def predict(text, model, vocab, max_len=128): model.eval() ids = [vocab.get(w, vocab["<UNK>"]) for w in jieba.lcut(clean_text(text))] if len(ids) < max_len: ids += [vocab["<PAD>"]] * (max_len - len(ids)) ids = torch.LongTensor(ids[:max_len]).unsqueeze(0) with torch.no_grad(): prob = torch.softmax(model(ids), dim=1)[0] label = "虚假评论" if prob[1] > 0.5 else "真实评论" return label, prob[1].item() print(predict("质量很好,下次还来", model, vocab))逻辑说明:unsqueeze(0)把单条样本变成 batch 维度为 1 的输入。prob[1]是虚假类的概率,阈值 0.5 可以按业务调整——宁可错杀就调低,宁可放过就调高。这个函数可以直接接到 Flask 或 Gradio 上做界面,演示时输入一句话就能看到结果。
5. 避坑与排查:那些让模型“看起来能跑”的陷阱
5.1 现象:训练 loss 一直不降,准确率停在 50% 左右
原因通常是标签没对齐或输入全是填充。检查X.npy里是不是大部分值都是 0,如果是,说明分词后编码出了问题,比如词表构建时用了错误的列。另一个常见原因是标签列被当成了文本读进来,y全是字符串。解决:打印X[:5]和y[:5],确认索引和标签都是整数,且非零值占多数。
5.2 现象:验证集准确率远低于训练集
这是过拟合的典型表现。原因可能是模型参数太多而数据太少,或者训练轮数过多。解决:先把embed_dim从 128 降到 64,num_filters从 100 降到 50,dropout从 0.5 提到 0.6,再减少 epoch。如果还不行,就加 L2 正则,在优化器里设weight_decay=1e-4。别急着上更复杂的模型,先让简单模型不过拟合。
5.3 现象:预测时所有评论都判成同一类
原因通常是类别不平衡导致模型学到了“全判多数类”的策略。解决:在CrossEntropyLoss里加weight参数,给少数类更高权重,比如weight=torch.tensor([1.0, 3.0])。另一个可能是推理时阈值设错了,检查prob[1]的分布,如果都在 0.5 以下,说明模型对虚假类的置信度整体偏低,需要重新训练或调整阈值。
5.4 现象:换一台机器跑,结果完全不一样
原因是没有固定随机种子。神经网络的初始化、数据打乱都带随机性,不设种子就无法复现。解决:在训练脚本开头加torch.manual_seed(42)、np.random.seed(42),并在DataLoader里设shuffle=True但配合种子。这样每次跑的结果基本一致,答辩演示时不会翻车。
5.5 现象:使用说明里的命令报“模块找不到”
原因通常是环境没装全,或者 Python 版本不匹配。解决:先确认torch、jieba、pandas、sklearn都装了,用pip list检查。如果报No module named 'torch',说明 PyTorch 没装或装到了另一个 Python 环境。建议用虚拟环境,python -m venv venv后激活再装依赖。使用说明里如果写了版本号,按那个版本装,别盲目升级。
6. 把模型推到能答辩的水平:阈值调优与错误分析
模型跑通只是及格线,要让答辩老师觉得你真正理解了这个任务,还得做两件事:阈值调优和错误分析。阈值调优是因为 0.5 只是默认值,实际业务里虚假评论的误判成本不同。如果平台宁可错杀,就把阈值降到 0.3;如果怕误伤真实用户,就提到 0.7。做法很简单:在验证集上遍历 0.1 到 0.9,画一条 F1 随阈值变化的曲线,取最高点。
import numpy as np from sklearn.metrics import f1_score model.eval() with torch.no_grad(): probs = torch.softmax(model(torch.LongTensor(X)), dim=1)[:, 1].numpy() best_th, best_f1 = 0.5, 0 for th in np.arange(0.1, 0.9, 0.05): preds = (probs > th).astype(int) f1 = f1_score(y, preds) if f1 > best_f1: best_th, best_f1 = th, f1 print(f"最佳阈值 {best_th:.2f}, F1 {best_f1:.4f}")这段代码输出的是验证集上的最优阈值,把它写进推理脚本,比固定 0.5 更合理。注意别在测试集上调阈值,否则指标会偏乐观。
错误分析则是把判错的样本导出来看。我一般会导出假阳和假阴各 20 条,人工读一遍。常见规律是:假阴往往是“短评+夸张词”的组合,比如“太好了”这种没有具体细节的评论,模型容易判成真实;假阳则常出现在带强烈情感但确实是真实体验的评论上,比如“太差了,退货”。针对假阴,可以在特征里加入“是否包含具体使用细节”这类规则特征;针对假阳,可以补充更多真实负面评论进训练集。这一步不需要改网络结构,但能明显提升 F1,而且写进论文的“错误分析”章节会很有说服力。
最后一个习惯:每次改完参数,把配置、F1、阈值记在一个表格里,别靠脑子记。我吃过亏,调了十几轮之后忘了哪组最好,只能重跑。下面是我常用的记录格式。
| 实验编号 | embed_dim | num_filters | dropout | 阈值 | F1 |
|---|---|---|---|---|---|
| 1 | 128 | 100 | 0.5 | 0.50 | 0.86 |
| 2 | 64 | 50 | 0.6 | 0.45 | 0.88 |
| 3 | 64 | 50 | 0.6 | 0.40 | 0.89 |
这张表比任何“总结”都有用,答辩时直接展示,老师能看到你的调参过程。希望帮到你。
本文还有配套的精品资源,点击获取