碣滥 发表于 2025-6-4 20:51:26

探秘Transformer系列之(13)--- FFN

探秘Transformer系列之(13)--- FFN


目录

[*]探秘Transformer系列之(13)--- FFN

[*]0x00 概述
[*]0x01 网络结构

[*]1.1 数学表示
[*]1.2 中间层比率
[*]1.3 position-wise
[*]1.4 激活函数

[*]常见函数
[*]ReLU
[*]GLU
[*]GELU
[*]SwiGLU

[*]Swish函数
[*]SwiGLU激活函数
[*]实现

[*]dReLU


[*]0x02 实现

[*]2.1 哈佛代码
[*]2.2 llama3

[*]0x03 FFN的作用

[*]3.1 提取更多语义信息
[*]3.2 增加表达能力
[*]3.3 存储知识
[*]3.4 增加参数量
[*]3.5 小结

[*]0x04 知识利用

[*]4.1 提取步骤
[*]4.2 知识记忆

[*]键值对形式

[*]记忆网络
[*]Key-Value
[*]Key模式
[*]值向量表示的是分布
[*]分布式存储和记忆聚合

[*]知识回路
[*]注意力模块

[*]4.3 知识的定位

[*]事实的定位

[*]知识归因 (Knowledge Attribution)
[*]精炼神经元 (Knowledge Neuron Refining)

[*]关系的定位
[*]字典学习和稀疏自编码器

[*]4.3 修改知识

[*]相关路线
[*]功能
[*]分类
[*]内在知识编辑

[*]FFN
[*]注意力头
[*]ROME


[*]4.4 学习知识

[*]前向传播
[*]反向传播


[*]0x05 优化与演进

[*]5.1 MoE
[*]5.2 MemoryFormer

[*]动机与挑战
[*]原理与创新

[*]5.3 Memory Layers at Scale
[*]5.4 KAN

[*]0xFF 参考


0x00 概述

Transformer抽取“序列信息”并加工的方法包含两个环节:以原始Transformer结构的编码器为例,每一层包含multi-head self-attention block(MHSA)和一个FFN(前馈神经网络/Feed Forward Network),即在自注意力层之后,编码器还有一个FFN。
FFN是一个包含两个线性变换和一个激活函数的简单网络(linear + relu + linear),考虑注意力机制可能对复杂过程的拟合程度不够,Transformer作者通过增加两层网络来增强模型加模型的容量和非线性。

0x01 网络结构


前馈网络可以分为两种主要类型:标准 FFN 和门限 FFN。

[*]标准 FFN:这是神经网络中常用的结构,网络由两层组成,利用一个激活函数。


[*]门限 FFN(gated FFN):在标准方法之外进一步采用了门限层,这个层增强了网络控制和调节信息流的能力。
随着时间的推移,人们对这些前馈神经网络类型的偏好也发生了变化。上图的右侧显示了2022年至2024年SLM使用的前馈网络类型的趋势,标准的 FFN 正在逐步被门限 FFN 所取代。本篇我们主要介绍标准FFN,就是Transformer论文的实现。
1.1 数学表示

FFN层是一个两层的全连接层,第一层的激活函数为 Relu,第二层不使用激活函数,在两个线性变换之间除了 ReLu 还使用了一个 Dropout。

\

[*]第一个线性层。其输入\(X∈R^{d_{input} \times d_{model}}\)是多头注意力的输出,可以看作是由每个输入位置( \(d_{input}\) 行)的注意力结果( \(d_{model}\) 列)堆叠而成。第一个线性层通常会扩展输入的维度。例如,如果输入维度是 512,输出维度可能是 2048。这样做是为了使模型能够学习更复杂的函数,也是为了更好的融合前面多头注意力机制的输出内容。
[*]ReLU 激活: 这是一个非线性激活函数。此函数相对简单,如果输入是负数,则返回 0;如果输入是正数,则返回输入本身。ReLU激活使得模型能够学习非线性化能力,也可以理解为引入非线性对向量进行筛选。其数学表达为\(max(0,xW_1+b_1)\)。
[*]第二个线性层。这是第一个线性层的逆操作,将维度降低回原始维度。FFN最终得到的输出矩阵维度与输入X的维度一致。
上述结构对输入X的每一行进行相同的信息变换(行与行之间无交错,即“separately and identically”),这个线性变换在不同的位置都表现相同,只是在不同的层之间使用不同的参数,即每行(每个token)之间共享参数,但是在不同层中,学习到的参数矩阵又是不同的。我们可以将上述结构表示如下,其中,d是embedding size(Transformer中为512),\(d_{ffn}\)是FFN的隐藏层维度(Transformer中为2048)。

最终,FFN的权重体现在这两个线性层上。注意力机制是在同一特征空间内,对不同的实体进行整合,强调了不同实体之间的重要性。而FFN完成对实体从特征空间A到特征空间B的映射。二者比较的粒度不相同。另外,从T5 开始,很多模型在FFN层都不用偏置了。
1.2 中间层比率

FFN的中间比率是指中间层维数与隐含层维数之间的比值。简单而言,它决定了中间层相对于整个网络的大小。标准 FFN通常设置中间比率为4。这意味着中间层通常比隐藏层小四倍。另一方面,门限 FFN 在中间比值上表现出更大的灵活性,比如可以是从2到8的任何范围,依据模型特点进行选择。
如果中间比率调得过小,会导致模型参数变少,性能变差。如果调节过大,则会造成峰值内存过高,因此需要综合考虑。下面图提供了从2022年到2024年不同前馈网络中间比率的趋势变化。

1.3 position-wise

论文中给这个 FNN 取名为 Position-wise feed-forward networks。"position-wise"表示对序列中的每个元素(每个位置)分别采用相同的线性变换。作者强调“position-wise"是因为FFN有如下特点(此处也会和注意力机制进行一定的比对):

[*]建模只考虑单独位置。FFN层对输入矩阵每行(每个token,即每个position)对应的单个token的信息表征进行独立的非线性变换(从矩阵运算角度可以理解为变换和平移)。因为FFN是对序列中每个位置的token向量分别进行相同的操作,所以每个时刻的全连接层是可以独立并行计算的,可以提高训练和推理的速度。
[*]不会进行元素间的信息交换。Transformer已经利用注意力机制来考虑单词在不同位置的语义和依赖关系,在每个位置上把序列中的信息做了一次全局的汇聚。因为信息当到达FFN时,每个token就包括了在token层面其感兴趣的信息,序列中的上下文已经被汇聚完成,所以不需要在FFN处再进行交互(元素间的互动完全靠自注意力)。FFN所做的是在注意力层进行元素间的信息交换之后,让每个元素消化整合自己的信息,为下一层再次通过自注意力交换信息做好准备。
[*]计算颗粒度是 token 内的维度。注意力机制可以捕捉序列中的上下文关系,是对不同位置的 token 混合,其计算是以token为颗粒度。而FFN在处理序列数据时只考虑单个位置的信息,是对每个 token 不同维度上的特征进行混合(各个token之间没有进行交互),是在token内部完成特征映射。
[*]精细再加工。MHA允许模型在不同的表示子空间中学习信息,FFN则允许模型利用注意力机制生成的上下文信息,并进一步转化这些信息,从而捕捉数据中更复杂的关系。所以,在FFN中,矩阵的每一行都是独立运算,把每个token的上下文信息加工成最终需要的的语义空间向量。

另外,也可以从卷积的角度解释。关于矩阵 \(W_1∈R^{d_{input} \times d_{model}}\) 和\(W_2∈R^{d_{input} \times d_{model}}\) 的维度倒置,Transformer作者认为可以将其理解为“two convolutions with kernel size 1”,即Position-wise FFN等价于kernel_size=1的卷积,这样每个position(token)都是独立运算的。为何要指定kernel大小为1?因为如果大于1,则相邻位置之间就具有依赖性了就不能叫做position-wise了。
综上所述,FFN的本质就是一个position-wise的"升维-过激活-降回原来维度"的MLP。
1.4 激活函数

激活函数是神经网络中的非线性函数,用于在神经元之间引入非线性关系,从而使模型能够学习和表示复杂的数据模。如果没有激活函数,神经网络无论有多少层,都只能表示输入和输出之间的线性关系,这大大限制了网络处理复杂问题的能力。
常见函数

在前馈神经网络(FFN)中,有几种常用的激活函数:

[*]ReLU(Rectified Linear Unit):ReLU 就像一个开关,打开或关闭的信息流,它应用广泛。
[*]GELU(Gaussian Error Linear Unit):GELU 是一种在平滑零值和正值之间转换的激活函数
[*]SiLU(Sigmoid Linear Unit):SiLU 是一个结合了 Sigmoid 函数和线性函数特性的激活函数,其实就是\(\beta\)为1时的Swish激活函数。
这些激活函数在论文“GLU Variants Improve Transformer”中有具体论述,该论文提出使用GLU的变种(将GLU中原始的Sigmoid激活函数替换为其他的激活函数)来改进Transformer的FFN层,并列举了替换为ReLU,GELU和SwiGLU的三种变体。命名上将激活函数的缩写加在GLU前面作为前缀。论文用这种GLU变体替换FFN中的第一层全连接和激活函数,并且去除了GLU中偏置项bias。具体公式如下。

下图是常见大模型的信息,从中可以看到对激活函数的使用情况。

随着时间的推移,这些激活函数的使用发生了变化。在2022年,ReLU成为许多 FFN 的首选激活函数。然而,进入2023年,过渡到使用 GELU 及其变体GELU Tanh。到2024年,SiLU成为激活函数的主要选择。具体如下图所示。

ReLU

ReLU函数是修正线性单元函数,由Vinod Nair和 Geoffrey Hinton在论文"Rectified Linear Units Improve Restricted Boltzmann Machines"提出,它的公式为:

\[\text{ReLU}(x) = \max(0, x)\]
ReLU函数在输入大于0时输出等于输入,否则输出为0。ReLU函数的优点是计算简单,收敛速度快。相比于Sigmoid和Tanh函数,ReLU在正区间的梯度为常数1,有助于缓解梯度消失问题,使得深层网络更容易训练。但它也存在一个问题,就是在输入小于0时,梯度为0,这会导致神经元无法更新权重,从而出现“神经元死亡”的问题。
GLU

论文GLU Variants Improve Transformer 提出,可以利用门控线形单元 —— GLU(Gated Linear Units)对激活函数进行改进。GLU激活则提出于2016年发表的论文"language modeling with gated convolutional networks"中。GLU其实不算是一种激活函数,而是一种神经网络层。它是一个线性变换后面接门控机制的结构。其中门控机制是一个sigmoid函数用来控制信息能够通过多少。其公式如下:\(GLU(x, W, V, b, c) = (xW + b) ⊗ \sigma(xV + c)\)。其中 ⊗ 表示逐元素乘法,\(X\) 是输入,\(W\) 和 \(V\) 是权重矩阵,\(b\) 和 \(c\) 是偏置项。注,有论文对将GLU的门控放在了权重W的部分,即\(GLU(x, W, V, b, c) = \sigma(xW + b) ⊗ (xV + c)\)。
GELU

论文“Gaussian Error Linear Units(GELUs)”提出了GELU(Gaussian Error Linear Unit,高斯误差线性单元)函数,这是ReLU的平滑版本。GELU通过高斯误差函数(标准正态分布的累积分布函数)对输入进行平滑处理,从而提高模型的性能。GELU函数的数学表达式为\(\text{GELU}(x) = x \cdot \Phi(x)\)$。其中:

[*]\(x\) 是输入。
[*]\(\Phi(x)\) 是标准正态分布的累积分布函数,定义为:\(\Phi(x) = \frac{1}{2} \left( 1 + \text{erf}\left( \frac{x}{\sqrt{2}} \right) \right)\) 。\(\text{erf}(x)\) 是误差函数。
之前由于计算成本较高,因此论文提供了两个初等函数作为近似计算,目前很多框架已经可以精确计算。

SwiGLU

SwiGLU(Swish-Gated Linear Unit)是一种结合了Swish和GLU(Gated Linear Unit)特点的激活函数。SwiGLU其实就是采用Swish作为激活函数,且去掉偏置的GLU变体。与ReLU相比,SwiGLU可以提升模型的性能。两者的核心差异在于:

[*]ReLU 函数会将所有负数输入直接归零,而正数输入则保持不变。
[*]SwiGLU 函数含有一个可学习的参数 \(\beta\),能够调节函数的插值程度。随着 \(\beta\) 值的增大,SwiGLU 的行为将逐渐接近 ReLU。
Swish函数

