July 22, 2025

Transformer代码深入理解

Harvard's code of transformer

全文主要由AI生成,是根据哈弗Annotated Transformer代码实现进行的代码解释

关于Transformer的内容理解见Transformer From Scratch

单元格 6:导入所有需要的库

是什么: 系统交互工具

import os
from os.path import exists

os 模块的本质:

  • os 是”操作系统”的缩写,是 Python 标准库的一部分
  • 这个模块提供了一套与操作系统交互的接口函数
  • exists 函数专门用来检查文件或目录是否存在,返回布尔值

为什么需要: 文件管理的必要性

在深度学习项目中,我们经常需要:

  1. 检查模型文件是否存在:避免重复下载或训练
  2. 创建保存目录:为训练结果和模型权重创建文件夹
  3. 路径处理:在不同操作系统上正确处理文件路径
  4. 环境检测:确认运行环境的配置

怎么做: 具体应用场景

# 典型使用示例
if not exists("model_weights.pt"):
    print("模型文件不存在,开始训练...")
else:
    print("发现已保存的模型,加载中...")

实际用途: 在这个Transformer项目中,主要用于检查预训练模型、数据集文件是否存在,以及创建输出目录。


是什么: 深度学习核心框架

import torch
import torch.nn as nn
from torch.nn.functional import log_softmax, pad

PyTorch 框架体系:

torch - 核心计算引擎:

  • PyTorch 是目前最流行的深度学习框架之一(由 Meta/Facebook 开发)
  • 核心数据结构是 Tensor(张量):多维数组,类似 numpy,但专为深度学习优化
  • 提供自动微分:自动计算梯度,这是神经网络训练的核心
  • 支持 GPU 加速:利用 CUDA 实现几十倍的计算加速

torch.nn - 神经网络构建工具包:

  • nn 是 “Neural Networks” 的缩写
  • 包含构建神经网络的所有基本组件:
    • 层(Layers):Linear(全连接)、Conv2d(卷积)、Embedding(嵌入)等
    • 激活函数:ReLU、Tanh、Sigmoid 等
    • 损失函数:CrossEntropyLoss、MSELoss 等
    • 容器:Sequential(顺序堆叠)、ModuleList(模块列表)等

torch.nn.functional - 函数式操作:

  • 包含无状态的函数版本,不需要创建层对象
  • log_softmax:对数softmax函数,用于多分类概率计算
  • pad:张量填充函数,为序列添加padding

为什么选择这些: 设计哲学与优势

为什么选择 PyTorch:

  1. 动态计算图:更灵活,便于调试和实验
  2. Python原生:与Python生态无缝集成
  3. 研究友好:易于实现复杂的研究想法
  4. 生产就绪:TorchScript可以部署到生产环境

为什么需要 nn 模块:

  1. 面向对象设计:将神经网络组件封装成类,便于管理
  2. 参数自动管理:自动跟踪和更新可学习参数
  3. 设备无关:一行代码即可在CPU/GPU间切换
  4. 状态管理:自动处理训练/评估模式切换

为什么使用 functional:

  1. 计算效率:某些操作的函数版本更高效
  2. 代码简洁:不需要预先定义层对象
  3. 灵活性:可以动态调整参数和行为

怎么做: 在Transformer中的具体应用

# 使用 nn 构建层
self.linear = nn.Linear(512, 30000)  # 全连接层:512维 -> 30000维(词汇表大小)

# 使用 functional 进行计算
output_probs = log_softmax(logits, dim=-1)  # 计算对数概率

# 使用 pad 处理变长序列
padded_sequences = pad(sequences, (0, max_len - seq_len))  # 右侧填充到最大长度

为什么这样设计: Transformer 模型需要处理变长文本序列、进行大规模矩阵运算、支持并行计算,PyTorch 的这些模块完美满足了这些需求。


是什么: Python标准库工具集

import math
import copy
import time

数学与工具模块详解:

math - 数学运算库:

  • Python内置的数学函数库
  • 提供基础数学函数:sqrt()log()sin()cos()exp()
  • 常数:math.pimath.e

copy - 对象复制工具:

  • 处理Python对象的复制操作
  • copy.copy():浅复制(shallow copy)
  • copy.deepcopy():深复制(deep copy),创建完全独立的副本

time - 时间处理模块:

  • 时间相关的功能:获取当前时间、计算时间差、休眠等
  • 主要函数:time.time()time.sleep()

为什么需要这些: 在Transformer中的关键作用

为什么需要 math:

  1. 注意力缩放:使用 math.sqrt(d_k) 缩放注意力分数
  2. 位置编码:使用三角函数 sincos 生成位置信息
  3. 初始化:某些权重初始化方法需要数学计算

为什么需要 copy:

  1. 层复制:Transformer有多层相同结构,需要创建独立的副本
  2. 参数隔离:确保不同层的参数不会互相影响
  3. 模型构建:避免多个层共享同一组参数

为什么需要 time:

  1. 性能监控:测量训练和推理的时间消耗
  2. 进度跟踪:计算剩余训练时间
  3. 性能优化:识别计算瓶颈

怎么做: 实际应用示例

# math 的使用
attention_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
# 为什么除以 sqrt(d_k)?防止 softmax 饱和,保持梯度流动

# copy 的使用
def clones(module, N):
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
# 创建N个相同但独立的层

# time 的使用
start_time = time.time()
# 训练代码...
training_time = time.time() - start_time
print(f"训练耗时: {training_time:.2f} 秒")

设计原理: 这些工具看似简单,但在大规模神经网络中起到精确控制的作用:数学函数确保数值稳定性,深复制保证模型结构的正确性,时间监控帮助优化性能。


是什么: 学习率优化工具

from torch.optim.lr_scheduler import LambdaLR

学习率调度器本质:

  • 学习率调度器是控制模型训练过程中学习率动态变化的工具
  • LambdaLR 允许使用自定义函数来调整学习率
  • 属于 PyTorch 优化模块的一部分

为什么至关重要: 训练稳定性的核心

学习率的重要性:

  1. 训练稳定性

    • 太大:梯度爆炸,训练发散,loss震荡
    • 太小:收敛极慢,容易陷入局部最优
    • 需要找到”甜蜜点”
  2. Transformer特殊需求:

    • 大模型敏感性:参数量巨大,对学习率极其敏感
    • 注意力机制:需要特殊的预热策略
    • 梯度特性:初期梯度不稳定,需要小心处理

为什么选择 LambdaLR:

  • 高度自定义:可以实现任意复杂的调度策略
  • 数学精确:直接使用数学函数描述学习率变化
  • 研究重现:能精确复现论文中的学习率策略

怎么做: Transformer的学习率策略

原论文的学习率公式:

def rate(step, model_size, factor, warmup):
    # lr = factor * (model_size^(-0.5) * min(step^(-0.5), step * warmup^(-1.5)))
    if step == 0:
        step = 1
    return factor * (
        model_size ** (-0.5) * min(step ** (-0.5), step * warmup ** (-1.5))
    )

# 实际使用
scheduler = LambdaLR(optimizer, lr_lambda=lambda step: rate(step, 512, 1, 4000))

策略分解:

  1. Warmup阶段(前4000步):学习率线性增长
    • 原因:让模型逐渐适应数据分布,避免初期的不稳定
  2. 衰减阶段:学习率按步数的平方根衰减
    • 原因:后期需要更精细的调整,避免过冲

为什么这样设计: 这种策略结合了稳定性(warmup)和收敛性(衰减),被证明对大型Transformer模型特别有效。


import pandas as pd
import altair as alt

数据分析和可视化工具:

pandas

  • 强大的数据分析库
  • 提供 DataFrame 数据结构,类似于 Excel 表格
  • 可以方便地读取、处理、分析数据

altair

  • 数据可视化库
  • 可以创建漂亮的交互式图表
  • 在这个项目中用于可视化注意力权重、训练进度等

from torchtext.data.functional import to_map_style_dataset
from torch.utils.data import DataLoader
from torchtext.vocab import build_vocab_from_iterator
import torchtext.datasets as datasets

文本处理和数据加载工具:

torchtext

  • PyTorch 的官方文本处理库
  • 专门用于处理自然语言数据

to_map_style_dataset

  • 将数据转换为 PyTorch 能够理解的数据集格式

DataLoader

  • 非常重要的工具,用于:
    • 将数据分成小批次(batches)
    • 打乱数据顺序
    • 并行加载数据,提高效率

build_vocab_from_iterator

  • 从文本数据中构建词汇表
  • 词汇表是单词到数字的映射,因为计算机只能处理数字

datasets

  • 包含了一些标准的数据集(如翻译数据集)

import spacy

自然语言处理工具:

  • spaCy 是另一个强大的 NLP 库
  • 主要用于分词(将句子拆分成单词或子词)
  • 支持多种语言
  • 在这个项目中用于预处理文本数据

import GPUtil

GPU 监控工具:

  • 用于监控 GPU 的使用情况
  • 可以查看 GPU 内存使用量、温度等
  • 在训练大型模型时,监控 GPU 状态很重要

import warnings
from torch.utils.data.distributed import DistributedSampler
import torch.distributed as dist
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP

高级功能模块:

warnings

  • 控制 Python 警告信息的显示
  • 可以忽略不重要的警告,让输出更清洁

分布式训练模块

  • DistributedSampler:在多GPU或多机器训练时分配数据
  • torch.distributed:分布式训练的核心模块
  • torch.multiprocessing:多进程处理
  • DistributedDataParallel:将模型分布到多个GPU上并行训练

为什么需要分布式训练: Transformer 模型通常很大,单个 GPU 可能无法承载。分布式训练可以将模型和数据分布到多个 GPU 或机器上,大大加快训练速度。


# Set to False to skip notebook execution (e.g. for debugging)
warnings.filterwarnings("ignore")
RUN_EXAMPLES = True

配置设置:

warnings.filterwarnings("ignore")

  • 告诉 Python 忽略所有警告信息
  • 这让输出更清洁,但在调试时可能需要注意

RUN_EXAMPLES = True

  • 这是一个自定义的全局变量(布尔值,True/False)
  • 用于控制是否运行示例代码
  • 当设为 False 时,可以跳过耗时的示例,只查看代码结构

单元格 7:辅助函数和虚拟类

是什么: 代码管理和测试工具

def is_interactive_notebook():
    return __name__ == "__main__"

函数定义详解:

  • 函数语法def 关键字定义函数,() 内为参数列表,: 后为函数体
  • 返回机制return 指定函数的输出值
  • 特殊变量__name__ 是Python内置变量,存储模块名称

__name__ == "__main__" 的深层含义:

  • 当Python文件直接运行时,__name__ 被设为 "__main__"
  • 当文件被导入时,__name__ 是文件名
  • 在Jupyter Notebook中,每个单元格都被视为”直接运行”

为什么需要环境检测: 代码适应性

不同运行环境的挑战:

  1. Jupyter Notebook:交互式环境,单元格独立运行
  2. Python脚本:整体运行,可能被其他模块导入
  3. 模块导入:作为库使用,不应执行示例代码

环境检测的价值:

  • 防止意外执行:避免导入时运行示例代码
  • 条件控制:只在合适的环境下运行测试代码
  • 代码复用:同一份代码在不同环境下表现不同

怎么做: 智能的示例管理

def show_example(fn, args=[]):
    if __name__ == "__main__" and RUN_EXAMPLES:
        return fn(*args)

def execute_example(fn, args=[]):
    if __name__ == "__main__" and RUN_EXAMPLES:
        fn(*args)

高阶函数设计:

  • 函数作为参数fn 是一个函数对象,体现了Python的函数是”一等公民”
  • 参数解包*args 将列表元素展开为独立参数
  • 默认参数args=[] 提供空列表作为默认值

双重条件检查:

  1. 环境检查__name__ == "__main__" 确保在正确环境
  2. 开关控制RUN_EXAMPLES 提供手动控制

两个函数的差异:

  • show_example有返回值,适用于需要显示结果的演示
  • execute_example无返回值,适用于执行操作的演示

使用示例:

# 使用 show_example 显示结果
result = show_example(my_calculation_function, [arg1, arg2])

# 使用 execute_example 执行操作
execute_example(my_visualization_function, [data])

是什么: 测试用虚拟对象

class DummyOptimizer(torch.optim.Optimizer):
    def __init__(self):
        self.param_groups = [{"lr": 0}]
        None

    def step(self):
        None

    def zero_grad(self, set_to_none=False):
        None

class DummyScheduler:
    def step(self):
        None

面向对象编程基础:

  • 类定义class 关键字创建新的对象类型
  • 继承机制(torch.optim.Optimizer) 表示继承父类功能
  • 方法重写:子类重新定义父类的方法
  • 构造函数__init__ 方法在对象创建时自动调用

虚拟对象模式(Stub Pattern):

  • 接口保持:提供与真实对象相同的方法接口
  • 功能禁用:方法体为空或返回None,不执行实际操作
  • 占位作用:在测试或演示中代替真实对象

为什么需要虚拟对象: 开发和测试的需要

软件开发中的常见需求:

  1. 单元测试:测试代码逻辑而不执行实际训练
  2. 快速原型:验证架构而不消耗计算资源
  3. 代码演示:展示代码结构而不等待训练完成
  4. 调试分析:隔离问题而不受训练过程干扰

优化器的复杂性:

  • 真实优化器需要计算梯度更新参数管理状态
  • 虚拟优化器避免了这些重计算操作
  • 保持了代码的接口一致性

为什么继承 torch.optim.Optimizer:

  • 类型检查:确保对象类型正确
  • 接口兼容:其他代码期望optimizer对象有特定方法
  • 属性继承:自动获得必需的属性结构

怎么做: 实际应用场景

必需属性的最小实现:

self.param_groups = [{"lr": 0}]  # PyTorch优化器必须有的属性
  • param_groups:存储参数组和学习率信息
  • 即使是虚拟对象,也必须提供这个属性来保持兼容性

方法的空实现:

def step(self):        # 通常用于参数更新
    None              # 什么都不做

def zero_grad(self, set_to_none=False):  # 通常用于梯度清零
    None              # 什么都不做

使用场景示例:

# 在快速测试中使用
if TESTING_MODE:
    optimizer = DummyOptimizer()
    scheduler = DummyScheduler()
else:
    optimizer = torch.optim.Adam(model.parameters())
    scheduler = torch.optim.lr_scheduler.StepLR(optimizer)

# 代码的其余部分保持不变
optimizer.step()        # 测试时不执行,训练时正常执行
scheduler.step()        # 同样的逻辑

设计哲学: 这种设计体现了依赖注入接口隔离的原则,让代码在不同场景下具有高度的灵活性可测试性


总结第一部分

到目前为止,我们已经看到了:

  1. 环境准备:安装依赖包的命令
  2. 工具导入:导入了构建 Transformer 模型所需的所有工具
  3. 辅助函数:定义了一些帮助管理和测试代码的工具

这些准备工作为后面的核心内容奠定了基础。在下一部分,我们将开始看到 Transformer 模型的实际实现。

每个导入的库都有其特定用途:

  • torch 系列:构建和训练神经网络
  • 数据处理:处理文本数据、创建词汇表
  • 可视化:创建图表、监控训练进度
  • 系统工具:文件操作、时间计算、GPU 监控

这种模块化的设计让复杂的深度学习项目变得可管理和可维护。


单元格 8-13:背景介绍和目录

这些单元格都是 Markdown 格式,包含了关于 Transformer 模型的背景介绍。它们解释了为什么需要 Transformer,以及它相比之前的模型有什么优势。这些都是文字说明,不是代码,所以我们直接跳到核心的模型实现部分。


单元格 14:EncoderDecoder 类 - Transformer 的整体架构

是什么: Transformer的顶层控制器

class EncoderDecoder(nn.Module):
    """
    A standard Encoder-Decoder architecture. Base for this and many
    other models.
    """

    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = tgt_embed
        self.generator = generator

    def forward(self, src, tgt, src_mask, tgt_mask):
        "Take in and process masked src and target sequences."
        return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)

    def encode(self, src, src_mask):
        return self.encoder(self.src_embed(src), src_mask)

    def decode(self, memory, src_mask, tgt, tgt_mask):
        return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

面向对象编程的核心概念:

  • 类定义class 创建新的数据类型,定义对象的结构和行为
  • 继承机制(nn.Module) 从PyTorch基类继承功能
  • 组合模式:将多个组件组合成一个完整的系统

继承 nn.Module 的深层原因:

  1. 参数自动注册:所有神经网络参数被自动跟踪
  2. 设备管理:一键在CPU/GPU间移动模型
  3. 状态管理:train()/eval()模式自动切换
  4. 序列化支持:模型保存/加载功能
  5. 钩子系统:支持前向/后向传播的钩子函数

为什么这样设计: 编码器-解码器的架构优势

序列到序列任务的挑战:

  1. 变长输入输出:输入和输出序列长度可能不同
  2. 语义理解:需要理解输入的完整含义
  3. 生成控制:输出需要逐步生成,保持连贯性
  4. 注意力机制:需要在生成时关注输入的不同部分

