简介:这是一份面向深度学习与自然语言处理学习者的多任务谣言检测项目,源于2022届本科毕业论文的后续优化,代码已从原版升级为BERT+图卷积结合自注意力的方案,同时处理谣言识别和立场分类两个关联任务,适合毕业设计、期末大作业或入门图神经网络应用时参考。项目以BERT作为统一编码器,分别提取词向量和句向量,再接入BiGCN或Self-Attention层完成双任务输出;数据集经过重新整理,从杂乱的JSON改为分目录存储,附带PHEME等公开谣言语料及预处理逻辑,可直接训练与评估。压缩包共75个文件,包括25个Python脚本(网络构建、训练入口、工具函数)、12个Jupyter Notebook(实验过程与可视化)、15个JSON配置/标签文件、17个文本说明和4个Markdown文档,总计18.67MB,目录按模型与任务划分,层次清晰。已有367人学习下载,适合需要快速复现论文实验、对比BiGCN与自注意力机制效果,或基于BERT做谣言检测改进的读者。
1. 谣言检测不是文本分类:为什么图卷积加注意力能同时抓内容和传播
把谣言检测当成文本分类来做,是大多数人第一次翻车的起点。同样是“某地发生某件事”的一句话,转发语境、传播速度和评论区的质询完全不同,单看正文几乎分不出真假。基于注意力机制和图卷积神经网络的多任务谣言检测,做的事情是把一条帖子的“案发现场”——谁转发了、谁质疑了、传播树长什么样——和内容本身一起送进模型:图卷积负责聚合传播结构,注意力负责在不同的信息粒度之间挑重点,多任务头同时输出谣言判别、立场分析和回复意图。这篇笔记会从数据建模写到关键代码,再给出能照着调的避坑清单,适合已经跑过基础文本分类、想进一步做内容安全与舆情分析的工程师。
2. 把谣言事件变成图:传播结构、节点特征与邻接矩阵的构建
2.1 一条谣言的“案发现场”是一棵传播树
一份以事件为单位的谣言数据集,最核心的单元不是单条帖子,而是“源帖 + 以它为根的所有转发和回复”。根节点是原始信息,子节点是转评,再往下是转发的转发,这样形成一棵多叉树。树上的每个节点都带有文本、发布者、时间戳等属性,树本身则记录了信息从出现到扩散的完整路径。
谣言与普通信息的传播结构存在统计差异:谣言更容易被转发,但更容易在回复中被质疑。这些差异藏在树的形状里,例如谣言树的“质疑回复”分支长在早期还是晚期、树是不是在一小时内快速长高。这意味着传播树本身就是一个强信号,只看正文会丢掉它。大多数数据集中传播树的深度集中在 2 到 5 层,少数爆款事件能到 8 层以上,这种结构差异本身就是可以学习的特征。
所以建模的第一步是:把一个事件组织成有向图,节点是帖子或用户,边是转发/回复关系。常见做法是用 networkx 存树,再用 PyG 的 Data 对象把它转成图神经网络能吃的格式。我一般不会把整个社交网络全图画进来,因为全图又大又稀疏,转发关系被大量噪音淹没,谣言检测用传播子树更可控。
2.2 从原始数据到图样本的最小处理流程
把一条条的帖子记录组织成传播树,核心只做三件事:找到根节点、建立父子边、剪掉孤立节点。下面这段代码是常见的最小实现,可以直接套在 CSV 或 JSONL 形态的数据上。
import networkx as nx def build_rumor_tree(posts): """ posts: list[dict],每个 dict 至少包含 id, parent_id, user_id, text, timestamp, label parent_id 为 None 或 '' 的帖子是源帖(根节点) """ tree = nx.DiGraph() posts_by_id = {p["id"]: p for p in posts} # 先建所有节点,避免边先于节点出现 for p in posts: tree.add_node( p["id"], user=p["user_id"], text=p["text"], ts=p["timestamp"], label=p.get("label", 0), ) # 再连父子边,delay 留作边上的时序特征 for p in posts: parent = p.get("parent_id") if parent and parent in posts_by_id: delay = p["timestamp"] - posts_by_id[parent]["timestamp"] tree.add_edge(parent, p["id"], delay=max(float(delay), 0.0)) # 只保留根节点能到达的节点,丢掉脏数据带来的孤立分支 roots = [n for n, d in tree.in_degree() if d == 0] if not roots: raise ValueError("no root node found") reachable = nx.descendants(tree, roots[0]) | {roots[0]} return tree.subgraph(reachable).copy()这里有个顺序上的讲究:必须先全部 add_node 再 add_edge,否则会出现“父节点还没加入就先连边”的报错或静默丢边。delay 这一项是后面边特征的基础,转得越快,谣言扩散早期的特征越明显,很多模型会把 delay 的倒数或负指数当作边权重。前提是数据里的 timestamp 必须是统一单位且已经对齐过的数值型时间戳,字符串时间要先转换。
建成 networkx 的有向图之后,再转成 PyG 的 Data 对象才算真正进入训练管线:
import torch from torch_geometric.utils import from_networkx, to_undirected def tree_to_pyg(tree, feature_rows): # 注意:feature_rows 的行顺序必须与 list(tree.nodes()) 一致 data = from_networkx(tree) data.x = torch.tensor(feature_rows, dtype=torch.float) # GCN 通常按无向图聚合;谣言传播虽是有向的, # 但无向化之后能同时利用“谁转了我”和“我转了谁”两跳信息 data.edge_index = to_undirected(data.edge_index) # 事件级标签取根节点的 label,批处理时一个图对应一个标签 roots = [n for n, d in tree.in_degree() if d == 0] data.y = torch.tensor([tree.nodes[roots[0]].get("label", 0)], dtype=torch.long) return datafrom_networkx 生成 edge_index 时按节点遍历顺序,因此 feature_rows 必须与 tree.nodes() 的次序对齐,这是最常见的低级错误之一。无向化不是可选项,GCN 的拉普拉斯聚合依赖无向对称归一化,直接用原始有向边矩阵也可以跑,但效果通常差一截,原因是对称归一化在有向图上的度数计算会畸变。
2.3 节点特征和边权重:哪些特征真有用
节点特征直接决定模型上限,这个阶段偷懒,后面换再大的网络也补不回来。我会把特征分成三组:文本特征、用户特征、时序特征。
| 特征组 | 常见成员 | 处理方式 | 说明 |
|---|---|---|---|
| 文本特征 | 预训练句向量、关键词命中、情感极性 | 句向量做池化后归一化 | 内容本身的强先验 |
| 用户特征 | 粉丝数、关注数、注册时长、历史被质疑比例 | 取对数后归一化到 0~1 | 粉丝数偏度极大,必须压缩 |
| 时序特征 | 转发延迟、转发频率、活跃时段 | delay 取倒数或 log 1+delay | 早期快速扩散是谣言关键信号 |
文本向量最少用的是 768 维句向量,配合用户特征拼接,喂给第一层 GCN。如果机器配置有限,用 TF-IDF 再加 64 维词向量平均也能跑,但泛化会差。用户特征里最有用的往往不是粉丝数,而是“历史发布内容的被质疑比例”,因为它直接刻画了账号的可信度,不过这个特征很多数据集里没有,常见做法是省略并接受一点性能损失。
边权重不是必须的,GCN 默认按二值邻接矩阵算归一化就够用。如果一定要加时序权重,我会用w = 1.0 / (1.0 + np.log(1.0 + delay)),这样几小时到几天的延迟差异被压缩,不会被极端值带偏。
还有一种常见误用是把所有事件拼成一张大图来训练。这样做等于让不同事件的节点互相传播信息,事件边界被抹掉。正确做法是每个事件单独成图,mini-batch 内多图并行。事件之间的共性由共享的 GCN 权重去学习,而不是靠跨事件连边。
3. 模型主体拆解:GCN、多头注意力与多任务头的协作方式
3.1 图卷积不是堆层数:局部聚合与过平滑
GCN 做谣言检测的直觉是这样的:一条帖子该不该信,不只取决于它自己说了什么,还取决于转发它的是些什么人、回复里有多大比例在质疑。GCN 用一层卷积完成“把邻居的表征加权聚合到自己身上”,也就是把“被谁转发”这件事编码到节点向量里。
用一行公式来表达:H' = σ(D^-1/2 A D^-1/2 H W)。A 是加了自环的邻接矩阵,D 是它的度矩阵,H 是当前层节点表征,W 是可学习的权重。所谓图卷积在这里其实是对每个邻居做“对称归一化的加权平均”,层数越多,一个节点能看到的邻居范围越远。
但这里有个很常见的误区:GCN 不是越深越好。传播树虽然有深度,但谣言相关的有效信息往往在 1 到 3 跳之内就足够,堆到 4 层以上会出现过平滑——所有节点表征趋于一致,事件级池化后变成一团均值向量,F1 不升反降。做谣言检测我一般固定 2 到 3 层,而不是像 CV 那样去增大网络深度。
3.2 注意力机制在谣言检测里到底在“注意”什么
注意力在这个任务里有三个常见落点,容易混淆:节点级、路径级、任务级。节点级注意力给一棵传播树里的每个转发算一个权重,因为不是每条转发都同等重要——跟风转发和实名质疑的语义权重完全不同。路径级注意力放在从根到目标节点的一条传播链上,可以理解成“沿着这条链,哪些环节加深或动摇了可信度”。任务级注意力则是多任务之间的信息共享方式,让谣言判别、立场判断各自取用不同的特征子空间。
在复现这套方案时最容易看错的地方是:“基于注意力机制”到底加在哪一层。通常的做法是在 GCN 编码完节点表征之后,加一个节点级加性注意力,再做事件级池化。也有人先做注意力再加 GCN,相当于先给根节点的邻居做加权采样。两种方式都有人用,前者对谣言判别更稳定,后者对早停更敏感,我建议先用前者跑通,再尝试换位。
3.3 核心代码走读:共享图编码器与多任务头
下面是一个能直接跑通的最小模型,用两层 GCN 做共享编码器,一个线性注意力做节点加权,最后分三个任务头。复刻时只需要把数据特征维度换成你自己的 in_dim。
import torch import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import GCNConv, global_add_pool class RumorGCN(nn.Module): """ 共享图编码器 + 节点级注意力 + 三个任务头 in_dim: 文本向量维度 + 用户/时序特征维度 hidden_dim: 图卷积隐层维度,128~256 足够 """ def __init__(self, in_dim, hidden_dim): super().__init__() # 两层图卷积。传播树 3 跳内有效信息最多,层数不宜超过 3 self.conv1 = GCNConv(in_dim, hidden_dim) self.conv2 = GCNConv(hidden_dim, hidden_dim) self.dropout = nn.Dropout(0.3) # 节点级加性注意力:对每个节点输出一个标量权重 self.attn = nn.Linear(hidden_dim, 1) # 三个任务头,输出类别数按标注协议决定 self.rumor_head = nn.Linear(hidden_dim, 2) # 谣言/非谣言 self.stance_head = nn.Linear(hidden_dim, 3) # 支持/否定/中性 self.intent_head = nn.Linear(hidden_dim, 4) # 回复意图 def forward(self, x, edge_index, batch): h = F.relu(self.conv1(x, edge_index)) h = self.dropout(h) h = F.relu(self.conv2(h, edge_index)) # 注意力得分经过 sigmoid 后作为乘法权重。 # 严格版本应按每个事件做 softmax,这里用 sigmoid 近似,训练更稳 alpha = torch.sigmoid(self.attn(h)) # 加权求和得到事件级表征 emb = global_add_pool(alpha * h, batch) return self.rumor_head(emb), self.stance_head(emb), self.intent_head(emb)参数上说几点。hidden_dim 用 128 已经能在大多数公开数据上跑到不错的 F1,256 在数据量大时略优但训练时间接近翻倍。dropout 对 GCN 尤其重要,因为图数据过拟合时指标会在两个 epoch 内突然掉盘。为什么没有按事件做 softmax 注意力而是用 sigmoid?按事件 softmax 需要对每个 graph 单独 mask,batch 里图大小不一实现更绕,实际任务里 sigmoid 加权配合 global_add_pool 效果差距很小,先跑通更重要。
3.4 多任务训练的三个基座:损失权重、任务顺序、梯度平衡
多任务不是把三个 loss 简单相加就完事。我常用的损失是各任务交叉熵的加权和:
loss = (weights["rumor"] * F.cross_entropy(rumor_logits, label_rumor) + weights["stance"] * F.cross_entropy(stance_logits, label_stance) + weights["intent"] * F.cross_entropy(intent_logits, label_intent))损失权重建议先固定成 0.5 / 0.3 / 0.2 跑几轮,观察各任务收敛速度。有一个容易忽略的点:谣言判别是主任务,它的梯度应该主导共享编码器的更新;如果立场任务的样本更均衡、梯度更大,共享层会被带偏。这时可以把谣言任务的权重调高,或者在反向传播时对 stance 的梯度乘一个 0.3 左右的缩放,而不是一味增加权重。
任务顺序上,常见的做法是三个任务共享同一个 batch 同步更新,而不是轮流训练。同步更新时建议对总 loss 做一次梯度裁剪(norm 设为 5.0),防止某个任务出现大梯度把共享层的分布冲坏。多任务真正的收益在于共享层学到“文本 + 传播结构”的通用表示,立场和意图任务相当于给共享层提供了正则化,这正是它比单任务谣言分类稳的原因。
4. 数据组织与训练配置:协议、损失函数和超参表
4.1 数据集目录结构与标注协议
解压这套压缩包之后,典型的数据组织方式是“事件目录 + 标签文件”。不同的数据源会改目录名,但结构基本一致。
data/ ├── events/ │ ├── event_0001/ │ │ ├── source.json # 源帖字段:文本、用户、时间、事件id │ │ └── replies.jsonl # 转评记录,每行一条,含 parent_id │ ├── event_0002/ │ │ └── ... └── labels.json # 事件级标注:谣言/非谣言/未经证实source.json 里通常有 text、user_id、timestamp、event_id;replies.jsonl 每行是一个转发或回复,靠 parent_id 指向父帖子。labels.json 则按 event_id 给出事件级标签,有些数据集还会给出帖子级立场标签,这是多任务能落地的前提。
多任务的“多”具体是哪几个任务,决定了代码里任务头的数量和标签映射。我见过最稳定的组合是:谣言判别(二分类或三分类)+ 立场分类(支持/否定/中性)+ 回复意图分类(质疑/求证/情绪化评论等)。如果你的数据只有前两个标签,第三个任务头可以去掉,或者用无监督的文本聚类打伪标签再参与训练;伪标签任务建议设很小的权重,否则会把噪声灌回共享层。
读取数据时有一个值得注意的坑:jsonl 文件的 parent_id 是字符串,很多数据里 “None” 被写成字符串而不是空值,直接做 parent 判断会把所有帖子都当成普通节点,根节点缺失。常见做法是先把 parent_id 统一转成 str 再判断:“None”、空串、null 三种形态都看作根。
4.2 按事件划分训练集:为什么随机切分是错的
同一个事件的转发帖共享传播树、共享话题和情绪倾向,如果按帖子随机切分,模型在训练时已经见过测试集里同一棵树的很多节点,泄露远比想象严重。正确的做法是按事件(event_id)分组切分,保证同一个事件的全部帖子要么在训练集要么在测试集。
from sklearn.model_selection import GroupShuffleSplit # samples 是全部事件样本,event_ids 与 labels 长度一致 event_ids = [s["event_id"] for s in samples] labels = [s["label"] for s in samples] gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next(gss.split(samples, labels, groups=event_ids))这里 GroupShuffleSplit 的 groups 参数是关键,它保证同一个 group 不会被拆到两个集合。验证集用同样的方式从 train_idx 里再切一次,groups 传 event_ids[train_idx];验证集也不能和训练集共享事件。
提示:验证集不能和训练集共享事件,否则早停会一直停在虚高的点上,换到全新事件时效果立刻打回原形。
如果不按事件划分,你会在测试集上看到漂亮的 90%+ 准确率,这是图模型最容易骗人的地方。我见过太多人拿着这种虚高结果上线,然后被完全没见过的传播树结构打败。
4.3 训练参数表:从 batch size 到早停阈值
图数据训练参数和普通 CV 差别不小,关键是每个 batch 内图的大小不同,没法按固定 sequence length 来估计显存。以下是基于这套框架的个人默认值,按数据量调整即可。
| 参数 | 建议值 | 说明 |
|---|---|---|
| batch_size | 16 ~ 32(按事件) | 每个样本是一整棵传播树,树大的时候显存压力大 |
| learning_rate | 5e-4 ~ 1e-3 | Adam 配合 warmup 5% 更稳 |
| GCN 层数 | 2 ~ 3 | 超过 3 层过平滑风险显著 |
| hidden_dim | 128 ~ 256 | 数据小用 128,追求上限用 256 |
| dropout | 0.3 ~ 0.5 | 图数据必须有一层 dropout 在 GCN 之间 |
| 损失权重 | 0.5 / 0.3 / 0.2 | 主任务谣言判别权重最高 |
| 早停 patience | 8 ~ 10(epoch) | 依据验证集宏 F1,不是 loss |
| 梯度裁剪 | max_norm=5.0 | 防止多头任务某分支梯度冲崩 |
训练循环本身很直接,用 PyG 的 mini-batch 加载器,一个 epoch 内按事件迭代。关键点是从第 2 个 epoch 开始看验证集宏 F1,而不是看加权 loss——多任务加权 loss 下降不代表谣言主任务在变好。
5. 避坑指南:图谣言检测复现中反复出现的五个问题
5.1 新事件没有传播图,冷启动直接失效
现象:模型跑完测试集指标不错,一接到新事件,在只有少量转发甚至零转发的阶段,输出的置信度完全不可信。
原因:整个模型建立在传播树上,图卷积没有图可用。部署阶段的新事件不会像数据集一样自带完整传播树,冷启动是图模型最容易被高估的地方。
解决:对早期事件回退到纯文本注意力基线,等传播树深度超过 2 层再切回 GCN 模型。常见做法是部署两条路径,冷启动用轻量文本模型,热启动用图模型,用事件报告时间做条件切换。我在生产环境里把max_depth > 2作为切换阈值,再往前宁可用低一点召回率的文本模型,也不让 GCN 在图上硬算出一个毫无依据的置信度。
5.2 过平滑:GCN 层数一多,所有节点表征趋同
现象:把层数从 3 调到 5,验证集 F1 掉了 4~5 个点,节点 embedding 的方差明显变小,所有节点的表征看起来都差不多。
原因:GCN 的拉普拉斯平滑本质上让每个节点反复聚合邻居,层数越多越接近全图均值。传播树本身深度不大,但“深”和“有效”是两回事。
解决:控制层数在 3 层以内,或加残差连接h = h + conv(h)。如果确实想保留深度,把 GCN 换成 JK-Net 的结构,每一层的输出都拼起来再进分类层,而不是只在最后一层输出。对谣言检测来说,第一层的“内容+直接邻居”和第二层的“子树结构”已经覆盖了绝大多数有效信息。
5.3 类别不平衡:谣言样本占比太低
现象:测试集准确率很高,但谣言类别的召回率只有 20%,模型把所有样本都判成非谣言。
原因:谣言在真实数据里占比常常不到 10%,普通交叉熵会被多数类主导。
解决:先用加权交叉熵做基线,不行就换 focal loss,常用参数是alpha=0.25, gamma=2。也可以在采样时按事件标签做平衡采样,保证每个 batch 里至少有一部分谣言样本。主任务评价必须盯着谣言类的 F1,而不是整体准确率,否则调参方向会被带偏。
5.4 切分泄漏:同一事件的兄弟帖子被拆进训练和测试
现象:离线 F1 高达 0.92,线上新事件只有 0.7。最典型的是改了一版模型后离线涨点,上线反而跌,回滚之后离线又涨。
原因:随机切分把同一棵传播树的节点泄漏进了测试集,共享层学到的“记忆”在离线评估里被当成泛化能力。
解决:用 GroupShuffleSplit 按事件分组,这是底线。更好的做法是模拟线上冷启动:按事件时间排序,用前 80% 时间的事件做训练、后 20% 做测试,这样评估结果才接近真实部署场景。时间切分会牺牲一点训练数据量,但换来的指标可信度完全值得。
5.5 显存与内存爆炸:单棵树规模过大
现象:某个事件有上万个转发节点,GPU 显存直接 OOM,CPU 内存也被邻接矩阵撑满。
原因:一个 batch 里塞了几棵大树,邻接矩阵非零元过多,PyG 的 mini-batch 会把所有图拼装成大稀疏矩阵,边数爆炸。
解决:限制最大深度,对超过阈值的传播树做随机子采样;或者用动态 batch size,按总边数而不是样本数来填充。经验法则是每个 batch 的总边数控制在 5 万以内,超过就减半 batch;树深度超过 6 层的分支可以直接截断,因为深层转发对谣言判别的边际贡献很小。
6. 评估指标、降级策略和我的一个复现习惯
6.1 别只盯着 Accuracy:谣言检测的指标三件套
谣言检测天然类别失衡,accuracy 是陷阱。离线评估我只看三个数:宏 F1(对所有类别算 F1 再平均)、谣言类别的召回率、以及 AUC。宏 F1 对每个类一视同仁,类别失衡时比准确率诚实得多;谣言召回率决定了系统能不能在真实场景中拦截到足够多的谣言;AUC 则用来判断置信度排序质量,当作后续人工复核队列的依据。三个指标一起看,才能判断模型是真的变强,还是只是把阈值调对了位置。
6.2 冷启动降级:图不可用时回退到文本注意力基线
我在生产环境里通常部署两套模型。传播树到达一定规模前走纯文本注意力基线,树成形后再切换图模型。这个切换要在事件级别判断,而不是帖子级别。回退到的文本模型不必重新写,把上面模型里的 GCN 编码器去掉,保留注意力池化层,就变成一个合法的文本基线;它本身就是多任务框架的退化版本,共享层和任务头都能复用。做这个降级的意义在于:谣言检测系统最怕的不是效果差,而是不知道什么时候该信自己的输出。
6.3 固定随机种子并保存每个 epoch 的 checkpoint
有一个教训我记了很久。有一次我在调整注意力权重,怎么调验证集都在 0.81 上下波动,换回旧代码也复现不了前两天跑出的 0.85。查了两天,最后发现是当初没固定随机种子,dropout 加 GCN 初始化的波动,让两次训练相差了 4 个点。从那以后,我每个训练脚本第一行就是固定 seed,并且每个 epoch 都保存 checkpoint,按 epoch 命名。同样的代码、同样的参数,不能稳定复现的模型,没法谈后续调优;把种子和 checkpoint 这个习惯固定下来,等于给自己买了后悔药。
做谣言检测,最难的不是把模型调到某个指标,而是搞清楚模型在什么数据上有效、在什么条件下失效。传播树的优势在新事件早期并不成立,这一点想清楚了,训练、上线、降级方案才不会被离线的漂亮数字骗过去。希望帮到你。
本文还有配套的精品资源,点击获取