在本节课中,我们将要学习生成式人工智能(Generative AI)的宏观定义,探讨其与人工智能其他子领域的关系,并深入两个核心的技术基础:自动微分(Autodiff)和循环神经网络语言模型(RNN-LMs)。我们将从概率建模的基本思想出发,理解现代生成模型是如何构建和训练的。
欢迎来到生成式AI课程。我们非常期待本学期与大家一起探索,不仅学习知识,更期待看到大家运用所学创造出有趣的项目。
首先,我们思考一个宏观问题:什么是生成式AI?在传统的人工智能课程中,我们常从感知、推理、控制、规划、沟通、创造、学习等子目标开始。这些目标似乎与生成式AI关系不大。然而,生成式AI的视角为这些传统目标提供了新的解读。
- 沟通:涉及语言的理解与生成,而大语言模型(LLMs)在这两方面都表现出色,尽管它们主要被训练用于生成。
- 学习:传统上被视为参数估计,但生成式AI展示了“上下文学习”(in-context learning)的新范式,这里的“学习”发生在推理阶段,而非参数更新。
- 推理:通过“思维链”(chain-of-thought)提示等技术,大语言模型能处理复杂的推理任务。
- 规划:已有研究将大语言模型用于具身智能体的规划任务。
- 创造:文生图、文生音乐等模型是显而易见的例子。
- 感知:多模态基础模型可以回答关于图像及其内部文字的问题;扩散模型甚至能被用作零样本分类器。
- 控制:例如“Daydreamer”技术,通过学习经验的生成模型来辅助强化学习。
因此,生成式AI与人工智能的众多子目标都紧密相关,并且其重要性日益增长。当然,关于智能机器的定义、目标实现以及评估方法,仍存在许多未解之谜。
大家可能已经见过许多生成式AI的应用实例。

- 文本生成:例如,让GPT-4以莎士比亚戏剧的风格,通过两个角色对话的形式来证明“存在无限多个素数”。
- 图像编辑:这创造了一种全新的图像编辑方式,例如,可以抹去图像的一部分并由模型填充,或让模型为黑白图像上色。
- 音乐生成:虽然仍是极具挑战性的问题,但已有如MusicGen这样的模型,基于音频的离散表示,使用Transformer解码器生成多层乐器音轨。
- 代码生成:一个经典例子是GPT-4生成LaTeX代码来绘制独角兽。LaTeX本身调试困难,而TikZ(LaTeX的绘图包)更甚,但GPT-4能逐步生成可工作的代码。
- 视频生成:最近的研究将潜在扩散模型应用于生成多帧相关图像,通过时间对齐技术(如保持噪声的时间相关性,或使用跨时空的卷积与注意力层)将这些帧组合成视频。


构建强大的生成式AI模型面临诸多挑战。

- 数据规模与混合:例如,开源数据集“The Pile”包含约1.2万亿个词元(token),来源多样,包括医学文献、法律文本、学术论文、网页、代码和电视字幕等。如何选择和混合数据是当前的核心挑战之一。有观点认为,某些领先模型(如GPT-4)的优势部分源于其获取了未公开的专有数据。
- 对齐与引导:仅靠概率训练,模型的行为可能不符合人类期望。因此,需要研究如何将奖励模型注入训练循环,以引导模型学习特定的行为方式。
- 内存与效率:现代模型的参数量巨大,如何将其高效地装载到有限显存(如80GB的A100 GPU)中,并理解GPU内存层次、访问速度等系统层面知识,对模型高效运行至关重要。
- 分布式训练与成本:模型的分布式训练及其高昂的成本也是必须面对的问题。

生成式AI的一个有趣趋势是语言和视觉领域的融合与统一。


2017年Transformer架构的发明是一个关键转折点。此后,语言建模领域迅速从RNN转向Transformer。计算机视觉领域虽然转向稍慢(直到2021年Vision Transformer出现),但也已广泛采用Transformer。
这种统一使我们能够讨论跨越不同模态的通用技术。以前,语言领域多用RNN,视觉领域多用CNN,技术迁移较慢。现在,基于Transformer,我们可以更快地将一个领域的洞见应用到另一个领域,并处理多模态任务。这对本课程非常有利,因为我们可以利用这些共性,加速学习不同生成技术的步伐。
大家可能已经对学习生成式AI的内部原理有了自己的理由。这里提供一个比喻:回顾汽车发明之初,内燃机是新技术。亨利·福特曾设想让每个美国人都拥有一辆汽车。汽车带来了便利,但也导致了交通拥堵、空气污染和行人伤亡等问题。
有人设想,如果我们拥有一支由自动驾驶电动汽车组成的车队,或许能解决所有问题。但这需要工程师深刻理解汽车的内部工作原理,才能进行改造。
生成式AI或许不像内燃机那样具有划时代意义,但它同样蕴含着巨大的潜力,既可能带来积极影响,也可能引发问题。因此,大家需要深刻理解其工作原理,这样才能在有人提出从A点前往B点时,有能力判断方向是否正确,或许我们应该前往C点,以避免重蹈污染环境、危害安全的覆辙。

从根本上说,生成式AI就是概率建模。其核心是定义下一个观测值 X_{t+1} 在给定所有历史观测值 X_{1:t} 条件下的概率分布:

P(X_{t+1} | X_{1:t})
我们将花费大量时间思考如何定义这个概率分布,以及如何有效地学习它。
在深度学习时代,我们倾向于建模所有可能的变量交互,让每个变量都依赖于之前的所有变量。我们摒弃了传统的条件独立性假设,转而使用像RNN语言模型这样的架构,它直接基于所有上文进行条件建模,没有任何条件独立性假设。
本学期,我们将遵循以下路线图进行学习:
- 文本生成模型:从基础开始。
- 图像生成模型:扩展生成能力到视觉领域。
- 大模型适配与高效方法:学习如何高效地微调和适配大型模型。
- 多模态基础模型:探索跨越文本、图像等模态的模型。
- 扩展性挑战:讨论与系统限制相关的算法问题。
- 模型的潜在问题:分析生成模型可能出现的错误与偏见。
- 高级主题:根据时间安排,探讨如3D建模等前沿话题。


上一节我们概述了生成式AI的广阔图景,本节中我们来看看支撑现代深度学习模型训练的一项关键技术:自动微分。
反向传播(Backpropagation)是训练神经网络的核心算法。它涉及对计算图进行前向传播和反向传播两次遍历。在反向传播中,我们利用链式法则计算梯度。高效的实现方式(反向模式自动微分)不是直接展开链式法则,而是在访问每个节点时,增量式地累加梯度到上游变量。这种方式代码更简洁,可复用性高。
早期,我们需要为每个特定的神经网络手动编写前向和反向传播代码,冗长且难以复用。模块化自动微分(Module-based Autodiff)通过将计算组件化为“层”或“模块”来解决这个问题。每个模块实现 forward 和 backward 方法。forward 计算输出,backward 根据输出的梯度计算输入的梯度。
更进一步的优化是引入“磁带”(tape)机制。在前向传播时,每个被调用的模块及其输入输出被记录在“磁带”(一个栈)上。在反向传播时,只需调用 tape.backward(),系统便会按相反顺序弹出模块并调用其 backward 方法,自动完成梯度计算和传递。PyTorch正是这样实现的。
在PyTorch中,我们通过定义继承自 nn.Module 的类来构建模型。其 __call__ 方法(使得对象可以像函数一样被调用)内部会调用 forward 并处理梯度磁带等簿记工作。参数被存储在模块内部,并由优化器(如SGD)自动管理。这使得深度学习变得非常容易,但理解其背后的原理能帮助我们更有效地使用它。
掌握了自动微分工具后,我们现在可以开始构建真正的生成模型。本节我们来看看文本生成的两个经典方法:N-gram语言模型和循环神经网络(RNN)语言模型。
最简单的生成数据方法是使用N-gram语言模型。其思想是基于前 n-1 个词来采样第 n 个词。例如,一个二元语法(bigram)模型假设下一个词 w_t 只依赖于前一个词 w_{t-1},即做出了条件独立性假设。这些概率可以通过在文本数据中简单计数来估计。虽然简单,但生成的文本(例如基于莎士比亚作品训练的模型)质量通常不高。
循环神经网络语言模型(RNN-LM)则强大得多。它利用链式法则将整个序列的概率分解为一系列条件概率的乘积:
P(w_1, w_2, ..., w_T) = Π_{t=1}^{T} P(w_t | w_{1:t-1})
关键创新在于,RNN充当了一个函数 f_θ,能够将任意长度的历史词序列 w_{1:t-1} 编码成一个固定长度的向量表示 h_t。然后,我们基于这个向量表示来定义下一个词的概率分布:
P(w_t | w_{1:t-1}) = P(w_t | h_t)
具体实现时,h_t 由RNN单元计算得出:h_t = H(W_{xh} x_t + W_{hh} h_{t-1} + b),其中 H 是非线性激活函数。x_t 是当前词 w_t 的向量表示(如词嵌入)。得到 h_t 后,通过一个线性层再接一个Softmax函数,即可得到在词汇表上的概率分布。
训练时,我们最大化真实序列的似然概率。生成(采样)时,过程类似于N-gram模型:给定起始符号,RNN产生第一个词的概率分布,我们从中采样一个词;然后将采样得到的词作为下一步的输入,重复此过程,即可自回归地生成整个序列。
实践证明,即使在相同数据(如莎士比亚全集)上训练,RNN语言模型生成的文本质量也远高于N-gram模型。这引出了一个关键问题:如果我们拥有更强大的模型和更海量的数据,能实现什么?我们将在后续课程中深入探讨。
本节课中我们一起学习了生成式AI的广泛定义及其与AI各子领域的联系,并通过实例了解了其多样化的应用。我们深入探讨了构建生成模型的核心——概率建模思想,并介绍了本学期的学习路线图。
在技术层面,我们掌握了自动微分(Autodiff)的原理,特别是PyTorch如何利用“磁带”机制实现高效的反向传播,这是训练所有现代深度学习模型的基础。最后,我们对比了文本生成的两种方法:基于统计的N-gram语言模型和基于神经网络的RNN语言模型,理解了RNN如何通过将变长历史编码为固定长度向量来更有效地建模序列概率,从而生成更高质量的文本。

这些基础知识为我们后续深入Transformer、扩散模型等更先进的生成技术奠定了坚实的基石。

在本节课中,我们将学习Transformer语言模型的核心原理。我们将从循环神经网络(RNN)的局限性开始,探讨Transformer架构如何解决这些问题,并详细介绍其核心组件——注意力机制、多头注意力、层归一化、残差连接以及位置编码。最后,我们将了解如何将这些组件组合成一个完整的Transformer语言模型。

在Transformer于2017年发明之前,自然语言处理领域主要依赖语言模型完成语音识别和机器翻译等任务。其核心思想是构建一个噪声信道模型,该模型结合了转导模型和语言模型。
公式:P(y|x) ∝ P(x|y) * P(y)
其中,P(y)是语言模型,P(x|y)是转导模型。目标是找到使该乘积最大化的y。
早期,人们使用N-gram模型。例如,谷歌在2006年发布的英语N-gram模型使用了1万亿个词元(约950亿个句子)进行训练,包含从1-gram到5-gram的统计信息,总计约30亿个参数。
如今的大型语言模型规模已远超从前:
- GPT-2 (2019): 15亿参数,100亿词元训练数据。
- GPT-3: 1750亿参数,3000亿词元训练数据。
- PaLM: 5400亿参数,近1万亿词元训练数据。
- Chinchilla: 参数减少但训练数据超过1万亿词元,证明了“更多数据,更少参数”的有效性。
- GPT-4: 推测其核心模型参数在数千亿级别。
上一节我们介绍了RNN语言模型。本节中,我们来看看RNN面临的一个核心挑战:难以学习长距离依赖关系,这本质上是“遗忘”问题。
为了理解这一点,我们考虑一个简单的Elman RNN。其隐藏状态H_t和输出Y_t的计算如下:
公式:
H_t = σ(W_hh * H_{t-1} + W_hx * X_t + b_h)
Y_t = σ(W_hy * H_t + b_y)
假设我们设置W_hh为单位矩阵,W_hx为0.5倍单位矩阵。如果输入序列在早期出现了关键信息比特,随着时间步的推进,这些信息在隐藏状态中的强度会不断衰减(例如,乘以0.5)。最终,在足够多的时间步之后,模型将“遗忘”这些早期信息,导致无法基于它们做出正确预测。
这种现象在训练中表现为梯度消失问题:在反向传播时,梯度需要跨越许多时间步进行连乘,导致其对早期输入的梯度变得极其微小,使得模型难以学习长距离的依赖关系。
为了解决RNN的遗忘问题,长短期记忆网络(LSTM)被提出。LSTM通过引入“门”机制来控制信息的流动。
LSTM单元在每一个时间步包含以下核心组件:
- 输入门 (i_t):控制当前输入有多少信息需要被存入细胞状态。
- 遗忘门 (f_t):控制上一个细胞状态有多少信息需要被保留或遗忘。
- 输出门 (o_t):控制当前细胞状态有多少信息需要输出到隐藏状态。
- 细胞状态 (C_t):代表网络的长期记忆。
其计算过程可以概括为:
- 基于当前输入
X_t和上一隐藏状态H_{t-1},计算三个门的激活值。 - 计算一个候选细胞状态
~C_t(类似于简单RNN的隐藏状态更新)。 - 更新细胞状态:
C_t = f_t ⊙ C_{t-1} + i_t ⊙ ~C_t。这里⊙表示逐元素相乘。 - 计算当前隐藏状态:
H_t = o_t ⊙ tanh(C_t)。
通过让遗忘门接近1,LSTM可以选择性地将信息长期保存在细胞状态中,从而缓解梯度消失问题。然而,LSTM仍然存在计算 inherently sequential(无法高效并行化)的问题,并且在某些情况下仍可能面临梯度爆炸的挑战。
Transformer架构通过注意力机制从根本上解决了长距离依赖和并行化的问题。注意力机制的核心思想是:在生成当前词元的表示时,直接查看并汇总序列中所有之前词元的信息,而不是像RNN那样必须通过一系列中间状态传递。
注意力机制的计算可以分为以下几步:
1. 创建查询、键和值
首先,为每个输入向量X_i创建三个新的向量表示:
- 查询 (Query, Q_i):代表当前词元在“寻找什么”。
- 键 (Key, K_i):代表每个词元“有什么特征”可供匹配。
- 值 (Value, V_i):代表每个词元实际要被汇总的“信息内容”。
2. 计算注意力分数
对于当前位置t,我们将其查询向量Q_t与所有之前位置的键向量K_i进行点积,来衡量t与i之间的相关性。为了稳定训练,点积结果会除以键向量维度d_k的平方根。
公式:score(t, i) = (Q_t · K_i^T) / sqrt(d_k)
3. 计算注意力权重
将上一步得到的分数通过Softmax函数转换为一个概率分布,即注意力权重。它表示在生成当前位置表示时,应该“关注”之前每个位置的程度。
公式:attention_weight(t, i) = softmax(score(t, i))
4. 生成输出表示
将注意力权重作为系数,对所有的值向量V_i进行加权求和,得到当前位置的最终输出表示X‘_t。
公式:X‘_t = Σ(attention_weight(t, i) * V_i)
这种机制的优势在于,无论两个词元相距多远,它们之间的关联计算都是直接的,不存在信息衰减。
单一的注意力机制可能只捕捉到一种类型的依赖关系。为了让模型能够同时关注来自不同表示子空间的信息,Transformer使用了多头注意力。
以下是多头注意力的实现步骤:
- 使用
h组不同的(W_Q, W_K, W_V)矩阵,将输入X并行地投影到h组查询、键、值。每一组称为一个“头”。 - 每个头独立地执行上一节介绍的缩放点积注意力计算,产生
h个输出序列。 - 将这
h个输出序列在特征维度上拼接起来。 - 最后通过一个线性投影层
W_O将拼接后的结果映射回预期的输出维度。
通过这种方式,模型可以同时学习到词语之间多种不同的关系模式(例如语法关系、语义关系等)。
一个完整的Transformer层不仅仅包含多头注意力。为了稳定和加速深度网络的训练,它还集成了以下组件:
层归一化
深度网络中,底层参数的微小变化可能被放大,导致高层输入分布发生剧烈变动(内部协变量偏移),不利于训练。层归一化对每个样本的每一个层输出进行标准化,使其均值为0,方差为1,然后应用可学习的缩放和偏移参数。
公式:LayerNorm(x) = γ * (x - μ) / σ + β
其中μ是均值,σ是标准差,γ和β是可学习参数。
残差连接
极深的网络有时会出现性能退化问题(训练和测试误差同时上升)。残差连接通过将层的输入直接加到其输出上来解决这个问题。这样,层只需要学习输入与期望输出之间的残差(即变化部分),使得深层网络更容易训练。
公式:Output = LayerNorm( Attention(x) + x )
前馈神经网络
每个Transformer层在注意力子层之后,还包含一个全连接的前馈神经网络。它通常由两个线性变换和一个激活函数组成,作用在每个位置独立且相同。
公式:FFN(x) = max(0, xW_1 + b_1)W_2 + b_2
注意力机制本身是置换不变的:打乱输入序列的顺序,只要注意力权重相应调整,输出表示可以保持不变。但这显然不符合语言特性(“猫追老鼠”和“老鼠追猫”意思不同)。
为了解决这个问题,我们需要向模型注入序列中词元的位置信息。最常用的方法是位置编码。
绝对位置编码:为序列中的每个位置t学习或定义一个固定的向量P_t。然后将词嵌入向量E_t与位置编码向量相加,作为Transformer的输入。
公式:X_t = E_t + P_t
这样,即使两个词相同,只要它们处于不同位置,其输入表示就会不同。除了可学习的位置嵌入,还有一种经典的正弦/余弦函数编码方案。
对于语言模型任务,我们在预测下一个词时,只能使用当前及之前的词信息,不能“偷看”未来的词。这需要通过因果注意力掩码来实现。
具体做法是:在计算注意力分数后,Softmax操作之前,将所有未来位置(j > i)的分数设置为一个极大的负数(如-1e9)。这样,经过Softmax后,这些位置的注意力权重就会变为0。
矩阵化实现
在实际编程中,我们使用矩阵运算来高效实现上述过程。假设输入序列矩阵X的形状为[序列长度, 模型维度]:
Q = X @ W_Q,K = X @ W_K,V = X @ W_V- 注意力分数矩阵:
S = (Q @ K^T) / sqrt(d_k) - 加上因果掩码矩阵
M(上三角为-inf):S_masked = S + M - 注意力权重矩阵:
A = softmax(S_masked, dim=-1) - 输出矩阵:
Output = A @ V
多头注意力则是将这个过程并行执行h次,然后将各头的输出拼接,最后通过线性层W_O投影。

本节课中,我们一起学习了Transformer语言模型的基础知识。我们从RNN的遗忘问题和LSTM的改进入手,引出了Transformer的核心创新——自注意力机制,它能够直接建模任意距离的词元依赖关系。我们详细剖析了缩放点积注意力、多头注意力的原理与优势。接着,我们了解了构建一个稳定、可深度堆叠的Transformer层所必需的其他组件:层归一化、残差连接和前馈网络。最后,我们探讨了为注意力机制注入位置信息的方法(位置编码),以及如何通过因果注意力掩码确保语言模型的自回归特性,并简要介绍了其高效的矩阵化实现方式。这些构成了现代大型语言模型(如GPT系列)的基石。下一节课,我们将探讨如何训练这些模型,并介绍一些更先进的变体。


在本节课中,我们将要学习如何训练一个大型语言模型。到目前为止,我们几乎没有讨论过学习过程,这对于一门机器学习课程来说有些特别。现在,我们将深入探讨之前提到的模型的学习方面。
首先,提醒一下,作业零的截止日期是今天。如果你需要额外时间,请注意不能使用宽限期。我们不希望你将宽限期浪费在作业零上,而是希望你按照教学大纲中的说明申请延期。通常延期适用于紧急情况,但这次是特殊情况。
作业一将于近期发布,预计在周四,截止日期大约在两周后。
关于如何获得5%的课堂参与分,我们将通过一些非作业性质的活动来分配,例如投票、调查以及项目过程中的不同环节。
我注意到一些同学反馈,深度学习内容进展很快,感到有些跟不上。我们并不要求你预先掌握深度学习知识。根据你之前课程的不同,对神经网络的讲解深度也有所不同。如果你感到困惑,可以在课后与我交流,或者复习一下2023年秋季课程的神经网络模块,这里有幻灯片和视频的链接。如果你已经上过我的课程但仍感到困惑,也请来找我,我们一起解决。
在深入之前,我们先简要回顾一下到目前为止的内容。
我们介绍了基于模块的自动微分概念。其核心思想是,存在称为“模块”的对象,它们定义了前向和后向方法,并且可以链接在一起形成一个计算图。自动微分就是计算该计算图输出(通常是损失)相对于图中叶子节点(通常是参数)的梯度。
我们定义了一个非常有趣的计算图,称为RNN语言模型。随后,我们又定义了一个更复杂的计算图,称为Transformer语言模型。
我们还讨论了语言建模的各个方面,这可以说是一个完全不同的主题。本质上,到目前为止,我们将自动微分视为计算梯度的工具,特别是针对可微函数。如果F是某个可微函数,那么最终我们可能只需要在前向方法中通过链接现有模块来编写代码,甚至不需要实现后向方法,因为每个模块都已经实现了后向方法,我们可以通过精心使用计算图来依赖它们。

计算图是定义函数F的另一种方式,它更便于在幻灯片上展示。到目前为止,我们已经看到了两种计算图,可以说Transformer语言模型是一种相当复杂的计算图,它无法在一张幻灯片上完整展示。但当我们将其模块化后,就可以分解展示。
对于语言建模,其核心思想是,我们根据前面的单词来采样下一个单词。另一种说法是,语言模型定义了序列上的概率分布,或者通过定义给定先前单词的下一个单词的概率分布来实现。
到目前为止,我们看到的两种实现方式是N-gram语言模型和神经网络语言模型(RNNLM或Transformer LM)。N-gram语言模型可以看作是一个巨大的、拥有50,000面的骰子集合(因为词汇表有50,000个单词),每次你选择适当的骰子并投掷以得到下一个单词。相比之下,RNNLM或Transformer LM使用神经网络来定义下一个单词的概率。你可以认为这个神经网络定义了那个50,000面骰子的权重。当我们“投掷”它时,实际上就是从神经网络给出的概率分布中采样。

关于学习,我们之前提到N-gram语言模型的学习很简单,只需统计共现次数。但到目前为止,我们实际上完全没有讨论如何学习RNNLM或Transformer LM。你可能已经推断出我们要做什么,但今天我们将详细展开这个话题。
本质上,到目前为止我们讨论了两件事:一是如何概念化语言建模的思想,二是如何定义一些深度学习模型。现在,我们终于要思考如何学习这些模型。此外,我们今天还将讨论如何实际从这些语言模型中解码或获取输出。
首先,我们快速回顾一下神经网络的视觉化术语和通用学习流程。
我们熟悉的机器学习流程是:给定一些训练数据,然后选择一个决策函数和一个损失函数,接着定义一个目标,即最小化经验风险(将损失应用于我们的预测和真实标签),最后使用随机梯度下降法,沿着梯度的反方向更新参数。
对于一个单隐藏层神经网络,反向传播可以模块化地思考,也可以过程化地思考。前向计算从底部开始,向上进行,计算线性层、Sigmoid层、另一个线性层、另一个Sigmoid层,最后是交叉熵损失。反向计算则从顶部开始向下进行,每一步计算一些偏导数。如果J是我们的损失,那么这些g_b就是J相对于b的导数,我们将其存储在g_b中。
自动微分的意义在于,我们现在可以自动完成整个反向传播过程。虽然可以说,确实有人必须为Sigmoid层、线性层和交叉熵层写出反向传播的步骤。
如果你想训练那个单隐藏层神经网络,你会按照以下步骤进行随机梯度下降:初始化网络参数(例如alpha和beta,其中alpha是第一层的参数矩阵,beta是第二层的参数矩阵)。对于每个训练轮次E,你遍历训练数据D中的每个训练样本(即XY对)。然后执行两个步骤:前向计算和反向计算。前向计算给出所有中间层的结果,反向计算给出参数的梯度(g_alpha和g_beta),我们将使用这些梯度进行随机梯度下降更新:alpha更新为alpha减去学习率gamma乘以g_alpha,beta更新为beta减去gamma乘以g_beta。在此过程中,通常最好跟踪训练和验证数据的平均交叉熵损失。最后,返回一些参数alpha和beta,不过如果你更聪明,可能会跟踪对应于**验证交叉熵的参数并返回它们。
为了快速对比,我一直在讨论随机梯度下降,它每次只处理一个训练样本。但通常,出于多种原因,使用小批量同时处理多个训练样本会更好。原因可能有两个:一是减少方差,二是提高效率。
在随机梯度下降中,我们只选择一个训练点(即单个x_i, y_i对)。而在小批量SGD中,我们首先将所有训练样本(假设有N个)随机分成批次。这里假设有B个批次,每个批次I1, I2, ..., IB大小相同(大小为M)。所有小批次的并集构成完整训练集,交集为空集(因为它们通常不重叠)。最后,当我们想要实际训练或优化目标时,我们将迭代时间步T,然后迭代小批次B。我们会选择下一个批次I_B(假设其大小为M,可能是16或64,通常根据GPU内存能容纳的训练样本数量来决定,并尽可能选择2的幂次方大小)。然后我们计算梯度,现在不是针对一个样本,而是小批次中梯度的平均值。对于小批次I_B中的每个训练样本i,我们累加目标函数或损失在样本i上的梯度。前面的1/M是为了求平均。然后我们更新参数:新参数等于旧参数减去学习率乘以小批次的梯度。最后,我们在每个训练轮次结束时评估平均训练损失。
我们将使用这种训练神经网络的基础思想来训练另一个神经网络——Transformer语言模型。但在开始之前,我认为有必要暂停一下,思考我们是如何学习N-gram语言模型的。
如果我们还记得,N-gram模型的概率是通过统计N-gram频率来学习的。你查看所有“cow eat”的情况,然后统计每个可能后续单词出现的次数。实际上,这种计数方法就是N-gram模型的最大似然估计。
你如何得出这种计数方法?你会按照通常的方式写下N-gram语言模型下句子的似然。N-gram语言模型中实际使用的概率分布是什么?实际上只有一个概率分布,我们只是用不同的参数反复使用它。它是什么?伯努利分布几乎正确,但伯努利分布就像抛硬币,只给我们两个单词,而我们需要更多单词。如果我们的词汇表只有“cat”和“dog”,那么它就是伯努利分布,但我们只能得到像“cat cat, dog dog dog”这样的句子。多项分布?是的,我更喜欢将其称为分类分布,因为通常定义的多项分布允许你从中抽取多个样本,而我们每次只抽取一个样本(下一个单词),所以它是多项分布的一个特例。
当你写下N-gram语言模型下句子的似然时,你得到的是许多多项概率的乘积,每个单词对应一个。这给出了一个似然值。你可以将这个似然设为目标函数,然后计算该似然的梯度。但这里有个棘手的问题:你们中有多少人求解过多项分布的最大似然估计?哦,真的吗?不多?嗯,我们应该把这个放到作业里。多项分布的最大似然估计很有趣,因为它有一个约束条件:概率之和必须为1。这个约束意味着你不能简单地将梯度设为零然后求解参数。如果你那样做,得到的答案会是所有参数都应该是正无穷。你需要一个约束条件,即当参数(概率)相加时不能超过1。当你以闭式求解这个约束优化问题时,你得到的就是这些计数作为最大似然估计的答案。
考虑到这一点,如果我们训练一个深度神经语言模型,我们也可以使用最大似然估计。这就是我们将对RNNLM和Transformer LM所做的,但不是以闭式形式。我们将遵循我们刚刚看到的普通神经网络的流程:写下深度神经语言模型下句子的(负)对数似然,然后通过自动微分计算一个小批量样本的梯度,接着使用小批量随机梯度下降(或者更实际地说,使用Adam优化器,因为几乎每个人都这么做)沿着负梯度方向更新。
那么,我们如何从循环神经网络语言模型中实际得到似然呢?这里有一个基本的Elman网络图,它是我们一个有用的构建模块,因为它允许我们讨论完整的计算图,而无需达到Transformer的复杂程度。
这里我们看到一些伪代码,类似于你会放入RNN前向方法中的内容。我们有隐藏状态(称为h_0,初始化为全零)。然后对于每个时间步T(注意:这些时间步不要与训练中的随机梯度下降时间步混淆,它们实际上是输入序列中的位置,例如一个句子“The cat saw food”中的单词位置)。我们接收时间步T的输入数据x_t,然后计算隐藏状态:h_t = σ(W_h * h_{t-1} + W_x * x_t + b)。其中W是矩阵,h、x和b是向量,σ是逐元素的激活函数。接着我们计算时间步T的输出:y_t = W_y * h_t + b_y。
问题来了:这些W矩阵和b向量从哪里来?在训练开始时,例如小批量SGD的第一次迭代,所有这些W和b(统称为参数θ)通常是随机初始化的。所以第一次调用前向计算时,这些参数是随机数。第二次调用时呢?它们来自小批量SGD更新后的当前θ值。第1000次调用时,它们可能已经是能够执行有用任务的参数表示了,因为我们已经进行了1000步小批量SGD更新。
我们可以对RNN做一个改动:将第9行(y_t = W_y * h_t + b_y)替换为y_t = softmax(W_y * h_t + b_y)。现在,我们的y_t从任意向量变成了概率分布。例如,你可能试图预测句子中每个单词的词性标签。假设句子是“The cat saw donuts”,我们试图预测的词性标签是形容词、限定词、名词、动词。那么在每个时间步,我们都有一个在形容词、限定词、名词、动词上的概率分布。
现在,假设你想为这个RNN计算损失函数。对于句子“The cat saw donuts”,我们需要一个黄金词性标签序列:The是限定词(D),cat是名词(N),saw是动词(V),donuts是名词(N)。所以黄金序列y_1, y_2, y_3, y_4 是 D, N, V, N。
当我们想要计算交叉熵损失时,我们在每个时间步T计算该标签的损失。因为每个y_t都是A, D, N, V上的概率分布,所以每个小损失L_t告诉我们模型为真实标签(例如y_1*是D)分配的概率是多少。你可以将其想象为:在第一个时间步,它挑出了蓝色小条(对应D的概率),放入L_1的盒子;第二个时间步,它挑出红色小条(对应N的概率),放入L_2的盒子,依此类推。实际上,它还应用了对数,所以不仅仅是放入概率值,而是放入概率的对数。
这里有一个重要的实现细节:在幻灯片上这样写是正确的,但在实际实现中,我们永远不会这样做。在作业零中,我们实际上揭示了一些重要的东西。有人看出我们在这里做了什么在实际实现中不会做的事情吗?是的,softmax。我们实际上不会计算softmax,而是直接使用logits(即输入到softmax的值)工作,因为这样交叉熵损失计算根本不需要处理概率,效率更高。
下一个问题是:我们如何将其用于语言建模损失?目前我们计算的损失函数是针对结构化预测问题(如词性标注)的。但这看起来与我们之前的语言建模问题非常不同。在这种情况下,我们定义的是给定向量X条件下向量Y的概率分布P(Y|X)。这是我们习惯的典型的判别式机器学习,而不是生成式问题。它可能比分类更有趣,因为我们实际上同时进行多个分类。
那么,我们如何将其用于语言建模?我认为我们根本不需要改变算法1。是的,你可以直接预测下一个单词,而不是预测D, N, V, N。只需小心处理输入。W_0将是x_1,而W_0始终是特殊的开始标记,告诉我们RNN开始了。这意味着我们的输入x_1是这个开始标记。然后我们得到单词1的概率分布,这就是我们称为向量y_1的东西。现在,单词1的正确标签就是那个单词本身。同样,如果我们使用之前的句子“The cat saw donuts”,那么单词“The”就是存储于y_1中的概率分布的真实标签。然后我们取单词“cat”作为应该跟在“The”后面的东西的真实标签,从而成为应该从这个概率分布中挑出的东西,依此类推。
你会注意到,我们实际上从未输入单词4。这感觉有点不对称。但我们在这里定义的是单词1、2、3、4的正确概率分布。如果你真的对此感到不舒服,并且真的想输入单词4,你如何改变它以便实际输入单词4到语言模型?是的,你可以添加一个特殊的结束标记。然后我们可以在这里添加一个框,即W_4,它被输入,而对于W_4,我们基于某个W_5(一个特殊的结束标记)计算L_5。这样,你就稍微改变了概率分布,因为现在你说的是计算W_1到W_T的概率,以及W_{T+1}(结束标记)的概率。所以结束标记现在是概率分布的一部分,因为我们生成的最后一个东西是结束标记。
这是一个很好的问题:直方图与单词概率分布有什么关系?实际上,我的图在这里不适用,因为实践中你的词汇表可能有大约50,000个单词,而不是四个。所以实际上,这个直方图应该是针对50,000个单词的直方图。在这种情况下,你可以想象y_t向量实际上是一个50,000维的向量。但由于它是softmax的输出,我们知道它所有值都是正的,并且这些正值之和为1。所以它是在50,000个不同单词上的概率分布。同样,如果我们将其绘制为直方图,幻灯片上没有足够空间,但每个概率分布实际上就是这样的。关键的是,每个时间步的这些概率分布看起来都会有点不同。例如,在输入“The cat”之后,动词的概率分布将对词汇表中所有动词赋予很高的概率。
让我们回到非语言建模的故事。当我们不做语言建模,只是用词性标签标注句子时,我们输入“The cat saw donuts”。在每个时间步,我们得到这个h,它是所有直到当前单词的表示。所以如果你试图标注单词“The”,那么h_1是单词“The”的某种固定大小的向量表示,我们用它来获取该单词可能词性标签的概率分布。同样,当我们得到h_3时,这是“The cat saw”的表示,它将为我们提供单词“saw”可能词性标签的概率分布。
然而,当我们进行语言建模时,我们需要更小心地处理输入方式。让我们思考一下,如果我们没有这一列(指输入列),会发生什么。如果我们去掉整个输入列,我们会定义什么概率分布?假设你为隐藏状态输入了相同的东西,那么它将是给定W_1条件下,W_2到W_T的概率。因为现在我们所做的是:我们乘以给定h_2条件下单词2的概率、给定h_3条件下单词3的概率、给定h_4条件下单词4的概率。但我们没有单词1的概率了。所以我们只剩下单词2、3、4的概率相乘,我们有一个在2、3、4上的分布,但1被遗漏了。因此,我们需要这个额外的特殊开始标记,以便我们实际上获得第一个单词的概率分布。
所以,学习RNN语言模型本质上如下:每个训练样本是一个序列。如果你在进行真实的人类语言建模,那么这个序列就是一个句子。尽管在实践中,可能不是句子,而是一个被分成1024个标记的单词序列。
我们的训练数据由N个句子或序列组成,其中每个序列由一系列单词组成。那么,深度神经语言模型(可以是RNNLM或Transformer LM)的目标函数通常是实际训练样本的对数似然。整个数据集的完整似然就是对所有N个训练样本求和,对每个样本,我们加上模型下第i个句子的概率的对数。然后我们通过小批量随机梯度下降(或你喜欢的任何优化器)进行训练。
我们讨论的所有内容都是为了训练循环神经网络语言模型。但那不是我们想做的,我们想训练Transformer语言模型。现在我们需要完全转变思路,思考如何训练Transformer语言模型。方法如下:你可能错过了,幻灯片上唯一的变化是标题和这个灰色框。仅此而已。因为Transformer语言模型只是为我们定义了一个不同的计算图。归根结底,我们上次花了所有时间讨论的那个庞大、复杂的Transformer语言模型,只是为我们定义了一系列下一个单词的概率。它通过注意力、层归一化、残差连接和前馈神经网络等全部堆叠在一起实现,但最终它所做的就是为我们定义这些下一个单词的概率分布。因此,无论我们使用RNNLM还是Transformer LM,训练算法看起来完全一样,唯一的区别是我们实际使用哪个可微函数来构建这些概率分布。
现在你可以明白为什么我们花这么多时间看RNNLM了,因为我们可以把所有内容塞到一张幻灯片上。我无法将Transformer的所有内容塞到一张幻灯片上,但我想你现在已经有了从单词到概率的图景——那就是Transformer语言模型,它是一个庞大复杂的可微函数。你现在也有了另一个图景:如果你有了这些概率,如何为你的数据集获得合适的损失函数。

事实证明,尽管当前最先进的语言模型确实依赖于Transformer模型,但RNN实际上构成了早期神经语言模型的大部分,并引领了当前最先进的架构。例如,著名的Penn Tree Bank数据集,包含约一百万个单词,被用作测试集。评估语言模型质量的方法之一是询问语言模型:你给一个未见过的单词序列分配多少概率?因为如果一个语言模型从未见过那个单词序列,并且假设Penn Tree Bank主要包含20世纪90年代的《华尔街日报》文章
在本节课中,我们将学习现代Transformer架构的关键改进,并探讨如何将Transformer模型应用于计算机视觉任务。我们将从预训练与微调的概念讲起,然后深入分析几种前沿的Transformer变体,最后介绍卷积神经网络(CNN)的基础知识,为后续理解视觉Transformer打下基础。
上一节我们学习了如何通过随机梯度下降和自动微分来训练语言模型。本节中,我们来看看模型训练中两个核心概念:预训练 与 微调。


现代深度学习架构有着悠久的历史。2006年,预训练思想的出现极大地推动了深度学习的复兴。我们可以通过三种不同的训练范式来理解这一发展:


- 监督微调:直接使用带标签的数据训练整个模型。
- 逐层监督预训练 + 监督微调:逐层贪婪地训练模型,每层都使用监督信号,最后对整个模型进行微调。
- 无监督预训练 + 监督微调:使用无标签数据预训练模型,然后使用带标签的数据进行微调。
2006年的一个关键实验展示了这些范式的效果。在MNIST手写数字分类任务上:
- 使用监督微调的浅层神经网络错误率低于2%。
- 使用相同方法训练的深层神经网络错误率反而更高。
- 采用逐层监督预训练后,深层网络的错误率有所下降,但仍不如浅层网络。
- 最终,无监督预训练 + 监督微调的方法使得深层网络的性能超越了浅层网络,这成为了深度学习复兴的“火花”。
无监督预训练的核心是自编码器。其目标是让模型重建输入数据本身,从而学习到数据的有用表示。
一个自编码器包含两部分:
- 编码器:将输入
x映射到低维表示z。
z = f_encoder(x) - 解码器:从表示
z重建输入x'。
x' = f_decoder(z)
损失函数是原始输入 x 与重建输出 x' 之间的距离(如均方误差)。通过这种逐层贪婪的无监督预训练,模型获得了良好的参数初始化,随后在特定任务(如分类)的带标签数据上进行监督微调,性能得到显著提升。
这一“预训练-微调”范式如今被广泛应用。例如,大型语言模型的生成式预训练就是在海量无标签文本上进行的,目标是最优化观测句子的似然概率。
了解了预训练的基础后,我们来看看几种具有代表性的现代Transformer模型及其核心改进。
以下是近年来一些重要模型及其特点:
- PaLM (2022年10月):5400亿参数,使用了GLU激活函数、多查询注意力、旋转位置编码,并在7800亿token上训练。
- LLaMA 1 (2023年2月):模型规模从70亿到650亿不等,使用RMSNorm、旋转位置编码,在1.4万亿token上训练。
- Falcon (2023年6月):完全开源,使用分组查询注意力、旋转位置编码、GELU激活函数,在3.5万亿token上训练。
- LLaMA 2 (2023年8月):引入了对话微调版本,使用分组查询注意力,上下文长度增至4096,使用2万亿token训练。
- Mistral (2023年10月):70亿参数的小模型,性能超越LLaMA 2 130亿,使用滑动窗口注意力和分组查询注意力,上下文长度超过8000。
从这些模型中,我们可以总结出几个重要的技术趋势:旋转位置编码、分组查询注意力和滑动窗口注意力。
旋转位置编码旨在替代传统的绝对位置编码,将相对位置信息更优雅地整合到注意力计算中。
标准注意力计算查询 q 和键 k 的相似度时,使用点积:
相似度 = (q · k) / sqrt(d_k)
而旋转位置编码在计算时,会考虑查询和键之间的相对距离 m。它通过一个旋转矩阵 R 来变换 q 和 k:
变换后的 q = R(theta, m) * q
变换后的 k = R(theta, m) * k
相似度 = (变换后的 q · 变换后的 k) / sqrt(d_k)
具体实现时,它将高维向量分解为多个二维向量对,并对每个二维向量进行不同角度的旋转。距离 m 越大,旋转角度也越大。这种方法保证了相对距离越远的token,其表示在向量空间中也越“疏远”,比简单的加法位置编码更具几何意义。
分组查询注意力是对标准多头注意力的高效改进,旨在减少计算和内存开销。
在标准多头注意力中,每个头都有独立的查询(Q)、键(K)、值(V)参数矩阵。分组查询注意力则将多个查询头分组,共享同一组键和值头。
假设总共有 H_q 个查询头, H_kv 个键值头(H_q 可被 H_kv 整除),每组大小为 G = H_q / H_kv。计算时,每组内的 G 个查询头共享同一个键头和值头。虽然最终输出的头数减少为 H_kv,但通过调整模型其他部分(如MLP层)的参数,模型整体性能与标准多头注意力相近,同时显著提升了效率。
滑动窗口注意力(又称局部注意力)通过限制每个token只能关注其附近一个窗口内的token,来降低长序列的计算复杂度。
它使用一个加强版的因果掩码。设窗口大小为 W,对于当前位置的token,它只能关注其左侧 W/2 个token、自身以及右侧 W/2 个token(在纯因果掩码中,只关注左侧)。这相当于将全连接的注意力图变成了一个带状矩阵。
关键在于高效实现。朴素实现(使用大矩阵并填充 -inf)无法节省计算。高效的实现需要将序列分块,每个块大小为 W,块之间有重叠,然后对每个块独立进行注意力计算。虽然单层只能看到局部信息,但通过堆叠多层,高层网络能够间接捕获长距离依赖。Mistral模型就成功使用了滑动窗口注意力来处理长上下文。
在深入视觉Transformer之前,我们需要先了解计算机视觉的传统主力——卷积神经网络。
卷积神经网络特别适合图像数据,因为它具有平移不变性:在图像一处学到的模式,可以应用到图像的其他位置。
卷积操作的核心是使用一个小的卷积核(或滤波器)在输入图像上滑动,并进行点积运算。
假设有一个3x3的输入图像 X 和一个2x2的卷积核 Alpha:
X = [[x11, x12, x13], [x21, x22, x23], [x31, x32, x33]]   Alpha = [[a11, a12], [a21, a22]]
输出 Y 是一个2x2的特征图:
y11 = a11*x11 + a12*x12 + a21*x21 + a22*x22 + bias
y12 = a11*x12 + a12*x13 + a21*x22 + a22*x23 + bias
...以此类推,滑动卷积核得到所有输出。



通过设计不同的卷积核,可以手动实现各种图像处理效果,如模糊、锐化、边缘检测等。
CNN的关键突破在于,卷积核的参数不再是手工设计的,而是通过数据学习得到的。模型会自动从数据中学习到有用的特征(如边缘、纹理、形状)。



一个典型的CNN由多种层堆叠而成:
- 卷积层:使用多个可学习的卷积核提取特征,产生多个输出通道。
- 激活层(如ReLU):引入非线性。
- 池化层(如最大池化):进行下采样,减少空间尺寸,增加感受野。
- 全连接层:在网络的末端,将特征图展平后进行分类或回归。
对于彩色图像(3个通道),卷积核也是三维的(宽 x 高 x 输入通道数)。每个卷积核会同时处理所有输入通道,并产生一个输出通道。多个卷积核则产生多个输出通道。
训练CNN与训练其他神经网络一样,使用反向传播和随机梯度下降优化损失函数(如交叉熵损失)。
CNN架构不断向更深、更有效的方向发展:
- LeNet-5 (1998):早期成功的CNN,用于手写数字识别。
- AlexNet (2012):在ImageNet竞赛中取得突破性成功,使用了ReLU激活函数和Dropout。
- VGGNet (2014):通过反复堆叠3x3卷积,构建了更深的网络,结构简洁。
- ResNet (2015):引入了残差连接,解决了极深网络中的梯度消失问题,使得构建数百层的网络成为可能。
小尺寸卷积核(如3x3)的流行,源于其参数效率高,并且通过堆叠多层,高层神经元能够获得很大的感受野,从而捕获图像的全局信息。

本节课中,我们一起学习了现代Transformer模型的几项重要改进:旋转位置编码、分组查询注意力和滑动窗口注意力,它们分别从位置表示、计算效率和长序列处理方面优化了模型。同时,我们也回顾了卷积神经网络的基本原理,理解了其平移不变性和层级特征提取的特点。下一节课,我们将探讨如何将去掉因果掩码的Transformer应用于视觉任务,开启生成式AI在视觉领域的大门。




在本节课中,我们将学习两种重要的生成模型:生成对抗网络及其条件变体。我们将从计算机视觉任务和视觉Transformer的现代视角开始,然后深入探讨如何构建能够生成图像的模型。

上一节我们回顾了Transformer在语言模型中的应用。本节中,我们来看看Transformer如何应用于计算机视觉领域。





计算机视觉包含多种任务,例如:
- 图像分类:给定图像,预测其类别标签。
- 分类与定位:预测类别标签及目标在图像中的边界框位置。这可以看作一个多输出回归问题,输出四个值:
(x, y, 宽度, 高度)。 - 语义分割:为图像中的每一个像素预测一个类别标签。
- 图像描述生成:为图像生成一段文字描述。早期模型通常结合CNN图像编码器和RNN语言模型,后来逐渐转向使用Transformer。





仅编码器Transformer(如BERT)使用非因果注意力机制,即每个位置的表示可以关注输入序列中的所有位置。其预训练通常采用掩码语言建模:随机遮盖输入句子中的部分词语,然后训练模型根据上下文预测这些被遮盖的词语。
视觉Transformer的结构与BERT几乎完全相同,区别仅在于输入处理:
- 将输入图像分割成固定大小的非重叠图像块(例如16x16像素)。
- 每个图像块通过一个线性层被投影为一个向量(例如1024维),作为“词嵌入”。
- 为每个图像块添加一维位置嵌入,以保留其顺序信息。
- 将处理后的序列输入标准的仅编码器Transformer。
- 使用一个额外的可学习“[CLS]”标记(在ViT中称为类别嵌入)的表示,通过一个多层感知机来预测图像类别。
视觉Transformer的预训练通常在大型有标签数据集(如ImageNet)上进行监督分类。尽管其位置嵌入是一维的,缺乏CNN固有的平移不变性等归纳偏置,但通过在大规模数据(数亿图像)上训练,模型可以学习到这些关系,从而取得超越传统CNN的性能。
接下来,我们将焦点转向图像生成。图像生成有多种形式:
- 类别条件生成:给定一个类别标签,生成属于该类别的图像。这是图像分类的逆过程,目标是学习
P(图像 | 类别)。 - 超分辨率:给定一张低分辨率图像,生成对应的高分辨率版本。
- 图像编辑:包括图像修复(填充图像中被移除的部分)、着色(为灰度图像添加颜色)等。
- 风格迁移:将一幅图像的内容与另一幅图像的风格相结合,生成新图像。
- 文本到图像生成:根据一段文本描述生成符合描述的图像。



本节课我们一起学习了计算机视觉中Transformer的应用以及多种图像生成任务。作为我们深入探讨生成模型的开始,下一讲我们将重点介绍生成对抗网络——一种通过让两个神经网络相互对抗来学习生成逼真数据的方法。







在本节课中,我们将学习两种重要的生成式模型:生成对抗网络和变分推断。我们将首先探讨GANs的基本原理和训练过程,然后介绍变分推断这一核心概念,为后续学习扩散模型和变分自编码器打下基础。
生成对抗网络由两个确定性的神经网络组成:一个生成器和一个判别器。它们相互竞争,共同进步。


生成器是一个神经网络,它接收一个随机噪声向量 z,并生成一张假图像 G_θ(z)。一种常见的架构是使用分数步长卷积(Fractionally Strided Convolution)来逐步增大图像的尺寸。
例如,一个生成器可能从4x4像素开始,通过几层分数步长卷积,逐步生成8x8、16x16、32x32,最终输出64x64像素的三通道(RGB)图像。
判别器是一个分类器神经网络,其任务是判断输入的图像是真实的(来自数据集)还是虚假的(由生成器生成)。一种改进的判别器是PatchGAN,它不直接判断整张图像,而是将图像分割成多个小块(如4x4的补丁),并对每个小块进行真假预测。这有助于生成更清晰、细节更丰富的图像。
GAN的训练是一个极小极大博弈。生成器试图生成足以“欺骗”判别器的逼真图像,而判别器则努力区分真假图像。
训练的目标函数基于标准的交叉熵损失。对于判别器,其目标是最大化正确分类真实图像和虚假图像的概率。对于生成器,其目标是最小化其生成的虚假图像被判别器识别为假的概率。
数学上,我们可以将目标函数 J(θ, φ) 定义为:
J(θ, φ) = E_{x~p_data}[log D_φ(x)] + E_{z~p_z}[log(1 - D_φ(G_θ(z)))]
其中:
- D_φ(x) 是判别器认为图像 x 为真的概率。
- G_θ(z) 是生成器根据噪声 z 生成的图像。
- 判别器 φ 试图最大化 J。
- 生成器 θ 试图最小化 J(具体是最小化公式的第二项)。
训练采用交替优化的策略,类似于块坐标下降法:
- 固定生成器,训练判别器:采样一批真实图像和一批由生成器产生的假图像,通过梯度上升更新判别器参数 φ,使其更好地区分真假。
- 固定判别器,训练生成器:采样一批噪声,通过梯度下降更新生成器参数 θ,使其生成的图像更能欺骗当前的判别器。
实践中,通常会为判别器执行 K 步更新(例如K=1),然后再为生成器执行一步更新。这确保了生成器是在与一个相对强大的判别器博弈,从而学习生成更高质量的图像。
GANs可以进行条件生成。通过向生成器和判别器同时输入一个类别标签(例如,“飞机”、“猫”),模型可以学习生成特定类别的图像。这使生成过程更具可控性。



随着模型规模的扩大,GANs能够生成极其逼真的图像。一些大型模型(如Pathways模型)采用了两阶段架构:首先用一个自回归变换器根据文本提示生成一系列图像语义标记,然后再用一个基于GAN的模型将这些标记解码成高分辨率图像。
生成式图像模型的强大能力带来了双重影响:
- 积极方面:为艺术家提供了强大的创作和编辑工具,开启了新的艺术形式。
- 挑战与风险:包括版权侵权、创意萎缩、制造虚假信息(如假新闻、深度伪造)等。应对措施包括开发数字水印技术、假图像检测模型和模型归属识别工具。然而,图像溯源(追溯生成图像所使用的原始训练数据)仍然是一个巨大的开放挑战。
在转向扩散模型和变分自编码器之前,我们需要理解变分推断这一核心概念。它为我们提供了一种通过优化来近似处理复杂概率计算(如后验推断)的方法。
图模型是一种用图结构直观表示变量间概率依赖关系的工具。
有向图模型(贝叶斯网络):用有向无环图表示因果关系或依赖关系。联合概率分布分解为每个节点给定其父节点的条件概率的乘积。例如:
P(A, B, C, D) = P(A) * P(B) * P(C|A,B) * P(D|C)
每个条件概率表(CPT)或概率密度函数是局部归一化的。
马尔可夫假设:在序列模型中,一个常见简化是假设当前状态只依赖于前一个状态,即:
P(X_t | X_1, ..., X_{t-1}) = P(X_t | X_{t-1})
这构成了一阶马尔可夫链的基础。

因子图:一种更通用的表示,包含变量节点和因子节点。因子是定义在变量子集上的非负函数。联合概率分布正比于所有因子的乘积,并通过一个全局归一化常数 Z 确保总和为1。
P(X) = (1/Z) * ∏_{k} ψ_k(D_k)
其中 Z = Σ_x ∏_{k} ψ_k(D_k) 需要对所有变量取值求和,这使得计算通常很困难。这种模型是全局归一化的。

在许多机器学习问题中,我们观测到变量 X(如数据),并希望推断潜在变量 Z(如模型参数或隐层表示)的后验分布 P(Z|X)。直接计算这个后验分布通常是难以处理的。
变分推断的核心思想是:用一个由参数 φ 定义的、形式更简单的分布 Q_φ(Z) 来近似真实的后验分布 P(Z|X)。 我们通过优化参数 φ,最小化 Q_φ(Z) 与 P(Z|X) 之间的差异(通常用KL散度衡量),从而得到最好的近似。
这引出了变分下界的概念,它允许我们将难以处理的后验推断问题转化为一个可优化的目标函数。我们将在下一节课中详细探讨这一点,并看到它如何直接应用于扩散模型和变分自编码器的构建。

本节课我们一起学习了生成对抗网络的基本原理和训练过程,了解了其“对抗博弈”的核心思想。随后,我们引入了变分推断这一重要概念,介绍了有向图模型、因子图以及局部/全局归一化的区别,为理解后续更复杂的生成模型(如扩散模型)奠定了理论基础。下一节课,我们将深入探讨变分下界,并开始学习扩散模型。


在本节课中,我们将学习扩散模型的基本原理。这是一种强大的生成模型,能够通过逐步添加和去除噪声来生成高质量的数据,如图像。我们将从直观的角度出发,先理解扩散模型的核心思想,再探讨其与变分推断的联系。
在深入扩散模型之前,我们需要了解一个关键的神经网络架构:U-Net。U-Net因其计算图呈“U”形而得名,最初用于生物医学图像分割任务。
U-Net的核心特性是输入和输出具有相同的空间维度。它包含一个“收缩路径”和一个“扩展路径”。收缩路径通过卷积和池化层逐步降低特征图的空间尺寸并增加通道数。扩展路径则通过上采样和卷积层逐步恢复空间尺寸。一个关键设计是,在扩展路径中,会将收缩路径对应层级的特征图拼接过来,这有助于保留细节信息。
上一节我们介绍了U-Net的基本结构,本节中我们来看看它在扩散模型中的作用。简单来说,U-Net将作为我们模型的核心组件,用于预测如何从带噪声的图像中恢复原始图像。
为了理解扩散模型,我们首先需要建立一个无监督学习的通用框架。
我们的假设是数据来自某个真实分布 $ Q(x_0) $,其中 $ x_0 $ 可以是一张图像或一段文本。我们的目标是学习一个参数化的模型分布 $ P_ heta $,使得 $ P_ heta approx Q $,并且从 $ P_ heta $ 中采样是可行的。
以下是几种生成模型在此框架下的对比:
- 自回归语言模型:选择 $ P_ heta $ 为自回归模型。其条件分布 $ P(x_t | x_{
- 生成对抗网络:选择 $ P_ heta $ 为一个将噪声向量 $ z $ 映射到图像的确定性生成器。然而,计算数据点 $ x_0 $ 在模型下的概率 $ P_ heta(x_0) $ 需要对所有可能的 $ z $ 进行积分,这通常是难以处理的。因此,GAN采用了对抗训练这种截然不同的学习方式。
- 扩散模型:其设置与GAN类似,$ Q $ 是真实数据分布,$ P_ heta $ 是生成模型。同样,直接计算 $ P_ heta(x_0) $ 的梯度是困难的。因此,我们将优化一个变分下界。
GAN和扩散模型都属于潜在变量模型。我们假设存在一些未观测到的潜在变量 $ z $,它们生成了我们观察到的数据 $ x_0 $。在GAN中,$ z $ 是输入生成器的噪声向量。学习后,我们可以在潜在空间 $ z $ 中进行插值,生成语义上平滑过渡的图像。
扩散模型也将利用潜在变量,但其形式更为复杂。
现在,我们开始正式定义扩散模型。首先,我们定义一个简单的概率模型,称为前向过程(或扩散过程)。

我们真正感兴趣的是反向过程,即从噪声生成数据的过程。根据概率链式法则,前向过程的联合分布也可以写成:
$$
Q_phi(x_{1:T} | x_0) = Q_phi(x_T | x_0) prod_{t=1}^{T} Q_phi(x_{t-1} | x_t, x_0)
$$
注意,这里我们将条件概率“翻转”了。虽然前向条件 $ Q_phi(x_t | x_{t-1}) $ 很简单,但反向条件 $ Q_phi(x_{t-1} | x_t) $(不依赖于 $ x_0 $ 时)通常难以计算,因为它依赖于复杂的真实数据分布 $ Q(x_0) $。


现在,我们具体化到最流行的扩散模型变体:去噪扩散概率模型。我们做出以下具体设定:
- 先验分布:令 $ P_ heta(x_T) = mathcal{N}(x_T; 0, I) $,即标准高斯分布。
- 前向过程:定义 $ Q_phi(x_t | x_{t-1}) = mathcal{N}(x_t; sqrt{alpha_t} x_{t-1}, (1-alpha_t)I) $,其中 $ alpha_t $ 是一个预先定义好的、介于0和1之间的调度表。这意味着每一步都在当前值的基础上添加一点高斯噪声。
- 反向过程:定义 $ P_ heta(x_{t-1} | x_t) = mathcal{N}(x_{t-1}; mu_ heta(x_t, t), Sigma_ heta(x_t, t)) $。其中,均值 $ mu_ heta $ 和方差 $ Sigma_ heta $ 是参数化的函数(如神经网络)。
关键设计:我们精心选择调度表 $ {alpha_t} $,使得当 $ T $ 足够大时,前向过程产生的 $ x_T $ 的分布 $ Q_phi(x_T | x_0) $ 近似为标准高斯分布 $ mathcal{N}(0, I) $。这确保了我们可以从简单的噪声开始生成过程。
扩散模型的有效性依赖于前向过程的两个关键数学性质:
性质一:任意步的噪声化分布是高斯分布
给定 $ x_0 $,经过 $ t $ 步前向过程后,$ x_t $ 的分布是封闭形式的高斯分布:
$$
Q_phi(x_t | x_0) = mathcal{N}(x_t; sqrt{bar{alpha}_t} x_0, (1-bar{alpha}_t)I)
$$
其中 $ bar{alpha}t = prod^{t} alpha_s $。通过设计调度表使 $ bar{alpha}T approx 0 $,我们就得到了 $ Qphi(x_T | x_0) approx mathcal{N}(0, I) $。
性质二:已知 $ x_0 $ 时,真实反向条件分布是高斯分布
虽然 $ Q_phi(x_{t-1} | x_t) $ 难求,但如果我们额外知道起点 $ x_0 $,那么分布 $ Q_phi(x_{t-1} | x_t, x_0) $ 有一个封闭形式的高斯表达式:
$$
Q_phi(x_{t-1} | x_t, x_0) = mathcal{N}(x_{t-1}; ilde{mu}_t(x_t, x_0), ilde{beta}_t I)
$$
其中均值 $ ilde{mu}_t $ 和方差 $ ilde{beta}_t $ 都可以由调度参数 $ alpha_t, bar{alpha}_t $ 以及 $ x_t, x_0 $ 计算出来。具体地:
$$
ilde{mu}t(x_t, x_0) = frac{sqrt{bar{alpha}{t-1}}beta_t}{1-bar{alpha}t} x_0 + frac{sqrt{alpha_t}(1-bar{alpha})}{1-bar{alpha}_t} x_t
$$
$$
ilde{beta}t = frac{1-bar{alpha}{t-1}}{1-bar{alpha}_t} beta_t
$$
这里 $ beta_t = 1-alpha_t $。
利用上述性质,我们可以推导出扩散模型的训练目标。


思路一:匹配方差
由于我们知道真实反向条件分布(在已知 $ x_0 $ 时)的方差 $ ilde{beta}t $ 是固定值,我们可以简单地将学习到的反向过程的方差设为该值:$ Sigma heta(x_t, t) = ilde{beta}_t I $。
思路二:参数化均值
接下来,我们需要学习均值函数 $ mu_ heta(x_t, t) $。观察 $ ilde{mu}_t(x_t, x_0) $ 的表达式,它由 $ x_0 $ 和 $ x_t $ 的线性组合构成。在训练时,我们有 $ x_0 $,但没有 $ x_t $;在生成时,我们有 $ x_t $,但没有 $ x_0 $。
一个自然的想法是:让神经网络根据当前的噪声图像 $ x_t $ 和时间步 $ t $ 来预测原始图像 $ x_0 $。我们将这个预测记为 $ x_ heta(x_t, t) $。然后,我们可以将学习的均值构造成与 $ ilde{mu}t $ 相同的形式:
$$
mu
heta(x_t, t) = frac{sqrt{bar{alpha}{t-1}}beta_t}{1-bar{alpha}t} x heta(x_t, t) + frac{sqrt{alpha_t}(1-bar{alpha})}{1-bar{alpha}t} x_t
$$
这样,如果神经网络 $ x
heta $ 能完美预测 $ x_0 $,那么 $ mu_ heta $ 就等于真实的 $ ilde{mu}_t $。
训练算法
基于此,我们可以得到简化的训练算法:
- 从训练集中采样一个干净图像 $ x_0 $。
- 在 $ 1 $ 到 $ T $ 之间均匀采样一个时间步 $ t $。
- 根据性质一,采样噪声 $ epsilon sim mathcal{N}(0, I) $,并计算加噪后的图像 $ x_t = sqrt{bar{alpha}_t} x_0 + sqrt{1-bar{alpha}_t} epsilon $。
- 让神经网络 $ x_ heta $ 以 $ x_t $ 和 $ t $ 为输入,预测原始图像 $ x_0 $。
- 最小化预测值 $ x_ heta(x_t, t) $ 与真实值 $ x_0 $ 之间的均方误差(或类似的重建损失)。
实际上,论文中发现直接预测所添加的噪声 $ epsilon $ 效果更好,这可以通过变量替换实现,但其核心思想与预测 $ x_0 $ 是一致的:都是让网络学习如何从带噪声的数据中恢复出干净信号。

本节课中我们一起学习了扩散模型的核心原理。我们从U-Net架构和通用生成框架出发,定义了扩散模型的前向(加噪)和反向(去噪)过程。通过深入分析DDPM这一具体实例,我们揭示了两个关键性质:任意步的噪声分布可解析计算,以及已知原始数据时真实反向过程是高斯分布。基于这些性质,我们推导出了扩散模型的训练目标——训练一个神经网络(如U-Net)来预测噪声或原始图像,从而学习到复杂的反向生成过程。扩散模型通过逐步去噪的方式,能够生成高质量、多样化的数据样本。




在本节课中,我们将回顾扩散模型的核心概念,并深入探讨如何构建一个扩散模型。随后,我们将转向变分推断,理解其作为训练复杂生成模型(如扩散模型)的理论基础。








上一节我们介绍了U-Net作为一种可能的骨干网络,它可以接收特定尺寸的图像,并输出一个具有相同空间维度(但可能通道数不同)的特征层。这对于我们构建扩散模型至关重要。

在无监督学习中,我们的目标是学习一个参数化的分布 P_θ,使其尽可能接近真实的数据分布 Q。这可以是一个语言模型(针对文本)或一个生成模型(针对图像)。
对于语言模型,如果选择自回归模型,通常可以使用基于梯度的优化器来最大化观测数据的似然,这是可处理的。然而,对于简单的图像生成架构(如我们见过的GANs),直接最大化似然通常并不可行。

对于GANs和扩散模型,我们实际上是在最大化一个包含隐变量 Z 的边缘似然。具体形式为:

P(x) = ∫ P(x|z) P(z) dz
扩散模型包含一个前向过程和一个反向过程。



前向过程(图中红色箭头)是一个向数据 x₀ 逐步添加噪声的马尔可夫链,直到最终变为纯高斯噪声 x_T。其条件分布定义为:


q(x_t | x_{t-1}) = N(√α_t * x_{t-1}, (1-α_t)I)


反向过程(图中蓝色箭头)则是从噪声 x_T 开始,逐步去噪以恢复数据 x₀ 的生成过程。我们试图学习一个参数化的分布 p_θ(x_{t-1} | x_t) 来近似这个真实但难以计算的反向过程。



时间步数 T 的选择:T 通常需要足够大(几十到几百步)以确保生成质量,但这会导致采样时计算成本高昂,因为每一步都需要运行神经网络。训练时,我们可以高效地随机采样一个时间步 t 来更新参数。







以下是前向过程 q 和精确反向过程 q 的一些关键性质。
性质一:任意时间步的分布
给定初始数据 x₀,我们可以直接采样得到任意时间步 t 的加噪图像 x_t,而无需逐步迭代:
q(x_t | x₀) = N(√ᾱ_t * x₀, (1-ᾱ_t)I)
其中 ᾱ_t = ∏_{i=1}^{t} α_i。这意味着我们可以通过一步计算得到 x_t:
x_t = √ᾱ_t * x₀ + √(1-ᾱ_t) * ε, 其中 ε ~ N(0, I)

通过精心设计 α_t 的调度表(如线性或余弦调度),可以确保最终分布 q(x_T) 接近标准高斯分布 N(0, I)。
性质二:精确反向条件分布
精确的反向条件分布 q(x_{t-1} | x_t, x₀) 是可处理的,并且也是一个高斯分布:
q(x_{t-1} | x_t, x₀) = N(μ̃_t(x_t, x₀), σ̃_t² I)
其方差 σ̃_t² 是一个仅与调度表相关的标量。其均值 μ̃_t 是 x₀ 和 x_t 的线性组合:
μ̃_t(x_t, x₀) = (√ᾱ_{t-1} * β_t / (1-ᾱ_t)) * x₀ + (√α_t * (1-ᾱ_{t-1}) / (1-ᾱ_t)) * x_t
其中 β_t = 1 - α_t。为简化,我们将 x₀ 和 x_t 前的系数分别记为 α_t⁰ 和 α_t^t。
然而,q(x_{t-1} | x_t)(不依赖于 x₀)是难以直接计算的,这正是我们需要用神经网络去近似的部分。






我们的目标是学习一个参数化的反向过程 p_θ,其每一步也是一个高斯分布:


p_θ(x_{t-1} | x_t) = N(μ_θ(x_t, t), Σ_θ(x_t, t))



我们希望 p_θ(x_{t-1} | x_t) 尽可能接近 q(x_{t-1} | x_t, x₀)。这引出了两个核心思想:
核心思想一:固定方差
我们不学习方差 Σ_θ,而是直接使用精确反向过程中已知的方差:



Σ_θ(x_t, t) = σ̃_t² I

核心思想二:学习均值 μ_θ
我们训练一个神经网络(如U-Net)来预测均值。关键在于,精确均值 μ̃_t 依赖于未知的 x₀,而我们的网络 μ_θ 只能看到加噪图像 x_t 和时间步 t。


因此,我们有以下三种参数化 μ_θ 的策略:


以下是三种主要的参数化策略:


- 选项A:直接预测均值
让网络 μ_θ(x_t, t) 直接输出对 μ̃_t 的估计。损失函数是预测均值与真实均值之间的均方误差(MSE)。

- 选项B:预测原始图像 x₀
让网络 f_θ⁰(x_t, t) 预测原始图像 x̂₀。然后利用公式 μ̃_t = α_t⁰ * x̂₀ + α_t^t * x_t 来构造估计的均值 μ̂_t。
- 选项C:预测噪声 ε
让网络 ε_θ(x_t, t) 预测在前向过程中添加到 x₀ 上以得到 x_t 的噪声 ε̂。由于 x_t = √ᾱ_t * x₀ + √(1-ᾱ_t) * ε,我们可以反推出:
x̂₀ = (x_t - √(1-ᾱ_t) * ε̂) / √ᾱ_t
再将 x̂₀ 代入选项B的公式,即可得到 μ̂_t。经验表明,选项C(预测噪声)通常效果最好,因为它更直接地利用了前向过程的噪声特性,并且训练更稳定。

训练流程:
在实际训练中,我们不会遍历所有时间步。对于每个训练样本 x₀,我们:
- 随机采样一个时间步 t ~ Uniform(1, T)。
- 采样噪声 ε ~ N(0, I),根据性质一计算 x_t。
- 根据选择的参数化策略(如选项C),计算网络预测值(如 ε_θ(x_t, t))与真实值(如 ε)之间的损失(如MSE)。
- 通过梯度下降更新网络参数 θ。


这种随机采样时间步的方式类似于随机梯度下降,能提供更多样的梯度信号。


训练完成后,我们可以从学习到的反向过程 p_θ 中采样生成新图像:
- 从标准高斯分布采样初始噪声:x_T ~ N(0, I)。
- 从 t = T 到 t = 1 循环:
- 根据参数化策略,利用 μ_θ(x_t, t) 和固定方差 σ̃_t²,定义当前步的条件高斯分布 p_θ(x_{t-1} | x_t)。
- 从该分布中采样:x_{t-1} = μ_θ(x_t, t) + σ̃_t * ε,其中 ε ~ N(0, I)。
- 最终得到的 x₀ 即为生成的图像。
为什么采样时要添加噪声? 因为我们的目标是从分布 p_θ(x₀) 中抽取样本,而不仅仅是获取其均值(均值可能是一张合理的图像,但不是随机样本)。通过每一步从高斯分布中采样,我们执行的是对边缘分布 p_θ(x₀) = ∫ p_θ(x_0:T) dx_{1:T} 的蒙特卡洛估计。
我们之前提到,直接最大化数据的对数似然 log p_θ(x₀) 是困难的。扩散模型的训练可以通过变分推断的框架来理解,其核心是最大化证据下界。

对于扩散模型,其ELBO可以推导并分解为以下形式(忽略常数项):

L = E_q [ log p_θ(x_0:T) / q(x_1:T | x_0) ] ≤ -log p_θ(x₀)
进一步分解后,ELBO包含一系列KL散度项:
L = L_T + Σ_{t=2}^{T} L_{t-1} + L_0
其中关键的一项 L_{t-1} 正是我们之前试图最小化的目标:



L_{t-1} = D_KL( q(x_{t-1} | x_t, x₀) || p_θ(x_{t-1} | x_t) )


KL散度 是衡量两个概率分布 Q 和 P 差异的非对称性度量:





D_KL(Q||P) = E_{x~Q} [ log Q(x) / P(x) ]
它在意 Q 中概率高的区域是否与 P 匹配,但对 Q 中概率低、P 中概率高的区域惩罚较小。最小化 D_KL(Q||P) 就是在寻找一个尽可能接近 P 的分布 Q。



在扩散模型中,我们通过最小化 L_{t-1}(即KL散度)来训练 p_θ,使其接近精确的反向过程 q,这恰好是在优化ELBO,从而间接地最大化了数据似然。


本节课我们一起学习了扩散模型的完整构建与训练流程:
- 回顾了扩散模型的前向(加噪)与反向(去噪)过程框架。
- 深入分析了前向过程的性质,使我们能够高效地采样任意时间步的加噪图像。
- 探讨了参数化反向过程的三种策略,并理解了预测噪声(选项C)通常是更优的选择。
- 描述了从训练好的扩散模型中采样生成新图像的迭代过程。
- 引入了变分推断和ELBO的概念,从理论层面解释了扩散模型训练目标(最小化KL散度)的合理性,即通过优化可处理的下界来近似难以直接计算的最大似然目标。

通过结合强大的神经网络架构(如U-Net)和严谨的概率图模型理论,扩散模型成为了当前最强大的生成式AI模型之一。


在本节课中,我们将要学习变分自编码器。VAE是一种强大的生成模型,它结合了概率图模型和神经网络的优点,能够学习数据的连续潜在表示并生成新的样本。我们将从变分推断的核心概念出发,逐步理解VAE的原理、目标函数及其与扩散模型的联系。

上一节我们介绍了KL散度,它是一种衡量两个概率分布差异的方法。本节中,我们来看看如何利用KL散度来处理复杂的概率分布。
KL散度不是对称的,因此不是一个真正的距离度量。公式 KL(Q||P) 不等于 KL(P||Q)。当两个分布相同时,KL散度达到最小值。
变分推断的高层动机源于我们希望处理复杂的概率分布。例如,我们有一些观测变量 X(如图像像素)和一些潜在变量 Z(如图像不同部分的标签),我们可能希望估计后验分布 P(Z|X)。如果因子图中存在循环,或者我们采用贝叶斯视角试图估计参数的分布,那么这种后验估计通常是难以处理的。
解决方案是使用一个更简单的分布 Q(Z) 来近似后验 P(Z|X)。通常,Q(Z) 比 P(Z|X) 具有更强的独立性假设,这是可以接受的,因为 Q(Z) 是针对某个特定的 X 进行调整的。
关键思想是从某个分布族 Q 中选择一个**的 Q(Z) 来近似 P(Z|X)。我们称 Q(Z) 为变分近似,Q 为变分族,即所有被考虑的概率分布的集合。通常,Q_θ(Z) 由一些称为变分参数 θ 的参数化。而 P_α(Z|X) 则由一些固定的参数 α 参数化。
在扩散模型中,我们可以说 Q_φ 是前向过程,P_α 是我们学习的反向过程。变分推断的不同之处在于,这里我们假设 α 是已知的,但无法直接处理该分布,因此我们推断出参数 θ,使我们能够使用该分布的近似版本。这与学习模型参数是不同的。
以下是变分推断算法的一些例子:
- 平均场变分推断
- 置信传播
- 树重加权置信传播
- 期望传播
我们将主要提及平均场方法。
上一节我们介绍了变分推断的基本思想,本节中我们来看看一个具体的近似方法:平均场近似。
平均场近似的思想是,假设 Q_θ(Z) 分布中每个变量都是完全独立的。这听起来可能是一个很糟糕的分布,但如果我们只处理某个特定的 X,这可能是合理的。
回到语义分割问题。P_α(Z|X) 是给定图像的语义分割分布。平均场近似则学习一个所有像素独立的概率分布。对于给定的图像,我们仍然可以处理这个分布。
我们可以考虑两种情况:
- 给定联合分布
P(X, Z),我们希望处理分布P(Z|X)。这里我们假设分母难以处理。 - 给定一个因子图和势函数,其全局归一化常数(分母)难以计算。
无论哪种情况,我们都希望处理 P(Z|X),但分母难以计算。平均场的思想是,我们做出假设,得到一个分解的近似分布 Q_θ,然后通过求解一个优化问题来选择最小化 KL(Q||P) 的 Q。最终,对于这个特定的 P 和 X,我们会得到一个超级容易处理的 Q_hat,因为所有变量都是独立的。
要获得这个 Q_hat,我们可以使用坐标下降、梯度下降等优化算法来解决。等价地,如果我们用某个 θ 参数化 Q,那么选择最小化KL散度的 Q 就等同于选择最小化KL散度的 θ。
在测试时,模型已经训练好,α 已知。这里所做的就是针对一个特定的测试图像 X,高效地提出一个更简单的分布 Q_θ,因为原始分布计算量太大。我们针对这一个图像优化目标函数,然后使用对应的 Q 进行处理。
上一节我们讨论了如何通过优化来获得近似分布,但直接最小化KL散度本身可能也难以计算。本节中,我们来看看如何通过一个可处理的目标函数——证据下界来实现。
如果我们想对 θ 最小化 KL 散度,log P(X) 是一个常数。因此,我们可以直接忽略它,因为最小化操作不关心这个难以处理的常数。然后我们可以取负号,将最小化问题转化为最大化问题,从而得到证据下界 (ELBO):
ELBO(Q) = E_Q[log P(X, Z)] - E_Q[log Q(Z)]
ELBO 包含两项:
- 第一项
E_Q[log P(X, Z)]:如果Q_θ将概率质量放在与P_α相同的高概率Z值上,则该项值较高。它促使Q匹配P。 - 第二项
-E_Q[log Q(Z)]:这实质上是Q_θ的熵。如果Q_θ像均匀分布一样均匀地分散其概率质量,则熵较高。它起到正则化的作用。
定理表明,对于任何 Q,都有 log P(X) >= ELBO(Q)。因此,ELBO 是 log P(X) 的一个下界。通过最大化 ELBO,我们实际上是在寻找对 P(Z|X) 归一化常数最紧的下界。
上一节我们建立了变分推断的理论基础,本节中我们来看看如何将其应用于构建生成模型,即变分自编码器。
我们之前见过自编码器,它有一个输入 X,一个学习 X 低维表示的瓶颈网络(编码器),以及一个从该隐藏表示重建原始输入 X 的解码器网络。自编码器的一个关键限制是无法从中采样,因为整个过程是确定性的。
变分自编码器将学习一个易于采样的连续潜在空间 Z,并通过从学习到的生成模型中采样来生成新的数据图像。
可以从两个视角看待VAE:
- 概率图模型视角:VAE模型是一个简单的图模型。潜在变量
Z服从多元高斯分布,给定Z,我们采样X。这里有一个函数G_Φ确定性地从Z计算X。 - 神经网络视角:VAE像一个概率自编码器,定义了两个分布:
Q_θ:编码器分布,给出给定X时Z的分布。P_Φ:解码器分布,给出给定Z时X的分布。
参数θ和Φ是神经网络参数。
对于VAE,模型是 P_Φ,变分近似是 Q_θ。我们定义了一个模型(从高斯分布采样 Z,然后确定性地生成 X),但如果我们想通过最大化训练样本 X_i 的对数概率来学习,将会遇到困难,因为边缘化潜在变量 Z 是难以处理的。因此,我们需要一个可以高效处理的简单分布,这就是编码器的作用。
上一节我们介绍了VAE的基本框架,本节中我们深入看看其具体架构和训练中的关键技术。
让我们详细看看这些概率分布:
- 解码器
P_Φ:采样一个潜在变量Z,将其输入解码器神经网络(例如反卷积网络或多层感知机)。网络输出一个均值和一个(协)方差矩阵,将这些作为高斯分布的参数,即可从中采样得到图像X的分布。 - 编码器
Q_θ:输入一个图像X到神经网络,网络输出一个均值和一个方差,作为高斯分布Q(Z|X)的参数,从而可以得到Z。
我们可以将VAE与扩散模型联系起来。这类似于一个单步(T=1)的扩散模型:
- 编码器
Q_θ(Z|X)类似于前向过程(从X0到X1)。 - 解码器
P_Φ(X|Z)类似于反向过程(从X1到X0)。
在扩散模型中,我们绕过了变分推断,因为我们直接定义了一个固定的前向过程Q(例如添加高斯噪声),没有复杂的神经网络编码器,只学习了反向过程的参数。而在VAE中,我们同时学习前向(编码)和后向(解码)过程的参数。
我们通过最大化证据下界 (ELBO) 来训练VAE。ELBO包含一个类似于重构损失的项(希望观测到的 X 概率高)和一个正则化项。我们通过对编码器分布进行采样来近似 E_Q 的期望。
训练中的问题是,计算图中存在采样操作 Z ~ Q_θ(Z|X),无法直接反向传播。解决方案是使用重参数化技巧。我们不是直接采样 Z,而是先从标准高斯分布采样一个噪声 ε ~ N(0, I),然后通过 Z = μ + σ * ε 计算 Z,其中 μ 和 σ 是编码器网络的输出。这样,采样过程就变成了计算图中的一个叶子节点,从而支持反向传播。
VAE的整体计算流程是:输入图像 X,用编码器网络得到高斯分布的均值和方差,通过重参数化技巧采样 Z,将 Z 输入解码器网络得到另一个高斯分布的参数,然后计算损失(重构损失和正则化损失),最后通过反向传播同时更新编码器和解码器的参数。
VAE最初用于图像生成(如MNIST手写数字、人脸),在当时效果很好。它也可用于文本生成。后来,人们提出了像矢量量化VAE 这样的方法,能够生成更高质量的图像。在VAE框架中,我们拥有一种变分采样方法。
此外,还有扩散模型的完全变分版本,例如变分DDPM,它将整个前向扩散过程(T 个时间步)也用神经网络参数化,并在训练过程中学习前向过程的参数。



本节课中我们一起学习了变分自编码器。我们从变分推断和KL散度出发,理解了如何用简单分布近似复杂后验分布,并推导出可优化的证据下界。接着,我们将这些原理应用于构建VAE,了解了其编码器-解码器架构、概率解释以及与扩散模型的联系。最后,我们探讨了VAE的训练技巧(重参数化)及其应用与发展。VAE为学习数据的连续潜在表示和生成新样本提供了一个坚实的概率框架。


在本节课中,我们将学习如何利用大型基础模型(特别是大语言模型)来适应不同的任务,而无需修改模型参数。我们将重点探讨“上下文学习”这一核心概念,这是一种通过巧妙设计输入提示(Prompt)来引导模型完成特定任务的方法。


上一节我们介绍了基础模型的概念,本节中我们来看看如何将它们应用于新任务。首先,我们需要理解两个关键术语:零样本学习和少样本学习。
零样本学习 假设训练数据中完全不包含测试数据中出现的新类别标签。例如,一个模型只训练过识别数字“1”、“2”、“3”,现在却需要它识别从未见过的字母“A”和“B”。核心挑战是:如何在没有任何标注示例的情况下进行有效预测?
一种可能的解决方案是使用辅助信息。我们可以训练一个模型 f(x, d(y)),其中 x 是输入(如图像),d(y) 是标签 y 的描述(如文本)。模型输出一个分数,表示 x 与描述 d(y) 的匹配程度。在训练时,我们让模型为正确的 (x, d(y)) 对输出高分,为错误的对输出低分。在测试时,对于新输入 x_test,我们计算它与所有可能标签描述的匹配分数,并选择分数最高的标签。这种方法允许模型处理无限多的新标签,只要能为它们提供描述。
少样本学习 则假设我们至少拥有每个新类别的少量(例如1-5个)标注示例。在这种情况下,我们可以采用更直接的方法,例如基于模型学到的特征表示,使用最近邻分类等算法。
现在,让我们将焦点转向大语言模型。这些模型通常通过自回归方式在大量文本上训练,以预测序列中的下一个词。提示 的核心思想是:我们可以提供一个前缀字符串(即提示),使得模型最有可能生成的后续文本正是我们想要的答案。
以下是几个零样本提示的例子:
- 风格模仿:给定一个标题和作者(如华莱士·史蒂文斯的诗),模型能生成风格相似的文本。
- 翻译:输入一句西班牙语,后接“English translation:”,模型能输出对应的英语翻译。
- 问答:提供一段文本,后接“Question: ...”,模型能生成“Answer: ...”及其解释。
- 摘要:输入一个故事,后接“one sentence summary:”,模型能生成简洁的摘要。
为什么未经专门训练的模型能完成这些任务?一个可能的原因是,其海量的训练数据中本身就包含了这些任务的“自然演示”。例如,网络上可能存在大量“某句话在法语中是...”或“问题:... 答案:...”这样的文本模式,模型从中隐式地学到了这些任务。
上一节我们看到了零样本提示的威力,本节中我们来看看如何利用少量示例来进一步提升性能,这就是上下文学习。
上下文学习是指在提示中不仅包含任务描述,还包含少量的输入-输出示例,然后让模型根据这个“上下文”来对新输入进行预测。这与需要更新模型参数的监督微调形成对比。
以下是上下文学习的两个示例:
- 情感分析:
输入:good movie 情感:positive 输入:it is terrible 情感:negative 输入:movie is great 情感:positive 输入:not good 情感:我们希望模型在最后一个“情感:”后生成“negative”。
- 翻译:
sea otter => loutre de mer plush girafe => girafe peluche cheese =>我们希望模型输出“fromage”。


上下文学习的优点在于无需反向传播和参数更新,计算成本相对较低,且对于仅提供API的模型也适用。但其效果受多种因素影响。

以下是上下文学习中一些值得注意的现象:
- 示例顺序敏感:提供给模型的少量示例的排列顺序会显著影响预测结果,导致性能波动。
- 标签平衡:提示中正负示例的比例会影响模型的预测倾向。
- 标签随机性:令人惊讶的是,即使将提示中示例的标签替换为随机标签,模型的性能有时也不会急剧下降,甚至仍优于零样本情况。这表明模型可能更多地是在学习输入数据的格式或分布,而非严格的输入-输出映射。
- 示例数量:增加上下文中的示例数量并不总是能提升性能,有时在达到一个峰值后性能会下降。
- 提示工程:提示的具体措辞对结果影响巨大。例如,在新闻分类任务中,使用“what is the most accurate label for this news article?”比“what is this piece of news regarding?”能获得高得多的准确率。研究表明,在模型看来概率更高(困惑度更低)的提示往往能带来更好的性能。
一个能显著提升模型在复杂推理任务上性能的技巧是思维链提示。其核心思想是,在提示的示例中,不仅给出答案,还展示得出答案的逐步推理过程。
例如,对于一个数学问题:
问题:食堂有23个苹果。做午餐用了20个,又买了6个。现在有多少苹果?
标准提示的示例可能直接给出答案。而思维链提示的示例则会展示:
问题:Roger有5个球。他又买了2罐网球,每罐3个。他一共有多少个球? 推理:2罐网球,每罐3个,就是6个网球。5 + 6 = 11。 答案:11
通过要求模型“逐步思考”,可以显著提升其在算术、常识推理等任务上的表现。甚至仅通过添加“让我们一步步思考”这样的指令,也能激发模型的推理能力。
鉴于手动设计**提示非常耗时,研究者开发了自动优化提示的方法:
- 提示复述:使用模型生成多个提示变体,并选择效果最好的。
- 基于梯度的搜索:在连续的词嵌入空间中进行优化,寻找能最大化任务性能的提示表示。
- 提示微调:直接优化一组连续的“软提示”向量,这些向量作为可训练参数被前置到输入中,而无需对应具体的词汇。


本节课中我们一起学习了如何利用大语言模型进行上下文学习。我们从零样本学习和少样本学习的经典定义出发,探讨了如何通过设计提示来激发模型完成翻译、问答、摘要等任务。我们深入分析了上下文学习的工作原理、其相对于监督微调的优势与局限,以及影响其效果的多种因素(如示例顺序、提示措辞)。最后,我们介绍了思维链提示这一强大技巧以及自动提示优化的前沿方向。下一节课,我们将继续探讨适配器与参数高效微调等其他模型适应技术。

在本节课中,我们将要学习参数高效微调。这是一种在微调大型基础模型时,显著减少所需内存和计算量的技术。我们将探讨其动机、核心方法以及它们如何在实际任务中发挥作用。



微调预训练模型是提升其在特定下游任务上性能的标准方法。然而,对于拥有数百亿参数的大型模型,全参数微调需要巨大的计算资源,通常需要将模型分割到多个GPU上。参数高效微调旨在通过仅更新模型的一小部分参数,来达到接近全参数微调的性能,从而大幅提升效率。
上一节我们介绍了微调的基本概念,本节中我们来看看为什么需要更高效的方法。
全参数微调虽然性能优异,但存在两个主要问题:
- 计算与内存开销大:反向传播所需的内存和计算时间通常是前向传播的三倍。
- 模型规模限制:当前顶级GPU(如A100或H100)的显存通常为40GB或80GB。要微调一个千亿参数模型,往往需要将模型分割到多个GPU上。
参数高效微调的目标,正是通过将反向传播的内存和计算时间大幅减少(例如3倍),使得在单个GPU上微调大型模型成为可能。
在深入具体方法前,我们先对比一下微调与上下文学习的性能。这有助于理解为什么即使微调效率较低,我们仍然需要它。
研究表明,在相同模型规模下,微调通常显著优于少样本上下文学习。例如,在一项公平比较中,使用16个训练样本时,微调模型的准确率比上下文学习高出约12%。更重要的是,一个经过微调的67亿参数模型,其性能可能与一个300亿参数的上下文学习模型相当。这解释了为什么像ChatGPT使用的GPT-3.5 Turbo(一个较小的微调模型)能够提供强大的服务。
参数高效微调的目标,就是在保持这种性能优势的同时,极大地提升效率。
接下来,我们将介绍几种主流的参数高效微调方法。我们的目标是获得与全参数微调相当的性能。以下是几种核心方法:
- 微调顶层参数:仅更新模型最顶部的若干层参数。
- 适配器:在模型的Transformer层中插入额外的小型神经网络模块,并仅训练这些模块。
- 前缀微调:为模型学习一个任务特定的“软提示”向量前缀,并将其添加到每一层的注意力机制中。
- LoRA:为模型中的权重矩阵学习一个低秩的增量更新,这是本节课的重点。
下面,我们将逐一探讨这些方法。
我们从最简单的方法开始:仅微调模型的顶层参数。
其核心思想是:在深度神经网络中,底层通常学习通用的特征表示(如文本的基础语义或图像的边缘),而高层则学习更任务相关的特征。因此,在微调时,我们可以固定所有底层参数,只更新顶部的K层。
实现方式:
在大多数深度学习框架中,这很容易实现。你只需在反向传播时,在指定层之后停止梯度计算。
优势:
- 内存节省:无需存储底层参数的梯度。节省的内存比例约为
K / (K+L),其中L是总层数。 - 简单易用:实现起来非常直接。
局限性:
- 性能可能不如更新更多参数的方法。实验表明,随着可训练参数减少,性能会出现明显下降。
适配器方法不是选择现有层进行更新,而是向模型中插入全新的、可训练的小型模块。
以下是适配器层的典型结构:
# 适配器模块结构示意 Adapter(x): h = DownProject(x) # 线性层,将维度D降至r (r << D) h = NonLinearity(h) # 例如ReLU激活函数 output = UpProject(h) # 线性层,将维度r升回D return x + output # 残差连接
工作原理:
- 在预训练好的Transformer层中的两个位置(通常在多头注意力模块之后和前馈网络之后)插入适配器模块。
- 微调时,冻结Transformer的所有原始参数,只训练这些新插入的适配器参数。
- 由于适配器内部的瓶颈维度r远小于模型维度D,其参数量仅占模型总参数的0.5%到8%。
优势:
- 参数量极少,性能却可以接近全参数微调,且没有微调顶层参数那样的性能陡降问题。
劣势:
- 推理延迟:在模型推理(测试)时,这些额外的层会增加计算量,导致延迟。尤其是在批大小为1的场景下,延迟可能增加20-30%。
前缀微调提供了一种不同的思路:它不修改模型内部的权重,而是通过优化一组“虚拟”的令牌嵌入来影响模型行为。
核心思想:
- 为模型每一层的注意力机制学习一组任务特定的“前缀”向量(
P_K和P_V)。 - 在计算注意力时,将这些前缀向量与实际的键(
K)和值(V)拼接起来,仿佛输入序列前有一些额外的“软提示”令牌。 - 微调时,只优化这些前缀向量,模型的所有原始权重保持冻结。
优势:
- 可以实现多任务共享一个核心模型,只需切换不同的前缀参数即可。
劣势:
- 性能对前缀长度(参数量)敏感,有时增加参数反而会导致性能下降,优化过程不太稳定。
- 仍需进行完整的反向传播来计算梯度(尽管不更新模型权重),效率提升有限。
LoRA是当前最流行且高效的参数高效微调方法之一。它的设计受到了一个重要观察的启发:大型过参数化模型学到的权重,可能实际上存在于一个低维的内在子空间中。
- 内在维度理论:研究表明,大型神经网络的参数可能位于一个低维流形上。这意味着,要适应新任务,可能只需要在低维空间中对参数进行微小的调整。
- 前缀微调的不稳定性:如前所述,前缀微调的性能变化不单调。
- 适配器的推理延迟:适配器会增加测试时的延迟。
LoRA的核心是为预训练模型中的权重矩阵添加一个低秩的增量更新。
假设我们有一个预训练好的线性层,其权重为 W0 ∈ R^(d×k),输入为 x,输出为 h = W0 * x。
关键步骤:
- 初始化:
A用随机高斯噪声初始化,B初始化为零矩阵。这确保训练开始时,ΔW = 0,模型行为与原始预训练模型完全一致。 - 训练:冻结原始权重
W0,只训练低秩矩阵B和A。 - 推理:训练完成后,可以将增量合并回权重:
W‘ = W0 + B*A。这样,在推理时就是一个标准的、无额外开销的线性层,实现了“热插拔”不同任务的适配权重。
在Transformer中的应用:
通常,LoRA被应用于Transformer每一层的查询(Q) 和键(K) 投影矩阵(有时也包括值(V)矩阵)。实验发现,仅适配Q和K矩阵通常就能取得很好效果,且比适配所有三个矩阵更高效。
- 参数量极少:即使秩
r=1或r=2,在许多任务上也能取得接近全参数微调的性能。 - 无推理开销:训练完成后可将低秩矩阵合并,推理速度与原始模型相同。
- 性能优异:在多种任务和数据集规模(从小样本到大数据)上,LoRA都表现出了强大且稳定的性能,常常匹配甚至超过全参数微调。
- 模块化:可以轻松为不同任务存储不同的
(B, A)对,并在同一个基础模型上快速切换。
参数高效微调的思想同样适用于视觉Transformer。例如,LN-LoRA 在应用低秩更新前加入了层归一化,在视觉任务上取得了更好效果。还有像FacT这样的方法,能够以更少的参数量实现优秀的性能。这表明,对于视觉和语言模态,低秩适应都是一种强大且通用的微调策略。
本节课我们一起学习了参数高效微调。我们首先了解了全参数微调面临的挑战,然后深入探讨了四种主要方法:
- 微调顶层参数:简单但性能可能受限。
- 适配器:插入小型网络模块,性能好但会增加推理延迟。
- 前缀微调:学习软提示向量,但优化不稳定。
- LoRA:为权重矩阵学习低秩增量,在保持高性能的同时,实现了参数高效且无推理开销,是目前最受欢迎的方法之一。


这些技术使我们能够在有限的计算资源下,有效地将庞大的基础模型适配到各种下游任务,是生成式AI实际应用中的关键一环。未来的研究可能会进一步探索如何优化反向传播过程本身,以实现更极致的微调效率。


在本节课中,我们将学习如何让大型语言模型更好地适应特定用例,特别是如何使其行为与人类用户的期望保持一致。我们将重点探讨两个核心主题:指令微调和基于人类反馈的强化学习。
上一节我们介绍了上下文学习,本节中我们来看看指令微调。我们之前提到,对于一个经过指令微调的大型语言模型,其提示前通常会有一个额外的系统提示,例如“你是一个有帮助的AI助手”。这个系统提示与实际指令(如用户的问题)是分开的。
我们曾看到一个例子,在Llama 2(70B参数)和Llama 2 Chat(7B参数)上使用相同的提示,得到的模型输出却大不相同。为了获得这种类似聊天的响应,我们需要对模型进行进一步的微调,这就是指令微调。
其动机在于,我们想构建一个聊天代理。大型语言模型通常是在海量网络文本、文章和代码上训练,以降低困惑度,其核心目标是预测下一个词。然而,聊天代理的任务不仅仅是预测下一个词,它需要以对话方式行事,并知道何时停止生成。因此,我们的目标是进行对齐,使大型语言模型的行为与人类用户对特定任务的期望保持一致。
关键思想是构建一个数据集,其中包含我们期望的聊天行为示例,然后在这些数据上微调模型。这项技术在文献中有许多名称,如指令微调、聊天微调、对齐微调或行为微调,但其核心都是构建数据集并训练模型。
那么,如何为聊天代理构建训练数据集呢?一个训练示例需要包含一个提示(X)和对应的响应(Y)。以下是几种获取数据的方法:
- 人工标注:可以请专家或标注员模拟聊天行为,创建提示并撰写理想的响应。
- 网络爬取:可以从网络上抓取对话数据,例如问答网站(如Stack Overflow),这些网站通常还包含答案的质量评分。
- 模型生成:可以利用现有的基础模型,通过上下文学习等方式,让其生成提示或响应,甚至对现有提示进行改写以提升质量。
接下来,让我们看看实际中人们是如何构建指令微调数据集的。
业界已涌现出大量开源指令微调数据集。我们深入分析几个代表性案例。
第一个是InstructGPT(OpenAI)使用的数据集,它包含13,000个提示-响应对。其来源有两种:一是由标注员直接编写提示和演示响应;二是从早期API用户提交的提示中采样,再由标注员撰写理想的响应。这个数据集是闭源的。
作为其开源替代,Databricks公司创建了Dolly数据集,包含15,000个手工编写的示例。他们通过竞赛激励员工,并遵循了与InstructGPT类似的分类,如开放式问答、封闭式问答、信息提取、总结、头脑风暴、分类和创意写作。
以下是Dolly数据集的一些示例类别:
- 开放式问答:例如,指令:“哪位个人在奥运会历史上获得的金牌最多?”,响应:“迈克尔·菲尔普斯以23枚金牌保持着获得最多金牌的记录。”
- 封闭式问答:提供一段上下文,然后基于上下文提问。例如,上下文是关于雷丁火车站的信息,问题是:“第一个雷丁火车站是何时开放的?”
- 信息提取:例如,指令:“提取本段落中提到的所有日期,并以‘日期 - 描述’的格式用项目符号列出。”
- 头脑风暴:例如,指令:“你可以自己制作哪些独特的窗帘绑带?”,响应会列出多种可能的物品。
- 创意写作:例如,指令:“写一首关于我多么喜欢泡菜的俳句。”
这些示例表明,像ChatGPT这类模型的行为并非凭空产生,而是高度依赖于特定标注员编写的交互范例。
另一种构建大规模数据集的方法是Flamingo/T5系列模型采用的。其核心思想是利用现有的自然语言处理(NLP)任务数据集,通过编写模板将其转化为指令微调格式。
例如,对于一个自然语言推理(NLI)任务的数据点(包含前提、假设和标签),可以设计多个模板来生成不同的指令示例:
- 模板1:
基于以上段落,我们能否推断出“俄罗斯人保持着在太空停留时间最长的记录”?选项:是/否 - 模板2:
阅读以下内容,判断假设是否可以从前提中推断出来。
通过这种方式,一个包含10,000个示例的NLI数据集可以轻松生成数万甚至数十万个指令微调示例。
指令微调也可以扩展到多模态领域。例如,Multi-Instruct数据集采用了与Flamingo类似的方法,从62个多模态任务中构建了包含图像的指令微调数据集。任务可能包括为图像中的指定区域生成描述、定位图像中的文本,或根据图像内容回答问题。
以上我们讨论了如何构建数据集并进行微调。接下来,我们将探讨一种替代标准最大似然训练的方法。
上一节我们介绍了指令微调,本节中我们来看看如何通过基于人类反馈的强化学习进一步优化模型行为。InstructGPT模型首次成功地将RLHF应用于大型GPT模型的训练。其论文声称,经过人类评估,13亿参数的InstructGPT模型的输出比1750亿参数的GPT-3更受青睐,尽管前者参数少了100倍。
标准的RLHF流程包含三个步骤,我们将逐一详解。
这一步与我们之前讨论的指令微调完全相同。使用一个相对较小的数据集(例如InstructGPT的13K示例)对基础语言模型进行微调,使其初步对齐聊天代理的行为。这一步的输出是一个微调后的语言模型 P_φ(参数为φ)。
这一步开始变得有趣。我们从第一步得到的模型中采样生成大量响应,并引入人类标注员进行评估。
具体流程如下:
- 收集大量提示(例如33,000个)。
- 对于每个提示,使用第一步的模型生成K个(通常为4到9个)不同的响应。
- 将每个提示对应的K个响应交给人类标注员进行排序(从**到最差),或进行两两比较评分。
接下来,我们需要训练一个奖励模型 R_θ(参数为θ)。这个模型本质上是一个回归模型,其任务是接收一个提示和对应的响应,输出一个标量分数,表示该响应的质量。
如何构建这个模型?通常,我们取一个大型语言模型(可以与第一步模型相同或更小),移除其用于词汇预测的最终输出层,替换为一个输出单个标量的线性层。
训练目标函数如下:
loss(θ) = -E_{(x, y_w, y_l) ~ D} [log(σ(R_θ(x, y_w) - R_θ(x, y_l)))]
其中:
(x, y_w, y_l)来自数据集D,y_w是人类标注中优于y_l的响应。σ是sigmoid函数。- 这个损失函数鼓励奖励模型给优质响应
y_w的打分高于劣质响应y_l。
一个训练技巧是,对于同一个提示 x 对应的所有 C(K,2) 个响应对,将它们放在同一个训练批次中。这样做有两个好处:一是提高训练稳定性,二是能高效地重用模型在处理相同提示 x 时的计算图,提升效率。
这一步,我们使用第二步训练好的奖励模型 R_θ 作为奖励信号,通过强化学习进一步微调第一步得到的语言模型 P_φ。
在强化学习框架中:
- 状态:当前的提示
x。 - 动作:模型生成的响应
y。 - 奖励:奖励模型给出的分数
R_θ(x, y)。 - 回合:每个提示-响应对被视为一个独立的回合。
目标是通过优化策略模型 P_φ 的参数 φ,来最大化期望奖励。通常,目标函数会结合强化学习目标(最大化奖励)和原始预训练目标(最大化似然),以防止模型过度优化奖励而偏离自然语言分布或导致退化。
最终的目标函数形式可能类似于:


objective(φ) = E_{x~D, y~P_φ(·|x)} [R_θ(x, y)] - β * KL(P_φ(·|x) || P_ref(·|x))
其中 P_ref 通常是第一步微调后的模型,KL 散度项用于约束新模型不要偏离参考模型太远,β 是控制约束强度的系数。
研究表明,RLHF能有效提升模型在人类评估中的帮助性和无害性,同时通常不会损害模型在各类NLP任务上的零样本或少样本性能。
然而,最新的研究(如LIMA: Less Is More for Alignment)提出了一个有趣的观点。该工作仅使用1000个精心挑选的指令微调示例对LLaMA模型进行微调,无需RLHF,其效果就能与经过大规模RLHF训练的商用模型(如Bard、Claude、GPT-4)相媲美或接近。
这暗示着,模型的大部分能力可能已经蕴含在预训练阶段,而指令微调和RLHF更多是在此基础上进行相对较小的行为风格调整。


本节课我们一起学习了如何通过指令微调和基于人类反馈的强化学习来对齐大型语言模型,使其行为更符合人类用户的期望。我们探讨了构建指令微调数据集的各种方法,并详细剖析了RLHF的三个关键步骤:监督微调、奖励模型训练和强化学习优化。最后,我们也了解到,有时“少即是多”,精心设计的小规模数据微调也能取得惊人的效果。


在本节课中,我们将要学习一种名为潜在扩散模型的技术。如果你听说过Stable Diffusion,那么这项技术就是其背后的核心。Stable Diffusion在其最初发布时,可以说是第一个潜在扩散模型。除了这个模型,我们还将概览其他一些文本到图像生成模型,以便对当前使用的技术范围有一个宏观的了解,然后我们将聚焦于潜在扩散模型。

上一节我们介绍了课程的整体进度,本节中我们来看看后续的安排。作业三将于明天截止,作业四将在春假后发布。我们计划在春假后探索一种名为“提示到提示”的方法以及其他图像生成方面的内容。
今天是我本学期的最后一节课。春假后,将由Huanzi接替授课。他曾在MSR有过许多合作,并在相关论文中有所贡献,将为课程带来新的视角。



展望未来,我们已经完成了五分之三的测验,学期后半段将有一次考试,但大部分时间将投入到项目工作中。我们基本遵循了学期初制定的计划,虽然这占用了大家不少时间去实现这些具有挑战性的任务,但我希望这些努力是值得的,并能帮助大家在后续的项目中做出出色的成果。
关于项目,我们暂时不会发布全部细节,但现在是与课程中其他同学交流、思考你想做什么项目的好时机。我们努力展示了广泛的生成式AI主题,但尚未深入探讨将大型语言模型扩展到海量数据和计算资源的系统层面。我们认为这可能是项目中一个相对小众的方向。如果你还没有找到队友,我们鼓励你通过Piazza的组队功能联系他人,分享你的兴趣或加入他人的想法。
我们之前提到过条件图像生成,但尚未深入探讨。以下是几种主要的条件图像生成任务:
- 类别条件生成:给定一个类别标签,生成该类型的图像。
- 超分辨率:从低分辨率图像生成高分辨率图像。
- 图像编辑:包括修复、上色、扩展画布、风格迁移等。
- 文本到图像生成:根据文本描述生成图像。
今天讨论的技术虽然以文本到图像生成为例,但可以非常直接地推广到上述其他任务。这些任务的核心主题都是基于某些数据进行条件生成,无论是标签、带掩码的现有图像还是文本提示,都可以表示为模型在生成时依赖的条件数据。
接下来,我们来看看文本到图像生成领域的几类主要方法。
以下是三类主要方法:
- GAN方法:用于文本到图像生成的生成对抗网络方法。
- 自回归模型:主要由基于Transformer的模型构成。
- 扩散方法:潜在扩散模型是其中的一个例子。
在这些方法中,最早的文本到图像生成工作实际上来自GAN或其风格的方法。相比之下,基于Transformer的自回归模型主要出现在视觉Transformer及其后续生成工作之后。第三类是扩散模型,值得注意的是,DALL-E是自回归领域的先驱之一,而GLIDE则是扩散模型领域的早期代表。我们还将涉及Imagen和LDM。
当我们最初讨论GAN时,提到进行类别条件生成的一种简单方法是让生成器将标签作为输入。你可以将标签(例如其独热编码)与噪声向量一起输入生成器。类似地,要对文本进行条件生成,也可以采用相同的方式,但需要一个函数 Φ 将文本转换为向量表示。这个向量表示可以来自我们讨论过的大型语言模型,如仅编码器或仅解码器的Transformer模型。
以下是2016年最早的生成对抗文本图像合成方法的示意图。其基本思路与类别条件生成类似,只是函数 Φ 首先获取提示的嵌入表示,然后将其与噪声拼接。这个完整的表示(噪声+提示嵌入)被传递给生成器 G 以生成图像 x̂。
判别器 D 同样是一个卷积网络,但在卷积过程中,它再次拼接了文本的表示。因此,判别器在判断输入图像是真实图像还是伪造图像时,也依赖于该提示。通过联合训练提示表示模型 Φ、生成器和判别器,判别器会迫使生成器利用 Φ 的表示,从而生成与提示相符的图像。
我们之前已经见过这类模型的一个例子,即Parti。其核心思想是将图像通过类似VQ-GAN的方法标记化为离散表示。这个离散表示就像一个由小图像块组成的码本,可以组合起来重建图像。
我们可以将其视为一个编码器-解码器模型。编码器将图像转换为中间的离散表示,解码器则将该离散表示重建为原始图像。这就像一个自动编码器,但其潜在表示是离散的。
一旦我们有了编码和解码这种离散潜在表示的方法,就可以将文本条件图像生成问题转化为自回归问题。对于许多训练样本,我们有提示文本,后跟图像的离散表示。
我们首先预训练编码器-解码器模型,以获得能够从该潜在空间进行良好重建的模型。然后,我们训练一个自回归模型,其训练样本是提示文本与编码器给出的离散表示配对。因此,自回归模型实际上并不需要查看原始图像,它只查看图像的离散表示,并学习这些离散表示上的良好分布。
生成过程:要生成图像,你向自回归模型输入提示文本和起始标记,然后开始采样。采样得到 I₁,将其反馈回模型,再采样 I₂,依此类推,直到生成 M 个标记。然后,将这些标记 I₁ 到 Iₘ 交给解码器,解码器会从该离散表示生成图像。
训练过程:
- 训练编码器-解码器部分,以最小化重建损失(通常包含对抗性成分)。
- 使用自回归模型和编码器,最小化已观测序列的负对数似然。
- 生成时,使用自回归模型和“解标记器”(即解码器)来生成图像。
现在,我们进入今天重点关注的领域:文本到图像扩散模型。但在深入潜在扩散之前,我们先简要了解一下DALL-E 2,这需要我们先理解CLIP。
CLIP本身不是生成模型,但其思想非常简单。它包含一个文本编码器(如Transformer语言模型)和一个图像编码器(如CNN或ViT)。对于一批 N 个图像-文本对,文本编码器产生 N 个文本表示向量 T₁...Tₙ,图像编码器产生 N 个图像表示向量 I₁...Iₙ,且确保它们长度相同。
CLIP的目标是构建一个矩阵,计算所有配对向量之间的点积,并尝试提高对角线上的值(即匹配的图像-文本对),同时降低非对角线上的值。本质上,它试图最大化匹配对的点积,使它们的嵌入表示接近。
CLIP的关键在于其规模:它在4亿个图像-文本对上进行了训练。通过扩展到如此巨大的规模,CLIP获得了强大的能力,可用于图像的零样本分类等任务。
零样本分类:对输入图像进行编码得到向量。对于每个可能的标签(如“飞机”、“汽车”、“狗”、“鸟”),将其填入提示模板“a photo of a {object}”,通过文本编码器得到标签的向量表示。然后计算图像向量与每个标签向量的点积,选择得分最高的标签作为预测结果。
CLIP的成功使得其文本编码器和图像编码器被广泛用于各种任务,包括图像生成。DALL-E 2 就利用了这一点。
DALL-E 2 的生成过程:首先,单独训练CLIP模型,获得高质量的图像编码器和文本编码器。然后,训练一个扩散模型,利用文本的实际表示作为先验,来影响扩散概率分布。这个先验试图引导扩散模型,使其生成那些编码后表示与给定文本表示接近的图像。需要注意的是,DALL-E 2 的扩散解码器仍然在像素空间操作。
Imagen 模型:Imagen是另一个扩散模型,它采用了一个文本到图像扩散模型,并耦合了额外的扩散模型进行超分辨率。首先生成一个64x64的图像,然后通过一个超分辨率扩散模型将其提升到256x256,再通过第二个超分辨率模型提升到1024x1024。将文本嵌入传递给超分辨率模型,可能是为了让模型在提升分辨率时也能利用丰富的文本信息。
现在,让我们深入探讨潜在扩散模型。
在像素空间运行扩散模型存在一个问题:训练通常需要数百个GPU天,推理速度也很慢。潜在扩散的核心思想是训练一个自动编码器,学习一个与数据空间在感知上等效的高效潜在空间。这意味着你可以将原始图像压缩到潜在空间,再解码回来,重建结果非常接近原始图像。
保持自动编码器固定不变,然后在真实图像的潜在表示上训练一个扩散模型。编码器将图像 x 转换为潜在表示 z₀。扩散过程包括前向过程(从 z₀ 逐步添加高斯噪声得到 z_T)和反向过程(从噪声 z_T 学习回到 z₀)。
生成图像:
- 在潜在空间中采样一个随机噪声向量 z_T。
- 应用反向扩散过程,从 z_T 得到 z₀。
- 使用自动编码器阶段学习的解码器,将 z₀ 解码回真实图像 x。
最后,为了能够对提示或其他条件进行生成,可以在潜在空间中使用交叉注意力机制。
类比:想象你很难直接将想法画在画布上。但也许你在梦中非常有创造力,可以从白天的随机“噪声”中产生丰富、超现实的意象 z₀。然而,你无法将 z₀ 直接变成画作。如果你是一位出色的作家,你的潜在表示可能就是文字。你可以用寥寥数语(如10个词)捕捉这个高层次的想法,然后将这10个词交给一位专业画家,画家就能根据这些词生成逼真的图像。这就是潜在扩散的思想。
自动编码器:在像素空间操作,将图像 x 编码为 z,并能将 z 解码回 x 的忠实重建。原始LDM论文考察了两种选项:一种是类似VAE的模型,添加了朝向高斯分布的额外正则化;另一种是VQ-GAN,在解码器中进行向量量化,使用图像的离散码本表示。无论使用哪种,自动编码器都提前训练并固定参数,其编解码器被设计为能高效处理高分辨率图像。
提示模型:这部分很简单,就是一个Transformer语言模型或仅编码器模型。我们将其参数与扩散模型一起学习,目标是构建好的文本提示表示,以指导潜在扩散过程。
扩散模型:它遵循DDPM模型。前向过程与标准DDPM相同,只是添加高斯噪声。反向过程也类似,但现在是条件于 ŷ = τ_θ(y),即提示文本的嵌入表示。具体来说,我们将 τ_θ(y) 传递给我们学习的用于预测均值 μ_θ 的函数。
如何定义 μ_θ:回顾标准DDPM,我们曾参数化 μ_θ。在潜在扩散中,我们定义 μ_θ(实际上是预测噪声的 ε_θ)时,加入了交叉注意力机制。
在U-Net的每一层,除了层归一化、卷积、自注意力和MLP外,还加入了交叉注意力层。交叉注意力的关键在于其键和值与查询来自不同的来源。

交叉注意力机制:
- 查询:来自U-Net当前层的表示 φ_i(z_t)。
- 键和值:由文本提示的表示 τ_θ(y) 经过线性变换(W_K, W_V)得到。

公式表示为:Attention(Q, K, V) = softmax(QK^T / sqrt(d)) * V,其中 Q = φ_i(z_t),K = W_K * τ_θ(y),V = W_V * τ_θ(y)。
这允许图像的每个部分(通过查询表示)与文本提示的不同部分进行注意力交互,从而更精细地利用文本信息。
训练:训练算法与DDPM几乎相同,只是现在 ε_θ 条件于文本提示的表示。模型同时优化U-Net噪声模型的参数和大型语言模型的参数。
作用:通过在每个反向扩散步骤中传入文本提示的表示,U-Net会利用这些信息来决定如何去除噪声,从而将图像朝着与该文本提示相关的方向移动。
将所有这些部分组合起来,潜在扩散模型能够在潜在空间中,利用带交叉注意力的U-Net,生成与训练中特定文本提示相对应的 z。在测试时,我们输入提示得到 ŷ,从随机噪声开始,以 ŷ 为条件生成某个 z,然后将其解码为实际图像。
潜在扩散模型的关键优势在于效率。例如,早期的Stable Diffusion模型仅有14.5亿参数,但其性能与参数量大得多的模型(如拥有60亿参数的GLIDE)相当甚至更好。这使得高质量文本到图像生成模型首次以开源形式出现,并且规模适中,其他研究人员可以在此基础上进行构建,而不再仅限于拥有海量计算资源的机构。


本节课中,我们一起学习了潜在扩散模型。我们从文本到图像生成的概览开始,介绍了GAN、自回归和扩散三类主要方法。然后,我们深入探讨了潜在扩散模型的核心思想:通过一个预训练的自动编码器在高效潜在空间中进行扩散,并利用交叉注意力机制将文本条件集成到扩散过程中。这种方法显著降低了计算需求,使得高性能的文本到图像生成模型得以普及。



在本节课中,我们将学习一种名为 Prompt to Prompt 的强大图像编辑技术。该方法允许我们仅通过修改文本提示词,来编辑由扩散模型生成的图像,而无需进行任何额外的模型训练。





上一节我们介绍了条件图像生成的概念。在图像编辑领域,常见的技术包括修复缺失像素的 Inpainting、为黑白图像上色的 Colorization 以及重构图像缺失部分的 Uncropping。本节中,我们将重点探讨如何通过文本提示词来编辑图像。
Prompt to Prompt 方法的核心在于,它能够接收一个由扩散模型生成的图像,并通过简单地调整其生成时使用的文本提示词来对其进行编辑。
为了理解 Prompt to Prompt,我们首先需要回顾其运行的基础——潜在扩散模型。
LDM 的核心思想是:为了提升扩散模型的效率,我们不在高维的像素空间直接操作,而是先在一个低维的潜在空间中进行扩散过程。这个潜在空间通过一个预训练的编码器-解码器模型获得,它能将图像压缩为低维表示,并能将其解码回像素图像。
同时,我们使用一个大语言模型将文本提示词编码为向量表示。在反向扩散过程中,我们通过交叉注意力机制,将文本编码作为条件信息注入到模型中,从而引导图像的生成。
LDM 的反向过程参数化遵循我们之前见过的形式,其均值函数 μ_θ 是 UNet 模型的输出,并且额外条件依赖于文本编码 τ_θ(y):
μ_θ(z_t, t, τ_θ(y))
模型的训练目标是,通过梯度下降最小化损失函数,使模型预测的噪声 ε_θ 接近真实噪声 ε。
Prompt to Prompt 方法的关键在于操纵交叉注意力权重。因此,我们需要深入理解交叉注意力在 LDM 中是如何工作的。
在交叉注意力中,我们有两组不同的输入:
- 来自 UNet 中间层的图像潜在表示(记为
X)。 - 来自文本编码器的文本提示词表示(记为
Y)。
以下是交叉注意力的计算过程:
- 键和值矩阵由文本表示
X通过线性变换得到:K = X * W_K,V = X * W_V。 - 查询矩阵由图像潜在表示
Y通过线性变换得到:Q = Y * W_Q。 - 注意力分数通过查询和键的点积计算,并经过缩放和 Softmax 得到注意力权重矩阵
A。 - 最终的输出是注意力权重
A与值矩阵V的加权和。
其中,d_k 是键向量的维度。注意力权重矩阵 A 的每一列对应一个文本词,每一行对应潜在空间的一个位置(或图像的一个区域)。这建立了文本词与图像区域之间的关联映射。
了解了交叉注意力后,我们现在可以探讨 Prompt to Prompt 的具体实现。该方法旨在无需用户提供掩码的情况下,仅通过修改文本来编辑图像。
其核心思想是:利用原始提示词生成图像时保存的交叉注意力图,在根据新提示词生成图像时,有选择地“注入”这些旧的注意力图,从而在改变内容的同时保持图像的整体结构和布局。
以下是该方法的算法步骤概述:
- 输入:原始提示词
y,目标提示词y*,随机种子s。 - 首次扩散:使用原始提示词
y和随机种子s运行完整的反向扩散过程,并保存每一步的交叉注意力权重矩阵A_t。 - 二次扩散:使用目标提示词
y*和相同的随机种子s初始化噪声,再次运行反向扩散过程。- 在扩散的早期时间步(例如前50%),不使用当前步骤计算出的注意力权重
A*_t,而是注入之前保存的、来自原始生成的注意力权重A_t(可能经过调整,见下文)。 - 在扩散的后期时间步,则切换回使用当前计算的注意力权重
A*_t。
- 在扩散的早期时间步(例如前50%),不使用当前步骤计算出的注意力权重
- 输出:得到由新提示词
y*生成的、但结构与原图相似的编辑后图像。
这种方法之所以有效,是因为图像的大部分结构和布局信息在扩散过程的早期就已确定。早期注入原图的注意力图,可以引导新图像继承相似的构图;后期切换回新提示词的注意力,则允许模型根据新内容自由发挥细节。
一个实际挑战是:原始提示词和目标提示词的词数(或标记数)可能不同,导致它们的注意力权重矩阵 A 和 A* 形状不匹配,无法直接替换。
Prompt to Prompt 通过定义一个映射矩阵 M 来解决这个问题。该矩阵负责将原始提示词的注意力权重“分配”到目标提示词的各个词上。
- 词数相同:
M近似为单位矩阵,直接对应替换。 - 目标词数更多(如“orange cat” -> “big tabby cat”):需要将原词(如“orange”)的注意力权重分配到多个新词(“big”和“tabby”)上。
M中对应的列会包含如[0.5, 0.5]的权重,表示平均分配。 - 目标词数更少(如“big orange cat” -> “tabby cat”):需要将多个原词(“big”和“orange”)的注意力权重合并到一个新词(“tabby”)上。
M中对应的行会包含合并权重。
通过计算 Â = A * M,我们得到一个与 A* 形状相同的、调整后的注意力矩阵 Â,它可以被注入到新提示词的生成过程中。


此外,控制何时从注入的注意力 Â 切换回标准注意力 A* 是一个重要的超参数。我们可以为每个时间步甚至每个词单独定义切换点,从而实现对图像不同部分编辑程度的精细控制。混合公式可以表示为:
A_final = α ⊙ Â + (1 - α) ⊙ A*
其中 α 是一个与注意力矩阵同形的矩阵,其元素在 0 到 1 之间,用于控制混合比例。
通过操纵交叉注意力,Prompt to Prompt 支持多种类型的图像编辑:
- 单词替换:如将“猫”替换为“狗”,保持骑自行车的姿势。
- 属性调整:减弱“拥挤的”这个词的注意力权重,让场景变得空旷。
- 风格转换:通过注入与艺术风格相关的短语,将照片变为油画。
- 内容添加:插入新的短语来为图像添加原本没有的元素。
这些操作都无需训练新模型,只需在采样过程中巧妙地操纵预训练模型的注意力机制即可实现。


本节课我们一起学习了 Prompt to Prompt 这一创新的图像编辑方法。我们回顾了其基础——潜在扩散模型与交叉注意力机制,深入剖析了该方法通过保存并注入原始生成过程的注意力图,以实现仅用文本编辑图像的核心原理。我们还探讨了处理不同长度提示词的注意力图对齐技术,以及控制编辑强度的注意力混合策略。这种方法展示了如何在不更新模型权重的情况下,通过深入理解并操纵模型内部表示(特别是注意力),来实现强大而灵活的生成后控制。
在本节课中,我们将要学习视觉语言模型的核心概念、两种主要的图像编码器架构以及相关的训练数据。视觉语言模型是一种能够同时处理图像和文本输入,并生成文本或图像输出的模型,是实现通用人工智能愿景的重要一步。


上一节我们介绍了视觉语言模型的基本概念。本节中我们来看看其核心架构。视觉语言模型的目标是让语言模型能够“看见”并理解图像。其基本思想是将图像转换为语言模型能够理解的格式。
一个标准的、基于Transformer的纯文本语言模型,其输入是一个文本序列 X。这个序列通过分词器被转换成一个整数列表,即词元。这些词元是Transformer能够接受的输入形式。

视觉语言模型的核心挑战在于,图像本身并不是Transformer能直接接受的词元序列。因此,我们需要一个视觉编码器,将图像映射为一系列向量或整数。
视觉语言模型通常由两部分组成:
- 一个标准的语言模型。
- 一个视觉编码器。
视觉编码器接收图像作为输入,并输出一个与语言模型输入兼容的序列(向量或整数)。然后,这个序列与文本词元一起被送入语言模型进行处理。


现在,我们来深入探讨第一种图像编码器。目前,业界主要使用两种类型的图像编码器:基于VQ-VAE的编码器(被Google Gemini等模型使用)和基于CLIP的编码器(被OpenAI GPT-4V等模型使用)。本节我们先介绍VQ-VAE。

VQ-VAE(Vector Quantized Variational Autoencoder)的核心目标是将一张任意尺寸的图像编码成一个整数词元序列。每个整数都来自一个固定的“图像词汇表”,其大小通常远小于语言模型的词汇表。

关键在于,我们如何确保这个整数序列能够有效地代表原始图像?VQ-VAE通过自编码器结构来解决这个问题。

VQ-VAE包含一个编码器和一个解码器,它们被同时训练。编码器将图像映射为整数序列,解码器则尝试根据这个整数序列重建原始图像。训练目标是使重建的图像尽可能接近原始图像。
公式表示的重建目标:
L_ae = E[ || Decoder(Encoder(I)) - I ||^2 ]
其中 I 代表原始图像。
如果解码器能够从整数序列中较好地恢复图像,那么就证明这个序列包含了图像的足够信息。
然而,编码器的输出是离散的整数,这导致梯度无法回传,无法直接使用梯度下降法训练。VQ-VAE使用了一个巧妙的训练技巧。
编码器实际上先输出一个连续的向量序列。然后,我们维护一个可学习的码本,其中包含 N 个向量 {E1, E2, ..., EN}。对于编码器输出的每个连续向量,我们找到码本中与之最接近的向量 E_i,并用该向量的索引 i 作为最终的离散词元。
向量量化损失:
L_vq = Σ_i || vector_i - E_{R(i)} ||^2
其中 R(i) 表示将第 i 个向量映射到的码本向量的索引。
这个损失函数鼓励编码器输出的连续向量聚集在码本向量周围,使得离散化过程的信息损失最小。在反向传播时,argmin 操作被视为常数,梯度通过“直通估计器”直接从解码器传到编码器的连续输出。
当VQ-VAE编码器训练完成后,它就被固定下来。对于任何图像,我们都能通过它得到一个固定的整数词元序列。

在训练视觉语言模型时,我们需要为图像词元和文本词元使用不同的嵌入层。模型通过特殊的标记(如 )来区分输入中的图像部分和文本部分。语言模型头也需要区分下一个要预测的是图像词元还是文本词元。
训练目标是标准的自回归下一个词元预测,同时作用于图像词元和文本词元。这意味着该架构不仅可以根据文本生成文本,还可以根据文本生成图像词元序列,再通过VQ-VAE解码器生成图像。
上一节我们介绍了能生成图像的VQ-VAE编码器。本节中我们来看看另一种思路不同的编码器:CLIP。
CLIP(Contrastive Language-Image Pre-training)编码器与VQ-VAE有根本不同。它不输出离散的词元,而是将图像编码为一个连续的向量序列。每个向量位于一个高维空间中(例如768维)。
CLIP的训练目标不是重建图像,而是学习图像和文本在语义上的对齐。它同时训练一个图像编码器和一个文本编码器。

CLIP训练目标是:对于匹配的图像-文本对,它们的编码应该相似;对于不匹配的对,它们的编码应该不同。这通过对比损失函数实现。
公式化的对比损失(简化):
模型需要最大化匹配对 (image_i, text_i) 的编码相似度 sim(E_img(image_i), E_txt(text_i)),同时最小化它与所有其他非匹配文本 text_j (j≠i) 的相似度。
这使得CLIP学习到的图像向量序列天然具有与文本向量相似的语义结构,非常适合被后续的语言模型理解。

使用CLIP编码器的视觉语言模型,其架构与使用VQ-VAE时类似,但有一个关键区别:由于图像输入是连续向量而非离散词元,因此训练损失只作用于文本词元。
模型流程如下:
- 文本被转换为词元序列。
- 图像被CLIP编码器转换为向量序列。
- 文本词元和图像向量通过各自的嵌入层映射到同一空间,并拼接在一起。
- 序列经过Transformer块处理。
- 在语言模型头处,只对文本部分的位置计算下一个词元预测损失,图像部分的位置被忽略。
因此,基于CLIP的视觉语言模型只能生成文本,不能直接生成图像。它的优势在于图像表示更丰富、训练更简单,且语义对齐更好。



我们已经了解了视觉语言模型的架构。然而,在生成式AI中,训练数据往往比模型架构更重要。以下是训练视觉语言模型常用的几种数据类型:

- 图像-标题对:从网络(如谷歌图片)爬取的带有描述性文本的图像。
- 交错图文数据:网页、PDF、学术论文中的图文混排内容。这是数据量最大但解析最困难的一类。
- 教科书与练习题:包含图表、公式的学术材料,对提升模型推理能力至关重要。
- 合成数据(Chart QA/Table QA):使用代码或语言模型自动生成的图表、表格及其相关问题。这是可无限扩展的高质量数据源。
- 文档布局理解数据:对网页或文档截图进行人工标注,用于训练模型理解UI元素、布局结构。
- OCR(光学字符识别)数据:将PDF页面作为图像输入,原始文本/LaTeX作为输出,训练模型“阅读”文档。
为了评估视觉语言模型的能力,业界也建立了一系列基准测试,它们与上述训练数据紧密对应:
- MMMU:涵盖大学级别多学科问题的综合基准。
- Chart QA / DocVQA:测试图表和文档理解能力。
- TextVQA:测试图像中文本的阅读和理解能力。
- OCR基准:测试光学字符识别精度。
- 视觉细节理解:测试高分辨率图像下的细粒度识别能力。

本节课中我们一起学习了视觉语言模型的核心内容。
我们首先了解了视觉语言模型的基本架构,即通过一个视觉编码器将图像转换为语言模型可处理的序列。接着,我们深入探讨了两种主流的图像编码器:基于VQ-VAE的编码器和基于CLIP的编码器。VQ-VAE通过向量量化将图像离散化为词元序列,支持图像生成;而CLIP通过对比学习得到连续的图像向量序列,语义对齐更好,但不支持直接图像生成。

最后,我们认识到对于视觉语言模型乃至所有大模型而言,高质量、多样化的训练数据是性能提升的关键,并介绍了几类重要的训练数据源和评估基准。


视觉语言模型是迈向多模态通用人工智能的重要一步,它将视觉感知与语言推理相结合,极大地扩展了模型的理解和应用边界。

在本节课中,我们将探讨一个在大型语言模型训练中至关重要但通常被视为商业机密的概念:缩放定律。我们将学习两种核心的缩放定律,它们能帮助我们科学地预测模型性能与模型规模、训练计算量之间的关系。


训练大型语言模型的计算成本极高。例如,GPT-4是一个拥有1.6万亿参数的模型,在约100万亿个令牌上进行训练。在投入如此巨大的资源之前,我们需要进行预测:对于一个给定规模的模型,投入多少计算量能获得怎样的性能?这就是缩放定律要解决的问题。它通过在较小规模的模型上进行实验,总结规律,并外推预测大规模模型的行为。



上一节我们介绍了缩放定律的基本概念,本节中我们来看看第一种具体的定律:容量缩放定律。它回答一个核心问题:一个拥有 M 个参数的模型,最多能记住多少条事实性知识?

为了精确研究,我们需要一个可控的实验环境。我们不会在真实的互联网数据上进行,因为几乎无法精确统计模型记住了多少知识。
我们采用一种简化的“传记”数据。每条知识被定义为一个 (名称,属性,值) 三元组。例如:(哈佛大学,所在地,马萨诸塞州剑桥市)。我们创建一批合成传记数据,每条包含6个固定属性(如生日、大学等),属性值随机生成。这样,知识的总量和结构是完全已知且可控的。

核心测量方法:
- 用纯传记数据训练一个语言模型。
- 训练后,向模型提问(例如,“某人的生日是什么?”),并测量其回答的准确率(精确匹配)。
- 模型能正确回答的知识数量,即为其记忆的知识量。
模型记忆知识的方式可以很“聪明”。例如,如果所有人的生日只集中在10个日期,模型可以建立一个映射表,而不是为每个人单独存储日期字符串。因此,我们需要一个基准来衡量模型记忆效率的上限:即信息理论最优编码所需的最少比特数。
对于我们的传记数据结构,存在一个数学公式可以计算这个最优比特数 B_opt。它取决于多个因素,例如知识条目数 N、属性值的多样性、以及模型达到的损失值 L。公式大致形式如下(具体系数取决于数据特性):

B_opt ≈ C1 * N * L + C2 * N * log(D) + C3 * T * log(L)
其中,D 代表属性值的可能取值数量,T 与文本长度相关,L 是交叉熵损失。这个 B_opt 值代表了存储这些知识所需信息量的理论下限,我们将用它来衡量模型实际记忆的“知识量”。
我们在不同规模(从2600万到10亿参数)的Transformer模型上进行实验,并充分训练它们(约1000个数据轮次),直至性能不再提升。
以下是关键发现:
- 2比特/参数定律:无论模型架构细节如何(层数、头数、MLP与注意力层的比例),所有充分训练的Transformer模型,其有效记忆容量都趋近于 每参数2比特。这意味着一个 M 个参数的模型,最多能记忆约 2M 比特的最优编码信息。
- 量化几乎无损:使用FP8甚至INT8格式训练模型,依然遵循相同的2比特/参数定律。这表明低精度训练是可行且高效的。
- 对GPT-4的启示:GPT-4约有1.6万亿参数,其理论记忆容量约为 3.2万亿比特。而估算所有人类教科书知识总量大约在 200亿比特 左右。因此,仅从记忆所有教科书知识的角度看,一个约100亿参数的模型就足够了。GPT-4的规模远超此需求。

然而,上述理想定律的前提是训练数据纯净(全是需要记忆的知识)。如果数据中混杂了大量“垃圾”信息(无用或随机的互联网文本),模型的训练效率会急剧下降。
实验表明,如果信号(有用知识)与噪声(垃圾信息)的比例为 1:7,那么要达到接近最优的容量,所需的训练数据量(或等效训练步数)将增加约 64倍。因此,高质量的数据过滤对于高效训练至关重要。

上一节我们了解了模型容量的上限,本节中我们来看看在有限计算预算下,如何最有效地训练模型。这就是由DeepMind提出的 Chinchilla 缩放定律(或称计算缩放定律)。
它研究模型规模(参数数量 N)、训练数据量(令牌数 D)和最终模型损失 L 三者之间的关系。其核心结论是:在固定的总计算预算 C ≈ 6ND(用于训练前向和反向传播)下,模型参数数量和训练数据量应该成比例地增长。

一个经验性的最优比例是:每10亿参数,大约需要20亿个训练令牌。例如,一个70亿参数的模型,最优训练数据量应在1.4万亿令牌左右。

许多现有模型的训练并未遵循此定律:
- 训练不足:例如,一些大型模型(如700亿参数)的训练令牌数可能和较小模型(如70亿参数)一样多。根据Chinchilla定律,这意味著大型模型训练不足,没有发挥其全部潜力。
- 规模效率悖论:一个反直觉但关键的推论是:扩大模型规模有时能提高训练效率。如果一个模型处于容量临界点(刚好能记住所有数据),其参数必须被极度压缩利用,优化过程会变慢。而一个规模更大的模型(“过度参数化”)有更宽松的优化空间,可能用更少的训练步数就能达到相同的性能。因此,在计算受限时,选择比理论最优容量更大的模型,可能是更快的训练策略。


本节课我们一起学习了两种核心的缩放定律:
- 容量缩放定律:揭示了Transformer模型记忆能力的理论上限(约每参数2比特),并强调了高质量训练数据过滤的极端重要性。
- 计算缩放定律:指出了在固定计算预算下,模型规模与训练数据量应平衡缩放,并解释了为何在实践中常常会使用“过度参数化”的模型以加速训练。

这些定律是指导大型语言模型研发的科学基础,帮助我们在投入海量资源前进行性能预测和资源规划。尽管真实的工业级缩放定律更为复杂且保密,但其核心思想均源于此类基础研究。

在本节课中,我们将学习一种称为混合专家模型 (Mixture of Experts, MoE) 的特殊架构。这种架构在当前非常重要,因为所有大规模模型实际上都使用它来加速推理时间。我们将了解MoE的直观原理、其为何有用,以及成功训练MoE层所需的一些计算技巧。





上一节我们介绍了扩展语言模型的好处:它能提升模型容量,并且令人惊讶的是,在总计算量(FLOPs)方面,更大的模型收敛得更快。因此,从训练成本角度看,我们希望模型尽可能大。

然而,这些大模型与Hessian矩阵模型相比有一个缺点:推理成本要高得多。例如,一个7B参数的模型推理成本远低于一个70B参数的模型。在模型部署时,我们每天需要为数百万甚至数亿用户提供服务,这是一个巨大的推理开销。因此,我们希望在不牺牲模型容量或训练优势的前提下,尽可能降低推理成本。
接下来的课程将介绍一些降低语言模型推理成本的技术。本节课重点讨论混合专家模型架构,它主要用于替换或高效实现Transformer中的MLP层。




一个标准的Transformer块主要由两种类型的层构成:自注意力层和前馈网络层。MoE层将作为MLP层的一个更高效的替代品,用于加速推理。下一讲我们将学习使注意力层推理更快的技巧。


目前,所有大型语言模型都使用了MoE架构,例如GPT-4、GPT-3.5、Mixtral和Google的Gemini 2等。


在Transformer中,MLP层更像是知识的存储单元,存储事实性知识,而自注意力层则负责逻辑推理。人类的实际知识是非常稀疏的。当我们向模型提问时,模型通常只需要提取与问题相关的一小部分知识。
MoE的核心理念是将MLP层划分为多个“专家”块,每个专家可能专注于某一类知识。在推理时,根据输入内容,模型只激活并使用少数相关的专家进行计算,而忽略其他专家。这就像为每个问题只调用相关的知识库。




一个MoE层包含以下组件:
- 输入:一个维度为
d的向量x。 - 路由器:一个线性层,将输入
x映射到维度为M的向量,其中M是专家数量。 - 门控值:对路由器输出应用Softmax,得到一个概率分布
s(x)。 - 专家选择:根据
s(x)选择概率最高的前K个专家(通常K=2)。 - 专家网络:每个专家本身是一个标准的MLP层。
- 输出计算:最终输出是所选专家输出的加权和,权重是所选专家门控值的重新归一化结果。


公式表示如下:


- 路由器输出:
g(x) = W_router * x - 门控概率:
s(x) = Softmax(g(x)) - 选择函数:
I_k(x)表示第k个被选中的专家索引。 - 重新归一化权重:
s'(x)_i = s(x)_i / (sum_{j in I_k(x)} s(x)_j),仅对选中的专家i有效。 - 最终输出:
MoE(x) = sum_{i in I_k(x)} s'(x)_i * Expert_i(x)

在Transformer中,MoE层替换了原有的MLP层,并对序列中的每个令牌独立应用。所有令牌共享同一个MoE层,但不同令牌可能激活不同的专家子集。

对于MoE模型,需要区分两个概念:
- 总参数量:模型中所有参数的总和,包括所有专家的参数。
- 有效参数量:每个令牌前向传播时实际激活的参数数量,约为
K * (单个专家参数量)。

MoE的关键优势在于,其知识存储的缩放律(约1.5比特/参数)是基于总参数量的,而推理成本仅与有效参数量相关。这意味着你可以用一个总参数量巨大(从而知识容量大)的模型,但每个令牌的推理只涉及其中一小部分,实现了效率与容量的平衡。


简单地用PyTorch实现上述公式无法利用稀疏性获得加速,因为框架仍会计算所有专家。为了实现高效计算,需要采用“快速编解码”策略。


以下是实现思路:
- 编码:对于一个批次中的令牌序列,根据路由器结果,将使用相同专家的令牌聚集到一起,形成一个规整的张量。
- 专家计算:将每个聚集后的张量发送给对应的专家进行批量计算。
- 解码:将专家计算后的结果根据原始令牌顺序重新组装回去。
为了处理专家间负载可能不均衡的问题,通常会设置一个“容量因子”(如1.1-1.25),为每个专家分配略高于平均预期的计算缓冲区,以避免令牌被丢弃。
MoE架构天然适合一种称为“专家并行化”的分布式训练策略。我们可以将不同的专家放置在不同的GPU上。在前向和反向传播时,只需将令牌数据路由到对应的GPU上进行专家计算,专家之间无需频繁通信。这比传统的张量并行或流水线并行效率更高,是训练超大规模MoE模型的关键。


在训练MoE时,我们希望所有专家都能被相对均匀地使用,以避免某些专家过载而其他专家未被充分训练。为此,我们会在损失函数中添加一个“负载均衡损失”。


负载均衡损失公式:
L_balance = sum_i (f_i * p_i)
其中:
f_i是实际使用专家i的令牌比例。p_i是所有令牌的门控概率在专家i上的总和。
最小化这个损失会鼓励路由分布和专家使用频率都趋向均匀。这是稳定训练MoE模型的重要技巧。

本节课我们一起学习了混合专家模型。我们了解了MoE如何通过为每个输入动态选择少数专家来大幅降低推理成本,同时保持庞大的总参数量以存储海量知识。我们还探讨了MoE的关键实现技术,如快速编解码和专家并行化,以及训练稳定所需的负载均衡损失。MoE是目前构建超大规模高效语言模型不可或缺的核心架构之一。

在本节课中,我们将学习如何更高效地计算注意力单元。我们将重点介绍两种方法:Flash Attention 和 Multi-Query Attention。这些技术能显著加速注意力计算并大幅降低内存使用量,尤其是在处理长序列时。




上一节我们介绍了如何通过稀疏化来加速MLP层的计算。本节中,我们来看看如何优化注意力层的计算。

首先,让我们回顾一下注意力层的基本结构。多头注意力层由多个注意力头组成。给定输入向量 V₁ 到 Vₙ(其中 n 是序列长度,每个向量的维度是 d),注意力层的输出需要经过一系列计算。


注意力公式 可以表示为:
输出_i = Σ_j (softmax( (Q_i * K_j^T) / sqrt(d_k) ) * V_j)



其中,Q、K、V 分别是查询、键和值矩阵。本质上,你需要计算一个 n × n 的注意力分数矩阵,然后对每一行进行 softmax 归一化,最后根据归一化后的权重对值向量进行加权求和。
你可以将 V_i 视为一个查询。对于一个查询,你需要计算它与所有键的内积,找出最相似的键。然后,该查询的输出将是相似区域值的加权平均。这就像根据当前词与文档中其他词的相似性来理解其上下文。
主要问题是,这个 n × n 的注意力矩阵在计算和内存使用上都是 O(n²) 的。当序列长度 n 很大时(例如 100k),这个矩阵会变得极其庞大,内存消耗会非常高。




在标准的 Transformer 块中,包含注意力层、层归一化、残差连接和 MLP(或 MoE)层。




计算时间分析:
- MLP层:计算时间约为 n × d²。
- 多头注意力层:计算时间约为 n² × d + n × d²。其中 n × d² 部分与 MLP 层类似,但 n² × d 项在序列很长时会占主导。


内存使用分析:
- MLP层:只需存储每个 token 的激活值,内存使用约为 n × d。
- 多头注意力层:需要存储 n × n × M 的注意力矩阵(其中 M 是注意力头数,通常与 d 成线性关系)。因此,内存使用约为 n² × d,这在序列很长时会成为主要瓶颈。
所以,注意力层在长序列处理上,无论是计算时间还是内存使用,都面临巨大挑战。我们的目标是学习一种名为 Flash Attention 的技术,它能将注意力层的内存使用从 O(n²) 有效降低到几乎 O(n)。


Flash Attention 的关键思想是:不将整个 n × n 注意力矩阵一次性存储在内存中,而是只存储其中的一部分,并通过更巧妙的计算和重计算来避免存储全部中间结果。



为了理解其原理,我们先看一个注意力头中单行的计算。问题是:能否在不将所有中间值存入内存的情况下,计算这一行的输出(即加权和)?


一个简单但低效的方法是顺序计算加权和,只维护一个累加器,而不存储所有中间分数。但这会带来两个问题:1) 顺序循环导致计算慢;2) 数值稳定性问题(softmax 中的指数运算可能导致浮点数溢出)。


以下是解决数值稳定性问题的关键技巧:在计算 softmax 时减去最大值。

稳定 softmax 计算伪代码:
# 输入: 向量 x = [x1, x2, ..., xn] m = max(x) # 找到最大值 exp_x_shifted = exp(x - m) # 每个元素减去最大值后求指数 sum_exp = sum(exp_x_shifted) softmax_x = exp_x_shifted / sum_exp
这样做确保了指数运算的结果在 (0, 1] 范围内,避免了溢出,并且不改变最终的 softmax 结果。





然而,上述方法需要遍历数据两次(一次求最大值,一次计算加权和),或者需要将数据存入内存。Flash Attention 的精妙之处在于引入了 “运行最大值” 的概念,使得只需遍历数据一次即可同时完成稳定化的加权和计算。


Flash Attention 核心算法思想(简化版):
初始化 O = 0向量, l = 0, m = -inf for i in 1 to n: xi = 计算当前查询与第i个键的内积 m_new = max(m, xi) # 根据新的运行最大值更新累加器 l = l * exp(m - m_new) + exp(xi - m_new) O = O * exp(m - m_new) + Vi * exp(xi - m_new) m = m_new 最终输出 = O / l
这个算法只存储了运行最大值 m、归一化因子 l 和输出累加器 O,内存使用很低,且只遍历数据一次。但它仍然是一个顺序循环,没有利用硬件擅长的快速矩阵乘法。


最终的 Flash Attention V2 算法结合了所有优点:
- 分块处理:将长的序列分成较小的块(例如 96x96)。
- 块内快速计算:在每个块内部,使用标准的、高效的矩阵乘法来计算注意力分数和 softmax。因为块很小,所以即使存储中间矩阵,内存开销也有限,同时能利用硬件对特定尺寸矩阵运算的优化。
- 块间顺序更新:像核心算法思想一样,在块与块之间使用运行最大值和累加器进行顺序更新,确保数值稳定性并避免存储全局大矩阵。

高级别流程:
将 Q, K, V 矩阵分成多个块 初始化全局累加器 O_global, l_global, m_global for 每个块 j: # 1. 使用快速矩阵乘法计算当前块内的注意力分数 S_block # 2. 在块内计算 softmax (使用块内最大值进行稳定化) # 3. 计算当前块的输出 O_block # 4. 用运行最大值方法,将 O_block 与全局累加器 O_global, l_global, m_global 合并 最终输出 = O_global / l_global




这个算法在数学上被证明能产生与原始注意力计算完全相同的精确结果。它极大地减少了内存占用(从 O(n²) 到 O(n)),同时通过分块利用了硬件的计算效率。目前,Flash Attention 已集成在 Hugging Face Transformers 库中,是加速推理和训练的关键技术。



除了 Flash Attention,还有其他用于加速注意力计算的方法:
- Multi-Query Attention (MQA):让多个注意力头共享同一个键(K)和值(V)矩阵,显著减少计算量。例如,Mistral 模型就使用了 MQA。
- Sliding Window Attention:每个 token 只关注其附近一个窗口内的 token,而不是整个序列,将计算复杂度从 O(n²) 降到 O(n × w),其中 w 是窗口大小。
- Blockwise Attention:将序列分成块,主要进行块内的注意力计算,减少长距离依赖的计算开销。

本节课中,我们一起深入探讨了如何优化 Transformer 中注意力机制的计算。
- 我们回顾了注意力层在长序列下面临的 O(n²) 计算和内存瓶颈。
- 我们详细剖析了 Flash Attention 的核心思想:通过避免存储完整的注意力矩阵,并利用运行最大值、分块计算和顺序更新等技巧,在保证数值稳定的同时,将内存占用降至 O(n),并保持了计算效率。
- 我们还简要了解了其他如 Multi-Query Attention 等优化方法。

掌握这些优化技术对于理解和部署高效的大语言模型至关重要。
在本节课中,我们将学习生成式AI中一个至关重要的主题:分布式训练。随着语言模型和生成模型规模的不断扩大,传统的单GPU训练方式已无法满足需求。分布式训练通过在多块GPU上协同工作,解决了大模型训练中的内存和计算瓶颈。我们将从基础概念入手,逐步探讨几种关键的分布式训练技术。




上一节我们介绍了单GPU训练的简单流程。本节中我们来看看当拥有多块GPU时,最直接的加速方法:数据并行。
在数据并行中,每块GPU都拥有完整的模型副本,但处理不同的数据批次。以下是其核心步骤:
- 数据分发:将训练数据批次分割,每块GPU获得一个子批次。
- 独立前向与反向传播:每块GPU使用自己的数据独立完成前向传播和损失计算,并计算梯度。
- 梯度同步:所有GPU计算出的梯度通过通信操作(如
all_reduce)进行求和或平均。 - 参数更新:每块GPU使用同步后的梯度统一更新其模型参数。
数据并行的核心优势是通信开销小,通常只需在梯度同步时进行通信。在PyTorch中,这可以通过一行代码实现:
model = torch.nn.DataParallel(model)
然而,数据并行要求每块GPU都能在内存中容纳完整的模型参数、梯度和优化器状态。对于当今动辄数十亿参数的大模型,这变得不可能。



上一节我们了解了数据并行的局限性。本节中我们来看看驱动更高级分布式训练技术的核心挑战:GPU内存限制。

训练一个大语言模型时,主要的内存消耗来自三个方面:
- 模型参数:存储模型权重,通常使用
FP16或BF16精度以节省内存。一个70亿参数的模型约需 14 GB 内存(FP16)。 - 优化器状态:对于像Adam这样的优化器,需要存储动量和二阶动量。为了数值稳定性,这些状态通常以
FP32精度存储。对于一个70亿参数的模型,优化器状态约需 56 GB 内存。 - 激活值(Activations)与梯度:在前向传播中产生的激活值,以及在反向传播中计算的梯度,也会消耗大量内存,其大小与模型参数和批次大小相关。
简单相加,仅存储一个70亿参数模型的权重和优化器状态就可能超过 70 GB,这已经接近甚至超过单块高端GPU(如H100的80GB)的内存容量,尚未计算激活值和梯度所需的内存。因此,无法在单GPU上训练此类模型。
上一节我们明确了内存是主要瓶颈。本节中我们来看看第一种高级技术:优化器状态分片(也称为DeepSpeed ZeRO阶段1)。
这项技术的核心思想是:不再在每块GPU上存储完整的优化器状态,而是将其均匀分片,每块GPU只存储其中一份。
以下是其工作流程:
- 模型复制:每块GPU仍然存储完整的模型参数副本。
- 优化器状态分片:将优化器状态(如Adam的动量和二阶动量)分割成N份(N为GPU数量),每块GPU存储其中一份。
- 梯度计算与同步:每块GPU独立计算其数据子批次对应的梯度。然后,通过一个
reduce_scatter通信操作:- Reduce(规约):将所有GPU上对应同一参数分片的梯度进行求和。
- Scatter(散射):将求和后的梯度分片发送回负责该参数分片优化器状态的GPU。
- 参数更新:每块GPU使用接收到的梯度分片和本地存储的优化器状态分片,更新其负责的那部分模型参数。
- 参数同步:更新完成后,通过通信(如
all_gather)将更新后的参数分片同步到所有GPU,确保每块GPU上的完整模型参数保持一致。
通过分片优化器状态,我们将其内存占用从 56 GB 降低到了约 56/N GB。对于8块GPU,这意味着一块70亿参数模型所需的内存从约70GB降至约 14(参数) + 7(优化器分片) + 梯度内存 ≈ 28 GB 左右,使得训练成为可能。
上一节我们通过分片优化器状态节省了大量内存。本节中我们来看看更激进的方案:完全分片数据并行,它同时分片模型参数、梯度和优化器状态。
FSDP是优化器状态分片的自然延伸。其核心思想是:
- 不仅将优化器状态分片,也将模型参数本身进行分片存储。
- 每块GPU只存储整个模型的一个参数子集以及对应的优化器状态分片。
其工作流程更为复杂:
- 前向传播:当计算需要某些不在本地的参数时,通过
all_gather通信从其他GPU收集这些参数,在本地临时组装成完整层进行计算,计算后释放这些临时参数。 - 反向传播:类似地,在计算梯度时,再次通过
all_gather组装所需参数。计算出的梯度根据其对应的参数分片进行分片。 - 梯度同步与更新:使用类似优化器分片中的
reduce_scatter操作同步梯度分片。每块GPU然后使用本地存储的参数分片、梯度分片和优化器状态分片进行更新。 - 参数同步:更新后的参数分片可能需要同步(取决于实现)。
FSDP的优势在于:
- 内存效率极高:理论上可以训练模型的大小与GPU总数成线性比例。
- 支持更高精度:可以在本地以
FP32精度维护参数分片和优化器状态,进行更精确的更新,同时在前向/反向传播时使用FP16/BF16以节省内存和加速计算。
在PyTorch中,FSDP可以通过一个包装器使用:
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP model = FSDP(model)
上一节介绍的FSDP通过在垂直方向(参数)上分片来节省内存。本节中我们来看看另一种维度:张量并行,它通过水平分割单个层的计算来分布内存和计算负载。
张量并行的核心是将一个大型神经网络层(如线性层)的矩阵运算分布到多个GPU上。以线性层 Y = XA 为例(X 输入,A 权重矩阵)。
主要有两种分割方式:
- 列并行(Column Parallel):将权重矩阵
A按列分割。每块GPU持有A的一部分列。计算时,每块GPU计算X与本地权重子矩阵的乘积,得到输出子部分,然后通过all_gather通信将所有GPU的输出子部分拼接成完整的Y。- 前向传播公式:
Y = [X * A1, X * A2, ...],然后all_gather。 - 内存:每块GPU存储的
A的大小变为原来的1/N。
- 前向传播公式:
- 行并行(Row Parallel):将权重矩阵
A按行分割。此时,需要先将输入X通过all_gather广播到所有GPU(或更高效地,按行分割X并all_gather结果)。每块GPU计算本地X子部分与本地A子矩阵的乘积,然后通过reduce_sum通信对所有GPU的结果求和,得到完整的Y。- 前向传播公式:每块GPU计算
Yi = Xi * Ai,然后对所有Yi进行reduce_sum。
- 前向传播公式:每块GPU计算
在实践中,一个线性层的前向传播可能采用列并行,而为了高效地进行反向传播,其对应的梯度计算会自然地采用行并行。像Megatron-LM这样的库提供了封装好的并行线性层,用户只需用它们替换标准线性层即可构建张量并行模型。
张量并行的主要挑战是通信频繁,因为几乎每一层的前后向传播都需要GPU间通信。它通常用于模型实在太大,无法用FSDP装入单个GPU内存的情况。
在实际的超大规模模型训练中(如训练GPT-4或Llama),通常会混合使用多种并行策略:
- 数据并行:在不同的GPU组上处理不同的数据批次。用于扩大有效批次大小。
- 张量并行:在单个GPU组内,将大型模型层拆分到多个GPU上。用于解决单层参数过大的问题。
- 流水线并行:将模型的不同层组放置在不同的GPU上。一个批次的数据像流水线一样依次经过这些GPU。用于解决模型深度过大的问题。
- 专家并行:用于混合专家模型,将不同的专家分布到不同的GPU上。
系统会根据硬件拓扑(如NVLink连接)和模型结构,将数千块GPU划分成不同的并行组,以最大化计算效率和最小化通信开销。
本节课中我们一起学习了分布式训练的核心概念。我们从最简单的数据并行开始,揭示了其内存瓶颈,进而深入探讨了优化器状态分片、完全分片数据并行和张量并行等高级技术。这些技术通过巧妙地分割模型参数、优化器状态和计算图,使得训练拥有数千亿参数的大模型成为可能。理解这些原理是从事前沿大模型开发和优化的基础。

在本节课中,我们将探讨大语言模型中长上下文处理的核心技术。我们将了解为何长上下文至关重要,以及如何通过序列并行化、改进的注意力机制和位置编码外推等技术来高效地训练和使用具有长上下文能力的模型。

上一节我们介绍了长上下文的基本概念。本节中,我们来看看长上下文为何成为现代大语言模型的核心能力。
目前,大语言模型最重要的应用之一是作为智能体,阅读和理解企业文档,例如公司政策或工具使用说明。模型需要能够从这些文档中提取信息并可靠地回答相关问题。
例如,如果你向一个支持100万令牌上下文的模型输入一本“书”,然后询问“要完成XX任务,我应该使用公司的哪个特定工具?”,模型需要能够从这海量信息中准确找到答案。这对于新员工快速熟悉公司架构和工具非常有价值,也是当前语言模型商业化的主要方向。
因此,长上下文能力是当前大语言模型产品的核心竞争力,仅仅支持4K上下文的模型已无法满足市场需求。

我们了解到长上下文很重要,但直接训练支持长上下文的模型并非易事。本节将探讨其中的挑战和主流训练策略。
回顾之前的内容,标准的语言模型训练通常分为三个阶段。在第一阶段(预训练),出于速度和效率考虑,上下文长度通常限制在4K。如果一开始就使用过长的上下文(如16K以上),模型在训练初期会感到“困惑”,因为它不知道在预测下一个词时应该关注前文中的哪个部分,导致损失难以下降。
较短的上下文强制模型专注于局部一致性,使训练更容易。因此,长上下文的扩展通常在第二阶段进行,此时模型已经理解了语言的基本规律。第二阶段通常是长短上下文混合训练,以保持模型在短上下文任务上的性能。第三阶段(后训练)则保持长上下文进行微调。
要训练支持长上下文的模型,首先需要解决计算和内存的挑战。本节介绍一个关键的基础技术:序列并行化。
对于一个大模型(例如70亿参数),如果使用8K上下文,在不进行任何并行优化的情况下,一个微批次就可能占满一张H100 GPU(80GB)的内存。当上下文长度扩展到128K时,内存消耗可能增加16倍以上,单卡根本无法容纳。
序列并行化是一种高效的解决方案。其核心思想是沿着序列维度对训练样本进行切分。
假设我们有一个长度为128K的序列。我们可以将其切分成多个片段,例如:
- GPU 0 处理第 1 到 8K 个令牌。
- GPU 1 处理第 8K 到 16K 个令牌。
- 以此类推。
对于MLP层,每个令牌的计算是独立的,因此序列并行化不需要额外的通信。主要的通信开销发生在注意力层,因为每个令牌在计算注意力时可能需要关注其他GPU上的令牌。为了减少这种通信开销,需要结合使用改进的注意力机制,如滑动窗口注意力、稀疏注意力或环状注意力。


序列并行化解决了内存问题,但注意力计算本身仍是瓶颈。本节我们看看几种能提升长上下文处理效率的注意力变体。
以下是几种常见的用于长上下文的注意力机制:
- 滑动窗口注意力
- 公式/概念:每个令牌
i只关注其前面一个固定窗口大小W(例如2047)内的令牌,即关注[i-W, i-1]范围内的令牌。 - 优点:极大减少了计算和通信量,因为注意力范围是局部的。
- 缺点:纯局部注意力可能难以捕捉长距离依赖,尽管信息可以通过多层网络间接传递。
- 公式/概念:每个令牌
- 稀疏注意力
- 公式/概念:在不同层使用不同的注意力模式。例如,第一层使用局部滑动窗口注意力;第二层则让令牌
i关注i-100,i-200,i-300等间隔较远的令牌。 - 优点:这种“跳跃式”连接非常适合信息检索任务。低层汇总局部窗口信息,高层则在这些汇总信息间进行搜索。
- 缺点:实现更复杂,需要精心设计稀疏模式。
- 公式/概念:在不同层使用不同的注意力模式。例如,第一层使用局部滑动窗口注意力;第二层则让令牌
- 环状注意力
- 概念:一种更极致的优化,确保通信只发生在相邻GPU之间,类似于滑动窗口,但设计上可能更高效。
结合序列并行化和这些稀疏注意力机制,可以有效地训练和运行支持长上下文的模型。

解决了计算问题后,另一个关键挑战是位置编码。模型需要知道令牌的顺序。本节探讨如何将位置编码扩展到训练时未见过的超长上下文。

标准的旋转位置编码(RoPE)可以理解为:将词嵌入向量的每两个维度视为一个复数,然后根据令牌的位置 pos 和该维度对应的旋转速度 θ_k 进行旋转。
- 公式:对于位置
pos上词嵌入向量的第k个二维分量(x_k, y_k),旋转后变为:
(x_k * cos(pos * θ_k) - y_k * sin(pos * θ_k), x_k * sin(pos * θ_k) + y_k * cos(pos * θ_k)) - 直观理解:不同维度以不同速度旋转。快速旋转的维度擅长捕捉局部位置信息(周期短),慢速旋转的维度擅长区分全局位置(周期长)。
在预训练阶段,θ_k 的设置通常使最慢旋转维度的周期覆盖训练时的最大上下文长度(如4K或10K)。当我们需要将上下文长度扩展到远大于这个周期(如100K)时,直接使用原来的 θ_k 会导致周期外的位置无法被正确区分(因为 cos 和 sin 函数是周期性的)。

因此,需要进行位置编码外推。一种在实践中有效的策略是非均匀缩放:
- 方法:不直接缩放所有位置的旋转角度,而是定义一个临界位置
N。对于位置pos < N的令牌,使用原始的旋转速度(保持短上下文性能)。对于pos >= N的令牌,让旋转速度随着位置增加而逐渐变慢。 - 公式(概念性):
θ_k’(pos) = θ_k * f(pos),其中f(pos)在pos < N时为1,在pos >= N时是一个缓慢衰减的函数。 - 优点:最大程度地保留了模型在短上下文上的性能,为长上下文微调提供了良好的起点。具体的
f(pos)函数形式可以通过超参数搜索确定。

拥有了训练长上下文模型的技术,我们还需要合适的数据。本节介绍如何构建用于训练和评估的长上下文数据。
高质量、包含长距离依赖的自然文本数据很难大量获取。因此,目前主流的方法是合成数据生成。
核心思路是“藏宝于海”:
- 从一个短的文档-问答对
(D, Q, A)开始,其中A的答案明确依赖于文档D。 - 用大量其他无关或相似的文档将目标文档
D包围起来,形成一个超长的合成文档。 - 将问题
Q放在这个长文档的末尾,并要求模型给出答案A。 - 为了增加难度,甚至可以将目标文档
D拆分成多个片段,分散插入到长文档的不同位置,要求模型进行信息聚合。
通过这种方式,可以大规模生成用于训练和评测模型长上下文理解与信息检索能力的数据集。
本节课我们一起学习了实现大语言模型长上下文能力的核心技术。
我们首先了解了长上下文对于智能体应用的重要性。接着,探讨了分阶段训练的策略,即在模型掌握语言基础后再进行长上下文扩展。然后,我们深入研究了实现长上下文训练的关键技术:序列并行化 用于解决内存瓶颈,滑动窗口/稀疏注意力 用于降低计算和通信开销,以及位置编码的外推(特别是非均匀缩放策略)用于让模型理解超长序列中的位置信息。最后,我们介绍了通过合成数据生成来构建训练和评估数据集的方法。

掌握这些知识,有助于理解当前主流大语言模型如何突破上下文长度的限制,以及在这一前沿领域进行探索和创新的可能方向。

在本节课中,我们将学习如何将图像生成的扩散模型技术扩展到视频生成领域。我们将深入探讨其背后的数学原理,特别是扩散模型的理论基础,并了解OpenAI的Sora模型是如何应用这些原理的。

上一节我们提到了扩散模型在视频生成中的应用。为了理解其工作原理,本节中我们来看看扩散模型背后的核心数学原理。
扩散模型包含一个前向过程和一个反向过程。前向过程将一个给定的分布转换为高斯分布,而反向过程则将高斯分布转换回原始分布。其数学基础是所谓的Wasserstein梯度流。
从数学角度看,概率分布可以视为一个度量空间中的点。这个空间的标准度量是Wasserstein度量。两个分布P和Q之间的Wasserstein距离(W₂)定义为:
W₂(P, Q) = inf_{(X, Y)} E[|X - Y|²]^(1/2)
其中,(X, Y) 是满足X服从P、Y服从Q的联合分布。这个度量衡量的是将分布P“移动”到分布Q所需的最小“工作量”。
扩散过程的前向过程,本质上就是沿着Wasserstein度量空间中的梯度流,将原始分布移动到高斯分布。这条路径是连接两个分布的最短路径。反向过程则是这条路径的逆过程。
在实践中,我们通过离散化(例如100个时间步)来近似这条连续的路径,类似于用梯度下降法近似梯度流。

上一节我们介绍了Wasserstein梯度流的概念。本节中我们来看看如何用具体的方程来描述这个过程。
Wasserstein梯度流有一个简洁的数学表征,即一个随机微分方程。假设我们想从初始分布P₀移动到最终的高斯分布P_∞。在时间t,变量X_t的分布为P_t,并且X_t满足以下关系:
X_t = e^{-t} * X_0 + sqrt(1 - e^{-2t}) * Z
其中,X₀ 服从初始分布P₀,Z 服从标准高斯分布。这意味着,前向过程本质上就是不断向原始数据添加噪声。
这个X_t作为时间t的函数,满足以下随机微分方程:
dX_t = -X_t dt + sqrt(2) dB_t
这里,dB_t是布朗运动。方程中的 -X_t dt 项使X_t收缩,而 sqrt(2) dB_t 项则不断添加噪声。随着时间t增大,X_t的分布逐渐趋近于高斯分布。
上一节我们描述了前向过程的随机微分方程。为了生成数据,我们需要一个反向过程。本节中我们来看看如何推导反向过程。
我们的目标是从高斯噪声(X_T)开始,通过一个反向过程恢复出原始数据(X₀)。我们定义一个新的变量Y_t = X_{T-t}。那么Y_0 = X_T(近似高斯分布),Y_T = X₀(目标分布)。


根据福克-普朗克方程,我们可以推导出Y_t满足的随机微分方程:
dY_t = [Y_t + 2 * ∇ log p_{T-t}(Y_t)] dt + sqrt(2) dB_t
其中,p_{T-t}(·) 是在时间 T-t 时变量的概率密度函数。与简单的前向过程相比,反向方程中多出了一项 2 * ∇ log p_{T-t}(Y_t),即概率密度函数对数的梯度,这被称为得分函数。
因此,运行反向过程、从噪声生成数据的关键,就在于估计这个得分函数。
上一节我们得出结论,反向过程的核心是估计得分函数。本节中我们来看看如何利用神经网络来实现这一点。
为了运行反向的随机微分方程,我们需要计算 ∇ log p_t(y)。我们可以训练一个神经网络 s(y, t) 来近似这个得分函数。

一个重要的数学结论是:训练神经网络去预测添加到数据中的噪声,在数学上等价于最小化得分匹配的目标函数。也就是说,如果我们给一个带噪声的图像,让神经网络预测所添加的噪声,那么训练好的网络输出就是得分函数的一个近似。
因此,整个扩散模型的流程在数学上是严谨的:
- 前向过程:通过添加噪声将数据分布变为高斯分布。
- 训练:训练神经网络来预测给定噪声数据所对应的噪声。
- 反向过程:从高斯噪声开始,使用训练好的神经网络来近似得分函数,运行反向SDE,逐步生成数据样本。
这个方法适用于任何分布,只要我们能最小化得分匹配目标。这就是为什么它可以被推广到图像、视频、音频等各种生成任务。
上一节我们明确了扩散模型是分布无关的通用方法。本节中我们来看看如何将其直接应用于视频生成。
视频可以看作是一系列图像的序列。因此,视频的分布就是图像序列的联合分布。从原理上讲,我们可以简单地将视频数据(所有帧的像素)视为一个非常高维的随机变量X₀,然后直接应用扩散模型。
然而,这里存在一个挑战:扩散模型的收敛速度和样本复杂度与随机变量的维度D大致成正比。对于视频,维度D = (帧高) × (帧宽) × (通道数) × (帧数)。当帧数很多时,维度会非常高,导致需要更精确的得分估计和更多的训练数据。
所以,虽然理论上可以直接应用,但为了高效地生成长视频,我们需要一些技术来降低处理维度。
上一节我们提到了视频生成中的维度挑战。本节中我们来看看一种常用的解决方案:潜在扩散模型。
为了降低数据维度,我们可以使用自动编码器。首先,用一个编码器E将高维数据x映射到一个低维的潜在空间z = E(x),其中z的维度远小于x。同时,存在一个解码器D,可以(近似地)从z重建回x,即 D(E(x)) ≈ x。

