简介:一份基于Python与图卷积神经网络(GCN)的虚假影评/水军检测项目源码包,面向具备一定机器学习基础的高校学生及大创项目团队,旨在解决网络评论中水军刷评、异常群体识别等问题。项目完整覆盖了从用户-评论图构建、GCN模型设计、训练调优到实验评估的流程,包含9个Python脚本、8个CSV数据文件,以及BERT预训练配置、XML和JSON等辅助文件,压缩包共26个文件、约14.21MB,结构清晰便于直接运行与二次开发。数据侧提供猫眼长评、电影名单、电影类型等真实文本数据,模型侧涵盖图邻接矩阵构建、GCN节点分类、交叉熵损失、精确率/召回率评估等关键模块。目前已有70人学习下载。通过该资源可掌握图结构数据处理、GCN模型训练与可视化、异常评论识别等实操细节,同时积累从数据清洗到结果分析的一体化开发经验,适合用于课程设计、毕业设计或竞赛备赛。
1. 基于图卷积神经网络的虚假影评水军检测:这个项目解决的是“抱团”而不是“单条”
单看一条评论,水军写的和普通用户写的几乎没有区别;可如果把用户、评论、电影放进同一张图里,水军“一批账号集中评价同一批电影、时间点又高度重叠”的抱团特征会立刻暴露。这个基于图卷积神经网络的虚假影评水军检测项目,正是把猫眼长评数据建成用户-评论二部图,用 BERT 把评论文本编码成节点特征,再让两层 GCN 在图上传消息,最终输出每条评论的嫌疑概率。数据、模型、训练、结果 CSV 全链路都齐,适合想跑通第一个 GCN 实验的人,也适合大创项目直接拿来做基线。它不是那种只给核心代码的 Demo,而是一份能照着复现的完整源码包。
2. 数据准备与图构建:从猫眼长评到邻接矩阵
2.1 数据文件梳理:四份 CSV 分别承担什么角色
项目的数据目录maoyan下有四份关键表格,第一眼容易混淆的是maoyan_long_comments.csv和猫眼长评总表.csv。前者是逐条长评明细,记录评论ID、用户ID、评论内容、评分、评论时间、电影ID 这些字段;后者更接近一个按电影或用户维度汇总过的结果表。实操里我只把明细表作为构图输入,汇总表留作结果分析时的辅助对照,不会两分都喂给模型。
| 文件 | 职责 | 关键字段/内容 |
|---|---|---|
maoyan_long_comments.csv | 构图主数据,逐条长评 | 评论ID、用户ID、评论文本、评分、时间、电影ID |
猫眼长评总表.csv | 汇总/备份视角 | 按电影或用户的聚合统计信息 |
电影名单.csv | 电影元数据 | 电影ID、电影名映射 |
电影类型.csv | 类型标签 | 电影ID、类型,用于特征扩展与分析 |
data_loader.py就是把这些 CSV 统一读进来,最终输出三个对象:节点特征矩阵X、归一化邻接矩阵A_norm、标签向量y。如果是从爬虫拿数据,常见做法是先落明细再单独写去重脚本,避免同一用户对同一电影的重复评论在构图时产生冗余边。
2.2 BERT 向量化:把评论文本变成 GCN 能用的节点特征
GCN 吃的不是文本,是向量。项目把bert_pretrain目录放在本地,里面有bert_config.json和bert-base-chinese-vocab.txt,说明作者用的是离线加载模式,不需要每次启动都去拉模型权重。data_utils.py里做编码的核心逻辑是这样的:
from transformers import BertTokenizer, BertModel import torch def encode_comment(texts, max_len=128): tokenizer = BertTokenizer.from_pretrained("./bert_pretrain") model = BertModel.from_pretrained("./bert_pretrain") model.eval() features = [] for text in texts: inputs = tokenizer.encode_plus( text, max_length=max_len, truncation=True, padding="max_length", return_tensors="pt", ) with torch.no_grad(): output = model(**inputs) cls_vec = output.last_hidden_state[:, 0, :] # [1, 768] features.append(cls_vec.squeeze(0)) return torch.stack(features)这里取的是last_hidden_state的[CLS]位置向量,而不是对所有 token 做平均池化。实际跑下来,[CLS]在句子级语义表征上更稳定,尤其适合“这条评论整体是吹捧还是贬低”这种判断。max_len=128对电影长评够用,如果评论明显偏长,可以提到 256,但显存占用会同步上涨。
注意:编码顺序必须和图节点编号严格一致。最稳的做法是先对评论ID排序,再用同一个排序结果生成节点特征和邻接矩阵,两边都按字典序来,否则后面整张表都会错位。
2.3 构图逻辑:用户-评论二部图怎么建
水军检测场景里最自然的图是用户-评论二部图:一边节点是用户,另一边节点是评论。用户节点和评论节点之间连一条边,表示“该用户写了这条评论”。这一步把“谁在写什么”直接编码进了结构里,GCN 才能在后续传播中学到“哪些用户和哪些评论绑定得异常紧密”。
构造邻接矩阵时,评论节点编号从用户数之后开始,避免两类节点编号冲突:
import numpy as np from scipy.sparse import coo_matrix def build_adj(user_ids, comment_ids, n_users, n_comments): row = user_ids col = n_users + comment_ids # 评论节点排在用户节点后面 data = np.ones(len(user_ids), dtype=np.float32) adj = coo_matrix( (data, (row, col)), shape=(n_users + n_comments, n_users + n_comments), ) return adj拿到原始邻接矩阵后,还要加自环并做对称归一化,否则模型训练时大概率出 NaN:
def normalize_adj(adj): adj = adj + np.eye(adj.shape[0]) # 加自环 deg = np.array(adj.sum(1)).flatten() deg_inv_sqrt = np.power(deg, -0.5) deg_inv_sqrt[np.isinf(deg_inv_sqrt)] = 0.0 return adj.multiply(deg_inv_sqrt[:, None]).multiply(deg_inv_sqrt[None, :])用D^{-1/2} A D^{-1/2}而不是直接乘A,是因为直接乘原始邻接矩阵时,度大的节点在聚合时会压过度小的节点。归一化之后,无论一个用户写了几百条评论还是一个普通用户只写一条,特征尺度都在同一量级。这一步属于图卷积的标配操作,谁跳过谁踩坑,后面避坑章节会专门展开。
3. 模型核心:两层 GCN 的实现细节与参数选型
3.1 graph_model.py 里的前向传播逻辑
model.py负责模型封装入口,graph_model.py是图卷积的核心实现。GCN 的单层传播公式是H^(l+1) = σ(A_norm · H^(l) · W^(l)),落到代码上:
import torch import torch.nn as nn import torch.nn.functional as F class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.linear = nn.Linear(in_dim, out_dim) def forward(self, x, adj_norm): # adj_norm 是已经做过对称归一化的稀疏矩阵 support = self.linear(x) # [N, out_dim] return torch.spmm(adj_norm, support) class GCN(nn.Module): def __init__(self, in_dim, hidden_dim, out_dim, dropout): super().__init__() self.gc1 = GCNLayer(in_dim, hidden_dim) self.gc2 = GCNLayer(hidden_dim, out_dim) self.dropout = nn.Dropout(dropout) def forward(self, x, adj_norm): h = self.gc1(x, adj_norm) h = F.relu(h) h = self.dropout(h) out = self.gc2(h, adj_norm) return out两层 GCN 意味着每个节点最终看到的是 2 阶邻居加权信息:用户 → 评论 → 其他用户。这条路径恰好覆盖水军的行为特征——同一个水军群组的账号会反复给同一批电影写评论,两层传播足够把它们聚到一起。如果只堆一层,模型退化成就看直接连接的节点,等于丢掉了“群体抱团”这个最关键的判别信号。
torch.spmm是稀疏矩阵乘法,adj_norm保持稀疏格式能省不少内存。默认out_dim=2,对应水军/非水军两类输出,最终通过 logits 过 softmax 得到概率。
3.2 config.py 里的超参数怎么调
项目的config.py把所有可调参数集中管理,省去了翻代码找参数值的麻烦,这是大创项目里很值得保留的习惯。典型的参数配置长这样:
class Config: hidden_dim = 64 dropout = 0.5 lr = 0.01 weight_decay = 5e-4 epochs = 200 max_len = 128 bert_path = "./bert_pretrain" data_path = "./maoyan/maoyan_long_comments.csv" seed = 42逐个说怎么调:
hidden_dim=64:几千到上万节点规模的图完全够用,数据量再大往上提到 128 收益才明显。dropout=0.5:必须开。图模型在小数据上极其容易过拟合,我试过关掉 dropout,训练集 acc 三五个 epoch 就冲到 90% 以上,验证集一塌糊涂。lr=0.01:配合 Adam 时这个学习率起手很快;如果 loss 曲线震荡明显,降到0.001更稳。epochs=200:这个规模的图 100 到 200 轮足够收敛,能早停就在 val 上挂一个 early stopping,省时间。seed=42:固定随机种子。水军样本少,不固定种子的话,换一次运行 F1 可能差好几个点,评估报告都没法写。
3.3 损失函数和评估口径:为什么 F1 比 accuracy 更能说明问题
模型是二分类输出,最直接的损失函数是交叉熵,PyTorch 里就是nn.CrossEntropyLoss()。真正容易出问题的不是损失函数,而是评估指标。假设测试集里水军评论只占 5%,模型只要全预测成非水军,accuracy 就能到 95%,可实际上一个水军都没抓住。
| 指标 | 计算方式 | 关注点 |
|---|---|---|
| Precision | TP / (TP + FP) | 预测成水军的评论里,多少真抓对了 |
| Recall | TP / (TP + FN) | 真实水军评论里,模型找回了多少 |
| F1 | 2 * Precision * Recall / (Precision + Recall) | 两者的调和平均,类别不平衡下最值得看 |
计算的时候用sklearn一行搞定:
from sklearn.metrics import precision_recall_fscore_support p, r, f1, _ = precision_recall_fscore_support( y_true, y_pred, average="binary", pos_label=1 ) print(f"Precision={p:.4f}, Recall={r:.4f}, F1={f1:.4f}")写结果 CSV 时,除了存 0/1 标签,强烈建议把预测概率也一起存下来。后面调阈值、做概率分布分析都要用,只存标签的话,一切阈值相关的后处理都得重训,很不值。
4. 训练复现:train.py、TensorBoard 日志与结果 CSV 对照
4.1 环境准备:从 Python 安装到依赖装齐
项目基于 Python 生态,建议直接用虚拟环境隔离,别把依赖装进系统 Python。Python 3.7 或 3.8 都可以,安装好之后建虚拟环境并装依赖:
python -m venv .venv source .venv/bin/activate pip install torch transformers pandas numpy scipy scikit-learn tensorboardtransformers的版本要注意:这个项目用本地bert_pretrain目录加载模型,transformers3.x 到 4.x 早期版本都兼容得比较好,装太新的版本反而可能因为 API 变动导致加载报错。如果你的机器有 CUDA,torch装对应的 cu 版本;只有 CPU 也能跑全套,就是 BERT 编码阶段会慢不少。编码一次跑完把特征存成.npy文件,下次训练直接加载,能省掉反复编码的重复开销。
4.2 启动训练:train.py 跑起来之后该看什么
确认config.py里的data_path、bert_path路径正确后,直接启动:
python train.py训练过程中终端会打印当前 epoch、loss、train acc 这类信息。同时train.py会把事件文件写到log/目录下,文件名类似events.out.tfevents.1690784923.York.19112.0,这就是 TensorBoard 的标准格式。另开一个终端:
tensorboard --logdir=log --port=6006浏览器打开http://localhost:6006,能看到 loss 曲线和验证准确率曲线。判断训练正不正常有两个标准:loss 前几十个 epoch 快速下降然后趋于平缓,验证集 acc 在六到八成的区间波动。不要指望这类任务冲到 95% 以上,水军检测本身不是线性可分问题,结构特征再强也有模糊地带。
4.3 结果 CSV 怎么对照:result1(7.25).csv 和 result(7.26).csv
项目里有两个原始结果文件,result1(7.25).csv和result(7.26).csv,从命名看是作者在不同日期各跑了一版。加载到 pandas 里查看:
import pandas as pd res = pd.read_csv("result(7.26).csv") print(res.head()) print(res["pred"].value_counts())结果 CSV 里一般会带上评论ID、真实标签、预测标签、预测概率这几个字段。先做两件事:看总行数是否和测试集评论数一致,再看标签分布是否合理。经常有人把两个结果文件搞混,分析半天发现用的是旧版本,数据都换了还在拿它下结论。我的习惯是加载之后立刻打印res.shape和唯一标签个数,确认无误再继续。
训练和测试的数据划分也很重要,常见做法是按评论时间排序,前 80% 作为训练节点,后 20% 作为测试节点。这样能模拟“用历史数据识别未来新评论”的真实场景,比随机划分更有说服力,写大创报告时也更好解释。
5. 常见避坑:图卷积水军检测里的五个翻车点
5.1 数据与构图阶段的坑
现象一:训练没几个 epoch,loss 直接变 NaN。
原因:邻接矩阵没有加自环,也没有做对称归一化。图里总有那种“评论狂”用户,度非常大,直接乘原始邻接矩阵时,数值在矩阵乘法里越滚越大,最终溢出。原因明确后,解决就是两句话:adj + np.eye(n)补自环,再用D^{-1/2} A D^{-1/2}归一化。训练开始前打印一次adj_norm的度分布,确认值域收敛在[-1, 1]附近再启动训练。
现象二:特征矩阵和图节点对不上,模型训练全程都是噪声。
原因:很典型的顺序错位。data_utils.py按 dataframe 原始顺序编码 BERT 特征,但构图时却按用户ID、评论ID 重新排了序,两边编号规则不一致,特征就串了。解决方法是把“节点编号 → 原始行号”的映射存成一个字典,特征、标签、邻接矩阵全部通过同一个映射取数。我一般会先对评论IDsort_values,让编码和构图都基于同一份排序后的列表,双保险。
5.2 训练与评估阶段的坑
现象三:训练集 acc 极高,测试集 acc 直接掉到五成左右。
原因:这是半监督图模型最容易踩的泄漏问题。训练节点和测试节点在同一个图里,消息传递会把测试节点的信息沿边传给训练节点,训练过程相当于“作弊”。解决思路是构图阶段就把训练和测试区域隔开:按时间切块,测试节点集中在新时间段内;或者按电影切分,用一部分电影做验证,让训练节点和测试节点之间没有直接的图路径。
现象四:F1 分数忽高忽低,同一个模型跑三次结果对不上。
原因:测试集里水军样本本身只有百来十条,随机种子一变,正例在训练集和测试集里的分配就变了,指标跟着大幅波动。解决方法是先固定seed=42,再对测试集做分层采样,保证测试集正样本比例和全量一致。报告指标时不要只报一次结果,跑至少三遍取 F1 均值和标准差,3.2 节里设置seed参数的意义也在这里。
现象五:拿旧结果文件当新实验的结论。
原因:result1(7.25).csv和result(7.26).csv是两次不同实验的产物,文件名日期版本不够清晰,分析时很容易载入旧数据,还以为是刚训练出来的输出。解决方法是输出文件强制带上参数语义,比如result_gcn64_ep200_0726.csv这种命名。加载前用pd.read_csv先看一眼行数和标签分布,确认和你当次训练的输出一致再往下做,这算是我交过学费之后养成的固定检查项。
6. 结果验证:从预测概率到水军用户画像
拿到预测结果后,先别急着写“模型效果良好”的结论,把预测概率分布拉出来看一眼,这一步比任何指标都直观:
import pandas as pd import matplotlib.pyplot as plt res = pd.read_csv("result(7.26).csv") res["prob"].hist(bins=50) plt.show()如果概率大量集中在 0.05 以下和 0.95 以上,模型属于过度自信,阈值可以往 0.5 上方微调;如果概率全挤在 0.4 到 0.6 之间,说明特征区分度不够,问题大概率出在构图阶段,而不是模型本身。
更硬的验证方式是反查用户行为。把预测概率大于 0.8 的评论取出来,聚合到用户维度,看这些用户是不是集中评价同一批电影、评论时间是否扎堆、文本结构是否相似。这一步不需要训练,但对交付结果最有说服力——直接回答“你抓的水军到底凭什么判定是水军”这个灵魂拷问。
至于新电影冷启动,项目有一个天然局限:新电影没有历史评论,建不出子图,GCN 没有边可用。我一般先用 BERT 编码拿到语义特征,再把它作为独立子图挂到现有图上做零样本预测;如果冷启动场景是常态,更根本的解法是构图阶段把电影节点也放进去,让新电影至少先和所属类型、同档期电影产生关联。从那以后,我每次拿到图模型项目,第一件事不再是急着调参,而是先检查邻接矩阵的度分布和节点对齐,这两点确认干净之前绝不启动训练——这个习惯帮我省掉了至少一半的 NaN 和错位问题。希望帮到你。
本文还有配套的精品资源,点击获取