🚀 作者 :“大数据小禅@yopai”

🚀 文章简介 :本专栏后续将持续更新大模型相关文章,从开发到微调到RAG、多Agent等,个V: 【yopa66】,持续分享前沿AI实战。本文主要对Transformer架构做了一个详细介绍,内容较多,欢迎交流!

🚀 欢迎小伙伴们 点赞👍、收藏⭐、留言💬

Transformer入门篇

一、先搞清楚:PyTorch和Transformer到底啥关系?

很多刚入门的朋友容易搞混这两个概念,但是压根不是一个层面的东西:

PyTorch是什么?

  • 它是一个深度学习框架,就像你盖房子用的工具箱
  • 提供了张量计算、自动求导、GPU加速等基础功能
  • 你可以用它来实现各种神经网络模型

Transformer是什么?

  • 它是一种神经网络架构,是一种具体的模型设计方案
  • 就像房子的设计图纸,规定了怎么组织各个组件
  • 可以用PyTorch、TensorFlow等任何框架来实现

打个比方:

  • PyTorch = 锤子、钉子、电钻(工具)
  • Transformer = 房子设计图(架构方案)
  • 你用PyTorch这套工具,按照Transformer的设计图,把模型搭建出来

所以当你听到"用PyTorch实现Transformer",意思就是用PyTorch这个工具箱,按照Transformer的架构把模型代码写出来。

二、AI建模的核心点:特征抽取器

什么是特征抽取?

咱们先从一个简单例子说起。假设你要教电脑识别猫和狗:

原始数据: 一张图片(比如256×256像素,3个颜色通道) 最终目标: 判断是猫还是狗

中间这个把"原始像素"转化成"可以用来分类的有用信息"的过程,就叫特征抽取

传统方法的困境:

原始像素 → 手工设计的特征(边缘、纹理、颜色等)→ 分类器
           ↑ 这一步需要专家经验,很难

深度学习的突破:

原始数据 → 神经网络(自动学习特征)→ 输出结果
           ↑ 这就是特征抽取器!

为什么说Transformer是最核心的特征抽取器?

在深度学习发展史上,特征抽取器经历了几个时代:

  1. CNN时代(2012-2017): 卷积神经网络,擅长处理图像
  2. RNN/LSTM时代(2014-2017): 循环神经网络,擅长处理序列
  3. Transformer时代(2017-至今): 统治级架构,横扫NLP、CV、语音等所有领域

Transformer为什么这么牛?

因为它有一个超级武器——注意力机制(Attention)!这个机制让模型能够:

  1. 并行处理: 不像RNN那样必须一个一个字处理,Transformer可以同时看所有输入
  2. 长距离依赖: 轻松捕捉句子中相隔很远的词之间的关系
  3. 动态权重: 根据上下文动态调整每个词的重要性

举个例子:

句子:"银行账户里的钱不够了"
  • 传统方法:按顺序处理,"银行"的信息可能传到"钱"时已经衰减了
  • Transformer:同时看整个句子,立刻知道"银行"指的是金融机构而不是河岸

这就是为什么GPT、BERT、DALL-E、Stable Diffusion等顶级模型全都基于Transformer架构!

三、Transformer整体架构详解

好,现在进入重头戏!咱们来看看Transformer长什么样。

3.1 高层视角:黑盒模型

Transformer黑盒

最简单的理解:Transformer就是一个"翻译机器"

  • 输入: 一句法语
  • 输出: 对应的英语翻译

3.2 打开黑盒:编码器-解码器结构

编码器解码器

Transformer由两大部分组成:

  1. 编码器(Encoder): 理解输入的含义
  2. 解码器(Decoder): 生成输出

这就像你听到法语(编码器理解),然后用英语说出来(解码器生成)。

3.3 堆叠结构:6层编码器 + 6层解码器

堆叠结构

原始论文中:

  • 6个相同结构的编码器堆叠起来(不共享权重)
  • 6个相同结构的解码器堆叠起来
  • 为什么是6?没有特别原因,你可以试试其他数字