理想情况下,我们应该在低维的潜在空间z上运行扩散过程(即对z加噪和去噪)。这被称为潜在扩散。
但在一些实际实现(如早期的潜在扩散模型)中,噪声仍然被添加到原始数据x上,而不是潜在表示z上。从数学角度看,这导致过程不再是原始Wasserstein度量空间上的梯度流,而是在编码器诱导出的新度量空间上的梯度流。
这个差异意味着,在反向过程中,除了要估计得分函数,理论上还需要考虑一个由诱导度量带来的额外项(与局部曲率相关)。为了获得更高质量的生成结果,需要在训练目标中加入对这个项的近似。

上一节我们讨论了潜在扩散的思想。本节中我们来看看OpenAI Sora模型采用的具体架构:扩散Transformer。
Sora的核心是一个基于Transformer架构的扩散模型,称为扩散Transformer。其工作流程如下:
- 编码:使用预训练的编码器将视频帧(或图像块)映射到低维潜在空间。
- 标记化:将潜在表示展平为一序列的标记,作为Transformer的输入。
- Transformer处理:序列标记通过多个Transformer块。每个块包含多头注意力层和前馈网络。
- 条件注入:在Transformer块中,通过交叉注意力等方式融入文本描述条件,指导视频生成。
- 预测:Transformer的输出被重新整形,用于预测噪声(得分函数)。
- 解码:去噪后的潜在表示通过解码器恢复为像素空间的视频。
以下是架构的简化表示:
[视频帧] -> [编码器] -> [潜在表示] -> [展平/标记化] -> [Transformer块 + 文本条件] -> [预测噪声] -> [去噪] -> [解码器] -> [生成视频]
上一节介绍了扩散Transformer的整体架构。本节中我们来看一个支持可变长度视频生成的关键技术:三维位置编码。

视频数据具有三维结构:宽度(X)、高度(Y)和时间(T)。为了将视频块输入Transformer,我们需要告诉模型每个块在三维空间中的位置。
Sora采用了类似Google NaViT 模型的思想,使用分解式的三维位置编码。对于一个位于坐标 (x, y, t) 的视频块,其位置编码是三个独立的一维位置编码之和:
位置编码(x, y, t) = PE_x(x) + PE_y(y) + PE_t(t)


这种方法的优势在于:
- 支持原生分辨率:视频可以保持原有的宽高比和帧率,无需强制缩放到统一尺寸。
- 支持可变长度:可以处理不同时长和不同尺寸的视频。
- 灵活性高:为模型提供了明确的空间和时间结构信息。
本节课中我们一起学习了视频生成扩散模型的原理与实现。


我们从扩散模型的数学基础出发,理解了其核心是Wasserstein梯度流,并通过随机微分方程和福克-普朗克方程描述了前向和反向过程。生成的关键在于使用神经网络进行得分匹配。
我们看到,该理论具有普适性,可直接应用于视频分布。为了应对高维挑战,采用了潜在扩散技术来降低维度。OpenAI的Sora模型基于扩散Transformer架构,并利用三维位置编码来处理可变长度的视频数据。

当前,这类模型的主要限制在于巨大的计算成本,无论是训练还是推理。然而,由于其坚实的数学基础,性能主要遵循扩展定律:更多的数据和更大的模型将直接带来更好的结果。这为未来的发展提供了清晰的方向。
在本节课中,我们将学习如何将图像生成中的扩散模型技术扩展到视频生成领域。我们将深入探讨其背后的数学原理,特别是瓦瑟斯坦梯度流,并了解OpenAI的Sora模型所采用的核心架构。
上一节我们介绍了扩散模型的基本概念,本节中我们来看看其背后的数学原理。扩散模型的核心在于其前向过程和反向过程。
从数学角度看,扩散过程实际上是在一个特殊的度量空间——瓦瑟斯坦空间中,沿着梯度流路径移动概率分布。我们可以将一个概率分布视为这个度量空间中的一个点。
瓦瑟斯坦距离 的公式定义如下:
$$
W_2(P, Q) = inf_{gamma in Gamma(P, Q)} sqrt{mathbb{E}_{(x, y) sim gamma} [|x - y|^2]}
$$
其中,$Gamma(P, Q)$ 是所有边缘分布分别为 $P$ 和 $Q$ 的联合分布 $gamma$ 的集合。
扩散模型的前向过程,就是将原始数据分布 $P_0$ 沿着该空间中的最短路径(即梯度流)转化为高斯分布 $P_infty$。反向过程则是这条路径的逆过程。
理解了梯度流的视角后,我们来看看如何用具体的方程来描述这个过程。前向过程可以通过一个随机微分方程来刻画。






为了从噪声中生成数据,我们需要运行反向过程。这涉及到随机微分方程的反转,而福克-普朗克方程为此提供了数学工具。
定义一个新的过程 $Y_t = X_{T-t}$,其中 $T$ 是一个足够大的时间。那么 $Y_t$ 从近似高斯分布($Y_0 approx X_T$)出发,最终应能恢复原始分布($Y_T = X_0$)。
反向过程的核心在于估计得分函数 $ abla log p_t(x)$。在实践中,我们训练一个神经网络来近似这个函数。
训练目标是最小化得分匹配损失。一个关键结论是,对于上述特定的前向过程,训练神经网络根据含噪输入预测所添加的噪声,在数学上等价于学习得分函数。因此,扩散模型的训练可以归结为一个去噪任务。
这个过程是数学上严格的,没有近似。只要我们能完美地学习到得分函数,运行反向SDE就能精确地采样出目标数据分布。这解释了扩散模型为何是一种通用的生成方法,可应用于图像、视频、音频等任何数据分布。
现在,我们将上述原理应用到视频生成。视频本质上是一个图像序列的概率分布。
我们可以将一段视频视为一个高维随机变量 $X in mathbb{R}^{H imes W imes C imes T}$,其中 $T$ 是帧数。扩散模型的理论保证,只要我们能学习该视频分布的得分函数,就能生成视频。
然而,维度 $D = H imes W imes C imes T$ 可能非常大。扩散模型的收敛速度和样本复杂度通常与维度 $D$ 成正比,这意味着生成长视频需要极高的计算成本和数据量。
为了应对高维度的挑战,常用的技术是潜在扩散。其核心思想是先用一个编码器将高维数据(如图像/视频帧)压缩到一个低维潜在空间,然后在潜在空间中进行扩散过程。
设编码器为 $E$,解码器为 $D$,满足 $D(E(x)) approx x$。理想情况下,我们应在潜在变量 $z = E(x)$ 上运行扩散过程。但许多实际实现(如Stable Diffusion的早期版本)仍然在原始像素空间加噪,这相当于在由编码器诱导的度量空间中进行梯度流,而非标准的瓦瑟斯坦梯度流。
这引入了额外的几何项(与诱导度量相关)。为了获得高质量的生成结果,在训练目标中需要考虑这个项,例如除了预测噪声外,还可能预测一个与局部协方差相关的项 $Sigma$。最新的潜在扩散模型已开始纳入这些修正。
OpenAI的Sora模型基于扩散Transformer架构。其核心思想是将扩散模型中的U-Net主干替换为Transformer。
以下是该架构的关键步骤:
- 输入处理:视频经过编码器被映射到潜在空间。潜在表示被展平为一序列的令牌(tokens),作为Transformer的输入。
- 位置编码:Sora采用了类似Google“原生分辨率ViT”的技术,使用3D位置编码。一个令牌的位置编码是其空间坐标 $(x, y)$ 和时间坐标 $t$ 的位置编码之和:$PE = PE_x(x) + PE_y(y) + PE_t(t)$。这支持可变分辨率、可变长度的视频输入,无需将视频裁剪成固定尺寸。
- Transformer块:序列令牌通过一系列Transformer块。这些块集成了多头自注意力机制和前馈网络。
- 条件注入:生成的条件信息(如文本描述)被注入到每个Transformer块中,通常通过交叉注意力或直接添加到隐藏状态来实现。
- 输出与训练:Transformer的输出被重新变换回潜在空间的形状,用于预测噪声(得分函数)以及可能的方差项 $Sigma$。训练目标是最小化去噪得分匹配损失。
本节课我们一起学习了视频生成扩散模型的数学基础与核心架构。
我们首先回顾了扩散模型的本质,即瓦瑟斯坦空间中的梯度流,并通过随机微分方程和福克-普朗克方程理解了其严格的反向过程。我们认识到,训练神经网络进行去噪等价于学习得分函数,这使得扩散模型成为适用于任何数据分布的通用生成框架。
接着,我们将该框架应用于视频生成,指出了高维度带来的挑战,并介绍了潜在扩散技术作为解决方案。最后,我们剖析了OpenAI Sora模型所使用的扩散Transformer架构,其关键创新在于使用了支持可变长视频的3D位置编码,并将Transformer的强大表达能力与扩散模型的严格生成理论相结合。
视频生成的突破主要得益于扩散模型的数学保证和计算规模的扩大,而非魔法般的算法创新。随着模型规模和数据量的持续增长,遵循缩放定律,我们有望看到更加强大和通用的生成模型。



在本节课中,我们将学习两个之前课程中未深入讨论的重要遗留主题:用于分布式优化的流水线引擎,以及一种用于初始化Transformer网络的新方法——μP。这些技术是当前训练超大规模模型(如万亿参数模型)的关键。
上一节我们介绍了专家并行,它主要解决了MLP层的内存问题。本节中我们来看看另一种关键的并行化技术——流水线并行,它能够处理模型深度(层数)的扩展问题。




流水线并行的核心思想非常简单:将神经网络按层切分,并将不同的层放置在不同的GPU上。对于一个由多个Transformer块顺序堆叠的网络,最自然的并行方式就是将每一层放在一个独立的GPU节点上。
例如,如果你的网络定义为一个nn.Sequential模块:
net = nn.Sequential(layer1, layer2, layer3, ..., layerL)
那么流水线引擎在精神上会自动将layer1放在GPU1,layer2放在GPU2,依此类推。这种方法理论上可以扩展到无限深度,只要你有足够多的GPU。
然而,这种按层切分的方式带来了一个核心问题:计算是顺序的,而非并行的。GPU1必须先完成第1层的计算,才能将结果传给GPU2进行第2层的计算。这导致了大量的GPU空闲时间,计算效率极低。如果有100层,计算时间可能比单GPU单层模型慢100倍。


如何解决GPU空闲问题,让流水线引擎更高效?核心思路是利用空闲时间。

观察计算图,当一个GPU完成当前数据(例如x1)在当前层的计算后,在等待下一层GPU接收并开始计算时,它处于空闲状态。高效的流水线引擎会利用这个空闲时间,开始处理下一个数据(例如x2)在当前层的计算。

以下是实现高效流水线的关键机制:
- 状态卸载与加载:在完成
x1的前向计算后,GPU立即将其激活状态(用于后续反向传播)卸载(offload)到CPU内存。由于卸载到CPU是非阻塞操作,GPU可以立刻开始计算x2的前向传播。 - 调度与重叠:通过精心调度,让不同数据样本(
x1,x2,x3...)在不同层上的计算相互重叠,形成一种“流水线”效果,从而填满GPU的空闲时间。 - 周期性同步:在实践中,为了鲁棒性和内存管理,流水线引擎通常会设置一个“微批次”(micro-batch)边界。在完成一定数量的重叠计算后,会进行一次同步,清空中间状态,然后开始新的计算周期。这比完全无休止的重叠更稳定,也更容易处理故障恢复。
流水线并行非常适合Transformer这类结构,因为每一层的计算量和张量形状大致相同,易于负载均衡。它与专家并行、数据并行结合,构成了训练超大模型的“3D并行”范式。
- 流水线并行:跨层切分,解决模型深度问题。
- 数据并行:跨数据批次切分,增加吞吐量。
- 模型并行(如专家并行):跨层内组件切分,解决层宽度问题。
对于注意力层,如果单层仍然无法放入内存,还可以使用序列并行等技术。
重要提示:流水线并行的实现(如Hugging Face的Pipeline Optimizer)对用户几乎是透明的。你只需要将模型定义为顺序模块,优化器会自动处理层间通信和调度,无需像张量并行那样手动指定复杂的张量切分。



接下来,我们探讨另一个主题:μP(Maximal Update Parametrization)。这是一种有争议但被OpenAI成功用于训练MOE模型的技术。它主要调整了Transformer网络的初始化和学习率调度策略。







要理解μP,我们先思考一个根本问题:当模型尺寸(例如宽度N)趋向无穷大时,我们常用的优化器(如Adam)和固定学习率还能稳定工作吗?
考虑一个简单的单层线性网络:y = Wx + b。假设使用标准初始化:W和b的元素服从N(0, 1/N)。
- 初始化输出:在
N很大时,输出y的尺度是O(1),这是合理的。 - 一次更新后:Adam等优化器的参数更新量级通常是
O(1)(经过梯度归一化)。对于偏置b,更新Δb是O(1)。那么更新后的输出包含一项Δb * x。由于Δb与梯度相关,而梯度又与权重W相关,这项的期望尺度可能是O(√N)。这意味着仅一次更新,网络输出就可能爆炸(或变得极大),导致训练不稳定。
如果为了避免爆炸而将学习率设为O(1/√N),那么更新权重所需的有效步数将变为O(√N)。当N很大时,这意味着需要极多的步骤才能有意义地更新网络,训练效率极低。
μP旨在寻找一个参数化(初始化+学习率缩放)方案,使得在模型宽度N → ∞的极限下,满足两个核心条件:
- 网络可快速更新:每个参数都能在常数步数内被有效更新。
- 更新过程稳定:单次参数更新不会导致网络输出发生剧烈(
O(√N))变化。
这需要在“更新幅度不能太小”(目标1)和“更新幅度不能太大”(目标2)之间找到一个精妙的平衡点。


μP通过层间差异化的初始化方差和学习率缩放来实现这一目标。其推导基于理论分析,确保在前向传播和反向传播过程中,各层激活值和梯度的尺度保持一致且可控。
一个简化的示意是,对于深度为L的Transformer,μP可能会规定:
- 输入嵌入层:保持标准初始化(如
N(0, 1))和常数学习率。 - 中间层:初始化方差需要按
1/N或1/(N√L)等因子缩放。 - 输出层:初始化方差需要按
1/N^2等因子缩放,并且其学习率可能需要按1/N缩放。
这些缩放因子确保了即使在超宽网络中,前向信号、反向梯度以及参数更新量的尺度都是O(1),从而满足上述两个设计目标。
μP提供了一种原则性的方法来初始化和大规模训练Transformer,尤其是宽度极大的模型或MOE模型。OpenAI的成功经验表明,这种精细的缩放对训练稳定性至关重要。
然而,μP在学术界和工业界其他团队中并未被广泛验证为“唯一有效”的方法。一些替代方案,例如使用极大的权重衰减(weight decay)配合层归一化(LayerNorm),也能起到类似的稳定作用。但μP的价值在于它从一个理论极限(N → ∞)出发,给出了一个系统性的超参数设置框架。



本节课中我们一起学习了两个支撑当今超大规模生成式AI模型训练的关键技术:

- 流水线并行引擎:通过将模型按层切分到不同GPU,并利用巧妙的调度重叠不同数据样本的计算,极大地提高了深度模型的训练效率,是实现模型深度扩展的核心手段。
- μP初始化:通过理论推导出一套针对超宽网络的初始化与学习率缩放方案,旨在保证模型在宽度趋向无穷时,仍能保持快速且稳定的训练动态。这是OpenAI成功训练MOE等巨型模型的重要“秘方”之一。




理解这些底层优化技术,有助于我们更好地把握大模型训练的工程挑战和前沿方向。
在本节课中,我们将学习两个在前几讲中因时间关系未深入讨论的重要主题:用于分布式优化的流水线引擎,以及一种旨在提升大规模Transformer网络训练稳定性的初始化与学习率调度方法——μP。这些技术是当前训练超大规模语言模型(如万亿参数模型)的核心。

上一节我们介绍了专家并行等模型并行技术。本节中,我们来看看另一种关键的并行范式——流水线并行。当模型过大,无法放入单个GPU内存时,除了数据并行和模型并行,我们还需要沿模型的深度方向进行切分。
以H100 GPU(约80GB内存)为例,其最大能容纳的模型大约是70亿参数、上下文长度8K的稠密模型。而当前业界关注的模型规模通常是这个的10倍甚至100倍以上。因此,必须采用更高级的并行技术。
实践中,为了扩展到极大模型,主要依赖两种技术组合:
- 将稠密模型转换为混合专家模型,以启用高效的专家并行。
- 在上述基础上,应用流水线并行。这一步尤为关键,其实现效率直接决定了整体优化速度。
以下是对这两种并行方式的高层次对比:
# 专家并行:每个专家放置在不同的GPU上 expert_1 = nn.Linear(D, 2*D).to('gpu:0') expert_2 = nn.Linear(D, 2*D).to('gpu:1') # ... 计算时,根据路由将token发送到对应的专家GPU # 流水线并行:将模型的连续层放置在不同的GPU上 model = nn.Sequential(layer1.to('gpu:0'), layer2.to('gpu:1'), layer3.to('gpu:2'))
流水线并行的思想非常直观:将一个由L层组成的神经网络(如Transformer)按层切分,分别放入L个不同的GPU中。每个GPU仅负责其对应层的前向和反向计算。
然而,最朴素的实现方式效率极低。因为计算是严格顺序的:必须等第1层计算完,才能开始第2层的计算,以此类推。这导致在任一时刻,大部分GPU都处于空闲状态,计算时间将是单GPU的L倍,无法有效利用多GPU的算力。

为了解决GPU空闲问题,核心思路是让GPU在等待当前数据流中上一层的计算结果时,提前开始处理下一批数据。这需要通过巧妙的调度和内存管理来实现。
以下是实现高效流水线的关键机制:
- 计算与通信重叠:当一层完成其前向计算后,立即将输出的激活值卸载到CPU内存(这是一个非阻塞操作)。与此同时,该GPU可以立即开始处理下一批数据的前向计算。
- 调度填充:通过合理安排不同批次数据在不同层上的计算顺序,可以填充大部分空闲时间,形成类似“流水线”的高效运作模式。理想情况下,调度应使得所有GPU持续处于工作状态。
一个简化的流水线调度时间线示意图如下(其中F代表前向,B代表反向,数字代表批次序号):
时间 -> GPU0: F1 -> B1 -> F4 -> B4 ... GPU1: 空闲 -> F1 -> B1 -> F4 ... GPU2: 空闲 -> 空闲 -> F1 -> B1 ...
(注:实际调度更复杂,需考虑内存和依赖关系)
在实际系统中(如OpenAI使用的引擎),流水线并行的实现还涉及更多细节:
- 微批次:将一个大批次拆分成更小的微批次,以更细粒度地填充流水线。
- 梯度累积:为了保持有效的批次大小,需要在多个微批次上累积梯度后再更新权重。
- 周期同步点:定期设置同步点以清空流水线,这有助于内存管理和提高系统容错性。
- 与其它并行方式结合:现代大规模训练通常采用 3D并行:流水线并行(层间)+ 数据并行(数据间)+ 专家并行/张量并行(层内)。
流水线并行的优势在于,对于像Transformer这样各层计算量均匀的模型,它几乎可以自动实现(将模型定义为nn.Sequential即可),无需像张量并行那样手动重写层内计算逻辑。
上一节我们探讨了如何通过并行化来容纳大模型。本节中,我们来看看另一个关键问题:当模型参数数量N趋向于极大时,如何保证优化过程依然稳定且高效?这就是μP方法要解决的核心问题。
当隐藏层维度N极大时,使用固定学习率(如0.1)的Adam优化器进行一步更新,可能会引发两个问题:
- 输出爆炸:更新步长相对于网络输出可能过大,导致一步更新后网络输出值急剧增大甚至溢出。
- 更新无效:为避免爆炸而过度调低学习率,又会导致参数更新幅度过小,需要极多步迭代才能对网络产生有意义的影响。
这两种情况都会导致训练不稳定或效率极低。
μP旨在寻找一个初始化与学习率调度的配置,使得当N → ∞时,满足以下两个条件:
- 网络输出稳定:单步更新不会导致网络输出发生剧烈(
O(√N)量级)变化。 - 参数有效更新:经过常数步(而非
O(N)步)的更新后,网络参数能够发生显著变化。
这需要在初始化尺度和层间学习率之间进行精细的协调。
μP提出了一套系统性的缩放规则。其核心思想是:不同层参数的初始化方差和学习率应随网络宽度N进行不同的缩放。

以一个简化版本为例(忽略层归一化等细节):
- 输入层(W1):保持标准初始化(方差~
1/D)和常数学习率。这使得嵌入层能在常数步内被有效更新。 - 输出层(W2):为了抑制输出爆炸,需要将其初始化方差缩小为~
1/N^2(而非标准的1/N)。同时,为了保证该层参数也能在常数步内被更新,其对应的学习率需要放大为O(N)。
这样,虽然W2的初始值很小(O(1/N)),但乘以放大的学习率(O(N))后,单步更新量级为O(1),从而能在常数步内显著改变该层参数。同时,由于初始化很小,即使更新量级为O(1),也不会使最终输出爆炸。
μP从理论上为超宽网络的稳定训练提供了一种原则性方法。据报道,它是OpenAI成功训练大规模MoE模型的关键技术之一。
然而,该方法也存在争议。在OpenAI之外,很少有团队能完全复现其宣称的效果。这可能是因为实际系统还包含了未公开的细节调整,或者依赖特定的硬件和软件栈。其他公司可能采用不同的启发式方法,例如使用极大的权重衰减配合层归一化,也能达到稳定训练的目的。
尽管如此,理解μP背后的思想——即在网络宽度缩放时,协调初始化与学习率以保持优化动态的稳定性——对于从事大规模模型训练的研究者和工程师至关重要。
本节课中我们一起学习了两个支撑当今超大规模生成式AI模型训练的关键技术:
- 流水线并行引擎:通过将模型按层切分到多个GPU,并利用智能调度重叠计算与通信,极大提高了内存利用率和训练效率,是实现模型深度扩展的核心手段。
- μP初始化:通过精心设计参数初始化方差和层间学习率的缩放规则,旨在保证当模型宽度极大时,优化过程依然保持稳定和高效。这体现了对神经网络训练动态的深刻理解。
这些技术代表了分布式AI系统与优化理论前沿的结合,是构建下一代更大、更强AI模型的基础设施的重要组成部分。
在本节课中,我们将学习语音识别模型。虽然语音识别在技术上并非纯粹的生成式模型,但它使用了与生成式模型相似的架构,并且在生成式机器学习中扮演着重要角色。我们将了解如何将语音信号转换为文本,以及两种主流的模型架构。

语音数据正变得越来越重要,因为它是训练语言模型的高质量数据源。例如,播客、辩论甚至总统演讲的文本记录,都是极其重要且高质量的数据。这些数据中没有广告,信息密度高。仅YouTube视频的字幕就可能包含超过10万亿个高质量的词元。然而,我们需要将语音转换为自然语言文本才能利用它们。本节课,我们将学习如何实现这一点。

上一节我们提到了语音数据的重要性,本节中我们来看看如何将连续的语音信号转换为计算机可以处理的数字形式。
声音在物理学上由两个基本特征描述:
- 振幅:决定声音的响度。
- 频率:决定声音的音高。频率越高,音调越高;频率越低,音调越低沉。

任何复杂的声音波形都可以分解为一系列具有特定振幅和频率的简单正弦波的组合。这种分解方法称为傅里叶变换。
一个在区间 [0, 2π] 上的连续函数 f(x) 可以表示为:
f(x) = Σ (α_j * e^(i * θ_j * x))
其中,α_j 是复数系数(代表振幅和相位),θ_j 是频率。
通过傅里叶变换,我们可以将时域中的声音信号转换到频域,用一个(理论上无限维的)系数向量 [α_0, α_1, α_2, ...] 来表示。在实际应用中,由于系数会衰减,我们可以找到一个有限的截断点 N,用前 N 个系数 [α_0, α_1, ..., α_N] 来足够精确地近似原始声音。
因此,对于一段较长的音频,我们可以先将其切分为多个时间窗口,然后将每个窗口内的声音信号转换为一个频域系数向量。最终,整个音频就被表示为一个向量序列,这与自然语言处理中词元序列的形式非常相似。

上一节我们介绍了通过傅里叶系数向量表示声音的方法,但这种方法存在一个问题:它生成的向量维度过高,且不符合人耳的感知特性。
原始的频谱系数在高频区域非常集中,振幅较大。然而,人耳对高频变化的敏感度低于中低频。例如,人耳很难区分600Hz和800Hz的声音,但对1000Hz附近的变化非常敏感。因此,我们需要一种更符合人耳感知特性的表示方法。
直接将高维向量截断或均匀分组(池化)会丢失重要信息。一个更聪明的方法是非均匀分组,即根据一个特定的函数将频率系数分组到不同的“桶”中,并对每个桶内的系数进行平均。这个函数近似于指数函数,被称为梅尔尺度。

梅尔频谱的计算过程如下:
- 计算音频窗口的傅里叶系数。
- 根据梅尔尺度函数,将频率系数分组到多个频带(例如80个)中。
- 对每个频带内的系数进行加权平均,得到该频带的能量值。
这样,我们就将一个高维的傅里叶系数向量,压缩成了一个低维的梅尔频谱向量。每个向量元素代表一个特定频带在某个时间窗口内的平均能量。整个音频因此被表示为一个二维矩阵:一个维度是时间窗口序列,另一个维度是梅尔频带。
这种表示不仅是维度上的压缩,更是对声音信息的一种感知优化,为后续的模型处理奠定了更好的基础。

现在我们已经将音频转换为向量序列,接下来看看如何训练模型来理解这些向量。第一种主流架构是 Wave2Vec,它主要采用无监督学习。
Wave2Vec 的训练数据大部分是未标注的纯音频。其核心思想是学习音频信号的良好表示,类似于 BERT 在文本领域所做的工作。
以下是 Wave2Vec 的无监督训练流程:
- 输入:音频经过梅尔频谱处理后的向量序列
Q = [q_1, q_2, ..., q_T]。 - 掩码:随机掩码掉其中约15%的向量(类似于 BERT 的掩码语言模型)。
- 编码:将掩码后的序列输入一个 Transformer 编码器。
- 输出:Transformer 输出一个上下文向量序列
C = [c_1, c_2, ..., c_T]。 - 对比损失:训练目标是让被掩码位置
t的输出向量c_t尽可能接近其真实的向量q_t,同时远离其他所有时间步的向量q_{t'}(t' ≠ t)。
其对比损失函数可以简化为:
Loss = -log[ exp(sim(c_t, q_t)) / Σ_{t'} exp(sim(c_t, q_{t'})) ]
其中 sim 是相似度函数,如余弦相似度。
为什么使用对比损失而非简单的预测损失?
因为相邻时间窗口的音频向量 q_t 和 q_{t-1} 通常非常相似。简单的回归损失会让模型倾向于复制前一个向量,而无法学习到有区分度的特征。对比损失能迫使模型关注于每个时间窗口的独特特征,并忽略持续的背景噪音,这更接近人脑处理声音的方式。
训练完成后,Wave2Vec 得到了音频信号的高质量上下文表示。要用于语音识别等下游任务,只需在其顶部添加一个简单的线性分类层,并用少量有标注数据进行微调即可。这种架构特别擅长声音分类、说话人分割等任务。
上一节我们学习了无监督的 Wave2Vec 模型,本节我们来看另一种主流的架构:Whisper。这是一个完全基于有监督学习的编码器-解码器模型,专门用于语音到文本的转录。
Whisper 模型的训练数据是成对的音频和文本字幕。但这里有一个关键挑战:字幕通常是整个音频段的概括,没有与音频时间轴精确对齐。我们不知道哪句文本对应哪段音频。
Whisper 巧妙地利用了解码器的自回归生成能力和交叉注意力机制来解决这个问题。
以下是 Whisper 的工作流程:
- 编码:音频被转换为梅尔频谱向量序列,然后送入一个编码器(可能包含CNN和Transformer)进行编码,得到音频的上下文表示。
- 解码:使用一个 Transformer 解码器来生成文本词元序列。
- 交叉注意力:解码器的核心创新在于其注意力机制。在预测下一个文本词元时,解码器不仅会关注之前已生成的所有文本词元(自注意力),还会通过交叉注意力层关注整个音频编码序列。
- 训练目标:模型的学习目标是最简单的下一个词元预测。给定一段音频和对应的文本字幕,模型需要根据音频信息和已生成的文本,预测出下一个正确的文本词元。
这类似于图像生成的扩散模型或DALL-E,只不过这里是条件于音频来生成文本。由于解码器在生成每个词时都能“听到”整个音频,理论上它应该能生成准确的转录文本。

Whisper 模型因其出色的开箱即用转录能力和多语言支持而广受欢迎。它代表了当前大规模有监督训练在语音识别领域的成功应用。
本节课我们一起学习了语音识别模型的基础知识。
我们首先了解了语音作为高质量数据源的重要性。接着,探讨了如何将声音从连续的波形,通过傅里叶变换和梅尔频谱处理,转换为适合神经网络处理的向量序列。
然后,我们深入分析了两种主流的模型架构:
- Wave2Vec:采用无监督对比学习,旨在学习音频信号本身的优良表示,适用于需要音频理解的分类任务。
- Whisper:采用有监督的编码器-解码器架构,利用交叉注意力机制,直接条件于音频生成文本,是目前主流的高质量语音转录方案。

目前,语音识别模型仍有改进空间,例如更好地处理说话人分离、背景音过滤等。未来的方向可能是结合 Wave2Vec 的无监督学习能力与 Whisper 的强转录能力,以更少的有标注数据获得更强大、更鲁棒的模型。这是一个非常活跃且重要的研究领域。
在本节课中,我们将学习语音识别模型。虽然语音识别在技术上并非纯粹的生成式模型,但它使用了与生成式模型相似的架构,并且在机器学习领域非常重要。我们将了解如何将语音信号转换为自然语言文本,并探讨两种关键的模型架构。
语音数据正变得越来越重要,因为它是训练语言模型的高质量数据来源。例如,播客、辩论甚至总统演讲的文本记录,都是极其重要且高质量的数据。这些数据不包含广告,信息密度高。粗略估计,仅YouTube转录文本就可能提供超过10万亿的高质量词元。然而,我们需要将语音转换为自然语言才能利用这些数据。本节课,我们将学习如何实现这一点。
上一节我们介绍了语音数据的重要性,本节中我们来看看如何将原始的语音信号转换为计算机可以处理的格式。
回想一下物理知识,声音由两个基本特征描述:
- 振幅:决定声音的响度。
- 频率:决定声音的音高。频率越高,音调越高;频率越低,声音越低沉。
任何复杂的声音波形都可以分解为一系列具有特定振幅和频率的简单正弦波的组合。这种分解方法称为傅里叶变换。
通过傅里叶变换,我们可以将时域中的声音信号转换到频域。在频域中,一个声音片段(例如一个时间窗口内的声音)可以表示为一个向量,向量的每个维度对应一个特定频率分量的振幅。
然而,原始傅里叶系数向量维度可能非常高(例如对应高达100kHz的频率)。直接使用这样的高维向量作为模型输入效率低下。此外,人耳对不同频率的敏感度不同,对中频区间的变化更敏感。
因此,我们需要一种更智能的降维方法,而不是简单截断高频系数。

