Transformers实战——深入剖析 Transformer

111 阅读25分钟

本章内容包括

  • Transformer 之前的序列建模
  • Transformer 模型的核心组件
  • 注意力机制及其变体
  • Transformer 如何帮助稳定梯度传播

如果你用过基于 Transformer 的工具(如 ChatGPT),你就亲身体验过大语言模型(LLM)在理解与生成自然语言方面的高效表现。但若想在自己的任务中真正用好这些模型,仅仅引入一个现成的流水线还不够。无论是微调 LLM、排查意外的性能问题、优化 GPU 资源,还是探索专家混合(MoE)LoRA 等参数高效技巧,你都需要扎实理解 Transformer 的内部机理。

本章将把看似复杂的 Transformer 架构拆解为若干基础概念:自注意力(self-attention)多头注意力(multihead attention)前馈网络(FFN)位置编码(positional encoding) 。理解这些核心组件,不仅能让你更有把握地使用现成语言模型,也能让你在真实生产场景中因地制宜地改造与优化它们。

2.1 从 Seq-2-Seq 到 Transformer

正如前文所述,在 Transformer 之前,机器翻译任务通常采用 RNN 编码器—解码器架构:读取源语言句子,构建其定长表示,再传给解码器(见图 2.1)。

image.png

图 2.1 序列到序列学习示意。RNN 编码器接收输入序列 x,构造中间的定长表示 z。解码器随后基于该中间表示预测输出。

解码器以自回归方式逐词生成目标语言句子,依赖先前生成的词与编码器的定长表示。Seq-to-Seq 建模对需要捕捉上下文的任务至关重要。它的关键优势是能够理解句子的语境语义,这对翻译尤为重要。比如 “date” 既可指也可指约会,在翻译中显然不能混为一谈。

2.1.1 训练 RNN 的困难

训练基于 RNN 的编解码架构相当具有挑战性,尤其在不同语序词汇差异巨大的语言之间进行翻译时。困难往往源自长时间序列中的误差传播。要缓解这些问题,需要谨慎的参数初始化非饱和激活函数,以避免梯度陷在特定区间,从而在反向传播时维持高效的梯度流;再配合批归一化以提升网络稳定性。在 LSTM 语境下,“稳定性”指模型能够准确可靠地学习与表征数据模式与关系,而不至于对输入中的微小变化或噪声过度敏感。关于训练 RNN(尤其是深层 RNN)的挑战,可参见 Glorot 等人的论文《On the Difficulty of Training Recurrent Neural Networks》的系统分析。

为了建模复杂模式与长期依赖,往往需要更深的网络。在 LSTM 中,这通常通过堆叠多个循环层来实现,使网络能随时间学习到越来越复杂的表征。然而,层数增加会使训练时的误差信号更难顺利传播,进而导致稳定性问题,如我们稍后将讨论的梯度消失

2.1.2 引入注意力机制

尽管 RNN 可以被设计为有选择地记忆/遗忘较早时间步的信息,但它们仍难以有效地学习这类依赖。将注意力机制引入 LSTM 有助于建模长期依赖。不过,LSTM 中的注意力仍然在顺序输入的框架下运行,训练与推理都较慢,因此不如 Transformer 中的注意力高效。

相较之下,Transformer 采用的注意力架构能够更高效地处理远距离依赖(见图 2.2)。Transformer 的另一个优势是多头注意力,可并行关注输入数据的不同侧面,进一步增强对长程依赖的建模能力。如图所示,Transformer 提供了一种无需顺序处理或深度堆叠也能捕捉长期关系的替代路径。

image.png

图 2.2 RNN(左)与自注意力(右)的对比。Transformer 通过自注意力可以直接关注序列中的所有位置,不受时间先后限制。虚线表示信息流/依赖关系。x1 到 x5 是输入,RNN 上方圆圈表示各时间步的网络状态;自注意力上方圆圈表示每个位置可综合考虑所有其他位置的信息。

尽管 Transformer 在许多任务上展现出惊人的效果,但 RNN/LSTM 仍有其适用场景,例如某些具有特定时序特性的时间序列预测。理解 RNN 的优劣,有助于我们理解为何新的技术路线在深度学习中是一次重要飞跃。

2.1.3 梯度消失:Transformer 来解困

