欢迎光临
我们一直在努力

全网最接地气的 Transformer 核心原理解析:告别公式,直击本质!

全网最接地气的 Transformer 核心原理解析:告别公式,直击本质!

文章目录

  • 全网最接地气的 Transformer 核心原理解析:告别公式,直击本质!
    • 一、 为什么需要 Attention?打破串行,建立群聊
      • 1. 核心比喻:微信群里的 Q、K、V
      • 2. Q、K、V 是怎么来的?
    • 二、 数学揭秘:如何高效计算 Attention?
      • 1. 第一步:

        Q

        ×

        K

        T

        Q \\times K^T

        Q×KT (计算相似度得分 Scores)

      • 2. 第二步:除以

        d

        k

        \\sqrt{d_k}

        dk

        (防止梯度消失的稳定器)

      • 3. 第三步:

        W

        e

        i

        g

        h

        t

        s

        ×

        V

        Weights \\times V

        Weights×V (加权融合信息)

    • 三、 Multi-Head Attention (多头注意力):多维度的影分身之术
      • 1. 为什么一个“头”不够用?
      • 2. PyTorch 4D 张量魔法:优雅的并发计算
  • 📍 深入浅出 Transformer:Positional Encoding(位置编码)的绝对定位
    • 一、 为什么需要位置编码?Attention 的“脸盲”缺陷
      • 1. “脸盲”的比喻
      • 2. 解决方案:发号码牌(位置时间戳)
    • 二、 数学直觉:为什么选正弦/余弦(Sine/Cosine)?
      • 作者的天才设计:正弦/余弦波 (Sine/Cosine)
        • 🕒 直观例子:时针、分针与秒针的编码机制
        • ➕ 编码是怎么和词向量“绑”在一起的?
    • 三、 代码实战:极简 Positional Encoding
      • 观察与验证
    • 四、 稳固基石与升华:Add & Norm 与 Feed Forward
      • 1. Add (残差连接):不忘初心与防断路
      • 2. Norm (层归一化 LayerNorm):情绪稳定器
      • 3. Feed Forward (前馈神经网络):各自闭关修炼
    • 五、 代码实战:组装完整的 Encoder Block
    • 六、 终极拼图:Decoder (解码器) 与交叉注意力
      • 1. Masked Self-Attention (掩码自注意力):禁止“偷看”未来
      • 2. Cross-Attention (交叉注意力):寻找外援
      • 3. 代码实战:组装完整的 Decoder Block
    • 七、 上帝视角:把 "I love you" 翻译成 "我 爱 你" 的全流程串联
      • 第一阶段:Encoder 读懂原文
      • 第二阶段:Decoder 开始逐字憋词
      • 第三阶段:Cross-Attention 跨界翻译与最终预测
    • 八、 终极大考:手写完整 Transformer 极简架构

本文通过直观的比喻和极简的 PyTorch 代码,彻底拆解 Transformer 的核心——注意力机制(Attention)和多头注意力(Multi-Head Attention)。


一、 为什么需要 Attention?打破串行,建立群聊

在 Transformer 之前,RNN 处理句子就像是“击鼓传花”,一个词一个词地往后传,速度慢且容易遗忘长距离的上下文。 Attention 的核心目的,就是打破这种串行机制,让句子里的所有词同时进入一个“微信群”,让它们直接进行互相交流,从而理解各自在当前语境下的真正含义。

这种“词与词之间互相考察、互相融合”的过程,我们称之为 自注意力机制 (Self-Attention)。

1. 核心比喻:微信群里的 Q、K、V

想象输入一句话,比如“我 啃 苹果” 和 “我 买 苹果 手机”。 在没进群之前(也就是刚查完词典,拿到原始词向量的时候),“苹果”的词向量在这两句话里是一模一样的,此时的向量只有字典里死板的字面意思。

为了实现交流与融合,每个词在发言时都会变幻出三重身份:

  • Q (Query / 寻租需求):我在这个句子里,需要寻找什么样的上下文来补充我自己?
    • 例如:在“我 啃 苹果”中,“苹果”的 Q 可能是“寻找一个动作来明确我是食物”。
  • K (Key / 自我展示标签):我能为别人提供什么样的信息线索?
    • 例如:“啃”提供的 K 是“我是一个动作,且通常作用于食物”。
  • V (Value / 内在真实价值):如果你关注我,我实际能给你的内容。
    • 例如:“啃”的 V 包含了关于动作、吞咽的相关特征。

在这个“群聊”里,“苹果”拿着自己的 Q 去和全群所有人的 K 计算缘分(打分),发现自己和“啃”这个词的缘分特别深。于是,“苹果”就会大量吸收“啃”的 V(内在特征)。最终加上自己的 V 混合在一起后,“苹果”这个词的向量就发生改变了,在多维空间里指向了**“水果”的概念,而不再是科技公司。 这就是把静态词汇变成“包含了整句话上下文语境的、活的动态特征向量”**的过程!

2. Q、K、V 是怎么来的?

在机器的世界里,这三重身份是如何产生的呢? 答案是:在自注意力中,Q、K、V 全部来自于同一个原始词向量! 通过三个不同的可学习全连接层(线性变换权重

W

Q

,

W

K

,

W

V

W_Q, W_K, W_V

WQ,WK,WV),对原始词向量进行“投影”。这就好比同一个员工(原始向量),穿上不同的制服,去执行不同的任务(Q, K, V)。

代码实战:

import torch
import torch.nn as nn

# 假设词向量维度 d_model 是 4
d_model = 4
# 输入:1句话,3个词,每个词4维特征 (Batch, Seq_len, d_model)
x = torch.rand(1, 3, d_model)

# 准备三套“制服”(线性层)
W_q = nn.Linear(d_model, d_model, bias=False)
W_k = nn.Linear(d_model, d_model, bias=False)
W_v = nn.Linear(d_model, d_model, bias=False)

# 输入同一个 x,分别穿上不同的制服,变出 Q, K, V
Q = W_q(x)
K = W_k(x)
V = W_v(x)


二、 数学揭秘:如何高效计算 Attention?

明确了 Q、K、V 的概念后,让所有词互相计算“缘分”并融合信息,在数学上可以通过极其优雅的矩阵乘法一步完成。这就是那条著名的 Transformer 核心公式:

A

t

t

e

n

t

i

o

n

(

Q

,

K

,

V

)

=

s

o

f

t

m

a

x

(

Q

K

T

d

k

)

V

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

Attention(Q,K,V)=softmax(dk

QKT)V

这个公式看起来吓人,其实可以拆解为清晰的三步运作:

1. 第一步:

Q

×

K

T

Q \\times K^T

Q×KT (计算相似度得分 Scores)

如何衡量缘分深浅? 在数学里,向量点积是衡量相似度最好的工具。 把全群所有人的 Q(需求)排成一个矩阵,和所有人的

K

T

K^T

KT(标签的转置)做矩阵乘法,就能批量一次性算出所有词两两之间的打分表。 如果得分高,说明 Q 和 K 匹配度高,这两个词在当前语境下关联就强。

2. 第二步:除以

d

k

\\sqrt{d_k}

dk

(防止梯度消失的稳定器)

为什么公式里突然多了一个

d

k

\\sqrt{d_k}

dk

的分母? 这是为了解决数值爆炸的问题:假设词向量维度

d

k

d_k

dk 很大(比如 512),点积算出来的得分数字往往会变得非常大。 过大的数值会把紧接着的 Softmax 激活函数推向“饱和区”(算出的概率非极接近 0 就是极接近 1),导致梯度趋近于零,模型就无法更新学习了。 因此,除以维度数量的平方根

d

k

\\sqrt{d_k}

dk

就像是一个“数值缩放器”,把打分重新拉平稳,保证模型训练的稳定。

3. 第三步:

W

e

i

g

h

t

s

×

V

Weights \\times V

Weights×V (加权融合信息)

把稳定后的得分通过 Softmax 转换成百分比(加和为 1 的注意力权重 Weights)。然后拿着这些权重去乘以 V 矩阵。 这就完成了核心的一步:大家各自按缘分深浅(权重),把全群的所有信息(V)混合汇聚到自己身上,生成融合上下文的新向量!

极简代码演示:

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

# 假设算出了 3个词互相的点积打分矩阵 (3×3 矩阵)
Scores = torch.tensor([
[10.0, 0.0, 5.0], # 词1 给全场的原始打分
[ 0.0, 10.0, 5.0], # 词2 给全场的原始打分
[ 0.0, 0.0, 0.0] # 词3 给全场的原始打分
])

# 缩放稳定器 (假设 d_k = 64)
d_k = 64
Scores_scaled = Scores / math.sqrt(d_k)

# Softmax 归一化 (每一行变成合为1的百分比权重)
Weights = F.softmax(Scores_scaled, dim=1)

# 真实的 V 矩阵 (3个词,每个词 2维特征)
V = torch.tensor([
[10.0, 0.0], # 词1 包含的关键信息
[ 0.0, 10.0], # 词2 包含关键信息
[ 5.0, 5.0] # 词3 包含关键信息
])

# 矩阵相乘:归一化权重 x 信息库 = 最终的上下文融合向量
Output = Weights @ V


三、 Multi-Head Attention (多头注意力):多维度的影分身之术

1. 为什么一个“头”不够用?

前文的 Attention 计算,相当于让每个词用一套统一的标准(一套 QKV 参数)去审视全身的词。但我们理解语言需要多个维度! 比如看“我正满心欢喜地啃大苹果”这句话:

  • 在语法维度,“果”需要关注修饰它的定语(大)或者它前面的动词(啃)。
  • 在语义维度,“果”可能更需要关注整个句子的情感或时态色彩(满心欢喜)。

如果只有一个头,模型就只能捕捉一种特征,必须在不同维度之间“妥协”。 多头注意力(Multi-Head Attention)完美解决了这个问题:它变出多个“影分身”(不同的 Head),每个分身拥有自己独立的一套 QKV 寻租和展示标准。这样就能同时关注语法、语义、语气等多种不同的特征维度,最后再把这些维度的信息综合起来。

2. PyTorch 4D 张量魔法:优雅的并发计算

你可能会想,多头是不是要在代码里写个 for 循环挨个算?绝对不需要! 深度学习框架可以利用简单的张量维度变换 (Reshape/view 与 Transpose),让 GPU 瞬间并行完成所有“头”的计算。