编码器-解码器分离的好处:

  1. 职责明确:编码器专注理解,解码器专注生成
  2. 可扩展性:两部分可以独立改进
  3. 通用性:这种架构适用于多种seq2seq任务
  4. 并行化:编码器可以并行处理整个输入序列

组件设计哲学:

  • encoder理解模块 - 将输入序列编码为语义表示
  • decoder生成模块 - 基于编码表示生成输出序列
  • src_embed输入映射 - 将源语言词汇转为向量空间
  • tgt_embed输出映射 - 将目标语言词汇转为向量空间
  • generator概率输出 - 将隐藏状态转为词汇概率

怎么做: 数据流和处理流程

前向传播的完整流程:

def forward(self, src, tgt, src_mask, tgt_mask):
    return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)

步骤分解:

  1. 编码阶段self.encode(src, src_mask)

    • 源序列经过嵌入层转为向量
    • 编码器处理整个序列,产生语义表示
  2. 解码阶段self.decode(memory, src_mask, tgt, tgt_mask)

    • 目标序列经过嵌入层
    • 解码器基于编码结果生成输出

掩码机制的重要性:

  • src_mask:隐藏源序列的填充部分
  • tgt_mask:隐藏目标序列的填充部分和未来信息(因果掩码)

方法设计的解耦原则:

def encode(self, src, src_mask):
    return self.encoder(self.src_embed(src), src_mask)

def decode(self, memory, src_mask, tgt, tgt_mask):
    return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
  • 单一职责:每个方法只负责一个明确的功能
  • 可测试性:可以单独测试编码或解码过程
  • 可复用性:推理时只需要调用encode一次,decode多次

实际使用场景:

# 训练时:teacher forcing,已知完整目标序列
output = model(src_tokens, tgt_tokens, src_mask, tgt_mask)

# 推理时:逐步生成
memory = model.encode(src_tokens, src_mask)
for step in range(max_length):
    output = model.decode(memory, src_mask, generated_so_far, causal_mask)
    next_token = select_next_token(output)
    generated_so_far = append(generated_so_far, next_token)

单元格 15:Generator 类 - 将隐藏状态转换为词汇概率

是什么: 概率分布生成器

class Generator(nn.Module):
    "Define standard linear + softmax generation step."

    def __init__(self, d_model, vocab):
        super(Generator, self).__init__()
        self.proj = nn.Linear(d_model, vocab)

    def forward(self, x):
        return log_softmax(self.proj(x), dim=-1)

神经网络输出层的本质:

  • 线性投影:将高维隐藏状态映射到词汇空间
  • 概率归一化:将实数分数转换为概率分布
  • 对数空间:在对数域进行计算,提高数值稳定性

为什么需要Generator: 从向量到词汇的转换

高维向量的语义问题:

  1. 解码器输出:每个位置产生d_model维向量(如512维)
  2. 词汇表映射:需要判断这个向量对应哪个词汇
  3. 概率解释:需要为每个可能的词汇分配概率
  4. 可微分性:整个过程必须支持梯度传播

线性投影的数学原理:

self.proj = nn.Linear(d_model, vocab)
# 数学表示:output = input × W + b
# W: (d_model, vocab) 权重矩阵
# b: (vocab,) 偏置向量

为什么使用线性层:

  1. 计算效率:矩阵乘法可以高度并行化
  2. 表达能力:线性变换足以学习向量到词汇的映射
  3. 梯度友好:线性函数的梯度计算简单稳定
  4. 内存效率:相比非线性层,参数量相对可控

怎么做: 概率计算的技术细节

Softmax函数的数学含义:

softmax(x_i) = exp(x_i) / Σ(exp(x_j))
  • 将任意实数转换为概率(0-1之间,和为1)
  • 较大的输入值得到较高的概率

Log-Softmax的优势:

return log_softmax(self.proj(x), dim=-1)

为什么使用对数概率:

  1. 数值稳定性:避免exp()函数的上溢/下溢
  2. 计算精度:在对数域计算更精确
  3. 损失函数:交叉熵损失天然使用对数概率
  4. 梯度计算:对数函数的梯度更稳定

维度处理的详细说明:

  • dim=-1:在最后一个维度应用softmax
  • 输入形状:(batch_size, seq_length, d_model)
  • 输出形状:(batch_size, seq_length, vocab_size)
  • 每个位置都有一个完整的词汇概率分布

实际计算流程:

# 假设 d_model=512, vocab_size=30000
hidden_state = torch.randn(32, 20, 512)  # (batch, length, hidden)
generator = Generator(512, 30000)

# 1. 线性投影
logits = generator.proj(hidden_state)     # (32, 20, 30000)

# 2. 对数概率计算
log_probs = log_softmax(logits, dim=-1)   # (32, 20, 30000)

# 3. 每个位置的概率分布
# log_probs[0, 0, :] 是第一个样本第一个位置的词汇对数概率

与训练损失的连接:

# Generator输出对数概率
log_probs = generator(decoder_output)

# 交叉熵损失直接使用对数概率
loss = F.nll_loss(log_probs.view(-1, vocab_size), target_tokens.view(-1))

这种设计确保了从语义向量词汇选择的平滑过渡,是整个生成过程的关键最后一步。


单元格 16-18:图片和说明

这些单元格包含了 Transformer 架构的图片说明,帮助理解模型的整体结构。


单元格 19:clones 函数 - 创建多个相同的层

是什么: 神经网络层的复制工厂

def clones(module, N):
    "Produce N identical layers."
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

函数的核心功能:

  • 输入:一个神经网络模块和要复制的次数
  • 输出:N个结构相同但参数独立的模块列表
  • 目的:批量创建具有相同架构的神经网络层

为什么需要这个函数: Transformer的结构重复性

Transformer的架构特点:

  1. 编码器堆叠:通常包含6个相同的编码器层
  2. 解码器堆叠:通常包含6个相同的解码器层
  3. 多头注意力:每层包含多个相同结构的注意力头
  4. 层的一致性:保持每层架构相同,但参数独立

为什么不能简单复制引用:

# 错误的做法
layer = SomeLayer()
layers = [layer] * 6  # 所有元素指向同一个对象!

# 正确的做法
layers = clones(layer, 6)  # 每个都是独立的对象

参数独立性的重要性:

  • 学习差异化:每层需要学习不同的特征表示
  • 梯度更新:参数必须能够独立更新
  • 功能分工:不同层可能专注于不同的语言现象

怎么做: 深度复制与容器管理

深度复制的技术细节:

copy.deepcopy(module)

深拷贝 vs 浅拷贝的区别:

  1. 浅拷贝:只复制对象的第一层引用

    shallow_copy = copy.copy(module)  # 参数仍然共享!
  2. 深拷贝:递归复制所有层级的对象

    deep_copy = copy.deepcopy(module)  # 完全独立的副本

列表推导式的解析:

[copy.deepcopy(module) for _ in range(N)]
  • range(N):生成0到N-1的序列
  • for _ in range(N):下划线表示不使用循环变量
  • copy.deepcopy(module):每次循环创建一个独立副本

nn.ModuleList的特殊作用:

return nn.ModuleList([...])

为什么不用普通的Python列表:

  1. 参数注册:PyTorch自动识别和注册其中的所有参数
  2. 设备管理.to(device) 会自动移动所有子模块
  3. 状态管理.train().eval() 会递归应用到所有子模块
  4. 序列化支持:模型保存时会包含所有子模块

实际使用示例:

# 创建一个注意力层模板
attention_template = MultiHeadedAttention(8, 512)

# 创建6个独立的编码器层
encoder_layers = clones(EncoderLayer(512, attention_template, ff_layer, 0.1), 6)

# 验证参数独立性
print(id(encoder_layers[0].self_attn))  # 不同的内存地址
print(id(encoder_layers[1].self_attn))  # 证明是独立对象

内存和计算考虑:

  • 内存开销:每个副本都占用独立的内存空间
  • 初始化一致性:所有副本从相同的初始状态开始
  • 训练动态:通过梯度下降,各层参数会逐渐分化

单元格 20:Encoder 类 - 编码器的实现

是什么: 多层编码器的堆叠容器

class Encoder(nn.Module):
    "Core encoder is a stack of N layers"

    def __init__(self, layer, N):
        super(Encoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
        "Pass the input (and mask) through each layer in turn."
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

编码器的架构设计:

  • 层次结构:由N个相同结构的编码器层堆叠而成
  • 顺序处理:输入依次通过每一层进行处理
  • 最终规范化:在输出前进行层归一化

为什么采用堆叠设计: 深度学习的表示能力

深度网络的优势:

  1. 层次特征学习

    • 浅层:学习局部特征(如词汇、短语)
    • 深层:学习全局特征(如语法、语义)
  2. 非线性表达能力

    • 每层增加模型的非线性变换能力
    • 更深的网络可以表示更复杂的函数
  3. 渐进式抽象

    • 第1层:词汇级别的表示
    • 第2-3层:短语级别的表示
    • 第4-6层:句子级别的语义表示

为什么是6层:

  • 经验最优:原论文实验发现6层在性能和计算成本间的平衡点
  • 梯度传播:足够深以学习复杂特征,又不至于梯度消失
  • 计算效率:训练时间和性能的权衡

怎么做: 前向传播的具体实现

顺序处理的实现:

def forward(self, x, mask):
    for layer in self.layers:
        x = layer(x, mask)
    return self.norm(x)

数据流分析:

  1. 输入x 是嵌入后的序列,mask 是注意力掩码
  2. 逐层处理:每层接收前一层的输出作为输入
  3. 掩码传递:所有层共享相同的掩码信息
  4. 最终归一化:输出前进行层归一化处理

为什么在最后进行层归一化: 确保编码器的最终输出具有稳定的数值分布,为后续的解码器或其他组件提供良好的输入。 经过6层的残差连接后,数值可能会发生累积偏移,最后的归一化起到校正作用。

return self.norm(x)

单元格 21-22:LayerNorm 类 - 层归一化

是什么: 神经网络中的数据标准化技术

class LayerNorm(nn.Module):
    "Construct a layernorm module (See citation for details)."

    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        self.eps = eps

    def forward(self, x):
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

层归一化的数学本质:

  • 标准化操作:将数据转换为均值为0、标准差为1的分布
  • 仿射变换:通过可学习参数进行缩放和平移
  • 数值稳定性:防止数值计算中的溢出和下溢问题

为什么需要层归一化: 深度网络训练的挑战

深度网络的训练问题:

  1. 内部协变量偏移:随着网络层数增加,每层的输入分布不断变化
  2. 梯度爆炸/消失:深层网络中梯度可能变得极大或极小
  3. 学习率敏感性:不同的初始化和学习率对性能影响巨大
  4. 收敛缓慢:训练过程可能非常缓慢或不稳定

层归一化的解决方案:

  1. 稳定分布:每层的输入保持相似的数值范围
  2. 加速收敛:标准化的数据更容易优化
  3. 减少依赖:对权重初始化和学习率不那么敏感
  4. 提升性能:通常能获得更好的最终效果

层归一化 vs 批归一化的区别:

  • 批归一化:在批次维度上计算统计量,适用于CNN
  • 层归一化:在特征维度上计算统计量,适用于RNN和Transformer
  • 独立性:层归一化不依赖批次大小,更适合变长序列

怎么做: 数学公式和实现细节

数学公式详解:

μ = mean(x)                    # 计算均值
σ = std(x)                     # 计算标准差
x_norm = (x - μ) / (σ + ε)     # 标准化
y = γ * x_norm + β             # 仿射变换

参数的含义和作用:

self.a_2 = nn.Parameter(torch.ones(features))   # γ (gamma) 缩放参数
self.b_2 = nn.Parameter(torch.zeros(features))  # β (beta) 偏移参数
self.eps = eps                                   # ε (epsilon) 数值稳定项

nn.Parameter的特殊性:

  • 自动注册:PyTorch自动将其识别为模型参数
  • 梯度计算:参与反向传播,可以被优化器更新
  • 设备管理:跟随模型在GPU/CPU间移动
  • 序列化:模型保存时包含这些参数的值

前向传播的具体实现:

def forward(self, x):
    mean = x.mean(-1, keepdim=True)     # 沿最后一维计算均值
    std = x.std(-1, keepdim=True)       # 沿最后一维计算标准差
    return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

维度处理的技术细节:

  • dim=-1:在最后一个维度(特征维度)上计算统计量
  • keepdim=True:保持维度,便于广播运算
  • 输入shape:[batch_size, seq_length, d_model]
  • 统计量shape:[batch_size, seq_length, 1]

为什么在特征维度归一化:

# 假设输入形状为 [2, 3, 4] (batch=2, seq=3, features=4)
x = torch.randn(2, 3, 4)
mean = x.mean(-1, keepdim=True)  # shape: [2, 3, 1]
# 每个位置的4个特征被独立归一化
  1. 语义一致性:每个位置的特征向量代表语义信息
  2. 尺度统一:不同特征维度的数值范围得到统一
  3. 位置独立:每个序列位置的归一化是独立的

初始化策略的原理:

  • torch.ones(features):γ初始化为1,保持标准化后的方差
  • torch.zeros(features):β初始化为0,保持标准化后的均值
  • 这样初始状态下,层归一化相当于标准的z-score标准化

数值稳定性考虑:

std + self.eps  # 防止除零错误
  • 当标准差接近0时,加上小常数防止数值不稳定
  • eps=1e-6是经验值,平衡精度和稳定性

实际效果演示:

# 未归一化的数据可能范围很大
before = torch.tensor([[-100.0, 50.0, 200.0], [0.1, 0.2, 0.3]])

layer_norm = LayerNorm(3)
after = layer_norm(before)

# 归一化后每行的均值接近0,标准差接近1
print(after.mean(-1))  # 接近 [0, 0]
print(after.std(-1))   # 接近 [1, 1]

这种标准化确保了神经网络中每一层都能接收到数值稳定、分布一致的输入,是Transformer稳定训练的重要基础。

class Encoder(nn.Module):
    "Core encoder is a stack of N layers"

    def __init__(self, layer, N):
        super(Encoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
        "Pass the input (and mask) through each layer in turn."
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

编码器的作用: 编码器的任务是理解输入序列(比如一个德语句子),并将其转换为一系列向量表示,这些向量包含了句子的语义信息。

构造函数分析:

def __init__(self, layer, N):
    super(Encoder, self).__init__()
    self.layers = clones(layer, N)
    self.norm = LayerNorm(layer.size)
  • layer:单个编码器层的模板
  • N:编码器层的数量(论文中是 6)
  • self.layers = clones(layer, N):创建 N 个相同的编码器层
  • self.norm = LayerNorm(layer.size):最后的层归一化

forward 方法分析:

def forward(self, x, mask):
    "Pass the input (and mask) through each layer in turn."
    for layer in self.layers:
        x = layer(x, mask)
    return self.norm(x)

数据流动过程:

  1. 输入 x 是词汇的向量表示
  2. mask 告诉模型哪些位置是真实的词汇,哪些是填充
  3. 依次通过每个编码器层,每一层都会更新 x
  4. 最后应用层归一化,得到最终的编码表示

为什么要逐层处理:

  • 每一层都会从不同角度分析输入
  • 浅层关注局部特征(如词汇和短语)
  • 深层关注全局特征(如句子的整体含义)
  • 堆叠多层可以获得更丰富的表示

mask 的重要性: 在批处理时,不同的句子长度不同,短句子会用特殊符号填充。mask 告诉模型忽略这些填充位置,只关注真实的内容。


单元格 21-22:LayerNorm 类 - 层归一化

class LayerNorm(nn.Module):
    "Construct a layernorm module (See citation for details)."

    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        self.eps = eps

    def forward(self, x):
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

层归一化的作用: 层归一化是深度学习中的重要技术,它可以:

  1. 稳定训练过程
  2. 加快收敛速度
  3. 减少对初始化的依赖
  4. 提高模型性能

参数解释:

  • features:输入特征的维度
  • eps:很小的数值(1e-6),防止除零错误
  • self.a_2:可学习的缩放参数,初始化为 1
  • self.b_2:可学习的偏移参数,初始化为 0

nn.Parameter 的含义: nn.Parameter 将普通张量转换为模型参数,这意味着:

  • 优化器会自动更新这些参数
  • 模型保存时会包含这些参数
  • 可以通过 model.parameters() 访问

归一化过程:

mean = x.mean(-1, keepdim=True)  # 计算均值
std = x.std(-1, keepdim=True)    # 计算标准差
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
  1. 计算输入的均值和标准差
  2. 将输入标准化:(x - mean) / (std + eps)
  3. 应用可学习的缩放和偏移:a_2 * 标准化值 + b_2

为什么在最后一个维度归一化:

  • -1 表示最后一个维度
  • 对于形状为 [batch_size, sequence_length, features] 的张量
  • 我们在 features 维度上进行归一化
  • 这意味着每个位置的特征向量都被独立归一化

单元格 23-24:SublayerConnection 类 - 残差连接和层归一化

是什么: 深度网络的连接模式

class SublayerConnection(nn.Module):
    """
    A residual connection followed by a layer norm.
    Note for code simplicity the norm is first as opposed to last.
    """

    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        "Apply residual connection to any sublayer with the same size."
        return x + self.dropout(sublayer(self.norm(x)))

残差连接的核心概念:

  • 跳跃连接:直接将输入加到输出上,创建”高速公路”
  • 恒等映射:当子层学习为零函数时,输出等于输入
  • 梯度流动:为梯度提供直接的反向传播路径

为什么需要残差连接: 解决深度网络的根本问题

深度网络的退化问题:

  1. 梯度消失:在深层网络中,梯度在反向传播时逐层衰减
  2. 训练困难:单纯堆叠层数并不能保证性能提升
  3. 表示学习:网络难以学习恒等映射,即”什么都不做”

残差连接的革命性解决方案:

  1. 梯度高速公路:梯度可以直接从输出传播到输入
  2. 学习简化:网络只需学习”残差”(调整量)而非完整映射
  3. 深度可扩展:理论上可以训练非常深的网络

数学表达的深层含义:

传统网络:H(x) = F(x)
残差网络:H(x) = F(x) + x
等价于:  F(x) = H(x) - x  (学习残差)

为什么学习残差更容易:

  • 如果最优映射就是恒等映射,F(x)只需学习为0
  • 如果需要微调,F(x)只需学习小的调整量
  • 比从头学习完整映射H(x)要简单得多

怎么做: 技术实现和设计选择

前向传播的执行顺序:

return x + self.dropout(sublayer(self.norm(x)))

步骤分解:

  1. self.norm(x):输入先经过层归一化
  2. sublayer(...):标准化后的输入通过子层(如注意力层)
  3. self.dropout(...):对子层输出应用dropout正则化
  4. x + ...:原始输入与处理后输出相加

层归一化前置的设计选择:

# Pre-LN (本实现):先归一化再处理
output = x + dropout(sublayer(norm(x)))

# Post-LN (原论文):先处理再归一化
output = norm(x + dropout(sublayer(x)))

Pre-LN的优势:

  1. 训练稳定性:归一化的输入让子层更容易训练
  2. 梯度流动:更好的梯度传播特性
  3. 收敛速度:通常收敛更快,需要更少的warmup

Dropout正则化的作用机制:

self.dropout = nn.Dropout(dropout)

Dropout的工作原理:

  1. 训练时:随机将部分神经元输出设为0
  2. 推理时:使用所有神经元,但按比例缩放
  3. 正则化:防止过拟合,提高泛化能力

为什么对残差应用Dropout:

  • 噪声注入:为学习过程增加随机性
  • 鲁棒性:让模型不过度依赖特定的连接
  • 泛化能力:提高在新数据上的表现

子层接口的通用性:

def forward(self, x, sublayer):

设计哲学:

  • sublayer可以是任何接受相同输入输出维度的层
  • 注意力层、前馈网络等都可以作为子层
  • 实现了高度的模块化和可复用性

实际使用示例:

# 在编码器层中使用
class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 2)
        self.size = size

    def forward(self, x, mask):
        # 第一个残差连接:自注意力
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        # 第二个残差连接:前馈网络
        return self.sublayer[1](x, self.feed_forward)

lambda函数的巧妙使用:

lambda x: self.self_attn(x, x, x, mask)
  • 将多参数函数包装成单参数函数
  • 满足SublayerConnection的接口要求
  • 闭包捕获了mask变量

残差连接的数值效果:

# 假设输入和子层输出
x = torch.randn(2, 10, 512)           # 原始输入
sublayer_output = torch.randn(2, 10, 512)  # 子层输出

# 没有残差连接:只有子层输出
without_residual = sublayer_output

# 有残差连接:原始输入+子层输出
with_residual = x + sublayer_output

# 梯度流动:
# without_residual对x的梯度 = 0
# with_residual对x的梯度 = 1 + 子层梯度 (至少保证梯度为1)

这种设计确保了即使在非常深的网络中,梯度也能有效地传播到早期层,是Transformer能够训练深层网络的关键技术。


单元格 25-26:EncoderLayer 类 - 编码器的单个层

是什么: 编码器的基本构建块

class EncoderLayer(nn.Module):
    "Encoder is made up of self-attn and feed forward (defined below)"

    def __init__(self, size, self_attn, feed_forward, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 2)
        self.size = size

    def forward(self, x, mask):
        "Follow Figure 1 (left) for connections."
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        return self.sublayer[1](x, self.feed_forward)

编码器层的双重结构:

  • 自注意力机制:让序列中的每个位置关注其他位置
  • 前馈网络:对每个位置独立进行非线性变换
  • 残差连接:确保信息流动和梯度传播

为什么这样设计: Transformer的核心创新

自注意力的革命性意义:

  1. 并行计算:不像RNN需要顺序处理,可以并行处理所有位置
  2. 长距离依赖:直接建立任意两个位置之间的连接
  3. 动态权重:注意力权重根据输入内容动态调整
  4. 位置无关:不受固定窗口大小限制

前馈网络的补充作用:

  1. 非线性变换:注意力机制本身是线性的,需要非线性增强表达能力
  2. 位置独立处理:对每个位置进行相同的变换
  3. 特征混合:在更高维空间中混合特征
  4. 模式识别:学习复杂的语言模式

两阶段处理的协同效应:

  • 注意力阶段:关注什么(Where to look)
  • 前馈阶段:处理什么(What to do)

怎么做: 具体实现和技术细节

初始化中的组件管理:

def __init__(self, size, self_attn, feed_forward, dropout):
    self.self_attn = self_attn
    self.feed_forward = feed_forward
    self.sublayer = clones(SublayerConnection(size, dropout), 2)
    self.size = size

组件的职责分工:

  • self_attn:自注意力层,处理序列间的关系
  • feed_forward:前馈网络,进行位置级的特征变换
  • sublayer:两个残差连接层,包装上述两个组件
  • size:特征维度,确保所有组件的维度一致性

前向传播的两阶段处理:

def forward(self, x, mask):
    x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
    return self.sublayer[1](x, self.feed_forward)

第一阶段:自注意力处理

x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))