基于 RNN 的模型存在一个固有局限——梯度消失问题:这会使误差难以在网络中有效反传、参数难以更新,从而难以学习长期依赖、难以正确建模序列数据(见图 2.3)。所谓梯度消失,是指梯度变得非常小,以致网络权重更新无效,训练缓慢性能不佳

image.png

图 2.3 梯度消失的简化示例:越靠左的早期步骤,其梯度贡献越微小。

Transformer 的架构通过注意力机制连接输入序列的所有位置(见图 2.2),在很大程度上化解了这一问题。图 2.4 基于一句超长示例句对比了 LSTM 与 Transformer 的梯度:随着句子变长,LSTM 的梯度会显著变小。你可以在本书仓库中运行代码,亲自观察句长变化如何影响两种架构的梯度。

既然注意力机制能够缓解梯度消失,接下来我们将审视 Transformer 的整体架构:先看两大组件——编码器(encoder)解码器(decoder) ——如何协同把输入序列变为输出序列;再深入关键创新——自注意力;最后介绍位置编码的工作原理。

image.png

图 2.4 基于长句示例对比 LSTM 与 Transformer 的梯度分布。

2.1.4 梯度爆炸:当过大的梯度扰乱训练

与梯度消失相反,梯度爆炸也常见于 RNN:反向传播时梯度过大,导致权重更新剧烈且不稳定,学习出现发散/抖动,甚至无法收敛。

例如,在训练 seq-to-seq 模型时,如果数据是极长、强重复的文本,或包含极端数值的序列,随着序列长度与复杂度增加,每个循环步都会放大梯度,使其量级复合增长。若不采用**梯度裁剪(gradient clipping)**等缓解手段以限制梯度的幅度,过大的梯度会令权重变化过猛,训练过程不稳定,难以有效学习长期依赖。梯度裁剪就是在训练中对梯度大小设定上限,从而稳定学习过程、避免梯度爆炸。

虽然基于自注意力的 LLM 由于非顺序连接、可直接跨位置建模而在很大程度上规避了上述问题,但理解梯度消失梯度爆炸这两类现象,对于全面把握 Transformer 所带来的改进至关重要。

2.2 模型架构

尽管 Transformer 与传统 RNN 的范式大相径庭,它在核心上仍遵循编码器—解码器(encoder–decoder)框架。这也从侧面印证了该范式的稳健性:至今,它依然是前沿模型的坚实基础。

Transformer 之所以与众不同,在于它在编码器解码器两侧都采用了堆叠的注意力层逐位置(point-wise)的全连接层。如图 2.5 左右两部分所示,这些架构选择造就了一个高度灵活、可扩展的模型,能在各类序列到序列(seq2seq)预测任务中表现出色。这种可扩展性与灵活性凸显了 Transformer 架构的真正威力,使其成为快速演进的 AI 版图中的基石

image.png

图 2.5 图左为 Transformer 架构的编码器,图右为解码器

有了总体结构示意,我们继续深入各组件。下面从编码器的具体结构与作用讲起,再转向 Transformer 模型的其他关键要素。

2.2.1 编码器与解码器堆叠(stacks)

Transformer 的总体结构由编码器与解码器两部分构成(见图 2.5)。

编码器(Encoder)

在 Transformer 中,编码器负责处理输入序列。它由若干层堆叠而成;每一层包含两个主要子层

  1. 多头自注意力(multihead self-attention)
  2. 逐位置前馈网络(position-wise FFN)

自注意力首先为输入序列的不同部分分配不同的“注意力”权重;随后 FFN 对经过注意力处理的表示进行非线性变换。

编码器设计中的一大亮点是残差连接(residual connections) 。不是简单把子层输出传给下一个子层,而是将该输出与其原始输入相加再传递。这就像在每一步都把“原句的信息”叠加到“处理后的序列”上,确保输入的初始上下文在整个编码过程中被保留与融合,从而维持语义的连贯性,避免丢失原文本中的固有含义与关系。

更具体地说,编码器由 N = 6 个相同的层构成。每层都含有上述两个子层:多头自注意力逐位置 FFN(本节稍后详述)。

建立在残差连接之上,信息可以直接从输入穿过网络的不同部分,这种“捷径”有助于绕过某些变换,保留关键信息。实践中,每个子层的输出形式为
LayerNorm(x + Sublayer(x)) ,其中 Sublayer(x) 表示该子层执行的变换。为增强鲁棒性并抑制过拟合,通常会在最终相加前施加 dropout