Swish函数由Google团队在2017年在论文“Searching for Activation Functions”中提出,其公式和效果如下图所示。Swish函数的曲线是平滑的,并且函数在所有点上都是可微的。这在模型优化过程中很有帮助,被认为是 Swish 优于 ReLU 的原因之一。
Swish函数的数学表达式为:\(\text{Swish}(x) = x \cdot \sigma(\beta x)\),其中\(\sigma\)为激活函数Sigmoid,定义为 \(\sigma(x) = \frac{1}{1 + e^{-x}}\)。输入x和\(\sigma\)相乘使得Swish类似LSTM中的门机制,因此Swish也被成为self-gated激活函数,只需要一个标量输入即可完成门控操作。
\(\beta\) 是一个可学习的参数,控制函数的形状,通常为一个常数或者让模型自适应学习得到。当\(\beta=0\) 时,Swish退化为一个线性函数,当\(\beta\) 趋近于无穷大时,Swish就变成了ReLU。在大多数情况下,\(\beta\) 被设置为1,从而简化为$$\text{Swish}(x) = x \cdot \sigma(x)$$,也叫SiLU( Sigmoid Gated Linear Unit)。

SwiGLU激活函数

SwiGLU的数学表达式为$ f(X) = (X ∗ W + b) ⊗ Swish(X ∗ V + c) \(,\)\otimes$ 表示逐元素乘法(Hadamard乘积)。此公式也可以转换为:$$\text{SwiGLU}(a, b) = \text{Swish}(a) \otimes \sigma(b)$$,其中,\(a\) 和 \(b\) 是输入张量。\(\sigma(x) = \frac{1}{1 + e^{-x}}\) 是Sigmoid激活函数。\(\text{Swish}(x) = x \cdot \sigma(x)\) 是Swish激活函数。
SwiGLU本质上是对Transformer的FFN前馈传播层的第一层全连接和ReLU进行替换。在原生的FFN中采用两层全连接,第一层升维,第二层降维回归到输入维度,两层之间使用ReLE激活函数。SwiGLU也是全连接配合激活函数的形式,不同的是SwiGLU采用两个权重矩阵和输入分别变换,再配合Swish激活函数做哈达马积的操作,因为FFN本身还有第二层全连接,所以带有SwiGLU激活函数的FFN模块一共有三个权重矩阵,其中W1,V为SwiGLU模块的两个权重矩阵,W2为原始FFN的第二层全连接权重矩阵,Swish为激活函数。

由于SwiGLU的原因,FFN从2个权重矩阵变成3个权重矩阵,为了使得模型的参数量大体保持不变,研究人员通常会对隐藏层的大小做一个缩放,比如把中间层维度缩减为原来的2/3,每个矩阵的形状应该是 (ℎ,8ℎ/3)。进一步为了使得中间层是256的整数倍,也会做取模再还原的操作。
实现

我们使用LlamaMLP的代码来看看。在LLaMA中采用常数\(\beta\) =1,此时Swish简化为$$\text{Swish}(x) = x \cdot \sigma(x)$$,SwiGLU就是使用了nn.SiLU。
class LlamaMLP(nn.Module):
    def __init__(self, config):
      super().__init__()
      self.config = config
      self.hidden_size = config.hidden_size
      self.intermediate_size = config.intermediate_size
      self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias)
      self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias)
      self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=config.mlp_bias)
      self.act_fn = ACT2FN

    def forward(self, x):
      down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
      return down_proj从ACT2CLS可以看出来,使用了nn.SiLU。
ACT2CLS = {
    "gelu": GELUActivation,
    "gelu_10": (ClippedGELUActivation, {"min": -10, "max": 10}),
    "gelu_fast": FastGELUActivation,
    "gelu_new": NewGELUActivation,
    "gelu_python": (GELUActivation, {"use_gelu_python": True}),
    "gelu_pytorch_tanh": PytorchGELUTanh,
    "gelu_accurate": AccurateGELUActivation,
    "laplace": LaplaceActivation,
    "leaky_relu": nn.LeakyReLU,
    "linear": LinearActivation,
    "mish": MishActivation,
    "quick_gelu": QuickGELUActivation,
    "relu": nn.ReLU,
    "relu2": ReLUSquaredActivation,
    "relu6": nn.ReLU6,
    "sigmoid": nn.Sigmoid,
    "silu": nn.SiLU,
    "swish": nn.SiLU,
    "tanh": nn.Tanh,
}
ACT2FN = ClassInstantier(ACT2CLS)dReLU