自注意力的三个输入:

  • 第一个 x:Query(查询),“我想要什么信息”
  • 第二个 x:Key(键),“我有什么信息”
  • 第三个 x:Value(值),“具体的信息内容”
  • mask:掩码,控制注意力的范围

为什么自注意力需要Q、K、V都是同一个输入:

  • 自注意力:序列与自身的注意力
  • 位置关系:每个位置都可以作为查询者和被查询者
  • 信息整合:允许每个位置收集来自所有位置的信息

第二阶段:前馈网络处理

return self.sublayer[1](x, self.feed_forward)

前馈网络的作用:

  • 接收注意力处理后的表示
  • 进行独立的非线性变换
  • 输出增强的特征表示

lambda函数的封装技巧:

lambda x: self.self_attn(x, x, x, mask)

为什么需要lambda封装:

  1. 接口适配:SublayerConnection期望单参数函数
  2. 参数绑定:将4参数的注意力函数转为1参数函数
  3. 闭包捕获:自动捕获mask变量
  4. 代码简洁:避免定义额外的辅助函数

数据流动的完整路径:

# 输入:[batch_size, seq_length, d_model]
input_x = x

# 第一个残差连接:self-attention
# 1. 层归一化
norm_x = layer_norm(input_x)
# 2. 自注意力
attn_output = self_attention(norm_x, norm_x, norm_x, mask)
# 3. dropout + 残差连接
x = input_x + dropout(attn_output)

# 第二个残差连接:feed-forward
# 1. 层归一化
norm_x = layer_norm(x)
# 2. 前馈网络
ff_output = feed_forward(norm_x)
# 3. dropout + 残差连接
output = x + dropout(ff_output)

# 输出:[batch_size, seq_length, d_model]

性能考虑:

  • 内存效率:残差连接避免了额外的内存分配
  • 计算并行:自注意力可以高度并行化
  • 梯度稳定:残差连接保证梯度流动

这种双阶段设计使得编码器层既能捕获序列内的关系(通过自注意力),又能进行深度的特征变换(通过前馈网络),是Transformer强大表示能力的核心。

    def forward(self, x, mask):
        "Follow Figure 1 (left) for connections."
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        return self.sublayer[1](x, self.feed_forward)

编码器层的组成: 每个编码器层包含两个主要组件:

  1. 自注意力机制 (self-attention)
  2. 前馈神经网络 (feed-forward network)

构造函数分析:

  • size:层的维度大小
  • self_attn:自注意力模块
  • feed_forward:前馈网络模块
  • dropout:dropout 概率
  • self.sublayer = clones(SublayerConnection(size, dropout), 2):创建两个残差连接

forward 方法详解:

第一个子层(自注意力):

x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
  • lambda x: self.self_attn(x, x, x, mask) 是一个匿名函数
  • 将相同的输入 x 作为 query、key、value 传给自注意力
  • 这就是”自”注意力的含义:序列关注自己

第二个子层(前馈网络):

return self.sublayer[1](x, self.feed_forward)
  • 将自注意力的输出传给前馈网络
  • 前馈网络对每个位置独立地进行非线性变换

lambda 函数解释:

lambda x: self.self_attn(x, x, x, mask)
  • lambda 是 Python 中创建匿名函数的关键字
  • 等价于:
def temp_function(x):
    return self.self_attn(x, x, x, mask)
  • 用 lambda 更简洁,适合简单的一行函数

数据流动过程:

输入 x 
→ 层归一化 
→ 自注意力 
→ dropout 
→ 残差连接 
→ 层归一化 
→ 前馈网络 
→ dropout 
→ 残差连接 
→ 输出

单元格 27-29:Decoder 相关类 - 解码器实现

是什么: 序列生成的核心引擎

class Decoder(nn.Module):
    "Generic N layer decoder with masking."

    def __init__(self, layer, N):
        super(Decoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, memory, src_mask, tgt_mask):
        for layer in self.layers:
            x = layer(x, memory, src_mask, tgt_mask)
        return self.norm(x)

解码器的根本使命:

  • 序列生成:逐步生成目标序列的每个位置
  • 条件生成:基于源序列(编码器输出)生成目标序列
  • 自回归特性:每个位置的生成依赖于之前已生成的位置

为什么解码器更复杂: 生成任务的挑战

编码器 vs 解码器的根本差异:

  1. 编码器:理解任务,可以看到完整输入,并行处理
  2. 解码器:生成任务,只能看到已生成部分,顺序依赖

解码器面临的三重挑战:

  1. 自回归约束:不能”偷看”未来的词汇
  2. 条件依赖:必须基于源序列信息生成
  3. 长序列生成:保持生成过程的一致性和连贯性

为什么需要memory参数:

def forward(self, x, memory, src_mask, tgt_mask):
  • memory:编码器的输出,包含源序列的完整语义信息
  • 解码器必须”记住”源序列的内容才能正确翻译

怎么做: 解码器的具体实现机制

多层堆叠的生成策略:

for layer in self.layers:
    x = layer(x, memory, src_mask, tgt_mask)

逐层抽象的生成过程:

  • 浅层:局部语法结构生成
  • 中层:短语级别的语义转换
  • 深层:句子级别的语义一致性

四种掩码的协同作用:

  • src_mask:隐藏源序列的填充部分
  • tgt_mask:防止解码器看到未来位置
  • 确保生成过程的正确性和有效性

是什么: 解码器的基本构建单元

class DecoderLayer(nn.Module):
    "Decoder is made of self-attn, src-attn, and feed forward (defined below)"

    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
        super(DecoderLayer, self).__init__()
        self.size = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 3)

    def forward(self, x, memory, src_mask, tgt_mask):
        "Follow Figure 1 (right) for connections."
        m = memory
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
        x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
        return self.sublayer[2](x, self.feed_forward)

解码器层的三阶段处理架构:

  • 阶段1:自注意力 - 整合已生成的目标序列信息
  • 阶段2:交叉注意力 - 结合源序列的语义信息
  • 阶段3:前馈网络 - 进行最终的特征变换

为什么需要三个组件: 生成任务的复杂性

自注意力的必要性:

x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))

自注意力解决的问题:

  1. 内部一致性:确保已生成部分的语法和语义一致
  2. 长距离依赖:让当前位置关注远程的已生成内容
  3. 上下文整合:整合目标序列的局部上下文

交叉注意力的关键作用:

x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))

Query-Key-Value的分工:

  • Query (x):当前目标位置的”问题” - “我需要什么信息?”
  • Key (m):源序列的”索引” - “我有什么信息?”
  • Value (m):源序列的”内容” - “具体信息是什么?”

交叉注意力的核心作用:

  1. 信息检索:从源序列中检索相关信息
  2. 对齐学习:学习源语言和目标语言的对应关系
  3. 条件生成:基于源序列内容生成合适的目标词汇

前馈网络的补强作用:

return self.sublayer[2](x, self.feed_forward)
  • 在整合了目标和源序列信息后,进行最终的非线性变换
  • 将复杂的语义表示转换为可用于下一层的格式

怎么做: 三阶段协同的技术实现

注意力机制参数模式的深层含义:

自注意力模式:

self.self_attn(x, x, x, tgt_mask)
# Q=目标序列, K=目标序列, V=目标序列
# 目标序列关注自身,但不能看到未来

交叉注意力模式:

self.src_attn(x, m, m, src_mask)
# Q=目标序列, K=源序列, V=源序列
# 目标序列查询源序列信息

残差连接的三重保护:

self.sublayer = clones(SublayerConnection(size, dropout), 3)
  • 每个阶段都有独立的残差连接
  • 确保梯度能够顺利传播到所有层
  • 防止信息在多阶段处理中丢失

lambda函数的参数绑定技巧:

lambda x: self.src_attn(x, m, m, src_mask)
  • 将4参数函数适配为SublayerConnection要求的1参数接口
  • msrc_mask通过闭包机制被捕获
  • 体现了函数式编程的优雅

数据流的完整路径:

# 输入:部分生成的目标序列 + 完整源序列编码
target_partial = x      # [batch, target_len, d_model]
source_memory = memory  # [batch, source_len, d_model]

# 阶段1:目标序列自注意力
self_attended = self_attention(target_partial, target_partial, target_partial, tgt_mask)
x = x + dropout(layernorm(self_attended))

# 阶段2:交叉注意力(目标查询源)
cross_attended = cross_attention(x, source_memory, source_memory, src_mask)
x = x + dropout(layernorm(cross_attended))

# 阶段3:前馈网络
ff_output = feed_forward(x)
output = x + dropout(layernorm(ff_output))

性能优化的设计考虑:

  • 并行化:自注意力和交叉注意力都可以并行计算
  • 缓存重用:推理时可以缓存已计算的key和value
  • 内存效率:残差连接避免额外的内存分配

这种三阶段设计使得解码器既能维护生成序列的内部一致性,又能有效利用源序列信息,还能进行复杂的语义变换,是序列到序列生成任务的核心架构。


单元格 30-32:subsequent_mask 函数 - 防止看到未来

是什么: 自回归生成的守护机制

def subsequent_mask(size):
    "Mask out subsequent positions."
    attn_shape = (1, size, size)
    subsequent_mask = torch.triu(torch.ones(attn_shape), diagonal=1).type(
        torch.uint8
    )
    return subsequent_mask == 0

掩码的数学本质:

  • 上三角矩阵:只保留下三角和对角线的信息
  • 因果关系:确保时间步t只能看到时间步≤t的信息
  • 自回归约束:维护生成过程的单向性

为什么需要这个掩码: 自回归生成的根本要求

自回归模型的基本原理:

  • 在生成序列时,每个位置只能依赖于前面已经生成的位置
  • 这模拟了真实的生成过程:写文章时,下一句依赖于前面已写的内容

“作弊”问题的严重性:

  1. 训练与推理不一致:训练时如果能看到未来,推理时却不能
  2. 模型能力虚高:模型可能学会”抄答案”而非真正理解
  3. 泛化能力差:在真实场景中性能会大幅下降

因果掩码的哲学意义:

  • 时间的不可逆性:模拟现实世界中时间的单向流动
  • 信息的渐进性:知识和理解是逐步积累的过程
  • 决策的顺序性:每个决策基于当前可用的信息

怎么做: 掩码的技术实现细节

逐步构建掩码的过程:

def subsequent_mask(size):
    attn_shape = (1, size, size)          # 步骤1:定义形状
    subsequent_mask = torch.triu(          # 步骤2:创建上三角
        torch.ones(attn_shape), 
        diagonal=1
    ).type(torch.uint8)                   # 步骤3:类型转换
    return subsequent_mask == 0           # 步骤4:逻辑反转

关键函数详解:

torch.triu的工作原理:

torch.triu(torch.ones(3, 3), diagonal=1)
# 输出:
# [[0, 1, 1],
#  [0, 0, 1], 
#  [0, 0, 0]]
  • triu:“triangular upper”的缩写
  • diagonal=1:从主对角线上方一位开始
  • 保留上三角部分,其余置为0

逻辑反转的必要性:

subsequent_mask == 0
  • PyTorch的注意力机制中:True表示可以关注,False表示屏蔽
  • 我们要屏蔽上三角(未来位置),所以需要反转逻辑

形状设计的考虑:

attn_shape = (1, size, size)  # (batch_dim, seq_len, seq_len)
  • 第一维为1,便于广播到任意batch_size
  • 后两维构成注意力矩阵的形状

实际掩码效果演示:

# 对于长度为4的序列
mask = subsequent_mask(4)
print(mask.squeeze())
# 输出:
# [[True,  False, False, False],
#  [True,  True,  False, False],
#  [True,  True,  True,  False],
#  [True,  True,  True,  True ]]

掩码的语义解释:

  • 位置0:只能看到自己(冷启动状态)
  • 位置1:可以看到位置0和1(基于前一个词生成)
  • 位置2:可以看到位置0,1,2(基于前两个词生成)
  • 位置3:可以看到所有位置(基于完整上下文生成)

在注意力计算中的应用:

# 注意力分数计算
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)

# 应用掩码
scores.masked_fill_(mask == 0, -1e9)  # 将False位置设为很大的负数

# Softmax后,-1e9变为接近0的概率
attention_weights = torch.softmax(scores, dim=-1)

数值技巧的重要性:

  • 使用-1e9而不是-inf:避免数值不稳定
  • 经过softmax后,大负数变为接近0的正数
  • 实现了”软掩码”而非”硬截断”

掩码与训练效率:

# Teacher Forcing + 掩码的组合
# 训练时:已知完整目标序列,但用掩码防止看到未来
target_input = target_sequence[:, :-1]  # 去掉最后一个
target_output = target_sequence[:, 1:]  # 去掉第一个

# 一次性计算所有位置,但每个位置只能看到合法的历史
decoder_output = decoder(target_input, encoder_memory, src_mask, tgt_mask)

推理时的逐步生成:

# 推理时的自回归生成
generated = [START_TOKEN]
for step in range(max_length):
    # 当前掩码只需要考虑已生成的部分
    current_mask = subsequent_mask(len(generated))
    
    # 生成下一个token
    output = decoder(generated, encoder_memory, src_mask, current_mask)
    next_token = output[:, -1, :].argmax(dim=-1)
    
    generated.append(next_token)
    if next_token == END_TOKEN:
        break

这种掩码机制确保了模型在训练时学会正确的依赖关系,在推理时能够逐步生成合理的序列,是自回归语言模型的核心技术基础。


单元格 33-34:example_mask 函数 - 可视化掩码

这个函数创建了一个可视化图表,展示掩码的效果。它使用 Altair 库创建热力图,帮助理解哪些位置可以相互关注。


单元格 35-37:注意力机制 - Transformer 的核心

是什么: 序列建模的革命性机制

def attention(query, key, value, mask=None, dropout=None):
    "Compute 'Scaled Dot Product Attention'"
    d_k = query.size(-1)
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    p_attn = scores.softmax(dim=-1)
    if dropout is not None:
        p_attn = dropout(p_attn)
    return torch.matmul(p_attn, value), p_attn

注意力机制的本质:

  • 信息检索系统:从大量信息中找到与当前任务最相关的部分
  • 动态权重分配:根据内容自适应地分配注意力权重
  • 关系建模工具:建立序列中任意两个位置之间的联系

为什么注意力机制如此重要: 解决序列建模的根本问题

传统RNN的局限性:

  1. 顺序依赖:必须逐步处理,无法并行计算
  2. 长距离问题:远距离信息容易丢失(梯度消失)
  3. 固定容量:隐藏状态容量有限,长序列信息压缩困难
  4. 位置偏见:更多关注近期信息,忽略远程依赖

注意力机制的革命性解决:

  1. 直接连接:任意两个位置可以直接交互,路径长度为1
  2. 并行计算:所有位置可以同时计算注意力
  3. 动态容量:根据需要分配注意力,没有固定瓶颈
  4. 全局视野:平等对待所有位置,无位置偏见

Query-Key-Value的哲学意义:

  • Query:“我想要什么?” - 表达信息需求
  • Key:“我是什么?” - 表达信息标识
  • Value:“我有什么?” - 表达信息内容

怎么做: 注意力的计算细节和技术实现

第一步:相似度计算

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

点积注意力的数学原理:

Attention(Q,K,V) = softmax(QK^T/√d_k)V

为什么使用点积:

  1. 计算效率:矩阵乘法高度优化,GPU友好
  2. 语义直觉:点积衡量向量相似度的自然选择
  3. 可微分性:完全可微,支持端到端训练
  4. 缩放性:通过批次矩阵操作实现高效并行

缩放因子的深层作用:

/ math.sqrt(d_k)

为什么需要缩放:

  1. 数值稳定性:防止点积过大导致softmax饱和
  2. 梯度保持:保持梯度在合理范围,避免梯度消失
  3. 维度无关:使注意力机制对特征维度不敏感
  4. 理论基础:保持点积的方差为1(假设输入是标准正态分布)

数学推导:

假设 q_i, k_j 是独立的标准正态分布
E[q_i * k_j] = 0
Var(q_i * k_j) = 1
对于 d_k 维向量:Var(q * k) = d_k
因此需要除以 sqrt(d_k) 来保持方差为1

第二步:掩码应用

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

掩码的精确机制:

  • mask == 0:找到需要屏蔽的位置
  • -1e9:使用大负数而非负无穷,避免数值问题
  • masked_fill:就地替换,内存效率高

为什么使用-1e9:

  • softmax(-1e9) ≈ 0,但仍是有限数值
  • 避免NaN和Inf的传播
  • 保持数值计算的稳定性

第三步:概率归一化

p_attn = scores.softmax(dim=-1)

Softmax的核心作用:

  1. 概率解释:将任意实数转为概率分布
  2. 竞争机制:突出重要信息,抑制不重要信息
  3. 可微分性:平滑函数,梯度传播友好
  4. 归一化:确保所有注意力权重和为1

第四步:正则化

if dropout is not None:
    p_attn = dropout(p_attn)

注意力Dropout的特殊意义:

  • 连接稀疏化:随机断开一些注意力连接
  • 鲁棒性增强:防止过度依赖特定位置
  • 泛化能力:提高模型的泛化性能

第五步:信息聚合

return torch.matmul(p_attn, value), p_attn

加权聚合的数学含义:

output_i = Σ(attention_weights_ij * value_j)
  • 每个输出位置是所有Value的加权平均
  • 权重由注意力分数决定
  • 实现了内容基础的信息选择

形状变换的追踪:

# 假设:batch_size=32, seq_len=20, d_model=512
query.shape    # [32, 20, 512]
key.shape      # [32, 20, 512]  
value.shape    # [32, 20, 512]

# 计算注意力分数
scores.shape   # [32, 20, 20]  # (batch, seq_len, seq_len)

# 注意力权重
p_attn.shape   # [32, 20, 20]  # 每行是一个概率分布

# 最终输出
output.shape   # [32, 20, 512] # 与输入相同形状

注意力矩阵的解释:

  • p_attn[b, i, j]:在批次b中,位置i对位置j的注意力权重
  • 每行代表一个query对所有key的注意力分布
  • 每列代表所有query对某个key的关注程度

实际应用示例:

# 机器翻译场景
# 德语:"Das ist ein Buch"
# 英语:"This is a book"

# 生成"book"时的注意力权重可能是:
# Das: 0.1, ist: 0.1, ein: 0.2, Buch: 0.6
# 模型学会了"Buch"对应"book"

这种设计让模型能够自动发现重要信息建立长距离依赖实现并行计算,是Transformer架构成功的核心基础。


单元格 38-40:多头注意力机制 - 让模型从多个角度看问题

是什么: 并行的多视角信息处理系统

class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
        "Take in model size and number of heads."
        super(MultiHeadedAttention, self).__init__()
        assert d_model % h == 0
        # We assume d_v always equals d_k
        self.d_k = d_model // h
        self.h = h
        self.linears = clones(nn.Linear(d_model, d_model), 4)
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
        "Implements Figure 2"
        if mask is not None:
            # Same mask applied to all h heads.
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)

        # 1) Do all the linear projections in batch from d_model => h x d_k
        query, key, value = [
            lin(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
            for lin, x in zip(self.linears, (query, key, value))
        ]

        # 2) Apply attention on all the projected vectors in batch.
        x, self.attn = attention(
            query, key, value, mask=mask, dropout=self.dropout
        )

        # 3) "Concat" using a view and apply a final linear.
        x = (
            x.transpose(1, 2)
            .contiguous()
            .view(nbatches, -1, self.h * self.d_k)
        )
        del query
        del key
        del value
        return self.linears[-1](x)

多头注意力的核心理念:

  • 认知多样性:不同的”注意力头”关注不同的语言现象
  • 并行处理:多个头同时工作,提高计算效率
  • 表示丰富性:组合多个视角得到更全面的理解

为什么需要多头: 单头注意力的局限性

单头注意力的问题:

  1. 视角单一:只能从一个角度理解信息
  2. 容量限制:单一表示空间可能无法捕获所有模式
  3. 注意力冲突:不同类型的信息竞争同一个注意力通道
  4. 表达瓶颈:复杂的语言现象需要多维度表示

多头的优势解析:

  1. 专业化分工

    • 头1:关注句法结构(主谓宾关系)
    • 头2:关注语义相似性
    • 头3:关注词汇对应关系
    • 头4:关注上下文信息
  2. 并行效率

    • 所有头同时计算,没有顺序依赖
    • 充分利用现代GPU的并行计算能力
  3. 鲁棒性增强

    • 即使部分头失效,其他头仍能维持功能
    • 不同头之间形成互补关系

怎么做: 多头机制的精密实现

初始化的设计思考:

def __init__(self, h, d_model, dropout=0.1):
    assert d_model % h == 0
    self.d_k = d_model // h
    self.h = h
    self.linears = clones(nn.Linear(d_model, d_model), 4)

维度分配的数学逻辑:

  • 约束条件d_model % h == 0 确保均匀分割
  • 单头维度d_k = d_model // h 每头处理的维度
  • 总维度保持h * d_k = d_model 确保信息不丢失

线性层的四重作用:

self.linears = clones(nn.Linear(d_model, d_model), 4)
  1. W_Q:Query投影矩阵
  2. W_K:Key投影矩阵
  3. W_V:Value投影矩阵
  4. W_O:输出投影矩阵

前向传播的三阶段处理:

阶段1:并行投影和重塑

query, key, value = [
    lin(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
    for lin, x in zip(self.linears, (query, key, value))
]

张量变换的详细追踪:

# 输入形状
input_shape = [batch_size, seq_len, d_model]  # [32, 20, 512]

# 线性投影
projected = linear(input)  # [32, 20, 512] → [32, 20, 512]

# 重塑为多头
reshaped = projected.view(32, 20, 8, 64)  # [batch, seq, heads, head_dim]

# 转置以便并行处理
transposed = reshaped.transpose(1, 2)  # [32, 8, 20, 64] [batch, heads, seq, head_dim]

为什么这样重塑:

  1. 并行计算:将不同头放在不同维度,便于并行处理
  2. 内存布局:优化内存访问模式,提高计算效率
  3. 批次处理:所有头可以用一次矩阵乘法完成

阶段2:并行注意力计算

x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)

并行处理的效率分析:

  • 输入:每个头获得独立的Q、K、V投影
  • 计算:8个头的注意力同时计算
  • 输出:每个头产生独立的表示

阶段3:头融合和输出投影

x = (
    x.transpose(1, 2)
    .contiguous()
    .view(nbatches, -1, self.h * self.d_k)
)
return self.linears[-1](x)

融合过程的技术细节:

形状恢复:

x.transpose(1, 2)  # [32, 8, 20, 64] → [32, 20, 8, 64]

内存连续性:

.contiguous()
  • PyTorch中transpose可能导致内存不连续
  • contiguous()确保内存布局适合后续操作
  • 这是view()操作的前提条件

拼接操作:

.view(nbatches, -1, self.h * self.d_k)  # [32, 20, 8, 64] → [32, 20, 512]
  • 将多个头的输出拼接成一个向量
  • 相当于concat操作,但更高效

最终投影:

self.linears[-1](x)  # 通过W_O矩阵进行最终变换

掩码处理的技巧:

if mask is not None:
    mask = mask.unsqueeze(1)  # 为头维度添加广播维度
  • 原始掩码形状:[batch, seq_len, seq_len]
  • 扩展后形状:[batch, 1, seq_len, seq_len]
  • 广播到所有头:[batch, heads, seq_len, seq_len]

内存优化的考虑:

del query
del key  
del value
  • 显式删除中间变量释放GPU内存
  • 在大模型训练中,内存管理至关重要

多头注意力的数学公式:

MultiHead(Q,K,V) = Concat(head_1, ..., head_h)W^O
where head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)

实际效果分析:

# 单头注意力容量:512维表示空间
# 多头注意力容量:8个64维子空间 = 8倍的表示多样性

# 每个头专注不同方面:
# head_1: 句法依赖关系
# head_2: 语义相似性  
# head_3: 共指消解
# head_4: 词汇对应
# ...

计算复杂度分析:

  • 时间复杂度:O(n²d) (n=序列长度,d=模型维度)
  • 空间复杂度:O(n²h) (h=头数)
  • 并行度:头数量和序列长度都可以并行

这种设计让Transformer能够同时处理多种语言现象高效利用计算资源提供丰富的表示能力,是其强大性能的关键技术基础。

class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
        "Take in model size and number of heads."
        super(MultiHeadedAttention, self).__init__()
        assert d_model % h == 0
        # We assume d_v always equals d_k
        self.d_k = d_model // h
        self.h = h
        self.linears = clones(nn.Linear(d_model, d_model), 4)
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
        "Implements Figure 2"
        if mask is not None:
            # Same mask applied to all h heads.
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)

        # 1) Do all the linear projections in batch from d_model => h x d_k
        query, key, value = [
            lin(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
            for lin, x in zip(self.linears, (query, key, value))
        ]

        # 2) Apply attention on all the projected vectors in batch.
        x, self.attn = attention(
            query, key, value, mask=mask, dropout=self.dropout
        )

        # 3) "Concat" using a view and apply a final linear.
        x = (
            x.transpose(1, 2)
            .contiguous()
            .view(nbatches, -1, self.h * self.d_k)
        )
        del query
        del key
        del value
        return self.linears[-1](x)

多头注意力的核心思想: 多头注意力就像是让模型从多个不同的角度来观察同一件事情。想象一下:

  • 如果你只用一只眼睛看东西,你只能得到一个视角
  • 用两只眼睛看,你可以感知深度
  • 多头注意力让模型用”多只眼睛”来理解语言

构造函数详解:

def __init__(self, h, d_model, dropout=0.1):
    super(MultiHeadedAttention, self).__init__()
    assert d_model % h == 0
    self.d_k = d_model // h
    self.h = h
    self.linears = clones(nn.Linear(d_model, d_model), 4)
    self.attn = None
    self.dropout = nn.Dropout(p=dropout)

参数解释:

  • h:注意力头的数量(论文中是 8)
  • d_model:模型的维度(论文中是 512)
  • dropout:dropout 概率

assert 语句:

assert d_model % h == 0
  • assert 是 Python 的断言语句,用于检查条件是否为真
  • 这里检查 d_model 是否能被 h 整除
  • 如果不能整除,程序会报错并停止
  • 这确保每个头都有相同的维度

关键计算:

self.d_k = d_model // h
  • // 是整数除法运算符
  • 如果 d_model=512, h=8,那么 d_k=64
  • 这意味着每个注意力头处理 64 维的特征

线性变换层:

self.linears = clones(nn.Linear(d_model, d_model), 4)
  • 创建 4 个线性变换层
  • 前 3 个用于 Query、Key、Value 的变换
  • 第 4 个用于最后的输出变换

forward 方法详解:

步骤 1:处理掩码

if mask is not None:
    mask = mask.unsqueeze(1)
  • unsqueeze(1) 在第 1 个维度插入一个新维度
  • 这是为了让掩码适配多头的结构

步骤 2:线性变换和重塑

query, key, value = [
    lin(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
    for lin, x in zip(self.linears, (query, key, value))
]

这是一个复杂的列表推导式,让我们分解它:

  1. zip(self.linears, (query, key, value))

    • 将线性层和输入配对
    • 第一个线性层配对 query
    • 第二个线性层配对 key
    • 第三个线性层配对 value
  2. lin(x)

    • 对输入应用线性变换
    • 输入形状:[batch_size, seq_len, d_model]
    • 输出形状:[batch_size, seq_len, d_model]
  3. .view(nbatches, -1, self.h, self.d_k)

    • 重塑张量形状
    • -1 表示自动计算这个维度的大小
    • 新形状:[batch_size, seq_len, h, d_k]
    • 这将 d_model 维度分割成 hd_k 维的头
  4. .transpose(1, 2)

    • 交换第 1 和第 2 个维度
    • 最终形状:[batch_size, h, seq_len, d_k]
    • 这样每个头就可以独立处理了

view() 和 transpose() 详解:

view() 方法:

  • 用于改变张量的形状,但不改变数据
  • 类似于 numpy 的 reshape()
  • 新形状的元素总数必须与原形状相同

transpose() 方法:

  • 交换张量的两个维度
  • .transpose(1, 2) 交换第 1 和第 2 个维度

为什么需要这些操作: 原始输入是 [batch_size, seq_len, d_model],我们需要将其转换为 [batch_size, h, seq_len, d_k],这样就可以对每个头独立地计算注意力。

步骤 3:计算注意力

x, self.attn = attention(
    query, key, value, mask=mask, dropout=self.dropout
)
  • 使用之前定义的 attention 函数
  • 对所有头同时计算注意力
  • 由于张量形状的设计,每个头都会独立计算

步骤 4:合并头并输出

x = (
    x.transpose(1, 2)
    .contiguous()
    .view(nbatches, -1, self.h * self.d_k)
)
del query
del key
del value
return self.linears[-1](x)

合并过程:

  1. .transpose(1, 2):将形状从 [batch, h, seq_len, d_k] 变为 [batch, seq_len, h, d_k]
  2. .contiguous():确保内存布局是连续的,这对 view() 操作是必需的
  3. .view(nbatches, -1, self.h * self.d_k):将形状变为 [batch, seq_len, d_model]
  4. self.linears[-1](x):通过最后一个线性层进行最终变换

内存管理:

del query
del key
del value
  • 显式删除不再需要的变量
  • 释放 GPU 内存,在处理大型模型时很重要

多头注意力的优势:

  1. 多样性:每个头可以关注不同类型的模式
  2. 并行性:所有头可以同时计算
  3. 表达能力:多个头的组合比单个头更强大
  4. 稳定性:即使某些头失效,其他头仍能工作

实际例子: 在翻译任务中,不同的头可能关注:

  • 头1:语法结构
  • 头2:语义关系
  • 头3:词汇对应
  • 头4:上下文信息

单元格 41:前馈神经网络 - 非线性变换

是什么: 位置级的深度特征变换器

class PositionwiseFeedForward(nn.Module):
    "Implements FFN equation."

    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        self.w_1 = nn.Linear(d_model, d_ff)
        self.w_2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        return self.w_2(self.dropout(self.w_1(x).relu()))

前馈网络的本质定位:

  • 非线性引入器:为线性的注意力机制添加非线性变换能力
  • 特征混合器:在高维空间中重新组合和变换特征
  • 位置处理器:对每个序列位置独立进行深度变换

为什么需要前馈网络: 弥补注意力机制的不足

注意力机制的局限性:

  1. 线性约束:注意力本质上是加权平均,缺乏非线性变换
  2. 交互单一:主要处理位置间关系,缺乏位置内的深度处理
  3. 表达瓶颈:单纯的注意力无法学习复杂的函数映射
  4. 模式识别:难以捕获需要非线性组合的复杂语言模式

前馈网络的补强作用:

  1. 非线性注入:ReLU激活函数提供强大的非线性建模能力
  2. 容量扩张:中间层维度扩大4倍,提供充足的表示空间
  3. 独立处理:每个位置获得深度的、个性化的特征变换
  4. 模式学习:能够学习复杂的语言模式和规则

与注意力的互补关系:

  • 注意力:负责”关注什么”(What to attend)
  • 前馈网络:负责”如何处理”(How to process)

怎么做: 前馈网络的精妙设计

网络架构的数学表达:

FFN(x) = max(0, xW₁ + b₁)W₂ + b₂

两层结构的设计逻辑:

self.w_1 = nn.Linear(d_model, d_ff)    # 512 → 2048 (扩张)
self.w_2 = nn.Linear(d_ff, d_model)    # 2048 → 512 (压缩)

维度变化的战略意图:

  1. 扩张阶段d_model → d_ff (512 → 2048)

    • 提供4倍的表示容量
    • 允许学习更复杂的特征组合
    • 为非线性变换提供充足空间
  2. 压缩阶段d_ff → d_model (2048 → 512)

    • 将丰富信息压缩回原始维度
    • 强制网络学习最重要的特征
    • 保持与其他组件的维度兼容性

前向传播的四阶段处理:

def forward(self, x):
    return self.w_2(self.dropout(self.w_1(x).relu()))

阶段1:线性扩张

expanded = self.w_1(x)  # [batch, seq_len, 512] → [batch, seq_len, 2048]
  • 通过权重矩阵W₁进行线性变换
  • 将每个位置的特征向量扩展到高维空间

阶段2:非线性激活

activated = expanded.relu()  # 应用ReLU激活函数

ReLU函数的关键作用:

ReLU(x) = max(0, x) = {x if x > 0; 0 if x ≤ 0}
  • 稀疏性:约50%的神经元被激活,产生稀疏表示
  • 非饱和:正值区域梯度为1,避免梯度消失
  • 计算效率:实现简单,计算速度快
  • 生物启发:模拟神经元的激活模式

阶段3:正则化

regularized = self.dropout(activated)
  • 训练时随机将部分神经元置零
  • 防止过拟合,提高泛化能力
  • 增强模型鲁棒性

阶段4:线性压缩

output = self.w_2(regularized)  # [batch, seq_len, 2048] → [batch, seq_len, 512]
  • 将高维表示压缩回原始维度
  • 提取最重要的特征信息

位置独立性的重要含义:

# 前馈网络对每个位置独立处理
for i in range(seq_len):
    output[i] = FFN(input[i])  # 位置i的处理不依赖其他位置

为什么设计为位置独立:

  1. 并行效率:所有位置可以同时处理
  2. 职责分离:注意力负责位置交互,FFN负责位置内变换
  3. 计算优化:可以高效地批量处理
  4. 模块化:保持架构的清晰和可理解性

容量分析:

# 参数量计算
w1_params = d_model * d_ff = 512 * 2048 = 1,048,576
w2_params = d_ff * d_model = 2048 * 512 = 1,048,576
total_params = 2,097,152  # 约200万参数

# 这占了Transformer单层参数的很大比例

为什么选择4倍扩张:

  • 经验最优:实验证明4倍是性能和计算的最佳平衡
  • 足够容量:提供充分的非线性变换能力
  • 计算可行:在现有硬件上可以高效训练

与传统MLP的区别:

  • 应用方式:每个位置独立应用,而非整个序列
  • 维度设计:扩张-压缩模式,而非单纯增加层数
  • 集成方式:与注意力机制紧密结合,形成完整架构

这种设计使得Transformer在保持注意力机制优势的同时,获得强大的非线性建模能力,是架构成功的关键组成部分。


单元格 42-43:嵌入层 - 将词汇转换为向量

是什么: 离散符号到连续向量的桥梁

class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        super(Embeddings, self).__init__()
        self.lut = nn.Embedding(vocab, d_model)
        self.d_model = d_model

    def forward(self, x):
        return self.lut(x) * math.sqrt(self.d_model)

嵌入层的根本使命:

  • 符号化解器:将离散的词汇ID转换为连续的向量表示
  • 语义编码器:学习词汇的分布式语义表示
  • 维度统一器:为后续处理提供统一的向量维度

为什么需要嵌入层: 计算机理解语言的基础

离散vs连续的根本差异:

  1. 计算机的限制:只能处理数字,无法直接理解文字符号
  2. 语义距离:离散符号无法表达语义相似性
  3. 梯度传播:离散表示无法进行梯度优化
  4. 维度一致:神经网络需要固定维度的输入

词汇嵌入的革命性意义:

  1. 语义空间:在高维空间中建立词汇的语义关系
  2. 相似性度量:语义相近的词在向量空间中距离更近
  3. 可学习性:嵌入向量可以通过训练不断优化
  4. 泛化能力:学到的语义表示可以迁移到新任务

怎么做: 嵌入机制的技术实现

nn.Embedding的工作原理:

self.lut = nn.Embedding(vocab, d_model)

查找表(Look-Up Table)的本质:

# 本质上是一个大的权重矩阵
embedding_matrix.shape = [vocab_size, d_model]  # [30000, 512]

# 对于输入的词汇ID
word_id = 1234
word_vector = embedding_matrix[word_id]  # 取出对应行作为词向量

为什么叫”查找表”:

  • 给定词汇ID,直接查找对应的向量
  • 比传统的one-hot编码+矩阵乘法更高效
  • 避免了稀疏矩阵的计算开销

缩放因子的深层含义:

def forward(self, x):
    return self.lut(x) * math.sqrt(self.d_model)

为什么乘以√d_model:

  1. 数值平衡:与位置编码的量级保持一致
  2. 理论基础:保持方差的数学期望
  3. 训练稳定:避免不同组件贡献的不平衡
  4. 论文一致:与原始Transformer论文保持一致

数学推导:

假设嵌入向量的每个维度都是独立的标准正态分布 N(0,1)
则 d_model 维向量的L2范数期望为 √d_model
通过乘以 √d_model,使嵌入向量与位置编码具有相似的量级

嵌入层的学习过程:

初始化阶段:

# 随机初始化
embedding_matrix = torch.randn(vocab_size, d_model) * 0.1

训练过程:

# 前向传播:根据词汇ID获取向量
word_vectors = embedding(input_ids)

# 反向传播:根据损失更新对应的嵌入向量
loss.backward()  # 只有被使用的词汇嵌入会被更新

共享权重的考虑: 在某些实现中,输入嵌入和输出投影共享权重:

# 共享权重可以减少参数量
generator.proj.weight = embeddings.lut.weight.transpose()

嵌入维度的选择:

  • d_model=512:在表达能力和计算效率间的平衡
  • 更大维度:更强的表达能力,但计算成本更高
  • 更小维度:计算效率高,但可能损失表达能力

词汇表大小的影响:

# 参数量计算
embedding_params = vocab_size * d_model
# 例如:30000 * 512 = 15,360,000 参数

# 这通常是模型参数的很大一部分

实际应用中的技巧:

  1. 预训练嵌入:使用Word2Vec、GloVe等预训练向量初始化
  2. 子词嵌入:使用BPE、SentencePiece等处理未登录词
  3. 层次嵌入:对于超大词汇表,使用层次化的嵌入策略

嵌入质量的评估:

# 语义相似性测试
similar_words = find_similar(embedding_matrix, "king")
# 期望结果:["queen", "prince", "royal", ...]

# 类比关系测试  
# king - man + woman ≈ queen

这种设计将离散的语言符号转换为连续的数值表示,为神经网络处理自然语言奠定了基础,是连接符号智能和连接主义的关键桥梁。

嵌入层的作用: 嵌入层是深度学习处理离散符号(如单词)的关键技术:

  1. 符号到向量:将单词 ID 转换为稠密向量
  2. 语义表示:相似的单词在向量空间中距离较近
  3. 可训练:嵌入向量在训练过程中会不断优化

nn.Embedding 详解:

self.lut = nn.Embedding(vocab, d_model)
  • lut 代表 “Look-Up Table”(查找表)
  • vocab:词汇表大小(比如 30000 个单词)
  • d_model:每个单词的向量维度(512)
  • 本质上是一个大小为 [vocab, d_model] 的矩阵

嵌入过程:

  1. 输入:单词的 ID(整数),比如 [1, 5, 3, 8]
  2. 查找:从嵌入矩阵中取出对应行
  3. 输出:每个 ID 对应一个 d_model 维的向量

缩放因子:

return self.lut(x) * math.sqrt(self.d_model)

为什么要乘以 sqrt(d_model):

  1. 数值稳定性:确保嵌入向量的数值范围合理
  2. 与位置编码匹配:位置编码的数值范围也是这个量级
  3. 论文建议:原始 Transformer 论文中的做法

嵌入学习的直觉:

  • 初始时:嵌入向量是随机的
  • 训练过程中:相似含义的词会逐渐靠近
  • 最终:语义相近的词在向量空间中聚集

单元格 44-46:位置编码 - 为序列注入位置信息

是什么: 序列顺序信息的编码器

class PositionalEncoding(nn.Module):
    "Implement the PE function."

    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        # Compute the positional encodings once in log space.
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(
            torch.arange(0, d_model, 2) * -(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)].requires_grad_(False)
        return self.dropout(x)

位置编码的核心使命:

  • 位置感知器:为无序的注意力机制注入序列顺序信息
  • 几何编码器:使用数学函数将位置信息映射到向量空间
  • 永恒记忆:提供不依赖参数学习的固定位置表示

为什么需要位置编码: 解决注意力机制的位置盲区

Transformer架构的根本问题:

  1. 置换不变性:注意力机制对输入顺序不敏感
  2. 位置无关:相同的词在不同位置得到相同的处理
  3. 语义缺失:丢失了语言中至关重要的位置信息
  4. 结构失效:无法理解句法结构和语序规则

举例说明位置的重要性:

"猫咬了狗" vs "狗咬了猫"  # 相同的词,不同的语义
"我很高兴" vs "很我高兴"  # 语序决定合法性

为什么不用位置嵌入:

  1. 固定长度限制:学习的位置嵌入受训练序列长度限制
  2. 泛化能力差:对超出训练长度的序列效果差
  3. 参数开销:需要额外的参数存储
  4. 数学优雅性:函数式编码更简洁和理论化

怎么做: 正弦位置编码的数学艺术

核心数学公式:

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

其中:

  • pos:序列中的位置 (0, 1, 2, …)
  • i:维度索引 (0, 1, 2, …, d_model/2-1)
  • 2i2i+1:偶数和奇数维度

初始化过程的逐步分析:

步骤1:创建位置张量

pe = torch.zeros(max_len, d_model)  # [5000, 512]
position = torch.arange(0, max_len).unsqueeze(1)  # [5000, 1]
# position 的内容:
# [[0], [1], [2], [3], ..., [4999]]

步骤2:计算频率项

div_term = torch.exp(
    torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)
)

频率项的数学含义:

# 对于维度 i (i = 0, 1, 2, ...)
div_term[i] = exp(-log(10000) * 2i / d_model)
            = exp(log(10000^(-2i/d_model)))
            = 10000^(-2i/d_model)
            = 1 / 10000^(2i/d_model)

为什么使用这种频率设计:

  1. 多尺度表示:不同维度具有不同的频率
  2. 几何级数:频率按几何级数递减,覆盖多个时间尺度
  3. 唯一性保证:每个位置都有唯一的编码向量
  4. 相对位置:能够表达相对位置关系

步骤3:应用三角函数

pe[:, 0::2] = torch.sin(position * div_term)  # 偶数维度使用sin
pe[:, 1::2] = torch.cos(position * div_term)  # 奇数维度使用cos

三角函数编码的优势:

1. 周期性特征:

# sin和cos函数具有周期性,能够处理长序列
# 即使超出训练长度,也能产生有意义的位置编码

2. 相对位置信息:

# 利用三角恒等式:
# sin(a + b) = sin(a)cos(b) + cos(a)sin(b)
# cos(a + b) = cos(a)cos(b) - sin(a)sin(b)
# 位置 pos+k 的编码可以表示为位置 pos 编码的线性组合

3. 有界性:

# sin和cos的值域为[-1, 1],编码值稳定
# 避免了位置编码随位置增长而发散

步骤4:添加批次维度

pe = pe.unsqueeze(0)  # [1, 5000, 512]

步骤5:注册为缓冲区

self.register_buffer("pe", pe)

register_buffer的作用:

  • 非参数存储:不作为模型参数,不参与梯度更新
  • 设备同步:自动跟随模型移动到GPU/CPU
  • 状态保存:在模型保存/加载时自动处理

前向传播的应用:

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

关键操作解析:

1. 长度匹配:

self.pe[:, : x.size(1)]  # 截取与输入序列长度相同的位置编码

2. 梯度控制:

.requires_grad_(False)  # 确保位置编码不参与梯度计算

3. 相加融合:

x = x + self.pe[...]  # 词嵌入 + 位置编码

为什么是相加而不是拼接:

  1. 维度保持:不改变特征维度,保持架构简洁
  2. 线性叠加:允许注意力机制同时关注内容和位置
  3. 计算效率:避免维度增长带来的计算开销
  4. 理论支持:线性叠加能够被后续的线性变换有效处理

可视化理解:

# 不同位置的编码模式:
# 位置0: [sin(0/1), cos(0/1), sin(0/100), cos(0/100), ...]
# 位置1: [sin(1/1), cos(1/1), sin(1/100), cos(1/100), ...]
# 位置2: [sin(2/1), cos(2/1), sin(2/100), cos(2/100), ...]

频率谱分析:

  • 低频成分:编码长期位置模式
  • 高频成分:编码短期位置变化
  • 多频融合:提供丰富的位置表示

实际效果验证:

# 相邻位置编码的相似性较高
similarity = cosine_similarity(pe[pos], pe[pos+1])

# 远距离位置编码的相似性较低
distance_similarity = cosine_similarity(pe[pos], pe[pos+100])

这种设计巧妙地将数学的优雅性计算的高效性表示的丰富性完美结合,为Transformer提供了强大而灵活的位置感知能力。

pe = pe.unsqueeze(0)
self.register_buffer("pe", pe)
  • unsqueeze(0) 添加批次维度
  • register_bufferpe 注册为模型的一部分,但不是参数
  • 缓冲区会随模型移动(CPU/GPU),但不会被优化器更新

forward 方法:

def forward(self, x):
    x = x + self.pe[:, : x.size(1)].requires_grad_(False)
    return self.dropout(x)
  • 将位置编码加到嵌入向量上
  • : x.size(1) 只取序列长度对应的位置编码
  • requires_grad_(False) 确保位置编码不参与梯度计算

正弦函数的优势:

  1. 周期性:可以表示相对位置关系
  2. 外推性:可以处理比训练时更长的序列
  3. 数值稳定:值域在 [-1, 1] 之间
  4. 几何性质:可以通过线性变换表示相对位置

单元格 47:可视化位置编码

这个单元格创建了一个图表,展示不同维度的位置编码是如何随位置变化的。你可以看到正弦和余弦波的周期性模式。


单元格 48-49:完整模型构建 - 组装所有组件

是什么: 完整Transformer模型的工厂函数

def make_model(
    src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1
):
    "Helper: Construct a model from hyperparameters."
    c = copy.deepcopy
    attn = MultiHeadedAttention(h, d_model)
    ff = PositionwiseFeedForward(d_model, d_ff, dropout)
    position = PositionalEncoding(d_model, dropout)
    model = EncoderDecoder(
        Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
        Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
        nn.Sequential(Embeddings(d_model, src_vocab), c(position)),
        nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),
        Generator(d_model, tgt_vocab),
    )

    # This was important from their code.
    # Initialize parameters with Glorot / fan_avg.
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)
    return model