3.4 编码器内部结构

编码器结构

每个编码器有两个子层:

  1. Self-Attention层(自注意力层):
    • 让模型在处理每个词时,能看到句子中的其他所有词
    • 这是核心!后面会详细讲
  2. Feed Forward层(前馈神经网络):
    • 就是普通的全连接神经网络
    • 对每个位置独立应用(位置之间不共享信息)

数据流动过程:

输入词 → Embedding(词向量)
      ↓
Self-Attention(关注上下文)
      ↓
Add & Norm(残差连接+归一化)
      ↓
Feed Forward(非线性变换)
      ↓
Add & Norm
      ↓
输出到下一层编码器

3.5 解码器内部结构

解码器结构

解码器比编码器多了一层,有三个子层:

  1. Masked Self-Attention: 只能看到当前位置之前的词(不能偷看后面的答案)
  2. Encoder-Decoder Attention: 关注编码器的输出(读取原文信息)
  3. Feed Forward: 和编码器一样

为什么要Mask?

假设你在翻译"我爱你"→"I love you":

  • 生成"I"时:只能基于"我"
  • 生成"love"时:可以看"我爱"和已生成的"I"
  • 生成"you"时:可以看"我爱你"和"I love"

不能让模型在生成"I"的时候就看到答案里的"love",那不就作弊了吗?

3.6 完整数据流

数据流

训练时的完整流程:

  1. 输入句子 → Embedding + 位置编码 → 进入编码器
  2. 编码器逐层处理,提取特征
  3. 编码器输出 → 传给解码器的Encoder-Decoder Attention
  4. 解码器逐词生成翻译
  5. 最后Linear层 + Softmax → 输出概率分布

四、Decoder-Only架构的应用(GPT系列的秘密)

现在最火的GPT系列(ChatGPT、GPT-4等)用的不是完整的Transformer,而是只用了解码器(Decoder-Only)

4.1 为什么只用Decoder?

原始Transformer是为了翻译任务设计的:

  • 编码器:理解源语言
  • 解码器:生成目标语言

但对于文本生成任务(比如写文章、对话),我们只需要:

  • 输入:一段文本提示
  • 输出:续写的内容

这种情况下,不需要分开的编码器和解码器,只用解码器就够了!

4.2 Decoder-Only的工作原理

GPT架构

核心思想: 把输入和输出看成同一个序列

举个例子:

输入提示:"今天天气"
模型处理:"今天 天气 真 好 啊"
           ↑输入↑ ←→ ↑输出↑

模型在生成"真"的时候:

  • 可以看到"今天 天气"(输入)
  • 不能看到"好 啊"(未来的输出)

关键配置:

  • 使用Masked Self-Attention(遮住未来信息)
  • 没有Encoder-Decoder Attention层
  • 堆叠N个相同的Decoder Block

著名的Decoder-Only模型:

  • GPT系列: GPT-2、GPT-3、GPT-4、ChatGPT
  • LLaMA系列: Meta开源的大模型
  • Claude:
  • PaLM: Google的大模型

4.3 Decoder-Only vs 完整Transformer

特性 完整Transformer Decoder-Only
典型应用 机器翻译 文本生成、对话
结构 Encoder + Decoder 仅Decoder
输入输出 输入和输出是不同序列 输入输出是同一序列
训练目标 Seq2Seq(序列到序列) 语言建模(预测下一个词)
代表模型 BERT(只用Encoder)、T5 GPT系列、LLaMA

五、注意力机制图解(Self-Attention的魔法)

好了,终于到最核心的部分了!注意力机制到底是什么?

5.1 直观理解:什么是"注意力"?

咱们先看一个经典例子:

句子: “The animal didn’t cross the street because it was too tired” 问题: "it"指的是什么?

人类秒懂:it = animal(而不是street)

但对于计算机,怎么让它理解这种关系呢?这就需要注意力机制

注意力可视化

当模型处理"it"这个词时:

  • 注意力机制让它"关注"句子中的其他词
  • 发现"animal"和"it"的关系最强
  • 把"animal"的信息融入到"it"的表示中