编程实现上,可参考下列简化的编码器层示例:

代码清单 2.1 简化的编码器层示例

class EncoderLayer(nn.Module):
    def __init__(self, d_model, nhead, dim_feedforward, dropout=0.1): 
        super().__init__()

        self.self_attn = nn.MultiheadAttention(
            d_model, nhead, dropout=dropout)   #1

        self.feed_forward = nn.Sequential(     #2
            nn.Linear(d_model, 2 * dim_feedforward),
            RELU(input_size=dim_feedforward, output_size=d_model), 
            nn.Dropout(dropout)
        )

        self.norm1 = LayerNorm(d_model)        #3
        self.norm2 = LayerNorm(d_model)
        self.dropout1 = nn.Dropout(dropout)    #4
        self.dropout2 = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        # self-attention layer                    #5
        attn_output, _ = self.self_attn(x, x, x, attn_mask=mask) 
        x = x + self.dropout1(attn_output)
        x = self.norm1(x)

        # feed-forward layer                      #6
        ff_output = self.feed_forward(x) 
        x = x + self.dropout2(ff_output) 
        x = self.norm2(x)
        return x

模型中的所有子层与嵌入层都会产生相同维度的输出,典型设定为 d_model = 512。这种统一性使残差连接能够顺畅工作:信息可从输入直达输出而跨越中间层。把“原输入”加到“层输出”上,得到的结果相当于两者之和,这能提升模型的可学习性训练稳定性,并带来更好的性能。

解码器(Decoder)

与编码器类似,解码器同样是 Transformer 的关键部分,负责基于编码器处理过的信息生成输出序列。解码器的每一层也包含与编码器类似的自注意力FFN两个主件;但与编码器不同的是,解码器还引入了第三个组件
跨注意力(encoder–decoder attention) ,即对编码器最后一层输出做多头注意力。

这一额外组件使解码器能在生成时有选择地对齐并利用输入序列的不同部分

解码器同样使用残差连接与**层归一化(LayerNorm)**来保持上下文并增强训练稳定性。

解码器的一项显著特性是带掩码的自注意力(masked self-attention) :在为某一位置生成输出时,模型只能利用先前已知的输出,以确保生成的因果性与顺序一致性。

解码器同样由 N = 6 个相同的层组成。与只有两类子层的编码器不同,解码器多出一类跨注意力子层以关注编码器的输出。它为解码器提供了更宽的上下文,从而丰富生成结果。

image.png

图 2.6 展示了多头注意力中的掩码机制

image.png

图 2.7 则展示了解码器组件的细节结构

在 seq2seq 任务中,掩码(masking)至关重要:模型需逐 token地、基于先前 token 生成后续 token。根据不同应用,还会使用padding mask序列(因果)mask等不同类型的掩码。

2.2.2 位置编码(Positional Encoding)

Transformer 没有循环结构,而循环正是传统上理解序列中先后顺序的常用机制。为弥补这一点,Transformer 引入了位置编码来表征 token 在序列中的绝对或相对位置。没有位置编码,模型将无法区分 token 的先后次序,进而可能误解序列含义。显式纳入位置信息,能帮助模型更准确地推断 token 间的关系与语义

具体做法是:将位置编码输入嵌入相加。其中,每个位置的每个维度通过正弦/余弦函数进行编码,使模型能够以线性函数的形式关注到词与词之间的相对位置(依赖位置索引 pos维度索引 i)。其数学公式见公式 2.1

image.png

随后,模型把位置编码与输入嵌入相加,即可在后续层中利用位置信息理解上下文。

image.png

图 2.8 展示了位置编码的示例,其中不同维度对应不同频率与相位的正弦波。

下面给出一个依据公式 2.1 的位置编码示例实现:

代码清单 2.2 位置编码的示例实现

class PositionalEncoding(nn.Module): 
    """Positional encoding class."""
    def __init__(self, num_hiddens, max_len=1000): 
        super().__init__()
        self.dropout = nn.Dropout(0.1)
        # Create a positional embedding matrix
        position = torch.arange(max_len, dtype=torch.float32).reshape(-1, 1) 
        div_term = torch.exp(
            torch.arange(0, num_hiddens, 2, dtype=torch.float32) *
            -(math.log(10000.0) / num_hiddens)
        )

        pe = torch.zeros((1, max_len, num_hiddens)) 
        pe[0, :, 0::2] = torch.sin(position * div_term) 
        pe[0, :, 1::2] = torch.cos(position * div_term) 
        self.register_buffer('pe', pe)

    def forward(self, X):
        X = X + self.pe[:, :X.shape[1], :].to(X.device) 
        return self.dropout(X)

