简介:面向自然语言处理毕设与期末大作业的多任务谣言检测Python源码与配套数据集。项目基于2022届毕业论文优化,采用BERT编码器获取词向量与句向量,再接入BiGCN或Self-Attention完成谣言检测与立场分类,并重新整理了PHEME、Semeval2017-task8等数据集,存储结构更规整。压缩包共75个文件,约18.67MB,含25个Python脚本、17个txt说明、15个JSON配置、12个Jupyter Notebook、4个Markdown文档及LICENSE等,覆盖BiGCN、MSA-BiGCN、ABGCN等模型实现与训练、测试、可视化脚本,便于对照论文复现;另有requirements依赖文件可快速搭建环境,以及数据预处理与结果分析Notebook,方便分步调试验证。已有367人学习下载。通过这份资料可获取完整训练流程、数据预处理代码、实验结果分析,以及多任务联合训练和单任务立场/谣言分类的灵活接口,适合快速上手图卷积与注意力机制谣言检测相关研究。
1. 基于注意力机制和图卷积神经网络的多任务谣言检测源码包,到底解决什么问题
基于注意力机制和图卷积神经网络的多任务谣言检测,是我在虚假信息治理方向复现过最值得做的一类方案。多数谣言从单条文本看根本不"假",它被判定为谣言,靠的是传播结构:谁在转、转发链多深、有没有节点密集扩散。这些图结构信号,普通文本分类模型看不到,只有图卷积能建模。
这个源码包把注意力机制叠在图上,给节点和边加权,并把"是不是谣言"和"扩散多热"合并成多任务,一份数据训练两个目标。压缩包里是完整可运行的 Python 源码:预处理、图构建、注意力图卷积、多任务训练与推理脚本,附带可直接训练的数据集,适合做虚假信息治理、社交舆情分析的工程师,也适合从纯文本转向图神经网络的开发者。
下面按落地路径展开:先讲为什么是"注意力+图卷积"这个组合,再讲怎么跑通、核心代码怎么读、训练有哪些坑,最后给出推理与指标解读。
2. 多任务谣言检测为什么选"注意力+GCN":模型选型与架构原理
2.1 传播树的图结构:为什么普通文本分类模型不够用
早期谣言检测的主流做法是文本分类:把博文丢给 CNN、LSTM 或预训练语言模型,输出谣言与非谣言。这类模型的效果上限很快就能看到——它只看得到"这条消息说了什么",看不到"这条消息是怎么被转起来的"。而社交平台上的谣言识别,结构信息往往比文本信息更值钱。一条被造出来的消息,通常会在短时间内出现深度异常的转发链:第一层转发集中在一个小圈子,接着被几个高影响力节点扩散出去,形成深而窄的传播树;正常消息的传播树则宽而浅,节点分布更加分散。
传播过程本身可以建模成一张有向图:原始博文是根节点,每次转发产生一个新节点,转发关系产生一条边,用户间的评论、引用还可以继续加边。这种数据既不是网格也不是序列,CNN 的卷积核和 RNN 的时间步都天然不匹配,硬要用只会把图拍平成向量,丢掉大量拓扑信息。图卷积神经网络(GCN)进入谣言检测领域,原因就在这:GCN 的操作对象就是任意拓扑的图,每一层做的事是把邻居节点的特征聚合到当前节点,更新公式可以写成 H(l+1)=σ(D^(-1/2) A D^(-1/2) H(l) W(l)),其中 A 是邻接矩阵,D 是度矩阵。堆两层网络,每个节点就能看到两跳之内的邻居,传播深度和广度被显式编码进特征里。
2.2 注意力机制在"节点"和"文本"两个层级各干什么
纯 GCN 的聚合权重由度矩阵固定,等于对邻居做等权平均。放在谣言场景里这是个明显的缺陷:传播树里不同邻居的价值差得很远。一个被高可信度机构转发过的节点,和一个刚注册、连续转发十次的小号,对判断谣言的作用完全不一样。注意力机制解决的就是"权重从哪来"的问题——让模型根据节点自身特征和邻居特征学习一个注意力分数,再用 softmax 在邻域内归一化,最后按分数加权聚合。这其实就是图注意力网络(GAT)的核心思路,也是标题里"注意力机制+图卷积神经网络"最常见的落地组合:图卷积仍然负责消息传递,注意力负责给消息分配权重。
注意力在图里做一层还不够,文本侧也要加。原始博文不是每个词都重要,"紧急""速转""独家"这类触发词和句式,往往比冗长的背景描述更有判别力。所以在文本编码阶段,一般还会加一层 token 级的自注意力,让模型把有限的特征预算花在关键表达上。两层注意力共享同一套梯度,多任务训练时,谣言判别任务会把结构注意力往"找异常传播模式"上拉,趋势预测任务会把注意力往"找高扩散潜力节点"上拉,两个信号互相补充,学出来的注意力分数会比单任务训练更稳。
2.3 多任务共享编码器与任务头设计
这个方案里的任务设计是典型的硬参数共享:文本编码层和注意力 GCN 层作为共享主干,往上分出两个任务头。一个头是谣言判别,输出二分类概率,损失用交叉熵;另一个头是传播趋势预测,输出转发量或热度值,损失用均方误差。联合损失写成 L = L_cls + λ L_trend,λ 控制两个任务的相对权重。
| 任务 | 输出头 | 损失 | 监督密度 |
|---|---|---|---|
| 谣言判别 | MLP + softmax | 交叉熵 | 稀疏,只有带标签样本 |
| 传播趋势 | MLP + 线性输出 | 均方误差 | 密集,每条样本都有转发量 |
为什么要做多任务而不是单独训练两个模型?两个理由。第一,谣言标签天然稀疏且标注贵,一个数据集里谣言样本可能只占两三成,光靠分类任务学不充分;趋势预测用的是连续监督信号,每条样本都有转发量,相当于给共享主干提供了更密集的梯度。第二,这两个任务在特征层面高度相关:异常传播模式既指向谣言,也指向高热扩散,共享表示让模型在两个目标之间找到一个更平滑的特征空间,泛化更好。λ 的取值在早期代码里通常是写死的,常见做法是先用 1.0 跑通再调;如果两个任务梯度量纲差太多,就要上不确定性加权,这个我在第 4 章给出可运行实现。
3. 把源码跑起来:目录结构、数据集格式与最小训练命令
3.1 解压后的目录划分与依赖安装
拿到压缩包先别急着训练,花两分钟把目录结构看清。常见划分是 data/ 放原始数据和处理脚本,src/ 下按 models、data_loader、utils 分包,根目录放 train.py、predict.py 和 config.py。具体命名可能不一样,但按照"数据处理、模型定义、训练入口、推理入口"四个职责去找,不会迷路。
依赖方面,核心是三样:深度学习框架、图计算能力、文本工具链。图卷积部分的示例代码基于 PyTorch 实现,图计算可以用 torch 的稀疏张量手写,也可以用现成的图神经网络库省去自己实现消息传递的麻烦。安装步骤通常是这样:
cd rumor_gcn_mtl pip install -r requirements.txt python -c "import torch; print(torch.__version__)"如果 requirements.txt 因为环境冲突装不上,手动装最小集也够:torch、transformers、pandas、scikit-learn。图神经网络库按需装,注意力 GCN 层用纯 torch 也能写出来,代码在第 4 章。这里提醒一句:不要一上来就追求最新版,跑完上面的版本检查再继续,框架版本不一致导致的报错后面最难排查。
3.2 数据集字段与预处理流程
这份数据集和解压即用的玩具样本不同,每条数据是一条完整传播链,常见字段如下:
| 字段 | 含义 | 示例 |
|---|---|---|
| id | 样本唯一编号 | post_0001 |
| text | 原始博文文本 | "网传某地出现感染病例" |
| repost_edges | 转发关系边列表 | [["root","u1"],["u1","u7"]] |
| node_feats | 每个节点的特征向量 | 文本向量拼接用户属性 |
| label | 是否谣言 | 1 |
| retweet_num | 总转发量,趋势监督信号 | 240 |
拿到原始数据后,预处理不只是清洗文本,核心是把转发关系转成可训练的图。流程分四步:第一步,把每条样本的用户和转发关系映射成连续节点编号;第二步,构造邻接矩阵并加自环,做 D^(-1/2) A D^(-1/2) 对称归一化;第三步,把超过预设最大节点数的传播树截断或丢弃,控制训练显存;第四步,按时间切分训练集、验证集和测试集——注意按时间而不是随机切分,这一点第 5 章还会专门讲。预处理入口一般是一个脚本:
python preprocess.py --input data/raw --output data/processed --max_nodes 64--max_nodes 这个参数值得多说一句:设太小,深传播树被截断,GCN 学不到"深度"这个关键信号;设太大,批处理时 padding 浪费显存。我一般先统计数据集里传播树的节点数分布,取 80% 分位左右的值当默认值,再根据显存上下调。
3.3 最小训练命令与关键参数
预处理完就可以训了。找一台有 GPU 的机器,最小命令长这样:
python train.py \ --data_dir data/processed \ --max_nodes 64 \ --hidden_dim 128 \ --num_layers 2 \ --num_heads 4 \ --lr 3e-4 \ --batch_size 32 \ --epochs 60 \ --task_weight uncertainty \ --device cuda:0关键参数的含义和调整优先级:
| 参数 | 作用 | 调参建议 |
|---|---|---|
| hidden_dim | 图卷积隐藏层维度 | 显存够就 256,小数据集 128 足够 |
| num_layers | GCN 层数 | 谣言传播树平均深度短,大于 3 层容易过平滑 |
| num_heads | 多头注意力数量 | 与 hidden_dim 联动,头多了要降维度 |
| lr | 学习率 | 用 AdamW,3e-4 起步,loss 震荡就降一半 |
| max_nodes | 传播树最大节点数 | 按 80% 分位定,别拍脑袋 |
| task_weight | 多任务权重策略 | 两个 loss 量纲差太多时用 uncertainty |
训练起来后重点看两个东西:训练 loss 有没有稳定下降,验证集 F1 在哪个 epoch 开始不再涨。如果验证集指标一路上涨但测试集崩了,基本可以断定是第 5 章要讲的数据泄漏问题,不一定是模型写错了。
4. 核心模块代码解读:图构建、注意力层与多任务损失实现
4.1 从原始数据构造传播图与邻接矩阵
先看数据侧最关键的模块:怎么把一条样本的转发关系变成 GCN 能吃的邻接矩阵。下面这个函数是常见实现,核心逻辑是节点编号映射、邻接矩阵构建、对称归一化三步:
import json import numpy as np import torch def load_propagation_graphs(data_file): """ 把原始转发记录转成邻接矩阵形式的传播图。 每行一个 JSON 对象,结构大致为: {"id": "post_0001", "text": "...", "repost_edges": [["root","u1"],["u1","u2"]], "node_feats": {"root":[...],"u1":[...]}, "label": 1, "retweet_num": 240} """ graphs = [] with open(data_file, 'r', encoding='utf-8') as f: for line in f: item = json.loads(line.strip()) edges = item["repost_edges"] feats = item["node_feats"] # 第1步:节点去重并映射成连续索引 node_ids = list({v for e in edges for v in e}) idx = {nid: i for i, nid in enumerate(node_ids)} # 第2步:构造邻接矩阵,加自环 n = len(node_ids) adj = np.zeros((n, n), dtype=np.float32) for src, dst in edges: adj[idx[src], idx[dst]] = 1.0 adj[idx[dst], idx[src]] = 1.0 # 无向化是常见做法 adj = adj + np.eye(n) # 自环:聚合时保留自身特征 # 第3步:对称归一化 D^{-1/2} A D^{-1/2} deg = adj.sum(axis=1) deg_inv_sqrt = np.power(deg, -0.5) deg_inv_sqrt[np.isinf(deg_inv_sqrt)] = 0.0 norm_adj = adj * deg_inv_sqrt[:, None] * deg_inv_sqrt[None, :] # 把节点特征dict转成定长特征矩阵 feat_dim = len(next(iter(feats.values()))) x = np.zeros((n, feat_dim), dtype=np.float32) for nid, fv in feats.items(): if nid in idx: x[idx[nid]] = fv graphs.append({ "x": torch.tensor(x), "adj": torch.tensor(norm_adj), "label": torch.tensor(item["label"], dtype=torch.long), "trend": torch.tensor(item["retweet_num"], dtype=torch.float32), }) return graphs几个值得注意的点。节点索引映射必须用 dict,原始数据里的用户 id 是字符串,直接当下标会翻车。自环不能省,没有自环的话每个节点只能看到邻居,自己的原始特征在第一层就被洗掉了。归一化用的是对称形式,等价于把每个节点的邻居特征按 1/sqrt(deg(i)*deg(j)) 缩放,避免大度节点把聚合值撑大。想把传播方向也建模进去,就把邻接矩阵改成非对称的,换成 D^(-1) A 的行归一化,特征传播语义会从"互相交流"变成"从父节点传向子节点",两种做法在谣言数据集上都有人用。
提示:如果预处理和训练脚本分开存放,务必把节点特征标准化的均值、方差放进同一份配置里持久化,推理时直接加载,不要重算。
4.2 注意力图卷积层的 PyTorch 实现
这段是标题的核心。注意力机制加进 GCN 的最小实现就是 GAT 的思路:先线性投影,再为每条边算注意力分数,softmax 归一化后加权聚合:
import torch import torch.nn as nn import torch.nn.functional as F class AttentionGCNLayer(nn.Module): """ 注意力图卷积层。GCN负责消息传递,注意力负责给邻居加权。 """ def __init__(self, in_dim, out_dim, dropout=0.2, negative_slope=0.2): super().__init__() self.w = nn.Linear(in_dim, out_dim, bias=False) self.attn_src = nn.Linear(out_dim, 1, bias=False) self.attn_dst = nn.Linear(out_dim, 1, bias=False) self.leaky_relu = nn.LeakyReLU(negative_slope) self.dropout = nn.Dropout(dropout) def forward(self, x, adj): # x: [num_nodes, in_dim] # adj: [num_nodes, num_nodes] h = self.w(x) # 线性投影到 out_dim # 用两个向量做加法式注意力,是GAT的原始做法 e = self.attn_src(h) + self.attn_dst(h).transpose(0, 1) e = self.leaky_relu(e) # [n, n] 注意力分数 mask = (adj > 0).float() e = e * mask + (1 - mask) * -1e9 # 无连接位置置为极小值 alpha = F.softmax(e, dim=-1) # 行归一化 alpha = self.dropout(alpha) # 注意力dropout out = torch.matmul(alpha, h) # 加权聚合 return out两个细节必须说。mask 操作是必须的,因为 e 矩阵是全连接的,如果不把无边的位置压成负无穷,softmax 会给不存在的邻居也分配注意力,等于在图上引入虚假连边。另一个细节是注意力分数用了 src 和 dst 两个独立可学习投影,而不是拼接一个大的全连接层,参数更少,训练更稳,这是 GAT 原始论文的写法。多头注意力只需要在外面循环叠加这个层,把多个头的输出 concat 或平均即可。
4.3 多任务损失与可学习的任务权重
多任务最常用的落地方式就是共享编码器加两个任务头。图级读出用注意力池化,把节点特征合并成整棵传播树的表示:
class MultiTaskRumorGCN(nn.Module): """注意力GCN + 谣言判别头 + 传播趋势回归头""" def __init__(self, text_dim, hidden_dim, num_layers, dropout=0.2): super().__init__() self.text_proj = nn.Linear(text_dim, hidden_dim) self.gcn_layers = nn.ModuleList([ AttentionGCNLayer(hidden_dim, hidden_dim, dropout) for _ in range(num_layers) ]) self.graph_attn = nn.Linear(hidden_dim, 1) self.rumor_head = nn.Linear(hidden_dim, 2) self.trend_head = nn.Linear(hidden_dim, 1) def forward(self, x, adj): h = torch.relu(self.text_proj(x)) for layer in self.gcn_layers: h = layer(h, adj) # 图级注意力池化 scores = torch.softmax(self.graph_attn(h).squeeze(-1), dim=0) g = torch.sum(scores.unsqueeze(-1) * h, dim=0) return self.rumor_head(g), self.trend_head(g) class UncertaintyMultiTaskLoss(nn.Module): """基于同方差不确定性的多任务损失,自动平衡两个任务""" def __init__(self): super().__init__() self.log_var = nn.Parameter(torch.zeros(2)) def forward(self, logits, labels, trend_pred, trend_label): cls_loss = F.cross_entropy(logits, labels) reg_loss = F.mse_loss(trend_pred.squeeze(-1), trend_label) loss = cls_loss / (2 * torch.exp(self.log_var[0])) + 0.5 * self.log_var[0] \ + reg_loss / (2 * torch.exp(self.log_var[1])) + 0.5 * self.log_var[1] return loss多任务训练时一个容易被忽略的细节是梯度尺度。交叉熵和均方误差的量纲差好几个数量级,直接相加的话回归任务的梯度会淹没分类任务。代码里的不确定性加权是常见解法:log_var 是可学习参数,代表模型对每个任务的置信度,损失除以 exp(log_var),相当于让模型自己决定每个任务该占多大权重。训练循环里再加上梯度裁剪:
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=5e-4) for epoch in range(epochs): for batch in loader: x, adj, label, trend = [t.to(device) for t in batch] logits, trend_pred = model(x, adj) loss = criterion(logits, label, trend_pred, trend) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 2.0) optimizer.step()为阅读直观,上面按单图逐步更新示意;实际跑 batch 时,x 和 adj 需要 padding 成 [batch, max_nodes, feat_dim] 和 [batch, max_nodes, max_nodes],把层内的 matmul 换成 torch.bmm 即可,padding 位置会被注意力 mask 挡住,不会污染有效节点。clip_grad_norm_ 把总梯度范数限制在 2.0 以内,防止个别样本的异常梯度把注意力层参数带飞。
5. 注意力GCN谣言检测训练避坑:四个典型案例与排查记录
5.1 Loss不降或震荡:先查学习率与图归一化
现象:训练 loss 在前几个 epoch 不动,或者降到某个值后开始周期性震荡,验证集 F1 始终在 0.5 附近打转。
原因:注意力层的 softmax 对输入尺度非常敏感,学习率太大时注意力分数会往极端值漂移,梯度容易爆炸;图归一化没做对也会让聚合值量级失控。另一个隐蔽原因是节点特征没标准化,文本向量和用户数值特征拼接后,某一维量级特别大,GCN 聚合时这一维会主导注意力分数。
解决:先把学习率降到 1e-4 到 3e-4,确认 loss 能稳定下降后再逐步加回去。同时对所有节点特征做逐维标准化,让每一维的量级接近。检查图归一化是否生效,可以在第一个 GCN 层后打印 h 的范数,如果范数随训练指数增长,基本就是归一化矩阵算错了。
5.2 显存溢出:全图计算撑不住,改邻居采样
现象:batch_size 从 32 降到 8 还是 OOM,报错位置在注意力分数矩阵那一行,[n, n] 的注意力矩阵在节点数几千的大图上直接爆显存。
原因:注意力分数矩阵是 O(n²) 的,传播树大、batch 内又 padding,块对角矩阵里的 padding 区域虽然没有有效边,但显存照样占。n=5000 的图,单张注意力矩阵就有 2500 万个元素,加上多头和梯度,爆显存是必然。
解决:三种手段按顺序用。第一,把 max_nodes 降到 64 或 128,截断过深的传播树;第二,训练时邻接矩阵用稀疏张量存储;第三,实在不行改邻居采样,每个节点只采样固定数量邻居,比如 20 个,注意力只在采样邻域内计算,空间复杂度从 O(n²) 降到 O(n×k)。注意采样会丢失部分拓扑信息,谣言检测里深链信息很重要,k 别设太小,我一般从 20 起步往下调。
5.3 指标虚高:随机切分导致"时间穿越"
现象:验证集 F1 达到 0.87,看起来很强,换成线上新数据直接掉到 0.6,测试集指标也明显高于同任务其他方案的公开水平。
原因:这是最典型的踩坑,没有之一。数据按用户、按 id 随机切分,同一条谣言事件的不同转发样本同时出现在训练集和测试集里,模型在测试时等于见过相似传播结构的答案。谣言传播结构在同一事件内部高度相似,这个泄漏比文本泄漏更难发现。
解决:切分必须在时间维度上进行,按原始博文发布时间排序,前 70% 训练、后 30% 测试。如果是事件级数据,还要先按事件聚合再切分,杜绝同一事件跨集合。预处理脚本里把随机种子固定也写上,否则每次复现结果都不同,调参等于对着会变的基准在工作。
注意:切分前先按事件聚合,再按时间排序,这是多任务谣言检测里最容易在数据侧翻车的一步。
5.4 多任务互相拖累:两个Loss量纲不一致
现象:加入趋势回归任务后,谣言判别的 F1 反而比单任务低,训练时分类 loss 在降,但回归 loss 纹丝不动。
原因:回归 loss(MSE)的数值通常是分类 loss(交叉熵)的几十倍,直接相加时梯度完全被回归任务主导,共享主干被拽着去优化"预测转发量",谣言判别能力退化。如果 λ 写死为 1.0,这个问题必然出现。
解决:先用固定 λ 从 0.1 到 10.0 扫一遍,找 F1 最高的点;更省事的办法是直接用第 4 章的不确定性加权,让模型学权重。还有一个小技巧:回归目标先做 log1p 变换,把转发量的长尾压平,MSE 量级会小很多,两个任务更容易平衡。这是我在复现时调了很久才想明白的,属于血泪经验。
6. 从checkpoint到预测:推理脚本与类别不均衡下的指标解读
6.1 加载训练好的模型输出预测结果
训练完的模型不能只活在验证集里,要能接受新样本、输出可解释的结果。推理脚本的核心逻辑和训练时完全相同,只是关掉 dropout 和梯度:
def predict_single(model, graph, device): """对一条新样本的传播图做预测""" model.eval() with torch.no_grad(): x = graph["x"].to(device) # [num_nodes, feat_dim] adj = graph["adj"].to(device) # [num_nodes, num_nodes] logits, trend = model(x, adj) prob = torch.softmax(logits, dim=-1) pred = torch.argmax(prob, dim=-1).item() return pred, prob.cpu().numpy(), trend.item()推理时有个必须守住的纪律:新数据走的预处理管道要和训练时完全一致,包括节点特征标准化的均值和方差、max_nodes 截断规则。最稳的做法是训练脚本里把预处理参数和模型权重一起打包保存,推理时直接加载,而不是在 predict 脚本里重写一遍逻辑——两套逻辑迟早会漂移。
提示:新数据的标准化参数必须和训练时一致,最稳的做法是随 checkpoint 一起持久化保存。
6.2 混淆矩阵与类别不均衡下的指标解读
谣言数据集的标签天然不均衡,非谣言样本通常占大头。这时候光看 accuracy 会自我欺骗,准确率 90% 可能只是模型学会了无脑预测"非谣言"。正确姿势是看混淆矩阵加 F1:谣言类的 precision 代表"模型说是谣言的里面有多少是真的",recall 代表"真的谣言里模型抓住了多少",F1 是两者的调和平均。类别不平衡严重时,我更关注谣言类的 F1 和召回率,因为漏报一条谣言的代价远高于误报一条正常消息。
多任务的趋势输出也可以利用起来:趋势预测值可以当置信度修正。模型报"非谣言"但预测转发量异常高,这种样本值得人工复核,实操中这个规则能多拦截一部分模型没把握的漏判。
最后说一个我自己的习惯:每次训练完,我会故意拿几条人工标注过、但模型从没见过的旧谣言去跑一遍推理,看概率分数是否稳定地贴近标签。这比看任何训练曲线都直观,能最快暴露预处理不一致或类别偏差的问题。希望帮到你。
本文还有配套的精品资源,点击获取