5.2 注意力计算的直观过程

假设我们在处理句子:“Thinking Machines”

Step 1:为每个词创建三个向量

QKV向量

对于每个词(比如"Thinking"),我们创建三个向量:

  • Query(查询): “我想查询什么信息?”
  • Key(键): “我能提供什么信息?”
  • Value(值): “我的具体信息内容是什么?”

类比:图书馆检索系统

  • Query = 你输入的搜索关键词:“人工智能”
  • Key = 每本书的索引标签:“机器学习”、“深度学习”、“AI”…
  • Value = 书的实际内容

怎么得到这三个向量?

# 伪代码
X = 词的embedding向量(维度:512)

Q = X @ W_Q  # 查询向量(维度:64)
K = X @ W_K  # 键向量(维度:64)
V = X @ W_V  # 值向量(维度:64)

其中W_Q、W_K、W_V是模型训练学习到的权重矩阵。

Step 2:计算注意力分数

计算分数

假设我们在处理"Thinking"这个词:

  • 计算"Thinking"的Q和所有词的K的点积(dot product)
  • 这给出了"Thinking"对每个词的"关注程度"
# 对于第一个词"Thinking"
score_1 = Q_thinking · K_thinking  # 自己和自己
score_2 = Q_thinking · K_machines  # 和"Machines"

为什么用点积?

  • 点积大 → 两个向量方向相似 → 关系密切
  • 点积小 → 两个向量方向不同 → 关系不大

Step 3 & 4:缩放 + Softmax归一化

Softmax

# 缩放:除以sqrt(d_k),这里d_k=64
scaled_scores = scores / sqrt(64) = scores / 8

# Softmax:转换成概率分布(和为1)
attention_weights = softmax(scaled_scores)

为什么要除以8?

  • 点积的结果可能很大,导致梯度消失
  • 除以sqrt(d_k)让数值更稳定

Softmax的作用:

原始分数:[12, 8, 15]
Softmax后:[0.2, 0.1, 0.7]  ← 变成概率,和为1

这样我们就知道了:对于"Thinking"这个词,应该给"Machines"0.7的权重。

Step 5 & 6:加权求和

输出

最后一步:用注意力权重对Value向量加权求和

output = attention_weights[0] * V_thinking + \
         attention_weights[1] * V_machines

这个output就是"Thinking"这个词经过Self-Attention后的新表示,它融合了句子中其他词的信息!

5.3 矩阵形式计算(实际代码中的做法)

实际中,我们不会一个词一个词算,而是用矩阵一次性处理所有词:

矩阵计算

Step 1:计算Q、K、V矩阵

# X: (seq_len, d_model) = (句子长度, 512)
# W_Q, W_K, W_V: (d_model, d_k) = (512, 64)

Q = X @ W_Q  # (seq_len, 64)
K = X @ W_K  # (seq_len, 64)
V = X @ W_V  # (seq_len, 64)

Step 2:一个公式搞定所有!

Attention公式

Attention(Q, K, V) = softmax(Q @ K^T / sqrt(d_k)) @ V

这一个公式包含了之前所有步骤:

  1. Q @ K^T → 计算所有词对之间的分数
  2. / sqrt(d_k) → 缩放
  3. softmax(...) → 归一化成概率
  4. @ V → 加权求和

维度变化:

Q: (seq_len, d_k)
K^T: (d_k, seq_len)
Q@K^T: (seq_len, seq_len)  ← 注意力矩阵!
softmax: (seq_len, seq_len)
@V: (seq_len, d_k)  ← 输出维度

5.4 真实案例演示

让我们用一个真实例子走一遍流程:

输入句子: “I love AI”(3个词)

假设参数:

  • 词向量维度 d_model = 512
  • Q/K/V 维度 d_k = 64

数据流:

第1步:Embedding
"I"    → x1: [0.2, 0.5, ..., 0.3]  # 512维
"love" → x2: [0.8, 0.1, ..., 0.7]  # 512维
"AI"   → x3: [0.4, 0.9, ..., 0.2]  # 512维