如果你想进一步直观理解位置编码,建议直接使用上述 PositionalEncoding 类(本书仓库亦提供:github.com/Nicolepcx/t…),改变模型输入,观察可视化曲线如何随之变化。

2.2.3 注意力(Attention)

将基于 Transformer 的模型与众不同的关键要素,是注意力机制,尤其是其中的自注意力(self-attention)变体。该组件在推动自然语言处理(NLP)任务方面功不可没。本节我们将深入剖析自注意力多头注意力(multihead attention) ,力图揭开其工作机理,并说明它们如何赋能 Transformer 架构。

“自注意力”之名来自这样一个事实:注意力权重是在同一个序列内部计算得到的。当一个输入序列通过多头自注意力层时,注意力权重是在该序列的各个位置之间计算的。自注意力使输入序列中的每个元素都能与序列中的所有其他元素(包括它自身)建立联系,因此输出是对整个序列的加权表示。

缩放点积的基础

自注意力与缩放点积注意力(scaled dot-product attention) 是 Transformer 中两个密切相关的概念,用于高效、有效地学习序列内部元素之间的关系。

为理解二者,我们先从缩放点积注意力的最简要组件入手,并配合图示来直观把握。设想我们有5 个输入5 个输出的序列,如图 2.9 所示。借助该可视化,我们可以考察缩放点积注意力的运作机制,以及它如何塑造 Transformer 内部的交互。

image.png

图 2.9 五个输入与五个输出的序列示意

为计算 (y3y_3),我们用向量 (x3x_3) 来决定相关权重:对 (x3x_3) 与序列中每个向量(从 (x1x_1) 到 (x5x_5))分别做点积,如图 2.10 所示。

image.png

图 2.10 通过与各向量点积来计算 (y3y_3) 的权重的图示

得到这 5 个权重后,对它们施加 softmax 使其和为 1;随后用这些权重分别乘以对应的输入向量并求和,即得向量 (y3y_3)。该过程见图 2.11。

image.png

图 2.11 计算 (y3y_3):先对所有权重做 softmax(和为 1),再用该权重对各输入向量加权求和

此外,我们会将每个输入向量 (xix_i) 通过矩阵乘法投射成三种角色:

  • 查询(query) :与序列中所有向量比较,计算其自身输出 (yiy_i) 的注意力权重;
  • 键(key) :被其他位置的查询拿来比较,用以计算输出 (yiy_i) 的权重 (wijw_{ij});
  • 值(value) :被加权求和的内容本体,形成注意力加权和的结果。

对 Query / Key / Value 的直观理解

用句子 “The movie was not bad” 举例:

  • Query(查询) :我们想找什么。可把它理解为“搜索词”。若我们关心 “not”“bad”“movie” 的相互关系,就可把这些词当作查询。
  • Key(键) :每个词的特征标识。它决定该词对当前查询的相关性。若查询是 “not”,那么每个词的 key 都会反映其与 “not” 的关联强弱。
  • Value(值) :我们要取回/加权内容。当根据 key 判断出各词与查询的相关性后,value 就是被加权求和的“信息载体”。在很多实现中,初始的 value 通常就是输入嵌入;随着层数堆叠,它们会逐渐演化为更细腻的上下文表示

在该例中,如果查询是 “not”,注意力机制可能会给 “bad” 较高的权重,因为 “not bad” 是常见搭配。于是与 “not”“bad” 对应的 value 按权重相加,生成 “not” 的最终上下文表示。

需要注意的是,在实际模型中,序列里的所有词同时充当查询、键和值。自注意力为序列里每个词,依据其查询表示与所有键表示的注意力分数,对所有值做加权求和,从而让每个词从全句收集信息,生成新的上下文嵌入

数学上可表示为:

  • 查询:(qi=WQxi\mathbf{q}_i = W_Q \mathbf{x}_i)
  • 键:(ki=WKxi\mathbf{k}_i = W_K \mathbf{x}_i)
  • 值:(vi=WVxi\mathbf{v}_i = W_V \mathbf{x}_i)

