0x02 FFN 前馈神经网络
概述
FFN 是一个两层的全连接层,第一层的激活函数是 ReLU,第二层不使用激活函数,在两个线性变换之间,除了 ReLU 还使用一个 Dropout
- 第一个线性层。输入是\(x\in \mathbb{R} ^{L \times d}\),表示多头注意力机制的输出。一般第一个线性层会升维,也就是\(W_q \in \mathbb{R}^{d \times 4d}\),如果输入维度 \(d=512\),那么经过第一个线性层之后,的维度是\(4d=2048\)
- ReLU 激活函数,非线性激活函数,增加模型的表示能力(非线性化能力)
- 第二个线性层。将升维后的维度将会到原始的维度,也就是说 \(W_2 \in \mathbb{R}^{4d \times d}\)
因此,FFN 的参数主要存在与两个线性层上。值得一提的是,在后续很多 LLM 的工作中,线性层中不再引入偏置项,也就是\(b_1,b_2\)。
另外, 一般将 \(4d\) 维度记为 \(d_{ffn}\),表示在 FFN 结构内部的升维后的维度,其中的 4 表示中间层比率,决定了 FFN 网络参数的大小。如果很小,模型的性能可能会变差,而太大的话,会造成峰值内存过高,因此需要综合考虑。
介绍
激活函数在 FFN 的作用是引入非线性关系,让模型学习和表示复杂的数据模式。FFN 常见的几种激活函数包括:
- ReLU (Rectified Linear Unit)
- GELU (Gaussian Error Linear Unit)
- SiLU (Sigmoid Linear Unit)
- SwiGLU (Swish-Gated Linear Unit)
下图是常见大模型的信息,从中可以看到对激活函数的使用情况
ReLU
ReLU函数是修正线性单元函数,由Vinod Nair和 Geoffrey Hinton在论文"Rectified Linear Units Improve Restricted Boltzmann Machines"提出,它的公式为:
GLU
论文GLU Variants Improve Transformer 提出,可以利用门控线形单元 —— GLU(Gated Linear Units)对激活函数进行改进。GLU 使用由 sigmoid 函数构成的门控机制,控制信息能够通过多少。公式为:
其中,\(\otimes\) 是逐元素乘法,而 \(\sigma(\cdot)\) 是 sigmoid 或者 Tanh 激活函数。
GELU
论文“Gaussian Error Linear Units(GELUs)”提出了GELU(Gaussian Error Linear Unit,高斯误差线性单元)函数,这是ReLU的平滑版本。GELU通过高斯误差函数(标准正态分布的累积分布函数)对输入进行平滑处理,从而提高模型的性能。GELU函数的数学表达式为:
其中,\(\Phi(x)\) 是标准正态分布的累积分布函数,定义为:\(\Phi(x) = \frac{1}{2} \left( 1 + \text{erf}\left( \frac{x}{\sqrt{2}} \right) \right)\)。之前由于计算成本较高,因此论文提供了两个初等函数作为近似计算,目前很多框架已经可以精确计算。
SwiGLU
SwiGLU(Swish-Gated Linear Unit)是一种结合了Swish和GLU(Gated Linear Unit)特点的激活函数。SwiGLU其实就是采用Swish作为激活函数,且去掉偏置的GLU变体。与ReLU相比,SwiGLU可以提升模型的性能。两者的核心差异在于:
- ReLU 函数将所有的负值归 0,正数不变
- SwiGLU 设置可学习参数 \(\beta\),调节插值程度,\(\beta\) 越大则越接近 ReLU
-
Swish 函数
Swish 函数由 Google 团队在2017年在论文“Searching for Activation Functions”中提出。Swish函数的曲线是平滑的,并在所有点上都是可微的。这对模型优化很有帮助,也被任务是优于 ReLU 的原因之一。Swish 函数的数学表达式为:
\[ \text{Swish}(x) = x \cdot \sigma(\beta x) \]其中,\(\sigma\) 为激活函数 Sigmoid,定义为\(\sigma(\cdot) = \frac{1}{1+e^{-x}}\)。而 \(\beta\) 是可学习的参数,当 \(\beta = 0\) 的时候,则退化为线性函数,当 \(\beta\) 为无穷大时,则变成了 ReLU,一般设置 \(\beta = 1\),此时函数为:
\[ \text{Switch}(x) = x \cdot \sigma(x) \]这个函数也被称为 SiLU (Sigmoid Gated Linear Unit)
-
SwiGLU 激活函数
SwiGLU 函数的表达式为
\[ \text{SwiGLU}(x) = (xW_1 + b_1) \odot \text{Swish}(xW_2 +b_2) \]SwiGLU 本质上是对Transformer的 FFN 前的第一层全连接和 ReLU 进行替换。因此,整个 FFN 或包含 3 个参数矩阵,分别为升维 SwiGLU 的 \(W_1,W_2\) ,以及后面降维用的全连接层的 \(W_3\)。下图比较清晰的两种 FFN 结构的运算和参数情况(其中图中的 \(V\) 对应文中的 \(W_2\),而图中的 \(W_2\) 对英文中的 \(W_3\))。
由于 SwiGLU 的原因,FFN 从 2 个权重矩阵变成 3 个权重矩阵,会带来整个模型的参数量的增加。为让模型参数大体保持不变,一般会对 FFN 的 \(d_{ffn}\) 维度做一个缩放,把中间隐藏层的维度缩减为原来的 \(2/3\) ,也就是从 \(4d\) 变更为 \(\frac{8d}{3}\)。进一步为了使得中间层是256的整数倍,也会做取模再还原的操作。
-
LLama3 的代码实现
class FeedForward(nn.Module): def __init__( self, dim: int, hidden_dim: int, multiple_of: int, ffn_dim_multiplier: Optional[float], ): super().__init__() hidden_dim = int(2 * hidden_dim / 3) # custom dim factor multiplier if ffn_dim_multiplier is not None: hidden_dim = int(ffn_dim_multiplier * hidden_dim) hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of) self.w1 = ColumnParallelLinear( dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x ) self.w2 = RowParallelLinear( hidden_dim, dim, bias=False, input_is_parallel=True, init_method=lambda x: x ) self.w3 = ColumnParallelLinear( dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x ) def forward(self, x): return self.w2(F.silu(self.w1(x)) * self.w3(x))
改进
后续又很多 LLM 针对 FFN 做改进和优化,主要围绕“提高计算性能”和“提升表示能力”两个角度出发,特别重要的改进就是 MoE 架构,MoE 的核心思想为,动态的将不同的 token 分配跟不同的 FFN(在 MoE 中称之为专家),而 FFN 的分配也有参与训练的 “路由” 模块分配。
在推理过程中,会由路由模块控制每个 token 的专家分配,每次激活给一个或多个专家。