工厂函数的核心职责:

  • 组件制造器:批量创建所有必需的神经网络模块
  • 架构装配器:按照正确的拓扑结构组装完整模型
  • 参数初始化器:使用最佳实践初始化所有可训练参数

为什么需要工厂函数: 模块化设计的智慧

复杂系统的组装挑战:

  1. 组件繁多:编码器、解码器、注意力、前馈网络等众多模块
  2. 依赖关系:各组件间有复杂的参数和结构依赖
  3. 配置管理:需要统一管理超参数和配置
  4. 重复使用:需要创建相同结构的多个实例

工厂模式的优势:

  1. 封装复杂性:隐藏组装细节,提供简洁接口
  2. 保证一致性:确保所有组件使用相同的超参数
  3. 便于实验:通过修改参数快速创建不同配置的模型
  4. 减少错误:避免手动组装过程中的配置错误

怎么做: 精密的模型装配流程

超参数配置解析:

def make_model(
    src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1
):

参数含义与典型值:

  • src_vocab:源语言词汇表大小(如英语:30,000)
  • tgt_vocab:目标语言词汇表大小(如中文:50,000)
  • N=6:编码器和解码器层数(原论文标准)
  • d_model=512:模型隐藏维度(平衡性能与计算)
  • d_ff=2048:前馈网络维度(4倍扩张)
  • h=8:多头注意力头数(并行处理)
  • dropout=0.1:正则化强度(防止过拟合)

深拷贝策略:

c = copy.deepcopy

为什么使用深拷贝:

  1. 独立实例:每个层都需要独立的参数,不能共享
  2. 避免别名:防止多个引用指向同一个对象
  3. 梯度隔离:确保各层的梯度更新相互独立
  4. 内存安全:避免意外的参数共享导致的训练问题

组件模板创建:

attn = MultiHeadedAttention(h, d_model)          # 注意力模板
ff = PositionwiseFeedForward(d_model, d_ff, dropout)  # 前馈网络模板
position = PositionalEncoding(d_model, dropout)  # 位置编码模板

模板的作用:

  • 作为”蓝图”创建多个独立实例
  • 保证所有实例使用相同的架构配置
  • 通过深拷贝确保参数独立性

模型主体组装:

model = EncoderDecoder(
    Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
    Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
    nn.Sequential(Embeddings(d_model, src_vocab), c(position)),
    nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),
    Generator(d_model, tgt_vocab),
)

组装逻辑深度分析:

编码器构建:

Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N)
  • 创建编码器层模板:EncoderLayer(...)
  • 复制N层:每层都是独立的深拷贝
  • 堆叠形成完整编码器

解码器构建:

Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N)
  • 注意双重注意力c(attn), c(attn)
    • 第一个:自注意力(self-attention)
    • 第二个:编码器-解码器注意力(cross-attention)
  • 每个解码器层需要两个独立的注意力模块

嵌入层构建:

nn.Sequential(Embeddings(d_model, src_vocab), c(position))  # 源语言
nn.Sequential(Embeddings(d_model, tgt_vocab), c(position))  # 目标语言

Sequential的设计意图:

  1. 流水线处理:词嵌入 → 位置编码
  2. 模块化封装:将两个步骤打包为一个模块
  3. 复用性:编码器和解码器都需要相同的流程
  4. 扩展性:便于添加其他预处理步骤

输出层配置:

Generator(d_model, tgt_vocab)
  • 将隐藏表示映射到目标词汇表
  • 产生最终的词汇概率分布

Xavier权重初始化:

for p in model.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

Xavier初始化的深层原理:

为什么需要合适的初始化:

  1. 梯度流动:不当初始化导致梯度消失或爆炸
  2. 激活分布:保持各层激活值的合理分布
  3. 收敛速度:好的初始化加速训练收敛
  4. 性能上限:影响最终模型的性能表现

Xavier公式:

# 对于权重矩阵 W ∈ R^(m×n)
std = sqrt(2.0 / (fan_in + fan_out))
W ~ Uniform(-std * sqrt(3), std * sqrt(3))

其中:

  • fan_in:输入维度
  • fan_out:输出维度
  • 保持前向和反向传播的方差稳定

维度判断逻辑:

if p.dim() > 1:
  • 多维参数:权重矩阵(需要Xavier初始化)
  • 一维参数:偏置项(通常初始化为0)

模型规模分析:

# 参数量估算(N=6, d_model=512, h=8, d_ff=2048)
encoder_params = N * (
    3 * d_model * d_model +  # 注意力的Q、K、V投影
    d_model * d_model +      # 注意力输出投影
    2 * d_model * d_ff +     # 前馈网络
    4 * d_model              # 层归一化参数
)

decoder_params = N * (
    6 * d_model * d_model +  # 两个注意力模块
    2 * d_model * d_ff +     # 前馈网络
    6 * d_model              # 层归一化参数
)

embedding_params = (src_vocab + tgt_vocab) * d_model
total_params ≈ 65M  # 约6500万参数

实际使用示例:

# 创建标准Transformer模型
model = make_model(
    src_vocab=30000,    # 英语词汇表
    tgt_vocab=50000,    # 中文词汇表
    N=6,                # 6层编码器和解码器
    d_model=512,        # 512维隐藏状态
    d_ff=2048,          # 2048维前馈网络
    h=8,                # 8个注意力头
    dropout=0.1         # 10%的dropout
)

print(f"模型参数量: {sum(p.numel() for p in model.parameters()):,}")

这个工厂函数优雅地封装了Transformer的复杂性,使得研究者和工程师能够专注于模型的使用和改进,而不必纠结于繁琐的组装细节。

    model = EncoderDecoder(
        Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
        Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
        nn.Sequential(Embeddings(d_model, src_vocab), c(position)),
        nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),
        Generator(d_model, tgt_vocab),
    )

组装过程分析:

  1. 编码器Encoder(EncoderLayer(...), N)

    • 创建一个编码器层的模板
    • 复制 N 次创建完整编码器
  2. 解码器Decoder(DecoderLayer(...), N)

    • 注意解码器层有两个注意力:c(attn), c(attn)
    • 第一个是自注意力,第二个是编码器-解码器注意力
  3. 源语言嵌入nn.Sequential(Embeddings(...), c(position))

    • 将嵌入层和位置编码串联
    • 输入先经过嵌入,再加上位置编码
  4. 目标语言嵌入:同源语言嵌入

  5. 生成器Generator(d_model, tgt_vocab)

    • 将解码器输出转换为词汇概率

参数初始化:

for p in model.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

Xavier 初始化:

  • 也叫 Glorot 初始化
  • 根据层的输入和输出维度来设置初始权重
  • 目标是保持梯度在合理范围内
  • p.dim() > 1 确保只初始化矩阵参数,不初始化偏置

为什么需要好的初始化:

  1. 梯度流动:好的初始化帮助梯度正常传播
  2. 收敛速度:影响训练的收敛速度
  3. 避免饱和:防止激活函数进入饱和区域

这样,我们就有了一个完整的 Transformer 模型!

单元格 50-54:推理测试和简单示例

是什么: 模型推理能力的验证器

这个函数展示了如何使用未训练的模型进行推理。虽然模型还没有训练,但可以看到推理的完整流程。

推理过程的本质:

  • 前向传播验证器:确保模型架构正确无误
  • 数据流测试器:验证输入输出的形状和类型匹配
  • 接口展示器:演示模型的使用方法

为什么需要推理测试: 验证架构完整性

未训练模型的测试价值:

  1. 架构验证:确保所有组件正确连接
  2. 维度检查:验证数据在各层间的形状变换
  3. 接口测试:确认模型的输入输出接口设计合理
  4. 调试基础:为后续训练提供基础的错误检查

早期测试的重要性:

  • 快速发现问题:在投入大量训练资源前发现架构问题
  • 节省时间:避免长时间训练后才发现基础错误
  • 建立信心:确认模型基本功能正常

怎么做: 推理测试的实现策略

虽然模型未训练,但输出应该具有合理的形状和数值特征。这为后续的完整训练和评估奠定了基础。


单元格 55-58:训练准备 - 批次和掩码

是什么: 训练数据的智能包装器

class Batch:
    """Object for holding a batch of data with mask during training."""

    def __init__(self, src, tgt=None, pad=2):  # 2 = <blank>
        self.src = src
        self.src_mask = (src != pad).unsqueeze(-2)
        if tgt is not None:
            self.tgt = tgt[:, :-1]
            self.tgt_y = tgt[:, 1:]
            self.tgt_mask = self.make_std_mask(self.tgt, pad)
            self.ntokens = (self.tgt_y != pad).data.sum()

    @staticmethod
    def make_std_mask(tgt, pad):
        "Create a mask to hide padding and future words."
        tgt_mask = (tgt != pad).unsqueeze(-2)
        tgt_mask = tgt_mask & subsequent_mask(tgt.size(-1)).type_as(
            tgt_mask.data
        )
        return tgt_mask

Batch类的核心使命:

  • 数据组织器:将原始序列数据转换为模型可处理的批次格式
  • 掩码生成器:为注意力机制创建必要的掩码矩阵
  • 训练适配器:为teacher forcing训练模式准备输入输出对

为什么需要Batch类: 解决训练中的实际问题

批次处理的必要性:

  1. 计算效率:GPU并行处理多个样本比逐一处理快得多
  2. 内存优化:批次处理充分利用GPU的并行计算能力
  3. 梯度稳定:批次内梯度的平均化提高训练稳定性
  4. 序列对齐:处理不等长序列需要填充和掩码机制

掩码的关键作用:

  1. 填充掩码:区分真实内容和填充内容
  2. 因果掩码:防止解码器看到未来信息
  3. 注意力控制:精确控制模型的注意力分布

怎么做: 精密的数据预处理流程

源序列处理的细节:

self.src = src  # 源序列,如英语句子
self.src_mask = (src != pad).unsqueeze(-2)  # 创建填充掩码

掩码创建的数学逻辑:

# 假设源序列:[1, 15, 234, 5, 2, 2, 2]  # 2是填充符
# 掩码结果:  [T,  T,   T, T, F, F, F]  # T=True(真实), F=False(填充)

unsqueeze(-2)的作用:

# 原始掩码形状:[batch_size, seq_len]
# 处理后形状:  [batch_size, 1, seq_len]
# 目的:为多头注意力的广播机制做准备

Teacher Forcing的巧妙实现:

if tgt is not None:
    self.tgt = tgt[:, :-1]    # 解码器输入:去掉最后一个词
    self.tgt_y = tgt[:, 1:]   # 期望输出:去掉第一个词

具体示例:

# 原始目标序列:[<start>, I, am, happy, <end>]
# 解码器输入:  [<start>, I, am, happy]        (self.tgt)
# 期望输出:    [I, am, happy, <end>]          (self.tgt_y)

这种设计的深层原理:

  1. 自回归性质:每个位置的输出只依赖前面的输入
  2. 并行训练:可以同时计算所有位置的损失
  3. 稳定性:避免了推理时的错误累积

目标掩码的双重保护:

@staticmethod
def make_std_mask(tgt, pad):
    tgt_mask = (tgt != pad).unsqueeze(-2)  # 填充掩码
    tgt_mask = tgt_mask & subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data)
    return tgt_mask

双重掩码的逻辑运算:

# 填充掩码:   [T, T, T, F, F]
# 因果掩码:   下三角矩阵
# 最终掩码:   填充掩码 AND 因果掩码

静态方法的设计考虑:

@staticmethod  # 不依赖实例状态的纯函数
  • 功能独立:可以独立测试和使用
  • 内存效率:不需要访问实例变量
  • 设计清晰:明确表示这是一个工具函数

词元统计的实用价值:

self.ntokens = (self.tgt_y != pad).data.sum()
  • 训练监控:跟踪处理的有效词元数量
  • 性能评估:计算每秒处理的词元数
  • 损失归一化:按有效词元数归一化损失

这种精心设计的批次处理机制,确保了训练过程的高效性和正确性,是深度学习训练管道中的关键环节。


单元格 59-61:训练循环核心 - 模型学习的引擎

是什么: 深度学习训练的执行引擎

class TrainState:
    """Track number of steps, examples, and tokens processed"""
    step: int = 0  # Steps in the current epoch
    accum_step: int = 0  # Number of gradient accumulation steps
    samples: int = 0  # total # of examples used
    tokens: int = 0  # total # of tokens processed

def run_epoch(
    data_iter, model, loss_compute, optimizer, scheduler,
    mode="train", accum_iter=1, train_state=TrainState(),
):
    """Train a single epoch"""
    start = time.time()
    total_tokens = 0
    total_loss = 0
    tokens = 0
    n_accum = 0
    
    for i, batch in enumerate(data_iter):
        out = model.forward(batch.src, batch.tgt, batch.src_mask, batch.tgt_mask)
        loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)
        
        if mode == "train" or mode == "train+log":
            loss_node.backward()
            train_state.step += 1
            train_state.samples += batch.src.shape[0]
            train_state.tokens += batch.ntokens
            if i % accum_iter == 0:
                optimizer.step()
                optimizer.zero_grad(set_to_none=True)
                n_accum += 1
                train_state.accum_step += 1
            scheduler.step()

        total_loss += loss
        total_tokens += batch.ntokens
        tokens += batch.ntokens
        
        if i % 40 == 1 and (mode == "train" or mode == "train+log"):
            lr = optimizer.param_groups[0]["lr"]
            elapsed = time.time() - start
            print(f"Epoch Step: {i:6d} | Accumulation Step: {n_accum:3d} | "
                  f"Loss: {loss / batch.ntokens:6.2f} | "
                  f"Tokens / Sec: {tokens / elapsed:7.1f} | "
                  f"Learning Rate: {lr:6.1e}")
            start = time.time()
            tokens = 0
        del loss
        del loss_node
    return total_loss / total_tokens, train_state

训练引擎的核心职责:

  • 状态跟踪器:监控训练进度和统计信息
  • 梯度协调器:管理前向传播、反向传播和参数更新
  • 性能监视器:实时展示训练效率和学习进度

为什么需要训练循环: 深度学习的核心机制

训练循环的根本意义:

  1. 参数优化:通过梯度下降不断调整模型参数
  2. 知识积累:每个批次都为模型提供学习机会
  3. 收敛控制:监控和引导模型向最优解收敛
  4. 资源管理:高效利用计算资源和内存

TrainState的设计哲学:

  1. 全局视角:跟踪整个训练过程的宏观统计
  2. 细粒度监控:从步骤、样本到词元的多层次计数
  3. 性能评估:为训练效率分析提供数据支撑
  4. 状态恢复:为训练中断恢复提供状态快照

怎么做: 训练循环的精密编排

TrainState的状态管理:

step: int = 0          # 当前epoch内的训练步数
accum_step: int = 0    # 梯度累积步数(实际参数更新次数)
samples: int = 0       # 处理的样本总数
tokens: int = 0        # 处理的词元总数

状态字段的具体含义:

  • step vs accum_step:区分批次处理和参数更新频率
  • samples:用于计算平均每个样本的处理时间
  • tokens:更精确的处理量度量,因为序列长度不同

run_epoch的核心循环:

阶段1:前向传播

out = model.forward(batch.src, batch.tgt, batch.src_mask, batch.tgt_mask)
  • 输入源序列和目标序列(带掩码)
  • 获得模型对每个位置的词汇概率预测

阶段2:损失计算

loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)
  • loss:标量损失值,用于统计
  • loss_node:保留计算图的张量,用于反向传播
  • batch.tgt_y:真实的目标标签
  • batch.ntokens:用于损失归一化

阶段3:反向传播与优化

if mode == "train" or mode == "train+log":
    loss_node.backward()                    # 计算梯度
    train_state.step += 1                   # 更新步数
    train_state.samples += batch.src.shape[0]  # 累积样本数
    train_state.tokens += batch.ntokens     # 累积词元数
    
    if i % accum_iter == 0:                 # 梯度累积条件
        optimizer.step()                    # 应用梯度更新
        optimizer.zero_grad(set_to_none=True)  # 清空梯度
        n_accum += 1                        # 累积步数递增
        train_state.accum_step += 1
    
    scheduler.step()                        # 学习率调度

梯度累积的巧妙机制:

if i % accum_iter == 0:
    optimizer.step()
    optimizer.zero_grad(set_to_none=True)