第2步:生成Q、K、V
q1 = x1 @ W_Q → [0.1, 0.3, ..., 0.5]  # 64维
k1 = x1 @ W_K → [0.2, 0.1, ..., 0.8]  # 64维
v1 = x1 @ W_V → [0.5, 0.4, ..., 0.2]  # 64维
# 同样处理 love、AI

第3步:计算attention scores(以"I"为例)
score_I_I    = q1 · k1 = 32.5
score_I_love = q1 · k2 = 28.3
score_I_AI   = q1 · k3 = 15.7

第4步:缩放
scaled_scores = [32.5/8, 28.3/8, 15.7/8] = [4.06, 3.54, 1.96]

第5步:Softmax
weights = softmax([4.06, 3.54, 1.96]) = [0.51, 0.41, 0.08]
# 意思是:"I"主要关注自己(51%)和love(41%),很少关注AI(8%)

第6步:加权求和
output_I = 0.51*v1 + 0.41*v2 + 0.08*v3

最终,output_I就是"I"这个词在这个句子中的新表示,它已经融入了"love"和"AI"的上下文信息!

六、多头注意力(Multi-Head Attention)

6.1 为什么需要多个头?

单头注意力有个问题:它只能学习一种关系模式。

但语言中的关系是多样的:

  • 语法关系:“主语-动词”、“动词-宾语”
  • 语义关系:“同义词”、“反义词”
  • 位置关系:“相邻词”、“远距离依赖”

多头注意力的思想: 让模型同时学习多种不同的关系!

多头注意力

6.2 多头注意力的实现

原理很简单:并行运行h个不同的注意力机制

# Transformer使用8个注意力头
h = 8

# 每个头有自己的W_Q、W_K、W_V
for i in range(h):
    Q_i = X @ W_Q_i
    K_i = X @ W_K_i
    V_i = X @ W_V_i
    
    # 独立计算attention
    Z_i = Attention(Q_i, K_i, V_i)

多头输出

最后得到8个输出:Z_0, Z_1, …, Z_7

6.3 合并多头输出

合并

问题:前馈网络期望一个矩阵输入,但我们现在有8个!

解决办法:拼接 + 线性变换

# 拼接所有头的输出
Z_concat = concat(Z_0, Z_1, ..., Z_7)  # (seq_len, 8*d_k)

# 线性变换回原始维度
Z_final = Z_concat @ W_O  # (seq_len, d_model)

6.4 完整流程图

多头完整流程

总结:

  1. 输入X分别传给8个注意力头
  2. 每个头独立计算attention
  3. 拼接8个头的输出
  4. 线性变换得到最终结果

6.5 多头注意力的可视化

看看不同的头在关注什么:

多头可视化

当模型处理"it"这个词时:

  • 头1: 主要关注"animal"(语义关系)
  • 头2: 关注"tired"(属性关系)
  • 其他头关注不同的语法和语义模式

这就是为什么多头注意力这么强大——它能同时从多个角度理解句子!

七、位置编码(Positional Encoding)

7.1 为什么需要位置信息?

Self-Attention有个严重问题:它不知道词的顺序

想象一下:

  • “狗咬了人” 和 “人咬了狗” 在Self-Attention眼里是一样的!
  • 因为注意力只看词之间的关系,不管它们的位置

但词的顺序在语言中太重要了!

7.2 位置编码的方案

位置编码

解决办法: 在词向量中加入位置信息

# 最终输入 = 词向量 + 位置向量
input_embedding = word_embedding + positional_encoding

位置编码的公式:

PE(pos, 2i)   = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

其中:

  • pos:词的位置(0, 1, 2, …)
  • i:向量维度的索引
  • d_model:词向量维度(512)

7.3 位置编码可视化

位置编码示例

这是20个位置、512维的位置编码:

  • 每一行是一个位置的编码
  • 颜色代表数值大小
  • 可以看出明显的周期性模式