研究人员一直没有停止优化的脚步,比如,因为激活稀疏性可以在不影响性能的情况下显著加速大型语言模型的推理过程,所以论文 Turbo Sparse: Achieving LLM SOTA Performance with Minimal Activated Parameters 提出了一种新的dReLU函数,该函数旨在提高LLM激活稀疏性(实现了接近90%的稀疏性)。dReLU公式和效果如下。

0x02 实现

2.1 哈佛代码

两个线性层的特点如下,其中B为batch_size,L是seq长度,D是特征维度。
名称算子类型输入形状权重形状输出形状其他说明FFN expansiondense(B, L, D)(D, 4D)(B, L, 4D)维度扩增到4DFFN contractiondense(B, L, 4D)(4D, D)(B, L, D)维度缩减回D代码实现非常简单:
# 定义一个继承自nn.Module,名为PositionwiseFeedForward的类来实现前馈全连接层
class PositionwiseFeedForward(nn.Module):

    def __init__(self, d_model, d_ff, dropout=0.1):
      """
      d_model:线性层的输入维度也是第二个线性层的输出维度
      d_ff:隐层的神经元数量。是第二个线性层的输入维度和第一个线性层的输出维度
      dropout:置0比率
      """   
      super(PositionwiseFeedForward, self).__init__()
                # 使用nn.Linear实例化了两个线性层对象,self.w1和self.w2
      self.w_1 = nn.Linear(d_model, d_ff) # 第一个全连接层,输入维度为d_model,输出维度为d_ff
      self.w_2 = nn.Linear(d_ff, d_model) # 第二个全连接层,输入维度为d_ff,输出维度为d_model
      self.dropout = nn.Dropout(dropout) # 定义一个dropout层,dropout概率为传入的dropout参数

    # 前向传播方法   
    def forward(self, x):
      """输入参数为x,代表来自上一层的输出"""
      """
      操作如下:
      1. 经过第一个线性层
      2. 使用Funtional中relu函数进行激活,公式中的max(0, xW+b)其实就是ReLU的公式
      3. 使用dropout进行随机置0
      4. 通过第二个线性层w2,返回最终结果
      """      
      return self.w_2(self.dropout(self.w_1(x).relu()))2.2 llama3

llama3的实现如下,其使用ColumnParallelLinear和RowParallelLinear这样分布式线性层。从llama的源码中可以看到,其有三个w参数需要训练。
class FeedForward(nn.Module):
    def __init__(
      self,
      dim: int,
      hidden_dim: int,
      multiple_of: int,
      ffn_dim_multiplier: Optional,
    ):
      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))0x03 FFN的作用

论文"Attention is Not All You Need: Pure Attention Loses Rank Doubly Exponentially with Depth"中发现,如果不加残差和FFN,堆叠再多层自注意力,整个模型的秩也会很快坍缩,也即所有表征趋于一个向量,模型都会变得不可用。因此,Attention, FFN, ResNet 可以被认为是 Transformers 架构的三驾马车,各司其职、缺一不可。了解了FFN的重要性,我们再来看FFN的几个作用:

[*]提取更多语义信息。
[*]增加表达能力。
[*]存储知识。
我们接下来一一进行分析。
3.1 提取更多语义信息

我们再来看看一个问题,为什么FFN要先升维后降维?具体分析如下:
LLM在自己构造的高维语言空间中,通过预训练,记录了人类海量的语言实例,从中提取了无数的结构与关联信息。我们可以把这个高维的语言空间,加上训练提取的结构与关联信息,理解为LLM的脑。而FFN就是提取信息的关键模块。FFN在把输入的词向量映射到输出的词向量的过程中,将多头注意力学到的东西进行一波混合操作,以提取更丰富的语意信息,混合操作具体分为两步:

[*]升维。其主要作用是拟合一个更高维的映射空间,从而提升模型的表达能力和拟合精度。

[*]第一个线性层及激活函数组合,可以看作是在学习一组基函数,每个神经元可以视作一个简单的分类器,用以近似输入数据的高维映射。升维操作有效扩展了网络的自由度,使得模型能够学习更多的特征表示,从而提升模型的拟合能力。从一维卷积的角度看,升维可以提取更多的特征,以哈佛代码为例,就是使用了2048个的卷积核来提取特征。
[*]升维把输入的词向量映射到一个更大维度的特征空间。FFN并非简单的直接在输入维度这个嵌入空间上进行建模,而是通过一系列线性变换来拟合一个高维的映射空间。若仅使用线性基,理论上我们只需使用等同于输入维度的基数量。然而,所有可能的平滑映射组成的空间是无限维的,因而需要通过升维来完整表示这一空间。

