注意力机制与图卷积结合的多任务谣言检测源码实战解析
简介基于注意力机制与图卷积神经网络的多任务谣言检测完整项目内含Python源码和整理后的数据集面向需要完成毕业设计、期末大作业的自然语言处理方向学生也适合关注社交网络谣言识别的研究者。项目源自2022届本科毕业论文在原工作基础上改用BERT编码器提取词向量和句向量并接入BiGCN或Self-Attention完成谣言检测与立场分类同时重新规整了数据集存储结构不再将所有信息塞入JSON文件而是采用更清晰直观的文件组织方式。压缩包共75个文件涵盖25个Python脚本、17个文本说明、15个JSON数据文件、12个Notebook演示、4个Markdown文档等整体大小约18.67MB。已有366人学习下载可据此快速复现实验、对比不同模型效果或结合源码调整损失函数和处理流程用于多任务谣言检测的进一步改进与二次开发。1. 注意力图卷积做多任务谣言检测这个 Python 源码包到底值不值得跑如果你接到过社交平台谣言识别的活儿多半会有过这种体验单独用文本分类换个事件、换个话题F1 就直接往下掉。把转发关系、用户之间的关注关系拼成图再用上多头注意力去抓关键线索同时让检测和立场分类一起训练模型才没有把“谣言”这个词固化成黑话而是学会看传播结构。标题里这个 Python 源码包就是把注意力机制、GCN 和多任务落进一个可复现的框架并自带训练/评测数据集。适合想快速验证多任务谣言检测思路、又不愿意从空白搭架子的人。下面我会把它拆成任务设计、数据预处理、调参和避坑四块。2. 多任务谣言检测的设计任务拆分、图结构构造与模型选型2.1 谣言检测拆成哪几个任务主任务与两个常用辅助任务多任务学习的出发点不是“任务越多越好”而是数据少、标签噪声大需要借助相关任务的正则化来稳定共享表征。在主任务上通常做的是二分类一条帖子或一个事件是否为谣言也有一些数据集会把标签定成真实、虚假、未验证三分类后者的类间边界更模糊但更贴近运营后台的真实场景。只靠这一个任务训练模型很容易记住“谣言”这个字眼本身而不是理解传播中的矛盾信号。立场分类是最经典的辅助任务。它把转发和评论的态度分为支持、否认、质疑、无关四类这些标签在标注上比真假标签更直观数据量也更充分。谣言传播里一条转帖如果附带大量“求证”“假的吧”这类否认态度的评论那它对识别结果来说是很强的结构信号。第二个常用辅助任务是传播态势预测一般用转发速度、传播深度或热度做回归或者把速度分成慢、中、快三档做分类。这个任务迫使模型不只盯着文本还要读出图里的时间节奏因为异常快速扩散往往是造谣内容的重要特征。这三个任务共享同一个文本编码和图编码然后再各自接独立的预测头。新手最容易误解的是“多任务”等于“输出平均”其实主检测任务有自己的逻辑回归头立场头和传播头只在训练阶段提供梯度测试时只保留主检测头。辅助任务相当于给共享编码器上约束让它不要把能力全部花在区分真假上还要保留态度和节奏的信息。这样当新事件出现时模型可以借助结构规律而不是死记词表。实践中主任务和辅助任务的数据并不是完全对齐的。有些帖子有立场标签但没有传播速度有些相反。所以数据预处理时要分别构造标签掩码在计算 loss 时只对有效位置做交叉熵否则一个nan会毁掉整个训练循环。2.2 图结构从哪来传播树、用户社交关系与事件共现边的构建GCN 输入的是图而谣言检测里“图”不是天然存在的需要从原始帖子记录里重建。最常用的是传播树以源帖为根转发和评论作为子节点parent_id字段指向父节点形成一棵有向树。这里节点可以是单条帖子也可以是聚合后的用户边权常见做法是用转发时间间隔的倒数间隔越短权重越大代表消息在短时间内被快速传导。这个结构直接对应谣言扩散的动力学过程。第二种是用户社交图。如果数据里有用户之间的关注、回复关系可以把参与同一事件的用户抽出来建图节点属性带上用户的历史发布数、粉丝数。这种图的好处是能够建模“谁转给了谁”坏处是原始数据不一定有完整社交关系。源码包如果只给了帖子层面的字段用户图往往要自己去额外拉数据建议先确认再动手。第三种是事件共现图两个帖子在短时间内提到了同一个话题标签、URL 或地点就连一条边。它适合异构平台数据比如把微博和新闻网站的文章放进同一张图通过共现实体串起来。这种做法实现成本偏低但边定义比较玄学时间窗大小对结果影响很大需要调参。构建图的代码逻辑并不复杂核心是按事件分组组内用parent_id恢复父子关系再把原始 ID 映射到连续的节点下标。要特别注意的是图不能跨事件把所有帖子塞进一张全连通图会引入大量噪声边因为不同事件的帖子之间根本没有传播关系。通常的做法是一个事件一张图训练时用 Batch 拼起来。2.3 注意力机制在图上的两个落点文本多头注意力与图注意力注意力机制在这个方案里不是只放在一个地方。文本端用多头自注意力处理帖子正文图端用 GCN 的邻域聚合让消息沿着传播路径扩散。先看文本端把 token 序列经过 Embedding 和位置编码再用两层多头自注意力头数 8每个头的维度d_k64。这样做的理由是谣言文本里“官方辟谣”“警方回应”这类关键证据往往离主语很远自注意力可以直接建模远距离依赖比 BiLSTM 更适合长距离语义关联。图端的注意力有两层含义。一是 GCN 卷积本身的邻域平均本质上是一种无参数等权注意力二是如果要把“官方账号转帖”和“路人吃瓜转帖”区分开就需要给每条边不同的权重这时常把 GCN 替换成图注意力网络 GAT。标题明确写着图卷积神经网络所以默认骨架用 GCN 卷积核但在实验时我会在 models 里加一个 GAT 分支做对照。经验是纯 GCN 要在传播图上叠 2 到 3 层才能消化多跳邻域而 GAT 一两层就能区分关键邻居且对源帖节点和反驳节点的敏感度更高。整个模型的串行结构一般是这样文本编码器输出每个帖子的文本向量作为图节点的初始特征GCN 把这些向量沿传播边做消息传递让“谁转发了它、在几跳内被转发、评论里有没有反驳”这些结构信息渗入节点表示最后从 GCN 输出上接多任务头。这个架构的好处是文本与结构相互补强注意力负责在文本内部找证据GCN 负责在传播结构上找证据。如果只有文本注意力没有图卷积模型就只是个句子分类器如果只有 GCN 没有注意力文本里细粒度的否认信号又会被平均掉。3. 从 zip 到跑通源码包目录检查、数据预处理与最小训练命令3.1 打开压缩包先看什么目录结构与数据 schema标题给的是一个 zip 压缩包拿到手不要急着装环境先把目录结构过一遍。常见做法是里面至少包含三块models 下的模型定义、preprocess 下的转换脚本、data 下的标注数据。下面是一个比较典型的布局不是某个固定版本但可以作为检查清单rumor_multitask/ ├── data/ │ ├── raw/ # 原始帖子 json/csv带 label │ ├── graph/ # 预处理后 graph.pt │ └── split/ # train.txt, dev.txt, test.txt ├── models/ │ ├── text_encoder.py # 多头注意力文本编码 │ ├── gcn_encoder.py # 图卷积编码 │ ├── heads.py # 多任务分类头 │ └── multitask.py # 组装 ├── preprocess.py ├── train.py ├── predict.py └── requirements.txt优先检查两个文件requirements.txt和data/raw下的示例数据。requirements.txt要确认 torch 和 torch_geometric 的版本关系这两个库的版本配对极其严格装错组合会直接出“undefined symbol”之类的底层错误。数据字段至少要覆盖四列text、label、timestamp、parent_id。parent_id-1表示这是源帖其他值表示父节点 id这是重建传播树的基础。如果还有stance列就更好了训练时可以多挂一个辅助头。建议在干净的虚拟环境里操作不要用全局环境。torch 和 torch_geometric 是出了名的依赖重灾区一个项目一个 venv 能省掉后面大量玄学的 import 报错。创建环境后先跑一个最小测试把from torch_geometric.nn import GCNConv能成功加载作为环境合格的标志。3.2 数据预处理从原始帖子到 torch_geometric 图对象数据转换的核心逻辑一般写在preprocess.py里这里给一个可读版本的传播图构建函数import pandas as pd import torch from torch_geometric.data import Data def build_propagation_graph(df): nodes {} edges [] for _, row in df.iterrows(): node_id int(row[post_id]) nodes[node_id] row if int(row[parent_id]) ! -1: edges.append((int(row[parent_id]), node_id)) node_ids sorted(nodes.keys()) idx_map {nid: i for i, nid in enumerate(node_ids)} x torch.tensor( [features_from_text(nodes[nid][text]) for nid in node_ids], dtypetorch.float ) edge_index torch.tensor( [[idx_map[src], idx_map[dst]] for src, dst in edges], dtypetorch.long ).t().contiguous() y torch.tensor( [int(nodes[nid][label]) for nid in node_ids], dtypetorch.long ) return Data(xx, edge_indexedge_index, yy) df pd.read_csv(data/raw/train.csv) graph build_propagation_graph(df) torch.save(graph, data/graph/train_graph.pt)逻辑说明features_from_text在这里只是一个占位函数正式代码里它应该是预先跑好的 BERT embedding 或 TFIDF 向量不能在训练循环里逐个过模型。edge_index使用 COO 格式第一行是源节点下标第二行是目标节点下标GCNConv 默认把有向边当作入边所以通常还要补一条反向边把树变成无向图否则信息只能从根流向叶子叶子节点得不到上游消息。参数说明idx_map把原始帖子 ID 映射到连续下标避免邻接矩阵出现空洞Data对象可以直接进torch_geometric.loader.DataLoader。如果数据里有stance列把它附到图对象上graph.stance torch.tensor([...])。不同事件的图要分别保存训练时用 Batch 把它们拼起来而不是全部连成一个巨大的连通图。3.3 最小训练命令与结果解读依赖装好、数据转成图之后最省事的运行方式是命令行入口。一般train.py会接收模型、学习率、轮数等参数python train.py \ --model att_gcn \ --data_dir data/graph \ --epochs 30 \ --lr 3e-4 \ --weight_decay 5e-4 \ --dropout 0.5 \ --loss_weight_aux 0.3 \ --multi_task参数说明--multi_task打开辅助任务头loss_weight_aux是辅助 loss 的权重设 0.2 到 0.5 比较常见太高会让立场分类主导优化方向lr取 3e-4图注意力模块对学习率很敏感超过 1e-3 容易震荡。如果显存不够再加--batch_size 32。跑通后先看训练日志主任务 loss 是否下降dev 的 F1 在 10 个 epoch 内是否稳定。正常情况是主 loss 在 5 轮内从 0.7 附近落到 0.45 左右立场辅助 loss 同步下降。如果主 loss 下降但 F1 一直卡在 0.5 附近优先怀疑标签不平衡如果 loss 直接变成 NaN去查features_from_text生成的向量有没有未归一化的大数值。“能跑通”和“效果好”之间还差一步。此时需要打开predict.py对 dev 里一条转发很多的谣言和一条只有寥寥几条评论的谣言分别输出概率观察模型是否真的用上了传播上下文。这一步能很快暴露出注意力没有落在关键字符上的问题具体的排查方法放在第 5 章。4. 注意力与 GCN 的调参清单头数、层数、图采样与多任务 loss 权重4.1 模型超参数表从 batch size 到谱归一化一个可复现的默认配置如下表。它不是最优解而是下限如果你连这个配置都跑不出合理结果多半是数据划分或预处理有问题。参数默认值调参方向文本编码层数2 层多头注意力长文本多时加到 3 层multi-head num8与 d_model 整除即可d_model256小数据集降到 128GCN 层数2超过 3 层注意过平滑GCN hidden256图规模大时降到 128dropout0.5谣言标签噪声大别低于 0.3loss_weight_aux0.3网格搜 0.0 / 0.1 / 0.3 / 0.5learning rate3e-4用 AdamW别用 SGDbatch_size32按事件图数量调整这里最容易被忽略的是图节点的度差异。有的源帖有几千条转发有的只有两条评论如果不做归一化大度节点的梯度会主导整个 GCN 层。GCNConv默认开了normalizeTrue用归一化邻接矩阵做卷积但如果你发现节点 embedding 的模长差异极大就要手动检查邻接矩阵的最大特征值必要时做谱归一化把特征值缩放到 1 以内。4.2 多头注意力参数头数、d_k 与残差多头注意力的核心实现一般长这样可以直接改参数import torch import torch.nn as nn class MultiHeadSelfAttention(nn.Module): def __init__(self, d_model, num_heads): super().__init__() assert d_model % num_heads 0 self.d_k d_model // num_heads self.num_heads num_heads self.qkv nn.Linear(d_model, 3 * d_model) self.out_proj nn.Linear(d_model, d_model) def forward(self, x, maskNone): B, L, D x.shape qkv self.qkv(x).reshape(B, L, 3, self.num_heads, self.d_k) q, k, v qkv.permute(2, 0, 3, 1, 4).unbind(0) scores torch.matmul(q, k.transpose(-2, -1)) / (self.d_k ** 0.5) if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn torch.softmax(scores, dim-1) out torch.matmul(attn, v) out out.transpose(1, 2).reshape(B, L, D) return self.out_proj(out)参数说明d_k一般取 64太小会让注意力对位置关系过于敏感太大则趋近均匀分布。mask的作用是屏蔽 padding 位置让它们不被关注。真正需要调的是num_heads8 个头通常够用如果 batch size 不大而头数很多每个头学到的东西方差很大反而像在乱摸。一个容易犯的错是把多头注意力直接用在图节点列表上。它处理的是“节点内的词序列”而不是“图上的节点序列”。节点之间的交互交给 GCN 完成如果要用注意力建模节点关系应该用 GAT 的邻域注意力而不是 Transformer。4.3 GCN 层数与过平滑阈值GCN 编码器的堆叠方式如下import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import GCNConv class GCNEncoder(nn.Module): def __init__(self, in_dim, hidden_dim, num_layers2): super().__init__() self.convs nn.ModuleList() for i in range(num_layers): self.convs.append( GCNConv(in_dim if i 0 else hidden_dim, hidden_dim) ) self.dropout nn.Dropout(0.5) def forward(self, x, edge_index): for conv in self.convs[:-1]: x F.relu(conv(x, edge_index)) x self.dropout(x) return self.convs[-1](x, edge_index)逻辑说明每一层 GCNConv 做一次“邻居平均加线性变换”。num_layers2意味着每个节点最终最多看到两跳邻居这个范围刚好覆盖“源帖 → 转帖 → 转帖的评论”的传播路径。谣言检测里两跳已经足够判断传播趋势再深的话节点表示会逐渐趋同也就是过平滑。如果必须加深不要直接堆层。常见补救是加残差连接把第一层前的输入加回最后输出或者用 Jumping Knowledge把每一层的输出 concat 后再降维。我在实践里倾向于先加残差再试 GAT而不是盲目把 GCN 加到 5 层。4.4 多任务 loss 权重网格搜索与不确定性加权多任务 loss 最常见的形式是加权和loss F.cross_entropy(logits_main, y_rumor) \ alpha * F.cross_entropy(logits_stance, y_stance)alpha 的选择有规律如果立场标签本身噪声大alpha 要调低如果主任务数据量少alpha 要调高。先在验证集上跑 alpha 等于 0.0、0.1、0.3、0.5 的网格取主任务 F1 最高的值。比网格搜索更稳的是不确定性加权让模型自己学习每个任务的噪声水平噪声大的任务自动降权。log_var_main torch.log(torch.exp(torch.randn(1, requires_gradTrue))) log_var_stance torch.log(torch.exp(torch.randn(1, requires_gradTrue))) loss 0.5 * (loss_main / torch.exp(log_var_main) log_var_main) \ 0.5 * (loss_stance / torch.exp(log_var_stance) log_var_stance)参数说明log_var是可学习标量训练中会自行调整两个任务的贡献。在谣言检测里立场任务通常比主任务噪声大因此这个 trick 经常能提升 1 到 2 个点的 F1。代价是训练初期两个log_var波动剧烈建议前 5 个 epoch 固定 alpha之后再把不确定性权重放开。5. 训练与推理中的常见坑数据泄漏、过平滑和内存爆炸排查5.1 按随机划分数据模型“提前看到答案”现象验证集 F1 在 0.85 以上换到一个新话题、新时间段后效果直接掉到 0.6 附近。原因源码包默认可能用random_split把同一事件的帖子拆进了训练集和验证集。同一个事件里的转帖语义高度相似图里邻居信息互相串门模型实际上在记忆传播模式而不是学习泛化的证据模式。谣言检测有很强的事件相关性跨事件验证才有意义。解决改成按事件或时间划分。先对所有帖子按事件做 groupby再用事件 id 划分保证训练集里出现的事件在测试集里完全不出现。这个修改是整个复现过程里影响最大的一步也最容易被人忽略。5.2 GCN 堆到 5 层F1 反而往下跌现象为了捕捉更多跳传播把 GCN 层数从 2 加到 5结果验证 loss 上升了甚至还不如不加图结构的文本模型。原因过平滑。消息每过一层就做一次邻域平均5 层之后节点表征高度趋同谣言和非谣言的特征分布重叠。事件级传播图的直径通常只有四五跳5 层几乎把全图信息平均成了一团浆糊。解决把层数退回 2 到 3 层并保持 dropout0.5。如果确实需要更长距离信息把源帖节点的 root embedding 拼到每个节点上或者改用 GAT让模型学会选择关键邻居而不是粗暴地全部平均。5.3 全量图的邻接矩阵让显存直接爆掉现象训练启动后几秒就 OOM甚至 CPU 内存也被占满。原因部分简化代码会把整批事件图转成稠密邻接矩阵 N×N。当节点总数到几十万这个矩阵直接占几十 G 内存再大的显卡也顶不住。GCN 本来只需要 COO 格式的edge_index但为了拼接方便有人会去构造 dense matrix。解决确认edge_index是 2×E 的 long tensor而不是 N×N 的矩阵。用torch_geometric.loader.NeighborSampler做按需邻居采样比如sizes[15, 10]代表第一层采 15 个邻居、第二层采 10 个。这样即使图很大每个 batch 的邻居数量都是可控的。5.4 辅助任务 loss 把主检测任务带偏现象多任务开启后stance loss 很快收敛到很低但主任务 F1 比单任务还差。原因立场分类通常有四类且样本相对均衡梯度比二分类的主任务大。alpha 过高时共享编码器被立场任务主导把特征都用来区分态度丢掉了真假判断要的语义细节。另一个原因是立场标签质量差很多“评论”类被强行标成“无关”。解决先跑 alpha0.0 的纯单任务基线再逐步增大。如果 0.1 有提升、0.3 就掉点说明立场任务只能当正则不能当主导。更稳妥的做法是给辅助任务加一个梯度缩放按主任务和辅助任务 loss 的比值做动态调整。5.5 注意力全开会权重均匀分布等于没有注意力现象打印注意力权重每个位置的概率都接近 1/88 个头仿佛在摸鱼。原因文本序列短、关键证据少softmax 温度又太高注意力落入了平坦区。另一个诱因是 dropout 太大把模型学到的聚焦能力稀释掉了。谣言文本往往只有几十个字但并非每个词都有证据价值。解决给注意力得分除以一个更小的温度比如scores scores / 0.5强制权重集中到少数关键 token。也可以用 entropy penalty 惩罚注意力的离散程度。如果问题不明显可以做一个简单的注意力可视化脚本看“官方、辟谣、质疑、求证”这些词有没有拿到高权重。6. 从离线到准实时按时间切分的评估与增量更新技巧6.1 时序划分脚本训练集永远比测试集早离线实验建议按时间戳排序后切分而不是按事件随机。脚本在训练前跑一次df pd.read_csv(data/raw/train.csv) df df.sort_values(timestamp) cutoff int(len(df) * 0.8) df_train, df_test df.iloc[:cutoff], df.iloc[cutoff:] df_train.to_csv(data/split/train_temporal.csv, indexFalse) df_test.to_csv(data/split/test_temporal.csv, indexFalse)谣言检测最终要部署在真实时间线上未来永远比训练集晚。时间切分能暴露模型面对分布漂移时的真实水平。如果源码包自带随机 split 脚本建议直接替换成这段。6.2 别忘了看 Macro-F1 和 AUC只盯 Accuracy 会假 happy因为多数谣言数据集中非谣言占比 60% 到 70%。训练时每 5 个 epoch 在验证集上算一次 Macro-F1 和 AUC-ROC用 Macro-F1 选 checkpoint。Micro-F1 会被多数类拉高Macro-F1 对少数类的变化敏感得多更贴近谣言检测这种不平衡场景。6.3 增量图推理的小技巧线上推理时新帖子的传播树还在生长通常先给一个初始图等转发量达到阈值后再补增量边。不用每次重建整张图只需把新节点加入edge_index旧的节点 embedding 可以缓存。我自己的教训是第一版直接全量重训结果每天固定时间段占用训练资源后来改成增量节点推理模型质量没有明显下跌计算开销省了七成。把评估和更新方式想清楚这个源码包才真正从“跑得通”变成“能用得住”。希望帮到你。本文还有配套的精品资源点击获取