为什么用sin/cos函数?

  1. 值在[-1, 1]之间,不会太大
  2. 对于任意长度的句子都能生成编码
  3. 相对位置关系可以通过简单的线性变换得到

八、QKV架构细节深入剖析

8.1 为什么是Query、Key、Value?

这个命名来自于信息检索领域,让我们用一个完整类比理解:

场景:你在YouTube搜视频

┌─────────────────────────────────────┐
│ 1. 你输入搜索词:"transformer教程" │  ← Query(查询)
└─────────────────────────────────────┘
            ↓
┌─────────────────────────────────────────────────┐
│ 2. YouTube数据库里每个视频都有标签:          │
│    视频1: "AI, 机器学习, 神经网络"    │  ← Key(索引)
│    视频2: "transformer, NLP, 注意力"  │  ← Key
│    视频3: "做饭, 美食, 教程"          │  ← Key
└─────────────────────────────────────────────────┘
            ↓
┌─────────────────────────────────────────────────┐
│ 3. 匹配度计算:                                │
│    视频1: 匹配度 0.2(有点相关)              │
│    视频2: 匹配度 0.9(非常相关!)            │
│    视频3: 匹配度 0.0(不相关)                │
└─────────────────────────────────────────────────┘
            ↓
┌─────────────────────────────────────────────────┐
│ 4. 返回视频内容(根据匹配度加权):           │
│    主要展示视频2的内容(90%)        │  ← Value(内容)
│    少量展示视频1的内容(10%)        │  ← Value
│    不展示视频3                     │
└─────────────────────────────────────────────────┘

对应到Self-Attention:

  • Query: 当前词想要查找什么信息
  • Key: 每个词能被什么样的查询匹配到
  • Value: 每个词实际包含的信息内容

8.2 QKV的维度设计

# 典型参数
d_model = 512    # 词向量维度
d_k = d_v = 64   # QKV维度
h = 8            # 注意力头数

# 关键点:d_model = h * d_k
# 512 = 8 * 64

为什么QKV维度(64)比词向量(512)小?

  1. 计算效率: 注意力计算复杂度是O(n²),维度越小越快
  2. 多头注意力: 8个头 × 64维 = 512维,刚好等于原始维度
  3. 信息压缩: 强迫模型学习最重要的特征

8.3 QKV变换的详细过程

单个词的变换:

# 假设词"love"的embedding
x_love = [0.8, 0.1, 0.3, ..., 0.7]  # 512维

# 三个权重矩阵(512×64)
W_Q = [[0.1, 0.2, ..., 0.5],
       [0.3, 0.1, ..., 0.8],
       ...]  # 512行64列

W_K = [...]  # 512×64
W_V = [...]  # 512×64

# 矩阵乘法
q_love = x_love @ W_Q  # (1, 512) @ (512, 64) = (1, 64)
k_love = x_love @ W_K  # (1, 64)
v_love = x_love @ W_V  # (1, 64)

整个句子的变换:

# 句子矩阵:每行是一个词
X = [[x_I],      # 512维
     [x_love],   # 512维
     [x_AI]]     # 512维
# X的形状:(3, 512)

# 批量计算
Q = X @ W_Q  # (3, 512) @ (512, 64) = (3, 64)
K = X @ W_K  # (3, 64)
V = X @ W_V  # (3, 64)

# 结果:
# Q[0] = "I"的query向量
# Q[1] = "love"的query向量  
# Q[2] = "AI"的query向量

8.4 实际代码实现

import torch
import torch.nn as nn

