跳转至

transformer

原理图 Transformer 架构图(出自论文 Attention is All You Need)。图的左侧是编码器(Encoder)和解码器(Decoder)的完整结构,而右侧则是对图中各个模块的图例和原理解释


结构总览

1. 数据的输入与预处理

  • Input Embedding / Output Embedding(词嵌入向量)
    含义: 计算机无法直接理解人类的文字(如“苹果”),词嵌入层的作用就是将自然语言中的词汇(Token)转换成计算机可以处理的连续数字向量。
    作用: 它不仅能把词变成向量,还能让语义相近的词在多维空间中距离更近(比如“国王”和“皇冠”的向量距离会比“国王”和“苹果”更近)。

  • Positional Encoding(位置编码,带波浪线图标)
    含义: 注入输入序列的时间/顺序信息。
    作用: Transformer 抛弃了传统 RNN(循环神经网络)顺序读取单词的模式,改为一次性并行处理所有单词。为了让模型知道单词的前后顺序(比如“我吃牛排”和“牛排吃我”的区别),就需要加上位置编码。图标中的波浪线代表它是通过正弦和余弦函数来计算位置信息的。

2. 核心注意力机制(三种不同的 Multi-Head Attention)

图例中列出了三种不同颜色的注意力机制,这是 Transformer 的灵魂所在:

  • Multi-Head Attention(黄色模块:多头自注意力机制)
    位置: 主要在编码器(Encoder)中使用。
    作用: 它的核心是“Self-Attention(自注意力)”。当模型处理一句话中的某个词时,这个机制会去查看这句话里的所有其他词,评估它们与当前词的相关性,从而更好地理解上下文语境。比如在“银行的河岸(bank)”和“银行的账户(bank)”中,它能通过上下文准确区分“bank”的意思。“多头(Multi-Head)”意味着模型会同时从多个不同的维度(子空间)去寻找这种关联。

  • Masked Multi-Head Attention(绿色模块:带因果掩码的多头自注意力机制)
    位置: 仅在解码器(Decoder)的第一层使用。
    作用: 在训练模型生成文本时,模型必须像人一样“从左到右”逐个预测单词。Mask(掩码)的作用是把当前位置之后的单词“遮挡”起来,防止模型在预测当前词时“作弊”提前看到后面的标准答案。

  • Multi-Head Attention(紫色模块:多头交叉注意力机制)
    位置: 在解码器(Decoder)的第二层使用,是连接编码器和解码器的桥梁。
    作用: 这里的注意力机制不是“自己看自己”,而是解码器去看编码器的输出。它帮助解码器在生成当前单词时,决定应该将注意力集中在原始输入句子(编码器的输出)的哪个部分。比如翻译时,生成中文“苹果”时,注意力会高度集中在英文输入的“apple”上。

3. 网络的基础组件

  • Add & Norm(黄色边框:层归一化与残差连接)
    Add(残差连接): 图中跨越模块的黑色箭头就是残差连接。它将模块的输入直接与输出相加。这样可以防止在网络很深时出现“梯度消失”的问题,让模型更容易训练。
    Norm(层归一化): 对数据进行标准化处理,使网络层之间的数值更加稳定,加速训练过程。

  • Feed Forward(蓝色模块:前馈神经网络层)
    作用: 这是一个包含两层的全连接神经网络。注意力机制的作用是整合上下文信息,而前馈神经网络的作用则是对注意力机制提取出的特征进行非线性变换,进一步提取更深层次的特征。

4. 输出预测层

  • Linear(紫色模块:全连接层)
    作用: 解码器经过一系列复杂的处理后,输出的是一串向量。Linear 层(线性映射层)的作用是将这个向量扩展到一个与你词典大小(Vocabulary Size)相同的维度。比如你的词典里有一万个词,它就会输出一个一万维的向量。

  • Softmax(绿色椭圆:Softmax层)
    作用: 将 Linear 层输出的一万维向量转化为概率分布。它会确保所有词汇的概率加起来等于 1,并且找出概率最大的那个词,作为模型当前这一步最终预测输出的字符/单词。


详细原理

词嵌入

词嵌入向量的具体实现原理:

在词嵌入出现之前,最直接的方法是 独热编码(One-Hot Encoding)。 假设我们的词典里只有三个词:["苹果", "香蕉", "汽车"]。

  • 苹果 = [1, 0, 0]
  • 香蕉 = [0, 1, 0]
  • 汽车 = [0, 0, 1]

这种方法的致命缺点: 1. 维度爆炸: 如果词典里有 10 万个词,每个词就是一个 10 万维的向量,里面全是 0,只有一个 1,极其浪费内存。 2. 毫无关联: 在数学上,“苹果”和“香蕉”的距离,跟“苹果”和“汽车”的距离是一模一样的。计算机无法知道前两者都是水果。

词嵌入不再用无数个 0 和一个 1 来表示词,而是用一组固定长度的、连续的浮点数来表示。

比如,我们设定向量长度(维度)为 4:

  • 苹果 = [0.89, -0.23, 0.55, 0.11]
  • 香蕉 = [0.82, -0.15, 0.60, 0.05]
  • 汽车 = [-0.55, 0.92, -0.12, 0.88]

在这个例子中,“苹果”和“香蕉”在数值上非常接近,而跟“汽车”差别很大。计算机通过计算这些数字之间的差异(如余弦相似度),就能立刻“懂得”词与词之间的语义关系。

数字是怎么来的?(训练过程)

  1. 随机初始化: 一开始,这个巨大的表格里填满了完全随机的数字。此时模型就是个傻子,觉得“苹果”和“汽车”毫无区别。
  2. 上下文学习(Distributional Hypothesis): 语言学中有一个核心假设——“一个词的含义由它的上下文决定”。比如在句子“我吃了一个___”中,空格里大概率是“苹果”或“香蕉”,绝不会是“汽车”。
  3. 反向传播与微调: 神经网络在大量阅读人类文本时,会不断做预测。当它发现“苹果”和“香蕉”总是出现在相似的语境中时,底层的数学优化算法(梯度下降)就会自动去调整表格里的那些浮点数,一点点把“苹果”和“香蕉”的向量往一起拉近,把“汽车”的向量推远。
  4. 大功告成: 当模型看了几百亿甚至上万亿字的文本后,表格里的数字就稳定下来了。此时,它们就蕴含了丰富的人类常识和语法规则。

位置编码

20260222163603 位置编码并不是简单地在词向量旁边加一个数字(比如标上 1, 2, 3),而是为每一个位置生成一个和词向量维度完全一样的“位置向量”。相加后得到的新向量,既包含了“这个词是什么意思”,又包含了“这个词在句子的哪个位置”。

\(pos\) 代表这个词在句子中的位置(比如第1个词,第2个词)。
\(i\) 代表向量中的第几个维度(0 到 511)。
\(d_{model}\) 是向量的总维度(比如 512),就是单个词向量的维度。
在位置向量的偶数维度(0, 2, 4...),它使用正弦函数计算;在奇数维度(1, 3, 5...),它使用余弦函数计算。

为什么非要用这么复杂的三角函数?
数值稳定: 无论句子有多长,正弦和余弦的值永远被限制在 -1 到 1 之间,不会因为数字过大破坏原本词向量的权重。
绝对位置与相对位置: 也就是为什么它能表示“距离”。在数学上,正余弦函数有一个特性:位置 \(pos + k\) 的编码,可以通过位置 \(pos\) 的编码进行线性变换得到。这意味着,模型不仅能知道每个词的绝对位置,还能轻易计算出任意两个词之间的相对距离(比如词A和词B隔了3个词)。20260222170805
无论你是从“第 1 个词”跳到“第 4 个词”,还是从“第 100 个词”跳到“第 103 个词”,虽然它们各自的绝对位置编码完全不同,但它们之间的转换关系(那个旋转矩阵)是完全一模一样的!