[*]降维。其主要作用是还原维度,限制计算复杂度。

[*]降维可以将维度还原,让下一层能够继续计算,从而保证encoder layer和decoder layer能够堆叠。
[*]降维可以浓缩特征,防止过拟合。尽管升维带来更多的特征表示,但隐藏维度(或键值对数量)并非越大越好。过多的隐藏维度可能导致信息瓶颈和过拟合,甚至使模型难以有效传递信息。
[*]降维可以限制计算复杂度。尽管升维有助于捕捉更多的信息,但理论上需要无限多的自由度来表达完整的光滑映射。然而,实践中我们不可能拥有无限的计算资源,因此必须通过降维来控制网络的规模和计算复杂度。降维操作通过将高维表示映射回较低维空间,有效地控制了模型的复杂度。

3.2 增加表达能力

Transformer架构中的非线性特征对Transformer模型的能力有重大影响。增强非线性可以有效地缓解特征坍塌的问题,并提高Transformer模型的表达能力。
注意力机制本质上是对Value的线性变换。虽然变换的权重是通过非线性的softmax计算得到,但是对于 value 来说,并没有任何的非线性变换。每一次 Attention 的计算相当于是对 value 代表的向量进行了加权平均,即使堆叠多个 Self Attention,依然只是对 value 向量的加权平均而已,无法处理一些非线性的特征。因此,无论堆叠多少层,都是最开始输入 x 的一个线性变换,其整体运算仍然是线性的,和单层变化没有本质区别,则其假设空间受限,无法充分利用多层表示的优势。
FFN中的激活函数是一个主要的能提供非线性变换的单元。通过它可以增加特征学习能力。非线性激活函数的引入打破了线性模型的限制,使得模型可以对数据进行更复杂的变换。降维操作将升维后的结果映射回原始维度,从而将这些非线性特征组合到最终的输出中。这种操作增强了模型的表达能力,使其能够表示更加复杂的函数关系。这就是 FFN 必须要存在的原因,或者说 FFN 提供了最简单的非线性变换。
3.3 存储知识

大型语言模型的强大能力离不开其对知识的记忆:比如模型想要回答“中国的首都是哪座城市?”,就必须在某种意义上记住“中国的首都是北京”。Transformer并没有外接显式的数据库,记忆只能隐式地表达在参数当中。而记忆可以通过两个基本能力实现普遍计算:递归状态维护和可靠的历史访问。
真正学到的知识或者信息大多都存储在 FFN 中。从某个角度来看,FFN可以类比为一种键值对存储结构。第一个线性层生成“键”,即为每个token计算一组召回权重。第二个线性层则计算“值”,并与召回权重进行加权求和。这种方式类似于通过大规模的记忆存储(升维)来提升网络的长期记忆能力。
但是,FFN这种存储是分布式的,或者说是多义的,即面对看似不相关的输入,神经元都会做出反应。特征与输出结果有因果关系,但特征与神经元并不对应。关于多义性的成因,有一种理论称为叠加假说(superposition):神经网络通过存储多个特征的线性组合的方式来表示比其神经元更多的独立的特征。如果我们将每个特征视为一个神经元对应的向量,那么这些特征组成了激活空间上的一组过完备基。对模型性能有帮助的特征,如果其在训练数据中的频率是稀疏的,那么在神经网络训练过程中会自然出现叠加现象。与压缩感知一样,给定任意的激活空间中的向量,稀疏性允许模型消除叠加现象带来的歧义。另外,根据交叉熵损失训练的模型通常更倾向于用多义表示更多特征,而不是单义表示较少的 "真实特征",即使在稀疏性约束使得叠加不可能的情况下也是如此。
既然FFN是存储知识的模块,那就意味着其难以压缩和加速,因为:如果FFN变小,则意味着模型容量变小,从而导致模型性能变差。而且FFN中间的激活难以看出低秩,没法加速。
3.4 增加参数量