因此,可将流程理解为矩阵乘法:用三组权重矩阵把输入向量分别投射为 QKVQ、K、V;随后通过点积softmax 得到注意力分布,并据此对 VV 加权求和,得到第 (i) 个位置的输出 (yi\mathbf{y}_i)(公式见式 2.2)。

image.png

核心在于:Q/K/V 的线性变换点积 + softmax 的组合,使模型得以捕捉序列中的复杂关系,这正是 Transformer 在 NLP 任务上表现优异的根基。

缩放点积注意力(Scaled Dot-Product Attention)

在理解了 Q、K、V 的基础后,我们来看更精妙的一步:缩放点积注意力。其整体设计如图 2.12 所示。

image.png

图 2.12 缩放点积注意力示意

该机制之所以称为“缩放”,是因为注意力权重的计算方式为:对每一对查询向量与键向量做点积,再除以键向量维度的平方根(见式 2.3),然后再过 softmax

image.png

此“缩放”的意义在于:它能在训练的反向传播阶段稳定梯度。通过用 (dk\sqrt{d_k}) 缩小点积值的幅度,可以避免 softmax 饱和抑制梯度爆炸。其效果如图 2.13 所示。

image.png

图 2.13 缩放后点积的方差更小,说明缩放有助于控制点积取值的方差,从而在反向传播时抑制梯度爆炸,提升训练稳定性与收敛性