自注意力机制

自注意力机制(Self-Attention)的作用,就是让句子中的每一个词,都去跟句子里的所有其他词“相亲”,看看谁跟自己最匹配,从而获取到完整的上下文语义。据此设计了一个极其精妙的 Q、K、V 机制。

20260222182937

要理解自注意力,你只需记住这三个字母:Q (Query)、K (Key)、V (Value)。你可以把它想象成你去图书馆找书的过程:
Q (Query / 查询): 你心里想找的东西。比如你在搜索框里输入:“我想找一本关于人工智能的书”。这就是你的 Query。
K (Key / 键): 图书馆里每一本书书脊上的“标签”或“书名”。比如某本书的标签是“深度学习、计算机、AI”。这就是 Key。
V (Value / 值): 这本书里面的实际内容。这就是 Value。在 Transformer 中,输入的每一个词向量,都会经过图例中紫色的 Linear(全连接层) 进行线性变换,“分裂”成三个不同的向量:\(Q\) 向量、\(K\) 向量和 \(V\) 向量。
也就是说,句子里的每一个词,既扮演着“提问者 (Q)”的角色,又扮演着“被查询的标签 (K)”的角色,同时还装着属于它自己的“真实内容 (V)”。

自注意力的计算过程

20260222175653 假设我们的句子是:“这只动物没有过马路,因为它太累了。” 当模型处理到 “它” 这个词时,它是如何知道“它”指的是“动物”而不是“马路”的?

第一步:计算匹配度(Q 去点乘 K)

  • 词汇 “它” 会拿出自己的查询向量 。
  • 然后,拿着这个 ,去和句子中所有其他词(包括它自己)的标签向量 进行数学上的点积(Dot Product)计算。
  • 点积的几何意义就是算两个向量的相似度。 与 的点积结果会非常大,而与 的点积结果会比较小。

第二步:分数归一化(Softmax)

  • 算出来的得分有大有小,为了方便处理,模型会把这些得分除以一个缩放因子(公式里的 ,为了防止数字过大),然后扔进图例顶部的 Softmax 层
  • Softmax 的作用是把所有得分变成 0 到 1 之间的概率值,且总和为 1
  • 结果可能变成了这样:“它”对“动物”的注意力分配了 80%,对“马路”分配了 10%,对“累”分配了 10%。这就是所谓的 “注意力权重”

第三步:提取内容,重新拼装(权重乘以 V 并求和)

  • 现在我们知道了各个词的重要性比例。接下来,就拿着这些比例去乘以它们各自的实际内容向量 。
  • 把所有乘出来的结果加在一起,就得到了词汇 “它” 在经过自注意力层之后的全新向量表示

这个新的向量,已经不再仅仅代表“它”这个代词本身了,而是“融汇了80%动物特征”的综合向量。计算机通过这种方式,完美地理解了上下文的指代关系。

这套逻辑用严谨的数学公式表达,就是论文中极其经典的那一行代码:

\[Attention(Q, K, V) = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)V\]

掩码注意力机制(由GPU并行计算限制产生)

为了迎合 GPU 的矩阵并行计算,我们必须用 把句子凑成长度一致的完美矩阵;而为了消除 带来的副作用,我们又必须在注意力机制中引入 Mask 掩码 将其屏蔽。 这就完美串联起了硬件底层的限制与算法上层设计的巧妙配合! 20260222193224

多头注意力机制

如果只用一组 Q、K、V 去看句子,模型可能会出现“管中窥豹”的问题,也就是只关注了某一种维度的联系(比如只关注了语法关系的“它”指代“动物”)。

为了让模型思考得更全面,让这个机制长了“多个脑袋(Multi-Head)”: 模型会把输入的词向量切分成好几块(比如 8 个头),每一块独立去生成自己的 Q、K、V 并计算注意力

20260222190014 * “头 1”可能专注于寻找句子的语法结构(主谓宾)。
“头 2”可能专注于寻找词语之间的情感联系
“头 3”可能专注于寻找空间位置的描述。

