TextGCN、TextING与LEAM复现对比:图神经网络文本分类的三种路径
简介面向自然语言处理期末大作业提供TextGCN、TextING、LEAM三种经典文本分类算法的完整复现代码源码与注释并重适合计算机、人工智能、自动化等专业学生用于课程设计、毕业设计或自然语言处理实践进阶。压缩包共91个文件以32个Python脚本为核心覆盖数据清洗、词向量生成、图构建、模型层定义、训练评估与可视化全流程同时包含5个Jupyter演示、10个PDF文档、2个词向量数据及权重等文件整体约805MB。已有437人学习下载该大作业答辩评审平均分达96分代码经测试运行成功。资源按三种模型分目录组织每种方法均提供独立可运行的训练脚本及建图、评估、模型等公共工具便于对照论文复现实验、替换数据集或调整超参数。详细注释配合说明文档、可视化输出与运行日志能系统理解文本图卷积、邻居聚合和深层注意力机制的实际实现描述中还提供运行咨询与远程教学对缺少项目经验的学习者尤为友好。1. 期末复现为什么选这三篇TextGCN、TextING 与 LEAM 的差异文本分类的期末大作业最怕的不是写代码而是选错复现对象。选太简单的模型答辩没内容选太重的模型训练跑不动。TextGCN、TextING、LEAM 这三篇放在一起恰好覆盖了图神经网络建模文本的三条典型路径TextGCN 把整个语料库拼成一张异构图TextING 退回到每篇文档内部的局部词图LEAM 则完全不用图结构把标签文本嵌进词向量空间做交互。三条路线互相对照答辩时可以从“全局图 vs 局部图 vs 无图”这个维度讲清楚各自的取舍这正是评审老师最愿意听到的对比深度。源码里三套模型共用一套数据预处理和评估接口能直接跑出对比实验对期末大作业来说性价比很高。2. TextGCN 复现链路构图、GCN 层与训练脚本2.1 先理清 TextGCN 的构图逻辑TextGCN 的核心思想很直接把整个训练语料库包含测试集文档构造成一张异构图节点是词和文档边则分两类——文档到词的边用 TF-IDF 加权词到词的边用 PMI点互信息加权。这个构图动作在build_graph.py中完成边权计算是整条复现链路的第一道坎。词与词的 PMI 计算为什么要用滑动窗口而不是直接数共现次数因为原始频次会偏向高频词而 PMI 能衡量两个词在多大程度上“非随机地”共同出现。源码里compute_pmi的核心逻辑是先统计每个词在窗口内出现的频次、每对词在窗口内共现的频次再按公式计算。常见窗口大小是 20但短文本数据集建议缩到 10 以下否则长距离共现会产生大量噪声边GCN 消息传递时会把不相关的词义混在一起。我一般会先在验证集上扫一遍窗口参数看准确率的波动幅度再定。文档到词的 TF-IDF 边在prepare_data.py里生成注意这里必须和build_corpus.py产出的语料顺序完全一致。源码中构图时用的是稀疏矩阵存储因为词表动辄几万、文档几千稠密邻接矩阵在 16GB 内存的机器上很容易直接 OOM。复现时如果报内存错误优先检查邻接矩阵是否不小心被to_dense()了。2.2 GCN 层与模型实现TextGCN 的模型结构只有两层 GCNlayers.py里定义了GraphConvolution层models.py里组装成完整模型。核心前向传播代码逻辑如下class GraphConvolution(nn.Module): def __init__(self, input_dim, output_dim, support, dropout0., actNone): super(GraphConvolution, self).__init__() self.support support self.dropout dropout self.act act self.weights nn.ParameterList() for i in range(support): self.weights.append(nn.Parameter(torch.FloatTensor(input_dim, output_dim))) def forward(self, x, adj): x F.dropout(x, self.dropout, trainingself.training) supports [torch.mm(adj[i], x) for i in range(self.support)] output sum(torch.mm(support, self.weights[i]) for i, support in enumerate(supports)) if self.act is not None: output self.act(output) return output这里的support参数决定聚合几阶邻居信息。support1时只利用一阶邻接矩阵support2时传入一阶和二阶两个邻接矩阵在两个矩阵上分别做adj[i] x再拼权重。两层 GCN 叠加后模型能捕获到“词→文档→词”的二阶传播路径这正好覆盖了文本分类中常见的同义词替换场景——两个不同的词通过同一篇文档产生关联。注意torch.mm是稠密矩阵乘法如果邻接矩阵是稀疏的这里会出现性能瓶颈。复现时我通常建议至少对adj[i] x这一步改用torch.sparse.mm尤其是在 20NG 这种语料规模下能省下近一半的训练时间。模型第一层 GCN 的输出维度论文给的是 200第二层直接映射到分类类别数。源码里models.py对两层 GCN 之间还插入了 ReLU 和 dropoutdropout 默认 0.5这个值在中小规模数据集上基本不用调。2.3 训练脚本与核心参数train.py是 TextGCN 的入口训练时按论文默认配置给定超参数。命令行启动方式通常是python train.py --dataset 20ng --epochs 200 --lr 0.02 --hidden 200 --dropout 0.5--dataset指定数据集名称源码内置了 20ng、R8、R52、Ohsumed 等常见文本分类基准。--epochs论文是 200实测中 R8、R52 这类小数据集 80 轮左右就能收敛20ng 需要跑满 150 轮以上。--lr0.02 是 Adam 优化器下的经验值改成 0.01 会略微稳定但收敛变慢。训练过程中每轮会输出 loss 和验证集准确率最终在测试集上评估。几个参数之间的联动关系值得注意参数建议范围影响window size10~20过小丢语义过大引入噪声边hidden100~300过小欠拟合过大无明显收益dropout0.3~0.6防过拟合小数据集偏高lr0.005~0.02过高震荡过低收敛慢复现时最容易忽略的是remove_words.py的过滤逻辑。源码会对低频词做裁剪默认保留词频大于 5 的词。这个阈值直接影响图规模阈值太低图里全是低频噪声节点阈值太高很多类别专属词被删掉分类准确率直接掉两三个点。我一般会在build_graph.py跑完后打印一下节点总数和边总数和词表大小对照判断过滤是否合理。3. TextING 复现局部词图的消息传递与 BiLSTM 序列分支3.1 TextGCN 的遗留问题与 TextING 的对策TextGCN 有一处设计在复现时很难绕开它必须用整个语料库构图一旦来了新文档需要重新构建整张图才能做预测。这在期末大作业答辩时很容易被问“模型能否在线推理”。TextING 论文正是抓住这个痛点把 GCN 的作用域从全局图缩小到每篇文档内部的局部词图——每个文档用自己的单词构成一张子图模型在子图上迭代更新节点表示不依赖语料库整体结构。这个差异带来的好处不只是可增量推理更重要的是避免了 TextGCN 里“训练集和测试集文档同时出现在一张图中”的潜在信息泄露。TextING 的图只由当前文档的词构成测试时不需要任何训练语料的全局统计信息。复现时把这两点讲清楚答辩的深度就出来了。3.2 局部词图的构建与批次化实现TextING 的构图代码在build_graph.py中逻辑比 TextGCN 简单对每篇文档取出去重后的词序列两两之间建立边边权设为 1。没有 TF-IDF没有 PMI因为局部图足够小不需要加权就能稳定传播信息。关键是这个图是逐个文档构建的训练时怎么把几千个独立小图拼成一个 batch源码的做法是构建分块对角邻接矩阵每个文档子图作为对角线上的一个块块间无连接然后一次性对整批文档做 GCN 前向传播。示意代码如下def build_batch_graph(seq_lists, max_len): # seq_lists: list of list每项是一篇文档去重后的词索引序列 batch_size len(seq_lists) adj_block torch.zeros(batch_size, max_len, max_len) for i, seq in enumerate(seq_lists): for w in seq: for v in seq: if w ! v: adj_block[i, w, v] 1.0 return adj_blockmax_len是当前 batch 内最长文档的词数其余文档在邻接矩阵尾部补零。这种实现把稀疏图存成了稠密的三维张量内存开销不小但换来的是 GPU 上可以并行计算。如果文档平均长度差异很大我会按长度对样本排序再分 batch这样每个 batch 的max_len不至于被个别长文档拉得太高显存利用率明显提升。3.3 模型主体与词序分支TextING 的模型在models.py中实现整体分两条分支图分支在局部图上做两轮 GCN 消息传递序列分支用 BiLSTM 编码原始词序。两条分支的输出在最后拼接后接分类层。核心更新逻辑如下class LocalGraphEncoder(nn.Module): def __init__(self, input_dim, hidden_dim): super(LocalGraphEncoder, self).__init__() self.gcn1 GraphConvLayer(input_dim, hidden_dim) self.gcn2 GraphConvLayer(hidden_dim, hidden_dim) def forward(self, x, adj): h self.gcn1(x, adj) # 第一轮消息传递 h F.relu(h) h self.gcn2(h, adj) # 第二轮消息传递 return h注意这里每一轮 GCN 后没有像 TextGCN 那样加 dropout因为局部图的边本身就稀疏再加 dropout 会让信息传播不充分。复现时如果发现准确率上不去先检查模型里是不是多了个额外 dropout。图中节点的初始表示用的是预训练词向量源码里通过pretrained_emb参数传入如果没有预训练向量模型也会随机初始化一路训练到底效果会差 3~5 个点但不至于崩。训练 TextING 时的经验是学习率要比 TextGCN 低一个量级论文默认 0.001Adam 优化器加上 L2 正则系数 1e-4。局部图小、参数量不大过拟合是主要风险尤其在小数据集上验证集准确率容易出现震荡建议加载在验证集上表现最好的 checkpoint 而不是最后一轮权重。4. LEAM 复现标签嵌入与注意力融合的两阶段流程4.1 LEAM 解决的问题与标签嵌入设计LEAM 是这三个模型里思路最不一样的一个。它不做图而是重新思考了一个问题文本分类时为什么不能把“类别标签本身”也当作输入的一部分比如一篇关于体育的新闻标签词 “sport”“game”“player” 本身就携带了语义线索。LEAM 的核心贡献是 JWEJoint Word and Label Embedding框架——把标签文本投影到和词向量同一个空间然后让文档中的每个词和标签向量做注意力交互。这个设计在复现时的直接收益是模型有可解释性。训练结束后可以把注意力权重可视化出来看看哪些词把文档“拉向”了某个标签。源码里plots目录和visualize.py就是干这个的。答辩时展示一张词级注意力热力图比放十条训练日志有说服力得多。4.2 标签文本的嵌入生成与 glove_generate.pyLEAM 依赖 GloVe 预训练词向量glove_generate.py负责把原始 GloVe 文件转换成语料库词表对应的向量矩阵。源码要求先把glove.6B.200d.txt下载后放到指定目录再运行python glove_generate.py --glove_dir data/glove --output_dir data/emb脚本会读取语料库全部词条从 GloVe 里挑出命中词构成嵌入矩阵同时输出一个word2idx映射。没命中的词统一初始化成一个很小的随机向量。转换完成后main_multiclass.py训练时会加载这个矩阵作为模型嵌入层的初始权重。标签文本的嵌入由generate_emb.py完成。每个类别先人工维护一个标签短语列表比如体育类写成[sport, game, athlete]脚本把这些标签短语中每个词的向量取平均得到该标签的固定向量。注意标签向量是训练前离线算好的训练过程中不更新。文本中的词向量则参与梯度更新两者在每次前向时做点积得到交互矩阵。4.3 训练流程与多分类支持LEAM 的主训练脚本是main_multiclass.py支持多分类数据集。训练流程中模型对每个样本先计算交互矩阵再对标签维度做 softmax 归一化得到注意力权重加权求和词向量后接全连接分类。核心注意力模块实现如下class LabelAttention(nn.Module): def __init__(self, embed_dim, num_labels, label_emb): super(LabelAttention, self).__init__() self.embed_dim embed_dim self.num_labels num_labels self.label_emb nn.Parameter(torch.tensor(label_emb), requires_gradFalse) def forward(self, word_emb): # word_emb: (batch, seq_len, embed_dim) # label_emb: (num_labels, embed_dim) attn torch.matmul(word_emb, self.label_emb.t()) # (batch, seq_len, num_labels) attn F.softmax(attn, dim1) # 对词维度归一化 fused torch.matmul(attn.transpose(1, 2), word_emb) # (batch, num_labels, embed_dim) return fused, attn代码中torch.matmul两次调用的含义是第一次计算每个词对每个标签的相似度得分得到交互矩阵第二次用归一化后的权重对词向量做加权求和得到每个标签视角下的文档表示最后把这组表示展平或池化后输入分类器。label_emb设置requires_gradFalse表示标签向量是冻结常量这个细节很关键——如果不动它反传到标签向量上的梯度会把标签语义拉偏。训练时 LEAM 的收敛速度明显快于 TextGCN 和 TextING通常 30~40 轮就能达到最佳验证集表现。学习率设置 0.001batch size 32~64 均可。复现时如果出现 loss 不下降优先检查glove_generate.py输出的词表覆盖率和预训练向量是否成功加载标签嵌入的初始化是 LEAM 最敏感的部分。5. 训练验证与参数调优的实操路径5.1 环境与依赖的版本选择三套代码共用 PyTorch 环境实测 Python 3.7~3.9 均可运行。依赖包集中在requirements.txt或 README 中核心是 torch 1.8、numpy、scikit-learn、tqdm。LEAM 还需要 gensim 生成文档向量。图相关的库如 torch-geometric这三套源码都没有用到邻接矩阵和稀疏运算全部手写降低了环境配置门槛。安装依赖时只要能正常import torch基本不会有版本冲突。5.2 数据预处理的关键差异TextGCN 和 TextING 共用remove_words.py做清洗流程是小写化、去数字和标点、去停用词、按词频过滤。LEAM 的preprocess_yahoo.py另起一套流程保留了更多原始信息因为标签嵌入需要完整的词形来匹配 GloVe 词表。三套模型在同一份原始数据集上跑出不同结果时根源往往不是模型本身而是预处理差异。复现时需要把每套代码的预处理单独跑一遍保存词表确保三份实验数据口径一致。5.3 常见失败点与验证建议表格式排查清单现象可能原因处理动作TextGCN 构图 OOM稠密邻接矩阵检查是否有to_dense()改用稀疏存储TextING 准确率偏低局部图加额外 dropout删除图分支的 dropout 层LEAM loss 不降GloVe 词表覆盖率低检查word2idx未命中词占比三种模型结果都差数据预处理不一致统一清洗流程并固定随机种子验证模型是否跑对的一个技巧是先用 R8 这种小数据集做烟雾测试三个模型各跑 10 轮确认 loss 在下降再切换到 20ng。另外每套源码都带了metrics.py统一输出准确率、宏平均 F1 和微平均 F1。做对比实验时记录这三个指标即可不用额外改评估逻辑。最终答辩把三份结果做成一张表格配合 LEAM 的注意力可视化图就是一份完整可查的复现链路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →