从Nadaraya-Watson核回归理解Transformer注意力机制

📅 发布时间:2026/10/21 3:25:05
从Nadaraya-Watson核回归理解Transformer注意力机制
注意力机制这个东西我最早是在Transformer的文章里认识的公式背得滚瓜烂熟softmax(QK^T / √d_k)乘V。但“为什么是softmax为什么除以√d_kQ、K、V三个矩阵到底在干嘛”这些问题我并没有真正想通。直到后来接触非参数统计读到Nadaraya-Watson核回归一下子像被点醒了这不就是注意力机制的雏形吗这篇文章想把这条线完整理一遍。从一个比较朴素的核回归实验出发看它如何一步步长成如今深度学习里无处不在的注意力机制中间每一步到底改了什么东西、为什么要这样改。同时我会给出可以直接运行的PyTorch代码和对比实验让你不只是“听懂”还能亲手把这个联系做出来。适合谁看如果你学过Transformer的公式但总觉得隔着一层或者你在实际项目中调过注意力参数却不太清楚手感这篇会很有帮助。我不依赖复杂的数学推导所有公式都压到最小必要程度重点放在直觉和可复现代码上。1. 先用一个最朴素的回归问题理解Nadaraya-Watson核回归1.1 核回归在干什么相似度加权平均先说场景。给定一堆散点(x_i, y_i)你想知道任意一个新点x对应的y是多少。最传统的方法是拟合一个线性模型或者多项式但问题来了你根本不知道数据的函数形式。它可能是带噪声的正弦波也可能有突变甚至可能是一个分类问题里的概率。这时候强行选一个参数模型很容易选错。核回归走了另一条路我什么都不假设只看“和x接近的那些训练样本它们的y应该和x的y差不多”。于是预测值写成f(x) (Σ_i K((x - x_i) / h) · y_i) / (Σ_i K((x - x_i) / h))K是核函数最常用的高斯核长这样K(u) exp(-u² / 2)带进去之后对每个训练样本计算一个权重离x越近权重越大离x越远权重越小。最后把y_i按权重加起来再除以总权重做归一化。整个公式说的就是一句话按相似度做加权平均。这句话建议你记住注意力机制的全部秘密都在这里面。Transformer里的注意力权重说穿了也是一个“相似度加权平均”只是它把前面那套东西全部换成了可学习的版本而且维度从一维推广到了高维向量。1.2 核函数和带宽注意力集中的“温度旋钮”高斯核里有个h叫带宽。这个参数极其关键它直接回答了“什么样的样本算近”。可以这么理解h相当于一个放大镜的焦距。h很小只有紧贴着的点才能拿到权重预测曲线就会剧烈振荡几乎穿过每个训练点h很大远近样本的权重差别越来越小最后曲线会被抹成一条接近常数的平线h取一个合适值曲线才能既贴合数据又不过度敏感。如果你熟悉softmax温度会发现h和温度T是一家人。softmax(x/T)里T越小、分布越尖锐T越大、分布越平缓。核回归里的h干的同样是这件事控制加权的集中程度。后面你会看到Transformer里的√d_k也是一个类似角色——只是缩放方向相反h是做除法√d_k也是做除法它们都在调节分数分布的“钝锐程度”。带宽怎么选比较正经的办法是交叉验证但实操中你只要把h从小到大多跑几组看图就能确定大概范围。麻烦的是在真正的Transformer训练里这个“带宽感”需要通过梯度下降自己学出来这也是注意力机制比核回归难训的一个原因。1.3 最小实现h0.05、0.5、10三种状态对比动手之前先说清楚这段代码不涉及任何神经网络就是纯粹的Nadaraya-Watson估计。我用PyTorch生成带噪声的正弦数据然后实现核回归。import torch import matplotlib.pyplot as plt torch.manual_seed(42) # 生成数据带噪声的 sin 曲线 N 80 x_train torch.rand(N) * 10 y_train torch.sin(x_train) 0.2 * torch.randn(N) def nadaraya_watson(x_query, x_train, y_train, h): # x_query: (M,) 或标量一维核回归 diff x_query.unsqueeze(-1) - x_train.unsqueeze(0) # (M, N) weights torch.exp(-(diff ** 2) / (2 * h * h)) weights weights / weights.sum(dim-1, keepdimTrue) y_pred (weights * y_train.unsqueeze(0)).sum(dim-1) return y_pred, weights x_grid torch.linspace(-1, 11, 200) for h in [0.05, 0.5, 10.0]: y_pred, _ nadaraya_watson(x_grid, x_train, y_train, h) plt.plot(x_grid.numpy(), y_pred.numpy(), labelfh{h}) plt.scatter(x_train.numpy(), y_train.numpy(), alpha0.4, labeldata) plt.legend() plt.show()跑出来你会看到非常直观的三种状态带宽h曲线形态对应到注意力机制的感受0.05曲线穿过每个点剧烈振荡注意力分数过于尖锐几乎只关注最近邻相当于softmax温度趋近00.5平滑拟合正弦曲线噪声被过滤注意力分数适中加权范围合理类似默认温度10.0曲线接近常数细节全部丢失注意力分数趋于均匀相当于大家都在做全局均值池化我建议你把这三个h都跑一遍亲身感受一下“带宽注意力的集中程度”。这比看一百篇论文都管用。代码里没有梯度、没有优化器它唯一的价值就是让你亲眼看到所谓“注意”是怎么从一次加权平均里涌现出来的。2. 三步把核回归改造成注意力机制2.1 从负距离到正点积相似度的两种写法前面核回归的核心是exp(-距离²)。这个形式有一个隐含倾向距离越小、权重越大因为它用的是负距离。但“相似度”不一定非得用距离表达。如果样本本身是向量可以用点积来刻画对齐程度两个向量方向越一致、模越大点积越大说明它们越相似。于是把指数里的负距离平方换成点积权重变成w_i exp(q · k_i)其中q是查询向量k_i是第i个键向量。再对所有w_i做归一化等价于weights softmax([q·k_1, q·k_2, ..., q·k_N])这一步做完你已经得到注意力机制的雏形了。这时候权重仍然来自固定规则不包含任何可学习参数本质上还是一个核回归只是相似度的度量从L2距离换成了点积。之所以用点积而不是距离一个很重要的原因是点积天然适合高维向量比较而且写成矩阵乘法后可以一次性并行计算所有位置之间的相似度。这也是Transformer能高效训练的原因之一。2.2 引入QKV让核函数和输出可学习核回归里x_i直接参与相似度计算y_i直接作为加权对象。但真实数据里输入向量不一定适合直接拿来比较。比如句子里的词向量它混合了语义、句法、位置各种信息直接算点积可能分不清该关注哪部分。于是引入三个可学习的线性投影q W_Q · x k_i W_K · x_i v_i W_V · x_i这里W_Q、W_K、W_V就是常说的QKV矩阵。加上投影之后注意力机制获得了两个能力比较的视角可以学习模型自己决定在什么子空间里算相似度而不是拿原始向量硬比输出的内容可以学习V经过投影后不再是原始y_i而是“值得被聚合的信息”。这一步是核回归和注意力最大的分水岭。核回归的核函数是固定的高斯核注意力机制的核函数则是数据驱动、可以训练的。用个不太严谨但很好记的说法注意力是一个“学出来的”Nadaraya-Watson核回归。这里也给个小提示Q、K、V虽然名字玄乎但它们的角色很清晰——Q负责提问“我要找什么”K负责回答“我这里有什么”V负责提供“找到之后读出什么内容”。2.3 除以√d_k给注意力打分定一个温度大多数教程讲到softmax(QK^T/√d_k)时只会说“防止点积过大导致softmax饱和”。这句话没错但如果你从核回归的角度看会有更深的体会。当d_k变大时q·k的点积是d_k个独立乘法相加方差会随维度线性增长。举例说假设q和k各分量是均值为0、方差为1的随机变量那么点积的方差大约是d_k标准差是√d_k。如果不做缩放维度64的时候分数标准差8维度512的时候标准差22softmax的输入很容易进入两端饱和区梯度接近0模型学不动。除以√d_k本质上是把打分方差拉回常数级别相当于给softmax设置了一个合理的“温度”让分布既不过于尖锐也不过于平坦。从核回归的类比看√d_k起到的作用和带宽h类似都是在控制加权的集中程度只是它固定为一个与维度相关的常数。实操建议如果你在某些变体里看到不用softmax、改用其他归一化的注意力比如线性注意力它的“温度”往往也是靠缩放因子来控制的。理解√d_k的这一层含义之后分析各种注意力变体都会轻松很多。3. Transformer里的自注意力其实是一组核回归3.1 逐位置拆解自注意力很多人背过自注意力的定义但背完就忘。现在我们回到核回归的视角把自注意力拆开。假设输入序列是[x_1, x_2, ..., x_T]每个x_t都是一个向量。经过Q、K、V投影之后得到Q、K、V三个矩阵。自注意力对每个位置t做下面这件事取出q_t把它当作核回归里的“查询点x”对所有位置j计算分数 q_t·k_j归一化得到权重用权重对V的所有行做加权平均得到输出z_t。这个过程和第一节里的核回归完全同构每个位置都在以自身为查询点对序列内所有其他位置做一次相似度加权平均。差别只在于维度从一维变成高维核函数不再是固定的exp(-距离²)而是softmax(q·k)参与比较的内容和用于聚合的内容由可学习的投影决定。换句话说自注意力就是“序列中每个位置各做一次核回归而且核函数本身由数据动态生成”。这也是为什么序列模型可以自动学到长距离依赖只要两个位置在语义上对齐不管它们中间隔多远注意力权重都能给得很大。3.2 多头注意力多次核回归的集成单头注意力虽然灵活但只有一个视角。比如在句子里你可能既想关注主谓一致又想关注指代关系这两个需求对“相似度”的定义很可能不一样。多头注意力做的事就是同时开h个核回归每个头在自己的子空间里算相似度。从核回归类比看多头注意力有三层含义集成效应多个核回归器各做各的最后拼起来相当于多专家投票误差更稳特征子空间分工每个头可以在不同维度上判断“谁和谁该互相注意”这是模型自己学会的分工表达能力增强单头注意力的加权平均本质上还是低秩结构多头把多个低秩结构拼起来表达能力上限显著提高。实操里头的数量并不是越多越好。头数太多会把d_model劈得太碎每个头的维度太小学不到稳定的相似度度量。我一般会先保证每个头的维度不低于32再去调头数。3.3 位置编码是在给核回归补“坐标”核回归天生没有“位置”概念。它只认特征距离两个点特征越近权重越高。自注意力也是一样如果不加位置信息它就是一个对序列内容做置换不变的加权平均器——把句子里的词随便换顺序注意力结果完全不变。这显然不行。自然语言里“猫追狗”和“狗追猫”的意思完全不同。解决办法也直白把位置信息加到输入embedding里。位置编码本质上就是给每个位置加一个“坐标”让相似度计算既看内容是否相似又看位置是否靠近。用核回归的语言说位置编码等效于在高维特征空间里额外增加了位置维度。如果你把每个token表示成“内容向量”和“位置向量”的组合那核回归在比较两个token时同时会在内容轴和位置轴上打分模型就可以选择“只看内容相似”或“同时要求位置接近”。这也是为什么相对位置编码在很多任务里效果更好的原因它相当于让每个查询点自带一个可学习的“局部窗口感受野”类似于把核回归的带宽h变成了随位置变化的形式。3.4 从零实现一个最小自注意力理解了原理代码就水到渠成。下面是一个最小但完整可运行的自注意力实现不追求效率只求可读性。import math import torch import torch.nn as nn import torch.nn.functional as F class SelfAttention(nn.Module): def __init__(self, d_model, d_k, d_v): super().__init__() self.W_Q nn.Linear(d_model, d_k, biasFalse) self.W_K nn.Linear(d_model, d_k, biasFalse) self.W_V nn.Linear(d_model, d_v, biasFalse) self.d_k d_k def forward(self, x): # x: (T, d_model) Q self.W_Q(x) # (T, d_k) K self.W_K(x) # (T, d_k) V self.W_V(x) # (T, d_v) scores Q K.T / math.sqrt(self.d_k) # (T, T) weights F.softmax(scores, dim-1) # 每行是归一化后的注意力权重 out weights V # (T, d_v) return out, weights这段代码只有十几行但包含了自注意力最核心的骨架。你可以把第一节的核回归代码和这段对照着看核回归里的exp(-距离²/2h²)变成了这里的softmax(QK^T/√d_k)核回归里的y_i变成了这里的V核回归里的x和x_i变成了这里的q和k。想更直观地看注意力学到的模式可以把weights打印出来对权重矩阵画热力图。你会看到对角线附近往往权重较高因为自己和自己通常最相似但模型也会学到跨位置的强关联——那些就是它认为应该重点关注的“远处邻居”。模型训练时在序列长度不长的情况下我们可以直接观察不同头关注的区域。你会发现有的头倾向于关注相邻位置有的头则跨越长距离捕捉关系。这时候再回头看核回归的带宽h你会理解其中联系每个头其实都在自己那份“核回归”里学了一个动态带宽。4. 实操中常见的问题与排查技巧4.1 注意力分数退化权重全变成均匀分布新手训练Transformer时经常遇到一个现象训练初期甚至整个训练过程中某些头的注意力权重都接近于均匀分布——所有位置几乎同样的权重模型退化成了全局平均池化。从核回归的角度看这相当于带宽h设置得太大高斯核几乎不区分远近。注意力层面最常见的原因是初始化不好或训练步数不够。如果持续出现可以试试调整QK投影的初始化方差让初始点积分数分布更分散加大训练步数或调整学习率让Q、K更快拉开差异换用温度可学习的softmax变体相当于允许模型自行调整“带宽”。如果你的模型有特殊任务比如机器翻译里某些头均匀化不一定代表模型坏了。很多情况下均匀头承担着“全局残差连接”的作用是正常现象。判断方法很简单看去掉这个头之后性能掉多少不掉就说明它确实可有可无。4.2 数值稳定性softmax溢出不只在日志里出现手写注意力时最容易踩的坑是softmax的数值稳定性。当分数矩阵里出现特别大的值exp之后可能溢出成inf反向传播时直接NaN。PyTorch里的F.softmax已经内置了max-shift技巧所以大部分人不会遇到。但如果你自己实现类似逻辑比如做论文复现建议手动加一下def stable_softmax(scores, dim-1): scores_max scores.max(dimdim, keepdimTrue).values exp_scores torch.exp(scores - scores_max) return exp_scores / exp_scores.sum(dimdim, keepdimTrue)这个技巧的原理很简单softmax对整体平移不变减掉最大值再算指数既能保证最大项不溢出又不会改变最终的分布结果。实际训练里除了手动实现还有可能是梯度爆炸导致分数越推越大。遇到NaN时先检查loss是否在某个step突然起飞再检查前向有无异常。4.3 缩放因子与学习率的关系很多人把√d_k当成一个无关紧要的细节实际上它直接影响可训练性。前面说过如果不缩放高维点积分数方差很大softmax会进入饱和区梯度变小甚至消失。但缩放太小也有问题分数分布太平坦梯度同样很小只是因为“太平”而不是“太陡”。实操中你会发现缩放因子和学习率是耦合的。如果你把缩放因子改了原来调好的学习率很可能要重新调。一个常见组合是使用默认√d_k缩放时学习率可以开到1e-4到3e-4如果你去掉缩放可能需要把学习率降一个量级才能稳住训练。这里给个排查思路如果训练中注意力权重很快变成one-hot分布大概率是分数尺度太大如果注意力权重长期均匀大概率是分数尺度太小或初始化不够好。两种极端都先从缩放因子下手检查而不是盲目调学习率。4.4 什么时候该用固定核什么时候该用学出来的注意力最后分享一个我在实践中养成的习惯遇到新任务先别急着上完整Transformer。我会先用固定核函数的加权平均做一个基线比如就是一个可选的指数核或高斯核设定一两个手工带宽。这个固定核基线能回答一个问题这个任务里“相似度加权平均”到底有没有用带宽大概在什么量级。如果固定核效果已经不错说明任务主要依赖局部或全局的相似结构那上注意力大概率有收益如果固定核效果很差说明要么相似度定义不适合要么模型需要更强的非线性那就要谨慎评估是不是Transformer真的对路。在时间序列、表格数据这类任务上固定核甚至可能比学出来的注意力更稳因为它没有额外的参数方差不容易过拟合。这也提醒我们注意力机制最大的优势不是它天生神奇而是它把“选择什么核、在哪比较、聚合什么内容”全部交给了优化器。理解这一点之后你会对各种注意力变体有自己的判断力而不是每个paper都看不懂。最后说点个人体会。我自己的学习过程特别曲折注意力公式背了好几年真正有豁然开朗的感觉是在我亲手把Nadaraya-Watson核回归写出来、又把带宽h从0.05调到10之后。那一刻我才意识到很多看起来高深的深度学习组件根上都是统计里非常朴素的思想。如果你也在挣扎建议你按这篇文章的顺序复现一遍先跑核回归感受带宽再写最小自注意力对照结构最后拿一个小数据集训练观察不同头的注意力热力图。这个过程全套下来比单看十篇综述都更能建立直觉。深度学习里的大部分组件都是这样真正理解它们的方法不是记住公式而是自己动手做一次。