最后,把这 8 个头得到的新向量拼接在一起,模型就能同时从多个角度(多维度子空间)深度理解这句话的复杂含义了。

因果掩码注意力机制

需要根据实现过程来理解

训练过程中

20260223110131 在QK相乘后加上一个倒三角的掩码,使得后面的注意力权重矩阵只知道和前面词的关系,不会预知未来
* 作用:为了防止模型在训练时“偷看答案”的“防作弊系统”。
在训练时,因为我们一次性把整句标准答案(比如长达 100 个词)塞进了模型。如果没有下三角矩阵的掩码,前面的词就会看到后面的词,这就成了“开卷考试”。 所以训练时,掩码是绝对必要的,它在物理层面上挡住了未来的数据。

推理过程中

20260223111416 核心思想和训练过程一样

为什么推理阶段也需要呢,未来的词不存在,掩码在遮什么?
* 假设模型已经生成了前三个词:[<开始>, 我, 爱],现在要把这三个词喂给模型,让它预测第四个词“你”。
此时,输入序列只有 3 个词,未来的词还没诞生。如果画出这个时候的注意力矩阵,它是一个 \(3 \times 3\) 的矩阵。如果不加掩码,会发生什么灾难?
如果没有掩码,在这个 \(3 \times 3\) 的注意力计算中:<开始> 会注意到 <开始>、我、爱。我 会注意到 <开始>、我、爱。爱 会注意到 <开始>、我、爱。
发现问题了吗?“我”竟然看到了它后面的“爱”! 在模型训练的漫长岁月里,它学到的死铁律是:“我”在任何情况下,都绝对不可以去注意它右边的词。它的神经网络权重,是完全基于“看不到右边”这个前提来优化的。如果推理时不加掩码,让“我”看到了“爱”,那么“我”这个词在经过这一层注意力机制后,输出的向量就会和训练时完全不一样。这种误差会随着网络层数(比如 12 层或 96 层解码器)不断放大,最终导致模型彻底崩溃,胡言乱语。

交叉注意力机制

20260223130554 在前面的“自注意力机制(Self-Attention)”中,Q(查询)、K(标签)和 V(内容)都来自同一个句子(自己查自己)。 但在“交叉注意力机制”中,它们的来源被彻底分开了:

  • Q(Query / 查询):来自右边的解码器(Decoder)。
    代表着“我已经翻译出来的部分”。解码器拿着自己当前的状态,向左边提问:“基于我现在要生成的这个词,我应该重点去寻找原文里的哪些信息?”

  • K(Key / 标签)和 V(Value / 内容):来自左边的编码器(Encoder)的最终输出。
    代表着“被彻底理解后的原文”。编码器把原文处理完毕后,打包成了一堆带有丰富上下文语义的 K 和 V,放在那里随时等候解码器的“召唤”。

假设我们正在做一个中译英的翻译任务:
左边编码器(原文): “我 吃 苹果”
右边解码器(已生成的译文): “I eat”
现在,解码器准备生成第三个词(也就是“苹果”对应的英文)。交叉注意力机制是这样工作的:
1、解码器发出查询(Q):解码器根据前面的 “I eat”,生成了一个针对第三个位置的查询向量 \(Q_{当前}\)。它心里的潜台词是:“我已经吃东西了,接下来我该看原文里的哪个词?
2、”与原文标签比对(Q 点乘 K):解码器拿着 \(Q_{当前}\),跑去和编码器送过来的三个词的标签(\(K_{我}\)\(K_{吃}\)\(K_{苹果}\))分别进行点乘计算(算相似度)。
3、计算注意力权重(Softmax):计算发现,\(Q_{当前}\)\(K_{苹果}\) 的匹配度极高!经过 Softmax 转换后,模型可能会把 95% 的注意力(权重) 分配给“苹果”,2% 给“吃”,3% 给“我”。这就是机器翻译中著名的对齐(Alignment)过程。
4、提取原文内容(权重乘以 V):最后,模型拿着这 95% 的权重,去乘以编码器中“苹果”的实际内容向量 \(V_{苹果}\),把这个富含“苹果”语义的向量“拽”回解码器这边。

