YAOTU INSIGHTS

句法信息注入Transformer:可学习距离偏置的软约束方案

句法信息注入Transformer:可学习距离偏置的软约束方案
1. 句法信息注入Transformer的动机与整体思路1.1 为什么要在Transformer里塞进句法树Transformer架构从2017年提出之后迅速在自然语言处理的各个任务上铺开机器翻译、文本分类、问答系统、语言模型几乎每个方向都在用。它的核心是自注意力机制每个词都能直接和序列中任意位置的其他词计算关联权重不受距离限制。这个特性让它在捕捉长距离依赖上比循环神经网络强出一大截训练时还能并行计算速度优势非常明显。但用久了就会发现一个问题自注意力学到的关联模式和人类理解语言时依赖的句法结构并不总是一回事。举个例子“那只追着猫跑的小狗终于停了下来”这句话里“小狗”是主语“停了下来”是谓语中间隔了很长一段修饰成分。自注意力理论上能通过多层堆叠学到这种主谓关系但它需要大量数据去隐式地拟合而且学到的注意力分布未必和句法树对齐。换句话说模型有能力学到句法但没有被明确告知句法是什么。这就引出了一个很自然的研究方向能不能把句法树的结构信息显式地注入到Transformer里让模型不用从零开始摸索句法关系这个思路在循环神经网络时代就有大量工作比如把句法树的路径编码进隐状态或者用树结构约束信息流动。到了Transformer时代问题变成了自注意力本身是一个全连接图怎么把一个树结构嵌进去同时不破坏它原有的并行性和表达能力这篇论文给出的方案核心思路是在自注意力的计算过程中引入一个基于句法树的距离偏置。不是硬性地把注意力限制在树的边上而是把树中两个词之间的路径长度转化成一个可以加到注意力分数上的偏置项。这样模型仍然能看到全序列但句法上靠近的词会获得额外的注意力倾向。这个设计的好处是既保留了Transformer的灵活性又给了模型一个句法上的“先验提示”。1.2 整体方案的设计哲学这个方案的设计哲学可以用一句话概括软约束优于硬约束。所谓硬约束就是直接把注意力矩阵中不符合句法树的位置mask掉强制模型只能沿着树的边做注意力。这种做法在依赖句法树质量的任务上可能有效但一旦句法分析器出错错误会直接传播到下游而且模型失去了捕捉句法之外关联的能力。软约束的做法则是在注意力分数上加一个偏置句法上近的加正偏置远的加负偏置但所有位置仍然可访问。模型可以根据任务需要自己决定多大程度上依赖这个偏置。如果句法信息有用模型会学会放大这个偏置的影响如果句法信息有噪声模型也可以学会忽略它。这种设计在工程上更稳健也更符合“让模型自己学”的深度学习哲学。另一个关键设计是偏置的计算方式。论文没有直接用句法树上的最短路径长度作为偏置而是把路径长度映射到一个可学习的嵌入向量。具体来说路径长度被离散化到若干个桶里每个桶对应一个可学习的标量偏置。这样模型可以自己学习“路径长度为1时偏置应该是多少”“路径长度为2时又该是多少”而不是人为设定一个固定的衰减函数。这个细节很重要因为不同任务对句法距离的敏感程度不一样可学习的偏置让方案能适应不同场景。2. 核心细节解析与实操要点2.1 句法树的获取与预处理要注入句法信息第一步是拿到句法树。论文里用的是依存句法分析器对每个输入句子生成一棵依存树每个词有一个父节点和一条依存弧标签。依存树的好处是它和词序列一一对应每个词恰好有一个父节点根节点除外路径长度的计算比较直接。实际操作中句法分析器的选择会影响最终效果。论文里用的是现成的依存分析器没有自己训练。这里有一个经验如果你做的任务有标注好的句法树比如某些语言有树库那直接用金标准句法树效果最好。但大多数下游任务没有句法标注只能用自动分析器。自动分析器的准确率通常在90%到95%之间意味着每句话里可能有一两个词的父节点是错的。软约束的设计在这里就体现出优势了个别错误不会导致整个注意力矩阵崩掉。预处理阶段还需要把句法树转换成距离矩阵。对于长度为n的句子需要计算一个n乘n的矩阵每个元素是词i和词j在句法树上的最短路径长度。这个计算可以用树的最近公共祖先算法来做时间复杂度是O(n^2)对于常规句子长度几十个词完全够用。如果句子特别长可以考虑用一些近似算法但论文里没有涉及这个场景。注意句法分析器对分词很敏感。如果你的任务是中文分词边界和句法分析器的分词边界必须一致否则词对不上距离矩阵就错了。我试过用不同分词工具混搭结果路径长度全乱模型效果直接掉到基线以下。2.2 距离到偏置的映射机制拿到距离矩阵之后下一步是把距离映射成偏置。论文的做法是设置一个最大距离阈值比如10超过这个阈值的距离统一当作10处理。然后为0到10这11个距离值各分配一个可学习的标量参数。这些参数和模型的其他参数一起训练初始化通常是零或者小的随机值。为什么用桶而不是连续函数因为句法距离的分布是长尾的大部分词对的路径长度在1到5之间超过10的很少。如果用一个连续函数比如指数衰减你需要调衰减率这个超参数调不好效果就差很多。用桶的话每个距离的偏置独立学习模型自己决定怎么分配。而且桶的数量是一个可控的超参数桶太少表达力不够桶太多参数多且容易过拟合。论文里用了10个桶这个数字在大多数任务上够用。偏置加到注意力分数上的方式也很关键。标准自注意力的分数是query和key的点积除以根号d然后softmax。加偏置就是在softmax之前把偏置加到分数上。这样偏置会影响softmax的分布正偏置让对应位置的注意力权重变大负偏置让它变小。注意这个偏置是加在分数上的不是乘在权重上的所以它不会改变注意力的归一化性质。2.3 多头注意力中的偏置共享Transformer是多头的每个头有自己的query、key、value投影矩阵。一个自然的问题是句法偏置是所有头共享还是每个头独立论文里做了实验发现共享偏置和独立偏置效果差不多但共享偏置参数更少训练更稳定。这个结论和直觉一致句法信息是句子层面的不应该随头变化。如果每个头学一套偏置容易过拟合而且不同头之间没有一致性约束可能学出矛盾的偏置。不过这里有一个细节偏置是加在分数上的而不同头的分数尺度可能不一样。如果某个头的点积分数普遍偏大同样的偏置对它影响就小反之影响就大。论文里没有对分数做额外的归一化直接加偏置。实测下来只要初始化合理这个问题不严重因为层归一化和残差连接会稳定训练。实操心得如果你在自己的任务上发现加偏置后效果不稳定可以试试对偏置做一个缩放比如乘以一个可学习的标量初始化为1。这样模型可以自己调整偏置的整体强度。我在一个文本分类任务上试过加了缩放之后收敛更快最终效果也略好。3. 实操过程与核心环节实现3.1 从零实现句法偏置注意力的步骤假设你已经有了一个标准的Transformer编码器现在要把它改造成带句法偏置的版本。下面是我实际操作的步骤以PyTorch为例。第一步准备句法距离矩阵。对每个batch里的每个句子用依存分析器生成依存树然后计算距离矩阵。距离矩阵的形状是batch_size乘seq_len乘seq_len。注意padding位置的距离要设成一个特殊值比如-1然后在加偏置时mask掉。第二步定义偏置嵌入层。创建一个Embedding层输入维度是最大距离加1输出维度是1。初始化用零。这个Embedding层的参数就是每个距离对应的偏置值。第三步修改自注意力的forward函数。在计算完query和key的点积之后除以根号d之前或之后都可以把偏置加进去。具体来说对于每个位置对(i,j)查表得到偏置b然后分数加上b。注意偏置的形状要broadcast到和分数矩阵一致。第四步处理padding。padding位置的分数要设成负无穷这样softmax之后权重为零。这个和标准Transformer一样但要注意偏置不要加到padding位置上否则可能把负无穷变成有限值。第五步训练和调参。偏置嵌入层的学习率可以和主模型一样也可以稍微大一点因为它的参数少。我试过用一样的学习率效果就很好。最大距离阈值这个超参数论文里用10我试过5和15在大多数任务上10是安全的。如果句子平均长度很短比如10个词以内可以降到5。import torch import torch.nn as nn import torch.nn.functional as F class SyntaxBiasedAttention(nn.Module): def __init__(self, d_model, n_head, max_distance10): super().__init__() self.d_model d_model self.n_head n_head self.d_k d_model // n_head self.max_distance max_distance self.q_proj nn.Linear(d_model, d_model) self.k_proj nn.Linear(d_model, d_model) self.v_proj nn.Linear(d_model, d_model) self.out_proj nn.Linear(d_model, d_model) self.distance_bias nn.Embedding(max_distance 1, 1) nn.init.zeros_(self.distance_bias.weight) def forward(self, x, distance_matrix, maskNone): batch_size, seq_len, _ x.shape q self.q_proj(x).view(batch_size, seq_len, self.n_head, self.d_k).transpose(1, 2) k self.k_proj(x).view(batch_size, seq_len, self.n_head, self.d_k).transpose(1, 2) v self.v_proj(x).view(batch_size, seq_len, self.n_head, self.d_k).transpose(1, 2) scores torch.matmul(q, k.transpose(-2, -1)) / (self.d_k ** 0.5) clamped_dist torch.clamp(distance_matrix, 0, self.max_distance) bias self.distance_bias(clamped_dist).squeeze(-1) bias bias.unsqueeze(1) scores scores bias if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) attn F.softmax(scores, dim-1) context torch.matmul(attn, v) context context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) return self.out_proj(context)这段代码的关键点在于distance_bias的初始化为零这样训练初期模型行为和标准Transformer完全一样然后逐渐学会利用句法偏置。如果初始化成随机值训练初期可能会引入噪声收敛变慢。3.2 距离矩阵的计算实现距离矩阵的计算需要一个函数输入是依存树输出是每对词的最短路径长度。依存树可以用一个父节点数组表示parent[i]是词i的父节点索引根节点的父节点设为自己或者-1。计算最短路径的常用方法是先找到两个词的最近公共祖先然后路径长度等于两个词到祖先的深度之和减去两倍祖先的深度。def compute_distance_matrix(parents, seq_len): depth [0] * seq_len for i in range(seq_len): node i while parents[node] ! node: node parents[node] depth[i] 1 dist [[0] * seq_len for _ in range(seq_len)] for i in range(seq_len): for j in range(seq_len): a, b i, j path_a, path_b [], [] while a ! parents[a]: path_a.append(a) a parents[a] path_a.append(a) while b ! parents[b]: path_b.append(b) b parents[b] path_b.append(b) set_a {node: idx for idx, node in enumerate(path_a)} lca None for idx, node in enumerate(path_b): if node in set_a: lca node break dist[i][j] set_a[lca] path_b.index(lca) return dist这个实现的时间复杂度是O(n^2)乘以每次找LCA的O(n)对于n小于100的句子完全够用。如果要做batch处理可以把每个句子的距离矩阵算好之后padding到相同长度然后堆叠成tensor。注意依存分析器的输出格式各不相同有的用0-based索引有的用1-based有的根节点索引是-1。在写代码之前一定要确认清楚否则距离矩阵全错。我踩过这个坑调了半天才发现是索引偏移了一位。3.3 训练配置与超参数选择论文里的实验设置是标准的Transformer base配置6层编码器8个头d_model是512ffn是2048dropout是0.1。优化器用Adam学习率用warmup加逆平方根衰减。这些配置在大多数任务上都是安全的起点。句法偏置相关的超参数主要有两个最大距离阈值和偏置的学习率。最大距离阈值论文里用10我建议根据任务的平均句子长度来定。如果平均长度是2010是合理的如果平均长度是50可以设成15或20。偏置的学习率可以和主模型一样也可以设成主模型的2倍因为它的参数少需要更快的学习。我试过2倍学习率收敛稍快但最终效果差不多。还有一个容易忽略的点句法分析器的质量。如果分析器的准确率低于90%注入句法信息可能反而有害。这种情况下可以考虑用集成多个分析器的结果或者降低偏置的初始强度让模型自己决定用多少。论文里没有讨论分析器质量的影响但这是实际部署时必须考虑的。4. 常见问题与排查技巧实录4.1 加了句法偏置反而掉点怎么办这是最常见的问题。明明句法信息应该有用为什么加了之后效果变差了根据我的经验原因通常有以下几个。第一个原因是句法分析器质量太差。如果你的任务领域和句法分析器的训练领域差距很大比如分析器是在新闻语料上训练的你的任务是社交媒体文本分析准确率可能掉到80%以下。这种情况下句法偏置引入的噪声大于信号模型学不到有用的东西。解决办法是换一个在目标领域上表现更好的分析器或者干脆不用句法偏置等有了高质量分析器再说。第二个原因是偏置的初始化太强。如果偏置初始化成非零值训练初期模型就被迫接受一个可能错误的句法先验导致优化困难。论文里初始化成零这是最安全的做法。如果你改了初始化先改回零试试。第三个原因是最大距离阈值设得太小。如果阈值是5但很多有用的句法关系路径长度是6或7这些关系就被截断成5偏置无法区分它们。解决办法是增大阈值或者用对数分桶让长距离的桶更稀疏。第四个原因是任务本身不依赖句法。有些任务比如情感分类主要靠关键词和局部模式句法结构帮助不大。这种情况下加句法偏置可能只是增加了参数没有带来信息增益。可以先做一个消融实验不加偏置的基线是多少加了之后是多少如果持平或略降说明这个任务不适合。4.2 训练不收敛或loss震荡加了句法偏置之后如果训练不收敛首先检查距离矩阵是否正确。一个简单的验证方法是打印几个句子的距离矩阵人工检查是否合理。比如相邻词的路径长度通常是1或2如果打印出来是0或很大的数说明计算有问题。另一个常见原因是padding处理不当。如果padding位置的距离没有mask掉偏置会加到padding位置上softmax之后padding位置可能获得非零权重污染上下文表示。检查mask是否在加偏置之后、softmax之前应用。还有一个可能的原因是偏置嵌入层的梯度太大。因为偏置直接加到分数上对softmax的影响是指数级的梯度可能很大。可以试试对偏置嵌入层的梯度做裁剪或者用更小的学习率。我遇到过一次loss震荡把偏置学习率降到主模型的0.5倍就好了。4.3 推理时没有句法分析器怎么办训练的时候有句法分析器推理的时候如果分析器不可用或者太慢怎么办有几个方案。方案一是用一个小型快速分析器准确率低一点但速度快。很多依存分析器有蒸馏版本速度是原版的几倍准确率只降一两个点。对于推理延迟敏感的场景这个方案很实用。方案二是缓存句法分析结果。如果推理请求有重复比如同一个句子被多次处理可以缓存距离矩阵。但自然语言任务里完全重复的句子不多这个方案效果有限。方案三是训练一个句法距离预测器输入是词向量输出是距离矩阵。这个预测器可以和主模型联合训练推理时只用预测器不需要外部分析器。这个方案比较复杂论文里没有涉及但我在一个项目里试过效果还可以代价是增加了一些参数和训练时间。方案四是直接去掉句法偏置用标准Transformer推理。如果训练时句法偏置的权重学得很小去掉之后影响不大。但如果权重很大去掉之后效果会明显下降。所以这个方案只适合偏置权重小的情况。4.4 常见问题速查表问题现象可能原因排查方法解决办法加了偏置后掉点分析器质量差检查分析器在目标领域的准确率换分析器或不用偏置训练不收敛距离矩阵错误打印距离矩阵人工检查修正距离计算逻辑loss震荡偏置梯度太大监控偏置层梯度范数降低偏置学习率或梯度裁剪推理速度慢分析器太慢测分析器延迟用蒸馏版分析器或缓存偏置权重学不动初始化太小或学习率太低检查偏置值变化增大学习率或初始化长句效果差最大距离阈值太小统计长句的距离分布增大阈值或对数分桶避坑技巧在正式训练之前先做一个小的消融实验用10%的数据训练标准Transformer和带偏置的版本对比验证集效果。如果带偏置的版本没有优势先别急着上全量数据回头检查分析器和距离矩阵。这个习惯帮我省了很多算力。5. 效果分析与扩展思路5.1 论文实验结果的解读论文在机器翻译和语言模型两个任务上做了实验。机器翻译用的是标准的数据集语言模型用的是维基百科语料。结果显示加了句法偏置之后BLEU值和困惑度都有改善。改善幅度不算大机器翻译上大概0.5到1个BLEU语言模型上困惑度降了几个点。这个幅度在学术界算是显著但在工业界可能不够惊艳。值得注意的是论文做了消融实验把偏置换成固定的距离衰减函数效果比可学习的偏置差。这说明可学习的偏置确实有用模型学到的偏置模式不是简单的衰减。论文还可视化了学到的偏置值发现路径长度为1时偏置最大然后逐渐减小但路径长度为0时偏置不是最大的。路径长度为0就是词自己自己对自己的注意力本来就应该大不需要额外偏置。这个发现符合直觉。另一个有趣的发现是句法偏置在低资源场景下帮助更大。论文里做了低资源机器翻译的实验训练数据少的时候句法偏置带来的改善更明显。这很好理解数据多的时候模型自己能从数据里学到句法数据少的时候显式的句法先验更有价值。这个结论对实际应用很有指导意义如果你的任务标注数据少值得试试句法偏置。5.2 可以尝试的扩展方向第一个扩展方向是语义依存而不是句法依存。句法依存关注的是主谓宾定状补这些语法关系语义依存关注的是施事受事时间地点这些语义角色。对于很多下游任务语义信息比句法信息更有用。可以把依存分析器换成语义角色标注器距离矩阵基于语义依存树计算。这个方向论文里没有做但逻辑上是通的。第二个扩展方向是多语言场景。不同语言的句法差异很大比如中文没有形态变化英文有丰富的形态。句法偏置在多语言模型里可能帮助更大因为模型需要处理不同语言的句法。可以试试在 multilingual BERT 上加句法偏置看看跨语言迁移效果有没有提升。第三个扩展方向是结合其他结构信息。除了句法树还有篇章结构、共指链、实体关系图等。这些结构信息都可以转化成距离矩阵或者邻接矩阵用类似的方式注入Transformer。关键是要设计好距离的定义和偏置的映射方式。第四个扩展方向是动态句法。论文里的句法树是静态的每个句子分析一次。但有些任务里句法结构可能随上下文变化比如歧义句在不同语境下有不同的句法树。可以试试用多个分析器生成多个句法树然后对偏置做集成。这个方向比较前沿实现复杂度也高。5.3 工程落地的经验总结在实际工程里落地这个方案有几个经验值得分享。第一句法分析器的延迟是瓶颈。依存分析器的速度通常比Transformer编码器慢如果推理延迟要求高分析器可能成为瓶颈。解决办法是用快速分析器或者把分析器部署成独立服务和主模型并行调用。第二句法偏置的参数很少对模型大小影响可以忽略。偏置嵌入层只有几十个参数相比Transformer的几百万参数微不足道。所以不用担心增加模型大小。第三句法偏置和其他正则化方法可以叠加。比如dropout、权重衰减、标签平滑这些和句法偏置不冲突。我试过同时用dropout和句法偏置效果比单独用好。第四句法偏置的可解释性是一个加分项。学到的偏置值可以可视化帮助理解模型在多大程度上依赖句法。如果偏置值都很小说明模型不太需要句法信息如果偏置值很大说明句法信息很关键。这个可解释性在调试和汇报时很有用。我个人在实际操作中的体会是句法偏置不是万能药它适合那些句法结构对任务有明确帮助的场景比如机器翻译、句法分析、语义角色标注。对于情感分类、主题分类这些任务句法偏置的帮助有限。另外句法分析器的质量是天花板分析器不行后面怎么调都白搭。所以如果你打算用这个方案先把分析器选好、调好再考虑偏置的设计。最后分享一个小技巧如果训练数据足够多可以先在大量无标注数据上预训练一个句法距离预测器然后用它来生成距离矩阵这样推理时就不依赖外部分析器了整个流程更可控。