Add & Norm
Add & Norm 层
由 Add 和 Norm 两部分组成,其计算公式如下:
编辑
其中 X表示 Multi-Head Attention 或者 Feed Forward 的输入,MultiHeadAttention(X) 和
FeedForward(X) 表示输出 (输出与输入 X 维度是一样的,所以可以相加)。
Add
Add指 X+MultiHeadAttention(X),是一种残差连接,通常用于解决多层网络训练的问题,可以让
网络只关注当前差异的部分,在 ResNet 中经常用到:
Norm 层
Norm指 Layer Normalization(层归一化),通常用于 RNN 结构,Layer Normalization 会将
每一层神经元的输入都转成均值方差都一样的,这样可以加快收敛。下面我们将从3个方面介绍这
个模型,具体包括:
(1)层归一化的计算方法(2)如何使用层归一化(3)为什么 Transformer 使用层归一化
层归一化的计算方法
层归一化的作用很简单,就是让神经网络训练更稳定,不会出现梯度炸掉或者梯度变得特别小(梯
度爆炸和梯度消失)。 它的做法也不复杂:会对神经网络某一层里所有神经元的输出结果,统一
做“归一化”处理。 举个例子说吧:假设某一层有4个神经元,把一个样本输进去后,这一层会输出4
个结果,咱们叫它们x1、x2、x3、x4。层归一化做的事儿,就是专门针对这4个数值来计算,把它
们调整到合适的范围里。
咱们先把层归一化的公式里各个符号说清楚,特别好理解: xi 就是神经网络某一层里第i个神经元
的输出结果,u(平均值)是这一层所有神经元输出的平均数,而xhat(带帽的x)就是经过层归
一化处理后,最终得到的结果。层归一化后的 x1 到 x4: 具有相似的尺度使得神经网络可以更快的
收敛训练过程也会变更更加稳定。
如何使用层归一化?
层归一化实现的还是比较简单的,我们直接调用nn.LayerNorm,然后传入当前层的输出。
import torch from torch import nn # 定义张量x # x是一个2×4大小的张量,代表了2个样本,每个样本有4个特征 # 当使用层归一化对x进行计算时,会对x的每一行的4个特征进行归一化 x = torch.tensor([[1.0, 2.0, 3.0, 4.0], [1.0, 1.0, 1.0, 1.0]]) # 定义层归一化实例 # 传入的参数4,表示层归一化的输入维度是4 # 也就是对应某一层的4个神经元的输出 layer_norm = nn.LayerNorm(4) # 计算层归一化的结果 output = layer_norm(x) print(output)
为啥要使用归一化?
在 Transformer 模型中,层归一化(Norm)是基于 “序列数据的特定组织形式” 设计的。
Transformer 中序列数据的尺寸为 B×Q×D,各维度定义:
B(Batch size):批量大小,即一次输入模型的样本数量;
Q(Sequence Length):序列长度,即每个样本(如句子)包含的元素(如单词)数量;
D(Features Dim):特征维度,即每个元素对应的向量维度(如词向量的维度)。
Transformer 中,层归一化会针对 每个样本的特征维度(D维度)进行归一化(即对B×Q×D中每
个D维度的向量单独计算均值和方差),以适配序列数据的结构,稳定模型训练。
观察结果可以发现,在做层归一化的时候,会分别对x中的每行单词对应的4个维度进行计算,
在两个样本中一共有6个单词,这样就会进行6次层归一化的计算,单词和单词之间不会产出影响。
总结一下:无论一组序列数据的批量 B 是多少在一组数据中,不同的序列有着不同的长度 Q不会
影响层归一化的计算。
import torch import torch.nn as nn # 当使用层归一化计算序列数据时,层归一化只会作用在最后的特征维度D上 # 其中批量大小B与序列长度Q,这两个维度都不会影响层归一化的计算 # 定义输入数据 x,形状为 (B, Q, D) -> (2, 3, 4) # 2个样本,每个样本3个单词,每个单词用4个维度表示 x = torch.tensor([ # 第1个样本: [ [1.0, 2.0, 3.0, 4.0], # 第1个单词的4个维度 [1.0, 1.0, 1.0, 1.0], # 第2个单词的4个维度 [1.0, 1.0, 1.0, 1.0] # 第3个单词的4个维度 ], # 第2个样本: [ [1.0, 1.0, 1.0, 1.0], # 第1个单词的4个维度 [1.0, 1.0, 1.0, 1.0], # 第2个单词的4个维度 [1.0, 1.0, 1.0, 1.0] # 第3个单词的4个维度 ] ]) # 初始化层归一化层 layer_norm = nn.LayerNorm(4) # x输入至层归一化layer_norm中 output = layer_norm(x) # 输出结果 print(output)
FeedForward
Feed Forward 层比较简单,是一个两层的全连接层,第一层的激活函数为 Relu,第二层不使用激
活函数,FeedForward的计算公式如下:
编辑
X是输入,Feed Forward 最终得到的输出矩阵的维度与X一致。max相当于Relu,所以
FeedForward的作用是:通过线性变换,先将数据映射到高纬度的空间再映射到低纬度的空间,
提取了更深层次的特征。
Feed-Forward Layer就是一个全连接神经网络,通常包含两个线性变换和一个激活函数。它的输入
是Multi-Head Attention的输出,这个输出首先会经过一个残差连接和Normalization,然后才会进入
Feed-Forward Layer。Feed-Forward Layer的主要任务是进一步提取和深化特征,使模型能够更
好地理解和处理输入数据。 FeedForward的输入是什么呢?是Multi-Head Attention的输出做了残
差连接和Norm之后得数据,然后FeedForward做了两次线性线性变换,为的是更加深入的提取特
征。
代码实现:
import torch import torch.nn as nn import torch.nn.functional as F class FeedForward(nn.Module): def __init__(self, d_model: int, d_ff: int = None, activation: str = "gelu", dropout: float = 0.1): """ Args: d_model: 输入/输出维度(即 Transformer 的隐藏层维度) d_ff: 中间层维度(默认扩展为 4*d_model) activation: 激活函数,支持 "gelu" 或 "relu" dropout: Dropout 概率 """ super().__init__() d_ff = d_ff or 4 * d_model # 默认扩展比例为4倍 # 定义两个线性层 self.linear1 = nn.Linear(d_model, d_ff) # 扩展维度 self.linear2 = nn.Linear(d_ff, d_model) # 压缩回原维度 # 激活函数选择 self.activation = F.gelu if activation == "gelu" else F.relu # Dropout 层(可选) self.dropout = nn.Dropout(dropout) if dropout > 0 else nn.Identity() # 参数初始化(关键!) self._init_weights() def _init_weights(self): # 使用 He/Kaiming 初始化(适合 ReLU) nn.init.kaiming_normal_(self.linear1.weight, nonlinearity='relu') nn.init.zeros_(self.linear1.bias) # 缩小输出层的初始化范围 nn.init.xavier_normal_(self.linear2.weight, gain=0.02) nn.init.zeros_(self.linear2.bias) def forward(self, x: torch.Tensor) -> torch.Tensor: """ 输入形状: (batch_size, seq_len, d_model) 输出形状: (batch_size, seq_len, d_model) """ x = self.linear1(x) # (batch, seq, d_ff) x = self.activation(x) # 非线性激活 x = self.dropout(x) # 可选 Dropout x = self.linear2(x) # (batch, seq, d_model) return x