class SelfAttention(nn.Module):
    def __init__(self, d_model=512, d_k=64):
        super().__init__()
        self.d_k = d_k
        
        # 定义三个线性变换
        self.W_Q = nn.Linear(d_model, d_k, bias=False)
        self.W_K = nn.Linear(d_model, d_k, bias=False)
        self.W_V = nn.Linear(d_model, d_k, bias=False)
        
    def forward(self, X):
        """
        X: (batch_size, seq_len, d_model)
        输出: (batch_size, seq_len, d_k)
        """
        # Step 1: 生成Q、K、V
        Q = self.W_Q(X)  # (batch, seq_len, d_k)
        K = self.W_K(X)  # (batch, seq_len, d_k)
        V = self.W_V(X)  # (batch, seq_len, d_k)
        
        # Step 2: 计算attention scores
        scores = torch.matmul(Q, K.transpose(-2, -1))  # (batch, seq_len, seq_len)
        scores = scores / (self.d_k ** 0.5)  # 缩放
        
        # Step 3: Softmax
        attention_weights = torch.softmax(scores, dim=-1)
        
        # Step 4: 加权求和
        output = torch.matmul(attention_weights, V)  # (batch, seq_len, d_k)
        
        return output, attention_weights

九、Attention输出计算的完整演示

9.1 手算一个简单例子

假设我们有一个极简化的例子(方便手算):

输入: 2个词,维度为4

# 词向量(实际是512维,这里简化成4维)
X = [[1, 0, 1, 0],   # "I"
     [0, 2, 0, 2]]   # "love"

# 权重矩阵(实际是512×64,这里简化成4×2)
W_Q = [[1, 0],
       [0, 1],
       [1, 0],
       [0, 1]]

W_K = [[0, 1],
       [1, 0],
       [0, 1],
       [1, 0]]

W_V = [[1, 1],
       [1, 1],
       [0, 0],
       [0, 0]]

Step 1: 计算Q、K、V

Q = X @ W_Q
  = [[1, 0, 1, 0],    @ [[1, 0],
     [0, 2, 0, 2]]       [0, 1],
                         [1, 0],
                         [0, 1]]
  = [[1*1+0*0+1*1+0*0, 1*0+0*1+1*0+0*1],
     [0*1+2*0+0*1+2*0, 0*0+2*1+0*0+2*1]]
  = [[2, 0],
     [0, 4]]

K = X @ W_K = [[0, 1],
               [2, 2]]

V = X @ W_V = [[2, 2],
               [2, 2]]

Step 2: 计算Attention Scores

Scores = Q @ K^T
       = [[2, 0],  @ [[0, 2],
          [0, 4]]     [1, 2]]
       = [[2*0+0*1, 2*2+0*2],
          [0*0+4*1, 0*2+4*2]]
       = [[0, 4],
          [4, 8]]

意义解读:

  • Scores[0][0] = 0: "I"对"I"的关注度
  • Scores[0][1] = 4: "I"对"love"的关注度
  • Scores[1][0] = 4: "love"对"I"的关注度
  • Scores[1][1] = 8: "love"对"love"的关注度

Step 3: 缩放(d_k=2)

Scaled = Scores / sqrt(2) = Scores / 1.414
       = [[0, 2.83],
          [2.83, 5.66]]

Step 4: Softmax

# 对每一行做softmax
Row 0: [0, 2.83]
  exp: [1, 16.95]
  sum: 17.95
  softmax: [0.06, 0.94]

Row 1: [2.83, 5.66]
  exp: [16.95, 287.28]
  sum: 304.23
  softmax: [0.06, 0.94]

Attention_Weights = [[0.06, 0.94],
                     [0.06, 0.94]]

意义:

  • “I"这个词:6%关注自己,94%关注"love”
  • “love"这个词:6%关注"I”,94%关注自己

Step 5: 加权求和

Output = Attention_Weights @ V
       = [[0.06, 0.94],  @ [[2, 2],
          [0.06, 0.94]]     [2, 2]]
       = [[0.06*2+0.94*2, 0.06*2+0.94*2],
          [0.06*2+0.94*2, 0.06*2+0.94*2]]
       = [[2, 2],
          [2, 2]]

9.2 完整架构图演示

让我们画一个完整的Attention计算流程图:

输入矩阵 X (seq_len × d_model)
    ↓
    ├─→ × W_Q → Q (seq_len × d_k) ┐
    │                               │
    ├─→ × W_K → K (seq_len × d_k) ─┼─→ Q·K^T → Scores (seq_len × seq_len)
    │                               │              ↓
    └─→ × W_V → V (seq_len × d_k) ─┘      ÷sqrt(d_k) → Scaled Scores
                     ↓                            ↓
                     │                     Softmax → Attention Weights
                     │                            ↓
                     └─────────── × ──────────────┘
                                  ↓
                            Output (seq_len × d_k)