大模型的涌现现象是一个复杂且引人入胜的话题。其产生原因主要与参数量有关。当大模型的训练参数达到一定规模时,模型内部各组件之间的相互作用开始显现。这种相互作用随着参数数量的增加而逐渐增强,最终可能导致模型整体性能的显著提升,即涌现现象。因此,参数量对于大模型至关重要。语言模型中的参数数量决定了语言模型在训练期间学习和存储信息的能力。更多的参数通常允许模型覆盖更多知识维度,捕获更复杂的模式和细微差别,从而提高语言任务的性能。
使用FNN替代RNN有个好处就是可以避免参数稀疏化,我们都知道CNN和RNN都是具备参数共享功能的,这种参数共享在处理简单任务的时候,可能具备一定的好处,但是在处理复杂任务的时候,参数的共享可能不会带来优势,反而是稠密连接的FNN有更大的优势,稠密连接意味着参数量的增加,而参数量的增加,至少可以让模型可承载的信息量变大。
不考虑词嵌入层,一个transformer架构的模型里,FFN 和 Attention 参数占据了模型参数的绝大部分,基本上超过了 90%。其中 FFN 和 Attention 参数量比例接近 2:1。或者可以说,前馈层占了模型大约三分之二的参数量。我们可以使用PyTorch快速获得答案。
import torch.nn as nn

def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

d_model = 512
n_heads = 8
multi_head_attention = nn.MultiheadAttention(embed_dim=d_model, num_heads=n_heads)
print(count_parameters(multi_head_attention))# 1050624
print(4 * (d_model * d_model + d_model))# 10506243.5 小结

最后,我们总结一下在Transformer模型中,为何要区分MHA和MLP?其原因就是这两个核心组件各有分工又彼此配合。Transformer用embedding解决无法定义的概念,用MHA+FFN来解决不能用已有运算符来表达的计算和变化。

[*]MHA考虑单词在不同位置的语义和依赖关系,并使用这些信息来捕捉句子的内部结构和表示,MHA是Transformer中最靓的仔。
[*]FFN 允许模型利用注意力机制生成的上下文信息,并进一步整合和转化这些信息,从而捕捉数据中更复杂的关系,为学习过程提供了深度和复杂性。同时FFN也提供了存储知识的场所。FFN是Transformer模型中的无名英雄。
这两者共同工作以提高模型的性能。
0x04 知识利用

既然提到了FFN是用来存储知识的,我们就来做进一步的分析。
知识被定义为对事实、概念等的认知和理解。掌握知识一直是人工智能系统发展的核心追求。在人工智能快速发展的今天,LLM展现出了令人惊叹的能力,经常被视为支撑知识导向任务的虚拟知识库,或者说,Transformer 的出色表现一定程度上要归功于其海量参数中存储的丰富信息,包括但不限于语言学知识、常识、算术知识以及世界知识等。然而,在这些表面性能的背后,LLM学习、存储、利用知识以及知识的动态演化规律依然是未解之谜。比如,针对“刘翔出生在哪个城市?”之类的问题,我们无法判断模型是真正理解它所处理的概念,并且基于内部知识和逻辑推理得到的答案,还是单纯因为该问题在训练集中出现过而依据表层的统计模式匹配之后输出答案。因此,我们需要探寻语言模型中概念形成、对齐及其认知机制的内在规律,需要探寻 LLMs 存储和管理事实知识的机制。
另外,尽管 LLMs 具有巨大的潜力,但直接将它们视作新一代知识库仍然存在某些局限,通常表现为实际应用中输出不准确或者错误的结果。而一个理想的知识库,不仅能够存储大量信息,还允许对其中的信息进行高效且有针对性的更新,以纠正这些错误并提高准确性。为了弥补这一差距,针对 LLMs 的知识编辑领域应运而生。这种方法旨在在保持模型处理通用输入的总体性能的同时,高效地改进 LLM 在特定领域的表现。
我们接下来从几个角度来学习下模型如何在 Transformer 的复杂架构中有效地检索、处理和运用已学习的信息,即如何更好的利用知识,具体包括。

[*]记忆,指模型如何存储知识。
[*]定位,指模型如何回忆基本知识。
[*]修改,指模型如何修改存储的某个知识。
4.1 提取步骤

我们首先看看知识提取的步骤,不同论文提出了不同思路和方案。

论文"Dissecting Recall of Factual Associations in Auto-Regressive Language Models"通过对信息流的分析,揭示了一个属性提取的内部机制。我们用实例进行说明,假设输入的prompt是"Beat music is owned by",LLM返回的正确答案应该是”apple"。和很多方案一样,此论文也把知识抽象成如下三元组 (
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 探秘Transformer系列之(13)--- FFN