梯度累积的作用原理:

  1. 有效批次大小扩大effective_batch_size = batch_size * accum_iter
  2. 内存限制应对:在GPU内存不足时模拟大批次训练
  3. 梯度稳定性:更大的有效批次提供更稳定的梯度估计
  4. 训练等价性:与真实大批次训练数学上等价

set_to_none=True的优化:

optimizer.zero_grad(set_to_none=True)
  • 内存效率:释放梯度张量的内存,而不是置零
  • 性能提升:避免不必要的内存操作
  • 现代最佳实践:PyTorch 1.7+推荐的梯度清理方式

学习率调度的时机:

scheduler.step()  # 每个批次后调用,而非每次参数更新后
  • 精细控制:提供更细粒度的学习率调整
  • warmup支持:支持复杂的学习率预热策略
  • 收敛优化:根据训练进度动态调整学习率

阶段4:实时监控与报告

if i % 40 == 1 and (mode == "train" or mode == "train+log"):
    lr = optimizer.param_groups[0]["lr"]    # 获取当前学习率
    elapsed = time.time() - start           # 计算时间间隔
    print(f"Epoch Step: {i:6d} | Accumulation Step: {n_accum:3d} | "
          f"Loss: {loss / batch.ntokens:6.2f} | "
          f"Tokens / Sec: {tokens / elapsed:7.1f} | "
          f"Learning Rate: {lr:6.1e}")
    start = time.time()                     # 重置计时器
    tokens = 0                              # 重置词元计数

监控报告的设计考虑:

  1. 适度频率:每40步报告一次,平衡信息量和性能
  2. 关键指标:损失、吞吐量、学习率三大核心指标
  3. 归一化损失:按词元数归一化,提供可比较的损失值
  4. 吞吐量监控:Tokens/Sec是衡量训练效率的重要指标

内存管理的细节:

del loss
del loss_node
  • 显式删除:及时释放不再需要的张量
  • 内存优化:防止循环中的内存累积
  • PyTorch最佳实践:在长循环中及时清理计算图

返回值的设计:

return total_loss / total_tokens, train_state
  • 平均损失:按词元数归一化的epoch平均损失
  • 状态传递:更新后的训练状态,支持多epoch训练

实际使用场景:

# 训练模式
train_loss, train_state = run_epoch(
    train_dataloader, model, loss_compute, 
    optimizer, scheduler, mode="train", accum_iter=4
)

# 验证模式
val_loss, _ = run_epoch(
    val_dataloader, model, loss_compute,
    None, None, mode="eval"
)

这种精心设计的训练循环,提供了高效、稳定、可监控的模型训练框架,是深度学习项目成功的关键基础设施。

训练循环详解:

1. 前向传播:

out = model.forward(
    batch.src, batch.tgt, batch.src_mask, batch.tgt_mask
)
  • 将批次数据传入模型
  • 获得模型的预测输出

2. 损失计算:

loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)
  • 比较模型预测和真实标签
  • 返回损失值和可计算梯度的损失节点

3. 反向传播(仅在训练模式):

if mode == "train" or mode == "train+log":
    loss_node.backward()
  • backward() 计算梯度
  • 梯度存储在模型参数的 .grad 属性中

4. 梯度累积:

if i % accum_iter == 0:
    optimizer.step()
    optimizer.zero_grad(set_to_none=True)

单元格 62-65:学习率调度 - 训练的节奏掌控者

是什么: 动态学习率控制策略

def rate(step, model_size, factor, warmup):
    """
    we have to default the step to 1 for LambdaLR function
    to avoid zero raising to negative power.
    """
    if step == 0:
        step = 1
    return factor * (
        model_size ** (-0.5) * min(step ** (-0.5), step * warmup ** (-1.5))
    )

学习率调度器的核心使命:

  • 训练节奏掌控者:在训练过程中动态调整学习步长
  • 收敛优化器:平衡探索能力和收敛稳定性
  • 性能提升器:通过精心设计的策略提升最终性能

为什么需要学习率调度: 优化过程的智慧

固定学习率的局限性:

  1. 探索vs利用矛盾:高学习率利于探索,低学习率利于收敛
  2. 训练阶段差异:早期需要大步探索,后期需要细致调优
  3. 损失景观复杂性:不同阶段的损失函数特性不同
  4. 泛化性能优化:动态调整有助于找到更好的泛化解

Transformer特有的挑战:

  1. 参数规模庞大:需要更谨慎的优化策略
  2. 梯度传播复杂:深层网络需要稳定的初始训练
  3. 注意力机制敏感性:对学习率变化较为敏感
  4. 收敛困难:大型模型容易陷入局部最优

怎么做: Transformer的学习率艺术

核心公式深度解析:

lr = factor * d_model^(-0.5) * min(step^(-0.5), step * warmup^(-1.5))

公式的三个关键组成部分:

1. 基础缩放因子:

factor * model_size ** (-0.5)
  • factor:全局缩放参数,通常设为1.0或2.0
  • d_model^(-0.5):根据模型维度自适应缩放
  • 数学直觉:更大的模型需要更小的学习率

为什么使用d_model^(-0.5):

# 对于d_model=512: 1/√512 ≈ 0.044
# 对于d_model=1024: 1/√1024 ≈ 0.031
# 模型越大,基础学习率越小

2. Warmup阶段(step < warmup):

step * warmup ** (-1.5)

Warmup的数学特性:

# 当step < warmup时:
lr = factor * d_model^(-0.5) * step * warmup^(-1.5)
   = factor * d_model^(-0.5) * (step / warmup^1.5)

# 这实际上是线性增长,直到warmup步数

Warmup阶段的渐进过程:

# 假设warmup=4000
# step=1000: lr ∝ 1000/4000^1.5 = 很小的值
# step=2000: lr ∝ 2000/4000^1.5 = 稍大的值  
# step=4000: lr ∝ 4000/4000^1.5 = 达到峰值

3. 衰减阶段(step >= warmup):

step ** (-0.5)

衰减的数学特性:

# 当step >= warmup时:
lr = factor * d_model^(-0.5) * step^(-0.5)
   = factor * d_model^(-0.5) / √step

# 这是平方根衰减,比指数衰减更温和

两阶段的平滑过渡:

# 在warmup点,两个表达式相等:
step^(-0.5) = step * warmup^(-1.5)
# 解得:step = warmup(过渡点)

特殊处理的边界情况:

if step == 0:
    step = 1
  • 数学稳定性:避免0的负数次幂导致的数值问题
  • 实现细节:LambdaLR要求学习率函数在step=0时有定义
  • 逻辑合理性:第一步应该有一个合理的小学习率

可视化理解学习率曲线:

# Warmup阶段:线性上升
# 峰值点:在warmup步数达到最大值
# 衰减阶段:平方根衰减,缓慢下降

不同参数的影响:

factor参数的作用:

factor = 1.0  # 标准设置
factor = 2.0  # 更激进的学习率
factor = 0.5  # 更保守的学习率

warmup参数的影响:

warmup = 4000   # 标准设置,适中的预热期
warmup = 8000   # 更长的预热期,更稳定
warmup = 2000   # 更短的预热期,更快进入衰减

model_size的自适应性:

d_model = 512   # Base模型
d_model = 1024  # Large模型,自动使用更小的学习率

实际应用中的优势:

1. 训练稳定性:

  • 避免初期的梯度爆炸
  • 为复杂的注意力机制提供平滑的优化路径

2. 收敛质量:

  • Warmup确保良好的初始化
  • 平方根衰减保持持续的学习能力

3. 实验可重复性:

  • 数学公式明确,减少超参数调优的主观性
  • 在不同规模的模型间具有良好的迁移性

与其他调度策略的比较:

# 指数衰减:lr *= gamma^step (衰减太快)
# 线性衰减:lr -= alpha*step (缺乏理论基础)
# 余弦衰减:lr = base * cos(π*step/max_steps) (缺乏warmup)
# Transformer调度:兼具warmup和适度衰减的优势

单元格 66-70:标签平滑技术 - 对抗过拟合的智慧

是什么: 概率分布的平滑化器

class LabelSmoothing(nn.Module):
    "Implement label smoothing."

    def __init__(self, size, padding_idx, smoothing=0.0):
        super(LabelSmoothing, self).__init__()
        self.criterion = nn.KLDivLoss(reduction="sum")
        self.padding_idx = padding_idx
        self.confidence = 1.0 - smoothing
        self.smoothing = smoothing
        self.size = size
        self.true_dist = None

    def forward(self, x, target):
        assert x.size(1) == self.size
        true_dist = x.data.clone()
        true_dist.fill_(self.smoothing / (self.size - 2))
        true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
        true_dist[:, self.padding_idx] = 0
        mask = torch.nonzero(target.data == self.padding_idx)
        if mask.dim() > 0:
            true_dist.index_fill_(0, mask.squeeze(), 0.0)
        self.true_dist = true_dist
        return self.criterion(x, true_dist.clone().detach())

标签平滑的核心使命:

  • 概率软化器:将硬性的one-hot标签转换为平滑的概率分布
  • 过拟合抑制器:防止模型过度自信于训练数据
  • 泛化增强器:提高模型在未见数据上的表现

为什么需要标签平滑: 对抗过度自信的陷阱

传统硬标签的问题:

  1. 过度自信:模型被训练为对正确答案给出100%的置信度
  2. 泛化差:在训练数据上表现很好,但在新数据上表现差
  3. 脆弱性:对输入的小变化过于敏感
  4. 校准问题:预测概率与实际准确率不匹配

硬标签的数学表示:

# 传统one-hot编码,假设正确答案是词汇3
hard_label = [0, 0, 0, 1, 0, 0, ...]  # 只有位置3是1,其他都是0

平滑标签的改进思路:

# 标签平滑后,假设smoothing=0.1
smooth_label = [0.02, 0.02, 0.02, 0.86, 0.02, 0.02, ...]  
# 正确答案仍然概率最高,但其他位置也有小概率

深层原理解析:

  1. 不确定性建模:承认训练标签可能不是唯一正确答案
  2. 正则化效应:阻止模型在训练集上过度拟合
  3. 概率校准:使预测概率更接近真实的置信度
  4. 鲁棒性提升:提高对噪声和变化的抗干扰能力

怎么做: 标签平滑的精确实现

初始化参数的深度含义:

def __init__(self, size, padding_idx, smoothing=0.0):
    self.criterion = nn.KLDivLoss(reduction="sum")  # KL散度损失
    self.padding_idx = padding_idx                   # 填充符索引
    self.confidence = 1.0 - smoothing               # 正确答案的概率
    self.smoothing = smoothing                      # 平滑参数
    self.size = size                                # 词汇表大小

KL散度损失的选择:

nn.KLDivLoss(reduction="sum")
  • KL散度定义:D_KL(P||Q) = Σ P(x) * log(P(x)/Q(x))
  • 适用性:自然地处理概率分布间的差异
  • 数学性质:非对称,惩罚模型与目标分布的偏差

平滑分布的构建过程:

步骤1:创建模板分布

true_dist = x.data.clone()  # 复制输入张量的形状
  • 获得与模型输出相同的形状 [batch_size, vocab_size]
  • 为每个样本创建概率分布模板

步骤2:填充均匀基础概率

true_dist.fill_(self.smoothing / (self.size - 2))

为什么除以(size-2)而不是size:

# size - 2 的原因:
# 1. 排除正确答案位置(它会被单独设置)
# 2. 排除padding位置(它会被设为0)
# 实际参与平滑分配的词汇数 = total_vocab - 1 - 1 = size - 2

数学推导:

# 设词汇表大小为V,平滑参数为ε
# 正确答案概率:1 - ε
# 错误答案总概率:ε
# 每个错误答案概率:ε / (V - 2)

步骤3:设置正确答案的高概率

true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)

scatter_操作详解:

# scatter_(dim, index, src)
# dim=1: 在词汇维度上操作
# index: 正确答案的位置索引
# src: 要填入的值(confidence = 1.0 - smoothing)

具体示例:

# 假设target=[3], confidence=0.9, smoothing=0.1, vocab_size=6
# 初始: [0.025, 0.025, 0.025, 0.025, 0.025, 0.025]  # (除了padding)
# scatter后: [0.025, 0.025, 0.025, 0.9, 0.025, 0.025]

步骤4:处理填充位置

true_dist[:, self.padding_idx] = 0  # 填充符概率设为0

填充位置的特殊处理:

  • 填充符不是真实词汇,不应参与概率分布
  • 设为0确保这些位置不影响损失计算

步骤5:处理填充样本

mask = torch.nonzero(target.data == self.padding_idx)
if mask.dim() > 0:
    true_dist.index_fill_(0, mask.squeeze(), 0.0)

边界情况处理:

  • 当目标本身就是填充符时,整行概率都设为0
  • 这种情况在序列末尾的填充位置会出现

概率分布的数学验证:

# 对于非填充位置,概率分布应该和为1:
# P(correct) + Σ P(incorrect) = confidence + (size-2) * smoothing/(size-2)
#                              = (1-smoothing) + smoothing = 1 ✓

实际效果对比:

传统硬标签:

# 目标: "I love you",词汇表: ["I", "love", "you", "hate", "like"]
hard_labels = [
    [1.0, 0.0, 0.0, 0.0, 0.0],  # "I"
    [0.0, 1.0, 0.0, 0.0, 0.0],  # "love"  
    [0.0, 0.0, 1.0, 0.0, 0.0],  # "you"
]

标签平滑后:

# smoothing = 0.1
smooth_labels = [
    [0.9, 0.025, 0.025, 0.025, 0.025],  # "I" 更多选择可能性
    [0.025, 0.9, 0.025, 0.025, 0.025],  # "love" 
    [0.025, 0.025, 0.9, 0.025, 0.025],  # "you"
]

性能提升的量化分析:

  1. 训练损失:稍微增加(因为目标概率降低)
  2. 验证损失:通常降低(因为泛化能力提升)
  3. BLEU分数:在机器翻译任务上通常提升1-2分
  4. 概率校准:预测概率与实际准确率更匹配

超参数选择指导:

# 常用smoothing值:
smoothing = 0.0   # 无平滑(传统训练)
smoothing = 0.1   # 轻度平滑(推荐起点)
smoothing = 0.2   # 中度平滑(某些任务有效)
smoothing = 0.3   # 重度平滑(可能过度)

平滑参数的影响分析:

  • 过小(<0.05):效果有限,接近硬标签
  • 适中(0.1-0.2):平衡性能和泛化,多数情况最优
  • 过大(>0.3):可能损害学习效率,正确答案置信度过低

单元格 71-77:简单复制任务示例 - 模型能力的初步验证

是什么: 模型学习能力的基础测试

def data_gen(V, batch_size, nbatches):
    "Generate random data for a src-tgt copy task."
    for i in range(nbatches):
        data = torch.randint(1, V, size=(batch_size, 10))
        data[:, 0] = 1
        src = data.requires_grad_(False).clone().detach()
        tgt = data.requires_grad_(False).clone().detach()
        yield Batch(src, tgt, 0)

class SimpleLossCompute:
    "A simple loss compute and train function."
    def __init__(self, generator, criterion):
        self.generator = generator
        self.criterion = criterion

    def __call__(self, x, y, norm):
        x = self.generator(x)
        sloss = (
            self.criterion(
                x.contiguous().view(-1, x.size(-1)), y.contiguous().view(-1)
            )
            / norm
        )
        return sloss.data * norm, sloss

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    memory = model.encode(src, src_mask)
    ys = torch.zeros(1, 1).fill_(start_symbol).type_as(src.data)
    for i in range(max_len - 1):
        out = model.decode(
            memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data)
        )
        prob = model.generator(out[:, -1])
        _, next_word = torch.max(prob, dim=1)
        next_word = next_word.data[0]
        ys = torch.cat(
            [ys, torch.zeros(1, 1).type_as(src.data).fill_(next_word)], dim=1
        )
    return ys

复制任务的核心价值:

  • 原理验证器:测试模型是否具备基本的序列学习能力
  • 架构调试器:快速发现模型实现中的问题
  • 性能基准:为更复杂任务建立性能底线

为什么从复制任务开始: 循序渐进的学习策略

复制任务的独特优势:

  1. 明确性:输入输出关系完全确定,容易验证正确性
  2. 简洁性:排除语言学复杂性,专注于模型架构测试
  3. 快速性:训练时间短,可以快速迭代和调试
  4. 必要性:连复制都学不会,更复杂任务必然失败

从复制到翻译的能力阶梯:

# 能力层次:复制 < 重排 < 转换 < 翻译
# 复制:[1,2,3] → [1,2,3]  (完全记忆)
# 重排:[1,2,3] → [3,1,2]  (位置变换)
# 转换:[1,2,3] → [4,5,6]  (符号映射)
# 翻译:["I","love"] → ["ich","liebe"]  (语言转换)

怎么做: 复制任务的精确实现

数据生成的巧妙设计:

def data_gen(V, batch_size, nbatches):
    for i in range(nbatches):
        data = torch.randint(1, V, size=(batch_size, 10))  # 随机序列
        data[:, 0] = 1                                     # 固定起始符
        src = data.requires_grad_(False).clone().detach()  # 源序列
        tgt = data.requires_grad_(False).clone().detach()  # 目标序列
        yield Batch(src, tgt, 0)                          # 返回批次