9.3 实际尺寸的计算

让我们用真实的Transformer参数走一遍:

# 参数设置
batch_size = 32      # 批大小
seq_len = 50         # 句子长度
d_model = 512        # 词向量维度
d_k = 64            # Q/K/V维度
num_heads = 8       # 注意力头数

# 输入
X: (32, 50, 512)

# 单头注意力
Q = X @ W_Q: (32, 50, 512) @ (512, 64) = (32, 50, 64)
K = X @ W_K: (32, 50, 64)
V = X @ W_V: (32, 50, 64)

# Attention计算
Scores = Q @ K^T: (32, 50, 64) @ (32, 64, 50) = (32, 50, 50)
Scaled = Scores / 8: (32, 50, 50)
Weights = softmax(Scaled): (32, 50, 50)  ← 注意力矩阵!
Output = Weights @ V: (32, 50, 50) @ (32, 50, 64) = (32, 50, 64)

# 8个头的结果拼接
All_Heads_Output: (32, 50, 8*64) = (32, 50, 512)

# 最终线性变换
Final = All_Heads_Output @ W_O: (32, 50, 512)

9.4 可视化Attention矩阵

Attention Weights矩阵(seq_len × seq_len)是最关键的,它告诉我们每个词关注哪些词。

例子: “The cat sat on the mat”

       The   cat   sat   on   the   mat
The  [ 0.8   0.1   0.0   0.0   0.1   0.0 ]
cat  [ 0.2   0.5   0.2   0.0   0.0   0.1 ]
sat  [ 0.0   0.3   0.4   0.2   0.0   0.1 ]
on   [ 0.0   0.0   0.3   0.4   0.2   0.1 ]
the  [ 0.0   0.0   0.0   0.3   0.5   0.2 ]
mat  [ 0.0   0.1   0.0   0.1   0.3   0.5 ]

解读:

  • “cat"这个词:50%关注自己,20%关注"The”,20%关注"sat"
  • “sat"这个词:40%关注自己,30%关注"cat”,20%关注"on"
  • 可以看出动词"sat"关注了主语"cat"和介词"on"

十、实战:用PyTorch实现完整的Transformer Block

10.1 完整代码

import torch
import torch.nn as nn
import math

class MultiHeadAttention(nn.Module):
    """多头注意力层"""
    def __init__(self, d_model=512, num_heads=8):
        super().__init__()
        assert d_model % num_heads == 0, "d_model必须能被num_heads整除"
        
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads  # 每个头的维度
        
        # Q、K、V的权重矩阵(包含所有头)
        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 split_heads(self, x, batch_size):
        """
        将最后一维分成(num_heads, d_k)
        x: (batch, seq_len, d_model)
        输出: (batch, num_heads, seq_len, d_k)
        """
        x = x.view(batch_size, -1, self.num_heads, self.d_k)
        return x.transpose(1, 2)
    
    def forward(self, Q, K, V, mask=None):
        """
        Q, K, V: (batch, seq_len, d_model)
        mask: (batch, seq_len, seq_len) 可选
        """
        batch_size = Q.shape[0]
        
        # 1. 线性变换
        Q = self.W_Q(Q)  # (batch, seq_len, d_model)
        K = self.W_K(K)
        V = self.W_V(V)
        
        # 2. 分成多头
        Q = self.split_heads(Q, batch_size)  # (batch, num_heads, seq_len, d_k)
        K = self.split_heads(K, batch_size)
        V = self.split_heads(V, batch_size)
        
        # 3. 计算attention
        scores = torch.matmul(Q, K.transpose(-2, -1))  # (batch, num_heads, seq_len, seq_len)
        scores = scores / math.sqrt(self.d_k)  # 缩放
        
        # 4. 应用mask(如果有)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        
        # 5. Softmax
        attention_weights = torch.softmax(scores, dim=-1)
        
        # 6. 加权求和
        output = torch.matmul(attention_weights, V)  # (batch, num_heads, seq_len, d_k)
        
        # 7. 合并多头
        output = output.transpose(1, 2).contiguous()  # (batch, seq_len, num_heads, d_k)
        output = output.view(batch_size, -1, self.d_model)  # (batch, seq_len, d_model)
        
        # 8. 最后的线性变换
        output = self.W_O(output)
        
        return output, attention_weights