若不缩放,点积值过大将导致梯度过大,引发训练不稳定。通过控制点积值的方差,我们间接控制了反传时梯度的量级。Transformer 中常用的缩放因子是键向量维度的平方根(例如 (dk=512d_k=512) 时用 (512\sqrt{512}),实践证明效果良好。

下面给出一个简化实现(清单 2.3)。为简便起见,假设训练阶段权重矩阵已学习好,我们直接使用词向量嵌入;实际中,这些嵌入由编码器产生。

清单 2.3 缩放点积注意力的简化实现

embed_1 = np.array([0, 1, 0])  #1
embed_2 = np.array([1, 0, 1])
embed_3 = np.array([0, 1, 1])
embed_4 = np.array([1, 1, 0])

embeddings = np.array([embed_1, embed_2, embed_3, embed_4])  #2

Wq = rand(3, 3)  #3
Wk = rand(3, 3) 
Wv = rand(3, 3)

Q = embeddings.dot(Wq)  #4
K = embeddings.dot(Wk)
V = embeddings.dot(Wv)

attention_scores = softmax(Q.dot(K.T) / sqrt(K.shape[1]), axis=1)  #5

attention_output = attention_scores.dot(V)  #6

小结一下:当 (d_k) 很大时,点积的量级也会变大,导致softmax 输入过大(发生饱和,丢失区分度)以及反传梯度过大(梯度爆炸)。用 (\sqrt{d_k}) 缩放能有效控制进入 softmax 的数值范围,从而稳定梯度避免饱和,保留输入间的原始关系。

为进一步说明注意力机制的工作流,图 2.14 给出了从输入文本到上下文嵌入的高层流程;得到上下文嵌入后,我们即可计算注意力得分(图 2.15)。

image.png

图 2.14 生成上下文嵌入的计算流程

image.png

图 2.15 注意力得分的计算:基于全序列为每个 token 形成上下文相关的表示

步骤概览:

  1. 分词与嵌入:将输入文本转为 token IDs(按词或子词视分词法而定),经嵌入层得到 token 向量;训练中,嵌入层会学习各 token 的最优表示。
  2. 位置编码:并行生成正弦位置编码并与 token 嵌入相加,使表示同时包含语义位置信息
  3. 线性映射为 Q/K/V:用已初始化(并可学习)的权重矩阵将位置编码后的嵌入分别线性变换为 Q、K、V。这三者是注意力机制的核心,使模型能动态评估并分配序列不同部分的重要性。
  4. 注意力计算:计算 (QK^\top),按 (\sqrt{d_k}) 进行缩放,对结果施加 softmax 得到归一化的注意力分布;再用该分布与 V 相乘,得到加权和作为注意力输出,即每个 token 的上下文相关表示。

自注意力(Self-Attention)

自注意力允许 seq2seq 模型在生成输出时,对输入序列的不同部分赋予不同权重。图 2.16 展示了我们在第 1 章句子 “The movie was not bad.” 上得到的自注意力权重矩阵分布。

image.png

图 2.16 自注意力的权重分布

在 Transformer 中,输入序列的每个元素(词)都可以关注序列的所有位置,从而确定对当前语境而言哪些位置更关键。并且这一过程在不同位置上共享同一套参数,保证机制的一致性。自注意力使模型能聚焦对当前步骤最相关的输入片段,从而突出并保留关键的上下文信息。

在 Transformer 的实现里,计算 Q/K/V 所用的线性层权重常被组织为一个大矩阵(参见《Attention Is All You Need》[1]),实际做法是将针对 Q、K、V 的线性变换权重拼接在一起,再在通道维上拆分为多个头进行注意力计算;各头的结果拼接后,再经一层线性变换得到输出。

此外,自注意力在数值上更为高效:它避免了为输入序列的每个位置单独计算矩阵的需求;并且由于任何位置都可响应其他任意位置,它对长期关系的建模更加灵活。

多头注意力(Multihead Attention)

为了让 LLM 同时理解一个词在句子中的不同关系,我们使用多头注意力:将查询、键、值通过不同的线性投影映射到 (hh) 个子空间(见式 2.4),分别做注意力,再把结果合并。

image.png

其中 (h=8h=8),(dkd_k) 为 key 的维度,(dvd_v) 为 value 的维度,(dmodeld_{\text{model}}) 为模型隐藏维度。

随后将各头输出拼接并再经线性变换得到最终结果。之所以称为“多头”,是因为模型可以同时从输入的**不同表示/视角(subspaces)**看信息。

采用多头注意力的一个关键原因是:不同的头可以学习不同类型的关系。例如,某些头偏向句法关系,另一些头偏向语义关系;有的着眼短程依赖,有的专注长程依赖。这种分工使模型能捕获比单头更丰富的信息;而单头往往只能得到一种“平均视角”。

图 2.17 与公式 2.5 清晰地表明:在多头注意力中,我们把 Q、K、V 分拆到多个头上分别计算注意力。

image.png

图 2.17 多头注意力示意

image.png

其中各投影均为可学习的参数矩阵

image.png

下面用 PyTorch 的函数式 API 给出一个简化代码示例。

清单 2.4 简化的多头注意力实现

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads): 
        super(MultiHeadAttention, self).__init__() 
        assert d_model % num_heads == 0

        self.d_model = d_model 
        self.num_heads = num_heads 
        self.d_k = d_model // num_heads

        self.W_q = nn.Linear(d_model, d_model) 
        self.W_k = nn.Linear(d_model, d_model) 
        self.W_v = nn.Linear(d_model, d_model) 
        self.W_o = nn.Linear(d_model, d_model)

    def forward(self, query, key, value, mask=None): 
        batch_size = query.size(0)

        query = self.W_q(query).view(batch_size, -1, 
            self.num_heads, self.d_k).transpose(1, 2) 
        key = self.W_k(key).view(batch_size, -1, 
            self.num_heads, self.d_k).transpose(1, 2) 
        value = self.W_v(value).view(batch_size, -1, 
            self.num_heads, self.d_v).transpose(1, 2)

        scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.d_k)

        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)

        attention = F.softmax(scores, dim=-1)
        output = torch.matmul(attention, value).transpose(1, 2) \
                 .contiguous().view(batch_size, -1, self.d_model) 
        output = self.W_o(output)

        return output

代码说明

  • 定义 MultiHeadAttention(继承 nn.Module),传入模型维度与头数;assert 确保 (d_{\text{model}}) 能被头数整除。

  • __init__ 中定义 Q/K/V/O 的线性变换(nn.Linear)。

  • forward 中:

    1. 先用各自的线性层得到 Q/K/V;
    2. 重塑并转置为 ((\text{batch}, \text{heads}, \text{seq_len}, \text{depth}));
    3. 计算缩放点积注意力(含可选 mask);
    4. 用注意力权重与 V 相乘得到各头输出;
    5. 拼接各头结果并通过 W_o 得到最终输出。

清单 2.5 使用示例

batch_size = 32
sequence_length = 100
d_model = 512
num_heads = 8

multi_head_attn = MultiHeadAttention(d_model, num_heads)

