简介:基于注意力机制与图卷积神经网络的多任务谣言检测源码及其数据集,源自2022届本科生毕业论文的后续优化实现,适合深度学习方向的学生用作毕业设计或期末项目参考。方案采用预训练语言模型BERT编码器获取词级与句级特征,下游分别接入图卷积网络与自注意力模块,同时完成谣言检测和立场分类,并重新整理了数据存储结构,较原始版本更规整直观。压缩包共七十五个文件,内含二十五份脚本、十五份配置与标注数据、十二份交互式实验文档,另附说明文件与依赖清单,整体大小约18.67兆,可直接按模块复现实验。目前已有三百六十六人学习下载,资源内提供两种模型路线,既可对比不同编码器与图卷积结构的训练效果,也能基于现有代码做进一步调优,具备较强的参考价值。
1. 一份能跑的“注意力+图卷积”谣言检测源码:MSA-BiGCN 项目初探
如果你正在找一份能直接跑起来的图神经网络毕业设计源码,这个项目值得认真拆一遍。它对应一篇 2022 届本科毕业论文《基于注意力机制和图卷积神经网络的多任务谣言检测》,作者把论文里的 MSA-BiGCN 模型做了后续优化,核心改动是放弃原来的静态词向量,改用 BERT 做编码器,再分别接双向图卷积网络(BiGCN)和多头自注意力机制,最后同时输出谣言检测和立场分类两个任务的结果。压缩包里不止有 python 源码,还带了 PHEME、PHEME-stance、semeval2017-task8 三套数据集,省掉了自己到处找数据和洗格式的步骤。适合正在做注意力机制、图卷积神经网络方向毕业设计或期末大作业的同学,也适合想快速上手多任务谣言检测实验的研究者。
2. 模型组成拆解:BERT做编码、多头注意力做交互、BiGCN做传播结构建模
2.1 从论文优化思路看整体架构
谣言检测的关键不只是看源推文写了什么,更要看回复是怎么铺开的。这条回复是支持还是质疑,由谁发起、顺着哪条线扩散,这些信息藏在会话树的结构里。早期做法是把整棵树的文本过一遍 LSTM,但 LSTM 对树形结构的建模很别扭,顺序信息会抹掉回复之间的父子关系。所以论文才选择图卷积神经网络:把每条推文和回复当作一个节点,谁回复谁就画一条边,这样图结构天然保留了传播路径。
当初论文的原版实现用的是 GloVe 或类似静态词向量做输入,节点特征基本靠统计词袋,语义泛化能力一般。作者在后续优化里把编码器整体换成了 BERT,一次拿到两样东西:每个 token 的词向量和整条文本的句向量。词向量可以喂给图卷积层,让图结构去聚合邻居信息;句向量可以喂给注意力层,让模型跨节点捕捉语义呼应。压缩包里MSA-BiGCN目录下就是主模型MSABiGCN.py,而BERT-BiGCN目录下的BertBiGCN.py是去掉自注意力之后的 baseline,方便做消融对比。这两个模型共用同一套数据加载和训练脚本,所以切换模型只是改一个命令行参数的事。
给整棵会话树建模时,每个节点拥有自己的文本特征,边代表回复关系。图卷积负责“从结构上学”,自注意力负责“从语义上学”,两条通道最后拼在一起进分类头。这样设计的动机很直接:纯图卷积看的是邻域,两个相隔很远的回复之间即便语义高度相关,GCN 也要堆很多层才能传到;多头注意力则没有这个限制,一次就能看到所有节点。反过来,注意力完全不看谁回复谁,会把父子关系和传播方向丢掉。两者互补,正好对应这个项目名里的“注意力机制 + 图卷积神经网络”。
2.2 编码器:BERT如何生成词向量和句向量
在MSABiGCN.py里,模型前半段基本就是加载一个预训练 BERT。常见的实现方式是这样:
from transformers import BertModel import torch.nn as nn class BertEncoder(nn.Module): def __init__(self, model_name="bert-base-uncased", max_len=128): super().__init__() self.bert = BertModel.from_pretrained(model_name) self.max_len = max_len def forward(self, input_ids, attention_mask, token_type_ids): outputs = self.bert( input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids, return_dict=True ) token_embeds = outputs.last_hidden_state # [B, L, H] sent_embeds = outputs.pooler_output # [B, H] return token_embeds, sent_embedslast_hidden_state返回的是每一个 token 的上下文向量,形状是[batch_size, seq_len, hidden_size],这条会作为词向量交给图卷积层使用。pooler_output取的是[CLS]位置经过全连接和 tanh 后的句子表示,形状是[batch_size, hidden_size],这条交给自注意力层做全局交互。两个输出各司其职,这也是摘要里明确提到的“获得词向量和句向量”的含义。
需要注意 BERT 的输入不只是input_ids,还要同时给attention_mask和token_type_ids。在谣言检测场景里,一个会话树有很多条回复,常见做法是把源推文放在前面,回复依次往后拼,用token_type_ids区分哪段是源推文、哪段是回复。max_len要按整棵树的拼接长度设置,PHEME 里长对话树很容易超长,我一般会先统计一下数据里的最大长度,再选择 128 或 384,避免截断后把关键回复切掉。
2.3 传播结构建模:图卷积在谣言检测中的角色
BiGCN 里的“Bi”指的是双向,不是双向 RNN 那种时间维度的双向,而是两个方向的图卷积。一个方向从源推文出发往下传到回复,模拟谣言信息的传播过程;另一个方向从回复往上传回源推文,模拟模型最终要判断源头推文是否被回复的证据支持或质疑。两条通道用不同的邻接矩阵,通常一个是原图,一个是转置图。
以两层 GCN 为例,消息传递可以写成下面这种形式:
import torch import torch.nn as nn class GraphConv(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.linear = nn.Linear(in_dim, out_dim) self.act = nn.ReLU() def forward(self, x, adj): # x: [node_num, in_dim],adj: [node_num, node_num] support = self.linear(x) h = torch.mm(adj, support) return self.act(h)注意这里adj一般不是原始 0/1 邻接矩阵,而是经过归一化的。常见做法是D^{-1/2} A D^{-1/2},也就是对称归一化,能避免度数大的节点聚合结果过大。这个邻接矩阵在项目的data.py里构建,rumorDataset.py负责把文本转成模型输入,两者通过样本索引对齐。ABGCN.py里应该就是这种双向图卷积层的组合,输入token_embeds,输出每个节点的图增强特征。
为什么图卷积适合谣言检测?因为谣言往往不是靠单条文本就能判断的,要看出回复里有大量质疑、求证和反对意见,这些信号出现在树的不同分支。GCN 的聚合操作天然能把一个节点周围的信息汇总起来,比如某个回复的父节点是辟谣帖,那么它自身的表示里就会有辟谣的证据。多叠几层 GCN,信息就能沿着树结构传播得更远。
2.4 多头自注意力机制为什么被加进来
图卷积虽有结构优势,但在长距离语义关联上较弱。两个节点在会话树里隔着很远,甚至在不同分支,GCN 传一次消息只能影响一跳,要传递到远端就得加深网络,训练难度和过平滑问题都会跟着来。多头注意力机制的引入就是为了弥补这个短板:它让所有节点两两之间直接计算关联,不管在图上的距离有多远。
参考实现并不复杂:
import torch.nn as nn class MultiHeadSelfAttention(nn.Module): def __init__(self, hidden_size, num_heads): super().__init__() self.attn = nn.MultiheadAttention( embed_dim=hidden_size, num_heads=num_heads, batch_first=True ) def forward(self, x): attn_out, attn_weights = self.attn(x, x, x) return attn_out, attn_weightsattn_weights是可可视化的注意力分数,能看出模型在判断一条推文是否是谣言时,把注意力重点放在了哪几条回复上。这个矩阵在plot.ipynb里经常被画成热力图,是论文和答辩里很加分的材料。在多任务谣言检测这种任务里,自注意力还能帮不同任务的分类头各取所需:谣言检测头更关注传播结构,而立场分类头更关注“支持/否定/质疑/评论”这类显式的语义态度,注意力分支对后者的贡献会更加直接。
3. 把代码跑起来的完整流程:环境、数据预处理与训练
3.1 环境准备与依赖检查
这类论文代码最常见的问题就是依赖版本对不上,尤其是transformers和pytorch。我的习惯是先建独立虚拟环境,再按压缩包里的requirements.txt安装。项目还带了一个requirements-version.txt,看名字应该是作者跑实验时锁定的版本清单,复现论文结果时优先看这个文件。
cd MSA-BiGCN python -m venv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txt如果你是用requirements-version.txt,直接替换最后一行即可。我本地常用的组合是 Python 3.8、PyTorch 1.9 或 1.10、transformers 4.x。这里提醒一句:transformers 3.x 和 4.x 在BertModel的输出结构上有变化,旧代码里outputs[0]、outputs[1]的取法在新版本下可能直接报错。所以如果安装后运行就报索引越界,先检查是不是from_pretrained之后拿到的是不是ModelOutput对象。用本项目自带的requirements-version.txt可以少踩这个坑。
3.2 数据集怎么组织:两套数据集分开的原因
压缩包里的datasets目录下有三个子集:PHEME、PHEME-stance、semeval2017-task8。PHEME 是社交平台上的谣言检测数据集,按事件划分,每个事件下有一棵以源推文为根节点的回复树,并标注了谣言类别。PHEME-stance是带立场标注的版本,用于立场分类任务,这是多任务里的第二个任务。semeval2017-task8是额外的立场分类评测集,项目里对应semeval2017-8-test.ipynb,用来验证模型在公开评测集上的泛化能力。
README 里特意提到,这次重新整理了数据集,不再像旧 repo 那样“把所有信息塞进 JSON 文件”,而是用更规整、更直观的文件存储方式。所以目录结构应该更接近按事件分文件夹的做法:
datasets/ ├── PHEME/ ├── PHEME-stance/ └── semeval2017-task8/具体到每个事件目录,一般会包含源推文文本、回复文本列表和对应的标签文件。不要自己去网上下 PHEME 官方 JSON 再手动解析,直接用项目整理好的版本最稳。数据加载时,rumorDataset.py负责读文本和标签,data.py负责把回复关系转成图结构。
3.3 加载数据:rumorDataset.py与data.py的使用逻辑
写一个简化版的 Dataset 加载逻辑,你看过之后就知道代码在做什么:
from torch.utils.data import Dataset import torch class RumorDataset(Dataset): def __init__(self, data_dir, tokenizer, max_len=128): self.samples = load_samples(data_dir) # 返回 list[dict] self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.samples) def __getitem__(self, idx): sample = self.samples[idx] texts = sample["texts"] # [source_tweet, reply_1, reply_2, ...] adj = sample["adj"] # [node_num, node_num] rumor_label = sample["rumor_label"] stance_label = sample["stance_label"] encoding = self.tokenizer( texts[0], texts[1:], padding="max_length", truncation=True, max_length=self.max_len, return_tensors="pt" ) return { "input_ids": encoding["input_ids"].squeeze(0), "attention_mask": encoding["attention_mask"].squeeze(0), "adj": torch.tensor(adj, dtype=torch.float), "rumor_label": torch.tensor(rumor_label), "stance_label": torch.tensor(stance_label) }这段代码里最关键的参数是max_len,它决定了 BERT 能看到的文本长度。PHEME 的会话树节点数差异很大,短的只有几条回复,长的可能超过一百条,data.py里通常会控制最大节点数,超出的部分截断或随机采样。adj矩阵的维度必须和节点数一致,否则图卷积层计算时会报维度不匹配。我在调试时一般会在__getitem__里打印一下texts的长度和adj.shape,确认二者是否对得上。
3.4 训练命令与参数说明
项目同时提供了train.py、trainOnlyRumor.py、trainOnlyStance.py三个训练入口。train.py跑多任务,后两个是消融实验用的单任务脚本。训练命令格式大概是这样:
python train.py \ --model MSABiGCN \ --dataset PHEME \ --batch_size 16 \ --epochs 30 \ --lr 2e-5 \ --max_len 128 \ --seed 2022关键参数可以参照下表:
| 参数 | 建议值 | 说明 |
|---|---|---|
--model | MSABiGCN或BertBiGCN | 切换主模型和 baseline |
--batch_size | 16 | 显存不足就降到 8 或 4 |
--lr | 2e-5 | BERT 微调常用学习率,太大容易破坏预训练权重 |
--epochs | 30 | 论文实验常用的轮数,早停可减到 15 |
--max_len | 128 | BERT 输入截断长度,长对话树建议 384 |
--seed | 2022 | 固定随机种子,保证实验可复现 |
学习率这块多说两句。BERT 微调一般用 2e-5 到 5e-5,比常规 CNN 训练小一个数量级。如果同时训练多任务头,学习率太大容易让分类头过拟合、BERT 表示崩塌。如果你跑出来的 loss 一开始就剧烈震荡,先把学习率降到 1e-5。
4. 用图和文本双通道训练:多任务谣言检测的核心实现
4.1 多任务怎么绑定:共享编码器与任务头
所谓的多任务谣言检测,是把“判断是不是谣言”和“判断回复立场”放到同一个模型里训练。两个任务共享同一个 BERT 编码器,但各自带独立的分类头。rumor_head输出二分类或四分类的谣言标签,stance_head输出四分类立场标签,训练时把两个交叉熵损失按权重加起来。
import torch.nn as nn criterion = nn.CrossEntropyLoss() alpha = 1.0 # 谣言检测损失权重 beta = 0.5 # 立场分类损失权重 loss = alpha * criterion(rumor_logits, rumor_labels) + \ beta * criterion(stance_logits, stance_labels) loss.backward()alpha和beta的取值直接影响训练走向。谣言检测是主任务,权重通常会高一些;立场分类是辅助任务,权重可以低一些,避免辅助任务噪声干扰主任务。如果你想验证多任务是否真的比单任务强,项目里的trainOnlyRumor.py和trainOnlyStance.py就是干这个的。先跑单任务拿到两个指标,再跑多任务对比,这是论文实验的标准操作。
4.2 图卷积层:从邻接矩阵到传播特征
在ABGCN.py里,双向图卷积的实现思路是准备两个 GCN 分支,一个处理原始邻接矩阵,一个处理转置后的邻接矩阵。GCN 层的前向传播虽然简洁,但有几个细节容易踩坑。
import torch import torch.nn as nn class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.linear = nn.Linear(in_dim, out_dim) self.act = nn.ReLU() def forward(self, x, adj): # x 是节点特征矩阵 [node_num, in_dim] # adj 必须提前做对称归一化,否则度数大的节点特征会被放大 support = self.linear(x) h = torch.mm(adj, support) return self.act(h)注意adj的归一化不能省。原始邻接矩阵里,一个节点如果有几百条回复,聚合出来的特征数值会很大,模型训练非常不稳定。常见做法是引入度矩阵 D,计算D^{-1/2} A D^{-1/2}。在data.py构建图时就会把归一化做掉,但如果你自己改数据集,这一步很容易漏。另一个容易犯的错是adj的 dtype,PyTorch 默认的 GCN 计算要求浮点矩阵,如果adj是整型的 0/1 矩阵,torch.mm会直接报类型错误。
4.3 特征融合与最终判别
BiGCN 分支输出的图卷积特征,和自注意力分支输出的注意力特征,最后要在送入分类头之前融合。融合方式有很多种,常见的是拼接后过一个全连接层:
gcn_feat = gcn_output.mean(dim=1) # [B, hidden] attn_feat = attn_output.mean(dim=1) # [B, hidden] final_feat = torch.cat([gcn_feat, attn_feat], dim=-1) # [B, 2*hidden] rumor_logits = self.rumor_head(final_feat) stance_logits = self.stance_head(final_feat)这里用mean(dim=1)而不是直接取[CLS],原因在于图卷积输出的关键信息分散在各个发言节点上,源推文节点固然重要,但多个回复节点的聚合特征能提供更完整的传播图景。final_feat的维度是两倍 hidden_size,所以分类头的输入维度要相应调整,MSABiGCN.py里如果你看到self.rumor_head = nn.Linear(hidden * 2, num_classes),就是这个原因。
4.4 推理与可视化:result/plot notebook的使用
训练完之后,可以用项目带的结果 notebook 做推理和可视化。result.ipynb负责在测试集上批量跑模型,输出每个样本的预测标签和置信度,方便计算准确率、F1 和混淆矩阵。plot.ipynb则偏向展示,常见操作是画出训练过程的 loss 曲线和多头注意力的热力图。
jupyter notebook result.ipynb plot.ipynb启动后按 notebook 里的单元格顺序执行即可。做可视化时,注意力热力图是最值得看的东西:把attention_weights画出来,能看到模型在判断某条推文时,是更多关注源推文本身,还是关注某些带有质疑信号的回复。这个信息在论文实验分析里非常有用,可以直接截图放进“可视化分析”一节。
5. 避坑记录:数据匹配、设备显存与训练异常
5.1 旧JSON格式与新文件结构混用
现象:运行train.py时报FileNotFoundError,或者在数据加载阶段报KeyError: 'text'、KeyError: 'label'。
原因:README 明确说了,数据集被重新整理过,不再把信息塞进大 JSON 里。如果你自己下载了 PHEME 官方 JSON 格式的数据,或者把 master 分支的旧代码拿过来跑,rumorDataset.py会按新目录结构找文件,找不到就抛异常。
解决:直接使用压缩包内的datasets目录。先打开 README 确认当前分支的数据格式,再检查事件目录下是不是有独立的源推文文件、回复文件夹和标签文件。如果你确实要导入外部数据,需要按rumorDataset.py的load_samples函数期望的结构重新整理,核心是给每条样本返回一个包含texts、adj、rumor_label、stance_label的字典。这块没有捷径,老老实实改数据加载函数。
5.2 设备显存溢出
现象:训练刚开始就报CUDA out of memory,只要 batch_size 大于 4 就必炸。
原因:BERT 本身就吃显存,这个项目还要同时处理整棵会话树的 token 序列和图邻接矩阵,两个大对象加在一起,显存压力翻倍。再加上max_len设置过大时,序列长度对所有节点拼接后的总长度负责,显存很容易不够。
解决:先降batch_size到 4 或 2,把max_len从 384 降到 128,再加混合精度训练。PyTorch 自带 AMP,改造很简单:
python train.py --batch_size 4 --max_len 128 --fp16如果脚本里没有封装--fp16参数,可以在训练循环外用torch.cuda.amp.autocast()手动包住前向传播和 loss 计算。另外注意num_workers别设太大,数据加载 worker 本身也占内存,在低显存机器上建议设成 0 或 2。
5.3 谣言检测和立场分类两个任务loss失衡
现象:总 loss 一直在降,但谣言检测准确率上升后开始震荡,立场分类准确率始终在很低的位置徘徊。
原因:两个任务的标签分布差异很大,PHEME 的谣言标签可能相对均衡,但态度标签里“评论”类别占比很高,“支持”“否定”类别很少。交叉熵默认对所有类别一视同仁,模型会更偏向学多数类。再加上alpha和beta如果都设成 1.0,立场分类的噪声会被放大,反过来干扰谣言检测。
解决:先用trainOnlyRumor.py单任务跑谣言检测,确认模型本身是收敛的;再用trainOnlyStance.py单任务跑立场分类,看这个任务单独能到多少分。两个都通了之后,再回到train.py把beta调到 0.3 或 0.5,给立场分类降权。如果还是不稳,对 stance 的交叉熵加类别权重,比如nn.CrossEntropyLoss(weight=torch.tensor([1.0, 1.5, 0.5, 1.0]))。
5.4 图邻接矩阵维度与文本序列不对齐
现象:训练到中途报IndexError: index out of range in self,或者 GCN 层里torch.mm(adj, support)报维度不匹配。
原因:data.py在构建邻接矩阵时按最大节点数统一尺寸,但有的会话树本身节点数超过预设上限,截断后某些边的起点或终点索引超出了节点范围;或者反过来,graph_max_nodes设得比实际节点数大,但token_embeds是按实际长度生成的,两者对不上。
解决:在数据预处理阶段统计所有会话树的节点数分布,把graph_max_nodes设成覆盖 95% 样本的上限。截断时不能只裁节点,还要同步过滤掉两端都被裁掉的边,否则邻接矩阵里会残留无效索引。
def build_subgraph(nodes, edges, max_nodes): # nodes: 节点id列表 # edges: (src, dst) 列表 if len(nodes) <= max_nodes: return nodes, edges keep_nodes = set(nodes[:max_nodes]) new_edges = [(s, d) for s, d in edges if s in keep_nodes and d in keep_nodes] return nodes[:max_nodes], new_edges这里的关键是keep_nodes和new_edges必须在同一个样本内保持一致。我自己第一次跑 BiGCN 时就是只裁了节点没裁边,结果每条样本的邻接矩阵里都有几个越界的边索引,报错信息特别迷惑。
6. 验证技巧:用小样本跑通全流程再上全量数据
无论你用的是 PHEME 还是自己整理的社交平台数据,我都建议先不要急着全量训练。rumorDataset.py的数据加载入口通常是一个data_dir参数,你可以在里面临时加一个max_samples参数,先只加载 20 到 30 个事件,跑一个 epoch,确认数据形状、损失计算、模型前向这三步都不会报错,再放开全量数据。
我的做法是先用codetest.ipynb做单 batch 调试。这个 notebook 在压缩包里正好对应代码测试场景,把数据集切出一个 batch,断点打在MSABiGCN.forward里,检查token_embeds的 shape 和adj的 shape 是否匹配,再确认rumor_logits和stance_logits的类别数是否和标签一致。这一步能把大部分维度问题挡在训练之前,而不是等机器跑了几百步之后才炸。
全量训练时,先固定随机种子,再按trainOnlyRumor.py和trainOnlyStance.py分别拿到单任务基线,最后跑train.py对比多任务效果。如果时间紧,可以把epochs从 30 减到 15,观察多任务是否真的比单任务有提升。我还习惯把 loss 曲线和有代表性的注意力热力图保存下来,用plot.ipynb渲染成图片,论文或报告里直接能用。从那以后我每次拿到新的数据集和模型代码,都强制先走一遍小样本冒烟测试,再决定要不要挂上全量训练,这个过程帮我省下的调试时间远比想象中多。希望这份拆解能帮你少踩几个坑,顺利把项目跑起来。
本文还有配套的精品资源,点击获取