设计细节的深层考虑:

1. 词汇范围控制:

torch.randint(1, V, size=(batch_size, 10))  # 从1到V-1随机选择
  • 避免0索引:通常保留给特殊符号(如填充)
  • 固定长度:简化实验,专注于学习机制验证
  • 随机性:确保模型学习通用模式,而非记忆特定序列

2. 起始符的统一设置:

data[:, 0] = 1  # 所有序列都以符号1开始
  • 一致性:为解码器提供统一的起始点
  • 可预测性:简化解码过程的初始状态
  • 实用性:模拟真实场景中的句子开始标记

3. 梯度管理:

src = data.requires_grad_(False).clone().detach()
  • 内存优化:数据不需要梯度,节省计算资源
  • 安全性:避免意外的梯度传播到数据生成过程

SimpleLossCompute的封装智慧:

class SimpleLossCompute:
    def __call__(self, x, y, norm):
        x = self.generator(x)                           # 生成词汇概率
        sloss = self.criterion(                         # 计算损失
            x.contiguous().view(-1, x.size(-1)),      # 展平为2D
            y.contiguous().view(-1)                    # 展平为1D
        ) / norm                                       # 归一化
        return sloss.data * norm, sloss               # 返回值和节点

形状变换的必要性:

# 原始形状:
# x: [batch_size, seq_len, vocab_size]
# y: [batch_size, seq_len]

# 展平后:
# x: [batch_size * seq_len, vocab_size]  # 每行是一个词的概率分布
# y: [batch_size * seq_len]              # 每个元素是正确的词汇ID

返回双值的设计:

return sloss.data * norm, sloss  # (标量损失, 张量损失)
  • 标量损失:用于监控和日志记录
  • 张量损失:保留计算图,用于反向传播

贪婪解码的逐步实现:

def greedy_decode(model, src, src_mask, max_len, start_symbol):

解码的四个关键阶段:

阶段1:编码源序列

memory = model.encode(src, src_mask)  # 获得源序列的表示
  • 一次编码:编码器只需运行一次
  • 记忆存储:存储源序列的完整信息

阶段2:初始化解码

ys = torch.zeros(1, 1).fill_(start_symbol).type_as(src.data)
  • 起始状态:从特殊的开始符号启动
  • 类型匹配:确保设备和数据类型一致

阶段3:逐步生成

for i in range(max_len - 1):
    out = model.decode(memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data))
    prob = model.generator(out[:, -1])      # 只看最后位置的输出
    _, next_word = torch.max(prob, dim=1)   # 选择概率最大的词
    next_word = next_word.data[0]           # 提取标量值
    ys = torch.cat([ys, torch.zeros(1, 1).type_as(src.data).fill_(next_word)], dim=1)

贪婪策略的实现细节:

torch.max(prob, dim=1)  # 返回 (最大值, 最大值索引)
  • 贪婪选择:总是选择当前步骤概率最大的词
  • 简单高效:无需复杂的搜索算法
  • 确定性:相同输入总是产生相同输出

序列拼接的动态过程:

ys = torch.cat([ys, new_word_tensor], dim=1)  # 在序列维度拼接
  • 动态扩展:序列长度逐步增长
  • 历史保持:保留之前生成的所有词汇

复制任务的成功标准:

# 输入:[1, 3, 7, 2, 9]
# 期望输出:[1, 3, 7, 2, 9]
# 成功指标:100%的词汇级别准确率

训练进度的典型模式:

  1. 初期混乱:随机输出,准确率接近1/vocab_size
  2. 起始符学习:快速学会复制第一个符号
  3. 模式识别:逐渐理解复制任务的本质
  4. 完美复制:最终实现100%准确率

这种精心设计的复制任务,为模型的复杂应用奠定了坚实的基础,体现了**“大道至简”**的深度学习训练哲学。


单元格 78-85:实际机器翻译任务 - 真实应用的挑战

是什么: 从人工数据到真实语言的跨越

# Load the dataset using datasets library
from datasets import load_dataset

# Load IWSLT 2017 German-English dataset
raw_datasets = load_dataset("iwslt2017", "iwslt2017-de-en")

# Tokenization using spaCy
import spacy

spacy_de = spacy.load("de_core_news_sm")
spacy_en = spacy.load("en_core_web_sm")

def tokenize_de(text):
    return [tok.text for tok in spacy_de.tokenizer(text)]

def tokenize_en(text):
    return [tok.text for tok in spacy_en.tokenizer(text)]

机器翻译的核心挑战:

  • 语言复杂性:处理真实语言的语法、语义、语用等多层面复杂性
  • 知识迁移:将在复制任务上验证的架构应用到复杂的双语映射
  • 评估标准:使用BLEU等标准指标衡量翻译质量

为什么选择机器翻译: Transformer的经典应用场景

机器翻译的代表性意义:

  1. 序列到序列本质:体现Transformer encoder-decoder架构的完整能力
  2. 历史意义:Transformer最初就是为机器翻译而设计
  3. 实用价值:具有直接的商业和社会应用价值
  4. 评估完善:有成熟的评估体系和基准数据集

IWSLT数据集的优势:

  1. 适中规模:数据量足够训练,但不会过度消耗计算资源
  2. 高质量:人工标注,质量可靠
  3. 标准基准:研究社区广泛使用,便于对比
  4. 语言对经典:德英翻译是NLP研究的经典语言对

怎么做: 真实数据处理的工程挑战

分词处理的深层考虑:

spacy_de = spacy.load("de_core_news_sm")  # 德语分词模型
spacy_en = spacy.load("en_core_web_sm")   # 英语分词模型

为什么使用spaCy:

  1. 语言特异性:针对不同语言的专门优化
  2. 工业级质量:经过大规模数据训练和验证
  3. 一致性:确保分词结果的可重复性
  4. 便利性:与深度学习框架良好集成

分词策略的影响:

# 例如德语复合词:
# "Bundesrepublik" → ["Bundes", "republik"] 或 ["Bundesrepublik"]
# 不同策略影响词汇表大小和翻译质量

这种从简单复制任务到复杂机器翻译的渐进式设计,体现了深度学习项目中**“从简到繁、步步为营”**的工程智慧。


**分词的重要性:**
1. **语言理解**:将文本分解成有意义的单元
2. **标准化**:处理标点、大小写等
3. **多语言支持**:不同语言有不同的分词规则

**spaCy 工具:**
- 业界标准的自然语言处理库
- 提供多语言支持
- 包含预训练的语言模型

### 词汇表构建:

```python
from torchtext.data.utils import get_tokenizer
from collections import Counter
from torchtext.vocab import vocab

def build_vocabulary(spacy_tokenizer, data_iter):
    def yield_tokens(data_iter):
        for data_sample in data_iter:
            yield spacy_tokenizer(data_sample)
    
    counter = Counter()
    for tokens in yield_tokens(data_iter):
        counter.update(tokens)
    
    return vocab(counter, specials=["<unk>", "<pad>", "<bos>", "<eos>"])

词汇表的作用:

  1. 词汇映射:将词汇转换为数字ID
  2. 处理未知词:使用 <unk> 标记
  3. 特殊标记
    • <pad>:填充短句子
    • <bos>:句子开始
    • <eos>:句子结束
    • <unk>:未知词汇

数据加载器:

def collate_batch(batch, src_pipeline, tgt_pipeline, src_vocab, tgt_vocab, device, max_padding=128, pad_id=2):
    bs_id = torch.tensor([0], device=device)  # <bos> token id
    eos_id = torch.tensor([1], device=device)  # <eos> token id
    src_list, tgt_list = [], []
    
    for (_src, _tgt) in batch:
        processed_src = torch.cat([bs_id, torch.tensor(src_vocab(src_pipeline(_src)), dtype=torch.int64, device=device), eos_id], 0)
        processed_tgt = torch.cat([bs_id, torch.tensor(tgt_vocab(tgt_pipeline(_tgt)), dtype=torch.int64, device=device), eos_id], 0)
        src_list.append(processed_src[:max_padding])
        tgt_list.append(processed_tgt[:max_padding])
    
    return pad_sequence(src_list, padding_value=pad_id), pad_sequence(tgt_list, padding_value=pad_id)

批次处理的挑战:

  1. 变长序列:不同句子长度不同
  2. 填充对齐:需要填充到相同长度
  3. 特殊标记:添加句子边界标记
  4. 内存管理:限制最大长度

单元格 86-92:模型训练和评估

训练配置:

def create_model(src_vocab_size, tgt_vocab_size, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1):
    "Helper: Construct a model from hyperparameters."
    c = copy.deepcopy
    attn = MultiHeadedAttention(h, d_model)
    ff = PositionwiseFeedForward(d_model, d_ff, dropout)
    position = PositionalEncoding(d_model, dropout)
    model = EncoderDecoder(
        Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
        Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
        nn.Sequential(Embeddings(d_model, src_vocab_size), c(position)),
        nn.Sequential(Embeddings(d_model, tgt_vocab_size), c(position)),
        Generator(d_model, tgt_vocab_size))
    
    # Initialize parameters with Glorot / fan_avg.
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)
    return model

超参数的选择:

  • N=6:编码器和解码器各6层(原论文配置)
  • d_model=512:模型维度
  • d_ff=2048:前馈网络维度
  • h=8:注意力头数
  • dropout=0.1:防止过拟合

参数初始化:

nn.init.xavier_uniform_(p)
  • Xavier初始化保持激活值的方差
  • 有助于训练稳定性
  • 避免梯度爆炸或消失

训练循环:

def train_worker(gpu, ngpus_per_node, config, model, criterion, opt):
    torch.distributed.init_process_group(
        backend="nccl", init_method="env://", rank=gpu, world_size=ngpus_per_node
    )
    
    torch.cuda.set_device(gpu)
    device = torch.device("cuda:{}".format(gpu))
    model = model.to(device)
    model = DDP(model, device_ids=[gpu])
    
    train_dataloader, valid_dataloader = create_dataloaders(device, config["vocab_src"], config["vocab_tgt"], config["spacy_de"], config["spacy_en"], config["batch_size"], config["max_padding"], config["is_distributed"])
    
    optimizer = torch.optim.Adam(model.parameters(), lr=config["base_lr"], betas=(0.9, 0.98), eps=1e-9)
    lr_scheduler = LambdaLR(optimizer=optimizer, lr_lambda=lambda step: rate(step, config["d_model"], factor=1, warmup=config["warmup"]))
    
    train_state = TrainState()
    
    for epoch in range(config["num_epochs"]):
        if config["is_distributed"]:
            train_dataloader.sampler.set_epoch(epoch)
        
        model.train()
        print(f"[GPU{gpu}] Epoch {epoch} Training ====", flush=True)
        _, train_state = run_epoch(
            (Batch(b[0], b[1], config["pad_id"]) for b in train_dataloader),
            model,
            SimpleLossCompute(model.module.generator, criterion),
            optimizer,
            lr_scheduler,
            mode="train+log",
            accum_iter=config["accum_iter"],
            train_state=train_state,
        )

分布式训练:

  • 使用多个GPU并行训练
  • DDP(DistributedDataParallel):PyTorch的分布式训练
  • 提高训练速度和处理能力

优化器配置:

  • Adam:自适应学习率优化器
  • betas=(0.9, 0.98):动量参数
  • eps=1e-9:数值稳定性

BLEU评估:

def check_outputs(valid_dataloader, model, vocab_src, vocab_tgt, n_examples=15, pad_idx=2, eos_string="</s>"):
    results = [()] * n_examples
    for idx in range(n_examples):
        print("\nExample %d ========\n" % idx)
        b = next(iter(valid_dataloader))
        rb = Batch(b[0], b[1], pad_idx)
        greedy_decode_result, _ = greedy_decode(model, rb.src, rb.src_mask, 64, 0)
        
        src_tokens = [vocab_src.get_itos()[x] for x in rb.src[0] if x != pad_idx]
        tgt_tokens = [vocab_tgt.get_itos()[x] for x in rb.tgt[0] if x != pad_idx]
        
        print("Source Text (Input)        : " + " ".join(src_tokens).replace("\n", ""))
        print("Target Text (Ground Truth) : " + " ".join(tgt_tokens).replace("\n", ""))
        print("Model Output               : " + " ".join([vocab_tgt.get_itos()[x] for x in greedy_decode_result[0] if x != pad_idx]).replace("\n", ""))
        results[idx] = (rb, greedy_decode_result)
    return results

模型评估的重要性:

  1. 定量评估:BLEU分数等指标
  2. 定性分析:人工检查翻译质量
  3. 错误分析:理解模型的局限性

BLEU分数:

  • 比较机器翻译和参考翻译的n-gram重叠
  • 分数范围0-100,越高越好
  • 业界标准的机器翻译评估指标

单元格 93-100:注意力可视化

注意力权重提取:

def draw_attention(data, layer, head, row, col, ax):
    "Draw attention weights"
    attention = data[layer][0, head].data
    ax.matshow(attention, cmap='Blues')
    
    ax.set_xticks(range(len(col)))
    ax.set_yticks(range(len(row)))
    ax.set_xticklabels(col, rotation=90)
    ax.set_yticklabels(row)

注意力可视化的价值:

  1. 模型解释性:理解模型在关注什么
  2. 调试工具:发现模型的问题
  3. 语言学洞察:揭示语言现象

多头注意力分析:

def visualize_attention(model, vocab_src, vocab_tgt, sentence_idx=0):
    # Set model to evaluation mode
    model.eval()
    
    with torch.no_grad():
        # Get a batch of data
        example_data = check_outputs(valid_dataloader, model, vocab_src, vocab_tgt, n_examples=1)
        example_data_idx = sentence_idx
        
        # Get the source and target sentences
        src_sent = example_data[example_data_idx][0].src[0]
        tgt_sent = example_data[example_data_idx][1][0]
        
        # Convert to tokens
        src_tokens = [vocab_src.get_itos()[x] for x in src_sent if x != 2]  # Remove padding
        tgt_tokens = [vocab_tgt.get_itos()[x] for x in tgt_sent if x != 2]  # Remove padding
        
        print("Source:", " ".join(src_tokens))
        print("Target:", " ".join(tgt_tokens))
        
        # Get attention weights for each layer and head
        attns = run_model_extract_attentions(example_data[example_data_idx][0], model)
        
        # Visualize different attention heads
        for layer in range(6):  # 6 layers
            fig, axes = plt.subplots(2, 4, figsize=(16, 8))
            for head in range(8):  # 8 heads
                draw_attention(attns, layer, head, tgt_tokens, src_tokens, axes[head//4, head%4])
                axes[head//4, head%4].set_title(f"Layer {layer+1} Head {head+1}")

不同注意力头的专门化: 研究发现不同的注意力头学会了不同的语言现象:

  • 语法关系:主谓关系、修饰关系
  • 长距离依赖:跨越多个词的关系
  • 位置信息:相邻词的关系

编码器-解码器注意力:

编码器-解码器注意力显示了:

  1. 对齐关系:源语言和目标语言词汇的对应
  2. 翻译策略:模型如何处理不同的语言结构
  3. 语言差异:德语和英语的语序差异

总结:完整的Transformer实现

我们学到了什么:

1. 模型架构:

  • 注意力机制:Self-attention和Cross-attention
  • 多头注意力:并行处理不同类型的关系
  • 位置编码:为模型提供位置信息
  • 残差连接和层归一化:稳定训练

2. 训练技术:

  • Teacher Forcing:训练时使用真实标签
  • 学习率调度:Warmup和衰减策略
  • 标签平滑:提高泛化能力
  • 梯度累积:模拟大批次训练

3. 实际应用:

  • 数据处理:分词、词汇表构建
  • 批次处理:填充和掩码
  • 分布式训练:多GPU并行
  • 模型评估:BLEU分数和可视化

为什么Transformer如此重要:

1. 并行化:

  • 不像RNN需要顺序处理
  • 可以并行计算所有位置
  • 训练和推理都更快

2. 长距离依赖:

  • 直接连接任意两个位置
  • 避免了RNN的梯度问题
  • 更好地处理长序列

3. 可解释性:

  • 注意力权重提供了解释
  • 可以看到模型关注什么
  • 有助于调试和理解

4. 可扩展性:

  • 架构简单但功能强大
  • 可以轻松调整层数和维度
  • 为GPT、BERT等奠定基础

后续发展:

这个基础的Transformer架构启发了:

  • BERT:双向编码器表示
  • GPT:生成式预训练Transformer
  • T5:Text-to-Text Transfer Transformer
  • 更多变体:优化效率和性能

通过这个详细的解释,你应该对Transformer有了深入的理解。从基础的Python语法到复杂的注意力机制,每一个组件都有其重要的作用。这为理解现代自然语言处理的基础打下了坚实的基础!


建议的学习路径:

  1. 先理解概念:每个组件的作用和原理
  2. 动手实践:运行代码,修改参数
  3. 可视化分析:观察注意力权重
  4. 尝试改进:调整架构或训练策略
  5. 应用扩展:尝试其他任务和数据集

记住,深度学习是一个实践性很强的领域,理论理解和动手实践同样重要!

Share