class FeedForward(nn.Module):
    """前馈神经网络"""
    def __init__(self, d_model=512, d_ff=2048):
        super().__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.relu = nn.ReLU()
        
    def forward(self, x):
        # x: (batch, seq_len, d_model)
        return self.linear2(self.relu(self.linear1(x)))

class TransformerBlock(nn.Module):
    """Transformer编码器块"""
    def __init__(self, d_model=512, num_heads=8, d_ff=2048, dropout=0.1):
        super().__init__()
        
        # 多头注意力
        self.attention = MultiHeadAttention(d_model, num_heads)
        
        # 前馈网络
        self.feed_forward = FeedForward(d_model, d_ff)
        
        # Layer Normalization
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        
        # Dropout
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, x, mask=None):
        """
        x: (batch, seq_len, d_model)
        mask: (batch, seq_len, seq_len)
        """
        # 1. 多头注意力 + 残差连接 + LayerNorm
        attn_output, attention_weights = self.attention(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_output))
        
        # 2. 前馈网络 + 残差连接 + LayerNorm
        ff_output = self.feed_forward(x)
        x = self.norm2(x + self.dropout(ff_output))
        
        return x, attention_weights

# 使用示例
if __name__ == "__main__":
    # 参数
    batch_size = 2
    seq_len = 10
    d_model = 512
    
    # 创建模型
    transformer_block = TransformerBlock(d_model=512, num_heads=8)
    
    # 随机输入
    x = torch.randn(batch_size, seq_len, d_model)
    
    # 前向传播
    output, attention_weights = transformer_block(x)
    
    print(f"输入形状: {x.shape}")
    print(f"输出形状: {output.shape}")
    print(f"注意力权重形状: {attention_weights.shape}")
    
    # 输出:
    # 输入形状: torch.Size([2, 10, 512])
    # 输出形状: torch.Size([2, 10, 512])
    # 注意力权重形状: torch.Size([2, 8, 10, 10])

10.2 代码详解

关键点1:split_heads函数

这个函数把(batch, seq_len, d_model)转换成(batch, num_heads, seq_len, d_k)

# 例子
x = torch.randn(2, 10, 512)  # batch=2, seq_len=10, d_model=512
# 目标:分成8个头,每个头64维

x = x.view(2, 10, 8, 64)  # (batch, seq_len, num_heads, d_k)
x = x.transpose(1, 2)      # (batch, num_heads, seq_len, d_k)
# 现在可以对每个头独立计算attention了!

关键点2:残差连接(Residual Connection)

# 不使用残差连接
x = self.attention(x)

# 使用残差连接(重要!)
x = x + self.attention(x)

为什么重要?

  • 防止梯度消失
  • 允许更深的网络(Transformer可以堆叠100+层)
  • 加速训练

关键点3:Layer Normalization

# 归一化每个样本的每个位置
x = self.norm(x)  # 在d_model维度上归一化

作用:

  • 稳定训练
  • 加速收敛

十一、小禅总结

11.1 核心要点回顾

  1. Transformer = Attention机制 + 残差连接 + LayerNorm
  2. 注意力机制的本质:
    • Query:我想找什么
    • Key:我是什么
    • Value:我有什么内容
    • 通过Q·K匹配,用权重对V加权求和
  3. 多头注意力的优势:
    • 同时学习多种关系模式
    • 提高模型表达能力
  4. Decoder-Only(GPT)vs 完整Transformer(T5):
    • GPT:只用Decoder,适合文本生成
    • T5:用完整架构,适合Seq2Seq任务
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