上一节我们提到了直接使用傅里叶系数的局限性,本节中我们来看看如何更有效地表示频域信息。
解决方案是使用梅尔频谱。其核心思想是:根据一个类似指数函数的“梅尔尺度”将频率分组到不同的“频带”或“桶”中。
在梅尔尺度下:
- 低频区域(如0-100Hz)被划分为较少的频带,分辨率较粗。
- 高频区域(如8000-10000Hz)被划分为较多的频带,分辨率较细。
然后,我们将同一个频带内的多个傅里叶系数振幅进行平均(或采取其他聚合方式),用这个平均值作为该频带的代表值。这样,我们就将一个高维的傅里叶系数向量压缩成了一个低维的梅尔频谱向量。
这个过程可以总结为以下步骤:
- 对音频时间窗口进行傅里叶变换,得到频域系数。
- 根据梅尔尺度将频率分组到多个频带。
- 聚合(如平均)每个频带内的系数值,形成最终的梅尔频谱向量。
最终,一个完整的音频文件被处理成一个向量序列:[向量_窗口1, 向量_窗口2, ..., 向量_窗口T]。这与自然语言处理中的词元序列非常相似,因此可以输入给Transformer等序列模型。
现在我们已经将语音转换为向量序列,接下来看看如何训练模型来理解这些向量。首先介绍一种以无监督学习为主的架构:Wave2Vec 2.0。
Wave2Vec 2.0 的训练主要使用大量无标注的纯语音数据。其灵感来源于BERT的掩码语言模型(MLM)目标,但针对语音数据的特点进行了调整。
以下是其无监督训练的核心流程:
- 输入:经过梅尔频谱编码的语音向量序列
Q = [q1, q2, ..., qT]。 - 掩码:随机掩码(例如遮盖15%)序列中的部分向量,用特殊的
[MASK]向量替换。 - 编码:将掩码后的序列输入一个Transformer编码器。
- 输出:Transformer输出一个上下文向量序列
C = [c1, c2, ..., cT],其中每个ct对应输入位置t的编码。 - 对比损失:对于每个被掩码的位置
t,训练目标是:- 使输出
ct与真实的、被掩码的输入向量qt尽可能接近。 - 同时,使
ct与序列中所有其他位置的向量qt'(t' ≠ t)尽可能远离。
- 使输出
这被称为对比损失。为什么在语音中要使用对比损失,而不是像BERT那样直接预测被掩码的词元呢?
原因在于语音数据的连续性。相邻时间窗口的语音向量 qt 和 qt-1 通常非常相似。如果使用简单的预测损失,模型可能学会简单地复制前一个向量,而无法捕捉细微的、有意义的改变(如音素变化)。对比损失迫使模型学习每个时间窗口的独特表征,从而更好地区分不同的声音片段,并忽略持续的背景噪音。
训练完成后,Wave2Vec 2.0 模型获得了一个强大的语音特征编码器。要用于语音识别(转写文字),只需要在编码器的输出上添加一个简单的线性分类层,并用少量有标注的(语音,文本)配对数据进行微调即可。这种架构特别适合语音分类任务,如说话人分割、语音活动检测等。
上一节我们介绍了无监督的Wave2Vec模型,本节中我们来看看另一种更近期、专注于有监督语音识别的架构:Whisper。
Whisper 是一个基于Transformer的编码器-解码器模型,主要用于有监督的语音转写任务。其训练数据是大量的(音频,转录文本)对。注意,这些数据通常是非对齐的,即我们只知道一段音频的整体转录文本,但不知道文本中每个词具体对应音频的哪一部分。
Whisper 的核心思想是将语音识别构建为一个条件生成任务:给定音频输入,生成对应的转录文本。这类似于图像生成模型根据文本描述生成图像。
以下是Whisper模型的工作流程:
- 编码:音频信号通过一个编码器(包含卷积层和Transformer层)被处理成一个特征向量序列。这类似于之前得到的梅尔频谱序列的进一步抽象。
- 解码:一个Transformer解码器负责自回归地生成文本词元(转录结果)。
- 交叉注意力:解码器的关键机制是交叉注意力。在生成每一个文本词元时,解码器不仅会关注之前已生成的所有文本词元(自注意力),还会通过交叉注意力机制去“聆听”或“关注”编码器输出的整个音频特征序列。
这意味着,在生成“apple”这个词时,模型不仅考虑了上文“I eat an”,还同时考虑了整个音频上下文的信息,从而能更准确地预测当前词。
通过在大规模有监督数据上训练这个编码器-解码器架构(使用标准的自回归语言建模损失),Whisper学会了将音频内容准确地翻译成文本。它无需显式的对齐信息,而是通过注意力机制隐式地学习音频与文本之间的对应关系。
本节课中我们一起学习了语音识别模型的基础知识和两种主流架构。
我们首先了解了语音数据作为高质量训练语料的重要性。接着,学习了如何通过傅里叶变换和梅尔频谱将连续的语音信号转换为离散的向量序列,为神经网络处理做好准备。

然后,我们深入探讨了两种模型:
- Wave2Vec 2.0:一种基于对比学习的无监督/自监督模型,擅长学习语音的通用表征,适用于多种语音任务,经过微调后可进行语音识别。
- Whisper:一种基于编码器-解码器架构的有监督模型,直接学习从音频到文本的端到端映射,在语音转写任务上表现出色。
目前,语音识别模型仍有改进空间,其质量尚不及最先进的语言模型。一个有趣的研究方向是结合Wave2Vec的无监督学习能力和Whisper的强大生成能力,以利用海量的无标注语音数据,同时提升有监督任务的性能。这将是获得更多高质量训练数据、推动生成式AI发展的重要一步。
在本节课中,我们将学习CUDA编程的基础知识。CUDA编程对于希望从零开始训练大型语言模型至关重要,因为它能让我们直接控制GPU的计算和内存访问,从而实现远超现有库(如PyTorch)的性能优化。我们将了解为什么大公司需要编写自己的CUDA内核,并探索其背后的核心架构和编程模式。
CUDA编程是使用C/C++在NVIDIA GPU上进行计算的过程。理解CUDA对于优化生成式AI模型(如大型语言模型)的训练至关重要。标准深度学习框架(如PyTorch)虽然方便,但其通用性设计使其在极致性能优化上存在瓶颈。为了处理万亿参数级别的模型并应对GPU固有的硬件错误,顶尖的AI公司(如OpenAI)需要编写高度定制化的CUDA内核,以控制内存访问、实现计算冗余校验,并融合操作以减少数据移动。
上一节我们介绍了学习CUDA编程的必要性,本节中我们来看看CUDA编程所基于的GPU架构。
CUDA编程的核心在于理解GPU的内存层次结构,这直接决定了代码的性能。GPU的计算单元组织如下:
- 线程:最基本的执行单元,类似于CPU中的寄存器。
- 线程块:一组线程的集合,它们可以协作并共享一块高速的片上内存,称为共享内存。
- 网格:由多个线程块组成,负责执行一个完整的CUDA内核。
内存访问速度是关键瓶颈。以下是主要的内存类型:
- 全局内存:即GPU的显存,容量大但访问速度慢。
- 共享内存:每个线程块独有的小块高速内存,访问速度极快。
CUDA编程的主要目标就是尽可能多地将数据保留在共享内存中进行计算,从而减少对缓慢的全局内存的访问。
理解了架构后,我们来看看如何编写一个基本的CUDA程序。
一个典型的CUDA程序包含两部分:
- 内核函数:在GPU上每个线程中执行的函数。使用
__global__关键字声明。 - 主机函数:在CPU上运行的函数,负责配置并启动内核。
以下是一个向量加法的内核函数示例:
__global__ void vectorAdd(float *A, float *B, float *C, int n) }
这个内核假设只使用一个线程块。threadIdx.x 是CUDA内置变量,表示当前线程在线程块内的索引。
主机函数调用内核的语法如下:
// 定义执行配置:使用1个线程块,该块包含n个线程 dim3 threadsPerBlock(n); dim3 blocksPerGrid(1); // 启动内核 vectorAdd<<
>>(A, B, C, n);
<<
语法指定了网格和线程块的维度,告诉GPU如何组织线程来执行这个内核。
上一节我们看到了一个简单的单线程块内核,本节中我们来看看如何利用所有线程实现真正的并行计算。
为了让所有线程协作处理整个向量,我们需要修改内核,使每个线程处理不同的数据片段。这通过结合 threadIdx.x 和 blockDim.x(线程块内的线程总数)来实现。
以下是优化后的并行向量加法内核:
__global__ void parallelVectorAdd(float *A, float *B, float *C, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; // 计算全局线程ID int stride = blockDim.x * gridDim.x; // 计算总线程数作为步长 for (; i < n; i += stride) { C[i] = A[i] + B[i]; } }
这个内核的关键点在于:
blockIdx.x:当前线程块在网格中的索引。blockDim.x:每个线程块中的线程数。- 通过
i = blockIdx.x * blockDim.x + threadIdx.x计算出每个线程负责的全局起始索引。 - 使用
stride进行循环,使有限数量的线程能够处理任意大小的数组。
主机调用需要配置合适的网格和线程块大小:
int threadsPerBlock = 256; int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock; // 向上取整 parallelVectorAdd<<
>>(A, B, C, n);
仅仅启动并行线程还不够,性能优化的核心在于智能地使用共享内存。我们以矩阵乘法为例。
一个朴素的矩阵乘法内核会频繁地从全局内存读取数据,速度很慢。优化的思路是将计算分块,先将数据块从全局内存加载到共享内存,然后在共享内存中进行高速计算。
以下是利用共享内存的矩阵乘法核心思想(伪代码表示):
__global__ void matrixMulShared(float *A, float *B, float *C, int width) { // 为子矩阵A和B声明共享内存 __shared__ float sA[TILE_WIDTH][TILE_WIDTH]; __shared__ float sB[TILE_WIDTH][TILE_WIDTH]; int bx = blockIdx.x, by = blockIdx.y; int tx = threadIdx.x, ty = threadIdx.y; // 计算C中当前线程要处理的元素坐标 int row = by * TILE_WIDTH + ty; int col = bx * TILE_WIDTH + tx; float sum = 0; // 循环遍历所有数据块 for (int m = 0; m < width/TILE_WIDTH; ++m) { // 协作地将数据块从全局内存加载到共享内存 sA[ty][tx] = A[row * width + (m * TILE_WIDTH + tx)]; sB[ty][tx] = B[(m * TILE_WIDTH + ty) * width + col]; __syncthreads(); // 等待块内所有线程完成加载 // 在共享内存中进行子矩阵乘法计算 for (int k = 0; k < TILE_WIDTH; ++k) { sum += sA[ty][k] * sB[k][tx]; } __syncthreads(); // 等待计算完成,再进行下一轮数据加载 } // 将结果写回全局内存 C[row * width + col] = sum; }
这种分块策略将全局内存访问量从 O(n^3) 显著降低到 O(n^2),因为每个数据块只需从全局内存加载一次到共享内存,然后在该块内的所有计算中重复使用。
除了使用共享内存,另一个关键的优化技术是内核融合。在标准框架中,连续的运算(如矩阵乘法和ReLU激活)会分别启动独立的内核,每个内核都会将中间结果写回全局内存,再由下一个内核读回,造成了不必要的延迟和带宽消耗。
内核融合将多个连续操作合并到单个CUDA内核中执行。例如,将 Y = matmul(M, X) 和 Z = relu(Y) 融合成一个内核 fused_matmul_relu。这样,中间结果 Y 可以保留在寄存器或共享内存中,直接用于ReLU计算,完全避免了写回和读取全局内存的开销。

这种优化虽然增加了内核编写的复杂性,但通常能带来10%-20%的性能提升,对于大规模训练至关重要。这也是为什么追求极致性能的定制化AI系统(如Flash Attention)会将整个注意力机制实现为一个融合内核的原因。

本节课中我们一起学习了CUDA编程的核心概念。我们了解到,为了极致优化生成式AI模型的训练性能,尤其是应对超大规模模型和硬件错误,直接进行CUDA级编程是必要的。关键要点包括:理解GPU的线程-块-网格层次结构和共享内存的重要性;掌握编写并行内核的基本方法;学会通过分块策略利用共享内存优化数据密集型运算(如矩阵乘法);以及认识内核融合技术对于减少冗余内存访问的巨大价值。这些知识是理解现代高性能AI系统底层实现的基础。
在本节课中,我们将学习CUDA编程的基础知识。理解CUDA编程对于希望从底层优化和训练大型语言模型至关重要。我们将探讨为何需要CUDA编程、GPU的基本架构、如何编写简单的CUDA内核,以及如何利用共享内存和融合内核来提升计算效率。
CUDA编程是使用C/C++在NVIDIA GPU上进行编程的简称。在生成式AI领域,为了高效训练大型语言模型,我们常常需要绕过现有的高级库(如PyTorch),直接编写底层的CUDA代码以获得显著的性能提升。本节课将介绍CUDA编程的核心概念,包括线程、块、网格的结构,以及如何通过优化内存访问来加速计算。
上一节我们介绍了学习CUDA编程的必要性,本节中我们来看看GPU的基本架构和CUDA编程模型。
GPU的计算核心组织成一种层次结构。在最底层是线程,每个线程类似于CPU中的一个寄存器,执行最基本的计算单元。多个线程被组织成一个线程块。多个线程块进一步组成一个网格。一个CUDA内核调用通常对应整个网格。
这种结构设计主要与内存层次有关,而非纯粹的计算结构。在一个线程块内的所有线程共享一块快速的共享内存。这块内存比GPU的全局内存(显存)访问速度快得多。CUDA编程的核心挑战之一就是尽量减少从慢速的全局内存中读取数据的次数,尽可能多地利用快速的共享内存进行计算。
理解了基本架构后,我们通过一个简单的例子来学习如何编写CUDA内核函数。
一个CUDA内核函数是一个在GPU上每个线程上并行执行的函数。我们以向量加法 C = A + B 为例。一个天真的实现是只使用一个线程,通过一个循环遍历所有元素进行加法。但这完全没有利用GPU的并行能力。
为了并行化,我们需要让多个线程同时工作。每个线程负责计算结果向量中不同位置的和。CUDA运行时提供了内置变量,如 threadIdx.x 和 blockDim.x,它们分别表示当前线程在线程块内的索引和线程块中的线程总数。
以下是利用多线程进行向量加法的核心思路:
// 假设每个线程块有 blockDim.x 个线程 int i = threadIdx.x + blockIdx.x * blockDim.x; int stride = blockDim.x * gridDim.x; // 总线程数 for (; i < N; i += stride) { C[i] = A[i] + B[i]; }
这段代码中,每个线程根据其唯一的全局索引 i 计算对应的向量元素。循环中的 stride 确保了即使向量长度 N 远大于总线程数,所有元素也能被覆盖到。通过这种方式,我们实现了计算的完全并行化。
上一节我们看到了如何并行化简单的向量操作,本节中我们来看看更复杂的操作——矩阵乘法,并学习如何使用共享内存进行优化。
在矩阵乘法 C = A * B 中,朴素的方法是每个线程计算输出矩阵 C 中的一个元素。这需要该线程读取 A 的一整行和 B 的一整列数据,这些数据都来自全局内存,访问速度很慢。
优化的关键在于利用线程块的共享内存。基本思想是将大矩阵分块。例如,将矩阵 A 和 B 划分为多个 BxB 的子块。计算时,先将 A 和 B 对应的子块从全局内存加载到共享内存中,然后线程块内的所有线程协作,在共享内存中完成子块的矩阵乘法计算。
以下是这个过程的简化描述:
- 将矩阵
A的一个BxB子块加载到共享内存As。 - 将矩阵
B的一个BxB子块加载到共享内存Bs。 - 线程块内所有线程同步,确保数据加载完成。
- 每个线程使用
As和Bs中的数据计算输出子块中自己负责的部分。 - 重复步骤1-4,遍历所有需要的子块对,并累加结果。
如果不使用共享内存,计算一个 BxB 输出子块需要 O(B^3) 次全局内存访问。而使用共享内存后,只需要 O(B^2) 次全局内存访问(用于加载子块),后续的 O(B^3) 次计算全部在快速的共享内存中进行,从而大幅提升性能。

我们了解了如何用共享内存优化单个操作。但在实际模型中,多个操作常常连续发生。本节介绍一种重要的优化技术:融合内核。
考虑一个简单的操作序列:先进行矩阵乘法 Y = matmul(M, X),然后对结果 Y 应用ReLU激活函数。在标准的PyTorch写法中,这两个操作是独立的。计算流程如下:
- 计算
matmul(M, X),结果Y被写回全局内存。 - 从全局内存读取
Y,计算relu(Y),结果再写回全局内存。

这个过程在全局内存和计算单元之间产生了不必要的往返。融合内核的思想是将多个操作合并到单个CUDA内核中。对于这个例子,我们可以编写一个“MatMul + ReLU”融合内核:
- 线程计算
matmul的部分结果。 - 该部分结果保留在寄存器或共享内存中,不写回全局内存。
- 立即对该中间结果应用
ReLU函数。 - 将最终结果写回全局内存。
通过避免中间结果的全局内存读写,融合内核可以显著减少内存带宽压力,通常能带来10%-20%的性能提升。像Flash Attention这样的先进优化,本质上就是将注意力机制中的矩阵乘、Softmax、缩放等多个步骤融合到一个精心设计的内核中。
本节课我们一起学习了CUDA编程的基础知识。我们首先了解了为什么在训练超大语言模型时需要绕过PyTorch等框架进行底层CUDA编程。接着,我们探讨了GPU的线程-块-网格架构及其对应的内存层次(共享内存 vs. 全局内存)。通过向量加法和矩阵乘法的例子,我们学习了如何编写并行化的CUDA内核,并利用共享内存优化数据访问。最后,我们介绍了融合内核的概念,它将多个连续操作合并,以减少耗时的全局内存访问。
掌握这些基础概念对于理解现代大模型训练中的高性能计算优化至关重要。
在本节课中,我们将要学习状态空间模型。这是一种结合了循环神经网络和Transformer架构思想的新型模型,旨在解决传统RNN在并行化和长程记忆方面的不足,同时保持线性计算复杂度。我们将从基础概念开始,逐步探讨其核心公式、优化方法以及实际应用。











上一节我们介绍了生成式AI的背景,本节中我们来看看状态空间模型的基本概念。






状态空间模型是由Albert Gu等人提出的一系列工作。它本质上是对Transformer出现之前的循环神经网络的一种升级。在Transformer架构之前,人们主要使用循环神经网络处理自然语言。
循环神经网络的核心思想是维护一个状态。当输入一个序列时,RNN会根据输入逐步更新这个状态。


这种架构类似于人脑的工作方式:你逐词阅读,大脑状态随之逐步更新。
然而,传统的RNN存在两个主要问题:
- 缺乏并行化:RNN的更新是严格顺序的,这与现代GPU偏好并行计算的特点不兼容。Transformer则通过自注意力层和前馈层实现了高度并行计算。
- 缺乏长程记忆:RNN的状态是逐步更新的,某个时刻的标记(Token)难以直接回溯到很久之前的上下文去寻找答案。而Transformer的自注意力机制允许任何标记直接关注序列中任何位置的标记。

正是这些缺点导致RNN一度被Transformer取代。但状态空间模型的出现,似乎让这种RNN结构重新焕发了活力。
上一节我们回顾了RNN的优缺点,本节中我们来看看状态空间模型是如何构建的。
状态空间模型可以看作是RNN与Transformer的一种结合。其核心思想是用一个类RNN的结构替换Transformer中的自注意力层,同时保留其前馈层。
一个基本的状态空间模型块结构如下:
- 一个状态空间层(替代自注意力层),用于处理序列信息。
- 一个前馈层(如MoE或MLP),用于进行逐标记的局部处理。
你可以将多个这样的块堆叠起来,形成一个深度模型,就像堆叠Transformer块一样。
状态空间模型的核心是一个线性化的RNN。它通过一个连续的或离散的线性过程来更新状态。
x_t是输入序列。y_t是输出序列。h_t是隐藏状态。Ā,B̄,C是可学习的参数矩阵。
Ā 和 B̄ 通常是通过对连续时间公式进行离散化(如零阶保持法)得到的:
Ā = exp(Δ * A)
B̄ = (exp(Δ * A) - I) * (Δ * A)^{-1} * Δ * B
其中 Δ 是步长参数。
如果我们展开上述更新公式,会发现输出 y_t 实际上是输入 x 与一个卷积核 K̄ 的卷积结果:
y = x * K̄
其中卷积核 K̄ 的元素由 C * Ā^{k} * B̄ 决定(k 为时间步偏移量)。
这意味着,线性状态空间模型在数学上等价于一个(可能无限长的)卷积操作。
上一节我们介绍了基础模型,本节中我们来看看如何优化它,使其更高效、更强大。
基础模型存在计算效率低和缺乏类似Transformer多头机制的问题。S4模型通过对角化技术来解决这些问题。
在S4中,参数矩阵 A 被约束为对角矩阵。这大大减少了参数量(从 N×N 降至 N),并使得计算 Ā 的幂次变得非常简单(只需对每个对角线元素进行幂运算)。

更重要的是,这种对角化形式天然支持一种“多头”机制。我们可以将输入的每个特征维度(通道)视为独立的,并为每个通道配备一组独立的 A_i, B_i, C_i 参数。
以下是其工作原理:
- 输入
x_t是一个向量。 - 对于该向量的第
i个维度(通道),我们应用一个独立的状态空间模型:
h_t^i = Ā_i * h_{t-1}^i + B̄_i * x_t^i
y_t^i = C_i * h_t^i - 每个通道的卷积核
K̄_i是不同的,这允许模型在不同的特征维度上捕获不同的时间模式。
例如,我们可以让某些通道的 Ā_i 接近1,使其关注长期历史(类似于求平均),而让另一些通道的 Ā_i 很小,使其只关注近期信息。这实现了某种与位置相关的编码功能。
尽管如此,S4的卷积核是上下文无关的,它们在训练后是固定的,不随输入内容变化。
上一节我们介绍了S4,本节中我们来看看其关键进化——Mamba模型(在论文中常称为S6),它如何引入上下文感知能力。
S4的主要限制在于其卷积核是静态的。Mamba的核心改进是让参数 B, C 以及步长 Δ 成为输入 x_t 的函数,从而使模型能够根据输入内容动态调整其行为。
这意味着:
B_t和C_t现在扮演着类似Transformer中“键”和“查询”的角色,它们基于当前上下文动态生成。Δ_t成为一个时间相关的缩放因子。
这个过程被称为选择性机制。模型可以根据当前输入 x_t(它已编码了之前的上下文信息)来决定当前标记的重要性。例如,对于关键词或实体名称,模型可以生成较大的 B_t,让该标记对隐藏状态产生更大影响;对于不重要的虚词,则可以忽略其影响。
Mamba的另一个巨大优势是其线性时间复杂度。与Transformer自注意力的 O(n²) 复杂度不同,状态空间模型按顺序处理序列,复杂度为 O(n)。这对于处理超长序列(如长文档、视频、音频)至关重要。
为了实现高速计算,Mamba使用了高度优化的CUDA内核,确保关键的中间状态(隐藏状态 h_t)始终驻留在GPU的高速缓存(SRAM)中,避免了与慢速显存(VRAM)的频繁数据交换。这是其性能远超朴素PyTorch实现的关键。
状态空间层(尤其是经过对角化后)的参数数量远小于标准的自注意力层。这意味着,在总参数量固定的情况下,使用状态空间模型的网络可以将更多参数分配给前馈层(如MoE),而前馈层通常对模型的知识存储和复杂模式建模能力贡献更大。因此,在同等计算预算或参数量下,Mamba架构的模型可能表现更优。
本节课中我们一起学习了状态空间模型,从传统的RNN出发,探讨了其基本形式S4和先进的Mamba模型。
我们了解到:
- 状态空间模型 本质上是线性RNN,可视为一个卷积操作。
- S4模型 通过对角化和为每个通道配备独立动态,引入了高效的多头机制,但卷积核是静态的。
- Mamba模型 通过使关键参数依赖于输入,实现了选择性机制,从而能够动态聚焦于相关上下文。
- 该架构的核心优势在于线性计算复杂度和高效的硬件利用,使其特别适合处理长序列数据。
状态空间模型并非要完全取代Transformer的自注意力机制,而是提供了一种高效的替代方案。在许多任务中,这种线性时间、上下文敏感的“总结”与“筛选”机制已经足够。对于需要精确回溯或多跳推理的复杂任务,未来可能会看到混合架构的出现,例如将Mamba与局部注意力相结合。无论如何,状态空间模型为生成式AI模型的设计开辟了一条富有前景的新路径。
在本节课中,我们将学习深度学习库PyTorch的基础知识,以及如何使用Weights & Biases(W&B)工具来记录和可视化训练过程。本教程旨在帮助初学者快速上手,为后续课程作业做好准备。
PyTorch的核心数据结构是张量(Tensor),可以将其理解为高维数组或矩阵。它与NumPy非常相似,但支持GPU加速和自动微分。
以下是创建张量的几种基本方法:
- 从列表创建:使用
torch.tensor()函数。data = [[1, 2], [3, 4]] x = torch.tensor(data, dtype=torch.float32) - 从NumPy数组创建:使用
torch.from_numpy()函数。import numpy as np np_array = np.ones((2, 2)) x = torch.from_numpy(np_array) - 创建特殊张量:PyTorch提供了类似NumPy的函数来创建全零、全一或随机张量。
zeros_tensor = torch.zeros((2, 3)) # 2x3的全零张量 ones_tensor = torch.ones((2, 3)) # 2x3的全一张量 rand_tensor = torch.rand((2, 3)) # 2x3的随机张量(值在0-1之间)
创建张量后,可以进行各种数学运算。
- 基本算术运算:支持加(
+)、减(-)、乘(*)、除(/)和矩阵乘法(@或torch.matmul)。a = torch.rand(2, 2) b = torch.rand(2, 2) c = a + b # 逐元素相加 d = a @ b # 矩阵乘法 - 形状操作:
view()用于改变张量形状(要求内存连续),reshape()更通用。transpose()用于转置,squeeze()和unsqueeze()用于删除或添加维度。x = torch.rand(4, 5) y = x.view(10, 2) # 改变形状为10x2 z = x.transpose(0, 1) # 转置,形状变为5x4 - 拼接与堆叠:
torch.cat()沿现有维度拼接张量,torch.stack()沿新维度堆叠张量。a = torch.rand(2, 3) b = torch.rand(2, 3) c = torch.cat([a, b], dim=0) # 沿第0维(行)拼接,形状变为(4, 3) d = torch.stack([a, b], dim=0) # 沿新维度堆叠,形状变为(2, 2, 3)
上一节我们介绍了张量的基本操作,本节中我们来看看PyTorch如何构建计算图以实现自动微分。
PyTorch的自动微分功能是其核心特性之一。要使用它,只需在创建张量时设置 requires_grad=True。
x = torch.tensor([1.0, 2.0], requires_grad=True) y = x 2 z = y.sum() z.backward() # 计算梯度 print(x.grad) # 输出梯度 tensor([2., 4.])
关键点:
- 调用
.backward()方法后,叶子节点(用户直接创建的张量)的梯度会被计算并存储在.grad属性中。 - 非叶子节点(计算中间结果)的梯度默认不会保留,以节省内存。
- 在训练循环中,通常的模式是:计算损失 ->
loss.backward()-> 优化器更新参数 (optimizer.step()) -> 清空梯度 (optimizer.zero_grad())。






在训练模型前,需要有效地加载和组织数据。PyTorch提供了 Dataset 和 DataLoader 类来简化这一过程。
torchvision 库包含许多计算机视觉领域的常用数据集。
import torchvision from torchvision import transforms # 定义图像转换(如转为张量、归一化) transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) # 加载Fashion-MNIST数据集 train_dataset = torchvision.datasets.FashionMNIST( root='./data', train=True, download=True, transform=transform ) test_dataset = torchvision.datasets.FashionMNIST( root='./data', train=False, download=True, transform=transform )




Fashion-MNIST数据集包含10类服装的灰度图像,训练集60000张,测试集10000张,每张图像大小为28x28。





对于自己的数据,可以通过继承 torch.utils.data.Dataset 类来创建数据集。
from torch.utils.data import Dataset class CustomDataset(Dataset): def __init__(self, feature_files, annotation_files, transform=None): # 初始化,加载文件路径等元数据 self.features = feature_files self.labels = annotation_files self.transform = transform def __len__(self): # 返回数据集大小 return len(self.features) def __getitem__(self, idx): # 根据索引加载单个样本(如图像)和标签 feature = load_feature(self.features[idx]) # 需实现load_feature函数 label = self.labels[idx] if self.transform: feature = self.transform(feature) return feature, label
**实践:对于大型数据集,建议在 __getitem__ 方法中动态加载数据(如从磁盘读取图像),而不是在 __init__ 中全部加载到内存。这样便于进行随机数据增强。
DataLoader 负责从 Dataset 中按批次抽取数据,并支持多进程加载、数据打乱等功能。
from torch.utils.data import DataLoader train_loader = DataLoader( train_dataset, batch_size=64, shuffle=True, # 训练时打乱数据顺序 num_workers=2 # 使用2个子进程加载数据 ) test_loader = DataLoader( test_dataset, batch_size=64, shuffle=False, # 测试时通常不需要打乱 num_workers=2 ) # 迭代数据 for batch_idx, (images, labels) in enumerate(train_loader): # images形状: [64, 1, 28, 28] # labels形状: [64] # ... 进行训练 ... pass
在PyTorch中,神经网络通常通过继承 torch.nn.Module 类来定义。
你需要定义 __init__ 方法来初始化网络层,以及 forward 方法来定义前向传播过程。
import torch.nn as nn import torch.nn.functional as F class SimpleMLP(nn.Module): def __init__(self, input_size=784, hidden_size=128, num_classes=10): super(SimpleMLP, self).__init__() self.fc1 = nn.Linear(input_size, hidden_size) self.fc2 = nn.Linear(hidden_size, num_classes) def forward(self, x): # 将图像展平 x = x.view(-1, 28*28) x = F.relu(self.fc1(x)) x = self.fc2(x) return x
注意:你只需定义 forward 方法。反向传播(计算梯度)由PyTorch的 autograd 系统自动处理,只需调用 loss.backward()。
虽然不常用,但你可以通过继承 nn.Module 或 torch.autograd.Function 来创建自定义层。

class CustomLinearFunction(torch.autograd.Function): @staticmethod def forward(ctx, input, weight, bias=None): ctx.save_for_backward(input, weight, bias) output = input @ weight.t() if bias is not None: output += bias return output @staticmethod def backward(ctx, grad_output): input, weight, bias = ctx.saved_tensors grad_input = grad_weight = grad_bias = None # ... 计算梯度 ... return grad_input, grad_weight, grad_bias    # 使用自定义函数封装成模块 class CustomLinear(nn.Module): def __init__(self, in_features, out_features): super().__init__() self.weight = nn.Parameter(torch.randn(out_features, in_features)) self.bias = nn.Parameter(torch.randn(out_features)) def forward(self, x): return CustomLinearFunction.apply(x, self.weight, self.bias)
以下是计算分类准确率的示例:
def accuracy(predictions, labels): # predictions形状: [batch_size, num_classes] # labels形状: [batch_size] _, predicted = torch.max(predictions, dim=1) # 获取预测类别 correct = (predicted == labels) acc = correct.float().mean() # 计算正确率 return acc
对于损失函数,PyTorch提供了许多内置选项(如 nn.CrossEntropyLoss, nn.MSELoss)。你也可以自定义,但通常利用PyTorch的自动微分,只需用基本运算组合出损失计算即可。
现在,我们将把前面所有的部分组合起来,进行模型训练,并使用Weights & Biases(W&B)来记录实验过程。
首先,安装W&B库并初始化一个运行(run)。
pip install wandb
import wandb # 登录W&B(需要API Key,在wandb.ai官网获取) wandb.login(key='your_api_key_here') # 初始化一个W&B运行 wandb.init( project="my-first-project", # 项目名称,用于分组实验 name="simple-mlp-run-1", # 本次运行的名称 config={ # 记录超参数 "learning_rate": 0.001, "batch_size": 64, "epochs": 10 } ) # 实例化模型、损失函数和优化器 model = SimpleMLP() criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 将模型架构保存为文件并记录到W&B torch.save(model.state_dict(), 'model_architecture.pth') wandb.save('model_architecture.pth')
在训练循环中,关键步骤包括设置训练模式、将数据移至GPU、前向传播、计算损失、反向传播和优化器更新。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model.to(device) num_epochs = 10 for epoch in range(num_epochs): model.train() # 设置为训练模式(影响Dropout、BatchNorm等层) running_loss = 0.0 running_acc = 0.0 for batch_idx, (images, labels) in enumerate(train_loader): # 1. 将数据移至GPU images, labels = images.to(device), labels.to(device) # 2. 梯度清零 optimizer.zero_grad() # 3. 前向传播 outputs = model(images) loss = criterion(outputs, labels) acc = accuracy(outputs, labels) # 4. 反向传播 loss.backward() # 5. 优化器更新参数 optimizer.step() # 记录批次损失和准确率 running_loss += loss.item() running_acc += acc.item() # 计算平均损失和准确率 epoch_loss = running_loss / len(train_loader) epoch_acc = running_acc / len(train_loader) # 6. 记录到W&B wandb.log({ "epoch": epoch, "train_loss": epoch_loss, "train_accuracy": epoch_acc }) print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {epoch_loss:.4f}, Acc: {epoch_acc:.4f}') # 7. (可选)保存模型检查点 checkpoint = { 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'loss': epoch_loss, } torch.save(checkpoint, f'checkpoint_epoch_{epoch}.pth') wandb.save(f'checkpoint_epoch_{epoch}.pth') # 备份到W&B云端
训练开始后,你可以在W&B网站查看实时更新的图表:
- 损失和准确率曲线:自动根据
wandb.log()记录的数据生成。 - 系统资源监控:如GPU、CPU和内存使用情况。
- 记录的文件:包括保存的模型架构文件和检查点。
- 控制台日志:训练过程中打印的信息也会被记录。
重要提示:在Colab等临时环境中,将检查点保存到W&B云端至关重要,这样即使运行时断开连接,也能恢复训练。
训练完成后,在测试集上评估模型性能。
model.eval() # 设置为评估模式 test_loss = 0.0 test_acc = 0.0 with torch.no_grad(): # 禁用梯度计算,节省内存和计算资源 for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) acc = accuracy(outputs, labels) test_loss += loss.item() test_acc += acc.item() test_loss /= len(test_loader) test_acc /= len(test_loader) print(f'Test Loss: {test_loss:.4f}, Test Acc: {test_acc:.4f}') wandb.log({"final_test_loss": test_loss, "final_test_accuracy": test_acc}) # 结束W&B运行 wandb.finish()
本节课中我们一起学习了PyTorch和Weights & Biases的核心基础:
- PyTorch张量:学习了如何创建和操作张量,这是构建所有计算的基础。
- 自动微分:理解了PyTorch如何通过
autograd自动计算梯度,这是训练神经网络的关键。 - 数据管道:掌握了使用
Dataset和DataLoader高效加载和处理数据的方法。 - 神经网络构建:学会了通过继承
nn.Module来定义自己的模型结构。 - 训练流程:实现了完整的训练循环,包括前向传播、损失计算、反向传播和参数更新。
- 实验跟踪:使用Weights & Biases记录超参数、指标、日志和模型文件,实现了实验的可视化与管理。


掌握这些基础知识,将为你顺利完成本课程的作业和深入理解生成式AI模型打下坚实的基础。

在本节课中,我们将学习如何从零开始构建一个基于Transformer的GPT模型。我们将使用莎士比亚的作品作为训练数据,并逐步了解模型的结构、数据加载、训练流程以及需要你实现的核心功能。
我们将使用一个名为 minGPT 的代码库。它包含几个核心文件,我们将逐一介绍它们的功能。你的主要任务是修改 model.py 文件,以实现诸如旋转位置编码和分组查询注意力等高级功能。
以下是起始代码的文件夹结构:
minGPT/:主文件夹。model.py:这是你需要修改的主要文件,包含了模型的核心架构。trainer.py:负责训练循环(前向传播、反向传播、权重更新)。utils.py:处理日志记录和配置的辅助文件。
cadgpt.py:主运行文件,你不需要修改它。input.txt:训练数据(莎士比亚全集)。
cadgpt.py 文件首先会加载数据。为了简化,我们的词表大小仅为65,对应莎士比亚作品中使用的所有字符(包括大小写字母、标点和空格)。数据集的功能是将每个字符映射为一个整数,然后将这些整数序列分割成块,供模型训练。
核心概念:字符级词表映射。
# 伪代码示例:将字符映射为索引 vocab = {‘a‘: 0, ‘b‘: 1, ..., ‘ ‘: 64}
使用小词表的好处是模型参数量更少,训练更快。未来你可以将其替换为更复杂的子词分词器。
trainer.py 文件实现了标准的训练循环:
- 从数据加载器中获取一个批次的数据。
- 将模型设置为训练模式。
- 执行前向传播,计算损失。
- 执行反向传播,更新模型权重。
- 循环上述步骤直至达到最大迭代次数。
你不需要修改这个文件,它已经提供了完整的训练逻辑。
现在,让我们深入 model.py,看看Transformer模型是如何构建的。

model.py 中定义了一个 GPT 类。它的结构如下:
- 输入嵌入层:将输入的词元索引映射为向量。
- Dropout层:用于正则化,防止过拟合。
- Transformer块列表:模型的核心,由多个相同的块堆叠而成。
- 输出线性层:将最终的隐藏状态映射回词表大小的输出。


每个 Transformer块 又包含两个主要部分:
- 自注意力层:计算输入序列中各个位置之间的关系。
- 前馈神经网络:一个简单的两层线性网络,用于非线性变换。
前向传播时,输入依次通过嵌入层、各个Transformer块,最后通过输出层。代码中还包含了对注意力计算时间和内存消耗的测量,这将用于后续的性能分析。
上一节我们介绍了模型的整体结构,本节中我们来看看其核心组件——因果自注意力机制。

在 model.py 的 CausalSelfAttention 类中实现了注意力机制。其步骤如下:


- 线性投影:输入
X通过一个线性层,同时被投影为查询、键和值矩阵。代码中通过一个线性层输出后再分割,这比使用三个独立的层更高效。# 伪代码:Q, K, V = split(linear(X)) - 维度重排:为了计算效率,将张量维度调整为
(batch, heads, seq_len, head_dim),确保计算维度在最右侧。 - 注意力计算:计算
Q和K的点积,得到原始注意力分数。 - 应用掩码:为了防止模型在训练时“偷看”未来的信息,我们需要应用一个因果掩码。这将未来位置(即当前词元之后的位置)的注意力分数设置为一个非常大的负数(如
-1e9),这样在后续的 softmax 操作中,这些位置的权重会接近于零。# 伪代码:mask = torch.tril(torch.ones(seq_len, seq_len)) - Softmax与Dropout:对掩码后的注意力分数进行缩放(除以
sqrt(head_dim)),应用 softmax 归一化得到注意力权重,然后使用 dropout 进行正则化。 - 加权求和:将注意力权重与值矩阵
V相乘,得到每个位置的上下文感知表示。 - 输出投影:将多个注意力头的输出拼接起来,并通过一个最终的线性投影层。
在实现分组查询注意力时,你需要深刻理解这段代码,因为其核心逻辑是相似的。

上一节我们了解了标准的注意力机制,本节中我们来看看如何让模型感知词元的位置信息,特别是通过旋转位置编码。


标准的注意力机制对输入顺序是不敏感的。例如,句子“Your cat is a lovely cat”中,两个“cat”在初始嵌入中是相同的。但第一个“cat”是主语,第二个是宾语,位置信息至关重要。因此,我们需要向模型注入位置信息。
主要有两种类型:
- 绝对位置编码:为序列中的每个绝对位置学习或计算一个固定的向量,然后加到词嵌入上。
- 学习式嵌入:直接作为模型参数学习。缺点是模型无法处理比训练时更长的序列。
- 正弦编码:使用正弦和余弦函数生成固定模式的位置编码,具有一定的外推性,但距离关系不直观。
- 相对位置编码:编码词元之间的相对距离。虽然更符合直觉,但计算更复杂,通常需要在注意力计算中引入额外的项,导致速度较慢。
RoPE 巧妙地通过旋转矩阵将绝对位置信息编码到查询和键向量中,使得注意力分数仅依赖于它们的相对位置。
核心思想:对于一个位置为 m 的词元嵌入向量 x,我们将其视为二维空间中的一组点对 (x_i, x_{i+1})。RoPE 使用一个旋转矩阵 R 对这个点对进行旋转,旋转角度 θ 与位置 m 和维度 i 相关。
# 二维旋转矩阵公式 R = [[cosθ, -sinθ], [sinθ, cosθ]]
对于高维向量,我们将其分成若干二维子空间,分别应用旋转。最终,经过旋转的查询 q 和键 k 的点积会自然地包含它们的位置差信息。
优势:
- 外推性好:可以处理比训练序列更长的输入。
- 相对性:注意力分数自动包含相对位置信息,无需修改注意力计算式。
- 性能佳:被 LLaMA、GPT-NeoX 等众多先进模型采用,能带来更快的收敛速度和更低的验证损失。


在作业中,你将在注意力计算前,对查询和键应用 RoPE。
上一节我们探讨了如何改进位置编码,本节我们来看如何优化注意力机制本身,以在性能和效率间取得平衡,即分组查询注意力。






- 多头注意力:将模型维度分割成
H个头,每个头有独立的Q, K, V矩阵。计算精度高,但存储所有头的K, V缓存需要大量内存,特别是在长序列生成时。 - 多查询注意力:所有头共享同一个
K和V矩阵。这大幅减少了内存占用和推理时间,但可能导致模型容量下降,性能受损。
GQA 是两者的折中方案。它将查询头分成 G 个组,每个组共享一套 K 和 V 矩阵。
工作原理:
- 假设有
H_q个查询头,H_kv个键值头(H_kv < H_q)。 - 将
H_q个查询头分成G = H_kv个组。 - 每个组内的查询头共享同一套
K和V。 - 注意力计算仍在每个查询头上独立进行,但使用的
K和V来自其所属的组。
优势:
- 内存效率:介于 MHA 和 MQA 之间,显著减少了
K, V缓存。 - 性能:通过分组保留了一定的模型容量,性能通常优于 MQA,接近 MHA。
- 推理速度:比 MHA 更快。
在作业中,你需要修改注意力代码,实现分组查询注意力机制。
最后,我们简要介绍另一种用于处理长序列的技术——滑动窗口注意力,它将在作业的书面部分涉及。


标准的全注意力时间复杂度为 O(n²),对于长文档(如数十页文本)来说,计算和内存开销是无法承受的。


滑动窗口注意力限制每个词元只关注其前后一定窗口大小 W 内的词元。
- 因果滑动窗口:在生成任务中,每个词元只关注其左侧的
W个词元。 - 复杂度:时间复杂度降低到
O(n * W)。当W远小于n时,近似为线性复杂度。
信息传递:虽然单层只能看到局部窗口,但通过堆叠多个注意力层,高层词元可以间接地接收到更远距离词元的信息(类似于感受野的扩大)。
注意:一个简单的实现可能仍然会计算完整的 n x n 注意力矩阵然后掩码,但这并不能节省内存。高效的实现需要避免计算被掩码的部分。

在实现上述功能时,以下两个函数可能会非常有用:

rearrange(来自einops库):可以直观地描述张量的维度重排和重塑操作,比一连串的permute和view更清晰。einsum:用于简洁地表示复杂的张量运算(如矩阵乘法、求和等)。torch.einsum和einops.einsum功能类似,后者允许使用更易读的维度名。

本节课我们一起学习了构建一个简易GPT模型的完整流程:
- 我们了解了代码库的结构和数据预处理方式。
- 我们深入分析了Transformer模型的核心架构,特别是因果自注意力机制。
- 我们探讨了旋转位置编码的原理与优势,它是让模型感知位置信息的关键。
- 我们学习了分组查询注意力,这是一种在模型性能和推理效率之间取得平衡的有效技术。
- 我们简介了用于处理长序列的滑动窗口注意力。
- 最后,我们介绍了一些能简化实现的工具函数。

你的主要编码任务是在 model.py 中实现 RoPE 和 GQA。建议先在本地CPU上调试代码,确保无误后再上传到Colab等GPU环境进行实验和性能测量。祝你编码顺利!
在本节课中,我们将学习扩散模型的核心概念、相关数学原理,并了解作业二的编程实现细节。课程内容涵盖扩散模型概述、数学证明、代码结构、评估指标以及实用编程技巧。
上一节我们介绍了课程安排,本节中我们来看看扩散模型的基本概念。
在扩散模型中,我们有一个前向扩散过程和一个反向扩散过程。前向扩散过程随时间逐步向图像添加噪声,直到图像完全被破坏。反向扩散过程则使用一个神经网络(通常是U-Net)来逐步去除噪声,从而恢复或生成图像。
需要明确的是,前向扩散与神经网络的前向传播是不同的概念。前向扩散是添加噪声的过程,而神经网络的前向传播是计算网络输出的过程。


扩散模型的前向过程是一个马尔可夫链,即每一步只依赖于前一步的状态。反向过程,在我们当前讨论的版本中,通常也被建模为只依赖于前一步的马尔可夫链。
我们的目标是训练一个U-Net网络,使其能够学习从噪声图像中去除噪声,从而执行反向扩散过程。这里,x_0 代表无噪声的原始图像,而 x_T 代表完全被噪声破坏的图像。
一个常见的误解是,去噪过程仅仅是移除了之前添加的“垃圾”信息,从而恢复原始图像。实际上,经过反向扩散生成的图像通常是全新的,并非训练集中的某张图片。这表明模型学习的是图像数据本身的分布,而非简单的记忆和恢复。

上一节我们澄清了扩散模型并非简单的图像恢复,本节中我们来看看如何从另一个角度理解扩散过程。
我们可以将一张图像视为一个高维空间中的向量。例如,一张32x32的RGB图像可以展开成一个3072维的向量。流形假设认为,所有可能的有效图像(例如所有猫的图片)构成了这个高维空间中的一个低维流形(子空间)。我们的目标是从这个复杂的图像分布中采样。

然而,直接定义和从这个分布中采样非常困难。扩散模型提供了一种解决方案:它从一个易于采样的简单分布(如高斯分布)开始,然后学习沿着目标图像分布梯度的方向移动,最终到达高概率区域(即有效的图像)。这个过程属于一类称为“分数匹配”的生成模型。
以下是该过程的核心思想:
- 从一个简单分布(如高斯噪声)开始采样。
- 使用神经网络(U-Net)估计目标图像分布的梯度(分数)。
- 沿着梯度方向,结合朗之万动力学进行采样,逐步移动到图像流形上的高概率点。
通过这种方式,模型学会了如何将随机噪声“塑造”成符合目标数据分布的图像。
上一节我们从概念上理解了扩散模型,本节中我们来看看支撑其训练的一些关键数学原理,这些将有助于完成作业的书面部分。
首先,回顾两个基础工具:
- 贝叶斯定理:
P(A|B) = P(B|A) * P(A) / P(B)。这是概率推理的基石。 - 琴生不等式:对于一个凹函数
f,有E[f(X)] <= f(E[X])。这在推导优化目标时至关重要。

在扩散模型中,我们通常不直接优化难以处理的目标函数,而是优化其证据下界。
我们的目标是最大化数据的对数似然 log p(x)。通过引入潜变量 z 和一个任意的变分分布 q(z),我们可以推导出ELBO:
log p(x) >= E_{z~q(z)}[log (p(x, z) / q(z))]
这里,我们利用了琴生不等式,其中函数 f 是 log(p(x,z)/q(z)),期望是关于 q(z) 计算的。优化这个下界等价于间接优化原始目标。


注意:在代码和公式中,要仔细区分方差 (σ^2) 和标准差 (σ),这是一个常见的错误来源。
理论上,扩散模型的推导是在连续数据上进行的。但实际上,图像像素值是离散的(0到255)。在实现时,我们通常会将图像像素值归一化到[-1, 1]的范围内进行计算,在需要可视化时再转换回来。在严格的数学证明中,需要留意连续与离散设定之间的差异。
上一节我们介绍了相关的数学背景,本节中我们来看看作业二的编程任务具体内容。
作业二的代码框架包含以下几个主要文件,你只需要修改 diffusion.py:
main.py: 程序入口,设置参数和训练流程。trainer.py: 训练循环的实现。unet.py: U-Net模型架构,用于预测噪声。diffusion.py: 需要你实现的核心文件,包含扩散过程的前向加噪、训练损失计算和反向采样。
- U-Net: 这是一个编码器-解码器结构的网络,用于在反向扩散过程的每一步预测所添加的噪声。
- 噪声调度器: 控制前向过程中每一步所添加噪声的量。本次作业采用改进的基于余弦函数的调度器,它比线性调度器能产生更平滑的噪声变化。
- 训练算法:
- 输入一张训练图像
x_0。 - 随机选择一个时间步
t。 - 采样噪声
ε ~ N(0, I)。 - 通过前向过程计算加噪后的图像
x_t。 - 让U-Net预测噪声
ε_θ(x_t, t)。 - 计算预测噪声与真实噪声之间的L1损失,并执行梯度下降。
- 输入一张训练图像
- 采样算法: 我们采用讲义中提到的“Option C”方法进行反向采样。
- 从纯噪声
x_T ~ N(0, I)开始。 - 从
t = T循环到t = 1:- 使用U-Net预测
x_t对应的x_0的估计值。 - 根据公式计算
x_{t-1}分布的均值。 - 该分布的方差是预先计算好的。
- 利用重参数化技巧,从该分布中采样得到
x_{t-1}。
- 使用U-Net预测
- 在得到估计的
x_0后,需要将其值裁剪到[-1, 1]范围内。
- 从纯噪声


在 diffusion.py 中,extract 函数是一个有用的辅助函数。它用于从一个包含所有时间步系数的张量中,提取出特定时间步 t 对应的系数。
例如:
# a 是一个包含T个系数的张量 # t 是一个形状为(batch_size,)的张量,包含每个样本的时间步索引 # 提取每个样本对应时间步的系数 a_t = extract(a, t, x.shape)
在初始化时预计算好各种系数(如 α, β),然后在采样循环中使用 extract 函数高效地获取对应时间步的值,可以提升计算效率。
我们提供了Google Colab笔记本,你可以直接运行。主要步骤包括:
- 挂载Google Drive。
- 安装依赖包。
- 下载AFHQ(动物面部高质量)数据集。
- 运行训练和可视化代码。
你可以通过修改命令行参数来调整图像大小、批次大小等,以适应你的GPU内存。作业还支持启用FID计算和生成过程可视化。
上一节我们介绍了如何实现和训练模型,本节中我们来看看如何评估生成图像的质量,这需要用到FID指标。
我们需要一个客观的指标来评估生成图像的质量,而不是依赖人工判断。弗雷歇初始距离(FID)是目前常用的指标。
FID基于两个概念:
- 弗雷歇距离:又称沃瑟斯坦距离或“推土机距离”。它衡量的是将一个概率分布变换成另一个概率分布所需要移动的“概率质量”的最小总成本。直观上,两个分布越相似,需要移动的质量越少,距离越小。
- Inception-v3模型:一个在ImageNet上预训练的图像分类网络。我们并不使用其分类层,而是使用其最后一个池化层之前的特征作为强大的图像特征提取器。
- 分别取一批真实图像和一批生成图像。
- 将它们输入Inception-v3网络,提取特征。
- 将真实图像的特征分布和生成图像的特征分布分别建模为多元高斯分布(这是基于最大熵原理的合理近似)。
- 计算这两个高斯分布之间的弗雷歇距离。对于高斯分布,该距离有闭合形式:
FID = ||μ_r - μ_g||^2 + Tr(Σ_r + Σ_g - 2(Σ_r Σ_g)^{1/2})
其中μ是均值,Σ是协方差矩阵,Tr是迹。
FID值越低,表示生成图像的特征分布与真实图像的特征分布越接近,即生成质量越高。
- FID不是损失函数:我们不会直接用FID来训练模型,因为它计算昂贵且与最终生成目标的联系不够直接。
- 对特征提取器的依赖:FID依赖于Inception-v3在自然图像上学习的特征。如果评估领域与自然图像差异很大(如医学影像),FID可能不适用。
- 与KL散度的对比:FID是一个对称的距离度量,而KL散度不对称。FID在实践中被证明与人类感知更一致。
- 实用工具:在实际应用中,可以使用现成的库(如
clean-fid)来计算FID,无需自己实现。
上一节我们学习了理论评估指标,本节中我们通过阅读几个PyTorch函数的文档,来巩固编程实践中的注意事项。
熟练阅读官方文档是编程的重要技能。以下是作业中可能用到的几个函数,我们一起来分析。
功能:将输入张量中的所有元素限制在 [min, max] 区间内。小于 min 的值变为 min,大于 max 的值变为 max。
代码示例分析:
import torch import numpy as np x = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8, 9]) y = torch.clamp(x, min=4, max=6) print(y) # 输出: tensor([4, 4, 4, 4, 5, 6, 6, 6, 6])
功能:计算输入张量在指定维度上的累积乘积。
代码示例分析:
x = torch.tensor([[1, 2, 3], [4, 5, 6]]) # 沿维度0(列方向)累积乘 y0 = torch.cumprod(x, dim=0) print(y0) # 输出: tensor([[ 1, 2, 3], # [ 4, 10, 18]]) # 计算过程: 第一行不变;第二行: [1*4, 2*5, 3*6] = [4, 10, 18] # 沿维度1(行方向)累积乘 y1 = torch.cumprod(x, dim=1) print(y1) # 输出: tensor([[ 1, 2, 6], # [ 4, 20, 120]]) # 计算过程: 第一行: [1, 1*2, 1*2*3] = [1, 2, 6] # 第二行: [4, 4*5, 4*5*6] = [4, 20, 120]
功能:创建一个指定形状的张量,并用填充值填满。
代码示例分析:
# 正确用法:size参数需要是一个元组或列表 x1 = torch.full((2, 3), 3.0) print(x1) # 输出一个2行3列,所有元素为3.0的张量 # 错误用法:将尺寸作为多个参数传递会导致错误 # x2 = torch.full(2, 3, 3.0) # 会报错 # 比较两个张量是否完全相等 x3 = torch.ones(2, 3) * 3 print(torch.equal(x1, x3)) # 输出: True print(x1 == x3) # 输出:一个所有元素为True的布尔张量
阅读文档的关键点:
- 注意函数要求的输入类型(如张量、元组、标量)。
- 理解每个参数的含义。
- 通过简单例子验证自己的理解。

