文本分类大概是NLP里最“平平无奇”的任务了。给一段文字打一个标签,听起来简单,做起来却有一堆问题:短文本信息量太少,长文本关键信息分散,领域数据标注又贵又慢,标签还不均衡。我见过不少团队在这个任务上一上来就上BERT,再往上堆更深的模型,可数据集总共只有一两万条,算力没少烧,精度却卡在某个点上不去。所以当我第一次看到那篇用图卷积网络做文本分类的论文时,最大的感觉不是“这也能行”,而是“为什么我没早想到这个角度”。它不预训练、不堆词向量、不用注意力,而是把所有文档和词放进一张大图里,让信息沿着文档与词、词与词之间的边流动起来,最后用图卷积网络完成分类。这个方法有个很直观的名字:TextGCN。很多没用过图模型的读者可能觉得陌生,但它的思路其实不难,而且在小规模、标注稀少的文本数据上,至今仍是一个非常可靠的基线。这篇博文,我想把那篇论文的动机、图构建、公式直觉和复现经验一次性讲清楚。
1. 为什么文本分类要用图卷积网络:先看懂这篇论文的动机
1.1 传统文本分类器的盲区:局部特征与词序之外的信息
文本分类最常用的几类模型,思路本质上都是“从文档内部找证据”。词袋模型统计词频,但会丢掉词序;卷积神经网络能抓n-gram局部模式,可窗口始终有限;循环神经网络和LSTM能按顺序建模,但长距离依赖依然吃力。它们的共同特点是:一篇文档就是一条独立样本,文档和文档之间、词和词在整个语料库里的共现关系,并没有被显式建模。
可文本分类里很多分类证据其实是跨文档的。比如某一类文档里经常同时出现“利好”和“估值”这两个词,另一类文档里经常同时出现“疗效”和“入组”这两个词。如果只看单篇文档,你很难发现这种稳定的词语搭配模式;但如果把整个语料库放到一起看,这些词与词之间的强共现关系就非常明显。传统模型没有直接利用这种全局语料结构的机制,而图卷积网络正好擅长这件事——它天然就是为处理“节点与节点之间的关系”设计的。
1.2 这篇论文的核心观点:文档和词汇都是图上的节点
TextGCN构建的图不是文档内部的关系图,而是把整个语料库当成一张大图。每个文档是一个节点,每个不同的词也是一个节点。文档与词之间如果有包含关系,就建立一条边;词与词如果在整个语料库中经常在滑动窗口内一起出现,也建立一条边。这样的图建好以后,一个文档节点经过图卷积传播,能逐层感知到与自己相关的词,而那些词又连接到其他词和其他文档,信息就会沿着边扩散出去。
为什么选图卷积网络而不是更简单的标签传播或矩阵分解?因为GCN适合做节点级别的分类预测,它通过可学习的权重矩阵把邻接信息转换为自适应的节点表示,训练时可以利用有标签节点去监督整个图上的消息传播。换句话说,它不只是把图结构当先验规则用,而是把结构融进了端到端的训练过程里。这个设计在当年看起来是“把NLP问题变成了图学习问题”,在现在来看,其实打通了文本建模和关系建模之间的一条路。
2. 词-文档异构图到底怎么建:TextGCN的图构建细节
2.1 节点类型与两类边关系
TextGCN的图里一共有两类节点:文档节点和词节点。文档节点数量等于语料库中的文档总数,词节点数量等于词表大小。边也分两类,每一类都有不同的权重计算方式。
第一类是文档-词边。一篇文档如果包含某个词,就在对应节点之间连一条边,边的权值用TF-IDF,也就是词频乘以逆文档频率。TF-IDF的含义是:一个词在一篇文档里出现次数越多、同时在全局文档里出现越少,那么它对这篇文档的区分度就越高。比如“模型”这个词在AI文档里到处都有,区分度很低;“过拟合”在AI文档里也很常见,但在其他文档里出现少,区分度就明显高。
第二类是词-词边。两个词如果在整个语料库中出现在同一个滑动窗口内,就建立一条边,权值用PMI(点互信息)计算。这里的直觉是:词和词之间的语义关联,应该由它们在整个语料库里的共同出现模式决定,而不是由某个单篇文档决定。无论是文档-词边还是词-词边,每个节点最后都要加上一条指向自己的自环边,对应到邻接矩阵里就是加一个单位阵,目的是让节点在卷积时保留自己原有的特征,而不是被邻居信息完全覆盖。
2.2 为什么词-词边要用PMI而不是共现次数
这是论文里一个容易被忽略但非常关键的细节。如果直接统计两个词共同出现的次数,高频词会形成大量虚假的强关联。举个例子,“数据”和“的”同时出现在很多文档里,直接看共现次数,它们的关系可能比“数据”和“模型”还要强,但谁都知道“的”是个几乎没有语义贡献的虚词。PMI相当于做了一次“概率校正”。
PMI的计算逻辑是:比较两个词同时出现的实际概率,和它们各自独立出现概率的乘积。如果实际同时出现的概率明显高于独立假设下的期望值,说明这两个词之间确实有超出随机水平的关联,PMI为正;如果接近零或为负,说明共现只是巧合或高频造成的假象。TextGCN通常只保留PMI为正的词-词边,这样留下来的基本都是真正的语义搭配,比如“模型”和“训练”、“性能”和“指标”这类强关联对。这个细节决定了词-词边到底是在传语义信号,还是在传噪声。
2.3 滑动窗口大小与词表控制
词-词边捕捉的是共现关系,但“共现”到底怎么定义,取决于滑动窗口大小。窗口太小,只能抓到相邻词,远处的搭配关系丢失;窗口太大,会把大量无关词也拉进连接,图变得极其稠密,噪声增多。论文中常见的滑动窗口设为20,我复现时试过15到25之间,结果差别不大,但如果把窗口直接缩到8或更小,准确率会有肉眼可见的下降。窗口这个超参数直接控制着图里的边数量,也会影响内存消耗和训练速度,所以它不是随便拍的。
词表也需要控制。原始语料里的词表往往有几十万甚至上百万,全部作为节点会让图非常庞大,而且低频词带来的边噪声很大。实际复现中通常要做一轮基础预处理:去掉数字、停用词,再过滤掉出现次数低于2次的低频词。这一步不是论文独创,而是文本预处理里的常见实践,但它对最终效果的影响非常大。经过词频过滤后,词节点数量可能直接减少一半以上,图的稀疏性会健康很多。
3. GCN层公式拆解:两层传播如何完成文档分类
3.1 核心公式:归一化邻接矩阵与两层传播
图卷积网络每一层的传播可以写成下面这个简洁的式子:
H^(l+1) = σ( D^(-1/2) A D^(-1/2) H^(l) W^(l) )
其中A是加入自环后的邻接矩阵,D是度矩阵——也就是每个节点有多少条边组成的对角矩阵。H^(l)是第l层的节点表示矩阵,W^(l)是这一层可学习的权重,σ是激活函数,这里用ReLU。最后一层再接一个softmax,输出每个文档节点属于各个类别的概率。
用一句话概括:每个节点的新表示,等于自己旧表示加上所有邻居节点旧表示的加权聚合,权重由归一化邻接矩阵决定,再经过一次线性变换和非线性激活。H^0是输入特征矩阵,论文里用的是one-hot编码,也就是每个节点用一个独立的维度表示自己,所以特征矩阵的维度是(文档数+词数)×(文档数+词数)。这个输入看起来“原始”,但它和图传播配合起来效果很好,因为图卷积本身已经在完成表示学习的工作,不需要额外的预训练向量。
3.2 用生活类比理解归一化:为什么不能直接平均邻居
可能有人会问,D^(-1/2) A D^(-1/2)这一串到底在做什么?能不能直接拿邻接矩阵乘以特征矩阵?答案是不能。因为不同节点的邻居数量差异太大了。一个词可能同时连接几千篇文档,另一个词可能只连接两三篇文档。如果不做归一化,那些“超大规模节点”会主导整个传播过程,让其他节点的信息被淹没。
用一个生活类比来理解:粉丝越多的博主,单条内容对全网热度的影响反而应该被稀释;粉丝很少的普通用户,一旦发了一条内容,它对于这个用户自身的影响力是巨大的。D^(-1/2) A D^(-1/2)做的事情,就是按度数给每条边“打折”,让每个节点在聚合邻居时既尊重邻居的贡献,又不至于被局部大节点劫持。这种对称归一化比简单除以度更稳定,也是图卷积网络里很经典的工程选择。
3.3 为什么两层够用,层数多了反而不行
第一层GCN让文档节点聚合了自己包含的词,词节点聚合了与自己共现的词。第二层继续聚合邻居的邻居,此时文档节点已经能感受到“自己文档里的词”所关联到的“更远的词”,这等于引入了两层之外的间接共现证据。对文本分类来说,这个感受野已经足够覆盖短文本和中等长度文本的分类需求。
那为什么不多堆几层?因为GCN有一个著名的过度平滑问题。层数越多,每个节点的表示越趋向于整个图的平均状态,节点与节点之间的区分度快速下降。我在实际调参中体会特别明显:从两层升到三层,验证集损失经常反而推不低,准确率也会掉。所以论文选择两层,不是省算力,而是平衡了表示能力和可分性之后的结果。这个经验对后来做任何图模型的项目都有参考价值——图模型不是越深越好。
4. 实验数字背后的门道:数据集、消融与超参数设置
4.1 评测数据集的选取逻辑
论文在四类比较有代表性的标准小规模数据集上做了评估:新闻分类(两个不同规模的子集)、医学文献摘要分类、电影评论情感二分类、多主题分类数据集。这四类数据覆盖了长文本和短文本、主题分类和情感分类,能够比较全面地展示模型的适用边界。
我在复现时最大的感受是:TextGCN在新闻分类和主题分类这类“文档间词汇重叠模式明显”的任务上优势突出,而在电影评论这类对词序和修辞风格更敏感的短文本情感任务上,优势并不明显。这不是说模型不行,而是不同任务的信息来源不同。如果任务的关键信号是“这篇文档属于哪个主题”,全局共现结构非常有用;如果任务的关键信号是“这个句子是褒义还是贬义”,局部词序和语法反而更重要。
4.2 关键结果:与CNN、LSTM的对比
论文里的对比结果大致可以整理成下面这种感觉:
| 任务类型 | TextGCN | CNN/LSTM基线 | 我的观察 |
|---|---|---|---|
| 新闻分类(大规模子集) | 97%上下 | 通常低一到两个点 | 优势明显,全局共现帮助大 |
| 新闻分类(中规模子集) | 93%上下 | 通常低两到三个点 | 优势依然存在但差距缩小 |
| 医学文献摘要分类 | 68%上下 | 相近或略高一点 | 长文本下优势变弱 |
| 电影评论情感二分类 | 77%上下 | 与LSTM基本持平 | 词序信息无法被图结构替代 |
这里的数字是论文公开报告的数量级,也是我复现时能对齐的经验值。结果说明TextGCN并不是一个“全面碾压”的模型,它的强项是挖掘语料库级别的全局共现信息。这样的结论在论文里反而显得更可信——如果一个模型在差异巨大的任务上都是碾压性的优势,那多半是评估设置出了问题。
4.3 消融研究:去掉词-词边会发生什么
论文里最有价值的实验是消融研究。如果把词-词边全部去掉,只保留文档-词边,TextGCN的表现会大幅下降。这说明词与词之间的共现关系才是性能的主要来源。文档-词边只是把文档和自身包含的词连接起来,本质上相当于一种“可学习的TF-IDF加权聚合”;而词-词边引入了文档之外的间接语义联系,等于让每篇文档都能借力整个语料库的词汇结构。
如果再把文档-词边的权重从TF-IDF换成单纯词频,效果同样会变差。两个消融结论放到一起看,就能发现图结构和边权设计是一个整体机制:节点划分决定信息能从哪里来,边的权重决定信息应该被多大程度信任,卷积层数决定信息能传多远。缺少任何一环,整个模型的性能都会迅速衰减。
5. 复现踩坑实录:从建图、训练到推理的一整条经验链
5.1 第一步坑:邻接矩阵太大,如何用稀疏矩阵救场
我复现时第一反应是用numpy建一个二维矩阵来当邻接矩阵,结果在几万文档、几万词表的数据集上直接内存爆炸。这是因为全图节点数等于文档数加词数,可能有十万量级。如果把这十万个节点存成十万乘十万的密集矩阵,即使每个元素只占8个字节,也需要约80GB内存,个人电脑基本不可能跑得动。
正确做法是全程使用稀疏矩阵。思路很简单:先扫描整个语料库,把文档-词边和词-词边统计成一个三元组列表,也就是“边的起点、边的终点、边的权重”,最后一次性组装成稀疏矩阵。Python里可以用SciPy的coo_matrix或者csr_matrix来处理。下面是一段简化版流程:
from scipy.sparse import coo_matrix n = num_docs + num_words rows = [] cols = [] data = [] # 1. 文档-词边:遍历每篇文档的TF-IDF权重 for doc_id, tfidf_dict in doc_tfidf.items(): for word_id, weight in tfidf_dict.items(): rows.append(doc_id) cols.append(num_docs + word_id) data.append(weight) # 2. 词-词边:遍历滑动窗口共现,用PMI权重 for word_i, word_j, pmi in word_cooccur_pmi: if pmi > 0: rows.append(num_docs + word_i) cols.append(num_docs + word_j) data.append(pmi) # 3. 加入自环 for i in range(n): rows.append(i) cols.append(i) data.append(1.0) adj = coo_matrix((data, (rows, cols)), shape=(n, n)).tocsr()稀疏矩阵按边数存储,几百万条非零元素在实际计算中只是几十MB到几百MB的规模,常规机器完全没有压力。
5.2 第二步坑:训练时哪些节点算 loss,哪些节点只参与传播
TextGCN是半监督学习。构建图的时候,会把包括测试集文档在内的所有节点都放进图里,但损失函数只能计算有标签的训练集文档节点。很多刚接触图模型的初学者容易在这里犯迷糊,直接把所有文档的预测结果都拿去算交叉熵,这样会悄悄把测试集标签信息带进训练,得到的指标虚高,部署时立刻露馅。
正确做法是用一个mask矩阵。先标记哪些节点有标签,只在有标签的训练节点上计算损失;测试集文档虽然没有标签,但它们的节点特征和它们与词的连接关系在训练时已经被图传播利用到了。这个设定叫转导式学习,它利用了无标注文本的结构信息,但并没有利用测试集标签,不算泄露。唯一需要注意的是,向团队里非图模型背景的同事解释时必须说清楚,否则很容易被误判为“作弊”。
5.3 第三步坑:调参起点与训练稳定性
GCN的训练比普通文本模型敏感得多,随机种子、学习率、dropout、正则系数都会影响最终结果。我自己的调参起点是:学习率0.02,dropout 0.5,L2正则系数1e-4,两层隐藏层维度200,训练200轮左右。如果loss震荡不稳,把学习率降到0.01;如果验证集过拟合,调大正则系数。
另外,用one-hot作为初始特征只是最简单的一种选择。如果你想加入预训练词向量,可以把词节点的特征换成词向量,文档节点用包含词的平均向量来初始化,这样输入特征矩阵的维度就不是节点总数,而是一个固定向量维度。这个改进不算复杂,但注意它不是原文的做法,我建议先完整复现原文,再在这个基础上做扩展,否则出了问题很难定位是结构的问题还是特征的问题。
5.4 第四步坑:推理阶段新样本怎么处理
TextGCN在标准实验里是转导式模型,训练阶段就已经把所有文档节点放进图里了。对于完全没见过的文档,严格来说不能直接复用训练好的模型做单条预测,因为新文档不在图里,也没有连接到任何词节点。正确的做法是把新文档作为新节点插入已经训练好的图中,重新做一次传播,再读它的预测结果。
这意味着TextGCN不适合毫秒级单条请求的在线推理场景,更适合批处理,或者配合动态图更新机制使用。论文里没有花太多篇幅强调这一点,但工程化的时候它非常关键。如果业务场景必须实时预测单条文本,需要额外设计图采样和图更新策略,复杂度会明显上升,这一点选型前要想清楚。
6. 站在今天的视角看TextGCN:价值与局限
6.1 为什么这篇论文在当时能引起关注
在预训练语言模型还没有大规模普及的年代,TextGCN用一套非常“轻”的方案证明:不依赖外部语料、不依赖大规模算力,一个精心设计的图结构就可以在多个文本分类任务上超过深度序列模型。它给当时做NLP的开发者打开了一个新思路——不是所有任务都需要堆更深的模型,把数据之间的关系结构造好,效果可能更直接。这也是它后来成为图神经网络用于文本分类的代表作之一的原因。
6.2 与预训练语言模型的关系:可以互补,不是替代
今天大家做文本分类大概率直接上预训练模型,但这不代表TextGCN没有价值。预训练模型擅长建模词序、语法和上下文,但它通常把数据当成一批独立样本处理;TextGCN擅长建模样本之间、词项之间的全局关系。两者其实是互补的。实际项目里可以结合:先用预训练模型得到文档向量,再用这些向量构造图、跑GCN做二次分类;或者把TextGCN作为小规模数据场景下的强基线。很多后续改进工作,比如在文本图上加入注意力机制、层次结构、动态时间信息,源头都可以追溯到这篇论文。
6.3 可扩展方向与个人体会
从TextGCN出发,可以扩展的方向很多:多标签分类、短文本社交数据、流式文本、跨语言迁移、小样本场景等。它的主要局限在于建图成本和转导式设定,但这恰恰也是图神经网络最活跃的研究方向之一。我做实际项目时,最常用的方式是把它当成一个快速基线:如果一个小型数据集连TextGCN都跑不过,那预训练模型的收益大概率也很有限,与其盲目堆模型,不如先分析数据关系结构。复现这篇论文最大的收获,并不是学会了一个模型,而是建立了一种习惯:遇到文本分类任务,先想想数据里有哪些天然的关系可以构成图,再决定用什么模型。这种“低成本先验判断”的思维,比单纯追某个任务的SOTA要实用得多。