资讯动态

注意力机制的本质:从Nadaraya-Watson核回归到Transformer

发布时间:2026/10/20 4:41:25 来源:尧图企业网站定制
注意力机制这几年几乎成了深度学习领域的“通用货币”从Transformer到多模态模型处处都有它的身影。但很多同学学完注意力之后只知道QKV和softmax打分却答不上来“注意力机制到底在做什么”“为什么这样设计”。我最近把Nadaraya-Watson核回归和注意力机制放在一起做了一个模拟项目从统计学老底子一路推到Transformer的缩放点积注意力算是把这个大坑彻底填平了。这篇就把整个项目的拆解思路、实现细节和调试经验记录下来尤其适合正在啃Transformer源码、或者想搞清楚注意力本质的读者。1. 项目拆解注意力机制不是“凭空发明”它是核回归的自然延伸1.1 换个视角看注意力本质就是加权平均先说一个我在项目里反复验证过的结论注意力机制的本质就是带自适应权重的加权平均。这里的核心不是“加权”这件事本身而是“权重怎么来”。传统神经网络处理序列信息时通常把上一个时刻的输出全部压成一个固定长度的向量再塞进下一个时刻的模块。这个做法的天然缺点是信息压缩会丢细节。你无法指望一个固定向量把所有位置的语义都完整承载住。注意力机制则是反着来——不压缩而是“按需查询”。具体来说注意力计算分为三步根据当前的查询Query计算和所有键Key的相似度分数。对相似度分数做归一化通常是softmax得到权重分布。用这个权重分布对所有值Value做加权求和得到输出。这个过程本质上就是“先找相关性再按相关性比例取信息”。它和Nadaraya-Watson核回归的套路几乎一模一样核回归也是先计算输入点与每个样本点的距离相似度再用核函数把距离转成权重最后加权平均得到估计值。1.2 为什么Nadaraya-Watson核回归是理解的钥匙很多资料会把注意力机制解释成“一种让模型关注重点的技术”这没错但太笼统。如果只停留在概念层面你写不出attention层更调不好Transformer。Nadaraya-Watson核回归给了我一个非常清晰的数学模型给定一组样本(x_i, y_i)我们要估计新输入x对应的输出f(x)核回归的做法是f(x) Σ( K(x, x_i) / Σ_j K(x, x_j) ) * y_i也就是说输出是所有y_i的加权平均权重是K(x, x_i)在所有核函数值总和中的比例。把这个公式和注意力公式对齐一下注意力的打分函数其实就是核函数K(query, key_i)。 softmax归一化就是分母Σ_j K(x, x_j)的归一化。 Value向量就是y_i。这就建立了一个非常关键的直觉注意力权重不是凭空学出来的而是由查询和键之间的“相似度”决定的就像核函数决定样本点之间的“影响强度”一样。1.3 项目的目标定位这个模拟项目用了两个阶段来验证这条思路第一阶段用Nadaraya-Watson核回归在合成数据集上做函数拟合观察不同核函数和带宽参数对拟合效果的影响。第二阶段把核回归的思路套进Transformer的注意力层对比手动实现的多头注意力和库内置实现的效果再验证注意力权重可视化能否反映输入序列中的关键位置。项目代码量不大全部用PyTorch实现重点在于“理解”而不是“刷分”。整个项目做完我在纸上画了三张图第一张是核回归权重分布第二张是缩放点积注意力的权重矩阵第三张是两者并排对比。三张图放在一起注意力机制的神秘感就彻底没了。2. Nadaraya-Watson核回归详解从统计学到注意力的完美过渡2.1 非参数回归的思想脉络参数回归比如线性回归假设数据服从一个固定的函数形式用有限个参数描述整体规律。它的优点是简洁缺点是如果假设错了模型就废了。非参数回归则不预设函数形式直接基于数据点之间的局部关系做估计。Nadaraya-Watson核回归是非参数回归里最经典的一种。它的核心思想非常朴素要预测一个点的值就看这个点附近的样本是什么情况然后让附近的样本按距离远近投票距离越近话语权越大。这里有一个非常关键的操作距离怎么变成权重答案就是核函数。如果直接用“距离的倒数”或者“距离小于阈值的样本权重为1”也能得到权重但这些做法都不够平滑也不方便求导。核函数的好处是它提供了一个平滑的、可微的权重映射。我项目中用了三种核函数做对比结论很有参考价值核函数公式特点均匀核K(u) 1 当u高斯核K(u) exp(-u²/2)平滑最常用可微Epanechnikov核K(u) (3/4)(1-u²)最优均方误差计算量小实际测试下来高斯核在拟合精度和损失平滑性上表现最均衡。Epanechnikov核在计算效率上略胜一筹但梯度传播时不如高斯核方便。2.2 关键参数带宽h和注意力的“温度”Nadaraya-Watson核回归最核心的参数是带宽h也叫窗宽。带宽决定了“邻居”的影响力范围。还是用高斯核举例K_h(x, x_i) exp(-‖x - x_i‖² / (2h²))h越小权重曲线越尖只有非常近的样本才对结果有显著影响模型就越容易过拟合。h越大权重曲线越平缓远处样本的影响变大曲线过于平滑模型就容易欠拟合。这个现象和Transformer里面缩放点积注意力的“缩放因子”简直就是一回事。Transformer里QK^T之后要除以sqrt(d_k)用我项目里的类比来说这个缩放因子就是在调节注意力分布的“锐利程度”。除以的数值越大softmax后的分布越平缓数值越小分布越尖锐。我在实际实验中试过将缩放因子调为1效果非常差梯度在softmax之后变得很不稳定训练损耗曲线上下剧烈震荡。把缩放因子调回sqrt(d_k)之后模型才稳下来。这不是什么玄学就是核回归里带宽参数在Transformer里的对应表现。2.3 从核回归到注意力的公式映射我在项目的文档里把两个公式并排抄在了一起做了这样一张对应关系表Nadaraya-Watson核回归注意力机制说明输入样本点 x_iKey键被查询的对象待估计点 xQuery查询主动发起查询的对象样本值 y_iValue值被加权的内容核函数 K(x, x_i)相似度打分函数决定相关性归一化分母 Σ_j K(x, x_j)softmax归一化让权重和为1带宽 h缩放因子决定分布锐利度这张表是整篇项目笔记最核心的一页。一旦你把两张公式并排看注意力机制的每一步设计都有了解释。为什么要有Q和K因为你需要一个“查询”和一个“被查询对象”它们之间的相似度就是权重。为什么要有V因为权重最终要作用在真实的内容上V就是内容本身。2.4 实操代码骨架从零实现核回归下面这段代码是本项目第一阶段的实现只依赖NumPy和Matplotlib没有任何深度学习框架。代码我尽量精简过保留了核心逻辑。import numpy as np import matplotlib.pyplot as plt def gaussian_kernel(dist, h): return np.exp(-dist ** 2 / (2 * h ** 2)) def nadaraya_watson(X, y, x_query, h0.5): weights gaussian_kernel(x_query - X, h) weights weights / np.sum(weights) return np.sum(weights * y) # 生成合成数据 rng np.random.default_rng(42) X np.sort(rng.uniform(-3, 3, 200)) y np.sinc(X) 0.1 * rng.normal(0, 1, 200) # 在密集网格上做预测 x_grid np.linspace(-3, 3, 500) y_pred np.array([nadaraya_watson(X, y, x, h0.3) for x in x_grid]) plt.scatter(X, y, s5, alpha0.4, label样本点) plt.plot(x_grid, y_pred, colorred, label核回归拟合) plt.legend() plt.show()这段代码有一点我想特别提醒nadaraya_watson函数对每个查询点都要遍历全部样本计算核函数时间复杂度是O(N)。在数据量小的时候无所谓但实际做大规模序列建模时肯定不能这么干所以Transformer里才要用矩阵乘法一次性算出所有查询和所有键的相似度。3. Transformer注意力机制实现把核回归的直觉变成可训练的模块3.1 从QKV到注意力权重的完整流程项目第二阶段开始进入Transformer核心地带。既然第一阶段已经把“相似度决定权重”的直觉建立了第二阶段就直接上手搭建缩放点积注意力模块。缩放点积注意力的公式Attention(Q, K, V) softmax(QK^T / sqrt(d_k)) V这里Q的维度是(batch_size, seq_len, d_k)K的维度是(batch_size, seq_len, d_k)V的维度是(batch_size, seq_len, d_v)。用一个生活化的例子解释这个过程假设你在图书馆找书你想找一本“深度学习入门”相关的书这就是Query。图书馆里每一本书都有一个标签这就是Key。你要做的是把Query和所有Key比一遍相似度挑出最相关的几本然后照着这些书的结论Value综合出一份摘要。细节上要注意QK^T计算出来的分数矩阵形状是(batch_size, seq_len, seq_len)第i行第j列表示第i个查询对第j个键的注意力分数。这个矩阵就是整个注意力机制的核心它决定了每个位置应该从哪些其他位置收集信息。关于d_k的缩放为什么不能省掉因为当d_k变大时QK^T的方差也会变大。原始分数的绝对值会变得很大经过softmax之后梯度会进入饱和区更新幅度变得极小。除以sqrt(d_k)之后分数的方差被拉回一个稳定范围softmax的梯度才能正常流动。我写了一个简化版的手动实现用来对照库里的实现import torch import torch.nn as nn import torch.nn.functional as F class ScaledDotProductAttention(nn.Module): def __init__(self, d_k): super().__init__() self.d_k d_k self.scale 1.0 / math.sqrt(d_k) def forward(self, Q, K, V, maskNone): scores torch.matmul(Q, K.transpose(-2, -1)) * self.scale if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) weights F.softmax(scores, dim-1) output torch.matmul(weights, V) return output, weightsQ、K、V本身一般不是原始输入直接拿来的。标准Transformer里输入通过三个独立的线性层投影得到Q、K、V这样可以让模型在不同子空间中学习不同的表示。我实践下来这个投影非常关键它本质上是在学习“把输入映射到更适合做匹配的度量空间里”。3.2 多头注意力为什么要“多头”一个独立注意力头能学到的分配模式是有限的。在实际任务里一个词可能需要同时关注语法上的依存关系、语义上的指代关系、以及位置上的邻近关系。如果只用一个头所有这些信息会被强制压进同一套权重分布里互相干扰。多头注意力的做法很简单把Q、K、V分成多个子空间每个头独立计算注意力最后把结果拼起来再过一层线性变换。我在项目里用8个头做了实验效果比单头有明显提升。有一组可视化尤其有意思不同的头学到的注意力模式完全不同有几个头关注相邻词的局部关系有几个头则捕捉了较远位置的语义关联。多头实现的关键代码模式class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads, d_k, d_v): super().__init__() self.n_heads n_heads self.d_k d_k self.d_v d_v self.d_model d_model # 每个头都有自己的Q、K、V投影 self.W_Q nn.Linear(d_model, n_heads * d_k) self.W_K nn.Linear(d_model, n_heads * d_k) self.W_V nn.Linear(d_model, n_heads * d_v) self.out_proj nn.Linear(n_heads * d_v, d_model) def forward(self, query, key, value, maskNone): batch_size query.size(0) # 投影并拆分多头 Q self.W_Q(query).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2) K self.W_K(key).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2) V self.W_V(value).view(batch_size, -1, self.n_heads, self.d_v).transpose(1, 2) attn_output, weights ScaledDotProductAttention(self.d_k)(Q, K, V, mask) attn_output attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.n_heads * self.d_v) return self.out_proj(attn_output)这块代码我踩过不少坑最明显的一个是多头拼接之后contiguous()忘了调用然后view直接报错。原因是transpose之后张量内存不连续view不能用在非连续张量上。虽然这是个小问题但几乎每个写Transformer代码的人都会遇到至少一次。3.3 注意力权重与位置编码的协作Transformer和RNN不同它是并行计算的没有天然的时序概念。两个位置交换顺序它们之间的注意力分数不会变。所以模型需要额外注入位置信息。位置编码的可视化我在项目里也做了加上位置编码之后注意力权重在距离相近的位置上明显积攒了更多概率质量。这实际上是模型“偷懒”的一种体现——它发现用位置信息来做局部匹配更容易优化于是自然地优先使用这种最简单的关系。4. 实操流程从合成数据到注意力可视化的完整过程4.1 第一阶段实验合成数据验证核回归第一阶段的实验目标非常明确验证Nadaraya-Watson核回归在不同参数下能不能拟合出合理结果。我最初测试的是均匀核拟合曲线有明显的不连续感边缘处出现锯齿状波动。换成高斯核之后曲线一下就平滑了。如果把带宽从0.1调到2.0可以看到拟合曲线从“剧烈震荡”过渡到“高瘦平滑”。这里分享一个带宽选择的经验我用了简单的网格搜索用留一法交叉验证找最优带宽。结果发现带宽在0.2到0.4之间拟合效果最好过小则拟合了噪声过大则丢失了曲线的起伏结构。4.2 第二阶段实验训练一个玩具Transformer第二阶段我构造了一个简单的序列预测任务给出一段随机游走序列的前半段让模型预测后半段。这个任务足够简单但又能让注意力机制派上用场。模型结构非常简单输入嵌入层把每个位置的值映射成64维向量。位置编码用正弦余弦编码维度与嵌入一致。一个TransformerEncoder层包含多头自注意力。输出层全连接。训练时我把相邻序列的窗口和预测目标做成了一批数据切分为训练集和测试集。因为数据是随机生成的不用考虑泄露问题。Transformer层数只用了一层隐层维度64注意力头4个。大约在20个epoch左右训练损失和验证损失都降到了一个很低的水平。这时候我仔细观察注意力权重的分布发现一个非常有趣的现象模型在预测后续序列时注意力几乎集中在前几个位置上后面的历史位置权重趋近于零。这说明模型学到了一个直观的策略最近几个时间点的数据对预测下一时刻最有用。4.3 可视化让注意力“显形”注意力可视化的代码我单独整理成了一个工具函数核心就是热力图。每一行代表一个查询位置每一列代表一个键位置颜色越深说明权重越大。import seaborn as sns import matplotlib.pyplot as plt def plot_attention(weights, tokens, figsize(10, 8)): plt.figure(figsizefigsize) sns.heatmap(weights[0].cpu().detach().numpy(), xticklabelstokens, yticklabelstokens, cmapYlOrRd, annotFalse, cbarTrue) plt.xlabel(Key) plt.ylabel(Query) plt.show()可视化能帮你快速诊断模型哪里学坏了。比如我在实验初期训练了一个模型发现注意力矩阵对角线附近颜色特别深其他区域几乎没有颜色。这个模型的预测效果其实还行但如果你在推理时把输入的序列顺序打乱它的表现立刻崩溃。这说明模型没有学会真正的内容关联只是在背位置。直到我把位置编码换成可学习版本并加了适当的dropout之后注意力才分散到有语义关联的位置上。5. 常见问题排查与避坑指南5.1 softmax导致的权重退化问题很多时候你会发现训练好的模型注意力总是集中在一两个位置上形成一种几乎“独裁式”的权重分布。这种情况通常意味着softmax之前的分数分布出现了异常某些位置的分数远高于其他位置。一个常见原因是训练初期梯度更新过猛某个key的映射方向恰好和所有query都对上了于是所有query都关注到同一个key上。解决办法之一是在注意力的score输出后加一层LayerNorm或者把分数除以一个更大的缩放因子再做softmax。另一个比较务实的办法是给注意力权重加dropout强制模型不要把赌注押在少数几个位置上。5.2 掩码位置泄漏在自回归任务里当前位置只能看到它之前的信息不能看到未来的信息。如果忘了加因果掩码模型就会“作弊”预测未来时直接偷看答案。因果掩码的实现是生成一个上三角矩阵把未来的位置填充成负无穷。有一个细节容易被忽略如果你把负无穷直接加到分数上softmax的结果在极端情况下会出现NaN因为exp(-inf * 某个数)还是0但分数矩阵里混合了-inf和普通数值时梯度可能会出问题。稳妥的做法是用masked_fill并且在mask之前确保分数矩阵是float类型。5.3 多头之间信息重叠多头注意力不是头越多越好。我试过将头数设置成32训练时间猛涨效果却没有明显提升。后来看注意力可视化才发现多个头学到的模式高度相似信息冗余很严重。采用两个改进后问题得到缓解在多头投影层之间加一定比例的dropout制造多样性。在loss里加入多头注意力的多样性正则项让不同头的注意力分布尽量不同。后者效果更明显但会增加不少调参工作量。一般情况下建议优先尝试dropout。5.4 数值稳定性问题总结项目过程中遇到的所有不稳定问题基本都能归因到几个点上现象常见原因解决建议训练loss震荡剧烈分数scale没调对确认除以sqrt(d_k)权重集中在一个位置分布熵过低加attention dropout出现了NaN掩码负无穷处理不当用masked_fill而非加法多头冗余投影层缺乏正则增加dropout或多样性约束输出结果全一样多个head拼接维度不对检查view和transpose的维度顺序6. 扩展方向与个人心得这个项目做完我对注意力机制的理解从“会用”提升到了“能设计”。以前看Transformer论文总觉得缩放点积是凭空跳出来的设计现在知道了它是为了控制核函数带宽以前觉得多头注意力是个复杂的工程技巧现在明白了不同头就是在学不同带宽、不同度量空间上的相关性。后续我打算做几个方向的扩展。一是把Nadaraya-Watson核回归进一步推广到可学习的核函数上比如把距离度量换成马氏距离看看对注意力模式的影响。二是尝试在Transformer的注意力打分函数里加入类似Epanechnikov核的截断机制观察稀疏注意力的效果。目前已经有稀疏注意力论文在做类似的事情但用核回归的视角重新审视会有很多值得玩味的地方。最后分享一个实操体会如果条件允许一定要把公式手推一遍再亲手实现一遍。网上有很多现成的Transformer代码直接往任务上套用完就忘了。但这个项目从核回归一路做过来我只花了一周时间对注意力机制的掌握程度却远超以往几个月拼凑代码的收获。希望这篇笔记对正在啃注意力机制的读者有点用也欢迎交流实现过程中的各种细节问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