input_data = torch.rand(batch_size, sequence_length, d_model)

output = multi_head_attn(input_data, input_data, input_data)

若打印 output 的形状,将得到 32 x 100 x 512,即 ((\text{batch_size}, \text{sequence_length}, d_{\text{model}}))。这表明:尽管多头注意力内部处理复杂,输出保持了原序列结构与模型维度

总结:借助多头注意力,模型可以并行关注输入的不同侧面,从而捕捉更复杂的关系;若只有单头,往往只能得到一种“平均化”的信息视角,限制对复杂模式的理解。因此,多头注意力是 Transformer 的核心组件,帮助模型在各类 NLP 任务上取得前沿水平的表现。

2.2.4 逐位置前馈网络(Position-wise FFNs)

前馈网络(FFNs)是一类在 NLP 任务中常用、专门用于变换定长向量的神经网络。它之所以有用,是因为可以把输入数据(如以定长向量表示的词或句子)转换成更抽象的表示。这些抽象表示能捕捉输入中的复杂模式,例如句子的语义、词语所处的上下文等。该转换通常通过两个全连接层(中间夹一个非线性激活函数)来实现。

在 FFN 中,向量的每个元素都会被独立地送入网络,并被映射到一个更高维的空间。此类“升维”的转换提升了模型容量,使向量各分量之间能产生更复杂的交互。随后,再用另一层全连接把输出映射回原始维度,从而让输出形状与输入一致——这有利于在神经网络中堆叠多层/多组件

在机器学习语境下,全连接层指该层中的每个神经元都与上一层的所有神经元相连,如图 2.18 所示。以这种方式变换输入向量,FFN 能从输入数据中提取并利用更高层次的特征,提升模型对底层模式的理解。

image.png

图 2.18 两个隐藏层的全连接神经网络

下面的代码清单展示了一个逐位置 FFN 的实现示例。

清单 2.6 简单的逐位置 FFN

class PositionwiseFeedforward(nn.Module):
    def __init__(self, input_dim, hidden_dim, dropout=0.1): 
        super(PositionwiseFeedforward, self).__init__() 
        self.input_dim = input_dim
        self.hidden_dim = hidden_dim 
        self.dropout = nn.Dropout(dropout)
        self.fc1 = nn.Linear(input_dim, hidden_dim) 
        self.fc2 = nn.Linear(hidden_dim, input_dim)

    def forward(self, x):
        x = self.dropout(torch.relu(self.fc1(x))) 
        x = self.fc2(x)
        return x

:上述示例仅展示了一个基础版的逐位置 FFN(单输出层)。而在完整的 Transformer 架构中(包括多头注意力等组件),处理的是序列的输入与输出,而非单个向量。

逐位置(position-wise) ”意指:同一套变换对序列中的每个位置/元素独立应用,与其在序列中的位置无关。采用这种方式,一方面可帮助网络学习更复杂的模式,另一方面也便于在大规模数据集上训练。此外,由于每个位置可以并行处理,逐位置 FFN 在数值上也更高效。

到此,我们已覆盖了原始 Transformer 架构的核心概念——即《Attention Is All You Need》[1] 中的编码器—解码器设计。你现在已经对这些具有深远影响的模型如何处理与理解语言有了扎实认识。基于此基础,下一章将介绍Transformer 家族与架构变体:探讨后来如何针对特定语言任务对首个编解码框架进行改造与迭代,进而形成仅解码器(decoder-only) 、**仅编码器(encoder-only)**与 MoE(专家混合)等不同架构。这将帮助你为自身需求选择并优化合适的 LLM。

小结

  • Transformer 模型由编码器解码器两部分构成:编码器将输入序列处理为上下文/记忆,解码器据此生成输出序列
  • Transformer 的核心注意力机制。从简化视角看,**查询(Q)像是以键(K)检索值(V)**的方式来获取信息。
  • 模型通过自注意力聚焦输入序列的不同片段,并用 FFN 对注意力输出做进一步变换;这些组件以多层堆叠的方式组织,并配合残差连接层归一化
  • 自注意力支持并行处理整段序列,计算高效;不同于 RNN 需要为序列中每个位置配置独立的可学习参数,自注意力在多类 NLP 任务上更灵活、有效。
  • 位置编码用于引导 Transformer 辨别序列中 token(词)的先后次序