层归一化

20260223100536 批归一化和层归一化的公式都相同,区别在于归一化的维度(方向)不同。

BN 是在批次(Batch)维度上进行的。
操作对象: 针对同一个 Batch 中所有样本的同一个特征通道(Channel)进行归一化。
最适用场景: 计算机视觉(CV)领域,如 CNN。

LN 是在特征(Feature/Layer)维度上进行的。
操作对象: 针对单个样本的所有特征维进行归一化。
最适用场景: 自然语言处理(NLP)领域,特别是 RNN 和 Transformer(如 BERT、GPT 系列)。

为什么 Transformer 用 LN 而不是 BN? 在处理文本时,不同句子的长度通常不同。如果用 BN,在填充(Padding)部分计算均值会引入大量噪声;而 LN 独立于每个样本内部进行计算,能更好地捕捉单个序列内的特征分布,因此在处理序列数据时更加稳定。

实现过程

推理过程

20260223102604 每次都将解码器生成的词再次喂给解码器,来不断根据前面的词预测下一个词(解码器下面的输入是之前的输出)

训练过程

20260223104410 解码器下面的输入是编码器输入对应的标签(即正确答案),每次输入一部分来预测后面的,用到因果掩码注意力机制

代码要点

注意力机制的核心实现

自注意力机制的完整计算过程,对应公式 \(Attention(Q, K, V) = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)V\),用 PyTorch 伪代码表示:

import torch
import torch.nn.functional as F
import math

def scaled_dot_product_attention(Q, K, V, mask=None):
    """
    Q, K, V: (batch_size, n_heads, seq_len, d_k)
    mask:     (batch_size, 1, 1, seq_len) 或因果掩码
    """
    d_k = Q.size(-1)

    # 第一步:Q 和 K 的转置做点积,算匹配度
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)

    # 第二步:如果有掩码(因果掩码或 PAD 掩码),把被遮挡位置设为 -inf
    if mask is not None:
        scores = scores.masked_fill(mask == 0, float('-inf'))

    # 第三步:Softmax 归一化为注意力权重
    attn_weights = F.softmax(scores, dim=-1)

    # 第四步:用注意力权重对 V 加权求和
    output = torch.matmul(attn_weights, V)
    return output

因果掩码的生成

def create_causal_mask(seq_len):
    """生成下三角掩码矩阵,防止看到未来的 Token"""
    mask = torch.tril(torch.ones(seq_len, seq_len))
    # 结果:
    # [[1, 0, 0, 0],
    #  [1, 1, 0, 0],
    #  [1, 1, 1, 0],
    #  [1, 1, 1, 1]]
    return mask

位置编码的生成

def positional_encoding(seq_len, d_model):
    """生成正余弦位置编码"""
    pe = torch.zeros(seq_len, d_model)
    position = torch.arange(0, seq_len).unsqueeze(1).float()
    div_term = torch.exp(
        torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)
    )
    pe[:, 0::2] = torch.sin(position * div_term)  # 偶数维用 sin
    pe[:, 1::2] = torch.cos(position * div_term)  # 奇数维用 cos
    return pe

从 Transformer 到 LLM

Transformer 是所有大语言模型的基石。在它之上,LLM 领域发展出了丰富的技术体系:

  • 📒 分词与词汇表 — Transformer 输入的第一步:文本如何变成 Token
  • 📒 预训练 — 如何用"预测下一个 Token"的方式训练出强大的语言模型
  • 📒 模型架构演进 — 从原始 Transformer 到 GPT、LLaMA 的架构改进(RoPE、GQA、SwiGLU 等)
  • 📒 推理优化 — KV Cache 如何加速 Transformer 的自回归生成