其核心技巧就是把原始大特征分配给不同的头:

  • 切分魔法 (view):把最后面的特征维度

    d

    _

    m

    o

    d

    e

    l

    d\\_model

    d_model 等分给各个头(比如总共 8 维特征,分给 2 个头,每个头负责 4 维)。

  • 换位魔法 (transpose):把“头”这个维度推到最前列 Batch 的旁边,假装这是增加的一个批次数据扔给 GPU 并行计算。
  • 代码实战(感受维度切分的魔法):

    import torch

    # 原始大 Q 矩阵 (1句话, 2个词, 4维特征)
    Q = torch.tensor([
    [ [1.0, 1.0, 9.0, 9.0], # 词 A
    [2.0, 2.0, 8.0, 8.0] ] # 词 B
    ])

    # 我们想拆成 2 个头 (num_heads=2),每个头被分给 2 维 (d_k=2)
    # 1. 维度切分 (view):将特征拆解 (1, 2, 4) -> (Batch, Seq, Heads, d_k)即(1, 2, 2, 2)
    Q_viewed = Q.view(1, 2, 2, 2)

    # 2. 轴换位 (transpose):把“头”提到前面,把属于同一个头的数据聚在一起
    # 形状变成 (Batch, Heads, Seq_len, d_k) -> 即 (1, 2, 2, 2)
    Q_transposed = Q_viewed.transpose(1, 2)

    # 观察拆分结果:这就像是平行时空里的两个独立运作的小宇宙!

    # 查看分给 Head 1 的专属 Q 矩阵 (拿到了代表前 2 维的概念):
    # tensor([[1., 1.],
    # [2., 2.]])

    # 查看分给 Head 2 的专属 Q 矩阵 (拿到了代表后 2 维的概念):
    # tensor([[9., 9.],
    # [8., 8.]])

    GPU 的并行计算: 当把这些拆分好的

    Q

    Q

    Q

    K

    K

    K 送去做矩阵乘法时,GPU 不会把它们混为一谈,而是会自动启动多个并行线程,让 Head 1 算 Head 1 的,让 Head 2 算 Head 2 的:互相独立,互不干扰,瞬间算完所有头的 Attention 打分与融合! 最后,我们再经过一层转置,通过 view 拍扁成原样拼接,并输入给带有

    W

    O

    W_O

    WO 权重的线性层,就得到了多维度完美融合的终极向量。

    📍 深入浅出 Transformer:Positional Encoding(位置编码)的绝对定位

    本文通过直观的比喻和极简的 PyTorch 代码,彻底拆解 Transformer 的另一个核心组件——位置编码(Positional Encoding)。


    一、 为什么需要位置编码?Attention 的“脸盲”缺陷

    在上一节《Attention 的本质》中,我们了解到 Attention 机制是通过计算词与词之间的相似度(打分)来提取信息的。 但是,纯粹的 Attention 机制有一个致命缺陷:它是一个“词袋模型”,完全没有词序(顺序)的概念!

    1. “脸盲”的比喻

    假设有一个相亲派对(Attention 计算过程),派对上的人只看条件匹配度(Q 和 K 的相似度),完全不在乎这些人是怎么排队的。

    不管你是排成: 👉 ["我", "打", "你"] 还是排成: 👉 ["你", "打", "我"]

    对于纯 Attention 来说,这三个词的词向量是一模一样的,它们互相之间的打分也是一模一样的!最终融合出来的向量特征也是一模一样的! 这就导致模型无法区分“我打你”和“你打我”这两个意思完全相反的句子。

    2. 解决方案:发号码牌(位置时间戳)

    为了解决这个问题,Transformer 的作者想出了一个巧妙的办法:给每个词发一个“号码牌”(位置时间戳),并把这个号码牌和词本身的含义“绑”在一起。 这样,排在第 1 位的“我”和排在第 3 位的“我”,在模型眼里就变成了两个不同的输入。

    这个“号码牌”,就是 Positional Encoding (位置编码)。


    二、 数学直觉:为什么选正弦/余弦(Sine/Cosine)?

    我们怎么给词发号码牌呢?

    思路 1:用绝对整数(1, 2, 3…)

    • 缺点: 句子很长时,后面的数字会非常大(比如 1000)。这会让模型在处理长句子时,数值范围爆炸,干扰原本词向量的权重。

    思路 2:用固定比例的分数(0.1, 0.2… 1.0)

    • 缺点: 如果规定第一个词是 0,最后一个词是 1。那么在 10 个词的句子里,相邻词差 0.1;在 100 个词的句子里,相邻词差 0.01。步长不统一,模型很难学到“相邻”这种相对位置关系。

    作者的天才设计:正弦/余弦波 (Sine/Cosine)

    作者最终选择了用 不同频率的 Sine 和 Cosine 函数 来生成位置编码。 想象一个有很多个齿轮的机械钟表:

    • 秒针(高频): 转得最快,记录微小的变化(相邻位置的差异很大)。
    • 分针(中频): 转得适中。
    • 时针(低频): 转得最慢,记录宏观的变化(远距离位置的差异)。
    🕒 直观例子:时针、分针与秒针的编码机制

    为了让你彻底明白这个时钟比喻,我们假设一个词的“号码牌”只有 3 个维度(分别代表秒针、分针、时针):

  • 第 1 维(秒针,高频):

    • 随着词的位置(1, 2, 3…)变化,它的值在 1 和 -1 之间剧烈波动。
    • 比如位置1是 1.0,位置2就是 -0.9,位置3又是 0.8。
    • 作用: 让模型能一眼看出“我打你”和“打我你”的区别。因为相邻两个词的“秒针”值差异极大,颠倒位置后,整个向量特征会发生剧烈改变。
  • 第 2 维(分针,中频):

    • 变化相对平缓,可能过了 10 个词,它的值才从 1 降到 -1。
    • 比如位置1是 1.0,位置2是 0.9,位置3是 0.8… 到位置10才是 -0.9。
    • 作用: 提供中等距离的定位信息。
  • 第 3 维(时针,低频):

    • 变化极其缓慢,可能过了 1000 个词,它才勉强完成一次循环。
    • 比如位置1是 1.00,位置2是 0.99,位置10 是 0.90,位置100 才是 -0.50。
    • 作用: 帮助模型把握宏观的篇章结构(比如这句话是在文章的开头还是结尾)。
  • 当一个词进入模型时,它的位置编码就像是这三根指针的组合:

    • 词 A(第 1 个词): [秒针=1.0, 分针=1.0, 时针=1.0]
    • 词 B(第 2 个词): [秒针=-0.9, 分针=0.9, 时针=0.99]
    • 词 C(第 10 个词): [秒针=-0.2, 分针=-0.9, 时针=0.90]

    你会发现:

    • 词A和词B的“时针”和“分针”差不多,说明它们距离很近;但“秒针”差异巨大,说明它们有明确的先后顺序。
    • 词A和词C的“秒针”也许碰巧相似,但“分针”和“时针”差异很大,说明它们距离很远。

    通过组合不同频率的齿轮,Transformer 用一组不会爆炸的小数(-1 到 1 之间),完美地表达了从微观(相邻词序)到宏观(段落位置)的所有位置关系!

    ➕ 编码是怎么和词向量“绑”在一起的?

    你可能会好奇,这“三根针”是怎么和原本的词汇含义结合的? 答案是:极其简单粗暴的“对应维度直接相加”。

    假设我们的原始词向量也是 3 维的,代表:[财富属性, 健康属性, 情感属性]。

    • “我” 的原始词汇向量: [8.0, 9.0, 5.0]

    当“我”出现在句子的第 1 个位置时,它的位置编码(上面说的三根针)是:[1.0, 1.0, 1.0]。

    合并方式就是把它们相同维度的数字直接加起来:

    • 最终的第1维 (财富+秒针) = 8.0 + 1.0 = 9.0
    • 最终的第2维 (健康+分针) = 9.0 + 1.0 = 10.0
    • 最终的第3维 (情感+时针) = 5.0 + 1.0 = 6.0

    最终进入模型进行 Attention 计算的“我(位置1)”,它的完整向量就是 [9.0, 10.0, 6.0]。

    如果“我”出现在句子的第 10 个位置,它的位置编码是:[-0.2, -0.9, 0.9]。 相加后,最终向量变成了 [7.8, 8.1, 5.9]。

    发现了吗? 原始词汇的本质(8.0, 9.0, 5.0)依然占主导地位(因为它们数值通常比较大),但位置编码(-1 到 1 之间的小数)就像给每个维度贴了一层带有微小波纹的“位置滤镜”。这就保证了模型既能认出这是个“我”字,又能通过数值的微小偏差,精确区分出它是出现在句首还是句尾!

    在位置编码矩阵中,每一列对应一个特定的频率(一个齿轮)。

    • 词向量的前几维(高频),像秒针一样,在相邻的位置迅速变换(比如交替呈现 1 和 -1),让模型能轻易区分 [我, 打] 和 [打, 我]。
    • 词向量的后几维(低频),像时针一样,变化非常缓慢,让模型能感知到长距离的相对位置关系。

    重要特性: Sine/Cosine 函数的值域永远在 [-1, 1] 之间,数值绝对安全,不会爆炸!而且通过三角函数的和差化积公式,模型可以很容易地学习到相对位置信息(偏移量

    k

    k

    k 的位置可以表示为当前位置

    p

    o

    s

    pos

    pos 的线性组合)。


    三、 代码实战:极简 Positional Encoding

    在 PyTorch 中,位置编码是怎么加到词向量上的呢? 答案是:直接相加(Add)! 最终输入 = 原始词向量 (Word Embedding) + 位置编码向量 (Positional Encoding)

    让我们手写一段极简的生成代码:

    import torch
    import torch.nn as nn
    import math
    import matplotlib.pyplot as plt

    class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=50):
    """
    d_model: 词向量的维度 (例如: 8)
    max_len: 句子的最大长度 (例如: 50)
    """

    super().__init__()

    # 1. 创建一个空的 PE 矩阵,形状为 (max_len, d_model)
    pe = torch.zeros(max_len, d_model)

    # 2. 生成绝对位置的列向量: 0, 1, 2, …, max_len-1
    # pos 形状: (max_len, 1)
    position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)

    # 3. 计算分母: 10000^(2i/d_model) -> exp(-2i * ln(10000) / d_model)
    # div_term 形状: (d_model/2)
    div_term = torch.exp(torch.arange(0, d_model, 2).float() * (math.log(10000.0) / d_model))

    # 4. 偶数维度 (0, 2, 4…) 用 Sine
    pe[:, 0::2] = torch.sin(position * div_term)

    # 5. 奇数维度 (1, 3, 5…) 用 Cosine
    pe[:, 1::2] = torch.cos(position * div_term)

    # 6. 增加 batch_size 维度,形状变为 (1, max_len, d_model)
    pe = pe.unsqueeze(0)

    # 7. 将 pe 注册为 buffer (不需要梯度更新的常量)
    self.register_buffer('pe', pe)

    def forward(self, x):
    """
    x: 输入的词向量矩阵,形状为 (batch_size, seq_len, d_model)
    """

    # 截取对应长度的 PE 并直接加到输入 x 上
    # self.pe[:, :x.size(1)] 会广播(broadcast)到与 x 相同的 batch_size
    x = x + self.pe[:, :x.size(1), :]
    return x

    # ==========================================
    # 动手观察验证
    # ==========================================
    if __name__ == "__main__":
    d_model = 16 # 词维度设为 16
    seq_len = 50 # 句子长度设为 50

    # 初始化 PE 模块
    pe_module = PositionalEncoding(d_model=d_model, max_len=seq_len)

    # 模拟一个全零的输入,方便直接观察 PE 的值
    dummy_input = torch.zeros(1, seq_len, d_model)
    output = pe_module(dummy_input)

    # 提取生成的 PE 矩阵 (取出第一个 batch)
    pe_matrix = output[0].numpy()

    print(f"生成的 PE 矩阵形状: {pe_matrix.shape}") # 预期: (50, 16)

    # 让我们看看第 0 个词和第 1 个词的前 4 维编码差异
    print(f"\\n第 0 个词的前 4 维: {pe_matrix[0, :4]}")
    print(f"第 1 个词的前 4 维: {pe_matrix[1, :4]}")

    观察与验证

    运行上面的代码,你可以观察到:

  • 直接相加:位置信息就像一层带有特殊纹理的“滤镜”,**直接叠加(相加)**在了原始词向量的数值上。
  • 高低频特征:你可以尝试用 matplotlib 画出 pe_matrix 的热力图(横轴是维度,纵轴是位置)。你会清晰地看到,靠左的列(前几个维度,高频)条纹变化非常密集,而靠右的列(后几个维度,低频)大块的颜色几乎不变,这就完美对应了前面“秒针”和“时针”的比喻。

  • 四、 稳固基石与升华:Add & Norm 与 Feed Forward

    到目前为止,我们已经通过 Attention 让词与词之间交流了信息。但是,深度学习模型要叠得很深才能变强,叠深了就容易出问题。因此,我们需要几个“基础设施”来保驾护航,并对交流后的信息进行“升华”。

    1. Add (残差连接):不忘初心与防断路

    公式: Output = x + Sublayer(x)

    • 直观比喻: 就像一个“保底机制”。无论 Attention 或其他层对你的词向量做了多么花里胡哨的操作(Sublayer(x)),最后都要加上你原本的样子(x)。
    • 实例说明: 假设你的词向量原本包含一个极其重要的特征,比如“否定情绪(值=9.0)”。但在复杂的 Attention 融合中,因为其他词的干扰,这个特征被意外冲淡成了 2.0。如果没有 Add,这个核心信息就丢失了。有了 Add(原特征 9.0 + 融合后特征 2.0 = 11.0),模型就能牢牢记住“这是一个否定句”的初心。
    • 作用:
    • 防止忘了原本的意思:即便 Attention 找错人了,加上原始输入还能兜底。
    • 防止梯度消失(最重要):在几十层深的网络里,反向传播的误差可以通过这个 + x 的“直达电梯”顺畅地传导回浅层,保证网络能训练起来。

    2. Norm (层归一化 LayerNorm):情绪稳定器

    机制: 对每个词的特征向量求均值和方差,把数值拉回到均值为 0、方差为 1 的标准分布。

    • 直观比喻: 每次做完 Add 操作后,词向量的数值可能会变得很大(就像一群人刚开完派对,情绪激动)。LayerNorm 就是一个“情绪稳定器”,让每个词的特征数值回归理智,防止后续计算时数据爆炸。

    3. Feed Forward (前馈神经网络):各自闭关修炼

    公式: FFN(x) = max(0, xW_1 + b_1)W_2 + b_2 (本质上是两个线性层夹着一个 ReLU 激活函数)。

    • 特点: Position-wise(逐词处理)。它对句子里的每个词,都独立、完全一样地进行一遍处理。此时词与词之间不交互。
    • 实例说明: 在 Attention 阶段,“苹果”这个词吸收了上下文中的“吃”和“削皮”,这只是信息的杂烩。进入 FFN 后,FFN 中的神经元(比如隐层维度扩大到 2048 维)就像是一个知识极其丰富的专家。专家看了一眼这些杂烩信息,通过非线性激活函数(ReLU)瞬间产生化学反应:“哦!这三个信息组合在一起,说明这里的‘苹果’是水果,绝对不是苹果手机!”这就是 FFN 把线性聚合的信息转化为高阶语义理解的过程。
    • 直观比喻:
      • Attention 阶段: 是“群聊模式”,词与词互相交换信息(理解上下文)。
      • FFN 阶段: 是“闭关模式”。大家聊完天,带着别人给的信息回到自己的房间,经过大脑思考(两层全连接网络 + 非线性激活),提炼出更高维度的内涵。

    五、 代码实战:组装完整的 Encoder Block

    现在,我们有了所有零件:Multi-Head Attention、Add & Norm、Feed Forward。让我们把它们组装成一个完整的 Transformer Encoder Block(编码器层)!

    import torch
    import torch.nn as nn

    # 假设我们已经有了一个写好的 Multi-Head Attention (为了简短,这里用 nn 库自带的演示)
    # 实际上这里放的就是我们之前拆解的 Attention 代码

    class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff):
    super().__init__()
    # 经典设计:先升维 (比如 512 -> 2048),再降维 (2048 -> 512)
    self.linear1 = nn.Linear(d_model, d_ff)
    self.relu = nn.ReLU()
    self.linear2 = nn.Linear(d_ff, d_model)

    def forward(self, x):
    # 独立对每个词进行非线性升华
    return self.linear2(self.relu(self.linear1(x)))

    class TransformerEncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff):
    super().__init__()
    self.self_attn = nn.MultiheadAttention(d_model, num_heads, batch_first=True)
    self.ffn = FeedForward(d_model, d_ff)

    # 两个独立的 LayerNorm 层
    self.norm1 = nn.LayerNorm(d_model)
    self.norm2 = nn.LayerNorm(d_model)

    def forward(self, x):
    # ——————————————
    # 子层 1:Multi-Head Self-Attention + Add & Norm
    # ——————————————
    # 1. 进 Attention 前,先存下原本的样子 (residual)
    residual = x

    # 2. 计算 Attention (Q=K=V=x)
    attn_output, _ = self.self_attn(x, x, x)

    # 3. Add & Norm
    # 注意:原始论文是先 Add 再 Norm (Post-LN)
    # 现在很多大模型(如 GPT, LLaMA)更流行先 Norm 再算(Pre-LN),这里按原论文标准写
    x = self.norm1(residual + attn_output)

    # ——————————————
    # 子层 2:Feed Forward + Add & Norm
    # ——————————————
    residual = x
    ffn_output = self.ffn(x)
    x = self.norm2(residual + ffn_output)

    return x

    # ==========================================
    # 动手观察验证
    # ==========================================
    if __name__ == "__main__":
    d_model = 16
    num_heads = 4
    d_ff = 64
    seq_len = 10
    batch_size = 2

    # 模拟带了位置编码的输入向量
    x = torch.randn(batch_size, seq_len, d_model)
    print(f"输入形状: {x.shape}")

    # 实例化一个完整的 Encoder Block
    encoder_block = TransformerEncoderLayer(d_model, num_heads, d_ff)

    # 前向传播
    output = encoder_block(x)

    # 形状不会改变!依然是 (batch_size, seq_len, d_model)
    # 这就是为什么 Transformer 可以无限叠积木(层)的原因
    print(f"输出形状: {output.shape}")
    print("🎉 恭喜!你已成功跑通了 Transformer 的核心大积木!")


    六、 终极拼图:Decoder (解码器) 与交叉注意力

    我们已经掌握了能看懂全文的 Encoder。现在,我们要让模型学会**“边看原文,边逐字吐词”**。这就是 Decoder(解码器)的工作!

    Decoder 的结构和 Encoder 很像,但它多了两个极其关键的“新魔法”:Masked Attention(掩码注意力) 和 Cross-Attention(交叉注意力)。

    1. Masked Self-Attention (掩码自注意力):禁止“偷看”未来

    场景设定: 机器翻译。原文(Encoder 输入)是 “I love you”,目标翻译(Decoder 输出)是 “我 爱 你”。

    在训练时,为了高效,我们会把 “我 爱 你” 一次性全部喂给 Decoder。 但问题来了:当模型在翻译“爱”这个词时,它绝对不能提前看到后面的“你”字!因为在真正推断(测试)时,模型是一个字一个字往外蹦的,它不可能知道未来还没生成的字。

    解决方案:Mask(掩盖面具) 我们在计算 Attention 相似度得分矩阵后,强行加一个“下三角面具”。

    • 把“我”和未来词(“爱”、“你”)的打分强制变成 -无穷大。
    • 经过 Softmax 后,这些 -无穷大 的位置权重就会变成 0。

    直观比喻:蒙眼派对 这就像在派对上,“我”只能回头看自己,不能看别人;“爱”只能回头看“我”和它自己,不能看“你”。这样就完美模拟了人类“从左到右,逐字生成”的真实过程。

    2. Cross-Attention (交叉注意力):寻找外援

    这是连接 Encoder(原文)和 Decoder(译文)的唯一桥梁!

    在 Decoder 层里,做完 Masked Self-Attention 后,词向量会进入 Cross-Attention 层。 这个时候,Q、K、V 不再是同源的了!

    • Q (Query / 寻偶标准):来自 Decoder。比如当前生成的词是“爱”,它的 Q 就是:“我现在需要翻译原句里的哪个词?”
    • K (Key / 自我展示标签):来自 Encoder 的最终输出。原句词向量的自我介绍:“我是 I”,“我是 love”。
    • V (Value / 内在真实价值):同样来自 Encoder 的最终输出。

    直观比喻: Decoder 里的“爱”拿着自己的需求(Q),跑去 Encoder 的“外援团”里,挨个问原句的词(K):“你们谁跟我的需求最匹配?” 发现 “love” 的得分最高!于是,“爱”就大量吸收了 “love” 的信息(V)。 这就是模型实现“翻译”或“对齐”的底层逻辑。

    3. 代码实战:组装完整的 Decoder Block

    import torch
    import torch.nn as nn

    class TransformerDecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff):
    super().__init__()
    # 1. 掩码多头自注意力 (禁止看未来)
    self.self_attn = nn.MultiheadAttention(d_model, num_heads, batch_first=True)
    # 2. 交叉注意力 (看 Encoder)
    self.cross_attn = nn.MultiheadAttention(d_model, num_heads, batch_first=True)
    # 3. 前馈神经网络
    self.ffn = FeedForward(d_model, d_ff)

    # 独立的 LayerNorm 层
    self.norm1 = nn.LayerNorm(d_model)
    self.norm2 = nn.LayerNorm(d_model)
    self.norm3 = nn.LayerNorm(d_model)

    def forward(self, x, memory, tgt_mask=None):
    """
    x: Decoder 的输入 (比如 "我 爱")
    memory: Encoder 的最终输出向量 (比如 "I love you" 的特征)
    tgt_mask: 遮挡未来词的面具 (下三角矩阵)
    """

    # ——————————————
    # 子层 1:Masked Self-Attention
    # ——————————————
    residual = x
    # 注意这里传入了 tgt_mask,防止偷看未来
    attn1_output, _ = self.self_attn(x, x, x, attn_mask=tgt_mask)
    x = self.norm1(residual + attn1_output)

    # ——————————————
    # 子层 2:Cross-Attention (核心:Q来自Decoder, K和V来自Encoder)
    # ——————————————
    residual = x
    # query = x (Decoder的当前状态)
    # key = value = memory (Encoder的输出)
    attn2_output, _ = self.cross_attn(query=x, key=memory, value=memory)
    x = self.norm2(residual + attn2_output)

    # ——————————————
    # 子层 3:Feed Forward
    # ——————————————
    residual = x
    ffn_output = self.ffn(x)
    x = self.norm3(residual + ffn_output)

    return x

    # ==========================================
    # 动手观察验证
    # ==========================================
    if __name__ == "__main__":
    d_model = 16
    num_heads = 4
    d_ff = 64
    batch_size = 2

    # 假设 Encoder 原文有 10 个词
    src_len = 10
    encoder_memory = torch.randn(batch_size, src_len, d_model)

    # 假设 Decoder 目前正在生成第 5 个词
    tgt_len = 5
    decoder_input = torch.randn(batch_size, tgt_len, d_model)

    # 生成下三角 Mask 矩阵 (掩盖未来信息)
    # 对于长度 5,生成一个 5×5 的矩阵,右上角全为 -inf,左下角全为 0
    tgt_mask = nn.Transformer.generate_square_subsequent_mask(tgt_len)

    print("目标词 Mask 矩阵 (只允许看过去和自己):")
    print(tgt_mask)

    # 实例化 Decoder Block
    decoder_block = TransformerDecoderLayer(d_model, num_heads, d_ff)

    # 前向传播
    output = decoder_block(decoder_input, encoder_memory, tgt_mask)

    print(f"\\nEncoder Memory 形状: {encoder_memory.shape}")
    print(f"Decoder Input 形状: {decoder_input.shape}")
    print(f"Decoder Output 形状: {output.shape}")
    print("🎉 恭喜!你已彻底打通 Transformer 的任督二脉!")


    结语: 从基础的 QKV 算缘分,到多头的影分身;从给词发号码牌的 Positional Encoding,到不忘初心的 Add&Norm;最后到禁止偷看的 Mask 和寻求外援的 Cross-Attention。 Transformer 的所有核心拼图你都已经掌握了!这也是当今大语言模型(如 GPT、Claude、LLaMA)最底层的运作逻辑。


    七、 上帝视角:把 “I love you” 翻译成 “我 爱 你” 的全流程串联

    我们现在就以上帝视角,把所有的“积木”串联起来,看看 Transformer 究竟是如何一步步把 “I love you” 翻译成 “我 爱 你” 的。

    整个过程分为三大阶段:Encoder 读懂原文、Decoder 准备发力、Cross-Attention 跨界翻译。

    第一阶段:Encoder 读懂原文

    目标:把 “I love you” 嚼碎,揉成一个包含全局语境的“高维记忆体(Memory)”。

    1. 查字典与发号码牌

    • 最开始,模型看到的是 ["I", "love", "you"]。
    • 它去英语字典(Embedding矩阵)里查到三个原始词向量。
    • 接着,Positional Encoding(位置编码) 启动!它给 “I” 贴上代表位置1的滤镜,给 “love” 贴上位置2的滤镜,给 “you” 贴上位置3的滤镜(对应维度的数值直接相加)。
    • 现状:此时的 “love” 不仅是个动词,还深刻地知道自己在这个句子中间。

    2. 群聊融合(Multi-Head Self-Attention)

    • 三个带有位置信息的词向量进入了 Encoder 的“微信群”。
    • 它们各自变出自己的身份:Q(需求)、K(标签)、V(内涵)。
    • “I” 发现自己是主语,它去看了看全群,发现 “love” 是动作,于是它吸取了 “love” 的信息。
    • “love” 也去看了全群,发现动作的发出者是 “I”,承受者是 “you”。于是 “love” 的向量疯狂吸收前后的信息。
    • 现状:现在的 “love” 已经不是死板的字典词汇了,它变成了一个动态向量,意思是“被 I 发出、作用于 you 的爱意动作”。

    3. 各自闭关修炼(Feed Forward)与基础设施(Add & Norm)

    • 群聊结束后,为了防止忘了自己原本是谁,加上原来的自己(Add 残差连接),并深呼吸平复一下数值(Layer Norm)。
    • 然后这三个词分别进入独立的小黑屋(Feed Forward 前馈网络),通过 ReLU 激活函数进行高级逻辑推理,把刚才群聊获取的线索提炼成更高阶的语义结论。
    • 这个过程(群聊+闭关)会重复好几次(比如叠 6 层 Encoder Block)。

    ✅ Encoder 任务完成! 最终,Encoder 输出了一组全新的向量集合。我们管这个东西叫 Memory(高维记忆体)。它把 “I love you” 的结构、主谓宾关系、单词内涵全部打包好了。


    第二阶段:Decoder 开始逐字憋词

    目标:看着刚刚做好的 Memory,结合自己已经吐出来的中文字,预测下一个中文字。

    假设我们现在正处于翻译的中期:Decoder 已经成功吐出了 <Start> 我 爱,正准备去预测下一个字(即“你”)。

    1. 准备当前已有的译文

    • 现在的输入是:[<Start>, "我", "爱"]。
    • 同样,查中文词典得到向量,并加上中文的位置编码(Positional Encoding)。

    2. 戴上面具的群聊(Masked Self-Attention)

    • 这三个中文字也建了个“微信群”,开始互相看,为了理顺中文自己的语法。
    • 关键点来了!Mask(面具)起作用了:
      • <Start> 只能看自己。
      • “我” 只能看 <Start> 和自己。
      • “爱” 只能看 <Start>、“我” 和自己。
    • 为什么?因为如果我们在训练时把后面的字漏给它,它就会作弊!Mask 保证了无论在训练还是推理时,当前字永远只能向左看历史,无法向右看未来。
    • 现状:通过这一层,中文内部的语境理顺了,“爱”这个字深刻理解了前面的主语是“我”。

    第三阶段:Cross-Attention 跨界翻译与最终预测

    目标:拿着中文的需求,去英文的 Memory 里进货。

    3. 寻求外援(Cross-Attention)

    • Decoder 这边刚刚处理完的词向量(特别是队伍最后面的那个词,代表当前最前沿的预测进度)想要获取翻译信息。
    • 大揭秘: Decoder 里当前队伍最末尾的向量(包含了 “我 爱” 的全部信息),化身为 Query (Q)。它的需求潜台词是:“我是中文‘爱’,我的主语是‘我’,我现在急需知道原文里我接下来该翻译谁?”
    • 它拿着这个 Q,冲进了第一阶段做好的 Encoder Memory 库里。
    • 原文 Memory 里的每个词 (“I”, “love”, “you”) 都举着自己的 Key (K) 来匹配。
    • Decoder 的 Q 发现,自己和 “you” 的 K 匹配得分奇高无比(因为主语和谓语都已经翻译过了,就差宾语了)。
    • 于是,它疯狂吸收 “you” 这个词的 Value (V)。

    4. 最后的提炼与输出(FFN + Linear + Softmax)

    • 吸收完 “you” 的英文精髓后,同样经过 Add & Norm 平复情绪,然后进入 Feed Forward 闭关提炼。
    • 最后,这个吸收了全部精华的向量,走进一个超级大的全连接层(Linear)。这个全连接层的大小等于中文词典的大小(比如包含 5 万个汉字)。
    • 它给字典里的每一个汉字打了一个分数。
    • 经过 Softmax 把分数变成概率。排在第一名、概率最大的那个字,正是:“你”!

    循环继续: 现在我们有了 <Start> 我 爱 你。下一轮,把这四个词送进去重复整个过程,最后预测出 <End>(句号/结束符)。 翻译彻底完成!


    总结全图:

  • Encoder 查字典加位置,通过无障碍群聊(Self-Attention)把英文句子的骨架和血肉揉成高维记忆体。
  • Decoder 查字典加位置,戴着面具群聊(Masked Attention)理顺已生成的中文半成品的逻辑。
  • 桥梁:Decoder 拿着半成品的 Q,去 Encoder 那里匹配 K 并提取 V(Cross-Attention)。
  • 最终在字典表里选出概率最大的下一个字!
  • 这就是 Transformer 震惊世界的魔法全过程。是不是有一种拨开云雾见青天的感觉?


    八、 终极大考:手写完整 Transformer 极简架构

    经过前面的拆解,我们已经把所有零件都打磨好了。现在,让我们把 Encoder、Decoder 加上 Embedding(词嵌入) 和 Positional Encoding(位置编码),组装成一个完整的、可运行的 Transformer 极简网络架构代码!

    import torch
    import torch.nn as nn
    import math

    # ———————————-
    # 1. 基础组件准备 (复用我们前面讲过的)
    # ———————————-

    class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
    super().__init__()
    pe = torch.zeros(max_len, d_model)
    position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
    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)
    pe[:, 1::2] = torch.cos(position * div_term)
    pe = pe.unsqueeze(0)
    self.register_buffer('pe', pe)

    def forward(self, x):
    x = x + self.pe[:, :x.size(1), :]
    return x

    class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff):
    super().__init__()
    self.linear1 = nn.Linear(d_model, d_ff)
    self.relu = nn.ReLU()
    self.linear2 = nn.Linear(d_ff, d_model)

    def forward(self, x):
    return self.linear2(self.relu(self.linear1(x)))

    # ———————————-
    # 2. 核心大积木:Encoder 和 Decoder 层
    # ———————————-

    class TransformerEncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff):
    super().__init__()
    self.self_attn = nn.MultiheadAttention(d_model, num_heads, batch_first=True)
    self.ffn = FeedForward(d_model, d_ff)
    self.norm1 = nn.LayerNorm(d_model)
    self.norm2 = nn.LayerNorm(d_model)

    def forward(self, x, src_mask=None):
    # 1. Self-Attention (群聊融合)
    attn_output, _ = self.self_attn(x, x, x, attn_mask=src_mask)
    x = self.norm1(x + attn_output)
    # 2. Feed Forward (闭关修炼)
    ffn_output = self.ffn(x)
    x = self.norm2(x + ffn_output)
    return x

    class TransformerDecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads, d_ff):
    super().__init__()
    self.self_attn = nn.MultiheadAttention(d_model, num_heads, batch_first=True)
    self.cross_attn = nn.MultiheadAttention(d_model, num_heads, batch_first=True)
    self.ffn = FeedForward(d_model, d_ff)
    self.norm1 = nn.LayerNorm(d_model)
    self.norm2 = nn.LayerNorm(d_model)
    self.norm3 = nn.LayerNorm(d_model)

    def forward(self, x, memory, tgt_mask=None):
    # 1. Masked Self-Attention (防作弊群聊)
    attn1_output, _ = self.self_attn(x, x, x, attn_mask=tgt_mask)
    x = self.norm1(x + attn1_output)
    # 2. Cross-Attention (寻找外援)
    attn2_output, _ = self.cross_attn(query=x, key=memory, value=memory)
    x = self.norm2(x + attn2_output)
    # 3. Feed Forward (闭关修炼)
    ffn_output = self.ffn(x)
    x = self.norm3(x + ffn_output)
    return x

    # ———————————-
    # 3. 终极组装:完整的 Transformer 架构
    # ———————————-

    class SimpleTransformer(nn.Module):
    def __init__(self, src_vocab_size, tgt_vocab_size, d_model=512, num_heads=8, d_ff=2048, num_layers=6):
    super().__init__()

    # 1. 词典查表层 (查字典)
    self.src_embedding = nn.Embedding(src_vocab_size, d_model)
    self.tgt_embedding = nn.Embedding(tgt_vocab_size, d_model)

    # 2. 发号码牌 (位置编码)
    self.pe = PositionalEncoding(d_model)

    # 3. 堆叠多层 Encoder
    self.encoder_layers = nn.ModuleList(
    [TransformerEncoderLayer(d_model, num_heads, d_ff) for _ in range(num_layers)]
    )

    # 4. 堆叠多层 Decoder
    self.decoder_layers = nn.ModuleList(
    [TransformerDecoderLayer(d_model, num_heads, d_ff) for _ in range(num_layers)]
    )

    # 5. 最后的分类器 (变成字典里每个词的概率打分)
    self.fc_out = nn.Linear(d_model, tgt_vocab_size)

    def forward(self, src, tgt, src_mask=None, tgt_mask=None):
    """
    src: 原文的整数索引序列 (batch_size, src_len)
    tgt: 译文的整数索引序列 (batch_size, tgt_len)
    """

    # ==================================
    # 阶段一:Encoder 读懂原文
    # ==================================
    # 查字典并加上位置编码
    enc_input = self.pe(self.src_embedding(src))

    # 穿过每一层 Encoder,生成最终的高维记忆体 Memory
    memory = enc_input
    for layer in self.encoder_layers:
    memory = layer(memory, src_mask)

    # ==================================
    # 阶段二与三:Decoder 憋词与交叉翻译
    # ==================================
    # 查字典并加上位置编码
    dec_input = self.pe(self.tgt_embedding(tgt))

    # 穿过每一层 Decoder (带着面具,并看着 Memory)
    x = dec_input
    for layer in self.decoder_layers:
    x = layer(x, memory, tgt_mask)

    # ==================================
    # 阶段四:预测下一个词
    # ==================================
    # 输出形状: (batch_size, tgt_len, tgt_vocab_size)
    logits = self.fc_out(x)
    return logits

    # ==========================================
    # 试运行一下我们的终极造物!
    # ==========================================
    if __name__ == "__main__":
    # 假设我们有一个超小型的词典
    src_vocab_size = 1000 # 英文词典有 1000 个词
    tgt_vocab_size = 2000 # 中文词典有 2000 个字

    # 实例化完整的 Transformer
    # (为了演示跑得快,我们把特征维度 d_model 设小点,层数设为 2)
    model = SimpleTransformer(
    src_vocab_size=src_vocab_size,
    tgt_vocab_size=tgt_vocab_size,
    d_model=64,
    num_heads=4,
    num_layers=2
    )

    # 伪造一些输入数据
    batch_size = 2
    src_len = 10 # 原文 10 个词 ("I love you…")
    tgt_len = 5 # 目前翻译了 5 个字 ("<Start> 我 爱…")

    src_data = torch.randint(0, src_vocab_size, (batch_size, src_len))
    tgt_data = torch.randint(0, tgt_vocab_size, (batch_size, tgt_len))

    # 核心:必须给目标端生成下三角面具防作弊!
    tgt_mask = nn.Transformer.generate_square_subsequent_mask(tgt_len)

    # 万事俱备,前向传播!
    predictions = model(src_data, tgt_data, tgt_mask=tgt_mask)

    print("模型运行成功!🎉")
    print(f"最终预测输出形状: {predictions.shape}")
    print("解释:")
    print(f"- Batch Size (批次大小): {predictions.size(0)}")
    print(f"- Target Length (当前生成长度): {predictions.size(1)}")
    print(f"- Vocab Probabilities (对字典里 {predictions.size(2)} 个词的概率打分)")

    代码架构说明:

    • 我们使用了 nn.ModuleList 来堆叠多层网络。原论文中堆叠了 6 层 Encoder 和 6 层 Decoder。深度越深,模型能够理解的语义就越抽象、越复杂。
    • 整个网络的数据流非常清晰:原文索引 -> Embedding -> 加位置编码 -> 进多层 Encoder 得出 Memory -> 译文索引 -> Embedding -> 加位置编码 -> 进多层 Decoder (结合 Mask 和 Memory) -> 线性层打分 -> 输出。

    这就是支撑起整个现代 AI 黄金时代的终极代码骨架。建议你可以自己复制这段代码,随意修改 d_model、num_layers 等参数,跑一跑看看张量维度的变化,这会对你的理解有极大的帮助!

    赞(0)
    未经允许不得转载:171主机测评 » 全网最接地气的 Transformer 核心原理解析:告别公式,直击本质!
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址