本节课中我们一起学习了扩散模型的工作原理、相关的数学基础(ELBO、重参数化)、作业二的代码实现框架、评估生成质量的FID指标,并通过实例练习了如何阅读PyTorch文档。希望这些内容能帮助你顺利完成作业二。
在本节课中,我们将学习如何利用少量数据进行学习,并深入探讨如何通过微调技术,特别是LoRA(低秩适应),来使大型语言模型适应特定任务。
上一节我们介绍了生成式模型的基础。本节中,我们来看看当数据量有限时,如何进行学习。这通常分为两种类型:少样本学习和零样本学习。
- 少样本学习:模型会看到新任务的少量示例,然后需要处理该任务的新实例。
- 零样本学习:模型在没有看到任何示例的情况下,直接处理新任务。
如何实现少样本学习?一种方法是元学习,即学习如何学习。另一种更直接的方法是将其视为一个标准的监督学习问题,将少量示例输入序列模型(如Transformer)进行训练。
上下文学习与元学习密切相关。它指的是大型语言模型仅通过其上下文窗口中提供的少量任务示例或指令,就能学会执行新任务的现象。这令人惊讶,因为模型并未经过明确的“学习如何学习”的训练。
一个自然的疑问是:能否通过提示工程来提升上下文学习的效果?答案是肯定的,一个核心技巧是思维链提示。
其基本思想是:在提供给模型的每个任务示例中,除了输入和答案,还提供得出答案的推理过程。例如,在解决数学问题时,不仅给出答案,还写出解题步骤。模型能够学习这种推理模式,并将其应用到新的问题上,从而提升表现。
少样本学习需要为每个任务收集示例,在大规模应用时可能成本高昂。那么,能否在零样本设置下也提升性能呢?可以,灵感同样来自思维链提示。


研究发现,如果仅仅在提示中告诉模型“让我们一步步思考”,就能获得与展示具体推理过程类似的大部分收益。这是一个非常便捷的技巧,因为它不需要依赖特定任务的示例。




到目前为止,我们学习了一些有趣的技巧,如上下文学习和提示工程。现在,假设我们有一个预训练好的大型语言模型,想将其用于非常特定的领域(如医疗、法律)。直接提问能得到结果,但答案可能不可靠。

一种选择是使用上下文学习,向模型提供大量领域文本。但上下文学习受限于模型的上下文窗口长度,成本高昂。那么,我们还能做什么?



这时就需要微调。微调的基本思想是:在预训练模型权重的基础上进行小幅调整,使其适应新任务。这是通过在特定任务数据上进行额外训练来实现的。这样既能保留模型的语言理解能力,又能使其适应特定领域。
微调有不同的类型,让我们先看看全参数微调。
全参数微调会更新模型每一层、每一个参数。但这对大型模型来说计算上是否可行?让我们分析一下内存需求。
LoRA正是为了解决全参数微调的这些挑战而提出的。其核心思想是:冻结预训练模型的权重,并向Transformer架构的每一层注入可训练的低秩分解矩阵。
LoRA重新构想了微调的过程:与其直接更新巨大的权重矩阵 W,不如学习一个代表所需调整的增量矩阵 ΔW。在推理时,将 ΔW 加回原始权重即可。
LoRA基于两个关键概念:
- 预训练模型具有较低的内在维度,这意味着可以将巨大的权重矩阵投影到更小的子空间中,同时保留大部分信息。
- 可以将这个低内在维度的
ΔW矩阵分解为两个更小的矩阵的乘积。
例如,假设 ΔW 是一个 5x5 的矩阵(25个参数)。如果将其分解为两个秩为1的矩阵 B (5x1) 和 A (1x5) 的乘积,则只需要训练 5 + 5 = 10 个参数。当 ΔW 非常大时,这种节省尤为显著。
对于一个130亿参数的模型,如果使用秩为4的低秩矩阵进行LoRA微调,可能只需要微调约91.2万个参数,远少于全参数微调。
以下是LoRA工作的具体步骤:
- 初始化:保持预训练权重
W冻结。创建两个低秩矩阵A和B,其维度使得B * A的乘积与W的维度相同。通常,A用随机高斯分布初始化,B初始化为零。 - 前向传播:前向传播时,输入同时通过原始权重
W和低秩矩阵的乘积B*A(即ΔW)。- 公式:
输出 = (W + (B * A)) * 输入
- 公式:
- 反向传播:只对矩阵
A和B进行梯度更新,预训练权重W保持不变。 - 推理:训练完成后,可以将
ΔW (B*A)加回到W中,得到一个更新后的权重矩阵用于推理,这样不会引入额外的推理延迟。
在实现中,还有一个缩放因子 α/r,其中 r 是秩,α 是一个超参数。它控制着 ΔW 对最终输出的影响程度。
在原始LoRA论文中,通常只将低秩矩阵应用到注意力机制中的查询和值投影矩阵。但在本次作业中,你需要将其应用到查询、键、值以及输出投影矩阵。
如果我们尝试在分类任务(例如电影评论情感分析)上使用仅预训练的GPT-2模型,它很可能无法正确执行,因为这些模型并非训练来遵循人类指令的。因此,我们需要进行指令微调。
模型在整个训练过程中会看到许多这样的“指令+输入+输出”样本,从而学会执行该指令。你可以尝试不同的指令模板,对于摘要任务,指令可能是“请为以下段落生成摘要”。
现在,我们将理论应用于实践,讨论作业中需要实现的代码部分。
你将主要修改以下几个文件:
lora.py:实现LoRA线性层的核心逻辑(约30-35行代码)。model.py:将原有的线性层替换为你实现的LoRA线性层。dataloader.py:实现指令微调的数据构造逻辑。train.py:修改训练流程以支持LoRA。generate.py:编写自己的准确率计算函数。
LoRA的核心是创建一个自定义的线性层。你将继承PyTorch的nn.Linear类,并重写其方法。



以下是需要在lora.py中实现的关键部分:
__init__函数:在此初始化LoRA的A和B矩阵(仅当秩r > 0时),以及缩放因子。reset_parameters函数:设置A和B的初始化值(如A使用kaiming_uniform,B初始化为零)。forward函数:这是核心,实现前向传播公式W*x + (B*A)*x * (alpha/r)。train/eval模式:你需要管理一个布尔标志,以确定在训练和评估/推理时,是否将B*A合并到W中。通常训练时保持分离,评估/推理时合并。make_lora_trainable函数:此函数遍历模型参数,仅将LoRA相关的参数(A和B)设置为可训练,冻结其他所有参数。
一个重要的细节是:当秩r <= 0时,你的LoRA线性层应退化为标准的线性层。这方便你后续进行全参数微调(只需设置r=0即可)。

作业将使用“烂番茄”电影评论数据集进行情感分类(正面/负面)。你需要:
- 在
dataloader.py中设计并添加指令模板,将原始(文本,标签)数据构造成指令微调格式。 - 在
generate.py中编写准确率计算函数。由于小模型(如GPT-2)可能生成不完整或错误的文本(如“半正面”),你的评估函数需要能处理这些情况,例如只检查生成文本的前几个字符是否包含“正面”或“负面”关键词。 - 进行最低限度的超参数调优(如学习率、秩
r、缩放因子α)。
本节课中,我们一起学习了:
- 从少量数据学习:了解了少样本学习、零样本学习,以及大型语言模型令人惊讶的上下文学习能力。
- 提示工程技巧:掌握了思维链提示和零样本思维链这两种提升模型推理能力的有效方法。
- 模型适配:认识到直接使用预训练模型处理专业领域任务的局限性,引入了微调的概念。
- 高效微调技术:深入探讨了LoRA(低秩适应) 的原理与优势,它通过冻结原有权重、训练低秩增量矩阵,大幅降低了微调的内存和计算成本。
- 指令微调:学习了如何通过构造“指令-输入-输出”格式的数据,教导模型遵循特定指令。
- 实践实现:概述了在编程作业中实现LoRA和指令微调的关键步骤和代码文件。
通过结合这些技术,你可以有效地将通用的大型语言模型定制化,以胜任各种特定的下游任务。
在本节课中,我们将学习如何利用预训练的文本到图像生成模型进行基于文本的图像编辑。我们将重点介绍一种名为“Prompt-to-Prompt”的方法,并学习如何使用Diffusers API来简化实现过程。
上一节我们介绍了基于文本的图像编辑任务。本节中,我们来看看实现这一目标的核心方法——Prompt-to-Prompt。
Prompt-to-Prompt方法的关键思想是:图像的布局和几何结构依赖于交叉注意力图。通过编辑这些交叉注意力图,可以实现对图像的编辑目的。
另一个关键发现是:图像的构图在扩散过程的早期步骤中就已确定。这意味着图像的结构在早期步骤中决定,而具体的图像内容细节则由后期步骤生成。
以下是Prompt-to-Prompt方法支持的三种主要编辑任务:
- 替换:交换源提示词和目标提示词的注意力图。
- 细化:插入新短语对应的注意力图。
- 重新加权:放大特定词语对生成过程的影响。
上一节我们了解了Prompt-to-Prompt的核心思想,本节中我们来看看其具体实现步骤。
以下是Prompt-to-Prompt编辑过程的伪代码描述:
# 输入: 源提示词 P_source, 目标提示词 P_target, 随机种子 # 输出: 源图像 I_source, 编辑后图像 I_edited # 1. 从高斯分布中采样一个噪声向量,并复制它。 z_source, z_target = sample_noise(), sample_noise()      # 2. 对于每个扩散步骤 t (从 T 到 1): for t in reversed(range(1, T+1)): # a. 使用源提示词对 z_source 执行去噪,得到去噪后的 z_source 和注意力图 A_source。 z_source, A_source = denoise_step(z_source, P_source, t) # b. 使用目标提示词对 z_target 执行去噪,得到注意力图 A_target。 _, A_target = denoise_step(z_target, P_target, t) # c. 根据编辑类型(替换、细化、重新加权)和时间步 t,编辑注意力图。 A_edited = edit_attention_maps(A_source, A_target, edit_type, t) # d. 使用目标提示词和编辑后的注意力图 A_edited 对 z_target 执行去噪。 z_target = denoise_step_with_custom_attention(z_target, P_target, A_edited, t)    # 3. 循环结束后,解码潜在向量得到图像。 I_source = decode(z_source) I_edited = decode(z_target)
对于真实图像编辑,需要先进行图像反演,将真实图像编码到潜在空间,然后从步骤4开始执行上述过程。
上一节我们介绍了Prompt-to-Prompt的原理,本节中我们来看看如何利用现有工具快速实现。Diffusers API是一个功能强大且便捷的库,可以让我们通过几行代码就调用开源模型进行生成和编辑。
使用Diffusers API通常涉及创建一个管道(pipeline)。以下是一个加载Stable Diffusion模型的示例:
from diffusers import StableDiffusionPipeline # 指定模型ID,管道会自动下载和加载模型 model_id = "runwayml/stable-diffusion-v1-5" pipeline = StableDiffusionPipeline.from_pretrained(model_id)





一个管道通常包含多个组件,可以通过属性访问:
tokenizer: 将文本转换为模型可理解的标记(tokens)。text_encoder: 将标记转换为文本嵌入向量。unet: 执行去噪过程的核心网络。vae: 在图像像素空间和潜在空间之间进行编码和解码。scheduler: 控制扩散过程的调度器。



上一节我们介绍了便捷的API工具,本节中我们深入分析作业代码库的具体实现。你需要重点关注三个文件。

以下是代码文件及其功能的简要说明:
attention_replacement.py: 实现注意力替换的核心逻辑,包括AttentionReplace类。seq_aligner.py: 实现标记对齐功能,核心是get_replacement_mapper函数,用于处理源提示词和目标提示词之间标记长度不一致的情况。ptp_utils.py: 提供各种工具函数,例如注册注意力控制钩子、定义注意力注入时间表(alpha schedule)等。


在AttentionReplace类中,你需要实现两个关键方法:
replace_self_attention: 替换自注意力图。replace_cross_attention: 替换交叉注意力图。
get_replacement_mapper函数至关重要,因为一个单词可能被分解为多个标记(token)。该函数生成一个映射矩阵(alpha),用于将源提示词和目标提示词的注意力图进行对齐。例如,当用单标记词“cat”替换三标记词“dogggish”时,映射矩阵会将“cat”的一个注意力图平均分配到“dogggish”对应的三个注意力图位置上。

注意力注入时间由get_time_words_attention_alpha函数控制。它生成一个由0和1组成的掩码(alpha),其中1表示使用原始注意力,0表示使用替换后的注意力。通过调整开始注入替换注意力的时间步比例(例如0.8表示在80%的时间步后开始注入),可以控制编辑效果:注入越早,图像结构变化越大;注入越晚,则越保持原图结构。

上一节我们剖析了代码结构,本节中我们通过一些运行示例来直观感受编辑效果。
以下是使用代码库运行得到的一些示例结果:
- 可视化交叉注意力:可以观察到图像中不同区域与提示词中各个单词(如“squirrel”、“burger”)的对应关系。
- 无注意力控制(简单替换):直接将提示词中的“squirrel”替换为“lion”、“cat”、“deer”等,生成的新图像会尝试将新物体的特征融合到原图(松鼠)的构图中,效果可能不自然。
- 调整注意力注入时间:比较不同注入时间比例(如0.1, 0.5, 0.9)对“dog”替换“squirrel”效果的影响。注入时间晚(0.9)的结果更像原松鼠,仅带有少许狗的特征;注入时间适中(0.5)的结果更像狗;注入时间过早(0.1)则会严重破坏原图结构。
- 最终编辑效果:在合适的注入时间控制下,能够生成既保持原图大致结构,又成功替换目标对象的图像。



本节课中我们一起学习了基于文本的图像编辑技术。我们重点介绍了Prompt-to-Prompt方法,该方法通过操纵扩散模型中的交叉注意力图来实现对生成图像的控制。我们还了解了如何使用Diffusers API来简化模型调用流程,并解析了实现Prompt-to-Prompt编辑功能的核心代码模块,包括注意力替换、标记对齐和注入时机控制。通过调整注意力注入的时间,可以在改变图像内容与保持原始结构之间取得平衡。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/247715.html