3. Transformer核心原理(下):Multi-Head Attention、Positional Encoding、Feed-Forward Network、Layer Normalization与残差连接
好,咱们接着聊。上一节我们把注意力机制和Self-Attention的底牌翻了个底朝天。这一节,咱们要把剩下的几个关键零件一一拆解。说白了,Transformer这个架构就像一台精密的瑞士手表,每个齿轮都不可或缺。我当年第一次手撸Transformer时,就是被这些细节折磨得够呛,尤其是多头注意力那块的维度变换,稍不留神就翻车。
3.1 Multi-Head Attention:让模型拥有「多个视角」
先问个问题:为什么一个注意力头不够?
你想想看,在金融时间序列里,价格波动可能同时受多种因素影响——短期动量、中期趋势、宏观事件冲击。单头注意力就像一个人用一只眼睛看世界,只能捕捉一种模式。多头注意力(Multi-Head Attention)则让模型长出「复眼」,每个头关注不同的子空间。
核心思想:将Q、K、V分别投影到h个不同的低维子空间,并行计算注意力,最后拼接并线性变换。
公式其实不复杂:
MultiHead(Q, K, V) = Concat(head_1, ..., head_h) · W_O
其中 head_i = Attention(Q · W_Q_i, K · W_K_i, V · W_V_i)
我在项目中遇到过这样一个坑:一开始我设了8个头,结果发现有些头学到的注意力模式几乎一模一样。后来我意识到,对于金融数据这种信噪比低的数据,头数太多反而容易过拟合。我个人习惯,股票日频数据用4个头就够了,高频数据可以适当增加到8个。
实战技巧:多头注意力的计算量是线性的,但效果提升显著。建议从4头开始尝试,观察验证集损失变化再调整。
3.2 Positional Encoding:给序列注入「位置感」
Transformer有个「先天缺陷」——它没有循环结构,无法感知词与词之间的先后顺序。你想想看,「我打你」和「你打我」在Transformer眼里,如果不加位置编码,就是一样的。
怎么解决?加位置编码(Positional Encoding)。
原论文用的是正弦余弦函数:
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
为什么用三角函数?说白了,这种编码方式有两个好处:
- 有界性:值域在[-1, 1]之间,不会像可学习位置编码那样可能发散
- 相对位置感知:任意位置的编码可以表示为其他位置的线性组合
嗯,这里要注意。在金融预测中,位置编码其实承载了「时间间隔」的信息。我曾经试过直接用可学习的位置编码,结果模型对时间间隔完全不敏感——比如第5天和第10天的关系,它学得稀里糊涂。换成三角函数编码后,模型对周期性模式(比如周内效应)的捕捉明显好了很多。
避坑指南:如果你处理的是不规则时间序列(比如有些交易日缺失),千万别直接用位置索引。我建议用时间戳差值作为位置编码的输入,或者干脆用可学习的位置编码,让模型自己适应。
3.3 Feed-Forward Network:非线性变换的「增压器」
注意力机制本质上是线性变换的加权求和。如果没有FFN,堆叠再多层也只是线性变换的叠加,那跟单层网络没区别。
FFN的结构很简单:
FFN(x) = max(0, x · W_1 + b_1) · W_2 + b_2
就是一个两层全连接,中间用ReLU激活。第一层把维度从d_model映射到d_ff(通常是4倍),第二层再映射回来。
我个人的理解是:注意力层负责「信息交互」,FFN负责「特征变换」。每个token经过注意力层获取上下文信息后,再通过FFN进行非线性映射,提取更高阶的特征。
在金融场景中,FFN的宽度(d_ff)是个超参数。我试过把d_ff设成8倍d_model,结果模型在训练集上表现极好,但一到验证集就崩——过拟合了。后来我总结出一个经验:对于金融数据,d_ff = 2~4倍d_model是比较稳妥的选择。
小技巧:可以用GELU或Swish激活函数替代ReLU,在某些任务上能提升1-2%的准确率。我最近的项目就在用Swish,收敛速度确实快一些。
3.4 Layer Normalization:稳定训练的「定海神针」
深层网络训练时,每层的输入分布都在变化——这叫「内部协变量偏移」。Layer Normalization就是用来解决这个问题的。
它的计算方式:
LayerNorm(x) = γ · (x - μ) / σ + β
其中μ和σ是每个样本在特征维度上的均值和标准差,γ和β是可学习的缩放和平移参数。
为什么用LayerNorm而不是BatchNorm?
BatchNorm在NLP任务中表现不好,因为序列长度变化大,batch统计量不稳定。LayerNorm则是对每个样本独立做归一化,不受batch大小和序列长度影响。
我记得有一次,模型训练到一半loss突然炸了——从0.3直接跳到NaN。排查了半天,发现是LayerNorm的epsilon设得太小,导致数值不稳定。从那以后,我习惯把epsilon设成1e-5或1e-6,而不是默认的1e-12。
注意:LayerNorm的位置有讲究。原论文用的是「后归一化」(Post-LN),即残差连接后再做归一化。但实践中「前归一化」(Pre-LN)更稳定,尤其对于深层模型。我建议新手直接上Pre-LN,省心。
3.5 残差连接:让梯度「畅通无阻」
残差连接(Residual Connection)是ResNet的经典设计,Transformer直接拿过来用了。公式很简单:
output = LayerNorm(x + Sublayer(x))
说白了,就是把输入和经过子层(注意力或FFN)的输出相加。这样做有两个好处:
- 梯度直通:反向传播时,梯度可以直接通过恒等路径传到前面层,缓解梯度消失
- 信息保留:即使子层学不到什么东西,至少能把原始信息保留下来
我刚开始做Transformer时,觉得残差连接可有可无。直到有一次我把残差去掉,训练一个6层的模型,结果loss死活降不下去。加上残差后,同样的模型训练速度提升了3倍,最终效果也更好。
嗯,这里有个细节:残差连接要求输入和输出的维度一致。如果子层改变了维度(比如多头注意力输出维度变了),需要加一个线性投影层来对齐。
3.6 整体架构:把这些零件拼起来
好了,现在我们把所有零件都过了一遍。来看看它们怎么组合成一个完整的Transformer层:
def transformer_layer(x):
# 多头注意力 + 残差 + LayerNorm
attn_output = multi_head_attention(x, x, x)
x = layer_norm(x + attn_output)
# FFN + 残差 + LayerNorm
ffn_output = feed_forward_network(x)
x = layer_norm(x + ffn_output)
return x
就是这么简洁。每个子层都遵循「子层 → 残差连接 → LayerNorm」的模式。堆叠N层,就构成了完整的Transformer编码器。
下面这张图展示了整个流程:
从图中可以清晰地看到:输入先加位置编码,然后经过多头注意力、残差连接、LayerNorm,再经过FFN、残差连接、LayerNorm。这就是一个完整的Transformer编码器层。
在实际的金融预测任务中,我们通常堆叠4-8层这样的结构。层数太少,模型表达能力不足;层数太多,训练难度和过拟合风险都会增加。我个人的经验是:对于日频股票数据,4层就够用了;对于分钟级高频数据,可以试试6层。
总结一下:多头注意力提供多视角特征提取,位置编码注入时序信息,FFN增强非线性表达能力,LayerNorm稳定训练,残差连接保证梯度流通。这五个组件缺一不可,共同构成了Transformer强大的基础架构。
好了,这一节的内容就到这。这些原理搞懂了,下一节咱们就可以动手搭建一个真正的金融预测模型了。
公众号:蓝海资料掘金营,微信deep3321