生成式人工智能数学基础笔记-全-
生成式人工智能数学基础笔记(全)
001:课程大纲 📚

在本课程中,我们将学*多种深度生成模型(也称为生成式AI)的数学基础。课程的核心目标是从数学角度深入理解这些模型的工作原理,以便能够阅读原始论文并理解其背后的公式化思想。我们将涵盖从经典到最前沿的多种模型。
课程目标与特色 🎯
本课程的独特之处在于其数学化的处理方式。我们将从数学角度探讨所有著名的生成模型,这并不意味着我们会忽略实践方面,而是旨在为理解这些模型的工作原理打下坚实的理论基础。通过这种方式,学*者可以深入理解模型的运作机制,并能够轻松阅读相关原始论文及其改进工作。
涵盖的模型家族 🤖
以下是本课程计划涵盖的深度生成模型家族。
- 生成对抗网络:我们将从生成对抗网络开始。尽管GANs在某些任务上已非最先进技术,但它为理解生成建模的基本原理奠定了坚实基础。
- 变分自编码器:接下来是变分自编码器。与GANs一样,VAEs是经典模型,但其工作原理和理论基础为学*许多其他先进模型(如DDPMs)铺平了道路。
- 去噪扩散概率模型:然后,我们将学*去噪扩散概率模型。这是当前许多生成任务(如图像生成)的先进模型,也是DALL-E等商业工具背后的核心技术。
- 基于分数的模型:与扩散模型密切相关的是基于分数的模型,我们也将探讨其运作方式。
- 自回归模型:另一个重要的类别是自回归模型。当前著名的大型语言模型,如构成GPT、Gemini等商业平台基础的模型,大多属于此类。
- 状态空间模型:我们还将学*状态空间模型,例如S4和Mamba。这是新兴的生成模型家族,可作为自回归语言模型的替代方案。
- 大模型对齐技术:课程最后,我们将探讨用于对齐大型语言模型的一些技术,特别是基于强化学*的方法,如*端策略优化和直接偏好优化。
教学方法与框架 📝
本课程的教学将主要采用概率论框架。我们将使用概率框架来处理这些生成模型,因为概率论为理解和形式化“生成数据”这一过程提供了最自然和强大的语言。
课程内容将通过手写板书的形式进行讲授,以确保数学公式的推导能够清晰、严谨地呈现,方便大家逐步理解。
数据模态与配套教程 📊
本课程的讲解将采用数据模态无关的方式。这意味着我们学*的技巧是通用的,只需稍作修改即可适应不同类型的数据(如图像、文本、语音)。当特定模态需要特别处理时,我们会进行说明。
此外,本课程将配有实践教程。针对每一类生成模型,都会有相应的教程指导大家使用PyTorch等框架进行实现,并设置评估环节。课程还包含测验、作业和考试等环节,具体安排将在后续公布。


本节课总结:我们一起了解了《生成式AI的数学基础》课程的整体框架。我们明确了课程将从数学角度深入探讨GANs、VAEs、扩散模型、自回归模型、状态空间模型以及对齐技术等一系列生成模型,并采用概率论框架和手写板书的方式进行教学,旨在为大家打下坚实的理论基础。
生成式AI的数学基础:P02:W1L2-介绍与问题设定

在本节课中,我们将从数学角度定义生成式建模问题。我们将探讨生成式模型是什么,其核心目标,以及解决该问题的通用框架。
生成式模型在当今随处可见。以下是几个例子:
- 条件文本生成器:例如ChatGPT、Google Gemini和Claude。这些模型以文本(提示词)作为输入,生成相应的文本响应。输入可以是自然语言或计算机代码。
- 条件图像生成器:例如Stable Diffusion。这些模型根据对图像的描述(提示词)生成对应的图像。
- 语音生成器:这些模型将给定的文本转换为对应的语音波形文件。
从数学角度看,我们面临的问题是什么?
任何机器学*技术的起点都是数据。我们将数据定义为一组独立同分布的样本,这些样本从一个未知的分布 PX 中抽取。我们拥有 n 个数据点,记为 {x1, x2, ..., xn}。
这些数据点 xi 是某个随机变量的实例,该随机变量服从分布 PX。我们假设所有数据点都来自同一个未知的底层分布。
通常,这些数据点 x 位于某个 d 维实数空间 ℝd 中,其中 d 称为数据的维度。在处理图像、文本或语音等高维数据时,d 的值可能非常大(例如数万或数十万维)。
需要强调的是,独立同分布假设是指不同数据点之间(例如第一张图片与第一百张图片)是统计独立的,并且来自同一分布。这不意味着数据点内部各维度之间(例如一张图片的第一个像素与第一千个像素)是独立的。这个假设是为了数学上的便利。
因此,数据点 xi 可以被视为一个 d 维向量值随机变量的实例,该变量服从分布 PX。这个数学形式化观点在后续课程中非常重要。
有了这个基础,我们现在来定义生成式建模问题。
给定从未知分布 PX 中独立同分布抽取的 n 个样本,生成式建模的目标是:
- 估计底层的概率密度函数 PX。
- 学*如何从该分布中采样。
这与判别式模型形成对比。判别式模型通常估计条件分布 P(Y|X),但不一定学*从数据分布中采样。而在生成式模型中,估计分布并学会采样是核心目标。
那么,解决这个问题的通用原则是什么?观察现有的生成式模型,可以抽象出一个通用的解决框架。
以下是解决生成式建模问题的一般步骤:
-
假设一个参数化模型族:首先,为待估计的分布 PX 假设一个参数化形式,通常记为 Pθ。鉴于现代深度神经网络具有强大的表达能力(万能*似定理),Pθ 通常由深度神经网络表示。在本课程中,当我们提到“模型”时,通常指的就是这个参数化的 Pθ。
-
定义并计算散度度量:定义一个散度或距离度量 D,用于衡量模型分布 Pθ 与真实分布 PX 之间的差异。一个关键问题是,既然两者分布都未知,如何计算这个散度?我们将在后续课程中回答。
-
解决优化问题:在参数 θ 的空间中解决一个优化问题,目标是最小化上述散度度量。即寻找最优参数 θ*,使得 D(PX || Pθ) 最小化。
通过不断调整代表 Pθ 的神经网络参数,使得 Pθ 尽可能接* PX。
现在,我们来看一个如何实现这个框架的具体例子,并解释采样是如何完成的。
我们的目标是估计 PX 并从中采样。采样通过以下方式实现:
假设存在一个随机变量 Z,它位于 k 维实数空间 ℝk 中,并服从一个已知的任意分布(例如,k 维标准高斯分布 N(0, I))。
定义一个由神经网络表示的确定性函数 Gθ,它将 Z 映射到数据空间 ℝd。
X̂ = G<sub>θ</sub>(Z)
根据概率论,一个随机变量经过确定性函数变换后,其输出也是一个随机变量。X̂ 的分布不同于 Z 的分布,它依赖于函数 Gθ。我们将 X̂ 的密度函数记为 Pθ(X̂)。
现在,这个神经网络的输出可以解释为来自分布 Pθ(X̂) 的样本。
接下来,遵循我们的通用框架:
- 模型分布是 Pθ(X̂)(由 Gθ 隐式定义)。
- 定义真实分布 PX 与模型分布 Pθ 之间的散度度量 D(PX || Pθ)。
- 解决优化问题以找到最优参数 θ*:
θ* = argmin<sub>θ</sub> D(P<sub>X</sub> || P<sub>θ</sub>)
成功解决这个优化问题后,Pθ* 将非常接* PX。此时,我们不仅通过 Gθ* 隐式地估计了分布 PX,还获得了从中采样的方法。
采样过程如下:从已知分布(如高斯分布)中抽取一个样本 z,然后将其通过训练好的神经网络 Gθ*:
x̂ = G<sub>θ*</sub>(z)
由于 Pθ* ≈ PX,因此 x̂ 可以*似看作是从真实数据分布 PX 中采样的一个样本。
回顾一下,我们的目标是在给定数据样本的情况下估计未知分布并从中采样。我们从一个易于采样的已知分布开始,利用随机变量经确定性函数变换后产生新随机变量的原理,并用深度神经网络表示该函数。通过优化网络参数,使新随机变量的分布逼*真实数据分布。训练完成后,该网络不仅能估计分布,还能通过前向传播从已知分布生成样本,从而间接实现从目标分布中采样。
这是生成对抗网络、变分自编码器、扩散模型等许多算法的通用原理。
基于这个通用框架,我们需要提出并回答以下几个关键问题:
- 如何计算散度度量?:在不知道 PX 和 Pθ 具体形式的情况下,如何计算它们之间的散度?我们只有来自两者的样本。
- 如何选择散度度量?:不同的散度度量选择会导致具有不同性质和特点的生成式模型。
- 如何选择模型(Gθ 或 Pθ)?:即应该为 Pθ 选择什么样的参数化形式?不同的模型族(如GAN、VAE、扩散模型)做出了不同的选择。
- 如何解决优化问题?:给定模型和散度度量后,如何有效地最小化该散度?
在本课程中,我们将探讨回答这些问题不同方法,每一种选择都将引向一类特定的生成式模型。


本节课中,我们一起学*了生成式建模问题的数学定义和通用解决框架。我们首先通过实例了解了生成式模型,然后形式化地定义了问题:给定从未知分布中独立同分布采样的数据,目标是估计该分布并学会从中采样。接着,我们介绍了一个包含三个步骤的通用解决框架:假设参数化模型、定义分布间散度、优化模型参数以最小化散度。我们还通过一个使用神经网络变换随机变量的具体例子,说明了如何同时实现分布估计和采样。最后,我们提出了构建生成式模型时需要解决的几个核心问题。在后续课程中,我们将深入探讨不同的散度度量、模型选择和优化方法,从而引出各种流行的生成式模型。
003:f-散度


欢迎来到深度生成模型课程的本节内容。在本节中,我们将探讨一类基于“变分散度最小化”原理的生成模型。著名的生成对抗网络也是这个生成模型家族的成员。
在深入探讨变分散度最小化的细节之前,我们先快速回顾一下上一节的概念,以保持连续性。
概述
在上一节中,我们学*了生成建模的形式化定义。我们被给予的数据集是一组独立同分布地从底层未知分布 P_x 中抽取的样本。目标是根据这个数据集估计底层分布 P_x 并学会从中采样。估计 P_x 可以是隐式的或显式的,但生成模型的一个主要要求是学会从底层分布中采样。
我们还学*了构建生成模型的通用原则,它包含三个主要步骤:
- 假设一个参数化分布族
P_θ,通常用深度神经网络表示。 - 定义并估计真实分布
P_x与模型分布P_θ之间的散度度量。 - 通过优化
P_θ的参数来最小化上述散度度量,使P_θ尽可能接*P_x。
我们以“前向映射”方法为例,其思想是从一个已知如何采样的任意随机变量(如标准正态分布)开始,通过一个确定性函数 G_θ(通常是神经网络)将其映射到数据空间。输出随机变量的分布就是 P_θ。目标是优化 G_θ 的参数,使 P_θ 与 P_x 之间的散度最小。
上一节结束时,我们提出了几个关键问题:
- 如何在不知道
P_x和P_θ具体形式、仅有其样本的情况下计算散度? - 应该选择哪种散度度量?
- 如何选择函数
G_θ(即模型结构)? - 如何解决最小化散度的优化问题?
变分散度最小化方法与之前看到的前向映射方法并无本质不同。现在,让我们开始学*这种方法。
定义散度度量
构建生成模型的关键要素之一是定义分布间的散度度量。让我们定义一类称为 f-散度 的分布散度度量。
给定两个概率分布函数及其对应的密度函数 p_x 和 p_θ(假设为连续随机变量且密度函数定义良好),它们之间的 f-散度 定义如下:
D_f(p_x || p_θ) = ∫ p_θ(x) * f( p_x(x) / p_θ(x) ) dx
其中:
- 积分在随机变量
x的整个空间(通常是R^d)上进行。 f是一个从正实数到实数的凸函数,且是左半连续的,并满足f(1) = 0。u = p_x(x) / p_θ(x)是密度比,由于密度函数非负,该比值为正实数,因此f(u)定义良好。


f-散度的性质
f-散度具有以下对我们构建生成模型至关重要的性质:
- 非负性:对于任何满足条件的
f,D_f(p_x || p_θ) ≥ 0。 - 同一性:
D_f(p_x || p_θ) = 0当且仅当p_x = p_θ(即两个分布完全相同)。
我们的目标是构建生成模型,使模型分布 P_θ 与真实分布 P_x 之间的散度为零。利用f-散度的这两个性质,当散度最小化为零时,就意味着两个分布匹配,从而可以通过模型生成*似来自 P_x 的样本。
f-散度的例子
通过选择不同的凸函数 f,我们可以得到具有不同性质的散度度量。以下是几个著名的例子:
1. KL散度
如果选择 f(u) = u log u,对应的f-散度就是著名的KL散度(Kullback-Leibler divergence):
D_f(p_x || p_θ) = ∫ p_x(x) log( p_x(x) / p_θ(x) ) dx
KL散度是f-散度的一个特例。需要注意的是,KL散度是非对称的,即 D_KL(p_x || p_θ) ≠ D_KL(p_θ || p_x),前者称为前向KL,后者称为反向KL。
2. JS散度
如果选择 f(u) = -(u+1)log((u+1)/2) + u log u,对应的f-散度称为Jensen-Shannon散度。
JS散度的一个版本被用于著名的生成对抗网络中。
3. 总变差距离
如果选择 f(u) = 0.5 * |u - 1|,对应的f-散度称为总变差距离。
所有这些 f 函数都满足从 R+ 到 R 的凸性、左半连续性以及 f(1)=0 的条件。
总结
通过选择特定的 f 函数,我们可以获得具有特定性质的f-散度。不同的选择将导致不同的散度度量,进而影响生成模型的构建方式。
在本节课中,我们一起学*了:
- 回顾了生成模型的通用构建框架。
- 正式定义了f-散度这一类分布距离度量,并了解了其非负性和同一性的关键性质。
- 探讨了f-散度的几个具体实例,包括KL散度、JS散度和总变差距离,认识到通过选择不同的凸函数
f可以得到不同特性的散度。


接下来,我们将描述一个通用算法,该算法利用我们之前看到的前向映射方法,通过最小化任意f-散度来构建生成模型。之后,我们将通过固定一个特定的f-散度(JS散度)来查看其如何演变成著名的生成对抗网络算法。
004:变分散度最小化

概述
在本节课中,我们将学*如何构建生成式模型的核心优化算法。具体来说,我们将探讨如何在不直接知道真实数据分布 Px 和模型分布 Pθ 的情况下,通过样本数据来最小化它们之间的 f 散度。我们将学*如何利用凸共轭函数将难以计算的积分形式的散度,转化为可以通过样本均值来*似的期望形式,从而为后续的实际优化奠定基础。
生成式建模的设定
上一节我们介绍了生成式建模的基本框架。本节中,我们来看看这个框架下的具体优化目标。
我们有一个数据集 D,包含 n 个独立同分布的数据点 {x1, x2, ..., xn},它们来自一个未知的真实数据分布 Px。
我们的目标是学*一个能够从 Px 中采样的生成模型。我们使用一个由神经网络参数化的函数 Gθ(z) 来实现这一点。其中,z 来自一个我们已知如何采样的简单分布(例如标准正态分布)。Gθ(z) 的输出 x̂ 服从模型分布 Pθ。
为了使 Pθ 成为 Px 的有效采样器,我们需要确保 Pθ 与 Px 尽可能相同。这可以通过最小化 Pθ 和 Px 之间的分布散度来实现,例如 f 散度。
我们的优化目标是:
目标:找到参数 θ,以最小化 Px 和 Pθ 之间的 f 散度。
然而,我们面临一个核心挑战:我们既不知道 Px 的精确形式,也不知道 Pθ 的精确形式。我们只有来自这两个分布的样本:
- 来自
Px的样本:数据集D。 - 来自
Pθ的样本:将随机噪声z输入生成网络Gθ得到的输出。
此外,f 散度的定义涉及在高维数据空间上的积分,直接计算是难以处理的。
关键思路:用样本*似期望
为了解决上述挑战,我们需要一个关键的统计学思想:涉及密度函数的积分可以用从该分布中抽取的样本来*似。
具体来说,假设我们需要计算一个积分 I,它是某个函数 h(x) 关于分布 Px 的期望:
I = ∫ h(x) Px dx = E_{x~Px}[h(x)]
根据大数定律,如果我们有从 Px 中独立抽取的样本 {x1, x2, ..., xn},那么这个期望可以用样本均值来*似:
E_{x~Px}[h(x)] ≈ (1/n) Σ_{i=1}^{n} h(xi)
这意味着,我们只需要样本,而不需要知道分布的确切形式,就可以*似计算关于该分布的期望。
这个思路对我们的问题至关重要。如果我们可以将 f 散度表示为关于 Px 和 Pθ 的期望形式,那么我们就可以利用已有的样本来*似计算它,并进行优化。
将 f 散度表达为期望形式
现在,我们的核心任务是将 f 散度用关于 Px 和 Pθ 的期望表示出来。这需要用到凸分析中的凸共轭函数工具。
凸函数与凸共轭
首先,我们回顾一下凸函数的定义。
一个函数 f(u) 是凸函数,如果对于其定义域内的任意两点 u1, u2 和任意满足 α1 + α2 = 1 且 α1, α2 ≥ 0 的系数,都有:
α1 f(u1) + α2 f(u2) ≥ f(α1 u1 + α2 u2)
对于任意凸函数 f(u),都存在一个对应的凸共轭函数 f*(t),定义为:
f*(t) = sup_{u} [ t * u - f(u) ]
其中,sup 表示上确界(可以理解为最大值)。这个定义是逐点进行的:为了得到 f*(t) 在某个 t 处的值,我们需要求解一个关于 u 的优化问题。
凸共轭函数 f*(t) 本身也是凸函数。此外,凸共轭的共轭会得到原函数(在闭凸函数条件下),即:
f(u) = sup_{t} [ u * t - f*(t) ]
对 f 散度应用凸共轭
f 散度的定义是:
D_f(Px || Pθ) = ∫ Pθ(x) * f( Px(x) / Pθ(x) ) dx
令 u = Px(x) / Pθ(x)。利用上面提到的凸共轭性质 f(u) = sup_{t} [ u * t - f*(t) ],我们可以将 f 散度重写为:
D_f(Px || Pθ) = ∫ Pθ(x) * sup_{t} [ (Px(x)/Pθ(x)) * t - f*(t) ] dx
现在,我们遇到了一个顺序问题:积分内部有一个上确界运算。由于内部优化问题的目标函数依赖于 x,其解 t 也将是 x 的函数。因此,我们不能简单地将 sup 移到积分外面。
正确的做法是,将优化变量视为一个函数 T(x),它属于所有可能的函数空间 𝒯(从数据空间 x 映射到 f* 的定义域)。这样,我们就可以将上确界移到积分外部:
D_f(Px || Pθ) ≥ sup_{T ∈ 𝒯} ∫ [ Px(x) * T(x) - Pθ(x) * f*( T(x) ) ] dx
请注意,这里变成了不等式(≥)。这是因为我们搜索的函数空间 𝒯 可能并不包含那个对每个 x 都能给出内部优化问题精确解的理想函数 T*(x)。因此,我们实际上得到了 f 散度的一个下界。
得到期望形式
现在,观察上述不等式的右边:
sup_{T ∈ 𝒯} { ∫ Px(x) T(x) dx - ∫ Pθ(x) f*( T(x) ) dx }
这正是我们想要的期望形式:
sup_{T ∈ 𝒯} { E_{x~Px}[ T(x) ] - E_{x~Pθ}[ f*( T(x) ) ] }
我们成功地将 f 散度表示为了一个关于 Px 和 Pθ 的期望的优化形式(确切地说,是散度下界的优化形式)。
总结
本节课中,我们一起学*了构建生成式模型的关键一步——将分布散度最小化问题转化为可计算的优化问题。
- 我们首先明确了问题:在只有样本、不知道分布具体形式的情况下,如何最小化模型分布
Pθ与真实分布Px之间的f散度。 - 我们引入了核心工具:利用大数定律,我们可以用样本均值来*似关于分布的期望。
- 我们完成了关键的推导:通过引入凸共轭函数
f*,我们将难以直接计算的f散度,转化为了一个关于Px和Pθ的期望的优化问题(最大化下界):
D_f(Px || Pθ) ≥ sup_{T ∈ 𝒯} { E_{x~Px}[ T(x) ] - E_{x~Pθ}[ f*( T(x) ) ] }


这个公式就是著名的 f-GAN 目标函数的核心。在下一节中,我们将看到如何将这个理论框架实例化,通过神经网络来参数化函数 T(x)(通常称为判别器或批评器),并设计实际的训练算法来优化生成器 Gθ 的参数,从而学*到逼*真实数据分布的生成模型。
005:前向传播与反向传播

概述
在本教程中,我们将学*神经网络的两个核心过程:前向传播与反向传播。我们将通过一个简单的多层感知器(MLP)模型,详细解释数据如何从输入流向输出以进行预测(前向传播),以及如何根据预测误差来更新网络权重以最小化损失(反向传播)。理解这两个过程是掌握深度学*,特别是生成式AI模型训练的基础。


监督学*简介
在监督学*中,我们已知数据的正确答案。目标是学*从输入特征到输出标签的映射函数。训练数据通常表示为一系列数据对 (X_i, Y_i),其中 X_i 是输入特征,Y_i 是对应的目标标签。我们的目标是找到一个函数 f,使得预测值 Ŷ_i = f(X_i) 尽可能接*真实值 Y_i。衡量接*程度的指标称为损失 L,我们的核心任务就是最小化这个损失。
前向传播过程
上一节我们介绍了监督学*的目标,本节中我们来看看神经网络如何通过前向传播进行预测。
我们构建一个简单的MLP作为示例。假设输入 X_i 有两个特征 (x_i1, x_i2)。网络结构包括一个输入层(2个节点)、一个隐藏层(2个节点)和一个输出层(1个节点)。这是一个全连接网络,意味着每一层的每个节点都与下一层的所有节点相连。
以下是网络中各部分的命名规则:
- 激活值:用
a表示,下标为(层号,节点号)。例如,a_21表示第2层第1个节点的激活值。输入层的激活值就是输入特征本身,即a_11 = x_i1,a_12 = x_i2。 - 权重:用
w表示,下标为(目标层节点,源层节点)。例如,w_211表示从第1层第1个节点连接到第2层第1个节点的权重。 - 偏置:用
b表示,下标为(层号,节点号)。例如,b_21是第2层第1个节点的偏置。 - 加权和:在应用激活函数前,节点会计算其输入的加权和,用
z表示,下标规则与a相同。例如,z_21 = w_211 * a_11 + w_212 * a_12 + b_21。
前向传播的计算步骤如下:
- 计算隐藏层节点的加权和
z和激活值a。我们使用一个激活函数g(如Sigmoid或ReLU)来引入非线性。z_21 = w_211 * a_11 + w_212 * a_12 + b_21a_21 = g(z_21)- 同理计算
a_22。
- 计算输出层节点的加权和与激活值,得到最终预测
Ŷ_i。z_31 = w_311 * a_21 + w_312 * a_22 + b_31Ŷ_i = a_31 = g(z_31)
这个过程从输入开始,逐层计算,直到产生输出,因此被称为“前向”传播。
损失计算
在得到预测值后,我们需要衡量预测的误差。损失函数 L 的具体形式取决于任务类型。在本教程中,我们以回归任务常用的均方误差(MSE)损失为例。
对于单个数据点,其MSE损失计算公式为:
L = 1/2 * (Y_i - Ŷ_i)^2
公式中的 1/2 是为了后续求导时形式更简洁。如果有多个数据点,总损失通常是所有数据点损失的平均值。我们的目标就是通过调整网络参数(所有权重 w 和偏置 b),使这个损失 L 最小化。
反向传播与梯度下降
上一节我们定义了需要最小化的损失,本节中我们来看看如何通过反向传播和梯度下降来更新网络参数。
我们无法直接修改输入数据,只能调整网络的参数(统称为 θ)。最小化损失函数的标准方法是梯度下降法。其核心思想是:计算损失函数相对于每个参数的梯度(即导数),然后沿着梯度反方向(即下降最快的方向)更新参数。
对于任意一个权重参数 w,其更新公式为:
w_new = w_old - α * (∂L / ∂w)
其中 α 是一个正数,称为学*率,它控制着每次更新的步长。∂L / ∂w 就是损失 L 对权重 w 的梯度,它告诉我们:如果稍微增加 w,损失 L 会如何变化。
计算 ∂L / ∂w 需要用到链式法则,因为 w 的影响是通过网络层层传递到最终损失 L 的。这个过程从损失 L 开始,逆向逐层计算梯度,直到最初的权重 w,因此被称为“反向”传播或“反向”传递。
让我们以权重 w_211 为例,演示梯度计算过程。根据链式法则,梯度 ∂L / ∂w_211 可以分解为路径上各环节导数的乘积:
∂L / ∂w_211 = (∂L / ∂Ŷ_i) * (∂Ŷ_i / ∂z_31) * (∂z_31 / ∂a_21) * (∂a_21 / ∂z_21) * (∂z_21 / ∂w_211)
以下是每个部分的计算:
∂L / ∂Ŷ_i = -(Y_i - Ŷ_i)(对MSE损失求导)∂Ŷ_i / ∂z_31 = g‘(z_31),例如,若使用Sigmoid激活函数,则g‘(z) = g(z) * (1 - g(z)) = Ŷ_i * (1 - Ŷ_i)∂z_31 / ∂a_21 = w_311(z_31对a_21的偏导就是其系数)∂a_21 / ∂z_21 = g‘(z_21),即a_21 * (1 - a_21)(假设隐藏层也用Sigmoid)∂z_21 / ∂w_211 = a_11(即x_i1)
将所有这些项相乘,就得到了损失 L 对于权重 w_211 的完整梯度。然后,我们就可以使用梯度下降公式来更新 w_211。对网络中的每一个参数重复此过程,就完成了一轮反向传播更新。
整体训练流程
现在,我们将前向传播和反向传播结合起来,看看神经网络的整体训练流程。
标准的训练流程是迭代进行的,包含以下步骤:
- 数据准备:将整个训练集划分为若干个小批次(Batch)。
- 迭代训练:对于每一个训练轮次(Epoch),遍历所有小批次数据。
- 前向传播:将当前批次的输入数据
X_batch送入网络,计算得到预测输出Ŷ_batch。 - 损失计算:根据预测
Ŷ_batch和真实标签Y_batch计算批次损失L_batch。 - 反向传播:计算损失
L_batch对网络所有参数的梯度。 - 参数更新:使用梯度下降法更新所有参数(权重和偏置)。
- 前向传播:将当前批次的输入数据
- 评估:重复步骤2直至达到预设的训练轮次。最后使用未见过的测试数据评估模型性能。
这个过程不断循环,通过大量数据反复调整参数,使得网络的预测能力逐渐提升。
总结
在本教程中,我们一起学*了神经网络的核心训练机制。
- 我们首先回顾了监督学*的目标:学*映射函数以最小化预测损失。
- 接着,我们通过一个简单的MLP网络,详细阐述了前向传播的过程,即数据如何从输入层经隐藏层流向输出层并产生预测。
- 然后,我们定义了均方误差损失来衡量预测误差。
- 为了最小化损失,我们引入了反向传播和梯度下降算法。我们以链式法则为基础,逐步推导了计算损失对某个权重梯度的完整过程。
- 最后,我们概述了结合前向传播和反向传播的整体训练流程,包括数据分批、迭代更新等关键步骤。


理解前向传播和反向传播是深入任何深度学*领域的基石。在接下来的教程中,我们将学*如何使用PyTorch等现代框架高效地实现这些过程。
006:PyTorch入门与张量基础 🧮

在本教程中,我们将学*PyTorch的基础知识。PyTorch是一个广泛用于实现深度学*模型的库。整个课程中,你将学*大量理论,了解如何从数学上构建模型。此外,你还将学*如何在简单数据集上实现这类模型。我们将使用Google Colab进行本次练*,它为我们提供了GPU访问权限。本课程的编码练*结构设计为无需更多计算资源,我们将实现这些模型的简化版本,并讨论在有计算资源时如何扩展它们。
在之前的教程中,我们探讨了前向传播和反向传播的概念,以及梯度如何计算和整体训练流程。在本教程中,我们将重点学*PyTorch的基础知识。
我们将使用PyTorch。如果有人感兴趣,也可以寻找使用TensorFlow实现类似模型的资源,但本课程的核心讨论将围绕PyTorch展开。
关于本教程的先决条件,我假设你了解多层感知机的基本结构和张量的基本概念。MLP的基本结构我们在之前的教程中讨论过。如果你对卷积神经网络的结构不太熟悉或想复*,可以参考斯坦福大学CS231n课程资源。该资源提供了非常详细的笔记和示例,解释了卷积层的工作原理、参数共享的概念等。你可以查看该资源中的CNN笔记部分。我假设这些先决知识已知,并已提供参考材料。
现在,回到PyTorch。我们将使用公开的标准PyTorch教程,并解释需要更多阐述的部分,在需要时运行代码。在本教程的最后,我们将编写一个简单的结构,读取一些数据集并对其进行操作。这是整体思路。
访问教程与Colab环境 🚀
现在,我们开始学*。你可以在浏览器中搜索“PyTorch tutorial”访问官方教程,或直接访问链接 https://pytorch.org/tutorials/。这是基础教程,涵盖了大部分核心概念。我们将从基础开始学*,扩展部分你可以自行查看。
我们首先从“快速入门”开始。教程提供了多种运行方式:可以在Colab上运行,可以下载笔记本在本地机器、Visual Studio或Jupyter Notebook上使用,也可以查看GitHub。在本教程中,我将在Colab上运行,以便你熟悉Colab环境和代码执行。
接下来,我们进入“张量”部分。我将在Colab中打开它。
Colab笔记本会有这些对话框。它带有.ipynb扩展名,代表交互式Python笔记本。当你运行任何指令或代码时,它需要在某个设备上物理运行。这个设备不是你的本地资源,而是Google服务器分配给你的一块资源,你的代码将在该资源块中运行。这消除了安装包、维护必要包的所有麻烦,我们无需担心这些,可以直接开始学*必要的知识。
我假设你了解Python。如果你对Python不太熟悉,我建议你先学*Python的基础课程。
什么是张量?🧱
张量是PyTorch的构建模块。你可以将它们视为一种专门的数据结构,类似于我们熟知的数组或矩阵。如果你使用过NumPy,它有一种叫做ndarray(n维数组)的结构,张量与此类似。但关键区别在于,张量可以在GPU和其他硬件加速器上运行。GPU极大地推动了深度学*的发展。因此,你可以在GPU上运行这些张量或对张量的操作,这是使用PyTorch等库的额外优势。

在第一个笔记本中,我们来看看如何创建张量,以及如何对张量进行一些基本操作。
创建张量 🛠️
首先,我们需要导入必要的库。使用PyTorch时,第一个需要的库是torch,因此需要导入torch。对于数据,你可以创建一个列表并将其转换为张量,或者创建一个NumPy数组再转换为张量。对于列表,你不需要导入其他库;如果要使用NumPy数组,则需要导入numpy。
让我们运行代码。第一次运行时可能需要一些时间,因为它需要与服务器建立连接。
这些是文本块和代码块。文本块提供更多信息,代码块是你编写和运行代码片段的地方。
我假设你对这些基本概念已经相当熟悉。
现在,你创建了数据。你创建了一个二维数组,这通常被称为矩阵。你试图将这个列表或二维数组转换为张量。如何操作呢?
你只需使用torch.tensor()方法,将data作为参数传递。这将返回一个张量类型的数据,其中的值保持不变。值的类型不会改变,但数据的类型会改变。data是一个列表,你可以使用type(data)来检查。现在,你正在改变这个列表的类型。让我们运行一下。
这就是如果你有一个列表,如何将其转换为张量。
另一方面,如果你的数据是NumPy数组,在第一行中,我将我的数据(一个列表)转换为NumPy数组。现在,如何将NumPy数组转换为张量呢?
你使用torch.from_numpy()方法,传入转换后的NumPy数组。这样,你就得到了创建的张量。
这意味着,你可以使用列表或NumPy数组来创建张量。
大多数时候,你可能需要创建一个随机张量,或者具有特定值的张量,例如所有值都为1的张量,或特定大小的随机张量。
我们有两种创建方式:一种是直接提供数据,库内部会计算大小并创建相应大小的张量;另一种是直接指定大小。让我们看看这两种方式。
你有这个x_data。x_data是一个张量,是一个2x2的张量。我想要一个所有值都为1的张量,并且也是2x2的张量。如果x_data具有某些特定属性,这些属性也会被传递到新创建的x_ones中。此外,如果你想要随机值,你可以传递这个数据并指定数据类型应为float。但请注意,x_data的数据类型是整数,这个方法会覆盖x_data的类型,然后你将得到一个所有值都是浮点数的2x2张量。我们可以打印出来看看。
所有值都是1,你可以看到它的大小是2x2的张量。同样,如果我再次运行,你会看到生成了不同的值,因为你在进行随机采样。
这就是如果你传递与结构相似的数据,如何获得张量。
相反,如果你知道张量的形状,你可以这样做。在这个标准教程中,他们指定张量的形状应为2x3。如果你使用torch.rand()方法并传入形状,它将创建一个大小为2x3的随机张量。
如果你想要所有值都为1的张量,可以使用torch.ones(),只需将形状传递给这个方法,就会创建一个所有值都为1的张量。torch.zeros()则创建一个所有值都为0的张量。
我们可以看到这里创建了一个2x3的张量。第一个是随机张量,第二个是所有值都为1的张量,第三个是所有值都为0的张量。
这意味着,你可以根据已有的数据结构和形状来创建张量,或者直接指定形状来创建。
张量的属性 📊
接下来,我们看看张量的一些属性。一个是张量的形状,另一个是每个值的数据类型,以及它在什么设备上运行。在你的系统中,必须有CPU,而张量可以在CPU或GPU上。根据这一点,我可以判断它是在CPU上还是在GPU上。让我们运行看看。
张量的形状是3x4,正如我们在这个块中看到的。然后你可以看到形状和数据类型的属性。dtype表示数据类型,这里是float32。然后,这些值所在的设备是CPU,我还没有使用GPU。
这些是目前张量的一些属性。
现在,让我们尝试看看如何将张量移动到加速器(如果可用的话)。我们稍后再回到这一点。
切片与索引 🔪
这是切片和索引操作。在我们的课程中,我们不会过多需要切片和索引。如果你了解NumPy的切片和索引,它与张量的操作完全相同。你可以查看相关部分。
这里,第一个例子是创建一个所有值都为1的张量,形状是4x4,然后进行一些切片和索引操作。我不会深入讲解这个。
连接张量 🔗
如何连接张量?你有一个张量,也就是一个矩阵。如何连接矩阵?问题在于,是需要水平并排连接还是垂直堆叠连接?每当我们说连接张量时,都需要回答这个问题。让我们看看如何操作。torch.cat()是用于连接的方法,你需要提供一个张量列表。
这里,所有张量都是相同的,然后你有一个叫做dim=1的参数。这个dim=1是什么意思?然后你打印结果。让我们看看发生了什么。每个张量都是一个4x4的矩阵,这是已知的。看看这里发生了什么。
你看到... 也许我没有运行它。我仍然有一个... 哦,现在这个张量有1, 2, 3... 它有四行,有多少列?1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12。是的,它重复了三次。所以,4x4旁边是4x4,再旁边是4x4。现在你有12列和4行。这意味着dim=1表示你正在按列连接。如果你想按行连接,只需将维度改为0。
现在你可以看到它已经被连接了。
因此,每当你想要连接时,出现的问题是:你想按列连接还是按行连接?这些可以通过改变连接的维度来处理。
矩阵乘法 ✖️

下一个需要的操作是矩阵乘法的概念,这是大多数神经网络中的核心思想。实际上,对于所有实际目的,神经网络中的整个前向传播都可以看作是矩阵乘法。那么,如何执行矩阵乘法呢?我假设你知道矩阵乘法的基本规则。让我再简述一下规则。
如果你有一个矩阵A,大小为m x n,另一个矩阵B,大小为n x p,那么你可以将它们相乘,得到一个矩阵C,大小为m x p。第一个矩阵的列数必须与第二个矩阵的行数匹配,然后你才能执行矩阵乘法。这是一个标准规则。

现在,你有一个大小为4x4的张量。你想用它乘以什么?你有一个4x4的矩阵想相乘,那么选项是什么?你必须有一个4 x ?的矩阵才能相乘。
所以,这个4x4的矩阵与tensor.T相乘。这里的.T是转置操作。@符号用于矩阵乘法。
tensor是一个4x4的矩阵,它的转置也是一个4x4的矩阵。你将一个4x4的矩阵与另一个4x4的矩阵相乘,结果将是一个4x4的矩阵。
你可以使用@,这是方法一。然后你有另一种方法。你可以从第一个张量调用一个方法,即.matmul(),然后传入第二个矩阵作为参数。
这与将第一个矩阵与第二个矩阵相乘相同,这是执行矩阵乘法的另一种方式。第三种方式是直接使用库函数torch.matmul(),你需要传入两个矩阵A和B,此外,你还需要传入结果矩阵。
这就是为什么你创建一个结果矩阵y3,它是一个具有随机值的张量。然后你传入它,将tensor与其转置相乘,输出将存储在y3中。这些是你可以进行矩阵乘法的三种方式:使用@符号,使用第一个矩阵的.matmul()方法,或者使用torch.matmul()并传入两个矩阵以及存储结果的张量。
逐元素乘法 ⚡

另一个我们需要的重要概念是逐元素乘法。让我们看看它是什么。
逐元素乘法是卷积运算中涉及的核心操作之一。要执行逐元素乘法,A和B必须具有相同的大小。
让我们取矩阵A,我放一些随机值,取一个简单的2x2矩阵:[[1, 2], [-2, 3]]。第二个矩阵B,也是同样的大小2x2,我取[[4, 2], [1, -3]]。如何执行逐元素乘法?C = A * B,结果将是1*4, 2*2, -2*1, 3*(-3)。你取每个元素并执行乘法,取这个元素乘以这个,取这个... 然后你就能得到结果。这就是逐元素乘法的概念。

那么,如何做逐元素乘法?你有一个大小为4x4的张量,我们已经创建了,使用tensor * tensor,你使用这个星号*。或者你可以使用tensor.mul(),注意不是matmul,matmul是矩阵乘法。我假设你现在已经清楚矩阵乘法和逐元素乘法的区别了。
逐元素乘积,第一个张量调用.mul()方法,然后传入第二个张量作为参数。或者,如我们之前所见,你可以使用torch.mul(),传入两个张量,甚至传入结果张量。
这些是你对张量执行的一些基本操作。你可以舒适地查看。所以,有了这些,我假设你了解了张量的基础知识,知道了张量的属性,如何创建随机张量、全1张量,以及如何执行逐元素乘积和矩阵乘法等基本操作。同样,你可以进行加法和其他操作,这些是琐碎的任务,因此教程没有详细阐述。
总结 📝



本节课中,我们一起学*了PyTorch中张量的核心概念与基础操作。我们了解了张量是PyTorch的基本数据结构,类似于NumPy数组但支持GPU加速。我们学*了如何从列表或NumPy数组创建张量,以及如何创建具有特定形状和值(如全1、全0或随机值)的张量。我们还探讨了张量的关键属性,如形状、数据类型和设备位置。最后,我们掌握了连接张量、执行矩阵乘法与逐元素乘法等基本运算的方法。这些知识是构建和理解后续更复杂深度学*模型的重要基石。
007:PyTorch数据集与数据加载器 🗂️



在本节课中,我们将学*如何在PyTorch中处理数据。所有输入模型的数据都需要是张量(Tensor)格式。因此,我们需要了解数据存储在哪里、如何获取,以及如何将其组织成批次(batches)以供模型训练。本节将重点介绍数据集(Dataset)和数据加载器(DataLoader)这两个核心概念。



内置数据集
上一节我们介绍了张量的基本操作。本节中我们来看看如何获取和处理现成的数据。PyTorch的torchvision库提供了一些常用的内置数据集,方便我们快速开始实验。

以下是PyTorch中可用的一些内置数据集示例:
- 图像分类:CIFAR-10, CIFAR-100, Fashion-MNIST, ImageNet等。
- 图像检测/分割:COCO, VOC等。
- 图像对:Stereo Matching数据集。
- 视频分类/预测:Kinetics, UCF101等。
这些数据集已经过预处理,我们可以直接使用它们进行模型训练和测试。在本教程中,我们将以Fashion-MNIST数据集为例。这是一个包含10个类别的图像分类数据集,例如T恤、裤子、鞋子等,类别标签通常用数字0到9表示。
要使用这些内置数据集,我们需要导入必要的库。
import torch
from torchvision import datasets
from torchvision.transforms import ToTensor
import matplotlib.pyplot as plt
torch: PyTorch核心库。datasets: 包含内置数据集的模块。ToTensor: 一个转换(Transform),用于将PIL图像或NumPy数组转换为PyTorch张量。matplotlib.pyplot: 用于可视化图像。
数据集通常分为训练集和测试集。训练集用于学*输入与输出之间的关系,测试集用于评估学*到的关系。我们可以如下加载Fashion-MNIST数据:
# 加载训练数据
training_data = datasets.FashionMNIST(
root="data", # 数据存储的根目录
train=True, # 指定加载训练集
download=True, # 如果数据不在本地,则下载
transform=ToTensor() # 将图像转换为张量
)

# 加载测试数据
test_data = datasets.FashionMNIST(
root="data",
train=False, # 指定加载测试集
download=True,
transform=ToTensor()
)
代码解释:
root="data": 指定数据存储在当前工作目录下的data文件夹中。download=True: 如果指定路径下没有数据,则自动从网络下载。transform=ToTensor(): 对加载的每张图像应用ToTensor转换,将其从PIL格式变为张量格式。
自定义数据集

然而,在实际项目中,我们经常需要处理自定义的、私密的或特定领域的数据,这些数据并不在PyTorch的内置仓库中。这时,我们需要创建自己的自定义数据集(Custom Dataset)。
一个典型的自定义数据集结构包含两部分:
- 图像目录(Image Directory):一个文件夹,里面存放着所有的图像文件。
- 标注文件(Annotation File):通常是一个CSV文件,其中每一行记录了图像相对于图像目录的路径(或图像文件名)以及其对应的标签。

例如,一个标注文件annotations.csv的内容可能如下:
image_path,label
train/cat.1.jpg,0
train/dog.1.jpg,1
test/cat.2.jpg,0
...
为了创建自定义数据集,我们需要继承PyTorch的torch.utils.data.Dataset类,并实现两个必要的方法:__len__和__getitem__。


以下是创建自定义数据集类的代码框架:
import os
import pandas as pd
from torchvision.io import read_image
from torch.utils.data import Dataset
class CustomImageDataset(Dataset):
def __init__(self, annotations_file, img_dir, transform=None, target_transform=None):
# 读取包含图像路径和标签的CSV文件
self.img_labels = pd.read_csv(annotations_file)
# 设置图像目录的路径
self.img_dir = img_dir
# 设置特征(图像)的转换函数
self.transform = transform
# 设置目标(标签)的转换函数
self.target_transform = target_transform
def __len__(self):
# 返回数据集中的样本总数
return len(self.img_labels)
def __getitem__(self, idx):
# 1. 根据索引idx拼接图像的完整路径
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
# 2. 读取图像
image = read_image(img_path)
# 3. 获取对应标签
label = self.img_labels.iloc[idx, 1]
# 4. 应用转换(如果提供了的话)
if self.transform:
image = self.transform(image)
if self.target_transform:
label = self.target_transform(label)
# 5. 返回图像-标签对
return image, label
代码解释:
__init__: 构造函数,初始化时读取标注文件、设置路径和转换函数。__len__: 返回数据集的样本数量,即标注文件的行数。__getitem__: 根据给定的索引idx,返回对应的单个样本(图像张量和标签)。它通过os.path.join拼接完整图像路径,使用read_image读取图像,并从标注文件中获取标签。

数据加载器(DataLoader)


有了数据集(无论是内置的还是自定义的)之后,我们通常不会一次将全部数据送入模型。相反,我们会将数据分成小批次(batches),这种分批处理的方式对于利用GPU并行计算、管理内存以及引入随机性(防止模型过拟合)至关重要。数据加载器(DataLoader)就是负责这项工作的工具。


数据加载器围绕数据集创建一个可迭代对象,能够自动完成分批、打乱顺序和多进程数据加载等任务。

以下是创建数据加载器的示例:


from torch.utils.data import DataLoader
# 为训练数据创建数据加载器
train_dataloader = DataLoader(training_data, batch_size=64, shuffle=True)
# 为测试数据创建数据加载器
test_dataloader = DataLoader(test_data, batch_size=64, shuffle=False)
参数解释:
batch_size=64: 每个批次包含64个样本。shuffle=True: 在每个训练周期(epoch)开始时,打乱训练数据的顺序。这有助于模型学*更通用的模式,防止记忆数据顺序。shuffle=False: 对于测试数据,通常不需要打乱顺序,因为我们只需要对模型进行一次评估。
创建好数据加载器后,我们可以像迭代器一样使用它:
# 获取一个批次的训练数据
train_features, train_labels = next(iter(train_dataloader))
print(f"特征批次形状: {train_features.size()}") # 输出: torch.Size([64, 1, 28, 28])
print(f"标签批次形状: {train_labels.size()}") # 输出: torch.Size([64])
此时,train_features是一个形状为[64, 1, 28, 28]的张量,代表64张灰度(通道数为1)的28x28图像。train_labels是一个形状为[64]的张量,包含这64张图像对应的标签。

总结




本节课中我们一起学*了PyTorch数据处理的核心组件。我们首先了解了如何使用内置数据集,然后掌握了如何为自定义数据创建数据集类,最后学*了如何使用数据加载器将数据高效地组织成批次供模型使用。数据准备是机器学*流程中的关键第一步,Dataset和DataLoader的配合使用,使得数据加载、预处理和批处理变得标准化且高效。
008:PyTorch模型构建教程 🧠





在本节课中,我们将学*如何使用PyTorch构建一个完整的神经网络模型。我们将从数据准备开始,逐步完成网络定义、训练、评估和保存的整个流程。

数据准备与转换

上一节我们介绍了数据加载,本节中我们来看看如何对数据进行预处理和转换。

首先,我们需要将数据转换为张量,这是模型处理的必要格式。除了基础的ToTensor转换,在实际应用中还会使用其他转换,例如随机裁剪和调整大小。这些转换将在后续的代码示例中具体介绍。
以下是数据转换的核心步骤:
ToTensor:将图像数据转换为PyTorch张量,这是强制性的步骤。- 其他转换:如
RandomCrop、Resize等,用于数据增强和标准化。


构建神经网络



数据准备就绪后,下一步是构建神经网络模型。

对于图像分类任务,我们有几种网络架构可以选择。最基础的是多层感知机,也可以使用卷积神经网络。*年来,视觉变换器也被广泛应用。本教程将重点介绍多层感知机和卷积神经网络。




构建多层感知机


现在,让我们以MLP为例,看看如何构建一个神经网络。构建MLP需要做出几个关键选择:网络的层数、每层的神经元数量以及使用的激活函数。

以下是构建一个MLP模型的关键代码结构:
import torch
import torch.nn as nn
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.linear_relu_stack = nn.Sequential(
nn.Linear(28*28, 512), # 输入层到第一个隐藏层
nn.ReLU(),
nn.Linear(512, 512), # 隐藏层到隐藏层
nn.ReLU(),
nn.Linear(512, 10), # 隐藏层到输出层
)
def forward(self, x):
x = self.flatten(x)
logits = self.linear_relu_stack(x)
return logits



在这个例子中,我们处理的是FashionMNIST数据集,图像大小为28x28。首先,我们使用nn.Flatten()将二维图像展平为一个784维的向量。然后,我们通过一个由nn.Sequential定义的线性层和ReLU激活函数堆叠而成的管道进行处理。

前向传播过程可以理解为一系列的矩阵乘法。例如,第一层的计算可以表示为:
z1 = W1^T * x + b1
其中,x是784维的输入向量,W1是形状为(512, 784)的权重矩阵,b1是512维的偏置向量。结果z1通过ReLU激活函数:a1 = ReLU(z1) = max(0, z1)。这个过程在后续层中重复。

最后一层输出10个“逻辑值”,对应10个分类。这些逻辑值随后会通过Softmax函数转换为概率分布。
模型训练与优化



网络构建完成后,下一步是训练和优化模型参数。



我们将使用梯度下降法来优化模型。其核心更新公式为:
W = W - α * ∇L/∂W
其中,α是学*率,∇L/∂W是损失函数关于权重W的梯度。PyTorch的自动微分机制会为我们计算这些梯度。

以下是训练循环的核心步骤:
- 前向传播:输入数据通过模型,得到预测值。
- 计算损失:使用损失函数(如交叉熵损失)计算预测值与真实标签之间的误差。
- 反向传播:调用
loss.backward()计算模型中所有参数的梯度。 - 参数更新:调用
optimizer.step()根据梯度更新权重。 - 梯度清零:调用
optimizer.zero_grad()将梯度缓冲区归零,防止梯度累积。


在训练过程中,我们通常会在多个“轮次”上重复这些步骤,并在每个轮次后评估模型在验证集上的性能,以监控训练过程。
模型评估与保存

模型训练完成后,我们需要评估其性能并保存下来以备后用。

在评估模式下,我们使用测试数据集来计算模型的损失和准确率。关键步骤是计算预测正确的样本数:我们取模型输出概率最高的类别作为预测结果,并与真实标签进行比较。

以下是计算批量准确率的示例:
pred = output.argmax(1) # 获取预测类别
correct = (pred == y).type(torch.float).sum().item() # 统计正确数

训练出满意的模型后,可以将其保存到磁盘。
# 保存模型
torch.save(model.state_dict(), ‘model.pth’)
# 加载模型
model.load_state_dict(torch.load(‘model.pth’))
这样,我们就不需要每次使用时都重新训练模型。


扩展到卷积神经网络


MLP的原理同样适用于更复杂的网络,如卷积神经网络。

构建CNN时,需要为每个卷积层指定关键参数:输入通道数、输出通道数、卷积核大小、步长和填充。CNN通常由多个“卷积块”(作为特征提取器)和顶部的“全连接层”(作为分类器)组成。在PyTorch中,只需将网络定义部分从MLP替换为CNN结构,而训练、评估和保存的流程完全保持不变。
总结

本节课中我们一起学*了使用PyTorch构建和训练神经网络的完整流程。
我们回顾了以下核心内容:
- 数据准备:使用
DataLoader和Transforms加载与预处理数据。 - 模型构建:通过继承
nn.Module类来定义网络结构,重点分析了多层感知机的前向传播过程。 - 模型训练:理解了训练循环的五个基本步骤:前向传播、损失计算、反向传播、参数更新和梯度清零。
- 模型评估与保存:学会了在测试集上评估模型性能,并将训练好的模型参数保存到文件中。
- 架构扩展:了解了将MLP替换为CNN的基本思路。




掌握了这些基础,我们就为后续实现更复杂的生成式模型打下了坚实的实践基础。
009:通过变分散度最小化进行生成建模


在本节课中,我们将继续学*生成式模型。我们将从上一节的内容出发,探讨如何利用变分散度最小化技术来构建生成模型或生成采样器。本节将重点介绍如何利用上一节构建的散度度量下界,来实际构建我们所需的采样器。
首先,让我们快速回顾一下上一节的核心内容。
问题设定与目标
我们拥有一个数据集,其中的样本点 x 是从一个未知分布 Px 中独立同分布地采样得到的。所有数据点 x(i) 都位于一个 D 维的实数空间 X 中。
我们的目标是:构建一个能够从该未知分布 Px 中进行采样的采样器。
核心方法
我们采用的方法是:
- 从一个已知的简单分布(例如标准正态分布 Z ~ N(0, I))中采样。
- 将这些样本 z 输入一个可学*的确定性函数 Gθ(通常是一个神经网络)。
- 我们希望神经网络输出的样本 x̂ = Gθ(z) 所遵循的分布 Pθ 能够与目标分布 Px 一致。
因此,我们的问题转化为:寻找神经网络参数 θ,使得 Pθ 与 Px 之间的某种分布散度度量最小化。如果这个散度度量在且仅在两个分布相同时达到最小,那么我们就达到了目标。
面临的挑战与解决方案
我们面临的主要挑战是:我们无法直接计算 Px 和 Pθ 的精确形式,只能获得来自它们的样本(数据来自 Px,生成器输出来自 Pθ)。
上一节中,我们引入了 F-散度 作为度量工具。对于一个给定的凸函数 f,两个分布之间的 F-散度定义为:
Df(Px || Pθ) = ∫ pθ(x) f( px(x) / pθ(x) ) dx
F-散度具有非负性,且当且仅当两个分布相等时为零。
为了在只有样本的情况下优化这个散度,我们利用了大数定律:涉及概率密度的积分可以用来自该分布的样本期望来*似。关键思路是,如果 F-散度可以表示为关于 Px 和 Pθ 的期望形式,那么我们就可以通过样本进行估计和优化。
通过引入凸函数 f 的凸共轭 f*,我们对 F-散度进行了推导,最终得到了一个下界(Lower Bound):
Df(Px || Pθ) ≥ supT∈𝒯 { Ex~Px[T(x)] - Ex~Pθ[f*(T(x))] }
这里,T(x) 是某个函数空间 𝒯 中的函数,sup 表示上确界。
从理论到实践:构建优化目标
由于我们无法直接最小化 F-散度 Df,我们转而优化它的这个下界。这等价于解决以下优化问题:
minθ maxT∈𝒯 { Ex~Px[T(x)] - Ex~Pθ[f*(T(x))] }
这是一个极小极大化问题:
- 内层最大化:对于固定的生成器参数 θ,寻找函数 T 使下界尽可能紧(即值更大)。
- 外层最小化:调整生成器参数 θ,使这个紧的下界值尽可能小。
为了在实践中有解,我们需要对函数空间 𝒯 进行参数化。由于神经网络是强大的通用函数逼*器,我们自然地用另一个神经网络 Tw(x) 来表示函数 T,其中 w 是该网络的参数。
于是,我们的最终优化目标变为:
minθ maxw { Ex~Px[Tw(x)] - Ex~Pθ[f*(Tw(x))] }
现在,我们有两个神经网络:
- 生成器网络 Gθ:负责将先验噪声 z 映射为数据空间中的样本 x̂,目标是让 Pθ 接* Px。
- 评判器/鉴别器网络 Tw:负责估计上述目标函数中的项,为生成器提供优化所需的梯度。
对抗性优化与鞍点问题
上述的 minθ maxw 形式定义了一个鞍点优化问题。我们寻找一组参数 (θ, w),使得:
- 在 θ* 附*,目标函数关于 θ 是局部最小的。
- 在 w* 附*,目标函数关于 w 是局部最大的。
这种优化问题也被称为对抗性优化:
- 生成器 Gθ 试图最小化目标函数。
- 鉴别器 Tw 试图最大化同一个目标函数。
两者相互竞争、相互促进,因此得名“对抗”。
具体实例:生成对抗网络
生成对抗网络 是变分散度最小化框架的一个著名特例。在该框架下:
- 生成器网络 即我们定义的 Gθ。
- 鉴别器网络 即我们定义的 Tw,在GAN的原始形式中,它通常被解释为一个试图区分真实数据与生成数据的二分类器。
通过选择特定的凸函数 f,我们可以推导出GAN的原始损失函数。这将在后续课程中详细展开。


总结
本节课中,我们一起学*了如何将生成建模问题转化为变分散度最小化问题。核心步骤包括:
- 使用F-散度衡量分布差异。
- 利用凸共轭导出散度的可计算下界。
- 将优化问题表述为关于生成器和鉴别器网络的极小极大化(鞍点)问题。
- 指出生成对抗网络是该框架的一个具体实现。


通过这一理论框架,我们为理解和使用GAN等现代生成模型奠定了坚实的数学基础。在接下来的课程中,我们将看到这一框架的具体应用与实例。
010:生成对抗网络介绍

概述
在本节课中,我们将学*生成对抗网络(GANs)的核心原理。我们将看到GANs如何作为更广泛的“变分散度最小化”算法框架的一个特例,并详细推导其目标函数和网络架构。
从变分散度最小化到GANs
上一节我们介绍了变分散度最小化的通用框架。本节中,我们来看看如何将其具体化,以得到著名的生成对抗网络。
首先,我们需要考虑如何表示目标函数中的 T函数。根据定义,T函数将数据空间映射到F函数的定义域。这意味着,根据所选F散度的不同,我们需要调整T网络,使其输出范围与F的定义域相匹配。
在实践中,这是通过以下方式实现的:我们将T网络(记作 T_w(x) )表示为一个复合函数。它由两部分组成:一个与F散度无关的神经网络 V_w(x),以及一个特定于所选F散度的激活函数 σ_f。
具体公式如下:
T_w(x) = σ_f( V_w(x) )
其中,V_w(x) 是一个神经网络,其最后一层是线性层,将输入x映射为一个实数。σ_f 是一个激活函数,它将实数映射到特定F散度对应的F*函数的定义域。
因此,整个T网络可以看作先通过V_w将x映射到实数空间R,再通过σ_f映射到F*的定义域。
将T_w(x)的表达式代入之前得到的下界损失函数,我们得到:
L(θ, w) = E_{x~P_data}[ T_w(x) ] - E_{x~P_θ}[ f*( T_w(x) ) ]
= E_{x~P_data}[ σ_f( V_w(x) ) ] - E_{x~P_θ}[ f*( σ_f( V_w(x) ) ) ]
这个公式构成了我们优化生成器参数θ和判别器(即T函数)参数w的基础。
GANs:一个具体实例
现在,我们来看变分散度最小化的一个著名特例——生成对抗网络(GANs)。
首先,我们需要选择GAN所使用的F散度。GAN对应的F函数如下:
f(u) = u * log(u) - (u + 1) * log(u + 1) + log(4)
注意,这个函数与Jensen-Shannon散度相似,但并不完全相同。
根据凸共轭的定义,我们可以求出其对应的F*函数(即f的凸共轭):
f*(t) = -log(1 - e^t)
此F函数的定义域是所有负实数,即 **domain(f) = R⁻**。
因此,我们需要设计的激活函数σ_f,必须将V_w(x)输出的实数映射到负实数域R⁻。对于GAN,这个激活函数是:
σ_f(v) = -log(1 + exp(-v))
其中v是V_w(x)的输出标量。可以验证,这个函数的输出始终为负。
推导GAN的目标函数
接下来,我们将上述F函数、F*函数和激活函数σ_f代入通用的损失函数,并进行代数整理。
以下是关键的推导步骤(建议读者自行验证):
- 将
T_w(x) = σ_f( V_w(x) ) = -log(1 + exp(-V_w(x)))代入损失函数。 - 将
f*(t) = -log(1 - e^t)中的t用σ_f( V_w(x) )替换。 - 为了与原始GAN论文的记号一致,我们引入一个新变量 D_w(x),定义为:
熟悉神经网络的读者会认出,这就是 Sigmoid函数。D_w(x) = 1 / (1 + exp(-V_w(x)))D_w(x)的值域在(0, 1)之间。
经过代入和化简(具体代数过程略),我们得到经典的GAN目标函数:
J_GAN(θ, w) = E_{x~P_data}[ log( D_w(x) ) ] + E_{z~P_z}[ log( 1 - D_w( G_θ(z) ) ) ]
其中:
G_θ(z)是生成器,它将从先验分布(如标准正态分布)中采样的噪声z映射为生成样本x̃。D_w(x)是判别器,它试图区分真实数据x(来自P_data)和生成数据x̃(来自P_θ)。
这个目标函数的意义是:判别器 D 试图最大化 J_GAN(即正确区分真假),而生成器 G 试图最小化 J_GAN(即欺骗判别器),两者形成对抗。
GAN的网络架构
根据上述推导,我们可以描绘出GAN的标准架构。
以下是GAN核心组件的结构图:

架构说明:
-
生成器 (Generator):
- 输入:从简单先验分布(如
N(0, 1))中采样的噪声向量z。 - 输出:生成的数据样本
x̃ = G_θ(z)。生成器G_θ的目标是使x̃的分布P_θ尽可能接*真实数据分布P_data。
- 输入:从简单先验分布(如
-
判别器 (Discriminator):
- 输入:一个数据样本,可以是真实数据
x或生成数据x̃。 - 在原始推导中,判别器内部可视为两部分:
- V_w网络:一个将输入映射到实数
v的神经网络。 - Sigmoid激活层:将实数
v转换为(0, 1)之间的概率值,即D_w(x) = sigmoid(v)。
- V_w网络:一个将输入映射到实数
- 在实践中,这两部分通常被合并为一个端到端的神经网络
D_w。 - 输出:一个标量概率值,表示输入样本为真实数据的(判别器认为的)概率。
- 输入:一个数据样本,可以是真实数据
重要提示:判别器的输出 D_w(x) 是一个介于0和1之间的概率值,它并不直接等于我们最初优化的 T_w(x) 函数。T_w(x) 的值域是负实数。然而,D_w(x) 通过Sigmoid函数与 V_w(x) 相关联,而 T_w(x) 又是 V_w(x) 的特定函数(σ_f(v))。因此,优化 D_w(x) 等价于间接地优化我们所需的 T_w(x) 函数。
总结
本节课中,我们一起学*了生成对抗网络(GANs)的数学基础。
- 我们首先回顾了变分散度最小化的通用框架,其核心是最大化关于T函数的下界。
- 接着,我们通过选择特定的F散度(
f(u) = u log u - (u+1) log(u+1) + log4),并将T函数构造为神经网络V_w(x)与特定激活函数σ_f的复合,将该框架具体化。 - 通过代入和代数推导,我们得到了GAN的标准目标函数,它表现为生成器
G和判别器D之间的极大极小博弈。 - 最后,我们明确了GAN的网络架构,包括生成器(将噪声映射为数据)和判别器(区分真实与生成数据),并解释了其与理论推导中T函数的关系。


理解GAN作为变分散度最小化特例的这一视角,有助于我们更深刻地把握其本质,并为理解后续各种GAN变种(如WGAN、f-GAN等)奠定坚实的基础。
011:生成对抗网络(GAN)的实现

在本节课中,我们将学*生成对抗网络(GAN)的具体实现步骤。我们将详细拆解如何交替训练生成器和判别器,并解释每一步背后的数学原理和代码逻辑。
概述
生成对抗网络(GAN)的核心思想是通过一个生成器网络和一个判别器网络的对抗训练,来学*真实数据的分布。生成器试图生成足以“欺骗”判别器的假数据,而判别器则试图准确区分真实数据和生成数据。这种对抗过程最终使得生成器能够产生非常逼真的数据。
上一节我们介绍了GAN的理论基础,本节中我们来看看如何将理论转化为实际的训练算法。
训练算法详解
GAN的训练过程是交替优化两个神经网络:生成器(G)和判别器(D)。我们假设读者熟悉神经网络的基本训练方法,特别是基于梯度下降的反向传播算法。
1. 网络结构与目标函数
我们有两个神经网络:
- 生成器网络:
G_theta(z),参数为theta。输入为从标准正态分布N(0, I)采样的噪声z,输出为生成的数据x_cap。 - 判别器网络:
D_w(x),参数为w。输入为数据x(可以是真实的或生成的),输出一个介于0和1之间的标量,表示x来自真实分布的概率。这本质上是一个二分类器。
我们的目标是最小化生成数据分布 P_theta 与真实数据分布 P_x 之间的 f-散度。通过推导,这等价于优化以下对抗性目标函数:
公式:
min_theta max_w [ E_{x~P_x}[log D_w(x)] + E_{z~N(0,I)}[log(1 - D_w(G_theta(z)))] ]
- 判别器
D的目标是最大化这个函数(尽可能区分真假)。 - 生成器
G的目标是最小化这个函数(使生成的数据尽可能骗过D)。
在实际训练中,我们使用样本平均来*似期望值。假设我们有一个包含 n 个样本的真实数据集 {x_1, ..., x_n}。
2. 训练判别器(固定生成器)
当训练判别器时,我们保持生成器的参数 theta 不变。
以下是训练判别器的步骤:
- 采样真实数据批次:从真实数据集中随机抽取一个批次(batch)的样本,记作
{x_1, ..., x_B1}。 - 采样生成数据批次:
- 从标准正态分布
N(0, I)中采样一个批次的噪声向量{z_1, ..., z_B2}。 - 将这些噪声向量输入固定的生成器
G_theta,得到生成数据{G_theta(z_1), ..., G_theta(z_B2)}。
- 从标准正态分布
- 前向传播计算损失:
- 将真实数据批次输入判别器
D_w,计算log D_w(x_i)的和。 - 将生成数据批次输入判别器
D_w,计算log(1 - D_w(G_theta(z_j)))的和。 - 判别器的损失函数
J_D为这两项之和的负平均(因为我们要最大化原函数,等价于最小化其负数):
公式:J_D = -1/B1 * sum_i log D_w(x_i) - 1/B2 * sum_j log(1 - D_w(G_theta(z_j)))
- 将真实数据批次输入判别器
- 反向传播与参数更新:
- 计算损失
J_D关于判别器参数w的梯度。 - 使用梯度上升(因为目标是最大化)更新
w:
公式:w_new = w_old + alpha_D * gradient_w(J_D)
其中alpha_D是判别器的学*率。
- 计算损失
代码逻辑描述(伪代码):
# 固定生成器,不计算其梯度
with torch.no_grad():
z = sample_noise(B2)
fake_data = generator(z)
# 判别器前向传播
real_score = discriminator(real_data)
fake_score = discriminator(fake_data)
# 计算判别器损失
loss_D = - (torch.log(real_score).mean() + torch.log(1 - fake_score).mean())
# 判别器反向传播与更新
loss_D.backward()
optimizer_D.step() # 执行梯度上升
optimizer_D.zero_grad()
3. 训练生成器(固定判别器)
当训练生成器时,我们保持判别器的参数 w 不变。
以下是训练生成器的步骤:
- 采样噪声批次:从标准正态分布
N(0, I)中采样一个批次的噪声向量{z_1, ..., z_B2}。 - 前向传播计算损失:
- 将噪声批次输入生成器
G_theta,得到生成数据{G_theta(z_1), ..., G_theta(z_B2)}。 - 将生成数据输入固定的判别器
D_w,得到判别分数D_w(G_theta(z_j))。 - 生成器的损失函数
J_G来自目标函数的第二项(第一项与theta无关):
公式:J_G = 1/B2 * sum_j log(1 - D_w(G_theta(z_j)))
生成器希望这个值越小越好,意味着D_w(G_theta(z))越大,即判别器认为生成数据是真实的。
- 将噪声批次输入生成器
- 反向传播与参数更新:
- 计算损失
J_G关于生成器参数theta的梯度。注意:梯度需要从判别器的输出反向传播通过整个判别器网络,再传到生成器网络,但在此过程中,判别器的参数w被锁定,不更新。 - 使用梯度下降更新
theta:
公式:theta_new = theta_old - alpha_G * gradient_theta(J_G)
其中alpha_G是生成器的学*率。
- 计算损失
代码逻辑描述(伪代码):
# 采样噪声
z = sample_noise(B2)
# 生成数据
fake_data = generator(z)
# 将生成数据输入固定判别器
fake_score = discriminator(fake_data)
# 计算生成器损失
loss_G = torch.log(1 - fake_score).mean() # 或使用 -torch.log(fake_score).mean() 等改进版本
# 生成器反向传播与更新
loss_G.backward()
optimizer_G.step() # 执行梯度下降
optimizer_G.zero_grad()
4. 交替训练与停止准则
在实际操作中,我们交替进行步骤2和步骤3:
- 执行
k_D步判别器训练(每次使用新的数据批次)。 - 执行
k_G步生成器训练(每次使用新的噪声批次)。 - 重复以上过程。
k_D 和 k_G 通常是1,但有时为了训练稳定,可能会让判别器或生成器多训练几步(例如 k_D=5, k_G=1)。
GAN没有像传统监督学*那样明确的损失函数收敛点作为停止准则。通常的停止方法是:
- 视觉检查:定期查看生成器输出的样本质量。
- 定量指标:使用如Inception Score (IS) 或 Fréchet Inception Distance (FID) 等指标来评估生成数据的多样性和真实性,当指标达到满意水平时停止训练。
总结
本节课中我们一起学*了生成对抗网络(GAN)的具体实现方法。我们首先回顾了GAN的对抗性目标函数,然后详细拆解了交替训练生成器和判别器的每一步:
- 训练判别器:需要同时使用真实数据和生成器产生的数据,目标是最大化其区分真假的能力。
- 训练生成器:只需要使用噪声数据,通过固定判别器并反向传播梯度,目标是让生成的数据骗过判别器。


这个过程本质上是求解一个极小极大优化问题。通过这种对抗性训练,生成器最终能学*到真实数据的高质量表示。在接下来的课程中,我们将探讨GAN的另一种解释(分类器视角),以及如何对训练好的GAN进行推理,并将其应用于图像合成等条件生成任务。我们也会看到,通过选择不同的 f-散度,可以衍生出具有不同特性的GAN变体。
012:GAN的实现 🎼

在本教程中,我们将学*如何实现一个基础的生成对抗网络。我们将从理论公式出发,逐步讲解如何用代码构建和训练一个Vanilla GAN,并使用MNIST数据集来生成手写数字图像。
概述
上一节我们介绍了GAN的数学原理。本节中,我们将把这些理论转化为实际的代码。我们将构建一个包含生成器和判别器的简单多层感知机网络,并使用PyTorch框架来实现训练过程。
网络结构
以下是GAN中两个核心网络的结构定义。
生成器网络
生成器网络的目标是将一个随机噪声向量 z 映射成一张逼真的图像。在我们的实现中,z 的维度是100,输出图像的维度是28x28(即784)。
代码描述:
class Generator(nn.Module):
def __init__(self, noise_dim=100, image_dim=784):
super(Generator, self).__init__()
self.model = nn.Sequential(
nn.Linear(noise_dim, 256),
nn.ReLU(),
nn.Linear(256, 512),
nn.ReLU(),
nn.Linear(512, 1024),
nn.ReLU(),
nn.Linear(1024, image_dim),
nn.Tanh() # 将输出归一化到[-1, 1]
)
判别器网络
判别器网络是一个二分类器,用于判断输入图像是真实的(来自数据集)还是虚假的(来自生成器)。输入是展平后的图像(784维),输出是一个概率值。
代码描述:
class Discriminator(nn.Module):
def __init__(self, image_dim=784):
super(Discriminator, self).__init__()
self.model = nn.Sequential(
nn.Linear(image_dim, 512),
nn.LeakyReLU(0.2),
nn.Linear(512, 256),
nn.LeakyReLU(0.2),
nn.Linear(256, 1),
nn.Sigmoid() # 输出概率
)
损失函数与优化目标
GAN的训练是一个极小极大博弈。我们需要分别优化生成器和判别器。
目标函数
从理论推导中,我们得到整体的目标函数为:
公式:
min_G max_D V(D, G) = E_{x~p_data(x)}[log D(x)] + E_{z~p_z(z)}[log(1 - D(G(z)))]
在实践中,我们使用小批量数据来*似期望值。
判别器更新
对于判别器,我们希望最大化 V(D, G)。这等价于一个二分类问题:将真实图像分类为1,生成图像分类为0。因此,我们使用二元交叉熵损失。

以下是判别器训练的步骤:
- 从真实数据分布
p_data中采样一个批次{x_1, x_2, ..., x_B1}。 - 从先验噪声分布
p_z(如标准正态分布)中采样一个批次{z_1, z_2, ..., z_B2},并通过生成器得到假图像{G(z_1), G(z_2), ..., G(z_B2)}。 - 计算判别器对真实图像的输出
D(x)和假图像的输出D(G(z))。 - 判别器的总损失为:
Loss_D = -[log(D(x)) + log(1 - D(G(z)))]。 - 在更新判别器参数
W时,保持生成器参数θ不变,并使用梯度上升。
生成器更新
对于生成器,我们希望最小化 V(D, G)。由于第一项与生成器无关,我们只需最小化第二项:E_{z~p_z(z)}[log(1 - D(G(z)))]。
以下是生成器训练的步骤:
- 从先验噪声分布
p_z中采样一个批次{z_1, z_2, ..., z_B2}。 - 通过生成器得到假图像
G(z),再通过判别器得到输出D(G(z))。 - 生成器的损失为:
Loss_G = -log(D(G(z)))。这里我们选择最大化log(D(G(z)))来“欺骗”判别器,其效果与最小化原式等价。 - 在更新生成器参数
θ时,保持判别器参数W不变,并使用梯度下降。
训练流程
现在,我们来看如何在代码中组织上述训练步骤。核心是交替训练判别器和生成器。
训练循环设置
我们设置了三种训练模式:
- 1:1模式:每轮迭代更新一次判别器,更新一次生成器。
- 5 Gen : 1 Disc模式:每轮迭代更新五次生成器,更新一次判别器。
- 5 Disc : 1 Gen模式:每轮迭代更新五次判别器,更新一次生成器。
判别器训练步骤
以下是判别器单次训练的关键代码逻辑:
# 1. 训练判别器
# 清零判别器梯度
d_optimizer.zero_grad()
# 计算真实图像的损失
real_output = discriminator(real_images)
real_loss = criterion(real_output, real_labels) # 希望输出为1
# 生成假图像并计算损失
z = torch.randn(batch_size, noise_dim).to(device)
fake_images = generator(z)
# 使用.detach()阻止梯度传播到生成器
fake_output = discriminator(fake_images.detach())
fake_loss = criterion(fake_output, fake_labels) # 希望输出为0
# 总损失并反向传播
d_loss = real_loss + fake_loss
d_loss.backward()
d_optimizer.step() # 只更新判别器参数
生成器训练步骤
以下是生成器单次训练的关键代码逻辑:
# 2. 训练生成器
# 清零生成器梯度
g_optimizer.zero_grad()


# 再次用噪声生成图像(这次需要梯度)
z = torch.randn(batch_size, noise_dim).to(device)
fake_images = generator(z)
fake_output = discriminator(fake_images)


# 生成器希望假图像被判别为“真”
g_loss = criterion(fake_output, real_labels) # 使用真实标签“1”作为目标


# 反向传播并更新
g_loss.backward()
g_optimizer.step() # 只更新生成器参数
代码实现与结果

我们使用MNIST数据集进行训练。在训练过程中,每隔一定轮次,我们使用固定的噪声向量来生成图像,以便直观观察生成质量的演变。

经过50轮训练后,即使在简单的MLP结构下,生成器也开始输出具有一定结构的、类似手写数字的图像。例如,在“1:1”训练模式下,从第10轮到第50轮,生成的图像从模糊的斑点逐渐变得可辨识。
总结
本节课中我们一起学*了Vanilla GAN的实现方法。我们从理论公式出发,定义了生成器和判别器的网络结构,详细解释了对抗训练的目标函数和交替更新策略,并逐步解析了训练循环中的代码逻辑。通过本教程,你掌握了用PyTorch构建和训练一个基础GAN的核心技能。
为了进一步探索,你可以尝试以下练*:
- 将网络结构从MLP改为卷积神经网络,观察生成图像质量的提升。
- 更换更复杂的数据集,观察GAN在不同数据上的表现。


希望本教程对你有所帮助,我们下个教程再见!
013:GAN作为分类器引导的生成采样器

在本节课中,我们将学*生成对抗网络的另一种解读视角:将其视为一个由分类器引导的生成模型。我们将探讨这种视角下的工作原理、潜在问题,以及它与之前学*的变分散度最小化框架的联系。
概述
在上一节中,我们学*了如何使用变分散度最小化算法来训练生成采样器,并介绍了其特例——生成对抗网络。本节我们将继续深入,目标是了解对朴素GAN的一些改进,并讨论GAN在领域自适应和分布转换等任务中的著名应用。
分类器引导视角下的GAN
首先,让我们快速回顾一下GAN。在作为变分散度算法特例的GAN中,我们有两个神经网络:生成器网络和判别器网络。其核心思想是,从一个任意的随机变量(如高斯分布)出发,通过最小化一个分布散度度量,使用神经网络将其变换为我们感兴趣的随机变量。我们使用f-散度作为度量,但直接最小化f-散度不可行,因此我们构造了f-散度的一个下界。构造这个下界本身涉及一个优化问题,由判别器网络解决。因此,我们交替执行两个优化:首先通过最大化问题找到f-散度的下界(训练判别器),然后最小化这个下界(训练生成器)。
现在,我们开始从另一个角度来解读GAN。
作为分类器引导的生成采样器
我们可以将GAN解释为一个由分类器引导的生成模型。让我们看看这是如何工作的。
我们有一个数据集,其中的样本 x 从真实数据分布 P_x 中抽取。同时,我们有一个生成器 G_θ,它从一个简单分布(如标准正态分布)中采样 z,并输出样本 x̂ = G_θ(z),这些样本服从分布 P_θ。
我们的目标始终是让 P_θ 尽可能接* P_x。
现在,假设存在一个二元分类器 D_w。我们这样定义它:
- 当输入样本
x来自真实分布P_x时,D_w(x)输出 1。 - 当输入样本
x̂来自生成分布P_θ时,D_w(x̂)输出 0。
核心问题:我们能否利用这个分类器来使 P_θ 和 P_x 彼此接*?
一个直观的策略是:不断调整生成器参数 θ,直到分类器 D_w 无法区分来自 P_x 和 P_θ 的样本。当 P_θ 与 P_x 完全相同时,样本变得不可区分,分类器自然会失败。
然而,这里存在一个关键问题:分类器的失败,并不必然意味着 P_θ 匹配了 P_x。
让我们通过一个反例来说明。
一个反例:固定分类器的问题
假设我们的数据是二维的。真实数据 P_x 分布在一个簇中(用十字表示),初始的生成数据 P_θ1 分布在另一个簇中(用圆点表示)。我们有一个分类器 D_w1,其决策边界(一条直线)成功地将两个簇分开。
现在,我们固定分类器 D_w1,并调整 θ 以使分类器失败。优化器可以很容易地将 P_θ1 的整个簇简单地移动到决策边界的另一侧(变成 P_θ2)。此时,对于固定的分类器 D_w1 来说,P_θ2 和 P_x 的样本都落在了分类器的同一侧,因此分类器失败了。
但是,P_θ2 的分布并没有与 P_x 重叠,它们仍然是完全分离的两个簇。我们只是“欺骗”了当前这个固定的分类器,并没有实现分布匹配的目标。
解决方案:交替优化分类器与生成器
为了解决上述问题,我们不能固定分类器。思路是:在调整生成器的同时,也同步地调整(重新训练)分类器。
回到之前的示意图:
- 初始状态:
P_θ1和P_x被分类器D_w1分开。 - 生成器步骤:调整
θ,将生成数据移动到P_θ2,使得D_w1分类失败。 - 判别器步骤:固定生成器,训练一个新的分类器
D_w2。D_w2会学*新的决策边界,再次将P_θ2和P_x分开。 - 生成器步骤:再次调整
θ,将生成数据移动到P_θ3,使得D_w2分类失败。 - 如此循环往复...
这个交替过程的目标是,最终找到一个生成分布 P_θ*,使得不存在任何一个分类器能够可靠地区分 P_θ* 和 P_x。这通常意味着两个分布已经非常接*或相同。
注意:这个过程可能陷入一种“模式崩溃”的困境。例如,生成器可能只在 P_θ1 和 P_θ2 两个模式之间来回振荡,而分类器也随之在这两个决策边界之间切换,永远无法让 P_θ 逼*真正的 P_x。这是GAN训练不稳定的一个已知原因。
目标函数的形式化
现在,让我们从数学上形式化这个分类器引导的视角。
假设 D_w(x) 表示样本 x 来自真实分布 P_x 的似然(概率)。那么 1 - D_w(x) 就表示 x 不是来自 P_x 的似然。
对于分类器 D_w,我们希望它满足两个目标:
- 对于来自
P_x的真实样本x,最大化其对数似然log(D_w(x))。 - 对于来自
P_θ的生成样本x̂,最大化其“非真实”的对数似然log(1 - D_w(x̂))。
因此,分类器的综合训练目标是最大化以下期望值:
公式:分类器目标
max_w [ E_{x~P_x}[log(D_w(x))] + E_{x̂~P_θ}[log(1 - D_w(x̂))] ]
对于生成器 G_θ,我们的目标是让分类器失败,即最小化上述分类器的目标。因此,生成器的目标是:
公式:生成器目标
min_θ [ E_{x~P_x}[log(D_w(x))] + E_{x̂~P_θ}[log(1 - D_w(x̂))] ]
将两者结合起来,我们就得到了GAN的极小极大博弈目标:
公式:GAN的极小极大目标
min_θ max_w [ E_{x~P_x}[log(D_w(x))] + E_{x̂~P_θ}[log(1 - D_w(x̂))] ]
在这个框架下:
- 判别器
D_w扮演分类器的角色,试图最大化目标,以更好地区分真假样本。 - 生成器
G_θ试图最小化同一目标,以“欺骗”判别器,使其无法区分。
这种一方试图最大化而另一方试图最小化同一个目标的对抗性博弈,正是“对抗网络”名称的由来。
与变分散度最小化的联系
值得注意的是,从数学角度看,这个分类器引导的视角与之前介绍的变分散度最小化框架是等价的。上面推导出的目标函数 J(θ, w) 正是特定f-散度(詹森-香农散度)的一个下界。
- 构造下界(最大化) 对应于 训练判别器/分类器。
- 最小化散度(最小化) 对应于 训练生成器。
分类器视角提供了一种更直观的理解:我们通过一个不断进化的“检验员”(分类器)来引导“生产者”(生成器)改进其输出,直到产品(生成样本)足以乱真。
总结
本节课中,我们一起学*了生成对抗网络的另一种重要解读——作为分类器引导的生成采样器。我们探讨了以下核心内容:
- 基本思想:通过一个二元分类器区分真实数据与生成数据,并通过对抗性训练使生成数据分布逼*真实数据分布。
- 关键问题:固定分类器会导致生成器通过“欺骗”特定分类器而非真正匹配分布来轻易达到目标。
- 解决方案:采用交替优化策略,同时训练生成器和判别器(分类器),形成一种极小极大博弈。
- 目标函数:我们推导出了GAN的标准对抗性损失函数,并解释了判别器和生成器各自的目标。
- 内在联系:这一视角与变分散度最小化框架在数学上相通,分类器的训练对应于寻找散度下界,生成器的训练对应于最小化该散度。


这种对抗性训练的范式虽然强大,但也因其训练过程的不稳定性(如模式崩溃)而闻名。在接下来的课程中,我们将探讨针对这些问题的各种GAN改进模型。
生成式AI的数学基础:P14:深度卷积GAN与条件GAN

在本节课中,我们将学*生成对抗网络(GAN)的两种重要变体:深度卷积GAN(DCGAN)和条件GAN(CGAN)。我们将了解它们如何改进基础GAN架构,以及如何实现条件生成。
概述
到目前为止,我们讨论的生成模型理论是数据模态无关的,可以应用于图像、语音等任何类型的数据。本节将介绍两种专门针对图像数据或需要条件控制生成的GAN架构改进。
深度卷积GAN(DCGAN) 🖼️
上一节我们介绍了基础GAN的通用架构。本节中我们来看看专门为图像生成设计的深度卷积GAN。
在典型的GAN中,生成器网络通常是一个多层感知机(MLP)。它接收一个低维噪声向量z(例如16维),并通过全连接层逐步增加维度,最终输出D维数据(例如展平的图像),之后可能需要重塑为图像格式。
在深度卷积GAN中,生成器网络使用转置卷积层(也称为上卷积层)来替代全连接层。这种方法允许网络直接从低维噪声z开始,通过转置卷积操作逐步增加空间维度和通道数,最终直接生成具有正确高度、宽度和通道数(例如 行 × 列 × 3)的图像,而无需额外的重塑步骤。
以下是DCGAN生成器架构的核心思想:
# 概念性架构示意
输入: z (例如,形状为 [batch_size, 100] 的噪声向量)
层1: 全连接层 -> 重塑为初始特征图
层2: 转置卷积层 (上采样,增加空间尺寸)
层3: 转置卷积层 (继续上采样)
...
输出层: 转置卷积层 -> 图像 (形状为 [batch_size, 高度, 宽度, 3])
DCGAN主要应用于图像数据,它使生成器能够更自然地学*图像的空间结构。
条件GAN(CGAN) 🎯
之前我们学*的是无条件生成,生成器无法控制输出样本的类别。但在实际应用中,我们通常希望根据特定条件(如类别标签或文本描述)生成样本。这就是条件GAN要解决的问题。
条件GAN的目标是学*条件分布 P(x|y),而不是边际分布 P(x)。这里,y 是条件变量,例如图像的类别标签或文本嵌入向量。
为了实现条件生成,我们需要对GAN的架构进行一个关键修改:
以下是构建条件GAN所需的步骤:
- 数据准备:需要成对的数据
(x, y),其中x是数据样本(如图像),y是对应的条件(如类别标签)。 - 修改生成器:生成器
G_θ的输入除了噪声向量z,还需要加入条件变量y。即x̂ = G_θ(z, y)。 - 修改判别器:判别器
D_w的输入除了数据样本x(或生成样本x̂),也需要加入条件变量y。即判别器需要判断(x, y)这个配对是否来自真实数据分布。
相应地,目标函数也修改为条件形式:
J(θ, w) = E_{(x,y)~P_{data}}[log D_w(x|y)] + E_{z~p(z), y}[log(1 - D_w(G_θ(z, y)|y))]
对于离散的类别标签 y,通常使用独热编码将其转换为向量后,输入给生成器和判别器。
GAN的推理过程 🔍
在训练好GAN之后,我们如何使用它进行生成(即推理)呢?
推理的目标是利用训练好的生成器 G_θ* 产生新的、不在原始训练集中的数据样本。
以下是进行推理的步骤:
- 无条件GAN推理:从先验分布(如标准正态分布)中采样一个噪声向量
z_test,然后将其输入训练好的生成器:x_test = G_θ*(z_test)。x_test即是从学*到的分布P_θ中生成的新样本。 - 条件GAN推理:除了噪声向量
z_test,还需要指定一个条件y(例如,想要生成的图像类别)。然后将两者一起输入生成器:x_test = G_θ*(z_test, y)。这样就能生成符合指定条件y的新样本。

一个著名的例子是网站“This Person Does Not Exist”。每次刷新页面,都会从一个训练好的StyleGAN生成器中生成一张不存在的人脸图片。这展示了无条件GAN的推理能力。若使用条件GAN,则可以控制生成人脸的特定属性,如性别、年龄等。




总结
本节课我们一起学*了两种重要的GAN变体:
- 深度卷积GAN(DCGAN):通过使用转置卷积层,使生成器能够更有效地生成图像,无需后处理的重塑操作。
- 条件GAN(CGAN):通过向生成器和判别器同时输入条件信息(如类别标签),实现了对生成样本内容的控制,使其能根据特定条件进行生成。



我们还了解了如何使用训练好的GAN模型进行推理,以生成新的数据样本。这些架构改进极大地增强了GAN的实用性和可控性。
015:DC-GAN与条件GAN的实现 🎼

在本教程中,我们将学*两种生成对抗网络(GAN)的实现:深度卷积GAN(DC-GAN)和条件GAN(C-GAN)。我们将重点探讨DC-GAN中生成器所使用的转置卷积(或称分数步长卷积)原理,并了解如何通过条件信息来控制生成图像的类别。
转置卷积(上采样卷积)原理
上一节我们介绍了朴素GAN(Vanilla GAN),其生成器使用多层感知机(MLP)。在本节中,我们将看到DC-GAN的生成器使用了一种不同的操作:转置卷积(Transposed Convolution),也称为上卷积(Up Convolution)或分数步长卷积(Convolution with Fractional Stride)。
在典型的卷积操作中,输出尺寸会小于输入尺寸。其公式可以表示为:
H_out = floor((H_in + 2*padding - kernel_size) / stride) + 1
然而,在转置卷积中,输出尺寸会大于输入尺寸。其输出尺寸的计算公式为:
H_out = (H_in - 1) * stride - 2 * input_padding + kernel_size + output_padding

例如,对于一个输入尺寸为 7x7,使用 4x4 卷积核,步长为 2,输入填充为 1,输出填充为 1 的转置卷积层,其输出尺寸计算如下:
H_out = (7 - 1) * 2 - 2 * 1 + 4 + 1 = 15
因此,7x7 的输入被转换成了 15x15 的输出。
为了更直观地理解,我们可以看一个一维的例子。假设输入为 [A, B, C],步长为 2。转置卷积首先会在元素间插入零值,得到 [A, 0, B, 0, C, 0]。然后,一个大小为 3 的卷积核会在这个扩展后的序列上进行滑动卷积操作,从而产生一个更长的输出序列。这就是转置卷积实现上采样的基本原理。
DC-GAN 架构与代码实现
理解了转置卷积后,我们来看DC-GAN的架构。在DC-GAN中,生成器由一系列转置卷积块构成,它将一个低维的噪声向量 z 逐步上采样,直至达到目标图像尺寸。判别器则使用标准的卷积层来处理图像。
以下是实现中的关键参数和步骤概述:
关键参数设置
batch_size = 128z_dim = 100(噪声向量维度)image_size = 28(MNIST图像尺寸)channels = 1(灰度图通道数)epochs = 50learning_rate = 2e-4- 优化器使用
Adam,其中beta1 = 0.5
生成器网络结构
生成器是一个 nn.Sequential 模块,它接收噪声向量 z,并通过多个转置卷积层逐步生成图像。
以下是生成器核心层的示例代码结构:
self.model = nn.Sequential(
# 将噪声向量映射到更高维度的特征图
nn.ConvTranspose2d(z_dim, 256, kernel_size=4, stride=1, padding=0, bias=False),
nn.BatchNorm2d(256),
nn.ReLU(True),
# 上采样过程
nn.ConvTranspose2d(256, 128, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(128),
nn.ReLU(True),
# 输出层,生成单通道图像
nn.ConvTranspose2d(128, channels, kernel_size=4, stride=2, padding=1, bias=False),
nn.Tanh() # 将输出值约束在[-1, 1]区间
)
判别器网络结构
判别器接收图像作为输入,使用标准卷积层提取特征,最后通过一个全连接层和Sigmoid激活函数输出一个概率值,判断图像是真实的还是生成的。

以下是判别器的示例代码结构:
self.model = nn.Sequential(
# 输入为图像
nn.Conv2d(channels, 64, kernel_size=4, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True),
# 更深层的特征提取
nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2, inplace=True),
# 展平并分类
nn.Flatten(),
nn.Linear(128 * 7 * 7, 1), # 假设经过两层卷积后特征图尺寸为7x7
nn.Sigmoid()
)
训练过程
训练循环与朴素GAN类似,但通常建议对生成器进行更多次的更新。在本例中,设置每训练判别器1次,就训练生成器3次(k=3, p=1)。经过多个epoch的训练后,DC-GAN生成的图像质量明显优于使用MLP的朴素GAN。

条件GAN(C-GAN)的原理与实现
无论是朴素GAN还是DC-GAN,我们都无法控制生成图像的类别。条件GAN(C-GAN)通过在生成器和判别器的输入中引入类别标签信息来解决这个问题,从而学*条件分布 P(X|Y)。
核心思想
- 生成器:输入不再是单纯的噪声
z,而是噪声z与目标类别标签y(通常经过嵌入或独热编码)的拼接。 - 判别器:输入不再是单纯的图像
x,而是图像x与其对应真实标签y的拼接。对于生成图像,则拼接生成时使用的条件标签。


代码实现要点
以下是条件GAN中网络结构的关键修改:
生成器输入:
generator_input_dim = z_dim + num_classes
判别器输入:
discriminator_input_dim = image_flattened_size + num_classes
在训练循环中,需要同时向生成器和判别器提供标签信息。对于从真实数据集中取出的批次,使用其真实标签。对于生成器,则需要为其创建对应的条件标签(例如,我们希望生成数字“7”的图像)。
当前实现的局限与改进
本教程示例中的条件GAN为了简化,生成器仍使用了MLP结构,因此生成的图像较为模糊。一个有效的改进练*是:将条件GAN中的MLP生成器替换为DC-GAN中使用的转置卷积结构。结合条件信息和更强大的卷积架构,有望生成更清晰、更可控的特定类别图像。
总结 🎯

在本节课中,我们一起学*了两种重要的GAN变体:
- DC-GAN:其生成器使用转置卷积层进行上采样,能够生成比朴素GAN质量更高的图像。判别器使用标准卷积层处理图像。
- 条件GAN(C-GAN):通过在生成器和判别器的输入中拼接类别标签信息,实现了对生成图像类别的控制,使其能够按需生成特定类别的图像。


两者的训练流程与朴素GAN基本一致,都遵循交替优化生成器和判别器的模式。通过将DC-GAN的上采样架构与C-GAN的条件控制机制相结合,可以构建出更强大、更可控的图像生成模型。
016:W4L10-GAN训练的饱和


在本节课中,我们将继续探讨对抗学*。具体来说,我们将研究如何改进对抗学*以使其训练过程更稳定。此外,我们还将了解对抗学*在生成建模之外的几个应用示例。
问题:为什么GAN训练不稳定?
上一节我们介绍了基于f散度最小化的对抗学*框架。其核心是构造f散度的下界,然后通过一个极小极大优化问题(鞍点优化问题)来最小化这个下界。这种方法也被称为对抗优化。
然而,这种方法存在两个主要问题。首先,由于它是一个鞍点问题,很难找到最优解。其次,任何基于f散度的最小化都可能导致训练不稳定。
大多数GAN及其变体都基于f散度。我们从一个f散度开始,构造其下界,然后尝试最小化它。我们通过交替优化生成器和判别器来实现这一点。但问题是,我们在最小化阶段构建的生成器的好坏,很大程度上取决于我们为f散度计算的下界(即判别器的学*效果)有多好。
那么,核心问题是:是什么导致了GAN训练如此困难和/或不稳定?我们将分析一些可能的原因,并探讨一种可能的补救措施。
流形假设
整个分析基于一个称为“流形假设”的假说。
流形假设的核心思想是:我们在实践中观察到的数据(无论是图像、文本还是任何其他类型的数据)都存在于环境空间中的一个非常低维的流形上。
这意味着,真实数据的分布(如图像)位于环境空间中的一个低维流形上。
为了理解“流形”,我们可以将其视为环境空间中的一个低维子空间。例如,想象在三维空间中拿着一张纸,纸上的所有点都可以看作是三维环境空间中的一个二维子空间。再比如,拿着一根理论上厚度为零的线,线上的所有点都位于一个一维流形上,尽管这些点可以用三个坐标表示,但它们实际上存在于一个一维子空间中。
这是一个假说,因为我们无法确定地测试数据是否真的位于低维流形上。但根据大量经验证据,我们可以认为获得的数据确实位于非常低维的流形上。
一个具有说服力的例子是:假设我们进行一个实验,填充一个28x28的网格(代表一张图像)。我们抛一枚概率为p的硬币,如果正面朝上,则将像素值设为1(黑色),如果反面朝上,则设为0(白色)。问题是,通过这种随机实验,生成具有语义意义(例如看起来像字母)的图像的概率是多少?这个概率极低。这类似于著名的“猴子打字机”问题:一只猴子随机打字,打出莎士比亚作品的概率微乎其微。
在这个例子中,所有通过上述随机实验生成的图像都可以表示在一个784维的二进制空间中。所有具有语义意义的图像也是这个空间的成员。但在这个所有可能结果的巨大空间中,那些对应语义上有意义图像的点所占的子空间(流形)非常小。因为在整个可能的结果中,通过随机实验最终生成有意义图像的概率非常低。
将此扩展到自然图像:在所有可能的图像(包括各种噪声图像)中,我们在实践中获得的数据所占据的流形,与环境空间相比,维度非常低。这个随机实验是为了让你大致理解为什么流形假设可能是成立的。
流形假设对GAN训练的影响
现在的问题是:这如何影响f散度最小化或GAN训练?
回想一下,我们处理的两个分布 Px(真实数据分布)和 Pθ(生成数据分布)都是定义在 R^D 上的分布。由于真实数据和生成数据都位于低维流形上,因此 Px 和 Pθ 的支撑集(即密度函数非零的区域)很可能没有对齐。
“支撑集”是指对应密度函数取非零值的集合。由于真实数据和生成数据位于不同的低维流形上,Px 和 Pθ 的支撑集(即这两个流形)很可能不一致(即没有对齐)。
接下来是一个关键定理:可以证明,当两个感兴趣分布 Px 和 Pθ 的支撑集没有完美对齐时,总可以学*到一个完美的判别器(即具有100%准确率的判别器)。
这意味着,在GAN训练中,如果判别器变得过于强大,能够以100%的准确率区分真实数据和生成数据,那么生成器的训练就会“饱和”。因为此时,我们为生成器计算的梯度会消失,生成器无法再获得有效的更新信号。
在极小极大优化问题的背景下,这意味着如果我们没有为f散度构造一个非常紧的下界,那么最小化这个较弱的下界就没有意义。
因此,针对这个问题的一个常见经验性补救措施是:在交替训练生成器和判别器时,不要以相同的频率训练它们。通常建议较少地训练判别器,以防止其变得过于强大,从而避免生成器训练饱和。但这只是一种经验性的解决方法。
问题的理论根源与解决方案
那么,问题的理论根源是什么?可以进一步证明,当你能找到一个准确率100%的判别器时,真实数据与生成数据之间的f散度将变得与生成器参数θ无关。正是这一点导致了GAN训练的饱和。
因此,我们需要一个更根本的解决方案。这个解决方案是:使用一个“更柔和”的散度度量。
所谓“更柔和”,是指这个度量不会在分布支撑集(流形)没有对齐时就达到饱和。我们知道,由于流形假设,流形很可能不会对齐。我们需要的度量是:即使流形没有对齐,它也能量化流形之间的距离,而不是简单地饱和。
换句话说,问题出在f散度上。大多数常用的f散度(如Jensen-Shannon散度、KL散度、皮尔逊卡方距离等)在流形不重叠时都会饱和,这不是我们想要的。
因此,解决方案是提出一种新的度量,它在流形未对齐时不会饱和,而这通常是实际情况。

通常情况确实如此。

总结


本节课中,我们一起学*了GAN训练不稳定的一个核心原因。我们首先介绍了流形假设,即真实数据存在于高维环境空间的低维流形上。基于此,我们分析了当真实数据分布Px和生成数据分布Pθ的支撑集(流形)未对齐时,总可以找到一个完美的判别器(准确率100%)。这会导致生成器训练的梯度消失,即训练饱和。问题的根源在于传统的f散度度量(如JS散度)在此情况下会失效。因此,我们需要一种更柔和的散度度量,它即使在流形未对齐时也能有效衡量分布间的距离,从而为生成器提供持续的优化信号。这为后续介绍Wasserstein距离等更稳定的度量方法奠定了基础。
017:Wasserstein GAN 🎼

在本节课中,我们将学*一种改进生成对抗网络(GAN)训练稳定性的方法——Wasserstein GAN。我们将探讨传统GAN在数据流形不重叠时遇到的问题,并引入一种名为Wasserstein距离(或最优传输距离)的度量来解决它。最后,我们将看到如何利用这个距离构建更稳定的WGAN。
从F散度到最优传输
上一节我们介绍了F散度作为衡量分布差异的度量。然而,当两个分布的支撑集(即数据实际存在的区域)不重叠时,F散度会达到饱和,导致梯度消失,使得GAN训练失败。
本节中,我们来看看另一种不会饱和的度量族——Wasserstein距离,它源于最优传输的思想。
理解最优传输与Wasserstein距离
Wasserstein距离的核心思想是:衡量将一个概率分布“搬运”成另一个概率分布所需的最小“工作量”。
直观解释:离散分布的例子
为了便于理解,我们考虑两个一维离散概率分布。我们可以用直方图表示它们。
- 分布Px:在位置x1, x2, ..., xk上有一定的概率质量。
- 分布Px_cap:在位置x1_cap, x2_cap, ..., xl_cap上有一定的概率质量。
我们的目标是将Px“改造”成Px_cap。这意味着我们需要重新分配Px在各个点上的质量,使其最终形态与Px_cap一致。
以下是实现这一目标的关键概念:
- 传输方案:每一个具体的质量搬运方法(例如,从x1搬多少质量到x1_cap,多少到x2_cap等)都对应一个联合分布。这个联合分布描述了从Px的每个点到Px_cap的每个点的质量转移量。
- 传输成本:将质量从一个点x搬运到另一个点x_cap需要做功。这个功可以量化为两点间的距离乘以搬运的质量,即
距离(x, x_cap) * 质量(x, x_cap)。 - 方案总成本:一个完整传输方案的总成本(或平均做功)就是所有点对之间的搬运成本之和。数学上,这等于在该联合分布(传输方案)下,
距离(x, x_cap)的期望值:E_(x, x_cap)~π [ ||x - x_cap|| ]。
Wasserstein距离的数学定义
给定两个分布Px和Px_cap,它们之间的(一阶)Wasserstein距离定义为所有可能传输方案中的最小成本。
公式:
W(Px, Px_cap) = min_(π ∈ Π) E_(x, x_cap)~π [ ||x - x_cap|| ]
其中:
π是一个联合分布,它是Px和Px_cap的一个可能传输方案。Π是所有满足边际分布为Px和Px_cap的联合分布的集合。即,对π关于x_cap积分得到Px,关于x积分得到Px_cap。
直观理解:如果两个分布非常接*,那么将其中一个“搬”成另一个所需的最小工作量就很小,因此Wasserstein距离也小。反之,如果分布相距甚远,最小工作量就大,距离也大。关键在于,即使两个分布的支撑集完全不重叠,这个“最小工作量”仍然是一个有意义的、非饱和的数值。
从距离到生成模型:Wasserstein GAN (WGAN)
我们的目标仍然是训练生成器G_θ,使其产生的分布P_θ尽可能接*真实数据分布Px。现在,我们选择最小化它们之间的Wasserstein距离,即:min_θ W(Px, P_θ)。
直接计算Wasserstein距离的极小值很困难。幸运的是,Kantorovich-Rubinstein对偶性提供了一个等价形式:
公式:
W(Px, P_θ) = max_(||f||_L ≤ 1) [ E_(x~Px)[f(x)] - E_(z~Pz)[f(G_θ(z))] ]
这个对偶形式将最小化问题转化为一个最大化问题:
f是一个函数,它需要满足 1-Lipschitz连续 的约束(即其梯度的范数几乎处处不大于1)。- 我们需要找到一个在约束条件下的最优函数
f,使得两个期望的差最大。这个最大值就是Wasserstein距离。
构建WGAN
这个对偶形式与原始GAN的目标非常相似!我们可以这样构建WGAN:
- 判别器变为评论家:函数
f由一个神经网络T_w(称为评论家)来参数化。它的任务是最大化E[T_w(真实数据)] - E[T_w(生成数据)]。 - 生成器:生成器
G_θ的任务是最小化上述差值,即让评论家的输出差变小。 - 关键约束:必须确保评论家网络
T_w满足1-Lipschitz约束。
因此,WGAN的优化目标是一个极小极大问题:
min_θ max_(w: ||T_w||_L ≤ 1) [ E_(x~Px)[T_w(x)] - E_(z~Pz)[T_w(G_θ(z))] ]
如何实施Lipschitz约束?
确保神经网络满足严格的Lipschitz约束是研究课题。一个简单有效的实践方法是:在每次梯度更新后,将评论家网络T_w所有权重裁剪到一个固定的小区间内(例如[-0.01, 0.01])。这被称为权重裁剪。
代码示意(训练循环核心):
# 训练评论家 (多次)
for _ in range(n_critic_steps):
real_data = ...
fake_data = generator(noise)
critic_real = critic(real_data)
critic_fake = critic(fake_data)
# Wasserstein 损失
loss_critic = -(torch.mean(critic_real) - torch.mean(critic_fake))
loss_critic.backward()
critic_optimizer.step()
# 权重裁剪,实施 Lipschitz 约束
for p in critic.parameters():
p.data.clamp_(-0.01, 0.01)
# 训练生成器
gen_fake = critic(fake_data)
loss_generator = -torch.mean(gen_fake) # 最大化评论家对假数据的评分
loss_generator.backward()
generator_optimizer.step()
总结
本节课中我们一起学*了Wasserstein GAN的核心思想。
- 问题:传统GAN使用的F散度在数据流形不重叠时会饱和,导致训练不稳定。
- 解决方案:引入Wasserstein距离作为分布差异的度量,它衡量了分布间转换的最小“工作量”,即使支撑集不重叠也不会饱和。
- 对偶形式:通过Kantorovich-Rubinstein对偶,将最小化Wasserstein距离转化为一个易于求解的极大极小问题,形式类似原始GAN。
- WGAN架构:将判别器改为评论家,其目标是最大化真实数据与生成数据评分之差;生成器则试图最小化这个差。关键改进是必须对评论家网络施加1-Lipschitz约束,实践中常通过权重裁剪实现。
- 优势:WGAN的训练通常比标准GAN更稳定,梯度行为更良好,减少了模式崩溃等问题。


因此,在应用GAN时,使用Wasserstein距离构建的WGAN是一个更可靠的选择。在下一个模块中,我们将探讨GAN在生成任务之外的一些应用。
018:基于GAN的反演

概述
在本节课中,我们将要学*生成对抗网络中的一个重要概念——反演。我们将探讨什么是GAN反演,为什么它是有用的,以及如何实现它。
继续讨论生成对抗网络与对抗学*
上一节我们介绍了GAN的基本原理,本节中我们来看看GAN的反演。
什么是GAN反演?
在目前我们所见的朴素GAN中,我们可以从一个任意的输入空间生成数据。通常的过程是:我们有一个生成器,它从我们感兴趣的数据分布中采样。这个生成器的输入是来自正态分布的样本,它会输出接*目标数据分布 P_data 的样本。这个过程可以表示为:
公式: G: Z -> X,其中 Z ~ N(0, I),X ~ P_data。
现在,假设我们想要“反转”这个过程。这意味着什么呢?具体来说,给定一个从数据分布 P_x 中采样的数据点 x_i,我们的目标是找到对应的潜在变量 z_i。也就是说,找到一个 z_i,使得训练好的生成器 G_θ* 满足:
公式: G_θ*(z_i) = x_i
这个问题被称为“反演”问题。在训练后,我们可以从正态分布中采样并生成数据点,但朴素GAN没有提供方法来根据给定的数据点找到对应的潜在变量 z。
为什么需要反演?
在探讨如何解决反演问题之前,我们先了解一下为什么它很重要。反演主要有两个应用场景:
以下是反演的两个主要用途:
- 特征提取:一个训练良好的GAN已经隐式地学*了数据的分布。如果我们能够反转生成器函数,那么通过反演得到的向量就代表了数据的某种特征表示。由于
z的维度通常远低于原始数据,因此反演可以得到数据的低维特征表示,这些特征可以用于分类或其他下游任务。 - 数据操控或编辑:假设我们有一个特定的图像,我们想使用GAN来编辑它。朴素GAN本身不支持编辑任务,因为它只能从分布中采样并生成数据。但是,如果我们能先通过反演得到该图像对应的潜在向量
z_i,然后对这个向量进行编辑(例如,通过一个编辑函数f_edit),最后再将编辑后的向量z_edit输入生成器,就能生成编辑后的新图像x_edit。
数据编辑流程可以概括为:
- 给定数据点
x_i。 - 通过反演找到对应的
z_i,使得G_θ*(z_i) ≈ x_i。 - 对
z_i应用编辑函数:z_edit = f_edit(z_i)。 - 生成编辑后的数据:
x_edit = G_θ*(z_edit)。
因此,找到能够生成特定数据点的输入向量 z 是非常重要的。


总结
本节课中我们一起学*了GAN反演的概念。我们明确了反演的目标是:给定一个数据点 x,找到其对应的潜在空间向量 z,使得生成器能够重建它。我们还探讨了反演的两个关键应用:一是作为特征提取器,为数据提供低维表示;二是作为数据编辑的基础,通过修改潜在向量来实现对生成内容的操控。理解反演是深入利用GAN模型能力的重要一步。
生成式AI的数学基础:P19:通过潜在回归进行GAN反演

在本节课中,我们将学*另一种实现GAN反演的方法——潜在回归法。我们将探讨其基本原理、网络结构、损失函数设计,并与之前介绍的BiGAN方法进行比较。
上一节我们介绍了通过联合分布匹配进行反演的BiGAN方法。本节中,我们来看看另一种被称为“潜在回归”的反演技术。
潜在回归法与BiGAN非常相似,但核心思想不同。它不是通过最小化联合分布匹配问题来显式地解决反演,而是采用回归的思路。
以下是潜在回归法的基本框架:
- 编码器网络:该方法仍然需要一个编码器网络。没有编码器,就无法实现反演。这个编码器网络接收数据点 x 作为输入,并直接回归(预测)出对应的潜在向量 ẑ。我们可以将其表示为 ẑ = E_φ(x),其中 E_φ 是编码器网络,φ 是其参数。
- 判别器网络:判别器 D_w 的功能与标准GAN中完全一致。它的任务是区分真实数据 x(来自真实分布 P_x)和生成数据 x̂(来自生成器分布 P_θ)。
- 生成器网络:生成器 G_θ 的功能也与标准GAN中一致,它接收一个随机潜在向量 z,并生成数据 x̂ = G_θ(z)。
现在,我们来看看如何训练这个模型。潜在回归法的损失函数是在标准GAN损失的基础上,增加了一个回归项。
标准GAN的对抗损失部分如下:
L_adv = E_{x~P_x}[log D_w(x)] + E_{z~P_z}[log(1 - D_w(G_θ(z)))]
其中,第一项鼓励判别器识别真实数据,第二项鼓励生成器生成以假乱真的数据。
为了训练编码器进行反演,我们增加一个回归损失项。这个损失项衡量的是生成数据 x̂ 所对应的原始潜在向量 z,与编码器预测的潜在向量 ẑ 之间的差异。通常使用L2范数(均方误差)来衡量:
L_reg = || z - E_φ(G_θ(z)) ||^2
这里,z 是生成 x̂ 时使用的原始潜在向量,E_φ(G_θ(z)) 是编码器对生成数据 x̂ 进行编码后得到的预测向量 ẑ。
最终的联合损失函数是这两部分的加权和:
L_total = L_adv + λ * L_reg
其中,λ 是一个超参数,用于控制回归损失在总损失中的权重。
在训练过程中,生成器参数 θ、判别器参数 w 和编码器参数 φ 这三个网络被同时训练。具体来说,对于一个由特定 z 生成的 x̂,我们将其输入编码器,计算预测的 ẑ,然后通过回归损失 L_reg 计算编码器的梯度并进行更新。生成器和判别器则主要通过对抗损失 L_adv 进行更新。
潜在回归法与BiGAN的关键区别在于对判别器的处理方式:
- 在BiGAN中,判别器被修改为接收数据-潜在向量对 (x, z) 或 (x̂, z) 作为输入,并学*区分来自联合分布的这些对。
- 在潜在回归中,判别器没有被修改,它仍然像在标准GAN中一样工作。额外的编码器网络只是简单地尝试回归输入潜在向量,因此得名“回归器”。它通过向标准GAN损失添加一个回归成本来训练编码器。
然而,研究发现,与这种简单或朴素的回归方法相比,修改判别器并求解联合分布匹配问题通常能产生更好的反演质量。潜在回归和BiGAN是文献中常用的两种GAN反演技术,当然也存在许多其他方法。


本节课中,我们一起学*了通过潜在回归进行GAN反演的方法。我们了解了其网络结构由生成器、判别器和一个额外的编码器组成,其损失函数结合了标准GAN的对抗损失和一个潜在向量回归损失。最后,我们将其与BiGAN方法进行了对比,指出了判别器角色和训练目标上的核心差异。
020:领域对抗网络 🎼


在本节课中,我们将学*对抗学*的另一个重要应用:领域适应。我们将探讨如何利用对抗学*的思想来解决训练数据(源领域)与测试数据(目标领域)分布不一致的问题,即领域偏移。
什么是领域偏移?
上一节我们介绍了对抗学*在生成模型中的应用。本节中,我们来看看它如何解决一个不同的问题:领域偏移。


领域偏移是一个实际问题。假设我们有一个源分布 ( D_S ),从中采样得到带标签的数据集 ( { (x_i, y_i) } ),其中 ( (x, y) \sim P_S(x, y) )。我们的任务是训练一个分类器。

在标准的分类任务中,分类器旨在估计条件分布 ( P(y|x) )。然而,在实际应用中,测试时的数据可能来自一个不同的分布,即目标分布 ( D_T ),其中 ( (x, y) \sim P_T(x, y) ),且 ( P_T \neq P_S )。
在这种情况下,仅在源数据 ( D_S ) 上训练的分类器或回归器,在目标数据 ( D_T ) 上的性能通常会显著下降。
领域偏移的实例
为了更直观地理解,让我们看一个例子。以下是PACS数据集的示例,它包含来自四个不同领域的数据:照片、艺术绘画、卡通和素描。



人类可以轻易识别出每个图像中的物体类别,无论它来自哪个领域。但是,如果一个神经网络仅在“照片”领域的数据上训练,当测试时遇到“素描”领域的数据时,其分类性能可能会很差。
因此,领域适应(Domain Adaptation)的核心问题是:我们能否找到一种方法,使得在源数据上训练的分类器,在与源分布相似但不完全相同的目标分布上也能表现良好?
无监督领域适应问题设定
我们关注的问题称为无监督领域适应。其设定如下:
- 源数据:我们有来自源分布 ( P_S ) 的样本 ( { (x_i, y_i) }_{i=1}^N ),包含数据和标签。
- 目标数据:我们有来自目标分布 ( P_T ) 的样本 ( { \hat{x}j }^M ),但没有对应的标签。
这是一个很实际的假设,因为收集数据本身相对容易,但获取准确的标签往往成本高昂。
问题的目标是:给定 ( D_S ) 和 ( D_T ),学*一个特征表示或分类器,使其在源数据和目标数据上都能表现良好。
使用对抗学*解决领域适应
接下来,我们看看如何利用对抗学*来解决这个问题。我们将使用一种称为领域对抗网络的方法。
其核心思想是:学*一个特征提取器,使得它提取的源领域特征和目标领域特征的分布尽可能一致。这样,基于源特征训练的分类器就能自然地适用于目标特征。
以下是网络结构:
- 特征提取器 ( \phi(x) ):这是一个神经网络,将输入数据(无论是源数据 ( x ) 还是目标数据 ( \hat{x} ))映射到一个特征空间 ( \mathcal{F} )。对于源数据,它输出特征 ( f_S = \phi(x) ),其分布为 ( P(f_S) )。对于目标数据,输出特征 ( f_T = \phi(\hat{x}) ),其分布为 ( P(f_T) )。
- 领域判别器 ( D_W(f) ):这是一个判别器网络,输入是特征 ( f_S ) 或 ( f_T ),目标是判断该特征来自源领域还是目标领域。它输出一个介于0和1之间的值。
- 标签分类器 ( h(f_S) ):这是一个标准的分类器,仅使用源数据的特征 ( f_S ) 来预测其标签 ( y )。
整个网络的训练过程是一个对抗博弈:
- 领域判别器 ( D_W ) 的目标是最大化其区分能力。其损失函数为:
[
L_{adv}(D) = \mathbb{E}{f_S \sim P(f_S)}[\log D_W(f_S)] + \mathbb{E}[\log(1 - D_W(f_T))]
] - 特征提取器 ( \phi ) 的目标是双重的:
- 对抗目标:最小化领域判别器的性能,即“欺骗”判别器,让它无法区分特征是来自源领域还是目标领域。这通过最大化判别器的损失(或最小化其负值)来实现,从而促使 ( P(f_S) ) 和 ( P(f_T) ) 的分布对齐。
- 分类目标:最小化标签分类器的损失(如交叉熵损失),确保提取的特征对分类任务是有用的。其损失函数为:
[
L_{cls}(\phi, h) = \mathbb{E}_{(x,y) \sim P_S}[-\log h(\phi(x))_y]
]
- 标签分类器 ( h ) 的目标是最小化其在源数据上的分类误差 ( L_{cls} )。
关键点:特征提取器 ( \phi ) 同时接收来自分类损失和对抗损失的梯度。这迫使它学*到既对分类任务有效,又对领域变化不敏感(即领域无关)的特征。
训练与推理过程
在训练阶段,我们联合优化特征提取器 ( \phi )、领域判别器 ( D_W ) 和标签分类器 ( h )。优化问题可以表述为一个极小极大问题:
[
\min_{\phi, h} \max_{D_W} \left( L_{cls}(\phi, h) - \lambda L_{adv}(\phi, D_W) \right)
]
其中 ( \lambda ) 是一个权衡两项损失的参数。
在推理阶段,对于来自目标领域的新测试样本 ( \hat{x}_{test} ),我们:
- 将其输入训练好的特征提取器 ( \phi^* ),得到特征 ( f_{T, test} = \phi^*(\hat{x}_{test}) )。
- 将 ( f_{T, test} ) 输入训练好的标签分类器 ( h^* ),得到预测标签 ( \hat{y}{test} = h^*(f) )。
由于对抗训练确保了 ( P(f_S) \approx P(f_T) \,因此分类器 ( h^* ) 在目标特征上的行为与在源特征上是一致的,从而实现了跨领域的有效分类。


总结
本节课中,我们一起学*了对抗学*在生成任务之外的一个重要应用——领域对抗网络。
- 我们首先认识了领域偏移问题,即训练与测试数据分布不一致导致的模型性能下降。
- 然后,我们介绍了无监督领域适应的问题设定:拥有带标签的源数据和不带标签的目标数据。
- 核心解决方案是领域对抗网络,它通过一个对抗性训练框架,迫使特征提取器学*领域无关的特征。
- 特征提取器同时优化两个目标:欺骗领域判别器以实现特征分布对齐,以及最小化分类误差以保证特征的有效性。
- 最终,仅在源数据上训练的分类器,可以成功地应用于目标数据,因为它使用的是领域无关的特征表示。


这种方法展示了对抗学*思想的强大与灵活性,它不仅能够生成数据,还能帮助模型学*更鲁棒、更具泛化能力的特征表示。
021:双向GAN

概述
在本节课中,我们将学*一种名为双向生成对抗网络的GAN变体。我们将探讨如何修改标准GAN的架构,使其不仅能够从随机噪声生成数据,还能为给定的真实数据样本找到对应的输入噪声向量,即实现反演功能。
双向GAN的架构
上一节我们介绍了标准GAN的基本结构。本节中我们来看看双向GAN在架构上的关键变化。
在标准GAN中,我们有两个神经网络:生成器 G_θ(z) 和判别器 D_w(x)。生成器将来自正态分布的噪声向量 z 映射到数据分布,判别器则区分真实数据 x 和生成数据 G_θ(z)。
在双向GAN中,除了生成器和判别器,我们引入了第三个网络:编码器或反演器 E_φ(x)。这个编码器网络的作用是,接收来自真实数据空间 x 的样本,并将其映射回输入噪声空间 z。我们称其输出为 ẑ。
以下是双向GAN的三个核心组件:
- 生成器:
x̂ = G_θ(z),其中z ~ p_z。 - 编码器:
ẑ = E_φ(x),其中x ~ p_data。 - 判别器:
D_w(·, ·),其输入和功能发生了关键变化。
判别器的改进
在标准GAN中,判别器处理的是单个数据点。为了实现反演,双向GAN对判别器的任务进行了重新设计。
判别器不再仅仅区分单个的真实数据点 x 和生成数据点 x̂。相反,它被设计来区分数据对的联合分布。
以下是判别器需要区分的两种数据对:
- 真实数据对:
(x, E_φ(x)),即一个真实样本x及其通过编码器得到的潜在向量ẑ。 - 生成数据对:
(G_θ(z), z),即一个噪声向量z及其通过生成器得到的数据x̂。
如果判别器无法区分这两类数据对,就意味着 (x, ẑ) 的联合分布与 (x̂, z) 的联合分布相匹配。这间接保证了编码器 E_φ 能够为真实数据 x 找到“正确”的潜在表示 z。
目标函数与训练
基于上述架构,我们可以定义双向GAN的优化目标。
双向GAN的目标函数 L_BiGAN 涉及三个网络的参数:生成器参数 θ、编码器参数 φ 和判别器参数 w。其公式如下:
L_BiGAN(θ, φ, w) = E_{x~p_data}[log D_w(x, E_φ(x))] + E_{z~p_z}[log(1 - D_w(G_θ(z), z))]
对应的优化问题是:
- 最小化
L_BiGAN关于生成器参数θ和编码器参数φ。 - 最大化
L_BiGAN关于判别器参数w。
训练过程与标准GAN类似,采用交替优化:
- 从真实数据分布
p_data中采样一个批次x,通过编码器得到ẑ。 - 从先验噪声分布
p_z中采样一个批次z,通过生成器得到x̂。 - 将数据对
(x, ẑ)和(x̂, z)输入判别器D_w。 - 计算损失,并反向传播梯度以同时更新生成器
G_θ、编码器E_φ和判别器D_w的参数。
工作原理与总结
为什么这种设计能够工作?其背后的理论保证是,当上述目标函数达到纳什均衡时,可以证明最优解满足以下条件:
p_data(x) * p_φ(ẑ|x) = p_z(z) * p_θ(x̂|z)
这意味着真实数据 x 与编码输出 ẑ 的联合分布,等于先验噪声 z 与生成数据 x̂ 的联合分布。当这两个联合分布一致时,编码器 E_φ 就成为了生成器 G_θ 的有效反演器。


本节课中我们一起学*了双向GAN。我们了解到,通过引入一个编码器网络并让判别器区分数据对而非单个数据点,我们可以训练一个既能生成数据又能为任何给定数据找到对应潜在表示的GAN模型。训练完成后,生成器 G_θ* 可用于从噪声生成数据,而编码器 E_φ* 则可用于实现反演,即 ẑ = E_φ*(x)。这是实现GAN反演的一种有效方法。
022:Bi-GAN的实现 🧠

在本节课中,我们将学*双向生成对抗网络(Bi-GAN)的核心概念与代码实现。Bi-GAN在标准GAN的基础上增加了一个编码器网络,旨在学*从数据空间到潜在空间的映射,从而实现潜在向量的“反演”。
标准GAN回顾
上一节我们介绍了标准GAN的基本结构。在标准GAN中,生成器网络 G 将来自正态分布的潜在向量 z 映射到图像空间。判别器网络 D 则接收真实图像或生成图像,并输出一个标量(0或1)来判断其真伪。
Bi-GAN的核心思想 🎯
本节中,我们来看看Bi-GAN的改进之处。Bi-GAN引入了一个编码器网络 E,其目标是将真实图像 x 映射回潜在空间,得到一个编码后的潜在向量 ẑ。这使得网络能够学*数据的双向映射。
Bi-GAN的网络结构包含三个部分:
- 生成器 (G): 从潜在空间到数据空间。
G(z) -> x̃ - 编码器 (E): 从数据空间到潜在空间。
E(x) -> ẑ - 判别器 (D): 接收一个“数据-潜在向量”对
(x, z),判断该对是来自真实数据分布(x, E(x))还是生成数据分布(G(z), z)。
以下是Bi-GAN的优化目标(鞍点问题)公式:
min_{G, E} max_{D} V(D, G, E)
其中价值函数 V 定义为:
V(D, G, E) = E_{x~p_data(x)}[log D(x, E(x))] + E_{z~p_z(z)}[log(1 - D(G(z), z))]
代码实现详解 💻
理解了理论框架后,我们现在进入代码实现环节。我们将使用PyTorch框架来构建Bi-GAN。
1. 初始化与数据准备
首先,我们需要设置基本参数并准备数据。

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms

# 参数设置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
batch_size = 128
latent_dim = 50
epochs = 50
learning_rate = 0.0002

# 数据转换与加载(以MNIST为例)
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)


2. 网络结构定义


接下来,我们定义生成器、编码器和判别器网络。
生成器网络:将潜在向量转换为图像。
class Generator(nn.Module):
def __init__(self, latent_dim):
super(Generator, self).__init__()
self.model = nn.Sequential(
nn.Linear(latent_dim, 256),
nn.ReLU(),
nn.Linear(256, 512),
nn.ReLU(),
nn.Linear(512, 784), # MNIST图像展平后为28*28=784
nn.Tanh() # 输出值归一化到[-1, 1]
)
def forward(self, z):
return self.model(z)

编码器网络:将图像编码为潜在向量。
class Encoder(nn.Module):
def __init__(self, latent_dim):
super(Encoder, self).__init__()
self.model = nn.Sequential(
nn.Linear(784, 512),
nn.ReLU(),
nn.Linear(512, 256),
nn.ReLU(),
nn.Linear(256, latent_dim)
)
def forward(self, x):
return self.model(x)

判别器网络:接收图像和潜在向量的拼接,判断其来源。
class Discriminator(nn.Module):
def __init__(self, latent_dim):
super(Discriminator, self).__init__()
# 输入维度:图像(784) + 潜在向量(latent_dim)
input_dim = 784 + latent_dim
self.model = nn.Sequential(
nn.Linear(input_dim, 512),
nn.ReLU(),
nn.Linear(512, 256),
nn.ReLU(),
nn.Linear(256, 1),
nn.Sigmoid() # 输出一个概率值
)
def forward(self, x, z):
# 将图像x和潜在向量z在特征维度上拼接
input_vec = torch.cat([x, z], dim=1)
return self.model(input_vec)
3. 模型、损失函数与优化器实例化
以下是初始化模型和优化器的步骤。
# 实例化网络
G = Generator(latent_dim).to(device)
E = Encoder(latent_dim).to(device)
D = Discriminator(latent_dim).to(device)


# 损失函数:二元交叉熵
criterion = nn.BCELoss()
# 优化器
# 判别器优化器
d_optimizer = optim.Adam(D.parameters(), lr=learning_rate, betas=(0.5, 0.999))
# 生成器和编码器共享一个优化器
ge_optimizer = optim.Adam(list(G.parameters()) + list(E.parameters()), lr=learning_rate, betas=(0.5, 0.999))


4. 训练循环


训练过程分为两步:先训练判别器,再训练生成器和编码器。
for epoch in range(epochs):
for i, (real_imgs, _) in enumerate(train_loader):
# 将真实图像转移到设备并展平
real_imgs = real_imgs.view(-1, 784).to(device)
batch_size = real_imgs.size(0)
# 创建标签
real_labels = torch.ones(batch_size, 1).to(device)
fake_labels = torch.zeros(batch_size, 1).to(device)
# ---------------------
# 训练判别器 (D)
# ---------------------
D.train()
G.eval()
E.eval()
d_optimizer.zero_grad()
# 来自真实数据的损失
z_fake = E(real_imgs) # 编码器为真实图像生成潜在向量
d_real_output = D(real_imgs, z_fake.detach()) # 判别器判断(真实图像, 编码向量)
d_real_loss = criterion(d_real_output, real_labels)
# 来自生成数据的损失
z_real = torch.randn(batch_size, latent_dim).to(device) # 随机潜在向量
fake_imgs = G(z_real) # 生成器生成图像
d_fake_output = D(fake_imgs.detach(), z_real) # 判别器判断(生成图像, 原始向量)
d_fake_loss = criterion(d_fake_output, fake_labels)
# 总判别器损失及反向传播
d_loss = d_real_loss + d_fake_loss
d_loss.backward()
d_optimizer.step()
# -----------------------------
# 训练生成器与编码器 (G & E)
# -----------------------------
D.eval()
G.train()
E.train()
ge_optimizer.zero_grad()
# 生成器与编码器的对抗损失
# 目标:让判别器将生成数据对判断为“真实”
z_real_ge = torch.randn(batch_size, latent_dim).to(device)
fake_imgs_ge = G(z_real_ge)
z_fake_ge = E(fake_imgs_ge)
d_output_fake = D(fake_imgs_ge, z_fake_ge)
g_loss = criterion(d_output_fake, real_labels) # 希望判别器输出1
# 编码器的重构一致性损失(可选,可增强稳定性)
# 目标:让编码器对生成图像编码后,得到的向量接*原始输入向量
consistency_loss = nn.MSELoss()(z_fake_ge, z_real_ge)
total_ge_loss = g_loss + 0.5 * consistency_loss # 加权求和
total_ge_loss.backward()
ge_optimizer.step()
# 打印训练信息
if i % 200 == 0:
print(f"Epoch [{epoch}/{epochs}], Step [{i}/{len(train_loader)}], "
f"D Loss: {d_loss.item():.4f}, G&E Loss: {total_ge_loss.item():.4f}")
5. 生成样本
训练完成后,我们可以使用生成器从随机噪声中创建新的图像。
# 切换到评估模式
G.eval()
with torch.no_grad():
# 生成随机噪声
sample_z = torch.randn(16, latent_dim).to(device)
# 生成图像
generated_imgs = G(sample_z).view(-1, 1, 28, 28).cpu()
# 此时 generated_imgs 可以用于可视化
总结 📝


本节课中我们一起学*了双向生成对抗网络(Bi-GAN)的原理与实现。我们首先回顾了标准GAN的局限性,然后重点介绍了Bi-GAN如何通过引入一个编码器网络来实现数据空间与潜在空间的双向映射。我们详细剖析了其目标函数,并一步步完成了从网络定义、损失计算到训练循环的完整PyTorch代码实现。Bi-GAN的核心优势在于其编码器能够学*有意义的潜在表示,为图像编辑、插值等任务奠定了基础。
023:无监督域适应的实现 🎯

在本教程中,我们将学*无监督域适应的核心思想及其实现方法。我们将探讨如何利用对抗训练,让一个在源域数据上训练的模型,能够良好地泛化到目标域数据上。课程将涵盖问题定义、模型架构、损失函数设计,特别是梯度反转层的关键作用,并通过代码示例进行实践。最后,我们还将简要介绍用于评估生成图像质量的FID分数。
问题定义 🎯
上一节我们介绍了生成式AI的基本概念,本节中我们来看看一个具体的应用场景:无监督域适应。
我们面临一个分类任务。假设我们有一个源域,其中包含带标签的图像数据 (X_s, Y_s),这些数据采样自分布 P_s。同时,我们有一个目标域,其中只包含无标签的图像数据 X_t,这些数据采样自另一个分布 P_t。关键点在于,源域分布 P_s 和目标域分布 P_t 是不同的。
我们的目标是:利用源域的有标签数据和目标域的无标签数据,学*一个特征提取器。这个特征提取器提取的特征,应该能让我们训练一个分类器,该分类器不仅在源域上表现良好,在目标域上也能取得不错的性能。
为了具体说明,我们使用两个数据集:MNIST(手写数字,单通道黑白图)和MNIST-M(彩色背景的手写数字,三通道图)。我们将MNIST转换为三通道图以保持一致。模型需要从MNIST(源域)学*特征,并能够正确分类MNIST-M(目标域)中的数字。
模型架构与公式化 🏗️
理解了问题定义后,我们来看看如何用模型来实现它。核心架构包含三个部分:
- 特征提取器 (Feature Extractor): 一个卷积神经网络,输入图像
x,输出特征f。对于源域和目标域输入,分别得到特征f_s和f_t。 - 域判别器/评论家网络 (Domain Discriminator/Critic): 一个分类器,输入是特征
f,目标是判断该特征来自源域还是目标域。 - 标签分类器 (Label Classifier): 一个分类器,仅输入源域特征
f_s,预测其类别标签y。
训练时,标签分类器只使用源域的有标签数据。在推理时,我们将目标域图像输入特征提取器得到 f_t,然后由标签分类器预测其类别。如果特征提取器学*到了域不变的特征,使得 f_s 和 f_t 的分布相似,那么标签分类器就能成功泛化到目标域。
这引出了两个核心的损失函数:
- 对抗损失 (Adversarial Loss): 用于训练特征提取器和域判别器。特征提取器试图“迷惑”域判别器,而域判别器试图准确区分域。其公式如下:
其中,L_adv(φ, w) = E_{x~P_s} [log(D_w(F_φ(x)))] + E_{x~P_t} [log(1 - D_w(F_φ(x)))]φ是特征提取器参数,w是域判别器参数,F_φ是特征提取函数,D_w是域判别函数。特征提取器的目标是最大化这个损失(让域判别器分不清),而域判别器的目标是最小化这个损失(准确区分)。

- 分类损失 (Classification Loss): 用于训练特征提取器和标签分类器。这是一个标准的交叉熵损失,确保模型能在源域上正确分类。
其中,L_cls(φ, θ) = E_{(x,y)~P_s} [CrossEntropy(C_θ(F_φ(x)), y)]θ是标签分类器参数,C_θ是标签分类函数。
因此,特征提取器 φ 的参数更新受到两个目标的驱动:最大化对抗损失(以获得域不变特征)和最小化分类损失(以学*有判别性的特征)。


梯度反转层 🔄


上一节我们介绍了两个损失目标,本节中我们来看看如何协调它们,特别是对抗训练中的关键技巧——梯度反转层。


在标准的对抗训练中,特征提取器接收来自域判别器的梯度并更新自身,以生成让域判别器更难区分的特征。然而,如果不加处理,域判别器会很快变得非常擅长区分域,导致特征提取器学*到的是域特定的特征,而非域不变的特征,从而无法泛化到目标域。


为了解决这个问题,我们引入了梯度反转层。它在正向传播中是一个恒等函数,但在反向传播中,会将经过它的梯度乘以一个负系数(通常是-1)。

以下是其工作原理:
- 正向传播:
输出 = 输入。数据流不受影响。 - 反向传播:
梯度_output = -梯度_input。梯度符号被反转。

当我们将这个层放置在特征提取器和域判别器之间时,效果是:域判别器试图通过输出梯度来最小化自己的损失,但这个梯度在反向传播回特征提取器之前被反转了。因此,特征提取器实际上是朝着增大域判别器损失的方向更新,即朝着“迷惑”域判别器的方向优化,这正是我们想要的。
如果不使用梯度反转层:
- 域判别器会变得擅长检测来源。
- 特征提取器会学*域特定的特征,导致模型无法泛化。
代码实现 💻
理解了理论之后,我们通过代码来具体实现无监督域适应。以下是核心步骤的概述:

1. 数据准备
我们将MNIST(源域)转换为三通道,并加载MNIST-M(目标域)数据集,创建对应的数据加载器。

2. 模型定义
我们定义三个模块:
FeatureExtractor: 一个CNN,用于从图像中提取特征。LabelClassifier: 一个分类器,输入特征,输出类别(10类)。DomainDiscriminator: 一个判别器,输入特征,输出一个标量(0/1表示目标域/源域)。GradientReversalLayer: 实现前述的梯度反转功能。

3. 训练循环
训练过程在每个批次中执行以下操作:
- 从源域和目标域加载一个批次的数据。
- 将两个域的数据拼接后通过特征提取器,得到特征
features。 - 分类损失: 将源域特征(
features的前半部分)输入标签分类器,计算交叉熵损失。 - 对抗损失: 将全部特征通过梯度反转层,然后输入域判别器。域判别器的标签是:源域特征对应1,目标域特征对应0。计算二元交叉熵损失。
- 参数更新:
- 用分类损失更新标签分类器和特征提取器。
- 用对抗损失更新域判别器。
- 通过梯度反转层,用对抗损失的反转梯度更新特征提取器(使其学*域不变特征)。

4. 评估
训练完成后,我们在MNIST-M的测试集上评估模型性能。将目标域测试图像输入特征提取器,得到的特征再输入标签分类器,计算分类准确率。在我们的示例中,达到了约75.8%的准确率。



FID分数简介 📊

在结束关于无监督域适应的讨论前,我们简要介绍一个在生成式模型中常用的评估指标——Fréchet Inception Distance。

FID分数用于衡量生成图像与真实图像分布之间的相似度。分数越低,表示生成图像的质量越高、多样性越好,且与真实图像分布越接*。

计算FID分数的步骤如下:
- 使用一个在ImageNet上预训练的Inception-v3网络(移除最后的分类层)作为特征提取器。
- 分别输入一批真实图像和一批生成图像,提取它们在特定中间层(例如
2048维)的特征。 - 对这两组特征分别计算多元高斯的均值和协方差矩阵(
μ_r, Σ_r和μ_g, Σ_g)。 - FID分数通过以下公式计算:
其中FID = ||μ_r - μ_g||^2 + Tr(Σ_r + Σ_g - 2*(Σ_r * Σ_g)^(1/2))Tr表示矩阵的迹。
我们提供了一个计算FID的代码示例,你可以将其作为一个独立工具,用于评估各种生成式模型的输出质量。在示例中,我们计算了生成图像与真实MNIST-M图像之间的FID分数。

总结 🎉
在本节课中,我们一起学*了无监督域适应的实现方法。

- 我们首先定义了问题:如何利用有标签的源域数据和无标签的目标域数据,让模型能泛化到目标域。
- 接着,我们介绍了基于对抗训练的解决方案架构,包括特征提取器、域判别器和标签分类器,并形式化了对抗损失和分类损失。
- 我们深入探讨了协调这两个损失的关键技术——梯度反转层,它通过在反向传播中反转梯度,驱使特征提取器学*域不变的特征。
- 然后,我们通过具体的代码示例,演示了如何准备数据、定义模型、组织训练循环并评估模型在目标域上的性能。
- 最后,我们简要介绍了用于评估生成图像质量的FID分数及其计算方法。


通过本教程,你应该对无监督域适应的基本原理、实现细节以及相关的评估指标有了清晰的理解。
024:WGAN的实现 🚀

在本教程中,我们将学*如何用代码实现Wasserstein GAN。我们将从理解Wasserstein距离的核心概念开始,然后逐步构建并解释一个完整的PyTorch实现。
概述
在之前的课程中,我们学*了训练传统GAN时可能遇到的鞍点问题和梯度饱和问题。为了解决这些问题,我们引入了Wasserstein GAN。WGAN使用Wasserstein距离(也称为Earth Mover‘s Distance)作为损失函数,从而提供了更稳定的训练过程。本节中,我们将深入探讨Wasserstein距离的数学定义,并动手实现一个WGAN模型。
Wasserstein距离(Earth Mover‘s Distance)📏
为了理解WGAN,我们首先需要理解其核心度量——Wasserstein距离。它衡量的是将一个概率分布“移动”成另一个概率分布所需的最小“工作量”。
数学定义
考虑两个连续的概率分布 ( P_X ) 和 ( P_{\hat{X}} )。它们之间的Wasserstein距离定义为:
[
W(P_X, P_{\hat{X}}) = \inf_{\gamma \in \Pi(P_X, P_{\hat{X}})} \mathbb{E}_{(x, \hat{x}) \sim \gamma} [|x - \hat{x}|]
]
其中:
- ( \Pi(P_X, P_{\hat{X}}) ) 是所有联合分布 ( \gamma(x, \hat{x}) ) 的集合,这些联合分布的边缘分布分别是 ( P_X ) 和 ( P_{\hat{X}} )。即:
[
\int \gamma(x, \hat{x}) d\hat{x} = P_X(x) \quad \text{和} \quad \int \gamma(x, \hat{x}) dx = P_{\hat{X}}(\hat{x})
] - ( \inf ) 表示下确界(最小值)。
- 这个公式可以理解为:在所有将质量从 ( P_X ) “运输”到 ( P_{\hat{X}} ) 的“运输方案” ( \gamma ) 中,找到总运输成本(距离的期望)最小的那个方案。
离散化示例
为了更直观地理解,我们来看一个离散分布的例子。
假设有两个离散分布 ( P_X ) 和 ( P_{\hat{X}} ),它们的支撑点都是 {1, 2, 3, 4}。
- ( P_X ) 的概率质量: [0.4, 0.3, 0.2, 0.1]
- ( P_{\hat{X}} ) 的概率质量: [0.1, 0.2, 0.3, 0.4]
我们的目标是找到将 ( P_X ) 重新分配成 ( P_{\hat{X}} ) 的最小成本方案。一个可能的“运输方案”(运输矩阵)如下所示:
| 从\到 | (\hat{X}_1) (0.1) | (\hat{X}_2) (0.2) | (\hat{X}_3) (0.3) | (\hat{X}_4) (0.4) | 行和 (P_X) |
|---|---|---|---|---|---|
| (X_1) (0.4) | 0.1 | 0.2 | 0.1 | 0.0 | 0.4 |
| (X_2) (0.3) | 0.0 | 0.0 | 0.2 | 0.1 | 0.3 |
| (X_3) (0.2) | 0.0 | 0.0 | 0.0 | 0.2 | 0.2 |
| (X_4) (0.1) | 0.0 | 0.0 | 0.0 | 0.1 | 0.1 |
| 列和 (P_{\hat{X}}) | 0.1 | 0.2 | 0.3 | 0.4 | 1.0 |
在这个矩阵中,每个单元格 ( \lambda_{ij} ) 表示从 ( X_i ) 移动到 ( \hat{X}j ) 的质量。矩阵的行和必须等于 ( P_X ) 的原始质量,列和必须等于目标分布 ( P{\hat{X}} ) 的质量。这就是“边缘化”约束。
Wasserstein距离就是所有满足此约束的运输方案中,计算 ( \sum_{i,j} \lambda_{ij} \cdot |X_i - \hat{X}_j| ) 并取最小值。
WGAN的损失函数 ⚖️
理解了Wasserstein距离后,我们来看WGAN的损失函数。根据Kantorovich-Rubinstein对偶性,Wasserstein距离可以转化为一个最大化问题。WGAN的损失函数定义如下:
[
\min_{G} \max_{D \in \mathcal{D}} \mathbb{E}{x \sim P{data}}[D(x)] - \mathbb{E}{z \sim P{z}}[D(G(z))]
]
其中:
- ( G ) 是生成器,( z ) 来自先验分布(如标准正态分布 ( \mathcal{N}(0, I) ))。
- ( D ) 是判别器(在WGAN中常称为Critic评论家)。关键区别在于,( D ) 的输出不再是一个概率,而是一个分数,用于衡量输入图像的真实性。
- ( \mathcal{D} ) 是所有1-Lipschitz函数的集合。这个约束保证了 ( D ) 的梯度不会过大,是Wasserstein距离对偶形式成立的条件。
在实践中,我们通过权重裁剪(Weight Clipping) 来*似实现1-Lipschitz约束,即将Critic网络的所有参数 ( w ) 限制在某个区间内,例如 ([-c, c])。
在代码中,对于一批次数据,损失函数具体计算为:
Critic损失(最大化):
[
L_{critic} = \frac{1}{B_1} \sum_{i=1}^{B_1} D(x^{(i)}) - \frac{1}{B_2} \sum_{j=1}^{B_2} D(G(z^{(j)}))
]
我们通过最小化 ( -L_{critic} ) 来更新Critic的参数。
生成器损失(最小化):
[
L_{generator} = -\frac{1}{B_2} \sum_{j=1}^{B_2} D(G(z^{(j)}))
]
即,生成器希望它生成的图像能获得Critic给出的高分。
PyTorch代码实现 🛠️
上一节我们介绍了WGAN的理论基础,本节中我们来看看如何用PyTorch实现它。以下是实现的关键步骤和代码解析。
1. 导入库与设置参数
首先,导入必要的PyTorch模块并设置训练超参数。


import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import os
# 设备配置
device = torch.device(‘cuda‘ if torch.cuda.is_available() else ‘cpu‘)
# 超参数
batch_size = 128
epochs = 20
lr = 1e-4
n_critic = 5 # 每训练一次生成器,训练Critic的次数
clip_value = 0.01 # 权重裁剪值
image_size = 28
channels = 1
latent_dim = 128
# 数据转换
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,)) # 将图像像素值归一化到[-1, 1]
])
# 加载MNIST数据集
dataset = datasets.MNIST(root=‘./data‘, train=True, transform=transform, download=True)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
2. 构建生成器网络
生成器将随机噪声向量 z 转换为图像。我们使用转置卷积层进行上采样。
class Generator(nn.Module):
def __init__(self, latent_dim, channels):
super(Generator, self).__init__()
self.init_size = image_size // 4
self.fc = nn.Linear(latent_dim, 128 * self.init_size ** 2)
self.conv_blocks = nn.Sequential(
nn.BatchNorm2d(128),
nn.Upsample(scale_factor=2),
nn.Conv2d(128, 128, 3, stride=1, padding=1),
nn.BatchNorm2d(128, 0.8),
nn.LeakyReLU(0.2, inplace=True),
nn.Upsample(scale_factor=2),
nn.Conv2d(128, 64, 3, stride=1, padding=1),
nn.BatchNorm2d(64, 0.8),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64, channels, 3, stride=1, padding=1),
nn.Tanh() # 输出值在[-1, 1]之间
)
def forward(self, z):
out = self.fc(z)
out = out.view(out.shape[0], 128, self.init_size, self.init_size)
img = self.conv_blocks(out)
return img
3. 构建Critic网络
Critic网络接收图像并输出一个分数。注意,它不使用Sigmoid激活函数,并且我们使用LeakyReLU。
class Critic(nn.Module):
def __init__(self, channels):
super(Critic, self).__init__()
def critic_block(in_filters, out_filters, bn=True):
block = [nn.Conv2d(in_filters, out_filters, 3, 2, 1),
nn.LeakyReLU(0.2, inplace=True),
nn.Dropout2d(0.25)]
if bn:
block.append(nn.BatchNorm2d(out_filters, 0.8))
return block
self.model = nn.Sequential(
*critic_block(channels, 16, bn=False),
*critic_block(16, 32),
*critic_block(32, 64),
*critic_block(64, 128),
)
# 输出层:将特征图展平后通过一个线性层输出一个分数
self.adv_layer = nn.Linear(128 * 2 * 2, 1) # 经过4次stride=2,28x28 -> 2x2
def forward(self, img):
out = self.model(img)
out = out.view(out.shape[0], -1)
validity = self.adv_layer(out)
return validity
4. 初始化模型与优化器
# 初始化生成器和Critic
generator = Generator(latent_dim, channels).to(device)
critic = Critic(channels).to(device)
# 使用RMSprop优化器(原论文推荐)
optimizer_G = optim.RMSprop(generator.parameters(), lr=lr)
optimizer_C = optim.RMSprop(critic.parameters(), lr=lr)
5. 训练循环

以下是训练过程的核心循环,它体现了WGAN与原始GAN在训练步骤上的主要区别。

for epoch in range(epochs):
for i, (imgs, _) in enumerate(dataloader):
# 配置输入
real_imgs = imgs.to(device)
# ---------------------
# 训练 Critic (判别器)
# ---------------------
optimizer_C.zero_grad()
# 从先验分布采样噪声
z = torch.randn(imgs.shape[0], latent_dim).to(device)
# 生成假图像
fake_imgs = generator(z).detach() # 阻断梯度流向生成器
# 计算Critic的损失
loss_critic = -torch.mean(critic(real_imgs)) + torch.mean(critic(fake_imgs))
# 反向传播并优化
loss_critic.backward()
optimizer_C.step()
# 权重裁剪:强制实施Lipschitz约束
for p in critic.parameters():
p.data.clamp_(-clip_value, clip_value)
# -----------------
# 训练生成器
# -----------------
# 每训练 n_critic 次 Critic,训练一次生成器
if i % n_critic == 0:
optimizer_G.zero_grad()
# 在新的噪声上训练生成器
gen_imgs = generator(z)
# 生成器希望Critic对假图像打高分
loss_generator = -torch.mean(critic(gen_imgs))
loss_generator.backward()
optimizer_G.step()
# 打印训练状态
if i % 100 == 0:
print(f“[Epoch {epoch}/{epochs}] [Batch {i}/{len(dataloader)}] ”
f“[D loss: {loss_critic.item():.6f}] [G loss: {loss_generator.item():.6f}]“)
# 每个epoch结束后,可以保存生成的图像样例
# save_image(gen_imgs.data[:25], ‘images/%d.png‘ % epoch, nrow=5, normalize=True)
关键点解析:
- Critic损失:计算真实图像得分的负均值与假图像得分均值的和。我们通过最小化其负值来最大化原始目标。
- 权重裁剪:在每次Critic参数更新后,我们将其所有参数裁剪到
[-clip_value, clip_value]范围内。这是WGAN实现1-Lipschitz约束的简单而有效的方法。 - 生成器训练频率:通常,Critic会比生成器训练得更频繁(例如
n_critic=5),以确保在更新生成器之前,Critic是一个相对较好的*似。
总结
在本节课中,我们一起学*了Wasserstein GAN的实现。我们从Wasserstein距离的直观概念和数学定义出发,理解了它如何解决传统GAN训练不稳定的问题。然后,我们详细剖析了WGAN的损失函数及其对偶形式,并重点强调了1-Lipschitz约束的重要性及其通过权重裁剪的实现方式。


最后,我们一步步实现了一个用于MNIST数据集的WGAN PyTorch模型,涵盖了网络结构定义、损失计算、权重裁剪以及交替训练的关键循环。通过本教程,你应该能够掌握WGAN的核心思想,并具备动手实现一个基本WGAN的能力。在后续的探索中,你可以尝试改进权重裁剪(如使用梯度惩罚WGAN-GP),或将模型应用于更复杂的数据集。
025:潜在变量模型介绍 🧠

在本节课中,我们将学*生成式模型的另一个重要家族——潜在变量模型。我们将定义什么是潜在变量模型,并探讨其学*的一般原理。著名的变分自编码器和扩散模型都属于这个家族。
什么是潜在变量模型?
假设我们有一组数据向量,它们根据一个未知的底层分布 P 独立同分布地抽取。在生成式建模中,我们通常用一个由参数 θ 参数化的模型分布 P_θ 来*似这个真实分布。
在生成对抗网络中,P_θ 被隐式地建模为生成器网络输出的分布。而在潜在变量模型中,定义有所不同。
定义:一个潜在变量模型
在潜在变量模型中,模型分布 P_θ(x) 被定义为数据变量 x 与潜在变量 z 的联合分布的边缘分布。具体公式如下:
- 当 z 是离散变量时:P_θ(x) = Σ_z P_θ(x, z)
- 当 z 是连续变量时:P_θ(x) = ∫ P_θ(x, z) dz
这里的 z 被称为潜在、隐藏或未观测的随机变量。它的关键特征在于,我们收集的数据只来自变量 x,而 z 是完全不被观测到的。
从直观上理解,z 代表了与每个数据点相关的额外信息。例如,如果 x 是图像,那么 z 可以表示图像中物体的方向、大小或数量等特征。从数学构造上讲,这个隐藏变量被引入建模框架,用以表示数据中未被直接观测到的某些特征或信息。
上述求和或积分操作在概率论中称为边缘化:对两个随机变量的联合分布,通过对其中一个变量求和或积分,可以得到另一个变量的分布。
潜在变量模型的学*
在潜在变量模型中,我们不仅需要估计模型参数 θ,还需要联合估计潜在变量 z 的分布。对于数据集中的每个数据点 x_i,我们都假设存在一个对应的潜在变量实例 z_i。
引入这个额外的随机变量有两个主要好处:首先,它可以使数学上的学*过程变得可行或更容易;其次,它能提供关于数据的丰富信息。
潜在变量模型的类型与示例
潜在变量可以根据其性质分为离散型和连续型,这决定了模型的不同用途。
示例1:离散潜在变量
当潜在变量 z 是离散的,意味着它可以取 M 个可能值中的一个。数据变量 x 通常是连续的(例如在 R^D 中)。
在这种情况下,对于每个数据点 x_i,其对应的潜在变量 z_i 会将它分配到 M 个类别中的一个。这实质上是一种聚类或无监督分类。
以下是此类模型的例子:
- 高斯混合模型: 将数据建模为多个高斯分布的混合,每个分布对应一个潜在的类别。
- K均值聚类: 一种将数据划分到K个簇的经典算法。
因此,具有离散潜在空间的潜在变量模型可用于将数据自动分类到不同的组别中。
示例2:连续潜在变量
当潜在变量 z 也是连续的(例如在 R^K 中),情况则不同。
在这种情况下,对于每个数据点 x_i,其对应的潜在变量 z_i 代表了一个特征向量。通常,潜在空间的维度 K 远小于数据空间的维度 D。因此,z_i 可以被视为高维数据 x_i 的一个低维表示或编码。
自编码器就是这类模型的一个典型例子。它通过学*将数据压缩到低维潜在空间,然后再重建回来,从而学*数据的有效表示。
潜在变量模型作为生成模型
除了用于特征提取或聚类,大多数潜在变量模型也可以直接用作生成模型。这正是本课程关注的重点。
作为生成模型,潜在变量模型的工作流程通常是:先从潜在变量的先验分布中采样一个 z,然后通过条件分布 P_θ(x|z) 生成数据 x。寻找给定数据对应的潜在特征,反而成了生成建模过程中的一个副产品。
总结


本节课我们一起学*了潜在变量模型的基础概念。我们了解到,潜在变量模型通过引入一个未观测的隐藏变量 z 来定义数据分布 P_θ(x)。根据 z 是离散还是连续,模型可以分别应用于数据聚类或降维与特征提取。更重要的是,这个框架天然支持生成建模,为我们接下来深入学*变分自编码器和扩散模型奠定了重要的理论基础。
生成式AI的数学基础:P26:证据下界(ELBO)

在本节课中,我们将学*构建潜变量模型的通用原理,并重点介绍一个核心概念——证据下界(ELBO)。我们将看到,如何通过最大化ELBO来*似解决潜变量模型中的最大似然估计问题。
潜变量模型的学*原理
上一节我们介绍了潜变量模型的基本概念。本节中,我们来看看如何学*一个潜变量模型。我们将要介绍的原理是一个通用原理,适用于从高斯混合模型(GMM)到扩散模型在内的所有潜变量模型类别。
假设我们拥有数据 D,这些数据是从一个未知的真实数据分布 P(x) 中采样得到的。我们有一个参数为 θ 的潜变量模型 P_θ。为简化数学表达,我们假设潜变量 Z 是连续的,但所有推导同样适用于离散潜变量。
潜变量模型定义了观测变量 X 与潜变量 Z 的联合分布 P_θ(x, z)。我们的目标,是找到能够最好地拟合数据 D 的模型参数 θ。
从最小化KL散度到最大似然估计
我们通常通过最小化分布间的KL散度来估计参数。具体来说,我们希望最小化真实分布 P(x) 与模型分布 P_θ(x) 之间的KL散度:
目标:min_θ KL( P(x) || P_θ(x) )
根据KL散度的定义,我们可以将其展开:
KL( P(x) || P_θ(x) ) = ∫ P(x) log[ P(x) / P_θ(x) ] dx = H(P) - ∫ P(x) log P_θ(x) dx
其中,H(P) 是数据分布的熵,与参数 θ 无关。因此,最小化KL散度等价于最大化 ∫ P(x) log P_θ(x) dx,即最大化 log P_θ(x) 在真实数据分布 P(x) 下的期望值。
log P_θ(x) 被称为对数似然函数。因此,这个估计参数的过程被称为最大似然估计(MLE)。我们可以利用从 P(x) 中采样的数据,通过大数定律来*似计算这个期望。
潜变量模型中的似然计算挑战
在潜变量模型中,观测数据的边际似然 P_θ(x) 需要通过积分(或求和)潜变量得到:
P_θ(x) = ∫ P_θ(x, z) dz
因此,我们的对数似然函数 L(θ) 变为:
L(θ) = log P_θ(x) = log ∫ P_θ(x, z) dz
这里出现了一个问题:我们需要对 log 函数内部的积分(期望) 进行优化。直接估计“对数的期望”在统计上非常困难,因为我们无法简单地将采样平均移到对数外面。
引入变分分布与Jensen不等式
为了处理这个“log-of-integral”的形式,我们引入一个任意的关于潜变量 Z 的分布 Q(z|x),称为变分分布。我们在积分中乘除这个分布:
L(θ) = log ∫ Q(z|x) * [ P_θ(x, z) / Q(z|x) ] dz
现在,积分内部可以看作是函数 g(z) = P_θ(x, z) / Q(z|x) 在分布 Q(z|x) 下的期望。因此:
L(θ) = log E_{z~Q(z|x)} [ P_θ(x, z) / Q(z|x) ]
接下来,我们应用Jensen不等式。由于 log 函数是凹函数,根据Jensen不等式,“期望的对数”大于等于“对数的期望”:
log E[·] ≥ E[ log(·) ]
将此不等式应用于我们的似然函数,我们得到:
L(θ) ≥ E_{z~Q(z|x)} [ log( P_θ(x, z) / Q(z|x) ) ]
证据下界(ELBO)的定义
不等式右侧的项,即为证据下界(Evidence Lower BOund, ELBO),记作 J(θ, Q):
ELBO: J(θ, Q) = E_{z~Q(z|x)} [ log P_θ(x, z) - log Q(z|x) ]
在贝叶斯框架中,数据 x 的似然 P_θ(x) 被称为“证据”。ELBO是这个证据的一个下界。通过最大化这个下界,我们间接地最大化了证据(似然)本身。
重要的是,ELBO是模型参数 θ 和变分分布 Q(z|x) 的联合函数。Q(z|x) 通常被称为变分后验分布,因为它*似了真实但难以计算的后验分布 P_θ(z|x)。
最终优化问题
因此,原始的、难以直接求解的最大似然估计问题:
原始问题:max_θ L(θ) = max_θ log P_θ(x)
被转化为一个可以迭代优化的*似问题:
*似问题:max_{θ, Q} J(θ, Q) = max_{θ, Q} E_{z~Q(z|x)} [ log P_θ(x, z) - log Q(z|x) ]
这个优化问题涉及两个部分:
- 优化模型参数 θ,以更好地生成数据。
- 优化变分分布 Q(z|x),以更紧地逼*证据下界,从而更准确地*似真实后验。


本节课中我们一起学*了构建潜变量模型的通用学*框架。我们了解到,直接最大化边际似然是困难的,因此我们引入了变分分布 Q(z|x) 并利用Jensen不等式,构造出了证据下界(ELBO)。最终,我们通过联合优化模型参数 θ 和变分分布 Q 来最大化ELBO,从而*似地实现最大似然估计。这个 max_{θ, Q} ELBO 的公式,是变分自编码器(VAE)、扩散模型等众多生成式AI模型的共同数学基础。
027:高斯混合模型与期望最大化算法

在本节课中,我们将学*生成式AI中一个核心的数学工具——期望最大化算法,并以高斯混合模型为例,展示如何利用该算法学*潜变量模型的参数。我们将理解为什么直接优化某些模型的对数似然函数是困难的,以及如何通过优化其下界来解决问题。
概述
上一节我们介绍了潜变量模型的基本概念和证据下界。本节中,我们来看看如何通过期望最大化算法,具体地求解一个经典的潜变量模型——高斯混合模型。
潜变量模型与优化目标
首先,让我们快速回顾一下核心思想。我们从潜变量模型开始,该模型定义为观测数据变量 x 和某个未观测的潜变量 z 的联合分布或边际分布。我们的目标是找到参数 θ,以最小化真实数据分布 p(x) 与模型分布 p_θ(x) 之间的KL散度。这等价于最大化数据的期望对数似然。
然而,由于我们不知道潜变量 z 的具体取值,目标是在学*模型参数的同时,也学*潜变量 z 的分布。我们引入 q(z|x) 作为给定 x 时潜变量 z 的密度函数。
我们从想要优化的对数似然函数开始:
log p_θ(x) = log ∫ p_θ(x, z) dz
由于对数内部是期望,难以直接计算,我们利用琴生不等式构造了对数似然的一个下界,称为证据下界:
ELBO = E_{z~q(z|x)}[log (p_θ(x, z) / q(z|x))]
现在,我们不再直接最大化对数似然,而是最大化这个下界。证据下界是模型参数 θ 和潜变量分布 q(z|x) 的函数。因此,我们需要同时找出最优的 θ 和 q(z|x)。
期望最大化算法
一个自然的问题是,最大化目标函数的下界是最优的吗?答案是否定的,但这通常是可行的。可以证明,使用一种称为期望最大化的特定算法来优化这个下界,能够保证我们希望优化的似然函数值不会下降。不过,这不能保证达到全局最优。然而,对于潜变量模型,我们无法直接优化对数似然,因此优化其下界是必要的途径。
在任何潜变量生成模型中,我们都要解决以下优化问题:
找到 θ 和 q,以最大化证据下界 ELBO。
解决这个问题的方式在不同模型中有所不同。接下来,我们看一个来自经典机器学*的例子。
高斯混合模型示例
高斯混合模型是一个具有离散潜空间的潜变量模型。这意味着潜变量 z 可以取 M 个值中的一个。
在GMM中,模型分布 p_θ(x) 定义为:
p_θ(x) = Σ_{j=1}^{M} p_θ(z=j) * p_θ(x | z=j)
其中:
p_θ(z=j)是潜变量取第 j 个值的先验概率,我们记作 α_j。p_θ(x | z=j)是给定潜变量取值 j 时,数据 x 的条件分布,我们将其建模为高斯分布:N(x; μ_j, Σ_j)。
因此,GMM的完整形式是:
p_θ(x) = Σ_{j=1}^{M} α_j * N(x; μ_j, Σ_j)
每个数据点 x 的似然是多个高斯分布的线性组合。模型的参数 θ 包括:所有混合权重 {α_1, ..., α_M},所有均值向量 {μ_1, ..., μ_M},以及所有协方差矩阵 {Σ_1, ..., Σ_M}。其中,α_j 在0和1之间,且总和为1。
我们的目标是通过优化ELBO来估计这些参数。
EM算法迭代过程
如何优化同时关于 θ 和 q 的ELBO呢?一种有效的方法是使用迭代算法,交替固定其中一个变量,优化另一个变量。这个算法就是期望最大化算法。
算法步骤如下:
- 随机初始化参数 θ^(0) 和分布 q^(0)。
- 对于迭代次数 t = 0, 1, 2, ...:
- E步(期望步):固定当前参数 θ^(t),找到能最大化ELBO的潜变量分布 q^(t+1)。
- M步(最大化步):固定刚找到的分布 q^(t+1),找到能最大化ELBO的模型参数 θ^(t+1)。
可以证明,按照EM算法迭代,数据的对数似然值 log p_θ(x) 不会下降。
GMM中EM步骤的具体形式
对于高斯混合模型,E步和M步有解析解。
E步:在固定参数 θ 的情况下,最大化ELBO的最优 q(z|x) 恰好是模型的后验分布 p_θ(z|x)。根据贝叶斯定理:
q*(z=j|x) = p_θ(z=j|x) = [α_j * N(x; μ_j, Σ_j)] / [Σ_{k=1}^{M} α_k * N(x; μ_k, Σ_k)]
这计算的是数据点 x 属于第 j 个高斯成分的概率(即“软分配”)。
M步:在固定分布 q(z|x) 为 E 步计算的结果后,通过最大化ELBO来更新参数 θ。这可以通过对ELBO表达式分别关于 α_j, μ_j, Σ_j 求导并令导数为零来实现。
更新公式如下(假设有 N 个数据点 {x_1, ..., x_N}):
- 更新混合权重:
α_j^(new) = (1/N) Σ_{i=1}^{N} q*(z_i=j | x_i) - 更新均值:
μ_j^(new) = [Σ_{i=1}^{N} q*(z_i=j | x_i) * x_i] / [Σ_{i=1}^{N} q*(z_i=j | x_i)] - 更新协方差:
Σ_j^(new) = [Σ_{i=1}^{N} q*(z_i=j | x_i) * (x_i - μ_j^(new))(x_i - μ_j^(new))^T] / [Σ_{i=1}^{N} q*(z_i=j | x_i)]
直观上,E步计算每个数据点对每个高斯成分的“归属度”,M步则根据这个归属度重新计算每个高斯成分的参数(均值、方差)和混合权重。
EM算法的局限与扩展
EM算法能够成功应用于GMM,关键原因在于我们可以精确计算出后验分布 p_θ(z|x)。然而,对于更复杂、更通用的潜变量模型(例如我们想要用强大的神经网络来表示分布),后验分布 p_θ(z|x) 通常是无法直接计算的。
如果 p_θ(z|x) 无法计算,EM算法就失效了,因为我们无法执行E步来得到最优的 q。
这就引出了后续课程的核心问题:如何学*那些后验分布 p_θ(z|x) 未知的潜变量模型?
像变分自编码器和扩散模型这样的神经网络生成模型,正是为了解决这个问题而设计的。它们通过使用神经网络来*似这些难以处理的分布,从而扩展了潜变量模型的学*能力。
总结
本节课中我们一起学*了期望最大化算法及其在高斯混合模型中的应用。我们了解到:
- 对于包含未观测变量的模型,直接优化对数似然是困难的,因此我们转而优化其下界——证据下界。
- EM算法通过交替执行E步(估计潜变量分布)和M步(更新模型参数)来优化ELBO。
- 在高斯混合模型中,E步和M步都有解析解,使得参数估计变得直接。
- EM算法的有效性依赖于后验分布
p_θ(z|x)的可计算性。对于更复杂的模型,我们需要像VAE和扩散模型那样,采用新的方法来*似后验分布。


在接下来的模块中,我们将探讨如何使用变分自编码器来构建神经潜变量模型。
028:变分自编码器(VAE) 🎼

在本节课中,我们将学*变分自编码器(VAE)。VAE是隐变量模型家族的一员,具体来说,属于神经隐变量模型。我们将从隐变量模型的基本定义出发,探讨如何利用神经网络来优化一个关键目标函数——证据下界,并最终理解VAE的架构和工作原理。
隐变量模型回顾
上一节我们介绍了隐变量模型。本节中我们来看看如何用神经网络来构建和优化这类模型。
一个隐变量模型 P_θ(x) 的定义如下:
公式:P_θ(x) = ∫ P_θ(x, z) dz
其中,x 是数据变量,z 是引入的隐变量(通常是K维,且K远小于数据维度D)。模型的数据分布 P_θ(x) 是联合分布 P_θ(x, z) 对隐变量 z 的边际化。
学*模型参数 θ 的目标是,最小化真实数据分布 p(x) 与模型分布 P_θ(x) 之间的分布差异度量。这等价于最大化期望对数似然,即最大似然估计。
然而,对于许多隐变量模型,由于隐变量 z 的引入,直接求解最大似然问题变得难以处理。因此,我们转而优化对数似然的一个下界,即证据下界(ELBO)。
神经隐变量模型的目标
神经隐变量模型有三个主要目标:
- 学*隐变量模型:在无法直接计算真实后验分布
P_θ(z|x)的情况下,学*模型参数θ。 - 实现数据生成:能够从条件数据分布
P_θ(x|z)中采样,从而生成新数据。 - 实现后验推断:能够计算或估计给定数据
x对应的隐变量z的后验分布。这相当于为数据点提取一个低维的特征表示或嵌入。
为了实现这些目标,我们需要一个通用的算法。
优化证据下界(ELBO)
我们从证据下界 J_θ(Q) 开始,它是我们最大化的目标函数:
公式:J_θ(Q) = E_{z∼Q(z|x)} [ log( P_θ(x, z) / Q(z|x) ) ]
其中,Q(z|x) 是隐变量的变分后验分布。优化ELBO时,我们同时寻找最优的模型参数 θ 和变分后验分布 Q。


我们可以利用对数运算法则简化这个表达式:
公式:J_θ(Q) = E_{z∼Q(z|x)} [ log P_θ(x|z) ] - D_{KL}( Q(z|x) || P_θ(z) )
这个形式非常重要。它包含两项:
- 第一项是条件数据似然
P_θ(x|z)的期望。 - 第二项是变分后验
Q(z|x)与隐变量先验P_θ(z)之间的KL散度。
用神经网络表示概率分布
为了用神经网络优化ELBO,我们需要用神经网络来表示分布 P_θ(x|z) 和 Q(z|x)。主要有两种方式:
以下是两种主要的表示方式:
- 确定性表示:神经网络本身输出的是从其建模的分布中采样的样本。例如,GAN的生成器输入一个随机噪声
z,输出一个数据样本x。网络是确定性的,随机性来源于输入。 - 概率性表示:神经网络输出的是其建模分布的参数,而非样本本身。例如,若要建模一个高斯分布
Q(z|x),网络会输入x,并输出该高斯分布的均值μ和方差σ^2。
在变分自编码器中,我们采用概率性神经网络来表示 Q(z|x) 和 P_θ(x|z)。
变分自编码器(VAE)架构
基于以上概念,VAE的架构如下所示。
VAE的目标是最大化ELBO。模型 P_θ(x) 是一个隐变量模型。我们使用两个概率性神经网络:
- 编码器网络:表示变分后验
Q_φ(z|x)。它以数据x为输入,输出分布Q_φ(z|x)的参数(例如,均值和方差)。其网络权重记为φ。 - 解码器网络:表示条件数据似然
P_θ(x|z)。它以隐变量z为输入,输出分布P_θ(x|z)的参数。其网络权重记为θ。
编码器网络被称为“编码器”,因为它将数据 x “编码”到隐空间 z 的参数;解码器网络被称为“解码器”,因为它从隐变量 z “解码”出数据 x 的分布参数。
重要提示:在VAE中,编码器和解码器网络之间没有直接的连接。它们通过采样过程间接关联:编码器输出分布参数后,我们从该分布中采样得到一个 z,再将这个 z 送入解码器。
VAE的优化过程
我们的优化目标是找到最优的编码器参数 φ 和解码器参数 θ,以最大化ELBO:J(θ, φ)。
我们使用梯度下降法(反向传播)来优化。更新规则如下:
对于编码器参数 φ:
公式:φ_{t+1} = φ_t + α * ∇_φ J(θ, φ)
对于解码器参数 θ:
公式:θ_{t+1} = θ_t + β * ∇_θ J(θ, φ)
其中 α 和 β 是学*率。由于是最大化问题,我们执行梯度上升。
总结


本节课中我们一起学*了变分自编码器的核心思想。我们从隐变量模型和证据下界出发,解释了如何用概率性神经网络(编码器和解码器)来参数化关键分布,并概述了通过梯度上升最大化ELBO来联合训练这两个网络的基本流程。VAE巧妙地解决了在未知真实后验时学*生成模型、实现数据生成和后验推断的难题。
029:詹森不等式证明

在本节课中,我们将学*并证明詹森不等式。这个不等式是推导隐变量模型(如变分自编码器)中证据下界(ELBO)的核心数学工具。我们将从凸函数的定义开始,逐步完成离散随机变量情况下的证明。
凸函数定义
在证明詹森不等式之前,我们需要理解凸函数的概念。
一个函数 f: R → R 是凸的,如果对于定义域内任意两点 x 和 y,以及任意标量 λ ∈ [0, 1],以下不等式成立:
f(λx + (1-λ)y) ≤ λf(x) + (1-λ)f(y)
这个定义意味着,连接函数图像上任意两点的线段,总是位于函数图像的上方(或重合)。
詹森不等式陈述
现在,我们来正式陈述詹森不等式。
设 f: R → R 是一个凸函数。设 X 是一个随机变量,其期望 E[X] 存在。那么,詹森不等式指出:
f(E[X]) ≤ E[f(X)]
换句话说,一个凸函数在期望值处的函数值,小于或等于该函数值的期望。
离散随机变量的证明
我们将针对离散随机变量 X 的情况进行证明。证明将采用数学归纳法。
以下是证明的步骤概述:
基础步骤 (n=2)
当只有两个点时,不等式直接由凸函数的定义得出。
归纳假设
我们假设不等式对于 n = k 个点成立。
归纳步骤
我们需要证明,在假设成立的前提下,不等式对于 n = k+1 个点也成立。
详细证明过程
设 x₁, x₂, ..., xₙ ∈ R,以及对应的权重 α₁, α₂, ..., αₙ,满足每个 αᵢ > 0 且它们的总和为1:∑ᵢ αᵢ = 1。
我们希望证明,对于凸函数 f,有:
f(∑ᵢ αᵢ xᵢ) ≤ ∑ᵢ αᵢ f(xᵢ)
1. 基础步骤 (n=2)
当 n=2 时,设 α₁ + α₂ = 1。根据凸函数定义:
f(α₁ x₁ + α₂ x₂) ≤ α₁ f(x₁) + α₂ f(x₂)
这恰好就是我们要证明的不等式形式。基础步骤成立。
2. 归纳假设
假设对于某个整数 k ≥ 2,不等式对 n = k 成立。即,假设对于任意满足 ∑ᵢ₌₁ᵏ αᵢ = 1 的权重,有:
f(∑ᵢ₌₁ᵏ αᵢ xᵢ) ≤ ∑ᵢ₌₁ᵏ αᵢ f(xᵢ)
3. 归纳步骤 (n = k+1)
现在考虑 n = k+1 的情况。我们有权重 α₁, α₂, ..., αₖ₊₁,满足 ∑ᵢ₌₁ᵏ⁺¹ αᵢ = 1。
我们进行如下操作:
- 令 β = αₖ₊₁。那么,剩下的权重之和为 1 - β = ∑ᵢ₌₁ᵏ αᵢ。
- 定义新的归一化权重:α̃ᵢ = αᵢ / (1 - β),其中 i = 1, ..., k。显然,∑ᵢ₌₁ᵏ α̃ᵢ = 1。
- 定义一个新的点:x̃ = ∑ᵢ₌₁ᵏ α̃ᵢ xᵢ。
现在,我们从左边的不等式开始推导:
f(∑ᵢ₌₁ᵏ⁺¹ αᵢ xᵢ) = f( (1-β) * (∑ᵢ₌₁ᵏ α̃ᵢ xᵢ) + β * xₖ₊₁ )
= f( (1-β) x̃ + β xₖ₊₁ )
由于 f 是凸函数,根据定义(n=2的情况),有:
f( (1-β) x̃ + β xₖ₊₁ ) ≤ (1-β) f(x̃) + β f(xₖ₊₁) ... (1)
现在,我们对 f(x̃) 应用归纳假设(因为 x̃ 是 k 个点 xᵢ 以权重 α̃ᵢ 的加权平均,且 ∑ α̃ᵢ = 1):
f(x̃) = f(∑ᵢ₌₁ᵏ α̃ᵢ xᵢ) ≤ ∑ᵢ₌₁ᵏ α̃ᵢ f(xᵢ) ... (2)
将不等式(2)代入不等式(1):
(1-β) f(x̃) + β f(xₖ₊₁) ≤ (1-β) * (∑ᵢ₌₁ᵏ α̃ᵢ f(xᵢ)) + β f(xₖ₊₁)
将 α̃ᵢ = αᵢ / (1-β) 代回:
= (1-β) * (∑ᵢ₌₁ᵏ (αᵢ/(1-β)) f(xᵢ)) + β f(xₖ₊₁)
= ∑ᵢ₌₁ᵏ αᵢ f(xᵢ) + αₖ₊₁ f(xₖ₊₁)
= ∑ᵢ₌₁ᵏ⁺¹ αᵢ f(xᵢ)
因此,我们得到了一个完整的链条:
f(∑ᵢ₌₁ᵏ⁺¹ αᵢ xᵢ) ≤ ∑ᵢ₌₁ᵏ⁺¹ αᵢ f(xᵢ)
这正好是 n = k+1 时的不等式。归纳步骤完成。
根据数学归纳法原理,詹森不等式对任意正整数 n 都成立。
总结
本节课中,我们一起学*了詹森不等式的证明。
- 我们首先回顾了凸函数的定义。
- 然后,我们正式陈述了詹森不等式:对于一个凸函数 f 和随机变量 X,有 f(E[X]) ≤ E[f(X)]。
- 接着,我们针对离散随机变量的情况,使用数学归纳法完成了严谨的证明。证明的关键在于巧妙地构造归一化权重和应用归纳假设。


这个不等式是理解生成式AI中许多优化目标(如最大化ELBO)的基石。对于连续随机变量的情况,证明思路类似,但会涉及积分而非求和,你可以尝试将其作为练*。
030:高斯混合模型与EM算法

在本节课中,我们将学*高斯混合模型(GMM)以及用于其参数估计的期望最大化(EM)算法。我们将从定义问题开始,逐步推导EM算法的E步和M步,并最终总结出完整的算法流程。
问题定义
高斯混合模型是潜变量模型的一个特例,其中潜变量 Z 是离散的。我们假设数据点 X 是从某个数据分布中独立同分布采样得到的。
- 观测变量:X ∈ ℝ^d
- 潜变量:Z 是一个离散随机变量,取值范围为
对于任意一个数据点 x_i,其概率可以表示为K个高斯分布的加权和:
p(x_i) = Σ_{k=1}^{K} π_k * N(x_i | μ_k, Σ_k)
其中:
- π_k 是混合系数,满足 Σ_{k=1}^{K} π_k = 1。
- N(x_i | μ_k, Σ_k) 是第k个高斯分布的概率密度函数。
责任值
在推导算法之前,我们需要引入一个关键概念:责任值 γ(z_k)。它表示在给定观测数据 x_i 的条件下,该数据点由第k个高斯分量生成的后验概率。
根据贝叶斯定理,责任值的计算公式为:
γ(z_{ik}) = p(z_k | x_i) = [π_k * N(x_i | μ_k, Σ_k)] / [Σ_{j=1}^{K} π_j * N(x_i | μ_j, Σ_j)]
这个值在E步中会被计算。
对数似然函数与EM算法框架
我们的目标是最大化观测数据的对数似然函数:
log p(X) = Σ_{i=1}^{N} log [ Σ_{k=1}^{K} π_k * N(x_i | μ_k, Σ_k) ]
直接优化这个函数是困难的,因为对数内部有求和。EM算法通过迭代方式解决这个问题,每次迭代分为两步:
- E步(期望步):固定模型参数(π, μ, Σ),计算每个数据点对每个高斯分量的责任值 γ(z_{ik})。
- M步(最大化步):固定责任值 γ(z_{ik}),重新估计模型参数(π, μ, Σ)以最大化期望似然。
上一节我们定义了问题和责任值,本节中我们来看看如何通过EM算法的M步来更新模型参数。
M步:参数更新推导
在M步中,我们需要更新混合系数 π_k、均值 μ_k 和协方差 Σ_k。这是一个带约束(Σ π_k = 1)的优化问题,需要使用拉格朗日乘子法。
我们构建拉格朗日函数 L:
L = Σ_{i=1}^{N} log [ Σ_{k=1}^{K} π_k * N(x_i | μ_k, Σ_k) ] + λ ( Σ_{k=1}^{K} π_k - 1 )
接下来,我们分别对 π_k、μ_k 和 Σ_k 求偏导并令其为零。
1. 更新混合系数 π_k
对 L 关于 π_k 求偏导并置零,经过推导可得:
π_k = (1/N) * Σ_{i=1}^{N} γ(z_{ik}) = N_k / N
其中 N_k = Σ_{i=1}^{N} γ(z_{ik}),可以理解为属于第k个分量的数据点的“有效”数量。
2. 更新均值 μ_k
对 L 关于 μ_k 求偏导并置零,经过推导可得:
μ_k = (1/N_k) * Σ_{i=1}^{N} γ(z_{ik}) * x_i
新的均值是所有数据点的加权平均,权重是每个数据点属于该分量的责任值。
3. 更新协方差 Σ_k
对 L 关于 Σ_k 求偏导并置零,经过类似的推导可得:
Σ_k = (1/N_k) * Σ_{i=1}^{N} γ(z_{ik}) * (x_i - μ_k)(x_i - μ_k)^T
新的协方差是基于新的均值 μ_k 计算的数据点的加权协方差。
完整的EM算法流程
以下是用于高斯混合模型的EM算法的标准步骤:
- 初始化:设置均值 μ_k、协方差 Σ_k 和混合系数 π_k 的初始值。计算初始对数似然值。
- E步:使用当前参数计算责任值 γ(z_{ik})。
- γ(z_{ik}) = [π_k * N(x_i | μ_k, Σ_k)] / [Σ_{j=1}^{K} π_j * N(x_i | μ_j, Σ_j)]
- M步:使用当前责任值重新估计参数。
- N_k = Σ_{i=1}^{N} γ(z_{ik})
- μ_k^{new} = (1/N_k) Σ_{i=1}^{N} γ(z_{ik}) x_i
- Σ_k^{new} = (1/N_k) Σ_{i=1}^{N} γ(z_{ik}) (x_i - μ_k^{new})(x_i - μ_k{new})T
- π_k^{new} = N_k / N
- 评估:计算新的对数似然值。
- log p(X) = Σ_{i=1}^{N} log [ Σ_{k=1}^{K} π_k^{new} * N(x_i | μ_k^{new}, Σ_k^{new}) ]
- 检查收敛:如果对数似然值或参数的变化未收敛(例如,小于某个阈值),则返回第2步(E步)。否则,算法停止。
总结
本节课中我们一起学*了高斯混合模型及其核心训练算法——期望最大化算法。
- 我们首先定义了GMM,它是一个由多个高斯分布加权组合而成的概率模型。
- 然后,我们引入了“责任值”这一关键概念,它度量了每个数据点属于各个高斯分量的概率。
- 接着,我们详细推导了EM算法的M步,展示了如何利用责任值来更新模型的三个参数:混合系数 π_k、均值 μ_k 和协方差 Σ_k。
- 最后,我们给出了从初始化到收敛的完整EM算法迭代步骤。


理解GMM和EM算法为学*更复杂的生成式模型(如变分自编码器)奠定了重要的数学基础。
031:训练VAE-重参数化方法

在本节课中,我们将学*变分自编码器训练过程中的一个核心技巧——重参数化。我们将了解为什么直接计算编码器参数的梯度会遇到困难,以及如何通过重参数化技巧巧妙地解决这个问题。
数据流回顾
在深入梯度计算之前,让我们先回顾一下VAE内部的数据流,以便更好地理解后续关于梯度和计算的讨论。
在VAE中,我们有两个神经网络:编码器和解码器。数据样本 x 作为输入传递给编码器网络,编码器网络输出潜在分布 Q_φ(z|x) 的参数。由于这是一个概率神经网络,我们需要通过该分布进行采样来获得一个 z 的样本。具体来说,我们利用编码器网络预测的分布参数,采样得到一个或多个 z 点,这个采样得到的潜在向量 z 将作为输入传递给解码器网络。解码器网络随后输出条件数据分布 p_θ(x|z) 的参数。
以下是三个关键步骤:
- 数据输入编码器,得到 Q_φ(z|x) 的参数。
- 在神经网络外部进行采样,从潜在分布中获得样本 z。
- 将采样得到的 z 作为输入传递给解码器网络,解码器输出 p_θ(x|z) 的参数。
这就是数据在VAE中流动的过程。请注意,流程中存在一个采样步骤,它发生在神经网络外部。这是一个需要关注的关键点,因为它会在梯度计算中带来挑战。采样过程不是一个网络,而是发生在编码器和解码器神经网络之外的一个操作。
梯度计算问题
现在让我们开始进行梯度计算。我们需要查看损失函数 L(即ELBO),并计算该损失函数相对于编码器参数 φ 和解码器参数 θ 的梯度。
让我们回顾一下损失函数。我们的损失函数包含两项:第一项是条件期望数据似然,第二项是KL散度。为了便于参考,我们将损失函数写在这里:
L(θ, φ; x) = E_{z~Q_φ(z|x)}[log p_θ(x|z)] - D_{KL}(Q_φ(z|x) || p(z))
我们需要计算这个表达式相对于编码器参数 φ 和解码器参数 θ 的梯度。因此,总共有四个梯度项需要计算:
- ELBO第一项相对于 φ 的梯度。
- ELBO第二项相对于 φ 的梯度。
- ELBO第一项相对于 θ 的梯度。
- ELBO第二项相对于 θ 的梯度。
让我们从计算相对于编码器参数 φ 的梯度开始。这意味着我们需要计算第一项(期望项)相对于 φ 的梯度。
这个期望项的形式是 E_{z~Q_φ(z|x)}[log p_θ(x|z)]。请注意,期望是针对分布 Q_φ(z|x) 计算的,并且我们条件依赖的变量 z 也是从 Q_φ(z|x) 中采样得到的。这个项以两种方式依赖于 φ:一是期望所基于的分布 Q_φ,二是条件变量 z 的采样也依赖于 Q_φ。
为了简化表示,我们用一个通用形式来描述这个问题。假设我们需要计算函数 f_ψ(v) 相对于分布 p_ψ(v) 的期望的梯度,且梯度是针对参数 ψ 的。在我们的具体问题中,v 对应 z,p_ψ(v) 对应 Q_φ(z|x),f_ψ(v) 对应 log p_θ(x|z)。
根据定义,期望是积分:∇_ψ E_{v~p_ψ(v)}[f_ψ(v)] = ∇_ψ ∫ f_ψ(v) p_ψ(v) dv。梯度是线性算子,可以移到积分内部:∫ ∇_ψ [f_ψ(v) p_ψ(v)] dv。
现在应用乘积求导法则:
∫ [ (∇_ψ f_ψ(v)) p_ψ(v) + f_ψ(v) (∇_ψ p_ψ(v)) ] dv
第一项 ∫ (∇_ψ f_ψ(v)) p_ψ(v) dv 可以写成一个期望:E_{v~p_ψ(v)}[∇_ψ f_ψ(v)]。这一项可以通过从分布 p_ψ(v) 中采样并计算样本平均值来*似估计。
然而,第二项 ∫ f_ψ(v) (∇_ψ p_ψ(v)) dv 不是一个期望。因为 ∇_ψ p_ψ(v) 是概率密度函数的梯度,它本身不是一个概率密度函数(其积分不一定为1)。因此,这个积分不能解释为某个随机变量的期望,也就无法使用样本平均值来*似计算。
这意味着我们需要的梯度 ∇_φ E_{z~Q_φ(z|x)}[log p_θ(x|z)] 无法直接计算。从数据流图来看,问题的根源在于编码器和解码器之间的采样步骤。如果我们将整个流程(编码器 -> 采样 -> 解码器)视为一个计算图,为了将梯度从解码器输出反向传播回编码器输入,梯度必须穿过采样操作。但采样是一个不可微分的操作,因此梯度无法通过。
重参数化技巧
为了解决这个梯度无法计算的问题,原始VAE论文的作者提出了一种称为重参数化技巧的解决方案。
重参数化是统计学中的一种常用技术,其核心思想是:使用一个确定性函数和另一个辅助随机变量来表示原随机变量。
具体思路是,将我们从中采样的分布 p_ψ(v),用另一个独立于参数 ψ 的辅助随机变量来表示。
假设存在一个辅助随机变量 ε,它服从某个分布 p(ε),且 p(ε) 独立于参数 ψ。同时,存在一个确定性函数 g_ψ,使得原随机变量 v 可以表示为 v = g_ψ(ε)。
那么,原来的期望可以重写为:
E_{v~p_ψ(v)}[f_ψ(v)] = E_{ε~p(ε)}[f_ψ(g_ψ(ε))]
这个等式的依据是统计学中的Law of the Unconscious Statistician (LOTUS)。它表明,一个随机变量函数的期望,可以通过该随机变量的变换以及变换后变量的分布来计算。
这样做的好处是什么?现在,我们需要计算的梯度变成了:
∇_ψ E_{v~p_ψ(v)}[f_ψ(v)] = ∇_ψ E_{ε~p(ε)}[f_ψ(g_ψ(ε))]
关键的变化在于,期望现在是对分布 p(ε) 求取的,而 p(ε) 不依赖于参数 ψ。因此,我们可以将梯度算子移入期望内部(因为期望运算不再依赖于 ψ):
= E_{ε~p(ε)}[∇_ψ f_ψ(g_ψ(ε))]
现在,这个梯度可以计算了!因为它是一个期望,我们可以通过从 p(ε) 中采样 ε,计算内部函数 ∇_ψ f_ψ(g_ψ(ε)) 的梯度,然后取样本平均值来*似:
≈ (1/M) Σ_{j=1}^{M} ∇_ψ f_ψ(g_ψ(ε^{(j)})), 其中 ε^{(j)} ~ p(ε)
本质上,重参数化技巧将采样过程的随机性“转移”到了一个与模型参数无关的辅助变量 ε 上。原来的随机变量 v 变成了一个关于 ε 和参数 ψ 的确定性函数 g_ψ(ε)。这样,梯度就可以顺利地从 f_ψ 通过确定性的函数 g_ψ 反向传播到参数 ψ,而无需经过不可微的采样操作。
重参数化示例
以下是两种常见的重参数化方法示例:
1. 仿射变换(适用于高斯分布)
这是在VAE中最常用的方法。我们通常假设潜在后验分布 Q_φ(z|x) 是一个高斯分布,其均值 μ_φ(x) 和方差 σ_φ²(x) 由编码器网络输出。
我们可以如下进行重参数化:
令 ε ~ N(0, I),即标准正态分布。
定义 z = μ_φ(x) + σ_φ(x) ⊙ ε,其中 ⊙ 表示逐元素相乘(若为多维)。
那么,z 将服从分布 N(μ_φ(x), diag(σ_φ²(x)))。这里的确定性函数 g 就是仿射变换:g_φ(ε, x) = μ_φ(x) + σ_φ(x) ⊙ ε。
2. 逆变换采样(适用于任意连续分布)
这是一种更通用的方法。设随机变量 Z 的累积分布函数为 F_z(z)。
令 ε ~ Uniform(0, 1),即均匀分布。
定义 z = F_z^{-1}(ε),即CDF的逆函数。
那么,z 将服从原来的分布。这里的确定性函数 g 就是逆CDF函数:g(ε) = F_z^{-1}(ε)。
在VAE的常规实践中,通常采用第一种方法,即假设 Q_φ(z|x) 为高斯分布,并使用仿射变换进行重参数化。选择高斯分布和这种重参数化方式主要是为了计算和实现的便利性。
总结
本节课中,我们一起学*了变分自编码器训练的关键——重参数化技巧。
我们首先回顾了VAE的数据流,指出了编码器与解码器之间不可微的采样步骤是梯度计算的主要障碍。直接计算损失函数中期望项相对于编码器参数的梯度时,会得到一个无法通过采样估计的项,导致梯度无法传播。
为了解决这个问题,我们引入了重参数化技巧。其核心思想是将原始随机变量 z 表示为一个确定性函数 g_φ(ε) 和一个辅助随机变量 ε 的组合,其中 ε 来自一个与参数 φ 无关的固定分布。这样,随机性被转移到了 ε 上,而 z 相对于 φ 的依赖性变成了确定性的、可微的。通过这种方式,梯度可以顺利通过计算图进行反向传播,使得编码器网络的训练成为可能。
我们还介绍了两种具体的重参数化方法:适用于高斯分布的仿射变换和适用于任意连续分布的逆变换采样法。在VAE的实际实现中,普遍采用高斯分布假设配合仿射变换的方法。


理解重参数化技巧是掌握VAE训练原理的重要一步,它巧妙地将概率模型中的采样问题与神经网络的可微分训练结合了起来。
032:训练变分自编码器 (VAE) 🧠

在本节课中,我们将学*如何训练变分自编码器。我们将详细探讨损失函数的计算、梯度传播,以及解决训练难题的关键技术——重参数化技巧。
概述
变分自编码器的训练目标是最大化证据下界。这个目标函数包含两项:第一项是重构数据的对数似然期望,第二项是编码器输出的分布与先验分布之间的KL散度。训练的核心挑战在于计算第一项对编码器参数的梯度,因为其中涉及对随机变量的采样。我们将使用重参数化技巧来解决这个梯度计算问题。
重参数化技巧
上一节我们介绍了VAE的损失函数。本节中我们来看看如何计算其梯度,特别是处理涉及采样的期望项。
给定一个输入数据样本 Xi,它通过编码器网络,得到均值 μ_φ(Xi) 和方差 σ_φ(Xi)。然后,我们从标准正态分布 N(0, I) 中采样一个随机变量 ε。潜在变量 Z 通过以下重参数化公式得到:
Z = μ_φ(Xi) + σ_φ(Xi) * ε
这个操作将原本从 q_φ(z|x) 的采样,转换成了从一个与网络参数 φ 无关的固定分布 p(ε) 中采样 ε,再通过一个确定性函数 g 得到 Z。这使得梯度可以顺利通过采样操作反向传播。
前向传播流程
以下是训练过程中,针对一个数据样本 Xi 的前向传播步骤:
- 编码:将
Xi输入编码器网络,得到均值μ_φ(Xi)和方差σ_φ(Xi)。 - 采样:从标准正态分布
N(0, I)中独立采样M个随机变量ε_1, ..., ε_M。此步骤在神经网络外部进行。 - 重参数化:对于每个
ε_j,计算对应的潜在向量Z_j = μ_φ(Xi) + σ_φ(Xi) * ε_j。 - 解码:将每个
Z_j输入解码器网络,得到重构输出x̂_θ(Z_j)。解码器输出被解释为给定Z时x的分布(例如高斯分布)的参数(如均值)。 - 计算损失:计算损失函数的两部分。
- 重构项:计算负对数似然的蒙特卡洛估计:
L_recon = -1/M * Σ_j ||Xi - x̂_θ(Z_j)||²(假设解码输出为高斯分布且方差为单位矩阵)。 - KL散度项:计算
q_φ(z|Xi)(均值为μ_φ(Xi),方差为σ_φ(Xi)的高斯分布)与先验分布p(z)(通常为标准正态分布N(0, I))之间的KL散度。此项有解析解:
L_KL = -1/2 * [log det(σ_φ(Xi)) - K + tr(σ_φ(Xi)) + ||μ_φ(Xi)||²]
其中K是潜在空间的维度。
- 重构项:计算负对数似然的蒙特卡洛估计:
反向传播与参数更新
损失函数的梯度需要分别对编码器参数 φ 和解码器参数 θ 进行计算。
更新编码器参数 (φ)
我们需要计算总损失 L = L_recon + L_KL 对 φ 的梯度。
- 重构项梯度:计算
L_recon对φ的梯度。由于重参数化,梯度可以从L_recon反向传播,经过解码器网络(此时θ固定),再通过重参数化操作Z = μ_φ(Xi) + σ_φ(Xi) * ε,最终到达编码器输出μ_φ和σ_φ。 - KL散度项梯度:
L_KL对φ的梯度可以直接根据其解析公式计算,因为它只依赖于编码器的输出μ_φ和σ_φ。 - 合并梯度:将上述两项梯度相加,得到总损失对
φ的梯度。 - 反向传播:将这个总梯度继续反向传播通过编码器网络,更新编码器参数
φ。
更新解码器参数 (θ)
我们需要计算总损失 L 对 θ 的梯度。注意,L_KL 项与 θ 无关,其梯度为零。
- 重构项梯度:计算
L_recon对θ的梯度。在前向传播中,Z_j是通过固定φ得到的。在反向传播时,梯度从L_recon直接反向传播通过解码器网络(此时φ固定,θ为变量),更新解码器参数θ。梯度不需要经过编码器或重参数化步骤。
训练步骤总结
本节课中我们一起学*了变分自编码器的完整训练流程:
- 对一个数据样本
Xi执行前向传播,获得编码器输出、采样潜在变量、解码器输出,并计算重构损失和KL散度损失。 - 更新编码器:
- 计算重构损失对编码器参数
φ的梯度(利用重参数化技巧使梯度可通)。 - 计算KL散度损失对
φ的解析梯度。 - 将两个梯度相加,反向传播更新
φ。
- 计算重构损失对编码器参数
- 更新解码器:
- 计算重构损失对解码器参数
θ的梯度(KL散度项梯度为零)。 - 反向传播该梯度以更新
θ。
- 计算重构损失对解码器参数
- 对数据集中的所有样本重复步骤1-3,完成一个训练周期(epoch)。迭代多个周期直至模型收敛。


通过交替更新编码器和解码器,VAE学会了将数据压缩到有结构的潜在空间,并能从该空间生成新的数据样本。在下一模块中,我们将探讨如何使用训练好的VAE进行数据生成和特征提取。
033:使用训练好的VAE进行推理

概述
在本节课中,我们将学*如何使用训练好的变分自编码器进行推理。我们将回顾VAE的训练过程,然后探讨其在数据生成和潜在后验推断等任务中的应用,并简要介绍一些针对VAE缺点的改进方法。
回顾:变分自编码器的训练
上一节我们介绍了如何训练变分自编码器。本节中,我们先快速回顾其训练流程。
VAE在架构上包含两个用于*似分布的神经网络。
- 编码器网络:输入一个数据样本
x,输出潜在后验分布Q_φ(z|x)的参数。 - 解码器网络:输入一个从潜在后验分布
Q_φ(z|x)中采样的z,输出数据似然分布P_θ(x|z)的参数。
在典型实现中,Q_φ(z|x) 被假设为一个高斯分布,其均值 μ_φ(x) 和方差 Σ_φ(x) 由编码器预测。方差矩阵通常被设定为对角矩阵,因此编码器只需输出K维的均值向量和对角方差向量。
为了从 Q_φ(z|x) 中采样 z 并允许梯度反向传播,我们使用重参数化技巧:
z = μ_φ(x) + Σ_φ(x)^(1/2) * ε,其中 ε 采样自标准正态分布 N(0, I)。
VAE的优化目标是最大化证据下界,其损失函数为:
L(θ, φ; x) = E_{z~Q_φ(z|x)}[log P_θ(x|z)] - D_{KL}(Q_φ(z|x) || P(z))
其中,P(z) 是先验分布,通常设为标准正态分布 N(0, I)。KL散度项有解析解:
D_{KL} = -1/2 * Σ_{j=1}^{K} (1 + log(σ_j^2) - μ_j^2 - σ_j^2)
我们通过梯度下降法交替更新编码器参数 φ 和解码器参数 θ。
使用训练好的VAE进行推理
现在,假设VAE已经训练完成。我们来看看如何将其用于不同的推理任务。
1. 数据生成(采样)
VAE本质上是一个潜在变量生成模型,因此可用于数据生成。
以下是使用VAE进行采样的步骤:
- 从先验分布采样:采样一个新的潜在变量
z_new来自先验分布P(z),即标准正态分布N(0, I)。 - 通过解码器生成:将
z_new输入到训练好的解码器网络中。解码器输出x̂_θ(z_new),即条件分布P_θ(x|z_new)的参数(通常是均值)。
原理:在训练过程中,损失函数的KL散度项会迫使所有 x 对应的后验分布 Q_φ(z|x) 都接*先验 P(z)(即 N(0, I))。因此,在推理时,直接从 N(0, I) 采样 z 等价于从某个 Q_φ(z|x) 采样。解码器已学会将这样的 z 映射回数据空间。
得到解码器的输出 x̂_θ(z_new) 后,有两种方式获得新数据点:
- 直接使用均值:将
x̂_θ(z_new)本身作为生成的数据点。 - 从分布中采样:因为
P_θ(x|z)通常被建模为以x̂_θ(z)为均值、单位矩阵为方差的高斯分布,所以我们可以从N(x̂_θ(z_new), I)中采样一个点x_new作为生成的数据。
注意:在数据生成任务中,我们只使用解码器,编码器不被使用。
2. 潜在后验推断(特征提取)
VAE也可以用作特征提取器,为输入数据计算一个低维的潜在表示(嵌入)。
以下是使用VAE进行特征提取的步骤:
- 使用编码器:将测试数据点
x_test输入到训练好的编码器网络中。 - 获取分布参数:编码器输出后验分布
Q_φ(z|x_test)的参数,即均值向量μ_φ(x_test)和对角方差矩阵Σ_φ(x_test)。 - 计算嵌入:数据点
x_test的潜在嵌入(特征向量)可以通过以下方式获得:- 使用均值:直接取
μ_φ(x_test)作为嵌入向量。这是一种常见选择。 - 从后验采样:从分布
N(μ_φ(x_test), Σ_φ(x_test))中采样一个点z_test作为嵌入。对于同一个x_test,可以采样多次得到多个嵌入。
- 使用均值:直接取
应用:得到的低维嵌入 z_test 可以用于多种下游任务,例如:
- 作为特征输入给分类器。
- 在其上构建新的生成模型(例如,在潜在空间而非原始数据空间进行生成)。
优势:通过编码器,原始高维数据(D维)被压缩到一个更低维(K维,K << D)的潜在空间,获得了数据的紧凑表示。
总结
本节课中,我们一起学*了如何使用训练好的变分自编码器进行推理。
- 数据生成:我们利用VAE的解码器部分。通过从标准正态先验分布中采样一个潜在变量
z,并将其输入解码器,可以生成新的数据样本。这体现了VAE作为生成模型的核心能力。 - 特征提取:我们利用VAE的编码器部分。通过将数据输入编码器,可以获得其对应的潜在后验分布的参数(均值和方差),进而得到数据在低维潜在空间中的嵌入表示。这种紧凑的特征可用于各种后续分析任务。


这种“编码器用于特征提取,解码器用于数据生成”的模式,是编码器-解码器类模型的通用范式。
034:Beta-VAE

概述
在本节课中,我们将学*变分自编码器(VAE)的一个改进版本——Beta-VAE。我们将探讨标准VAE中存在的“后验坍塌”问题,理解其成因,并学*如何通过引入一个超参数β来平衡重构质量与潜在空间的正则化,从而解决该问题。
从标准VAE到其改进
上一节我们介绍了变分自编码器的基本框架。本节中,我们来看看对标准VAE的第一个重要改进。
后验坍塌问题
考虑标准VAE的工作流程。存在一个编码器网络和一个解码器网络。给定输入数据x,编码器会输出后验分布Q_φ(z|x)的参数。解码器则从该分布中采样一个z,并以此生成P_θ(x|z)的参数。
在VAE的损失函数(ELBO)中,第二项是KL散度项,用于衡量Q_φ(z|x)与先验分布P(z)(通常是标准正态分布)之间的差异。这项损失被最小化。
如果这项损失被过度优化,会发生什么?对于所有输入x,Q_φ(z|x)都会被强制变得与P(z)完全相同。例如,无论输入什么x,编码器都被训练为输出均值为0、方差为1的正态分布。
这种现象被称为后验坍塌。因为潜在后验分布对于所有输入x都“坍塌”成了同一个特定的分布(即先验分布)。
后验坍塌会带来一个问题:解码器将难以区分不同的输入样本。假设有两个输入样本x_i和x_j,由于它们的后验分布Q_φ(z|x_i)和Q_φ(z|x_j)都相同,从这两个分布中采样得到的潜在变量z_i和z_j将来自完全相同的分布。因此,解码器在尝试重构原始输入x_i和x_j时会遇到困难,导致重构损失(ELBO的第一项)性能下降。
解决方案:将VAE视为正则化自编码器
为了解决后验坍塌问题,我们首先需要重新审视VAE的损失函数。VAE的损失函数J(θ, φ)可以写作:
J(θ, φ) = E[log P_θ(x|z)] - D_KL(Q_φ(z|x) || P(z))
- 第一项是重构项:它促使解码器从潜在变量
z中准确地重构出原始输入x。这体现了“自编码”的核心思想——将数据编码到潜在空间再解码回来。 - 第二项是KL散度项:它促使潜在变量的分布
Q_φ(z|x)接*我们预设的先验分布P(z)。这可以看作是对潜在空间的一种正则化。
因此,VAE可以视为一个带有潜在空间正则化的自编码器,有时也被称为正则化自编码器。
在经典的机器学*正则化中,损失函数通常形如:
L(θ) = [成本函数] + λ * [正则化函数]
其中,λ是一个正则化常数,用于控制正则化的强度。λ值越大,模型越倾向于被正则化(可能欠拟合);λ值越小,模型越倾向于拟合数据(可能过拟合)。λ在偏差-方差权衡中起着关键作用。
观察标准VAE的损失函数,我们发现它缺少这样一个显式的正则化常数λ。这意味着我们无法灵活地控制重构损失和正则化损失之间的权衡,从而难以避免后验坍塌。
Beta-VAE:引入权衡参数
一个简单的改进方法就是为KL散度项添加一个正则化常数β。修改后的损失函数为:
J_β(θ, φ) = E[log P_θ(x|z)] - β * D_KL(Q_φ(z|x) || P(z))
这个公式就是Beta-VAE。其中,β是一个介于0和1之间的超参数。
- 当
β = 1时,就是标准的VAE。 β成为一个可以调节的设计选择。
以下是β值的影响:
- 较高的
β值:赋予KL散度项更大的权重,强调潜在空间必须严格符合先验分布。这可能导致后验坍塌。然而,这也有一个好处:由于训练时Q_φ(z|x)被强制接*P(z),在生成新数据时,直接从先验分布P(z)中采样并输入解码器会得到更好的效果。因此,较高的β有利于生成质量。 - 较低的
β值:降低了对KL散度项的要求,允许潜在空间保留更多关于输入x的信息。这减轻了后验坍塌,使得不同输入对应的后验分布更容易区分,从而提高了重构质量。但代价是,由于Q_φ(z|x)与P(z)差异较大,从先验P(z)中采样生成的样本质量可能会下降。
因此,β在重构质量与生成质量之间提供了一个明确的权衡杠杆。
在实践中,当需要实现VAE时,通常会实现Beta-VAE,并将β作为一个超参数。开发者会根据具体应用的需求(是更看重数据重构的保真度,还是更看重生成样本的多样性/质量),在验证集上调整β的值,以达到最佳平衡。


总结
本节课中,我们一起学*了Beta-VAE。我们首先分析了标准VAE中后验坍塌问题的成因,即KL散度项过度正则化导致潜在空间失去区分度。接着,我们通过将VAE解读为一种正则化自编码器,引出了通过添加超参数β来控制正则化强度的解决方案。最后,我们详细探讨了β值如何影响模型在数据重构和样本生成之间的权衡。Beta-VAE是理解和应用VAE家族模型的一个重要基础。
生成式AI的数学基础:P35:矢量量化变分自编码器(VQ-VAE)

概述
在本节课中,我们将学*变分自编码器(VAE)的一个重要改进版本——矢量量化变分自编码器(VQ-VAE)。我们将了解其核心思想、工作原理、训练方法以及如何进行推断和生成。
核心思想:离散的潜在空间
上一节我们介绍了标准VAE,其潜在空间是连续的。本节中我们来看看VQ-VAE的核心改进:它将潜在空间离散化。
在标准VAE中,潜在变量z通常来自连续分布(如标准正态分布)。然而,对于图像、语音、文本等实际数据,用离散的潜在符号来表示更为直观。例如:
- 语音 可以由有限的音素组合构成。
- 文本 本身就是由离散的字符或词元组成。
- 图像 也可以看作由线条、形状、颜色等基本元素组合而成。
因此,VQ-VAE将潜在空间设计为离散的,这更符合许多实际数据的本质。
VQ-VAE的架构与工作原理
VQ-VAE同样包含编码器和解码器,但与标准VAE不同,其编码器是确定性的。
以下是VQ-VAE的工作流程:
- 编码:编码器
E接收输入数据点x,输出一个向量z_e(x)。 - 矢量量化:系统维护一个可学*的潜在字典
L,其中包含M个K维向量:{z_1, z_2, ..., z_M}。量化过程是找到字典中与z_e(x)最接*的向量:
j* = argmin_j || z_e(x) - z_j ||
然后,用该向量z_j*作为量化后的潜在表示z_q(x)。 - 解码:解码器
D接收量化后的向量z_q(x),并尝试重建原始输入,输出x'。
在实践中,为了更有效地表示复杂数据(如图像),编码器的输出 z_e(x) 通常是一个 K x P 的矩阵,其中每一列都是一个 K 维向量。随后,对矩阵的每一列独立进行上述矢量量化操作,最终得到一个由 P 个离散字典向量组成的矩阵,作为解码器的输入。
如何训练VQ-VAE
VQ-VAE的训练目标函数包含两部分:
损失函数公式:
L = || x - D(z_q(x)) ||^2 + || sg[z_e(x)] - z_q ||^2 + β || z_e(x) - sg[z_q] ||^2
以下是损失函数各部分的解释:
- 重建损失:
|| x - D(z_q(x)) ||^2,确保解码器能准确重建输入。 - 字典学*损失:
|| sg[z_e(x)] - z_q ||^2,使用停止梯度操作符sg[...],将编码器输出视为常数,更新字典向量z_j,使其向编码器输出靠*。 - 承诺损失:
β || z_e(x) - sg[z_q] ||^2,同样使用sg[...],将量化向量z_q视为常数,促使编码器输出靠*其被量化到的字典向量。β是一个超参数。
在训练过程中,我们不仅优化编码器 E 和解码器 D 的参数,还会通过梯度下降更新潜在字典 L 中的所有向量。
如何使用VQ-VAE进行推断
训练好VQ-VAE后,我们可以用它进行两种主要操作:嵌入提取和生成。
嵌入提取(后验推断)
这个过程用于获取输入数据的离散潜在表示。
- 将输入
x_test输入训练好的编码器E,得到输出z_e(x_test)。 - 对该输出的每一列(若为矩阵),在潜在字典
L中寻找最*的向量进行替换(矢量量化)。 - 得到的量化矩阵
z_q(x_test)就是该数据的嵌入表示。
生成新样本
在VQ-VAE中生成新样本比在标准VAE中更复杂,因为我们没有对潜在空间 z_q 显式地定义一个简单的先验分布(如标准正态分布)。
生成新样本的步骤是:
- 学*潜在空间的分布:首先,用训练好的VQ-VAE编码器处理所有训练数据,收集得到的所有量化潜在向量
{z_q},形成一个数据集。 - 训练一个额外的生成模型:在这个
{z_q}数据集上,训练另一个生成模型(例如自回归模型、GAN或另一个VAE)。这个模型学会了如何生成新的、合理的z_q样本。 - 采样与解码:从新训练好的生成模型中采样出一个新的
z_q样本,将其输入VQ-VAE的解码器D,即可生成新的数据点x_new。
由于这个两步过程较为繁琐,VQ-VAE在现实中更多被用作强大的特征提取器,而非直接的生成模型。例如,在著名的图像生成模型Stable Diffusion中,就使用了一个预训练的VQ-VAE的潜在空间作为其扩散过程的起点。
总结
本节课我们一起学*了矢量量化变分自编码器(VQ-VAE)。
- 其核心思想是将连续VAE的潜在空间离散化,通过一个可学*的字典进行矢量量化。
- 我们分析了它的架构、训练目标函数(包含重建、字典学*和承诺损失)以及训练过程。
- 最后,我们探讨了如何使用它进行嵌入提取,以及通过训练一个额外的生成模型来生成新样本的间接方法。VQ-VAE因其强大的特征表示能力,常作为更复杂生成模型(如扩散模型)的组件。


接下来,我们将进入另一个前沿的生成模型家族——去噪扩散概率模型(扩散模型),它实际上是层次化VAE的一个特例,并构成了当前图像生成领域许多顶尖模型的基础。
036:变分自编码器的实现 🎼

在本教程中,我们将学*如何实现变分自编码器。我们将回顾VAE的核心概念,详细解析其前向传播与反向传播过程,并最终通过一个具体的代码示例来展示如何构建和训练一个VAE模型。
变分自编码器回顾
上一节我们介绍了变分自编码器的基本概念。VAE是一种隐变量模型。给定数据集 D = {x_i}_{i=1}^n,其中 x_i 是从真实数据分布 p(x) 中独立同分布采样得到的,x_i ∈ R^d。
隐变量模型需要满足的条件是:
p_θ(x) = ∫ p_θ(x, z) dz,其中 z ∈ R^k,通常在实现中 k 远小于 d。
我们的目标是找到最优参数 θ*,以最小化真实数据分布 p(x) 与模型分布 p_θ(x) 之间的KL散度。这等价于最大化证据下界:
J(θ, φ) = E_{q_φ(z|x)}[log p_θ(x|z)] - KL(q_φ(z|x) || p_θ(z))。
其中:
p_θ(x|z)被称为条件数据似然,由解码器建模。q_φ(z|x)被称为变分隐变量后验,由编码器建模。
我们使用神经网络来参数化这两个分布。编码器模型输入 x,输出隐变量后验分布 q_φ(z|x) 的参数(均值和方差)。解码器模型输入 z,输出数据生成分布 p_θ(x|z) 的参数。
VAE的结构与流程
以下是VAE的核心结构示意图:

具体流程如下:
- 编码器:输入数据
x,输出隐变量分布的均值μ_φ(x)和对角协方差矩阵的元素σ_φ(x)(假设为高斯分布)。 - 重参数化:为了进行可微分的采样,我们使用重参数化技巧:
z = μ_φ(x) + σ_φ(x) ⊙ ε,其中ε ~ N(0, I)。 - 解码器:将采样得到的
z输入解码器,输出重构数据x̂_θ或生成分布的参数。
前向传播过程
现在,让我们回顾一下VAE的前向传播步骤:
以下是前向传播的四个步骤:
- 编码:给定数据
x_i ∈ D,将其输入编码器,得到均值μ_φ(x_i)和方差σ_φ(x_i)。 - 采样噪声:从标准正态分布
N(0, I)中采样M个噪声向量ε_1, ..., ε_M。这一步在神经网络计算图之外。 - 重参数化:通过公式
z_i = μ_φ(x_i) + σ_φ(x_i) ⊙ ε_i计算得到隐变量样本z_1, ..., z_M。 - 解码:将
z_1, ..., z_M输入解码器,计算得到重构数据x̂_θ(z_i)。
参数优化:反向传播
前向传播完成后,我们需要通过反向传播来优化模型参数。我们的目标函数包含两项,需要优化的参数有两组:编码器参数 φ 和解码器参数 θ。因此,我们需要计算四个梯度项。
训练编码器
要训练编码器(参数 φ),我们需要计算目标函数关于 φ 的梯度,这涉及两项。
第一项梯度(重构项):
我们需要计算 ∇_φ E_{q_φ(z|x)}[log p_θ(x|z)]。
通过重参数化,期望可转化为对噪声 ε 的期望,并通过蒙特卡洛采样*似:
≈ (1/M) Σ_{j=1}^M ∇_φ log p_θ(x | g_φ(ε_j, x))。
假设 p_θ(x|z) 是各向同性的高斯分布 N(x̂_θ(z), I),则 log p_θ(x|z) 正比于 -||x - x̂_θ(z)||^2。
因此,该项的梯度正比于 (1/M) Σ_{j=1}^M ∇_φ ||x_i - x̂_θ(z_j)||^2。
这本质上是重构数据与原始数据之间的均方误差的梯度。如果数据(如图像像素)值在0到1之间,并且解码器输出使用了Sigmoid激活函数,此损失函数可视为二元交叉熵损失。
第二项梯度(KL散度项):
我们需要计算 ∇_φ KL(q_φ(z|x) || p_θ(z))。
我们假设先验 p_θ(z) 为标准正态分布 N(0, I),变分后验 q_φ(z|x) 为对角高斯分布 N(μ_φ(x), diag(σ_φ^2(x)))。此时KL散度有解析解:
KL = -1/2 Σ_{i=1}^k (1 + log(σ_i^2) - μ_i^2 - σ_i^2)。
其中 k 是隐空间的维度。该项与 θ 无关。
编码器参数更新:
综合以上两项,编码器参数 φ 的更新公式为:
φ_{t+1} = φ_t + α * ∇_φ J(θ, φ),其中梯度 ∇_φ J 由重构项梯度和KL散度项梯度相加得到。
训练解码器
要训练解码器(参数 θ),我们需要计算目标函数关于 θ 的梯度。
第一项梯度(重构项):
我们需要计算 ∇_θ E_{q_φ(z|x)}[log p_θ(x|z)]。
与编码器训练类似,通过重参数化和蒙特卡洛采样,其梯度*似为 (1/M) Σ_{j=1}^M ∇_θ log p_θ(x | z_j)。
在计算此梯度时,反向传播在解码器处停止,不会继续流向编码器。
第二项梯度(KL散度项):
∇_θ KL(q_φ(z|x) || p_θ(z))。如前所述,在标准VAE设定下,此项与 θ 无关,因此梯度为零。
解码器参数更新:
解码器参数 θ 的更新公式为:
θ_{t+1} = θ_t + α * ∇_θ J(θ, φ),其中梯度 ∇_θ J 仅由重构项的梯度构成。
关于后验坍塌的提醒:在优化过程中,KL散度项会迫使所有输入 x 对应的变分后验 q_φ(z|x) 都接*标准正态先验 N(0, I)。这可能导致“后验坍塌”问题,即不同输入对应的隐变量分布差异过小,模型无法有效区分它们,从而影响生成样本的多样性。
VAE的代码实现
理论部分介绍完毕,现在让我们来看一个具体的实现。以下是使用PyTorch实现一个卷积VAE(在MNIST数据集上)的关键部分。
1. 导入库与准备数据
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt

# 数据转换:将图像转换为Tensor
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
2. 定义Beta-VAE模型
我们实现一个Beta-VAE,其中超参数 beta 用于加权KL散度项。当 beta=1 时,即为标准VAE。
class BetaVAE(nn.Module):
def __init__(self, latent_dim=20):
super(BetaVAE, self).__init__()
self.latent_dim = latent_dim
# 编码器
self.encoder = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=4, stride=2, padding=1), # 28x28 -> 14x14
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=4, stride=2, padding=1), # 14x14 -> 7x7
nn.ReLU(),
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1), # 7x7 -> 4x4
nn.ReLU(),
nn.Flatten(), # 输出形状: 128 * 4 * 4 = 2048
nn.Linear(128 * 4 * 4, 256),
nn.ReLU()
)
# 输出隐变量的均值和对数方差(为了数值稳定性)
self.fc_mu = nn.Linear(256, latent_dim)
self.fc_logvar = nn.Linear(256, latent_dim)
# 解码器
self.decoder_input = nn.Linear(latent_dim, 256)
self.decoder = nn.Sequential(
nn.Linear(256, 128 * 4 * 4),
nn.ReLU(),
nn.Unflatten(1, (128, 4, 4)), # 重塑为 (128, 4, 4)
nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1), # 4x4 -> 7x7
nn.ReLU(),
nn.ConvTranspose2d(64, 32, kernel_size=4, stride=2, padding=1), # 7x7 -> 14x14
nn.ReLU(),
nn.ConvTranspose2d(32, 1, kernel_size=4, stride=2, padding=1), # 14x14 -> 28x28
nn.Sigmoid() # 将输出压缩到 [0, 1] 区间
)
def encode(self, x):
h = self.encoder(x)
mu = self.fc_mu(h)
logvar = self.fc_logvar(h)
return mu, logvar
def reparameterize(self, mu, logvar):
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + eps * std
def decode(self, z):
h = self.decoder_input(z)
reconstruction = self.decoder(h)
return reconstruction
def forward(self, x):
mu, logvar = self.encode(x)
z = self.reparameterize(mu, logvar)
reconstruction = self.decode(z)
return reconstruction, mu, logvar
3. 定义损失函数
def loss_function(recon_x, x, mu, logvar, beta=4):
# 重构损失:二元交叉熵(因为输入和输出都在0-1之间)
BCE = nn.functional.binary_cross_entropy(recon_x, x, reduction='sum')
# KL散度损失
KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
# 总损失 = 重构损失 + beta * KL损失
total_loss = BCE + beta * KLD
return total_loss, BCE, KLD
4. 训练循环
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = BetaVAE(latent_dim=20).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
epochs = 10
beta = 4
for epoch in range(epochs):
model.train()
train_loss = 0
recon_losses = 0
kld_losses = 0
for batch_idx, (data, _) in enumerate(train_loader):
data = data.to(device)
optimizer.zero_grad()
recon_batch, mu, logvar = model(data)
loss, BCE, KLD = loss_function(recon_batch, data, mu, logvar, beta)
loss.backward()
train_loss += loss.item()
recon_losses += BCE.item()
kld_losses += KLD.item()
optimizer.step()
avg_loss = train_loss / len(train_loader.dataset)
avg_recon = recon_losses / len(train_loader.dataset)
avg_kld = kld_losses / len(train_loader.dataset)
print(f'Epoch {epoch+1}, Total Loss: {avg_loss:.4f}, Recon Loss: {avg_recon:.4f}, KLD Loss: {avg_kld:.4f}')
5. 生成新样本
训练完成后,我们可以从隐变量的先验分布 N(0, I) 中采样,然后通过解码器生成新图像。
model.eval()
with torch.no_grad():
# 从标准正态分布采样隐变量
z_sample = torch.randn(64, 20).to(device) # 生成64个样本
# 通过解码器生成图像
generated = model.decode(z_sample).cpu()
# 可视化生成的图像
# ... (绘图代码)
以下是训练过程中损失下降的示意图,以及当 beta=4 时模型生成的手写数字样本示例:




总结与建议
在本节课中,我们一起学*了变分自编码器的实现。我们从理论出发,回顾了VAE的证据下界目标函数,详细推导了其前向传播和针对编码器、解码器的反向传播梯度更新过程。随后,我们通过一个具体的PyTorch代码示例,展示了如何构建一个卷积Beta-VAE模型,包括网络结构定义、重参数化技巧、损失计算以及训练循环。
核心要点总结:
- VAE通过编码器-解码器结构,并引入重参数化技巧,实现了对概率生成模型的可微分训练。
- 目标函数包含重构损失和KL散度正则项,后者迫使隐变量分布接*预设的先验分布(如标准正态分布)。
- 超参数
beta平衡了重构能力与隐空间正则化的强度。beta=1是标准VAE。调整beta会影响生成图像的质量和多样性。 - 训练时,编码器的梯度来自重构项和KL散度项,而解码器的梯度仅来自重构项。

练*建议:
- 尝试调整
beta的值(例如0.25, 0.5, 1, 2, 4, 8),观察训练损失曲线和生成样本质量的变化。 - 增加训练轮数(epochs),观察重构效果是否提升。
- 尝试更改隐变量维度
latent_dim,观察其对模型容量和生成结果的影响。 - 了解“后验坍塌”现象,并思考如何通过改进模型结构或目标函数来缓解它。

通过本教程,你应该已经掌握了实现和训练一个基本VAE模型的能力。在接下来的教程中,我们将探讨更先进的生成模型,如VQ-VAE。





生成式AI的数学基础:6:VQ-VAE的实现 🎼


在本教程中,我们将学*一种变分自编码器的变体,称为向量量化变分自编码器。VQ-VAE与之前学*的标准VAE和β-VAE的主要区别在于,其潜在空间是离散的。
现在,我们将了解如何使用这些潜在向量来构建一个可学*的字典,通常称为码本。然后,我们将探讨其工作原理。

其优化目标函数包含标准的重构损失。此外,还包含一个潜在向量与量化后向量之间的均方误差损失。
接下来,让我们看看如何在代码中实现它。

以下是所需的库,我们现在应该很熟悉了。这里使用了一个向量量化器,它是VQ-VAE网络的一部分。我们先查看这个网络,然后深入了解向量量化器。
在VQ-VAE网络中,编码器部分包含卷积层,解码器部分包含转置卷积层。在前向传播方法中,输入通过编码器得到潜在表示 z,然后将 z 通过量化器得到量化后的结果。量化后的向量再通过解码器以获得重构输出。最后,计算重构损失,并利用该损失更新网络权重。
VQ-VAE可用于嵌入提取,也称为后验推断,也可用于样本生成。现在,让我们具体查看向量量化器。
向量量化器包含一定数量的嵌入向量,每个嵌入向量具有特定的维度。此外,还涉及一个称为“承诺损失”的项。我们将在查看向量量化器代码时详细讨论它。
首先,num_embeddings 参数定义了码本或字典的大小,即需要多少个离散向量。embedding_dim 参数定义了码本中每个向量的维度。commitment_cost 是一个标量,用于权衡两个特定的损失项。
接着,创建一个嵌入矩阵,其大小为 num_embeddings × embedding_dim。如果设 num_embeddings 为 K,embedding_dim 为 D,那么就创建了一个 K × D 的字典。权重的初始化采用均匀分布,范围从 -1/num_embeddings 到 1/num_embeddings。这个初始化步骤不是必须的。
第一步是获得潜在表示 z。让我们回顾一下 z 是如何得到的。

输入 x 通过编码器,得到该 x 对应的编码后潜在表示 z。



然后,z 被送入向量量化器。现在,让我们看看 z 的维度以更好地理解。这里的维度是 32 × 7 × 7。实际上,这是嵌入维度乘以空间维度 7 × 7。输入维度格式为 (B, D, H, W),即批次大小、通道数、高度、宽度。
接着,对维度进行置换,将格式从 (B, D, H, W) 转换为 (B, H, W, D),即把通道维度 D 移到最后。然后,将其转换为一个矩阵,大小为 (B*H*W, D)。这两个步骤在代码中完成。
之后,需要为每个输入向量在码本中找到最*的嵌入向量。为此,计算每个输入向量与码本中所有嵌入向量之间的距离,并找到最小距离对应的索引。这代表了最*的嵌入向量。

通过这个过程,我们得到了最*的嵌入向量。



一旦获得最*的嵌入向量,就需要将其重塑并传递给解码器。为此,首先将其重塑为 (B, H, W, D) 的格式。具体做法是,先使用 .view 方法将其转换为向量,然后根据参数 z_shape 改变形状。接着,使用 .permute(0, 3, 1, 2) 将其维度顺序转换为 (B, D, H, W)。这样就得到了量化后的向量 quantized。
量化完成后,接下来是计算损失。这部分非常关键。
我们之前提到了“承诺损失”。承诺损失鼓励编码器的输出 z 靠*量化后的嵌入向量 z_q。为了实现这一点,代码中执行了 .detach() 操作。这是为了防止梯度通过码本反向传播。
另一方面,在码本损失中,我们希望更新码本的可学*参数,将嵌入向量拉向编码器的输出。在这里,我们阻止了从编码器到码本的梯度,以防止编码器基于这部分损失进行训练。
最后,使用 commitment_cost 这个标量来权衡这两种损失。通过调整 commitment_cost,可以控制对承诺损失的重视程度。
计算完损失后,将量化后的结果和损失返回。以上就是网络结构的相关内容。
接下来是标准的训练流程:创建VQ-VAE实例,设置优化器等。
训练时,将模型设置为训练模式,获取输入 x 并将其移至相应设备。然后将 x 送入模型,得到重构输出 x_hat 和VQ损失。
我们需要计算重构损失。回忆一下目标函数,它由重构损失和VQ损失组成。

VQ损失来自向量量化器,重构损失是均方误差。将两者相加得到总损失。



然后,通过 loss.backward() 计算梯度,并使用优化器更新网络权重。同时,进行一些日志记录以跟踪损失变化。
训练完成后,可以进行重构。这里展示了两种方式:一种是将 x 通过编码器得到 z,再将 z 量化后通过解码器得到重构输出;另一种是完整的流程。第一张图是原始图像,第二张图是重构图像。这是对输入进行重构的标准过程。
一个重要的问题是:如何进行采样生成新样本?这涉及到从学*到的离散潜在分布中进行采样。

为了从该分布中采样,我们需要知道其分布形式。一种方法是训练一个高斯混合模型来拟合这个分布。


具体做法是,首先获取所有训练数据的潜在表示。



将模型设置为评估模式,然后仅通过编码器获取所有潜在表示 z。对 z 进行维度置换和重塑,并将其收集到一个列表中。
现在,我们有了所有潜在表示的集合。接下来,使用Scikit-learn的GaussianMixture模型来训练一个GMM。这里设置了100个混合成分,但这会导致训练时间很长,因此可以酌情减少成分数量(如20-30个)。用所有潜在表示来拟合这个GMM。
拟合好GMM后,从中采样新的潜在向量。然后,对于每个采样得到的潜在向量,计算其与码本中所有嵌入向量之间的距离,并找到距离最小的那个嵌入向量。

这个过程就是为采样的潜在向量找到最*的码本嵌入。



最后,将这个找到的嵌入向量通过解码器,生成新的样本图像。需要注意的是,示例中生成的图像质量较差,这是因为GMM没有经过充分训练,而不是VQ-VAE结构本身的问题。如果使用更少的混合成分并充分训练GMM,采样生成的效果会更好。这留作一个练*。
VQ-VAE的核心思想是拥有一个离散的潜在空间,它可以用于获取数据的嵌入表示,也可以用于生成新的样本。
本节课我们一起学*了向量量化变分自编码器的基本原理和实现步骤,包括其离散潜在空间、码本学*、损失函数构成以及如何进行样本生成。我们将在下一个教程中继续探讨其他主题。




038:去噪扩散概率模型(DDPMs)🎼

在本节课中,我们将学*去噪扩散概率模型,简称DDPMs或扩散模型。这些模型是当前条件图像生成等任务的最先进技术。我们将从潜变量模型的角度来理解扩散模型,并将其视为分层变分自编码器的一种特殊形式。
概述
我们面临的问题是:给定一个从未知分布 p(x) 中独立同分布采样的数据集 D = {x1, x2, ..., xn}。生成式建模的目标是学*如何从这个未知分布 p(x) 中进行采样。
DDPMs 从潜变量模型的角度来解决这个问题。具体来说,我们可以将 DDPMs 视为一类称为分层变分自编码器的潜变量生成模型的特殊案例。
从变分自编码器到分层变分自编码器
首先,让我们回顾一下标准变分自编码器的基本思想。
在VAE中,我们有一个数据空间 x 和一个潜空间 z。编码过程将数据从数据空间投影到潜空间,解码过程则从潜空间映射回数据空间。整个过程通过最小化证据下界来优化。
公式表示:
- 编码分布:
q_φ(z|x) - 解码分布:
p_θ(x|z)
在分层变分自编码器中,情况有所不同。与VAE只有一个潜空间不同,分层VAE拥有多个潜空间,并以分层方式组织。
过程描述:
- 编码:数据首先被投影到第一个潜空间
z1,然后从z1转移到第二个潜空间z2,依此类推,直到第T个潜空间zT。 - 解码:过程与编码相反,从
zT开始,逐步映射回z(T-1),z(T-2),最终回到数据空间x。
这种设计的直觉在于:将数据的所有信息压缩到一个低维潜空间的一步转换可能很困难。而以分层、“缓慢”的方式进行多次投影,可能使数据的编码和重建(解码)变得更加容易。
扩散模型的三个核心特性
扩散模型可以看作是一种具有以下三个特定属性的分层变分自编码器:
- 多个潜空间:与分层VAE一样,DDPM包含一系列潜变量
z1, z2, ..., zT。 - 维度一致:在DDPM中,所有潜空间的维度都与数据空间的维度相同。这与VAE通常使用低维潜空间的做法不同。
- 固定的编码过程:这是最关键的区别。在DDPM中,编码过程是一个固定的、非学*的概率过程。只有解码过程是需要学*的。
对比总结:
- 在 VAE 中,编码和解码过程都是可学*的。
- 在 DDPM 中,只有解码过程是可学*的;编码过程是固定的。
这种设计的直觉部分来源于随机微分方程理论。在生成式建模中,我们最终需要的是从潜空间到数据空间的映射(解码器)。如果我们有一个固定的、将数据“扩散”到潜空间的编码过程,那么只要我们能学会逆转这个过程,就得到了一个生成模型。因此,编码过程本身并不一定需要是可学*的。
构建与学*扩散模型
上一节我们介绍了扩散模型作为分层VAE的特殊形式及其核心特性。本节中,我们来看看如何具体构建和学*一个扩散模型。
基于以上特性,一个DDPM的框架可以概括如下:
- 前向过程(编码):这是一个固定的马尔可夫链,逐步向数据
x添加高斯噪声,经过T步后,数据逐渐变为纯噪声zT。这个过程是预先定义且不可学*的。 - 反向过程(解码):这是一个需要学*的马尔可夫链,其目标是学*如何从噪声
zT开始,逐步去噪,最终重建出数据x。这个过程通过神经网络参数化。
学*的目标是训练反向过程的神经网络,使其能够准确地逆转前向的加噪过程。通过优化一个与变分下界相关的目标函数,模型最终学会从简单的噪声分布中生成复杂的数据样本。
总结


本节课中,我们一起学*了去噪扩散概率模型的基础。我们首先回顾了生成式建模的问题定义,然后从潜变量模型的视角出发,将DDPM理解为一种特殊的分层变分自编码器。我们重点阐述了DDPM的三个核心特性:多个潜空间、潜空间与数据空间维度一致,以及编码过程固定、仅解码过程可学*。这些特性为理解扩散模型如何通过逐步去噪从噪声中生成数据奠定了理论基础。
039:DDPM公式推导

概述
在本节课中,我们将学*去噪扩散概率模型(DDPM)的数学公式。我们将了解其前向过程(编码)和反向过程(解码)的定义,并推导出其核心的训练目标——证据下界(ELBO)。我们将看到DDPM如何通过一个固定的、逐步添加噪声的前向过程,以及一个可学*的、逐步去噪的反向过程来生成数据。
前向过程(编码)🔀
上一节我们介绍了DDPM的基本概念,本节中我们来看看其数学形式化定义。首先,我们需要明确DDPM中的符号约定。在DDPM文献中,数据点本身用 x₀ 表示,而一系列潜在变量则用 x₁, x₂, ..., x_T 表示。请注意,这与我们之前在VAE中使用 z 表示潜在变量的*惯不同,但为了与主流文献保持一致,我们在此采用DDPM的符号体系。
前向过程,也称为编码过程,是一个固定的、非学*的随机过程。它从一个数据点 x₀ 开始,通过一系列步骤逐步向其添加高斯噪声,最终得到一个纯噪声 x_T。
以下是前向过程的定义步骤:
- 从数据点 x₀ 开始。
- 第一步:x₁ = √α₁ * x₀ + √(1-α₁) * ε₁,其中 ε₁ ~ N(0, I)。
- 第二步:x₂ = √α₂ * x₁ + √(1-α₂) * ε₂,其中 ε₂ ~ N(0, I)。
- 以此类推,第t步:x_t = √α_t * x_{t-1} + √(1-α_t) * ε_t,其中 ε_t ~ N(0, I)。
这里的 α₁, α₂, ..., α_T 是介于0和1之间的固定标量,构成了所谓的“方差表”。ε_t 是从标准正态分布中采样的噪声向量。这个过程可以直观地理解为:从一张清晰的图片(x₀)开始,每一步都为其添加一点噪声,经过足够多的步骤T后,图片就变成了完全无意义的噪声(x_T)。
从概率角度看,这个前向过程定义了一个一阶马尔可夫链。这意味着给定前一个状态 x_{t-1},当前状态 x_t 的条件分布独立于更早的历史状态。
因此,我们可以将每一步的转移概率定义为高斯分布:
q(x_t | x_{t-1}) = N(x_t; √α_t * x_{t-1}, (1-α_t) * I)
模型定义(反向过程)🔄
在定义了固定的前向过程之后,我们需要定义模型本身,即反向过程或解码过程。与VAE不同,在DDPM中,只有反向过程是需要学*的。
模型定义了数据 x₀ 和所有潜在变量 x₁:T 的联合分布。我们假设反向过程也是一个一阶马尔可夫链,但它的转移概率是带有可学*参数的高斯分布。
模型分布 p_θ 定义如下:
p_θ(x₀, x₁, ..., x_T) = p_θ(x_T) * ∏_{t=1}^{T} p_θ(x_{t-1} | x_t)
其中,每一步的反向转移概率被建模为高斯分布:
p_θ(x_{t-1} | x_t) = N(x_{t-1}; μ_θ(x_t, t), Σ_θ(x_t, t))
这里的 μ_θ 和 Σ_θ 是以 x_t 和时间步 t 为输入、由神经网络参数化的函数,它们是我们要学*的核心。反向过程的目标是:从一个纯噪声 x_T ~ N(0, I) 开始,通过这个学*到的马尔可夫链,一步步“去噪”,最终生成一个逼真的数据样本 x₀。
证据下界(ELBO)推导🎯
现在,我们有了前向过程 q(固定)和模型分布 p_θ(可学*)。为了训练模型参数 θ,我们需要最大化数据 x₀ 的似然 p_θ(x₀)。与VAE一样,我们通过最大化其证据下界(ELBO)来间接最大化这个似然。
DDPM的ELBO形式如下:
L = E_{q(x_{1:T} | x₀)} [ log( p_θ(x₀, x_{1:T}) / q(x_{1:T} | x₀) ) ]
将我们之前定义的联合分布代入,并经过一系列代数推导(利用马尔可夫链的性质和贝叶斯定理),这个ELBO可以分解为以下几项:
- 重构项:衡量模型从第一个潜在变量 x₁ 重建数据 x₀ 的能力。
- 先验匹配项:确保最终噪声 x_T 与标准正态先验匹配。
- 去噪匹配项(核心):这是一系列项的总和,每一项都衡量了反向过程的一步 p_θ(x_{t-1} | x_t) 与前向过程对应的后验分布 q(x_{t-1} | x_t, x₀) 之间的相似度。
其中,去噪匹配项 是训练的关键。一个重要的推导结果是,前向过程的后验分布 q(x_{t-1} | x_t, x₀) 也是一个高斯分布,其均值是 x_t、x₀ 和噪声 ε 的线性组合。
这使得ELBO的目标可以简化为一个非常直观的形式:训练神经网络 ε_θ 来预测在前向过程中添加到 x_{t-1} 上以得到 x_t 的噪声 ε。也就是说,对于任意时间步 t,我们:
- 从数据 x₀ 开始,根据前向过程采样得到 x_t。
- 模型 ε_θ 以 x_t 和时间 t 为输入,尝试预测所使用的噪声 ε。
- 训练目标是最小化预测噪声与实际噪声之间的均方误差。
最终,我们得到简化的训练目标函数:
L_simple(θ) = E_{t, x₀, ε} [ || ε - ε_θ( √ᾱ_t * x₀ + √(1-ᾱ_t) * ε, t ) ||² ]
其中 ᾱ_t = ∏_{i=1}^{t} α_i。



总结
本节课中,我们一起学*了DDPM的核心数学公式。
- 我们明确了DDPM的符号:x₀ 是数据,x₁,...,x_T 是潜在变量。
- 我们定义了固定的前向过程:一个通过添加高斯噪声将数据逐渐变为纯噪声的马尔可夫链。
- 我们定义了可学*的反向过程模型:一个通过去噪从噪声生成数据的马尔可夫链,其参数由神经网络预测。
- 我们推导了DDPM的证据下界(ELBO),并展示了如何通过简化,将训练目标转化为一个噪声预测任务。即训练一个网络 ε_θ,使其能够预测在任何给定时间步 t 和带噪数据 x_t 的情况下所添加的噪声。这个简单而强大的目标是DDPM能够成功生成高质量数据的关键。
040:U-Net架构详解 🧠

在本节课中,我们将学*一种名为U-Net的有趣新架构。U-Net架构主要用于图像分割任务。
什么是图像分割?

图像分割是指,给定一张图像,我们需要识别出图像中我们感兴趣的区域。例如,在一张包含动物的图片中,动物本身就是我们感兴趣的区域。我们的目标是能够区分动物、前景和背景。
这意味着我们需要为图像中的每一个像素预测它属于哪个类别或对象。例如,如果图像中有三种对象,那么每个像素就属于这三种类别之一。因此,模型的输出是一个与输入图像尺寸相同的“掩码”(mask),其中每个像素位置包含多个值(对应每个类别的概率)。由于这是一个逐像素的分类问题,我们通常使用交叉熵损失函数。


U-Net架构概览

U-Net的名称来源于其独特的“U”形结构。如下图所示,该结构主要包含以下几个部分:
- 编码器(Encoder):位于“U”形的左侧下降部分,负责通过卷积操作逐步提取特征并缩小特征图尺寸。
- 瓶颈(Bottleneck):位于“U”形的底部,是编码器提取到的最深层特征表示。
- 解码器(Decoder):位于“U”形的右侧上升部分,负责通过转置卷积(Transposed Convolution)逐步上采样,恢复特征图尺寸。
- 横向连接(Lateral Connections):连接编码器和解码器对应层的跳跃连接。它们将编码器提取的细节特征传递到解码器,帮助模型在恢复尺寸时保留更多空间信息。
最终,解码器的输出是一个与原始输入图像尺寸相同的掩码。
使用Oxford Pets数据集
为了理解U-Net如何工作,我们使用Oxford Pets数据集。这是一个标准的图像分割数据集。


以下是数据加载和处理的基本步骤,我们假设你已经熟悉PyTorch的基本操作:
- 初始化图像和掩码路径。
- 将数据集的80%划分为训练集,20%划分为测试集(比例可根据需要调整)。
- 定义数据转换(如归一化、调整尺寸等)。
- 在
__getitem__方法中返回具体的图像和掩码对。
一个简单的U-Net实现
现在,让我们深入查看一个简单的U-Net模型实现。该模型接受3通道(RGB)输入,并输出1通道的掩码。
初始化方法(__init__)
我们首先定义一个卷积块(conv_block),它由两个连续的卷积层组成。每个卷积层后都跟随ReLU激活函数。卷积核大小为3,填充为1,步长为1(默认值),这样可以保持特征图尺寸不变。
以下是编码器部分的构建过程:
# 编码器路径:通道数逐渐增加,特征图尺寸通过最大池化(MaxPool2d)逐渐减半
self.enc1 = conv_block(in_channels=3, out_channels=64)
self.pool1 = nn.MaxPool2d(2)
self.enc2 = conv_block(64, 128)
self.pool2 = nn.MaxPool2d(2)
self.enc3 = conv_block(128, 256)
self.pool3 = nn.MaxPool2d(2)
self.enc4 = conv_block(256, 512)
self.pool4 = nn.MaxPool2d(2)
self.bottleneck = conv_block(512, 1024)
接下来是解码器部分,它使用转置卷积进行上采样,并通过横向连接与编码器的特征进行拼接(torch.cat):
# 解码器路径:通过转置卷积上采样,并与编码器对应层的特征拼接
self.upconv4 = nn.ConvTranspose2d(1024, 512, kernel_size=2, stride=2)
self.dec4 = conv_block(1024, 512) # 输入通道1024 = 512(上采样输出) + 512(编码器enc4输出)
self.upconv3 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2)
self.dec3 = conv_block(512, 256) # 输入通道512 = 256 + 256
self.upconv2 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2)
self.dec2 = conv_block(256, 128) # 输入通道256 = 128 + 128
self.upconv1 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2)
self.dec1 = conv_block(128, 64) # 输入通道128 = 64 + 64
self.final_conv = nn.Conv2d(64, out_channels, kernel_size=1)
前向传播方法(forward)
在前向传播过程中,数据流清晰地展示了U-Net的工作方式。我们以一个假设的输入 x(形状为 [1, 3, 128, 128],即1张128x128的RGB图像)为例:


- 编码过程:输入
x依次通过enc1 -> pool1 -> enc2 -> pool2 -> enc3 -> pool3 -> enc4 -> pool4 -> bottleneck。经过每个池化层后,特征图尺寸减半,通道数按设定增加。最终,在瓶颈层得到深层特征表示。 - 解码与融合过程:从瓶颈层开始,首先进行转置卷积上采样(
upconv4),将特征图尺寸放大。然后,将上采样的结果与编码器enc4的输出(在编码过程中已保存)沿通道维度进行拼接。拼接后的特征通过解码器卷积块(dec4)处理。 - 重复步骤2的过程(
upconv3与enc3拼接后过dec3;upconv2与enc2拼接后过dec2;upconv1与enc1拼接后过dec1),逐步恢复特征图尺寸并融合多尺度信息。 - 最终输出:经过最后一个解码器块后,通过一个1x1卷积(
final_conv)将通道数映射到目标输出通道数(例如,二分类分割为1通道),得到与输入图像同尺寸的预测掩码。
训练流程
训练U-Net遵循标准的深度学*流程:
- 从训练数据加载器中获取批次数据(图像
x和真实掩码y)。 - 将数据和模型移至GPU设备。
- 前向传播获取预测掩码。
- 计算逐像素交叉熵损失:
loss = criterion(predictions, y)。 - 反向传播计算梯度:
loss.backward()。 - 使用优化器(如Adam)更新模型权重:
optimizer.step()。 - 循环多个周期(Epoch),并监控训练损失和验证指标。
由于我们仅训练了很少的周期(例如5个),初始的预测结果可能不理想。训练更多周期后,模型性能会显著提升。
总结与展望

本节课我们一起学*了U-Net架构。它的核心特点是编码器-解码器结构、瓶颈层以及关键的横向连接。这些连接确保了在解码(上采样)过程中,能够利用编码阶段捕获的细节特征,从而生成更精确的分割掩码。



U-Net不仅是强大的图像分割工具,其设计思想也被广泛应用于其他生成式模型。在接下来的课程中,当我们实现去噪扩散概率模型(DDPM) 时,将会看到如何利用U-Net架构来预测噪声,并最终生成全新的图像。我们下节课再见!
041:DDPM的ELBO推导 - 第一部分

在本节课中,我们将继续探讨去噪扩散概率模型。具体来说,我们将推导DDPM对应的目标函数——证据下界。这是本模块的核心目标。
概述
首先,让我们快速回顾一下之前的内容。我们使用分层变分自编码器的框架构建了DDPM模型。DDPM是一个潜变量模型,其潜空间是一个马尔可夫链,且所有潜变量的维度与数据空间相同。我们写出了数据与潜变量的联合分布,这与VAE中的 P(x, Z) 类似。
DDPM中数据与潜变量的联合分布由以下公式给出:
p_θ(x_0:T) = p(x_T) * ∏_{t=1}^{T} p_θ(x_{t-1} | x_t)
这个公式源于我们假设在反向(解码)过程中,潜变量遵循一阶马尔可夫链。我们还假设反向过程中的马尔可夫转移分布是一个高斯分布,其均值 μ_θ(x_t) 和方差 σ_θ(x_t) 是可学*的参数。我们将通过标准的ELBO优化来学*这些参数。
基于此,我们写出了适用于任何潜变量生成模型的证据下界标准方程,即我们的目标函数。对于DDPM,该方程具有特定的形式。
ELBO目标函数推导
上一节我们介绍了DDPM的模型框架,本节中我们来看看如何具体推导其ELBO表达式。我们的目标是优化DDPM的证据下界。
首先,我们写出标准ELBO的通用形式:
L = E_{q(z|x)} [ log( p_θ(x, z) / q(z|x) ) ]
对于DDPM,其中 x_0 是数据变量,x_1 到 x_T 是 T 个潜变量。q 代表固定的、非学*的正向(编码)过程。
我们的目标是将DDPM中定义的编码分布 q 和解码分布 p_θ 代入上述方程,得到一个可以在DDPM上下文中优化的表达式。接下来,我们将逐步进行代数推导。
展开ELBO表达式
考虑ELBO中的对数项(暂时忽略外部的期望):
log [ p_θ(x_0:T) / q(x_1:T | x_0) ]
根据DDPM的定义:
p_θ(x_0:T) = p(x_T) * ∏_{t=1}^{T} p_θ(x_{t-1} | x_t)- 由于正向过程是一阶马尔可夫过程,
q(x_1:T | x_0) = ∏_{t=1}^{T} q(x_t | x_{t-1})
将定义代入,得到:
log [ p(x_T) * ∏_{t=1}^{T} p_θ(x_{t-1} | x_t) ] - log [ ∏_{t=1}^{T} q(x_t | x_{t-1}) ]
利用对数的性质,可以将其写为和的形式。为了代数推导的便利,我们将分子和分母中关于 t=1 的项单独提出:
= log [ p(x_T) * p_θ(x_0 | x_1) / q(x_1 | x_0) ] + log [ ∏_{t=2}^{T} ( p_θ(x_{t-1} | x_t) / q(x_t | x_{t-1}) ) ]
观察第二项,分子是反向条件概率,分母是正向条件概率。为了简化,我们希望用同一方向(最好是反向)的分布来表示它们。
利用贝叶斯定理重写分母
考虑第二项的分母 q(x_t | x_{t-1})。由于正向过程是一阶马尔可夫过程,增加条件 x_0 不会改变分布:
q(x_t | x_{t-1}) = q(x_t | x_{t-1}, x_0)
现在,我们对等式右边应用贝叶斯定理(针对三个随机变量):
q(x_t | x_{t-1}, x_0) = [ q(x_{t-1} | x_t, x_0) * q(x_t | x_0) ] / q(x_{t-1} | x_0)
这个变换的关键在于,它将正向分布 q(x_t | x_{t-1}) 用反向条件分布 q(x_{t-1} | x_t, x_0) 表示了出来。q(x_{t-1} | x_t, x_0) 具有直观意义:它表示在已知起始点 x_0 和当前点 x_t 的情况下,前一个点 x_{t-1} 的分布。这可以看作是一种“真实”的反向分布,为我们学*参数化的 p_θ(x_{t-1} | x_t) 提供了一个参考目标。
代入并简化ELBO
将贝叶斯定理的结果代回ELBO的第二项:
第二项 = log [ ∏_{t=2}^{T} ( p_θ(x_{t-1} | x_t) / ( [q(x_{t-1} | x_t, x_0) * q(x_t | x_0)] / q(x_{t-1} | x_0) ) ) ]
= log [ ∏_{t=2}^{T} ( p_θ(x_{t-1} | x_t) * q(x_{t-1} | x_0) / [ q(x_{t-1} | x_t, x_0) * q(x_t | x_0) ] ) ]
现在,ELBO的对数项变为三项之和:
- 第一项:
log [ p(x_T) * p_θ(x_0 | x_1) / q(x_1 | x_0) ] - 第二项:
log [ ∏_{t=2}^{T} ( p_θ(x_{t-1} | x_t) / q(x_{t-1} | x_t, x_0) ) ] - 第三项:
log [ ∏_{t=2}^{T} ( q(x_{t-1} | x_0) / q(x_t | x_0) ) ]
让我们简化第三项。它是一个连乘积的比值:
∏_{t=2}^{T} [ q(x_{t-1} | x_0) / q(x_t | x_0) ] = [q(x_1|x_0)/q(x_2|x_0)] * [q(x_2|x_0)/q(x_3|x_0)] * ... * [q(x_{T-1}|x_0)/q(x_T|x_0)]
可以看到,中间项全部被约去,最终只剩下:
= q(x_1 | x_0) / q(x_T | x_0)
因此,第三项简化为 log [ q(x_1 | x_0) / q(x_T | x_0) ]。
合并项并引入期望
现在,将第一项 p(x_T) * p_θ(x_0 | x_1) / q(x_1 | x_0) 与简化后的第三项 q(x_1 | x_0) / q(x_T | x_0) 合并。q(x_1 | x_0) 项被约去,得到:
合并项 = log [ p(x_T) * p_θ(x_0 | x_1) / q(x_T | x_0) ]
最终,ELBO内部的对数项可以写为:
log [ p_θ(x_0 | x_1) ] + log [ p(x_T) / q(x_T | x_0) ] + ∑_{t=2}^{T} log [ p_θ(x_{t-1} | x_t) / q(x_{t-1} | x_t, x_0) ]
现在,我们需要将之前忽略的外部期望 E_{q(x_1:T | x_0)}[ ... ] 加回来。由于期望的线性性质,我们可以将其分配到每一项上。每一项只依赖于部分随机变量,因此期望可以相应地简化:
- 第一项
log p_θ(x_0 | x_1)只依赖于x_1,其期望为E_{q(x_1 | x_0)}[ log p_θ(x_0 | x_1) ]。 - 第二项
log [ p(x_T) / q(x_T | x_0) ]只依赖于x_T,其期望为E_{q(x_T | x_0)}[ log (p(x_T) / q(x_T | x_0)) ]。 - 第三项求和中的每一项
log [ p_θ(x_{t-1} | x_t) / q(x_{t-1} | x_t, x_0) ]依赖于x_{t-1}和x_t。其期望可以写为:
内层的期望正是KL散度的定义。E_{q(x_t | x_0)} [ E_{q(x_{t-1} | x_t, x_0)} [ log ( p_θ(x_{t-1} | x_t) / q(x_{t-1} | x_t, x_0) ) ] ]
总结
本节课中,我们一起学*了DDPM证据下界推导的第一部分。我们通过一系列代数操作,将ELBO表达式从通用形式转化为适用于DDPM的具体形式。关键步骤包括:
- 代入DDPM的联合分布和变分后验分布。
- 利用正向过程的马尔可夫性。
- 应用贝叶斯定理将正向条件概率用反向条件概率表示,从而引入了“真实”反向分布
q(x_{t-1} | x_t, x_0)。 - 通过合并和约简项,最终得到了一个包含三项的表达式。
- 重新引入外部期望,并利用条件期望的性质,将第三项中的期望写成了KL散度的形式。


这个推导过程虽然代数上有些复杂,但清晰地展示了DDPM优化目标的结构。在下一节中,我们将进一步分析这些项的具体含义,并展示它们如何导向一个可行的训练目标。
042:DDPM的ELBO推导 - 第二部分

在本节课中,我们将继续推导去噪扩散概率模型(DDPM)的证据下界(ELBO)。我们将利用条件期望的性质重写ELBO,并将其分解为三个具有明确含义的项。最后,我们将重点分析其中最重要的“一致性项”,并展示如何计算其内部的KL散度。
利用条件期望性质重写ELBO
上一节我们得到了ELBO的初步形式。本节中,我们将利用条件期望的性质对其进行重写和分解。
条件期望的性质允许我们将期望内的求和项进行拆分。应用此性质后,ELBO可以展开为以下三项之和:
- 第一项:
E_{q(x_1|x_0)} [log p_θ(x_0 | x_1)] - 第二项:
- Σ_{t=2}^T E_{q(x_t|x_0)} [D_KL( q(x_t | x_0) || p(x_t) )] - 第三项:
- Σ_{t=2}^T E_{q(x_t|x_0)} [ D_KL( q(x_{t-1} | x_t, x_0) || p_θ(x_{t-1} | x_t) ) ]
现在,我们可以识别每一项的含义:
- 第一项是数据条件似然的期望,通常称为重构项。
- 第二项是先验匹配项,它试图让给定数据时潜变量的条件分布
q(x_t | x_0)匹配其先验分布p(x_t)。 - 第三项是一致性项或去噪匹配项,这是DDPM中特有的关键项。
理解ELBO的三项构成
以下是ELBO三项的详细说明:
-
重构项:
E_{q(x_1|x_0)} [log p_θ(x_0 | x_1)]- 此项与变分自编码器(VAE)中的重构损失类似,目标是基于第一个潜变量
x_1来重建原始数据x_0。
- 此项与变分自编码器(VAE)中的重构损失类似,目标是基于第一个潜变量
-
先验匹配项:
- Σ_{t=2}^T D_KL( q(x_t | x_0) || p(x_t) )- 此项也与VAE中的KL正则项类似,目的是让所有时间步
t的潜变量后验分布q(x_t | x_0)都接*其先验分布p(x_t)(通常为标准正态分布)。此项与模型参数 θ 无关,在优化时可以忽略。
- 此项也与VAE中的KL正则项类似,目的是让所有时间步
-
一致性项:
- Σ_{t=2}^T E_{q(x_t|x_0)} [ D_KL( q(x_{t-1} | x_t, x_0) || p_θ(x_{t-1} | x_t) ) ]- 这是DDPM的核心。
q(x_{t-1} | x_t, x_0)是根据前向过程定义的、已知的、真实的去噪步骤。p_θ(x_{t-1} | x_t)是模型要学*的去噪步骤。最小化它们之间的KL散度,就是让模型学*的去噪过程与真实的反向去噪过程保持一致。
- 这是DDPM的核心。
我们的优化目标是找到参数 θ* 以最大化ELBO。由于先验匹配项是常数,优化重点在于重构项和一致性项。重构项相对简单,而一致性项的计算则需要进一步推导。
计算一致性项中的KL散度
接下来,我们专注于计算一致性项中的KL散度:D_KL( q(x_{t-1} | x_t, x_0) || p_θ(x_{t-1} | x_t) )。这需要我们知道两个分布的具体形式。
首先,计算已知的真实分布 q(x_{t-1} | x_t, x_0)。利用贝叶斯定理,我们可以将其转化为前向过程中已知的分布:
q(x_{t-1} | x_t, x_0) = [q(x_t | x_{t-1}, x_0) * q(x_{t-1} | x_0)] / q(x_t | x_0) = [q(x_t | x_{t-1}) * q(x_{t-1} | x_0)] / q(x_t | x_0)
根据前向过程的定义,q(x_t | x_{t-1}) 是已知的高斯分布。因此,问题转化为求解边缘分布 q(x_t | x_0) 和 q(x_{t-1} | x_0)。
推导 q(x_t | x_0) 的解析形式
前向过程定义为:x_t = √α_t * x_{t-1} + √(1-α_t) * ε_{t-1},其中 ε ~ N(0, I)。
通过递归展开 x_t 直到 x_0,我们可以得到一个重要结论:
x_t = √(ᾱ_t) * x_0 + √(1 - ᾱ_t) * ε,其中 ᾱ_t = Π_{i=1}^{t} α_i,ε ~ N(0, I)。
这意味着 q(x_t | x_0) 也是一个高斯分布:
q(x_t | x_0) = N(x_t; √(ᾱ_t) * x_0, (1 - ᾱ_t)I )
这个性质非常关键:它表明我们可以直接从原始数据 x_0 和噪声 ε 采样得到任意中间时刻 t 的加噪数据 x_t,而无需逐步进行 t 次前向扩散步骤。 这极大地简化了训练过程。
同理,q(x_{t-1} | x_0) = N(x_{t-1}; √(ᾱ_{t-1}) * x_0, (1 - ᾱ_{t-1})I )。
得到 q(x_{t-1} | x_t, x_0) 的分布
现在,我们将已知的三个高斯分布(q(x_t | x_{t-1}), q(x_{t-1} | x_0), q(x_t | x_0))代入贝叶斯公式。通过“配方法”对 x_{t-1} 进行配方,可以证明 q(x_{t-1} | x_t, x_0) 也是一个高斯分布。
其均值和方差如下:
- 均值 μ_q:
μ_q = [ √α_t * (1 - ᾱ_{t-1}) * x_t + √(ᾱ_{t-1}) * (1 - α_t) * x_0 ] / (1 - ᾱ_t) - 方差 Σ_q:
Σ_q = [(1 - α_t) * (1 - ᾱ_{t-1}) / (1 - ᾱ_t)] * I
其中所有 α 和 ᾱ 都是预先定义的标量调度参数。因此,给定 x_t 和 x_0,μ_q 和 Σ_q 都是可计算的。
设定模型分布 p_θ(x_{t-1} | x_t)
在DDPM中,我们将学*到的反向过程也建模为高斯分布:p_θ(x_{t-1} | x_t) = N(x_{t-1}; μ_θ, Σ_θ)。
为了简化,通常将方差 Σ_θ 设为与真实分布 q 的方差 Σ_q 相同的固定值(或另一个固定调度)。这样,模型只需要学*预测均值 μ_θ。
计算高斯分布之间的KL散度
现在,一致性项中的KL散度变成了两个高斯分布之间的KL散度:
D_KL( q(x_{t-1} | x_t, x_0) || p_θ(x_{t-1} | x_t) ) = D_KL( N(x_{t-1}; μ_q, Σ_q) || N(x_{t-1}; μ_θ, Σ_q) )
当两个高斯分布的方差相同时,它们之间的KL散度有一个非常简洁的形式(忽略常数项):
D_KL ∝ || μ_q - μ_θ ||^2
这意味着,最小化这个KL散度,等价于最小化模型预测的均值 μ_θ 与真实去噪分布的均值 μ_q 之间的均方误差(MSE)。 这为训练DDPM提供了一个清晰且易于实现的目标函数。
总结
本节课中,我们一起学*了DDPM的ELBO推导的第二部分:
- 我们将ELBO分解为重构项、先验匹配项和一致性项。
- 我们推导了前向过程的一个重要性质:
q(x_t | x_0)是高斯分布,允许直接从x_0采样x_t。 - 我们计算了真实的反向去噪分布
q(x_{t-1} | x_t, x_0),并证明它也是高斯分布。 - 通过设定模型分布
p_θ也为高斯分布,我们将一致性项的优化目标简化为:让模型预测的去噪均值μ_θ尽可能接*真实去噪均值μ_q。


这为下一节最终推导出DDPM简洁的训练目标——预测所添加的噪声——奠定了坚实的基础。
043:DDPM损失的优化

在本节课中,我们将要学*扩散概率模型(DDPM)中证据下界(ELBO)的优化过程。我们将推导出最终的损失函数形式,并探讨如何通过神经网络来参数化和优化这个损失函数。
证据下界(ELBO)的组成
上一节我们介绍了DDPM的ELBO由三项组成。本节中,我们来看看这三项的具体形式及其简化过程。
ELBO由以下三项组成:
- 重建项:与从第一个潜变量 \(x_1\) 重建原始数据 \(x_0\) 有关。
- 先验匹配项:与模型参数 \(\theta\) 无关,因此在优化时可以忽略。
- 一致性项:也称为去噪匹配项,是优化的核心。
我们的目标是简化一致性项,并最终得到一个可优化的损失函数表达式。
一致性项的简化
一致性项涉及两个分布之间的KL散度:真实的反向分布 \(q(x_{t-1} | x_t, x_0)\) 和模型的反向分布 \(p_\theta(x_{t-1} | x_t)\)。
我们通过贝叶斯规则推导出 \(q(x_{t-1} | x_t, x_0)\) 是一个高斯分布,其均值 \(\tilde{\mu}_t\) 是 \(x_t\) 和 \(x_0\) 的线性组合,方差 \(\tilde{\beta}_t\) 是一个标量乘以单位矩阵。
模型分布 \(p_\theta(x_{t-1} | x_t)\) 也被假设为高斯分布,其均值 \(\mu_\theta\) 由神经网络预测,方差通常被设定为与 \(\tilde{\beta}_t\) 相同的常数。
因此,两个高斯分布之间的KL散度可以精确计算。由于方差相同,KL散度简化为两者均值之间的平方差。
以下是KL散度的简化公式:
这个结果非常简洁:一致性项本质上是一个回归任务,即让神经网络预测的均值 \(\mu_\theta\) 去匹配真实后验分布的均值 \(\tilde{\mu}_t\)。
完整的ELBO表达式
结合重建项和简化后的一致性项,我们得到DDPM的完整ELBO表达式。
最终的ELBO损失函数 \(J_\theta(q)\) 如下:
其中,期望是在前向扩散过程 \(q(x_t | x_0)\) 下计算的。这就是我们需要优化的目标函数。
损失函数的参数化与实现
为了优化上述损失,我们需要用神经网络来参数化未知的均值函数 \(\mu_\theta(x_t, t)\)。
基本思路是使用一个神经网络(如U-Net架构)来预测 \(\mu_\theta\)。该网络以噪声数据 \(x_t\) 和时间步 \(t\) 作为输入,输出预测的均值。
以下是实现的关键点:
- 网络架构:通常使用U-Net,它通过下采样和上采样来保持输入输出的维度一致,适合进行像素级的回归预测。
- 时间步输入:为了让网络感知当前的时间步 \(t\),我们将标量 \(t\) 通过正弦位置编码转换为一个向量,然后与 \(x_t\) 一起输入网络。
- 参数共享:我们使用同一个神经网络来处理所有时间步 \(t\) 的预测,这实现了跨时间步的参数共享。
正弦位置编码的函数如下,它将一个整数标量 \(t\) 映射为一个 \(d\) 维向量:
训练流程概述
基于上述分析,DDPM的训练流程可以概括为以下步骤:
以下是训练一个DDPM模型的核心步骤:
- 从数据集中采样一个干净样本 \(x_0\)。
- 从 \(1\) 到 \(T\) 中均匀采样一个时间步 \(t\)。
- 根据前向扩散公式,采样得到加噪样本 \(x_t\)。
- 将 \(x_t\) 和时间步 \(t\) 输入神经网络,得到预测的均值 \(\mu_\theta(x_t, t)\)。
- 根据 \(x_t\) 和 \(x_0\) 计算真实的后验均值 \(\tilde{\mu}_t(x_t, x_0)\)。
- 计算损失函数 \(\| \mu_\theta - \tilde{\mu}_t \|^2\) 的均值(忽略权重系数)。
- 通过梯度下降更新神经网络参数 \(\theta\)。
- 重复以上步骤直至收敛。
损失函数的等价形式
在实践中,为了数值稳定性和简化计算,我们通常不会直接预测 \(\tilde{\mu}_t\)。根据 \(\tilde{\mu}_t\) 的表达式,它可以重写为:
其中 \(\epsilon\) 是前向过程中添加到 \(x_0\) 上的标准高斯噪声。
因此,让网络直接预测这个噪声 \(\epsilon\) 是另一种等价且更常见的做法。此时,损失函数变为:
这个形式更为简洁,它要求神经网络 \(\epsilon_\theta\) 直接预测在前向过程中加入的噪声。这是大多数现代扩散模型代码实现所采用的形式。
总结


本节课中我们一起学*了DDPM模型证据下界(ELBO)的优化过程。我们从ELBO的三项分解出发,重点推导并简化了一致性项,将其转化为一个均值匹配的回归问题。我们得到了最终的损失函数表达式,并讨论了如何通过一个接收噪声数据 \(x_t\) 和时间步 \(t\) 的U-Net神经网络来参数化这个损失。最后,我们概述了训练流程,并介绍了预测噪声 \(\epsilon\) 这一更常用的等价损失形式,这为后续的实际代码实现奠定了理论基础。
044:ELBO等价性

在本节课中,我们将探讨证据下界(ELBO)的等价性表示。我们将看到,虽然之前推导的损失函数在数学上是正确的,但通过重新参数化,我们可以得到一个在解释上更直观、在实践上更有用的等价形式。
回顾一致性项
上一节我们介绍了去噪扩散概率模型(DDPM)损失函数中的一致性项。现在,我们来看看如何对其进行等价变换。
该一致性项最初的形式为:
L = 1/(2σ_q^2) * || μ_θ(xt) - μ_q(xt, x0) ||^2
其中,μ_q 是已知的,其表达式为:
μ_q(xt, x0) = c1 * xt + c2 * x0
这里,c1 和 c2 是由噪声调度参数 α 和 α_bar 决定的常数。
重新参数化 μ_θ
我们设计的神经网络输出 μ_θ 可以表示为与 μ_q 类似的形式。具体做法如下:
我们可以将 μ_θ 也写成 xt 和另一个神经网络预测值的线性组合:
μ_θ(xt) = c1 * xt + c2 * x_θ(xt)
这里的 x_θ(xt) 是神经网络的输出。这种重新参数化是可行的,因为 μ_θ 是一个我们可以自由设计的向量。给定一个固定的 xt,c1 * xt 是常数。我们总是可以通过对神经网络输出进行缩放和平移(即线性变换),使其符合上述形式。
推导新的损失函数形式
将重新参数化后的 μ_θ 代入原损失函数,并进行代数化简。
经过整理,一致性项变为:
L = 1/(2σ_q^2) * C * || x_θ(xt) - x0 ||^2
其中,C 是一个由 α_t 和 α_bar_t 等参数组成的常数系数。
新形式的解释优势
这个新的形式在数学上与原始形式完全等价,但在解释上带来了显著优势。
以下是新形式的核心优势:
- 直观的解释:损失函数现在变成了在原始干净数据
x0上的回归。神经网络x_θ的任务是,给定一个噪声版本xt,预测出原始的、未加噪的数据x0。 - 作为去噪器:因此,整个训练好的神经网络可以被视为一个去噪器。它学*的是如何从不同噪声水平(对应不同时间步
t)的噪声数据xt中,恢复出原始数据x0。 - 模型命名的由来:这也正是“去噪扩散概率模型”中“去噪”一词的来源之一。模型的学*过程本质上是在学*一个去噪函数。
重要注意事项
需要特别注意的是,DDPM中的神经网络与之前学过的其他生成模型有根本不同。
以下是关键区别:
- 非采样器:与生成对抗网络(GAN)的生成器或变分自编码器(VAE)的解码器不同,DDPM中训练好的神经网络
x_θ并不直接输出一个来自目标分布p(x0)的新样本。 - 回归器的角色:它只是一个在输入数据点
x0上的回归器。它的输入是噪声数据xt,输出是对应原始数据x0的估计。 - 采样过程:要生成新样本,我们需要运行一个完整的反向(去噪)扩散过程,这个过程会迭代地调用这个神经网络。我们将在后续课程中详细学*推理(采样)过程。
训练流程概要
现在,我们已经拥有了训练一个DDPM所需的所有要素。
以下是DDPM的训练算法概要:
- 从数据集中采样一个干净数据样本
x0。 - 从均匀分布
Uniform(1, ..., T)中采样一个时间步t。 - 从标准正态分布采样噪声
ε,并根据前向扩散过程公式计算加噪后的数据xt。 - 将
xt和t输入神经网络x_θ,得到预测的x0(或等价地,预测噪声ε,这是另一种常见参数化方式)。 - 计算损失函数(如上所述的均方误差),并通过反向传播更新神经网络参数
θ。 - 重复以上步骤直至收敛。
总结


本节课中,我们一起学*了ELBO的等价性表示。我们通过重新参数化技巧,将原本在 μ 空间上的回归损失,转换为了在原始数据 x0 空间上的回归损失。这种形式不仅数学等价,而且让我们能够将DDPM中的神经网络清晰地解释为一个去噪器。同时,我们明确了该网络在推理时不直接充当采样器的关键特性,为下一节学*完整的生成(采样)过程打下了基础。
045:DDPM的训练

在本节课中,我们将学*去噪扩散概率模型(DDPM)的训练过程。我们将了解如何通过一个简单而优雅的优化过程来训练模型,使其能够学*从噪声中恢复原始数据。
概述
DDPM的训练过程非常直接。其核心思想是训练一个神经网络,使其能够预测在给定噪声样本和特定时间步长的情况下,原始数据点是什么。与GAN或VAE相比,DDPM的训练更加稳定,因为它不涉及复杂的对抗优化或潜在空间的后验坍塌问题。
训练步骤详解
上一节我们介绍了DDPM的损失函数,本节中我们来看看如何将这个理论转化为实际的训练步骤。
给定一个从真实数据分布中采样的数据点 ( x_0 ),DDPM的训练流程如下。
以下是训练循环的步骤:
- 采样时间步长:从0到预设的最大步长 ( T )(通常设为1000)中均匀随机采样一个时间步长 ( t )。
- 生成噪声样本:根据前向扩散过程,计算在时间步长 ( t ) 对应的噪声样本 ( x_t )。其计算公式为:
[
x_t = \sqrt{\bar{\alpha}_t} \cdot x_0 + \sqrt{1 - \bar{\alpha}_t} \cdot \epsilon_t
]
其中,( \epsilon_t \sim \mathcal{N}(0, I) ) 是标准高斯噪声。 - 前向传播:将噪声样本 ( x_t ) 和时间步长 ( t ) 输入神经网络 ( f_\theta ),得到网络对原始数据 ( x_0 ) 的预测 ( \hat{x}_0 )。
- 计算损失:计算预测值 ( \hat{x}_0 ) 与真实值 ( x_0 ) 之间的均方误差(MSE)作为损失函数:
[
J(\theta) = | \hat{x}_0 - x_0 |^2
] - 反向传播与参数更新:计算损失函数 ( J(\theta) ) 关于网络参数 ( \theta ) 的梯度,并使用梯度下降法更新参数:
[
\theta_{k+1} = \theta_k - \eta \cdot \nabla_\theta J(\theta)
]
其中 ( \eta ) 是学*率。
批处理与时间步采样
在实际操作中,我们通常不会在单个数据点和单个时间步长上进行训练。为了提高效率,会采用批处理和子采样策略。
以下是具体的优化方法:
- 批处理:从一个批次(batch)的多个数据点 ( {x_0^{(1)}, x_0^{(2)}, ..., x_0^{(B)}} ) 中同时采样进行训练。最终的损失是批次内所有数据点损失的平均值。
- 时间步子采样:原始的损失函数需要对所有时间步长 ( t=1 ) 到 ( T ) 求和。在实践中,我们会在每个训练步骤中,从所有可能的时间步长中随机采样一个子集 ( M )(例如,采样 ( M ) 个不同的 ( t )),只计算这些时间步长上的损失并进行梯度更新。这相当于对完整损失函数的一个随机估计,但能显著加速训练。
因此,一次完整的训练迭代包含:采样一个数据批次,为批次中的每个数据点采样一组时间步长,生成对应的噪声样本,计算总损失,然后进行一次梯度下降更新。
训练的优势与特点
DDPM的训练设计具有几个关键优势,使其在图像生成领域表现出色。
以下是其主要特点:
- 训练稳定:整个过程是一个简单的回归任务(预测原始数据),避免了GAN训练中常见的模式崩溃和难以收敛的极小极大博弈问题。
- 避免后验坍塌:与变分自编码器(VAE)不同,DDPM没有显式的潜在变量编码器,因此不存在VAE中“后验坍塌”(即编码器忽略输入数据)的风险。
- 过程优雅:训练仅需标准的前向传播、损失计算和反向传播,无需复杂的技巧或辅助网络。
需要注意的是,在完整的损失函数中,还有一个对应于 ( t=1 ) 的对数似然项。在训练时,这一项也会被纳入考虑,其处理方式与上述回归损失类似。
总结
本节课中我们一起学*了DDPM模型的训练方法。其核心是训练一个神经网络,通过预测加噪数据中的原始信号来完成学*。训练过程通过随机采样时间步长、利用重参数化技巧生成噪声样本,并执行简单的梯度下降来实现。这种方法的稳定性、简洁性和高效性,是扩散模型能够成为当前图像生成领域主流技术的重要原因之一。


下一节,我们将探讨一个关键问题:训练好的DDPM模型如何用于生成全新的样本。
046:DDPM中的推理 🎼


在本节课中,我们将学*如何在去噪扩散概率模型中进行推理或采样。我们将详细探讨从训练好的DDPM中生成新样本的具体步骤和原理。
概述
上一节我们介绍了DDPM的训练过程,本节中我们来看看如何利用训练好的模型进行推理,即生成新的数据样本。与GAN或VAE等模型不同,DDPM的生成过程需要多步迭代。
推理或采样过程
在DDPM中进行推理或采样,意味着我们需要运行反向链。具体来说,为了从真实数据分布 P(x0) 中获取一个样本 x0,我们需要遍历反向过程(或称解码过程)。这个过程从 x_T 开始,逐步得到 x_{T-1}, x_{T-2},最终得到 x_0。
因此,我们需要迭代地从解码分布 P_θ(x_{t-1} | x_t) 中进行采样。对于 t 从 T 到 1,我们依次执行此操作。
如何从解码分布中采样
我们知道,解码分布 P_θ(x_{t-1} | x_t) 被建模为一个高斯分布。其均值是 μ_θ,方差是 σ_q^2 * I。
为了从这个高斯分布中采样,我们可以使用重参数化技巧。具体步骤如下:
- 从标准正态分布
N(0, I)中采样一个随机噪声ε。 - 使用公式
x_{t-1} = μ_θ + σ_q * ε计算样本。
这样,我们就得到了一个来自 P_θ(x_{t-1} | x_t) 的样本。
均值 μ_θ 的计算
均值 μ_θ 是关键。如果我们训练的神经网络直接回归均值 μ_θ,那么 μ_θ 就是神经网络的输出。
如果我们训练的神经网络是去预测原始数据 x_0(记作 x̂_θ),那么 μ_θ 需要通过以下公式计算:
μ_θ(xt) = (1 - ᾱ_{t-1}) * √α_t / (1 - ᾱ_t) * x_t + (1 - α_t) * √ᾱ_{t-1} / (1 - ᾱ_t) * x̂_θ(xt)
其中,α_t 和 ᾱ_t 是预定义的前向过程超参数。
DDPM推理算法
以下是DDPM推理过程的算法步骤:
- 初始化:从标准正态分布
N(0, I)中采样一个随机点x_T。 - 迭代去噪:对于
t从T到 1,执行以下循环:- 将当前
x_t输入训练好的神经网络,得到预测值x̂_θ(xt)(或直接得到μ_θ)。 - 根据所选公式计算均值
μ_θ(xt)。 - 从
N(0, I)采样一个随机噪声ε。 - 计算
x_{t-1} = μ_θ(xt) + σ_q * ε。
- 将当前
- 输出:循环结束后,返回
x_0作为新生成的样本。
请注意,与GAN或VAE的单步生成不同,DDPM需要执行 T 次神经网络前向传播和采样步骤,因此其生成速度相对较慢。这个过程可以看作是逐步“去噪”,从一个纯噪声 x_T 开始,一步步还原出数据样本 x_0。
原理回顾与总结
本节课中我们一起学*了DDPM的推理机制。其核心在于,我们训练模型的目标是学*反向马尔可夫链中高斯转移分布的均值。生成样本时,我们就是沿着这条学*到的反向链,从噪声开始,通过多次迭代采样,最终得到符合数据分布的新样本。
虽然推理过程需要多步计算,但DDPM的训练非常稳定,因为它本质上是一个回归任务,避免了GAN中的对抗训练难题或VAE中的后验坍塌问题。超参数 α_t 通常被预设为一个从接*1递减到接*0的序列,具体细节会在相关教程中讨论。
后续内容预告
目前我们所讨论的DDPM只能从无条件分布中采样,生成随机的数据。然而,在实际应用中(如根据文本生成图像),我们常常需要从条件分布 P(x|y) 中采样,其中 y 是条件信息(如文本描述)。
下一节,我们将探讨如何修改DDPM的公式,使其能够进行条件生成。主要有两种方法:分类器引导扩散 和 无分类器引导。我们将学*这两种方法如何将条件信息融入DDPM的生成过程。




047:DDPM的实现 🎼

在本教程中,我们将动手实现最先进的扩散模型(Diffusion Model)的核心思想。我们将从理论回顾开始,逐步讲解前向过程、反向过程、训练流程以及代码实现,最终生成图像样本。
概述:扩散模型流程
首先,我们通过一张简单的图像来回顾扩散模型的前向和反向过程。

上图展示了扩散模型的核心流程。左侧是前向过程,从原始数据 x0 开始,逐步添加噪声,经过 T 步后,数据 xT 最终变为一个标准高斯分布(均值为0,方差为I)。右侧是反向过程,从高斯噪声 xT 开始,逐步去噪,最终恢复出原始数据 x0。
上一节我们介绍了扩散模型的理论基础,本节中我们来看看如何用代码实现它。
前向过程与反向过程
前向过程
前向过程是一个固定的、没有可学*参数的概率过程。其目的是通过逐步添加噪声,将数据分布转化为一个简单的已知分布(如高斯分布)。
给定初始数据 x0,第 t 步的加噪数据 xt 可以通过以下公式计算:
xt = sqrt(alpha_bar_t) * x0 + sqrt(1 - alpha_bar_t) * epsilon
其中,epsilon 是从标准高斯分布中采样的噪声,alpha_bar_t 是预先计算好的系数。
这个过程是确定性的吗?不,因为每一步都需要从高斯分布中采样噪声 epsilon,所以它是一个概率性过程。
反向过程
反向过程是扩散模型生成新数据的核心。它从一个纯高斯噪声 xT 开始,通过一个学*到的去噪模型(如U-Net),逐步预测并移除噪声,最终得到清晰的数据 x0。
与GAN或VAE的一步生成不同,扩散模型的生成需要顺序执行所有 T 个去噪步骤,因此更为耗时。
训练流程详解
现在,让我们深入理解扩散模型的训练过程。我们将使用U-Net作为去噪模型,你需要对U-Net架构有所了解。

训练的目标是让模型学会预测添加到数据中的噪声。以下是训练步骤的分解:
- 准备数据:取一个批量的真实数据
x0。 - 采样时间步:为批量中的每个样本随机采样一个时间步
t(范围从1到T)。 - 计算加噪数据:使用前向过程公式,为每个
x0和对应的t计算加噪后的xt。 - 模型预测:将
xt和对应的时间步t(经过嵌入处理)一起输入U-Net模型,得到模型预测的噪声epsilon_theta。 - 计算损失:计算模型预测的噪声
epsilon_theta与真实添加的噪声epsilon之间的均方误差(MSE)。 - 参数更新:通过反向传播计算梯度,并更新U-Net模型的参数。



需要注意的是,在完整的损失函数中,除了上述主要项,还应包含一项针对 t=1 时 x0 重建的损失。但在许多简化实现中,可能只使用噪声预测损失。
时间步嵌入
在将时间步 t 输入U-Net时,不能直接将其作为标量加入,因为 xt 是高维张量,标量 t 可能会被忽略。因此,我们需要对 t 进行嵌入(Embedding)。


以下是两种常见的嵌入方法:
- 正弦位置嵌入:这是Transformer中常用的方法,能更好地编码时间步的顺序信息。
- 简单通道添加:一种更简单的方法。例如,对于单通道的32x32图像,我们将
t扩展成一个32x32的矩阵(所有元素值都为t),然后将其作为额外通道与xt拼接。这样,输入就从[1, 32, 32]变成了[2, 32, 32]。本教程代码将采用这种方法。



代码实现:从理论到实践


理解了理论之后,我们开始结合代码进行实现。由于计算资源限制,部分演示在本地完成,但代码在Colab中同样可以运行。

1. 定义超参数与预计算量

首先,我们需要定义总时间步数 T 和一些关键的预计算系数。

import torch
import torch.nn as nn
T = 1000 # 总扩散步数
# 定义beta调度表,从1e-4线性增加到0.02
betas = torch.linspace(1e-4, 0.02, T)
# 计算alpha和alpha_bar
alphas = 1. - betas
alphas_bar = torch.cumprod(alphas, dim=0) # alpha_bar_t = prod_{s=1}^{t} alpha_s
# 计算后续公式中常用的平方根项
sqrt_alphas_bar = torch.sqrt(alphas_bar)
sqrt_one_minus_alphas_bar = torch.sqrt(1. - alphas_bar)



这些值(betas, alphas, alphas_bar等)在前向过程中是固定的,不需要学*。



2. 构建U-Net模型


我们将使用一个简化版的U-Net作为去噪模型。其输入是带噪图像 xt 和时间步嵌入,输出是预测的噪声。


class UNet(nn.Module):
def __init__(self, in_channels=2): # 输入通道为2 (xt + 时间嵌入)
super(UNet, self).__init__()
# 这里应定义下采样、瓶颈和上采样层
# 例如:Conv2d, BatchNorm2d, ReLU, MaxPool2d, ConvTranspose2d等
# 具体架构细节略,可参考标准U-Net实现
self.down1 = nn.Sequential(nn.Conv2d(in_channels, 64, 3, padding=1), nn.ReLU())
# ... 更多层定义
self.up1 = nn.Sequential(nn.ConvTranspose2d(128, 64, 2, stride=2), nn.ReLU())
# ... 输出层
self.out = nn.Conv2d(64, 1, 1) # 输出单通道噪声图
def forward(self, x, t_emb):
# t_emb 是时间步嵌入,需要调整形状并与x拼接
# 前向传播逻辑:下采样 -> 瓶颈 -> 上采样
# ...
return predicted_noise



3. 前向扩散过程(加噪)



这个函数根据给定的 x0 和时间步 t,计算加噪后的 xt。



def forward_diffusion(x0, t, sqrt_alphas_bar, sqrt_one_minus_alphas_bar):
"""
x0: 原始数据 [batch, channel, height, width]
t: 时间步索引 [batch],每个元素在[0, T-1]之间
"""
# 从标准高斯分布采样噪声
noise = torch.randn_like(x0)
# 通过索引获取对应时间步的系数
sqrt_alpha_bar_t = sqrt_alphas_bar[t].view(-1, 1, 1, 1)
sqrt_one_minus_alpha_bar_t = sqrt_one_minus_alphas_bar[t].view(-1, 1, 1, 1)
# 计算 xt
xt = sqrt_alpha_bar_t * x0 + sqrt_one_minus_alpha_bar_t * noise
return xt, noise


4. 训练步骤



在训练循环中,我们执行以下操作:
def train_step(model, x0, optimizer, t, sqrt_alphas_bar, sqrt_one_minus_alphas_bar):
model.train()
optimizer.zero_grad()
# 1. 前向扩散,获取加噪数据xt和真实噪声
xt, noise = forward_diffusion(x0, t, sqrt_alphas_bar, sqrt_one_minus_alphas_bar)
# 2. 准备时间步嵌入 (简单通道添加法)
# 将t扩展为与xt空间维度相同的张量
t_emb = t.view(-1, 1, 1, 1).expand(-1, 1, xt.shape[2], xt.shape[3]).float()
# 拼接xt和时间嵌入
model_input = torch.cat([xt, t_emb], dim=1)
# 3. 模型预测噪声
predicted_noise = model(model_input, t_emb) # 这里t_emb也可能用于其他方式的嵌入
# 4. 计算损失 (MSE between predicted and true noise)
loss = nn.MSELoss()(predicted_noise, noise)
# 5. 反向传播与优化
loss.backward()
optimizer.step()
return loss.item()

5. 反向采样过程(生成)



训练好模型后,我们可以通过反向过程从噪声生成图像。

@torch.no_grad()
def sample(model, sqrt_alphas_bar, sqrt_one_minus_alphas_bar, alphas, betas, img_shape, num_samples=16):
"""
从纯噪声开始,逐步去噪生成图像。
"""
model.eval()
# 1. 从标准高斯分布初始化 xT
x = torch.randn((num_samples, 1, *img_shape), device=device)
# 2. 从 T-1 逐步迭代到 0
for t in reversed(range(T)):
# 当前时间步
t_tensor = torch.full((num_samples,), t, device=device, dtype=torch.long)
# 准备时间嵌入
t_emb = t_tensor.view(-1, 1, 1, 1).expand(-1, 1, *img_shape).float()
# 拼接输入
model_input = torch.cat([x, t_emb], dim=1)
# 3. 模型预测噪声
predicted_noise = model(model_input, t_emb)
# 4. 计算 alpha_t, alpha_bar_t 等系数
alpha_t = alphas[t].view(-1, 1, 1, 1)
alpha_bar_t = sqrt_alphas_bar[t].view(-1, 1, 1, 1)
sqrt_one_minus_alpha_bar_t = sqrt_one_minus_alphas_bar[t].view(-1, 1, 1, 1)
# 5. 根据公式计算 x_{t-1}
# 简化公式: x_{t-1} = 1/sqrt(alpha_t) * (x_t - (1-alpha_t)/sqrt(1-alpha_bar_t) * predicted_noise) + sigma_t * z
# 其中当 t > 0 时,z ~ N(0, I)
if t > 0:
z = torch.randn_like(x)
else:
z = 0
x = (1 / torch.sqrt(alpha_t)) * (x - ((1 - alpha_t) / sqrt_one_minus_alpha_bar_t) * predicted_noise) + torch.sqrt(betas[t]) * z
# 6. 将像素值限制在合理范围
x = torch.clamp(x, 0.0, 1.0)
return x
运行结果与改进方向

运行上述代码进行训练(例如15个周期)后,可以进行采样。初始结果可能不理想,生成图像模糊或噪声较多。

这主要有两个原因:
- 训练不充分:在训练循环中,我们通常需要对一个批次内的数据采样多个不同的时间步t,并计算损失的平均值,而不是只用一个随机的t。这能提供更稳定的梯度信号。
- 损失函数不完整:如前所述,我们只实现了损失函数的主要部分(噪声预测损失),而忽略了
t=1时的重建损失项。添加此项有助于改善生成质量。
要获得更好的生成样本,你需要:
- 增加训练周期(Epoch)。
- 确保在训练批次中采样多个时间步。
- 考虑实现完整的损失函数。
- 可能使用更复杂的时间步嵌入(如正弦嵌入)和U-Net架构。

总结

本节课中我们一起学*了去噪扩散概率模型(DDPM)的代码实现。我们回顾了扩散模型的前向与反向过程,详细拆解了训练流程,并逐步实现了关键代码模块,包括超参数定义、U-Net构建、前向加噪、训练循环以及反向采样生成。


关键点在于理解扩散模型通过学*去噪来生成数据,以及时间步信息在模型中的重要性。虽然初始实现结果可能简单,但它构成了更高级扩散模型(如DDIM、Stable Diffusion)的基础。在下一个教程中,我们将探索DDPM的其他变体或改进实现。
048:W8T16 证明 🧮

在本教程中,我们将完成第八周课程中遗留的三个数学证明。我们将推导两个独立正态分布随机变量之和的分布,并计算两个正态分布之间的KL散度。这些推导是理解扩散模型等生成式AI模型数学基础的关键步骤。
两个独立正态分布随机变量之和 📊
上一节我们提到了几个未完成的证明,本节中我们首先来看第一个:两个独立正态分布随机变量之和的分布。
设随机变量 X 和 Y 相互独立,且:
- X ~ N(μₓ, σₓ²)
- Y ~ N(μᵧ, σᵧ²)
定义 Z = X + Y。我们的目标是证明 Z 也服从正态分布,并求出其均值 μ_z 和方差 σ_z²。
我们将使用特征函数法进行证明。正态分布 N(μ, σ²) 的特征函数公式为:
φ(t) = exp(iμt - (σ²t²)/2)
根据特征函数的性质,独立随机变量之和的特征函数等于各自特征函数的乘积。因此,Z 的特征函数为:
φ_z(t) = φ_x(t) * φ_y(t)
代入公式:
φ_z(t) = exp(iμₓt - (σₓ²t²)/2) * exp(iμᵧt - (σᵧ²t²)/2)
合并指数项:
φ_z(t) = exp(i(μₓ + μᵧ)t - ((σₓ² + σᵧ²)t²)/2)
观察上式,这正是均值为 (μₓ + μᵧ)、方差为 (σₓ² + σᵧ²) 的正态分布的特征函数。因此,我们得出结论:
Z ~ N(μₓ + μᵧ, σₓ² + σᵧ²)
这个结论可以推广到线性组合 aX + bY 的情况。基于此结果,你可以自行推导其分布。
两个正态分布之间的KL散度 📐
接下来,我们计算两个正态分布之间的KL散度。这是评估概率分布差异的重要度量。
设两个一维正态分布:
- P ~ N(μ₁, σ₁²)
- Q ~ N(μ₂, σ₂²)
KL散度的定义为:
KL(P || Q) = ∫ p(x) log(p(x)/q(x)) dx = E_{x~P}[log(p(x)/q(x))]
以下是计算步骤:
-
写出概率密度函数并代入公式:
KL(P || Q) = E_{x~P}[ log( (1/(√(2π)σ₁)) * exp(-(x-μ₁)²/(2σ₁²)) ) - log( (1/(√(2π)σ₂)) * exp(-(x-μ₂)²/(2σ₂²)) ) ] -
利用对数性质合并项:
= E_{x~P}[ log(σ₂/σ₁) + ( - (x-μ₁)²/(2σ₁²) + (x-μ₂)²/(2σ₂²) ) ] -
展开平方项 (x-μ₂)²,并利用期望的线性性质,将各项分开计算:
= log(σ₂/σ₁) + (1/2) * E_{x~P}[ - (x-μ₁)²/σ₁² + (x² - 2μ₂x + μ₂²)/σ₂² ] -
分别计算各项期望。已知对于 X ~ P:
- E[(x-μ₁)²] = σ₁²
- E[x] = μ₁
- E[x²] = μ₁² + σ₁²
-
将期望值代入表达式:
= log(σ₂/σ₁) + (1/2) * [ - (σ₁²/σ₁²) + ( (μ₁²+σ₁²) - 2μ₂μ₁ + μ₂² )/σ₂² ]
= log(σ₂/σ₁) + (1/2) * [ -1 + ( (μ₁ - μ₂)² + σ₁² )/σ₂² ] -
整理最终公式。通常我们使用方差比的形式,将 log(σ₂/σ₁) 改写为 (1/2)log(σ₂²/σ₁²),并进一步调整得到最终形式:
KL(P || Q) = log(σ₂/σ₁) + (σ₁² + (μ₁ - μ₂)²)/(2σ₂²) - 1/2
或者更常见的等价形式:
KL(P || Q) = (1/2)[ (μ₁ - μ₂)²/σ₂² + σ₁²/σ₂² - 1 - log(σ₁²/σ₂²) ]
这个一维情况的推导过程,为理解高维(向量值)正态分布的KL散度公式(涉及矩阵迹和行列式)提供了清晰的直觉。
总结 ✨
本节课中我们一起学*了两个核心推导:
- 独立正态变量之和:我们使用特征函数法证明了 N(μₓ, σₓ²) 与 N(μᵧ, σᵧ²) 之和服从 N(μₓ+μᵧ, σₓ²+σᵧ²)。
- 正态分布间的KL散度:我们通过展开期望、代入矩的计算,推导出了一维正态分布 P 与 Q 之间KL散度的解析表达式。


这些结论是构建和分析扩散模型等生成式AI算法的基石。虽然涉及一些代数运算,但每一步都基于概率论的基本定义和性质。理解这些推导有助于你更深入地掌握模型背后的数学原理。
049:DDPM的替代解释

概述
在本节课中,我们将学*扩散模型的条件生成能力。具体来说,我们将探讨如何修改去噪扩散概率模型的结构,使其能够从条件分布而非边缘分布中进行采样。为了实现这一目标,我们首先需要理解DDPM的几种不同数学表述。
DDPM的替代解释
上一节我们回顾了DDPM的基本框架。本节中,我们来看看对DDPM的两种重要替代解释。这些解释本质上是对同一组方程的不同参数化方式,但它们为理解和扩展模型提供了新的视角。
DDPM作为噪声预测器
这是DDPM原始论文中提出的著名解释之一。其核心思想是将模型视为对所添加噪声的回归器。
首先,回顾DDPM的前向过程。第t个潜在变量(即加噪后的数据)可以递归地定义为:
x_t = sqrt(α_t_bar) * x_0 + sqrt(1 - α_t_bar) * ε_t
其中,ε_t 是从标准正态分布 N(0, I) 中采样的噪声。
通过重新排列上述项,我们可以将原始数据 x_0 表示为 x_t 和所添加噪声 ε_t 的函数:
x_0 = (x_t - sqrt(1 - α_t_bar) * ε_t) / sqrt(α_t_bar)
现在,回顾我们之前推导的一致性损失项。它涉及真实反向分布 q(x_{t-1} | x_t, x_0) 的均值 μ_q 和模型预测分布 p_θ(x_{t-1} | x_t) 的均值 μ_θ 之间的差异。
μ_q 原本是 x_t 和 x_0 的线性组合。通过将上述 x_0 的表达式代入 μ_q 的公式,并进行代数整理,我们可以将 μ_q 重新参数化为 x_t 和 ε_t 的函数:
μ_q = (1 / sqrt(α_t)) * x_t - ((1 - α_t) / (sqrt(1 - α_t_bar) * sqrt(α_t))) * ε_t
类似地,我们可以将模型预测的均值 μ_θ 设计为具有相同结构的形式:
μ_θ = (1 / sqrt(α_t)) * x_t - ((1 - α_t) / (sqrt(1 - α_t_bar) * sqrt(α_t))) * ε_θ(x_t, t)
这里,ε_θ(x_t, t) 是一个神经网络,其输入是 x_t 和时间步 t。
当我们计算 μ_q 和 μ_θ 之间的差异时,许多系数会相互抵消。最终,一致性损失项(忽略常数因子)简化为:
L_t ∝ || ε_t - ε_θ(x_t, t) ||^2
结论:在这种解释下,DDPM的训练目标可以看作是让一个神经网络 ε_θ 去预测在前向过程中添加到原始数据 x_0 上的噪声 ε_t。模型输入是加噪样本 x_t 和时间步 t,输出是预测的噪声。
以下是这种解释的直观理解:
- 神经网络接收一个含噪图像
x_t。 - 它的任务是预测出为了从
x_0得到x_t所必须添加的噪声量ε_t。 - 通过最小化预测噪声和真实噪声之间的均方误差,模型学会了“理解”噪声是如何被添加的,从而在推理时能够逐步去除噪声。
DDPM作为分数预测器
接下来,我们看看第二种重要的解释:将DDPM视为对数据分布对数概率密度梯度(即分数)的预测器。这种解释建立了扩散模型与基于分数的生成模型之间的深刻联系。
分数函数的定义是数据对数概率密度对数据本身的梯度:
score(x) = ∇_x log p(x)
在DDPM的框架下,可以证明,在特定条件下,我们之前定义的噪声预测器 ε_θ(x_t, t) 与分数函数之间存在一个比例关系。
具体而言,对于在时间步 t 的加噪数据分布 p(x_t),其分数与预测噪声的关系*似为:
∇_{x_t} log p(x_t) ≈ - ε_θ(x_t, t) / sqrt(1 - α_t_bar)
推导思路:前向过程定义 x_t = sqrt(α_t_bar) * x_0 + sqrt(1 - α_t_bar) * ε。如果我们将 x_t 的分布视为 x_0 的先验分布与高斯噪声的卷积,那么根据Tweedie公式,其后验均值(即给定 x_t 下 x_0 的期望)与分数有关。结合我们之前 x_0 与 ε_t 的关系式,即可建立上述联系。
结论:在这种解释下,训练DDPM的噪声预测网络 ε_θ,等价于训练一个模型来估计各个噪声水平下数据分布的分数。反向(生成)过程则可以被视为一种沿着分数场(即指向数据高概率区域的方向)进行的迭代去噪过程,这类似于朗之万动力学采样。
两种解释的意义与关联
我们已经介绍了DDPM的两种核心替代解释。
- 噪声预测视角 更直观,直接对应于去噪任务。
- 分数匹配视角 更具理论深度,将扩散模型纳入了更广泛的基于分数的生成模型框架。
这两种视角是内在统一的。预测噪声本质上是在估计一个与分数成比例的量。这种等价性为理解扩散模型为何有效提供了坚实的基础,也为其扩展(如下一节将要讨论的条件生成)铺平了道路。
总结
本节课中,我们一起学*了DDPM的两种关键替代解释:
- 作为噪声预测器:模型学*预测前向过程中添加到数据上的噪声,其损失函数为
|| ε_t - ε_θ(x_t, t) ||^2。 - 作为分数预测器:模型学*估计数据分布在对数概率密度空间中的梯度(分数),建立了与基于分数的生成模型的联系。


理解这些不同的数学表述至关重要,它们不仅是理论上的优美结果,更是我们修改模型以实现条件生成、加速采样等高级功能的理论工具。在下一节中,我们将利用这些知识,探讨如何引导扩散模型进行条件生成。
050:DDPM作为分数预测器 🎯

在本节课中,我们将学*扩散模型(DDPM)的另一种重要解释:将其视为分数预测器。这种视角与分数匹配模型有很强的联系,并且对于理解条件生成任务非常有用。
概述:从统计公式到分数函数
上一节我们介绍了DDPM的几种等价解释。本节中,我们将从一个经典的统计学结果出发,推导出DDPM作为分数预测器的解释。
在统计学中,有一个著名的结果称为“Tweedie公式”。假设有一个高斯随机变量 T,其均值为 μ,方差为 σ²。那么,在观察到该随机变量的一个具体值 t 后,其真实均值的条件期望可以表示为:
E[μ | T = t] = t + σ² * ∇_t log p(t)
其中,∇_t log p(t) 被称为分布 p(t) 的分数函数。分数函数是对数似然函数关于随机变量本身的梯度。它指示了在随机变量的取值空间中,哪个方向能最大程度地增加数据的似然概率。
DDPM中的分数函数
现在,我们将这个公式应用到DDPM的框架中。回忆一下,在DDPM中,第 t 步的潜变量 x_t 的条件分布是高斯分布:
q(x_t | x_0) = N( x_t; √(ᾱ_t) * x_0, (1 - ᾱ_t) I )
其中,均值 μ = √(ᾱ_t) * x_0,方差 σ² = (1 - ᾱ_t)。
根据Tweedie公式,在给定观测值 x_t 的情况下,真实均值 √(ᾱ_t) * x_0 的最佳估计(即条件期望)为:
E[√(ᾱ_t) * x_0 | x_t] = x_t + (1 - ᾱ_t) * ∇_{x_t} log p(x_t)
这里,∇_{x_t} log p(x_t) 就是边际分布 p(x_t) 的分数函数。
由于我们知道真实均值就是 √(ᾱ_t) * x_0,因此我们可以将上述等式写为:
√(ᾱ_t) * x_0 = x_t + (1 - ᾱ_t) * ∇_{x_t} log p(x_t)
通过简单的代数重排,我们可以用 x_t 和分数函数来表示原始数据 x_0:
x_0 = (1 / √(ᾱ_t)) * x_t - ((1 - ᾱ_t) / √(ᾱ_t)) * ∇_{x_t} log p(x_t)
将一致性损失重写为分数匹配
接下来,我们利用这个关系来重新表达DDPM训练中的“一致性项”损失。回忆一下,一致性损失是真实后验均值 μ_q 和模型预测均值 μ_θ 之间的差异。
首先,我们将 μ_q 用分数函数表示。通过将上面 x_0 的表达式代入 μ_q 的定义式,并进行代数运算(此处省略详细步骤),可以得到:
μ_q = (1 / √(α_t)) * x_t - ((1 - α_t) / √(α_t)) * ∇_{x_t} log p(x_t)
遵循DDPM的一贯做法,我们将所有常数因子吸收进神经网络,并定义模型的预测目标。因此,我们可以类似地定义模型的预测均值 μ_θ 为:
μ_θ = (1 / √(α_t)) * x_t - ((1 - α_t) / √(α_t)) * s_θ(x_t, t)
这里,s_θ(x_t, t) 是一个神经网络,其任务是预测分数函数 ∇_{x_t} log p(x_t)。
现在,一致性损失函数 || μ_q - μ_θ ||² 就转化为(忽略常数缩放因子):
L = || ∇_{x_t} log p(x_t) - s_θ(x_t, t) ||²
这正是一个分数匹配的目标:训练一个神经网络 s_θ 来匹配真实数据分布的分数函数。
真实分数的计算与等价性
你可能会问:我们如何知道真实的分数函数 ∇_{x_t} log p(x_t) 是什么呢?这可以通过DDPM的前向过程推导出来。
我们知道,根据前向加噪过程,x_t 可以直接由 x_0 和所加的噪声 ε_t 定义:
x_t = √(ᾱ_t) * x_0 + √(1 - ᾱ_t) * ε_t,其中 ε_t ~ N(0, I)
由此可以解出 x_0:
x_0 = (x_t - √(1 - ᾱ_t) * ε_t) / √(ᾱ_t)
将这个表达式与我们之前通过Tweedie公式得到的 x_0 表达式联立:
(1 / √(ᾱ_t)) * x_t - ((1 - ᾱ_t) / √(ᾱ_t)) * ∇ log p(x_t) = (x_t - √(1 - ᾱ_t) * ε_t) / √(ᾱ_t)
通过比较和代数运算(建议作为练*),我们可以得到一个优美而关键的结论:
∇_{x_t} log p(x_t) = - ε_t / √(1 - ᾱ_t)
这个结果表明,真实数据分布的分数函数,本质上就是所添加噪声的负值,再经过一个缩放。分数指向噪声的反方向,这很直观:为了从带噪数据 x_t 回到干净数据 x_0(分布的高概率区域),我们需要沿着与所加噪声相反的方向移动。
核心结论与不同解释的等价性
由此,我们得到了DDPM的第四种解释:
DDPM可以被视为一个分数函数预测器(Score Predictor)。 神经网络 s_θ(x_t, t) 学*预测分数 ∇ log p(x_t)。
更重要的是,我们揭示了不同解释之间的深刻联系:
- 预测噪声:神经网络输出 ε_θ,目标是匹配添加的噪声 ε_t。
- 预测分数:神经网络输出 s_θ,目标是匹配分数函数 ∇ log p(x_t)。
由于 ∇ log p(x_t) ∝ - ε_t,因此预测噪声和预测分数是完全等价的。预测噪声的神经网络隐式地也在预测分数函数。
以下是DDPM几种核心解释的总结,它们本质上是同一模型的不同视角:
- 数据预测:直接预测原始数据 x_0。
- 均值预测:预测后验分布的均值 μ。
- 噪声预测:预测前向过程中添加的噪声 ε_t。(最常用)
- 分数预测:预测数据分布的分数函数 ∇ log p(x_t)。
所有这些解释对应的损失函数,彼此之间只相差一些常数缩放因子。因此,无论采用哪种解释,神经网络都在学*相同的内在规律。
总结
本节课中,我们一起学*了扩散模型(DDPM)作为分数预测器的解释。
- 我们从统计学中的Tweedie公式出发,引入了分数函数的概念,即对数似然关于数据本身的梯度。
- 我们将此公式应用于DDPM的高斯扩散过程,推导出可以用分数函数表示原始数据 x_0。
- 基于此,我们将DDPM的一致性训练目标重新表述为分数匹配问题,即让神经网络 s_θ 去逼*真实分布的分数。
- 通过分析DDPM的前向过程,我们证明了真实分数与所加噪声成比例,从而揭示了“预测噪声”和“预测分数”这两种解释的等价性。
- 最后,我们总结了DDPM的多种等价解释,并指出最常用的噪声预测视角隐含着分数预测的能力。


理解分数预测的视角至关重要,因为它为后续扩展到更复杂的生成模型(如条件生成、基于分数的生成模型)提供了清晰的理论桥梁。在需要引导生成过程时,操作分数函数往往比直接操作噪声更为直观和方便。
051:引导扩散模型

概述
在本节课中,我们将学*如何将扩散模型用于条件生成,即根据给定的条件(如文本描述或类别标签)生成数据。我们将探讨两种主要方法:分类器引导扩散和分类器自由引导扩散。
条件生成与引导扩散
上一节我们介绍了扩散模型的基本原理。本节中,我们来看看如何将其用于条件生成。
目前所有商业化的生成工具(如文本生成图像模型)本质上都是条件生成器。它们的目标不再是简单地从数据分布 ( p(x_0) ) 中采样,而是从条件分布 ( p(x_0 | y) ) 中采样。这里的 ( y ) 是条件变量。
以下是条件变量 ( y ) 的常见示例:
- 类别标签:例如,在图像数据集中,( y ) 可以表示“猫”、“狗”等类别。
- 文本嵌入:一段自然语言文本可以被编码成一个向量(嵌入),作为生成图像的条件。
为了实现条件生成,我们需要成对的数据 ( (x_0, y) ),例如(图像,文本描述)或(图像,类别标签)。我们的核心问题是如何修改去噪扩散概率模型,使其能够从条件分布 ( p(x_0 | y) ) 中采样。
我们将学*两种实现方法。
分类器引导扩散
第一种方法是分类器引导扩散。它利用了DDPM是分数预测器这一重要解释。
在无条件生成中,DDPM预测的是边际分布 ( p(x_t) ) 的分数(梯度):
[
\nabla_{x_t} \log p(x_t)
]
在条件生成中,我们的目标是预测条件分数 ( \nabla_{x_t} \log p(x_t | y) )。根据贝叶斯定理,我们可以将其分解:
[
\nabla_{x_t} \log p(x_t | y) = \nabla_{x_t} \log p(x_t) + \nabla_{x_t} \log p(y | x_t)
]
这个结果非常关键:
- 第一项 ( \nabla_{x_t} \log p(x_t) ) 是无条件分数,可以由我们的DDPM模型预测。
- 第二项 ( \nabla_{x_t} \log p(y | x_t) ) 是分类器梯度。它表示在给定噪声数据 ( x_t ) 时,条件 ( y ) 的对数概率梯度。
因此,要估计条件分数,我们需要同时获得无条件分数和分类器梯度。
以下是具体的实现步骤:
- 训练一个分类器:首先,我们需要一个预训练的分类器(或回归器,取决于 ( y ) 的类型)。这个分类器以噪声样本 ( x_t ) 为输入,预测条件概率 ( p(y | x_t) )。这个分类器需要在不同噪声水平 ( t ) 的数据上进行训练,这是一个具有挑战性的任务。
- 修改DDPM训练:在训练DDPM(即预测噪声 ( \epsilon_\theta ) 的模型)时,我们进行前向传播计算无条件分数。同时,我们将相同的 ( x_t ) 输入预训练的分类器,计算其输出 ( p(y | x_t) ),然后通过反向传播计算该输出相对于输入 ( x_t ) 的梯度,即分类器梯度 ( \nabla_{x_t} \log p(y | x_t) )。
- 组合梯度进行更新:在DDPM的反向传播步骤中,我们不只使用无条件分数的梯度来更新模型参数 ( \theta ),而是将无条件分数的梯度与分类器梯度相加,用这个总和来更新参数。这样,DDPM就学会了预测接*条件分数的值。
关于推理:在推理(生成)阶段,我们只需要训练好的DDPM模型。我们将条件 ( y ) 作为额外输入提供给模型,模型会输出条件分数的估计。然后,我们使用与标准DDPM完全相同的迭代去噪公式进行采样,只是将公式中的分数替换为条件分数估计。预训练的分类器在推理阶段不再使用。
这种方法的主要问题在于,其生成质量严重依赖于分类器的性能。而在高噪声水平 ( x_t ) 上训练一个准确的分类器是非常困难的。
分类器自由引导扩散
为了解决分类器引导的难题,研究者提出了分类器自由引导扩散。这种方法无需单独训练分类器,而是通过修改训练过程来*似条件分数。
我们从条件分数的贝叶斯分解开始:
[
\nabla_{x_t} \log p(x_t | y) = \nabla_{x_t} \log p(x_t) + \nabla_{x_t} \log p(y | x_t)
]
再次利用贝叶斯定理处理分类器项,并引入一个超参数 ( \lambda ) 进行缩放,我们可以得到一个*似表达式:
[
\nabla_{x_t} \log p(x_t | y) \approx \lambda \cdot \nabla_{x_t} \log p(x_t | y) + (1 - \lambda) \cdot \nabla_{x_t} \log p(x_t)
]
这里,( \lambda ) 是一个可调节的超参数。当 ( \lambda = 1 ) 时,上式退化为精确的条件分数;当 ( \lambda > 1 ) 时,会增强条件的影响。
这个公式表明,我们想要的条件分数,可以通过无条件分数和条件分数本身的凸组合来*似。关键在于,我们现在需要同时估计无条件分数和条件分数。
以下是具体的实现方式:
- 使用单一模型:我们只训练一个神经网络 ( \epsilon_\theta )。这个网络以噪声 ( x_t ) 和时间步 ( t ) 为输入,同时它也以条件 ( y ) 为输入。
- 随机丢弃条件进行训练:在训练过程中,我们以一定的概率(例如10%)将条件 ( y ) 设置为“空”(如零向量、空文本的嵌入)。这样:
- 当提供有效条件 ( y ) 时,模型学*预测条件分数 ( \nabla_{x_t} \log p(x_t | y) )。
- 当条件 ( y ) 被置为空时,模型学*预测无条件分数 ( \nabla_{x_t} \log p(x_t) )。
- 组合分数进行推理:在推理阶段,对于同一个 ( x_t ),我们运行模型两次:
- 一次输入有效条件 ( y ),得到条件分数估计 ( s_c )。
- 一次输入空条件,得到无条件分数估计 ( s_u )。
- 然后,我们按公式 ( s = \lambda \cdot s_c + (1 - \lambda) \cdot s_u ) 计算引导后的分数,并用它进行去噪采样。
这种方法避免了训练独立分类器的困难,简化了流程,并且被Stable Diffusion等当前最先进的文本到图像模型所广泛采用。超参数 ( \lambda ) 允许我们在生成样本的多样性和与条件的对齐程度之间进行权衡。
总结
本节课中,我们一起学*了如何扩展扩散模型以实现条件生成。
- 我们首先明确了条件生成的目标是从 ( p(x_0 | y) ) 采样。
- 接着,我们学*了分类器引导扩散,它通过引入一个预训练的分类器来提供梯度信号,引导生成过程朝向给定的条件 ( y )。
- 最后,我们探讨了更流行的分类器自由引导扩散,它通过在同一模型的训练中随机丢弃条件,并在线性组合条件与无条件分数估计的方式,实现了无需分类器的条件引导。这种方法更稳定,是当前的主流选择。


下一节,我们将讨论另一类扩散模型——去噪扩散隐式模型,它相比DDPM具有一些优势。
生成式AI的数学基础:P52:潜在扩散模型 🎼


在本节课中,我们将学*扩散模型。这是本课程的最后一个模块,我们将讨论两种在DDPM基础上的改进模型:潜在扩散模型和扩散隐式模型。这两种模型都在当前最先进的技术中广泛使用,相比基础的DDPM公式具有显著优势。
首先,我们来探讨潜在扩散模型。
潜在扩散模型,有时也被称为稳定扩散模型。它在算法层面没有新的突破,但在实现层面有所不同。其核心思想是,不在原始数据空间上构建扩散模型,而是在某个编码器-解码器结构所诱导的潜在空间上构建扩散模型。
核心思想
潜在扩散模型的基本思想是:在由另一个编码器-解码器模型诱导的潜在空间上构建扩散模型。
为什么要这样做呢?以图像数据为例,它们通常存在于非常高维的空间中。在这种极高维空间中创建马尔可夫链并进行操作,会在稳定性和正确学*分布等方面带来许多问题。
潜在扩散模型的思路是,在一个预先训练好的编码器-解码器模型的潜在空间上进行操作。具体步骤如下:
给定数据 x_0(我们假设它在某个维度为 D 的空间 R^D 中),第一步是学*一个潜在表示 z_0,它位于维度 K 的空间 R^K 中,且 K 远小于 D。这通过一个编码器-解码器模型实现。
一个常用于图像数据集的例子是构建一个我们之前见过的向量量化变分自编码器(VQ-VAE)来学*潜在空间。在VQ-VAE中,潜在空间是确定性的,即给定一个特定的 x_0,它会给出一个经过向量量化的确定性向量。
模型结构如下:有一个编码器 E_φ 和一个解码器 D_θ。x_0 输入编码器,得到潜在表示 z_0。解码器则根据 z_0 重建数据 x_0_hat。编码器和解码器函数 E_φ 和 D_θ 是预先训练的。
这些编码器和解码器函数使用原始数据 x_0 或与 x_0 相似的其他数据集进行预训练。预训练完成后,所有位于真实数据空间 R^D 中的数据点,都可以通过编码器投影到 K 维潜在空间。即:
z_0 = E_φ*(x_0)
完成投影后,我们在 z_0 的空间上构建一个DDPM扩散模型。
推理过程
在推理阶段,我们的最终目标是从真实数据对应的分布 P(x_0) 中生成一个新点,而不是从潜在空间生成。
为了从 P(x_0) 生成一个新点,我们首先需要从潜在空间中采样一个新点。由于扩散模型构建在数据的潜在空间上,我们通过反向扩散过程从潜在空间中采样一个新点,记作 z_novel。
然后,将这个新点 z_novel 输入预训练的解码器,得到最终的新点 x_novel:
x_novel = D_θ*(z_novel)
这样得到的 x_novel 就是从 P(x_0) 中采样的点。因为解码器正是以这种方式学*的。
优势总结
潜在扩散模型的关键优势在于:我们不是在原始的高维数据空间上构建扩散模型,而是在一个通过预训练编码器-解码器学*到的、维度更低、更紧凑的潜在空间上构建扩散模型。
目前大多数可用的实现都基于这一原则:首先在给定数据上预训练一个强大的编码器-解码器模型,然后在其潜在空间上构建扩散模型。在推理时,扩散模型学*如何从潜在空间生成潜在点,一旦获得潜在点,只需将其通过解码器,即可得到数据空间中的点。
这种方法也被称为稳定扩散,因为与直接在原始高维数据空间上构建扩散模型相比,它在生成图像类数据集时被观察到更加稳定。
从算法和理论角度看,潜在扩散模型没有改变DDPM的数学本质——它仍然是在一个随机变量(此处是 z 而非 x)上构建马尔可夫链。但从实现角度看,潜在扩散或稳定扩散是指先将数据投影到预训练编码器-解码器的潜在空间,然后在其上构建DDPM的过程。

本节课我们一起学*了潜在扩散模型的核心概念。我们了解到,其核心创新在于将扩散过程应用于一个低维的、由编码器-解码器学*到的潜在空间,而非原始高维数据空间。这带来了更好的稳定性和效率。在下一节中,我们将探讨另一种重要的改进模型:扩散隐式模型。



053:去噪扩散隐式模型(DDIMs)

在本节课中,我们将学*去噪扩散隐式模型(Denoising Diffusion Implicit Models, DDIMs)。这是一种与去噪扩散概率模型(DDPMs)非常相似的生成模型家族,但旨在解决DDPMs的两个主要限制:采样速度慢和缺乏确定性映射。我们将探讨DDIMs的动机、核心数学定义,以及它们如何在不改变训练过程的前提下,实现更高效的采样。
DDPMs的局限性
上一节我们介绍了DDPMs的基本原理。本节中,我们来看看DDPMs在实际应用中面临的两个主要问题。
1. 采样速度慢
DDPMs的生成过程需要从噪声图像 x_T 开始,逐步执行 T 次反向去噪步骤(通常 T 为数千步),才能得到最终样本 x_0。这个过程非常耗时。
2. 缺乏确定性映射(不可唯一逆推)
DDPMs的前向过程是随机的。给定一个数据点 x_0,运行前向过程多次会得到不同的最终潜在表示 x_T。这意味着:
- 不存在一个唯一的
x_T能通过反向过程确定性地生成给定的x_0。 - 因此,我们无法为一张给定的图像(如“森林中的老虎”)找到一个确定的潜在向量,从而难以在潜在空间中对图像进行精确的编辑操作(如将“老虎”替换为“大象”)。
以下是DDPMs的两个核心问题:
- 采样速度慢
- 缺乏唯一可逆性
为了解决这些问题,研究者提出了DDIMs。
DDIMs:非马尔可夫前向过程
DDIMs也是一种隐变量模型。与DDPMs不同,它定义了一族非马尔可夫的前向过程(即后验分布)。
我们考虑一族由标量参数 σ(σ ≥ 0)参数化的后验分布 q_σ:
q_σ(x_{1:T} | x_0) = q_σ(x_T | x_0) ∏_{t=2}^{T} q_σ(x_{t-1} | x_t, x_0)
其中:
q_σ(x_T | x_0)定义为均值为√α_T * x_0,方差为(1 - α_T)I的高斯分布。- 对于所有
t > 1,q_σ(x_{t-1} | x_t, x_0)定义为高斯分布,其均值和方差如下:
均值公式:
μ = √α_{t-1} * x_0 + √(1 - α_{t-1} - σ_t^2) * ( (x_t - √α_t * x_0) / √(1 - α_t) )
方差公式:
Σ = σ_t^2 * I
需要指出的是,这个定义本身不是一个马尔可夫链。x_t 不仅依赖于 x_{t-1},还依赖于原始数据 x_0。
DDIMs与DDPMs的关键联系
尽管DDIMs的前向过程与DDPMs不同,但它们有一个至关重要的共同点:
边际分布 q_σ(x_t | x_0) 与DDPMs中的完全相同。
即:
q_σ(x_t | x_0) = 𝒩(√α_t * x_0, (1 - α_t)I)
这个观察是DDIMs理论的基石。因为DDPMs的损失函数(证据下界,ELBO)只依赖于这些边际分布 q(x_t | x_0),而不依赖于前向过程的具体路径(即 q(x_{1:T} | x_0) 的联合分布形式)。
这意味着:
只要保证所有 t 的 q(x_t | x_0) 与DDPMs一致,任何形式的前向过程(包括我们定义的这族非马尔可夫过程)在优化ELBO时,都会得到与训练DDPMs完全相同的解(相差一个常数)。
换句话说:
通过训练一个标准的DDPM模型,我们实际上已经隐式地训练了无限多个不同 σ 参数的DDIM模型。
DDIMs的反向(生成)过程
训练完成后,我们需要定义如何从DDIM中采样(即推理过程)。我们定义参数化的反向过程 p_θ:
对于 t > 1:
p_θ^{(t)}(x_{t-1} | x_t) = q_σ(x_{t-1} | x_t, x̂_0)
其中 x̂_0 是由神经网络预测的 x_0 的估计值:
x̂_0 = (x_t - √(1 - α_t) * ε_θ(x_t, t)) / √α_t
对于 t = 1:
p_θ^{(1)}(x_0 | x_1) = 𝒩(μ_θ(x_1), σ^2 I)
这里,ε_θ(x_t, t) 就是训练DDPM时所用的同一个去噪神经网络。因此,DDIMs的训练过程与DDPMs完全一致。
DDIMs的采样算法
由于反向过程是非马尔可夫的,且依赖于预测的 x̂_0,我们可以设计出比DDPM更高效的采样算法。核心思想是:我们可以跳过一些时间步,因为 x_{t-1} 的计算直接依赖于预测的 x_0 和当前的 x_t,而不需要严格按顺序遍历所有中间状态。
一个确定性的采样步骤(当 σ_t = 0 时)可以表示为:
x_{t-1} = √α_{t-1} * x̂_0 + √(1 - α_{t-1}) * ε_θ(x_t, t)
通过选择一组递减的时间步子序列 {τ_1, τ_2, ..., τ_S},其中 S 远小于DDPM的总步数 T,我们可以仅用 S 步就从 x_T 生成 x_0,从而大幅提升采样速度。
总结
本节课中我们一起学*了去噪扩散隐式模型(DDIMs)。我们了解到:
- DDIMs旨在解决DDPMs采样慢和缺乏确定性映射的问题。
- DDIMs的核心是构造了一族非马尔可夫的前向过程,但其边际分布
q(x_t | x_0)与DDPMs保持一致。 - 一个关键结论是:训练一个DDPM模型等价于同时训练了所有可能的DDIM模型。两者的训练过程完全相同。
- DDIMs的优势体现在推理(采样)阶段。通过利用非马尔可夫特性,可以设计出能跳过大量步骤的采样算法,实现数十倍甚至百倍的加速,并且当
σ=0时,生成过程是确定性的,使得图像编辑等任务成为可能。


因此,DDIMs是在不增加额外训练成本的前提下,对DDPMs在推理效率和应用灵活性上的一个强大改进。
054:DDIM中的推理 🎼

在本节课中,我们将学*扩散模型中一个重要的推理方法——去噪扩散隐式模型(DDIM)。我们将了解DDIM如何从DDPM中推导出来,其推理过程有何不同,以及它为何在图像编辑等应用中具有优势。
概述
上一节我们介绍了扩散模型的基本框架。本节中,我们来看看DDIM中的推理过程。推理,本质上就是从反向过程中采样。
什么是推理?
推理就是从反向过程中采样。具体来说,就是从条件分布 P_θ(x_{t-1} | x_t) 中采样。对于一个训练好的DDPM模型,我们已得到最优参数 θ,因此这个分布就是 **P_{θ}(x_{t-1} | x_t)**。
根据定义,这个分布是一个高斯分布。其均值为 μ_θ*,方差为 σ_t^2。因此,采样过程就是从这个高斯分布中抽取样本。
通用采样过程
以下是DDIM的通用采样步骤。我们需要从高斯分布 P_{θ*}(x_{t-1} | x_t) 中采样。
- 首先,采样一个标准高斯噪声:z ~ N(0, I)。
- 然后,计算 x_{t-1}:
x_{t-1} = μ_{θ*} + σ_t * z
其中,均值 μ_{θ*} 的表达式为:
μ_{θ} = (1 / √α_t) * [ x_t - ( (1-α_t) / √(1-α_t) ) * ε_{θ}(x_t, t) ]
这里,ε_{θ*}(x_t, t) 是训练好的神经网络预测的噪声。将均值代入,完整的采样公式为:
x_{t-1} = (1 / √α_t) * [ x_t - ( (1-α_t) / √(1-α_t) ) * ε_{θ*}(x_t, t) ] + σ_t * z
这个公式定义了一个参数化的采样族,其中 σ_t 是一个非负的向量,不同的 σ_t 值对应不同的前向过程。关键在于,训练一个DDPM网络,就等价于训练了这一整个模型族。
DDPM:一个特例
当 σ_t 取一个特定值时,上述通用过程就退化成了标准的DDPM。
具体来说,当 σ_t 满足以下公式时:
σ_t = √( (1-α_{t-1})/(1-α_t) ) * √(1 - α_t/α_{t-1})
采样过程就与DDPM完全一致。这说明,DDPM只是这个更大家族模型中的一个特例。
DDIM:另一个特例
另一个重要的特例是当 σ_t = 0。
此时,采样公式中的随机噪声项 σ_t * z 完全消失。x_{t-1} 的表达式变为:
x_{t-1} = (1 / √α_t) * [ x_t - ( (1-α_t) / √(1-α_t) ) * ε_{θ*}(x_t, t) ]
这个采样过程是完全确定性的。唯一的随机性仅来自于初始噪声 x_T ~ N(0, I)。一旦 x_T 确定,整个反向过程就会生成唯一确定的图像 x_0。
DDIM的优势
DDIM(即 σ_t = 0 的情况)带来了两个关键优势:
- 可逆性:由于前向和反向过程都是确定性的,给定一张图像 x_0,通过DDIM的前向过程可以得到一个唯一的隐变量(噪声)x_T。这个过程称为 DDIM反转。反之,从这个 x_T 出发,通过反向过程也能确定性地重建回 x_0。
- 快速采样:DDIM的确定性特性允许使用更少的采样步数(即更小的 T)来生成高质量图像,从而大大加快了推理速度。
DDIM在图像编辑中的应用
DDIM的可逆性使其在图像编辑中非常有用。以下是典型的编辑流程:
- 对原始图像(例如“森林中的老虎”)进行 DDIM反转,得到其对应的唯一隐变量 x_T。
- 修改文本条件提示词(例如,改为“森林中的大象”)。
- 以修改后的提示词为条件,从 x_T 开始运行条件DDIM反向过程。
- 生成的图像将保持背景(森林)几乎不变,而主体对象(老虎)被替换为新对象(大象)。
如果没有这种确定性的可逆性,每次反转得到的 x_T 都可能不同,导致编辑后的图像背景也会发生变化。
总结
本节课中我们一起学*了DDIM的推理过程。核心思想是定义一个参数化的非马尔可夫过程族,它们与DDPM具有相同的边缘分布。因此,训练一个DDPM就相当于训练了整个模型族。通过改变推理过程中的 σ_t 参数,我们可以从不同的过程中采样。
- 当 σ_t 取特定值时,得到DDPM(随机过程)。
- 当 σ_t = 0 时,得到DDIM(确定性过程)。


DDIM因其确定性可逆性和更快的采样速度,在实践中被广泛采用。现代图像生成软件(如Stable Diffusion等)大多在隐空间中使用DDIM或其变体。虽然扩散模型目前主要应用于图像生成,但其思想也正被探索用于文本等序列数据的生成,我们将在后续学*自回归大语言模型时再次遇到相关概念。
055:自回归模型 🎼


在本节课中,我们将学*生成式AI中的另一类重要模型——自回归模型。我们将了解其基本定义、数学原理,并回顾其从经典模型到现代Transformer架构的发展历程。
概述
在之前的课程中,我们讨论了扩散模型。接下来我们将要讨论的生成模型被称为自回归模型。自回归模型在历史上被广泛用于时间序列分析和序列数据处理等任务。最*的例子包括基于Transformer的语言模型,它们构成了当今所有主流生成式模型的基础。
自回归模型的定义
上一节我们介绍了课程背景,本节中我们来看看自回归模型的具体定义。大多数自回归模型建立在具有序列性质的数据之上。
一个数据点 x 可以表示为一个序列:x = [x₁, x₂, ..., x_T]。请注意这里的符号变化:在扩散模型中,x₁ 到 x_T 表示不同的潜变量;而在自回归模型中,它们代表构成单个数据点的序列元素。
以下是序列数据点的例子:
- 自然语言句子:句子中的每个词是序列的一个元素。
- 多元时间序列。
- 图像:通过从左到右、从上到下扫描像素,图像也可以被视为序列数据。
我们的目标与整个课程一致:给定从未知分布 p(x) 中独立同分布采样的数据点,目标是估计 p(x) 并学会从中采样。我们通过参数化一个模型分布 p_θ(x),并通过最小化某个散度度量来学*参数 θ 来解决这个问题。数学上,这等价于最大化期望对数似然:
θ = argmax_θ E_{x~p(x)}[log p_θ(x)]*
自回归模型的不同之处在于其对 p_θ(x) 的特定参数化方式。


自回归模型的公式
在自回归模型中,序列数据点 x 的似然 p_θ(x) 通过概率链式法则建模:
p_θ(x) = p_θ(x₁) * p_θ(x₂|x₁) * p_θ(x₃|x₁, x₂) * ... * p_θ(x_T|x₁, x₂, ..., x_{T-1})
= ∏_{t=1}^{T} p_θ(x_t | x_{<t})
其中,x_{<t} 表示 x_t 之前的所有元素(x₁ 到 x_{t-1})。
这个模型被称为“自回归”,是因为每个元素 x_t 都被建模为依赖于其自身的历史(即之前的所有元素)。接下来,我们将看几个自回归模型的例子。
自回归模型的例子
以下是自回归模型发展过程中的几个重要类别:
1. 经典AR模型
这是历史悠久的经典模型。在P阶线性预测AR模型中,给定数据点 x_t 被建模为前P个数据点的线性组合加上噪声:

x_t = Σ_{i=1}^{P} a_i * x_{t-i} + ε_t

其中,a_i 是可学*的模型参数,ε_t 是高斯噪声。该模型假设数据来自高斯分布,适用于依赖关系不太复杂的序列。
2. 神经自回归模型
为了建模更复杂的序列(如自然语言),人们转向神经自回归模型。其中条件概率 p_θ(x_t | x_{<t}) 由某个神经网络建模:
p_θ(x_t | x_{<t}) = NeuralNetwork_θ(x_{<t})
这个神经网络可以是全连接多层感知机、卷积神经网络等。通过反向传播算法最大化似然目标来训练。
3. 循环神经网络
RNN是另一类重要的自回归模型,包括GRU、LSTM等变体。其核心思想是引入一个隐藏状态 h_t 来总结历史信息:
h_t = f_θ(h_{t-1}, x_{t-1})
x_t = g_φ(h_t)
其中,f_θ 和 g_φ 是可学*的非线性函数。RNN的关键特性是跨时间步的参数共享(θ 和 φ 保持不变),这使其能处理任意长度的序列。
然而,RNN也存在两个著名问题:
- 梯度消失/爆炸问题:当序列很长时,在反向传播中多次连乘参数会导致梯度变得极小或极大,使得模型难以训练。
- 信息压缩瓶颈:最后一个隐藏状态 h_T 需要压缩整个序列的历史信息来预测 x_T,对于长序列这可能不够。
注意力机制与Transformer
为了解决RNN的上述问题,研究者提出了注意力机制和基于注意力的模型,这最终催生了Transformer架构。Transformer完全摒弃了循环结构,转而使用自注意力机制来直接计算序列中任意两个元素之间的依赖关系,从而更有效地处理长程依赖,并成为当今大语言模型(如GPT、Gemini等)的基础。我们将在后续课程中详细探讨Transformer。
总结


本节课中我们一起学*了自回归模型的核心概念。我们了解到自回归模型通过链式法则将序列的联合分布分解为一系列条件分布的乘积。我们回顾了从经典的线性AR模型,到神经自回归模型和循环神经网络,再到现代基于注意力机制的Transformer的发展脉络。理解自回归模型是理解当前主流生成式AI(尤其是大语言模型)工作原理的关键基础。在接下来的课程中,我们将深入探讨Transformer的细节及其训练方法。
056:注意力机制 🎯



在本节课中,我们将学*注意力机制的核心概念。注意力机制是许多现代生成式AI模型(如Transformer)的基础。它的核心思想是让模型在处理整个序列时,能够动态地关注输入序列的不同部分。
从自回归模型到注意力机制
上一节我们介绍了自回归模型。在传统的自回归模型(如RNN)中,模型在计算当前时刻 t 的隐藏状态 H_t 时,需要依赖之前所有时刻 x1 到 x_{t-1} 的输入信息。模型通过 H_t 来“记住”历史信息。
然而,注意力机制采用了一种不同的方式。在计算每个隐藏状态 H_t 时,模型以一种可学*的、动态的方式去“看”输入序列的其他部分。这解决了传统模型难以捕捉长距离依赖关系的问题。
注意力机制的工作原理
接下来,我们来看看注意力机制具体是如何工作的。以下是其核心步骤:
我们给定一个输入数据序列 X,它包含 T 个标记(token),每个标记是一个 D 维向量。因此,输入 X 是一个 T x D 的矩阵。
第一步:计算查询、键和值
首先,我们需要为输入序列计算三组不同的表示,分别称为查询(Query)、键(Key)和值(Value)。
具体做法是,将输入矩阵 X 通过三个不同的可学*线性变换(矩阵)进行投影:
- 查询(Q):
Q = X * W_Q - 键(K):
K = X * W_K - 值(V):
V = X * W_V
其中,W_Q、W_K、W_V 是可学*的权重矩阵。通常,这三个投影空间的维度是相同的,我们记为 D_v。因此,Q、K、V 都是 T x D_v 的矩阵。矩阵的每一行对应一个输入标记在相应空间(查询、键、值)中的表示。
第二步:计算相似度(注意力分数)
现在,假设我们有一个特定的查询向量 q(即 Q 矩阵中的某一行),我们想计算它与所有键向量 k_i(即 K 矩阵的所有行)的相似度。
相似度通常通过点积来计算。对于查询 q 和所有键 K,其相似度向量 s 为:
s = q * K^T
这个 s 是一个 1 x T 的向量,它的第 i 个元素代表了查询 q 与第 i 个键 k_i 的相似度。直观上,这衡量了当前查询标记与序列中所有其他标记的“匹配”程度。
第三步:缩放与归一化
为了防止点积结果过大导致梯度不稳定,我们通常对相似度进行缩放:
s_scaled = s / sqrt(D_v)
接着,我们使用 Softmax 函数 将缩放后的相似度向量转换为一个概率分布(和为1,值在0到1之间):
alpha = softmax(s_scaled)
得到的 alpha 向量中的每个元素 alpha_i 可以理解为当前查询 q 对序列中第 i 个标记的“关注权重”。
第四步:计算加权和(注意力输出)
最后,注意力机制的输出是所有值向量 v_i 的加权和,权重就是上一步计算出的 alpha_i:
Attention(q, K, V) = sum_{i=1}^{T} (alpha_i * v_i)
这个输出向量就是查询 q 对应的注意力表示。它融合了序列中所有标记的信息,但根据 q 与各标记的“相关性”(由 alpha 决定)进行了加权。
矩阵形式的完整计算
在实际操作中,我们会对所有查询(即 Q 矩阵的所有行)同时进行计算。注意力机制的完整矩阵运算公式如下:
Attention(Q, K, V) = softmax( (Q * K^T) / sqrt(D_v) ) * V
这个公式一次性为序列中的所有标记计算了其对应的注意力表示。
总结


本节课中,我们一起学*了注意力机制。我们了解到,注意力机制通过为输入序列计算查询(Q)、键(K)、值(V)三组表示,并利用点积相似度和Softmax归一化来动态计算每个位置应“关注”序列中其他位置的权重。最终,输出是值(V)的加权和。这种机制使得模型能够直接捕捉序列中任意两个位置之间的关系,克服了传统循环神经网络在处理长序列时的局限性,成为Transformer等强大模型的核心组件。
057:用于自回归模型的Transformer

概述
在本节课中,我们将学*Transformer架构,这是一种专门用于自回归建模的流行架构。我们将详细探讨其核心组件——注意力机制,并了解它如何被用来建模序列数据中下一个标记的概率分布。
自回归模型回顾
上一节我们介绍了自回归模型。本节中,我们来看看如何用Transformer架构来实现它。
自回归模型处理具有序列形式的数据。假设我们有一个数据点,其序列形式可表示为 x₁, x₂, x₃, …, x_T。请注意,这里的 x₁ 到 x_T 表示一个单一的数据点,该数据点具有序列性质。自然语言就是一个例子,其中每个句子是一个数据点,由一系列词或字符(通常称为标记)组成。
自回归模型对该数据点施加的概率密度函数(也称为似然函数)由以下公式给出:
p_θ(x₁, …, x_T) = ∏_{t=1}^T p_θ(x_t | x_{<t})
其中,每个条件密度函数 p_θ(x_t | x_{<t}) 捕获了在给定序列中所有前序标记 x_{<t} 的情况下,第 t 个标记 x_t 出现的可能性。这就是“自回归”名称的由来——模型对数据自身进行回归。
每个标记 x_t 通常属于一个有限的离散集合 V,称为词汇表。现代自回归模型的词汇表大小可达数万甚至数十万。
Transformer架构简介
我们的核心任务是建模条件分布 p_θ(x_t | x_{<t})。Transformer是一种用于建模此分布的特定架构选择。它主要基于注意力机制的思想。
Transformer本身不是一种生成模型的方法论,而是一种架构。当前最先进的生成模型(如GPT系列)主要基于Transformer架构的自回归建模。
Transformer的核心:注意力机制
Transformer架构的核心是注意力机制。它用于将输入序列的表示从一个空间转换到另一个空间,同时量化序列中每个标记对其他标记表示的重要性。
以下是实现注意力机制的关键步骤。
步骤1:输入表示与嵌入
我们从一个数据矩阵 X 开始,其维度为 T × D_m,其中 T 是序列长度,D_m 是模型维度(一个可任意选择的超参数,如512或768)。矩阵的每一行对应序列中的一个标记。
初始时,每个标记 x_i 使用其词汇表中的独热编码表示。然后,通过一个可学*的嵌入层,将独热向量投影到 D_m 维的连续空间。这个嵌入矩阵 X 本身也是模型训练过程中通过梯度下降学*的参数。
步骤2:计算查询、键和值矩阵
给定嵌入矩阵 X,Transformer的第一步是计算三个不同的投影:查询矩阵 Q、键矩阵 K 和值矩阵 V。
计算公式如下:
Q = X W_Q
K = X W_K
V = X W_V
其中:
- W_Q ∈ ℝ^{D_m × D_k} 是可学*的查询权重矩阵。
- W_K ∈ ℝ^{D_m × D_k} 是可学*的键权重矩阵。
- W_V ∈ ℝ^{D_m × D_v} 是可学*的值权重矩阵。
D_k 和 D_v 是超参数。通常,为了便于计算点积注意力,D_k(查询和键的维度)被设置为相同,而 D_v(值的维度)可以不同。在仅解码器模型中,三者常被设为相同。
经过此步骤,我们将原始数据从 D_m 维空间投影到了三个不同的子空间:查询空间、键空间和值空间。Q 和 K 的维度为 T × D_k,V 的维度为 T × D_v。
步骤3:计算注意力权重
接下来,需要计算注意力权重。注意力权重通过查询矩阵 Q 和键矩阵 K 的点积,并经过Softmax归一化得到。
注意力权重矩阵 A 的计算公式为:
A = softmax( (Q K^T) / √{D_k} )
这里,Q K^T 得到一个 T × T 的矩阵,其中每个元素 A_{i,j} 表示第 i 个查询向量(对应第 i 个标记)与第 j 个键向量(对应第 j 个标记)之间的关联强度。除以 √{D_k} 是为了稳定梯度。Softmax函数沿着键的维度(即矩阵的行)进行归一化,使得每一行的权重之和为1。
步骤4:计算注意力输出
最后,利用注意力权重矩阵 A 对值矩阵 V 进行加权求和,得到注意力机制的输出矩阵 Z。
计算公式为:
Z = A V
输出矩阵 Z 的维度为 T × D_v。它的每一行是值向量 V 的加权组合,权重由对应标记的注意力分数决定。直观上,Z 的第 t 行包含了序列中所有标记的信息,但根据它们与第 t 个标记的相关性进行了重新加权。
自注意力机制
在上述过程中,查询、键和值都来自同一个输入序列 X。这种机制被称为自注意力。它允许模型在处理序列时,动态地关注并整合序列中任何其他部分的信息,从而学*每个标记更丰富的上下文表示。
如果查询和键/值来自不同的序列(例如在编码器-解码器架构中),则称为交叉注意力。
从注意力到自回归建模
回顾我们的目标:建模 p_θ(x_t | x_{<t})。注意力机制 Z 为我们提供了序列中每个标记基于全局上下文的增强表示。
然而,标准的自注意力允许标记“看到”序列中所有位置的信息,包括未来的标记。这对于自回归生成来说是不允许的,因为在生成 x_t 时,模型只能依赖于 x_{<t}。
因此,在自回归Transformer中,我们使用因果注意力(或掩码注意力)。这通过在计算注意力权重时,添加一个掩码矩阵来实现。该掩码矩阵将 Q K^T 矩阵中对应于未来位置(即 j > i)的元素设置为一个非常大的负数(如 -∞),这样在Softmax之后,这些位置的注意力权重就变为0。
因果注意力的计算公式为:
A = softmax( (Q K^T + M) / √{D_k} )
其中 M 是一个上三角矩阵,主对角线及以下为0,以上为 -∞。
通过这种方式,第 t 个标记的表示 z_t 仅依赖于序列中前 t 个标记(x_{<t})的信息。这个 z_t 随后会被送入一个前馈神经网络,最终通过一个Softmax层映射到词汇表 V 上的概率分布,即 p_θ(x_t | x_{<t})。
总结
本节课中,我们一起学*了Transformer架构如何用于自回归建模。
我们首先回顾了自回归模型的目标是建模序列数据的联合概率。接着,我们深入探讨了Transformer的核心——注意力机制,详细介绍了从输入嵌入到计算查询、键、值矩阵,再到计算注意力权重和输出的完整过程。我们特别强调了在自回归生成中必须使用的因果注意力掩码,以确保模型在预测下一个标记时不会“偷看”未来信息。


Transformer通过自注意力机制,能够高效地捕捉序列中长距离的依赖关系,这使其成为当今最强大生成模型(如大语言模型)的基石架构。
058:Transformer架构

概述
在本节课中,我们将学*Transformer架构中的两个核心概念:因果注意力(Causal Attention)与多头注意力(Multi-head Attention)。我们将了解如何通过修改注意力机制来确保模型的自回归特性,以及如何通过多头设计来增强模型的表达能力。
因果注意力(Causal Attention)
上一节我们介绍了基本的注意力机制。本节中我们来看看如何确保注意力机制符合自回归模型的要求。
在自回归模型中,第 t 个标记 x_t 仅依赖于其之前的标记 x_1 到 x_{t-1},而不依赖于未来的标记。然而,我们之前定义的注意力权重计算方式,假设了序列中的每个标记都依赖于所有其他标记,无论其位置如何。
为了确保我们构建的模型是真正的自回归模型,必须修改注意力机制,使其只关注过去的标记。这是通过一个称为掩码矩阵(Mask Matrix)的机制来实现的。
掩码矩阵的定义
定义一个掩码矩阵 M,其维度为 T x T(T 是序列长度)。该矩阵的元素定义如下:
- 如果
j < i(即j是i的过去),则M_{ij} = 0。 - 如果
j >= i(即j是i的现在或未来),则M_{ij} = -∞(一个非常大的负数)。
修改注意力权重
原始的注意力权重 A 计算为:
A = softmax( (Q * K^T) / sqrt(d_k) )
为了引入因果性,我们将掩码矩阵 M 按元素加到 (Q * K^T) / sqrt(d_k) 上,然后再进行 softmax 操作:
A_masked = softmax( (Q * K^T) / sqrt(d_k) + M )
由于 M 中未来位置的值是 -∞,在 softmax 的指数运算中,这些位置对应的权重会趋*于 0。这样,每个标记的表示就只依赖于其过去的标记,而不依赖于未来。
通过添加掩码矩阵 M 这一技巧,我们确保了所构建的模型具有自回归性质。得到的 A_masked 再与值矩阵 V 相乘,就得到了符合因果依赖关系的数据新表示。
多头注意力(Multi-head Attention)
在理解了如何实现因果注意力后,我们接下来探讨另一个提升模型能力的关键技术:多头注意力。
到目前为止,我们讨论的注意力机制是将 m 维的数据向量一次性投影到 d_v 维空间。这意味着所有 m 个数据维度都使用同一组查询、键、值矩阵进行变换。
然而,为了提高模型的表达能力,我们可以将数据维度 d_m 划分为多个子部分,并为每个子部分学*独立的查询、键、值矩阵。这就是多头注意力的基本思想。
多头注意力的定义
设 h 为一个正实数标量,代表注意力头(Attention Heads)的数量。通常,我们会设置 d_k = d_m / h,即将模型维度 d_m 均匀地分割给 h 个头。
对于第 j 个头(j 从 1 到 h),我们执行以下操作:
- 计算该头独有的查询、键、值矩阵:
Q_j = X * W_j^Q(维度:T x d_k)K_j = X * W_j^K(维度:T x d_k)V_j = X * W_j^V(维度:T x d_v)
其中W_j^Q,W_j^K,W_j^V是可学*的参数矩阵。
- 计算该头的注意力输出
Z_j:
Z_j = Attention(Q_j, K_j, V_j) = softmax( (Q_j * K_j^T) / sqrt(d_k) ) * V_j
Z_j的维度为T x d_v。
合并多头输出
计算完所有 h 个头的输出后,我们将它们沿着特征维度进行拼接(Concatenate):
Z_concat = Concat(Z_1, Z_2, ..., Z_h)
此时 Z_concat 的维度为 T x (h * d_v)。通常我们会设置 h * d_v = d_m,以恢复到原始的模型维度 d_m。
最后,为了灵活地组合各个头的信息,我们使用一个额外的可学*投影矩阵 W^O 对拼接后的结果进行线性变换,得到最终的多头注意力输出 Z:
Z = Z_concat * W^O
其中 W^O 的维度为 (h * d_v) x d_m,因此 Z 的维度为 T x d_m。
简而言之,多头注意力不是学*一组全局的查询、键、值矩阵,而是将数据维度拆分,为每个子空间学*独立的注意力机制,最后再将结果合并并投影回目标维度。这增加了模型的表达能力和灵活性。
前馈神经网络层(Fully Connected Layers)
在Transformer架构中,计算完多头注意力后,通常会接着应用一个或多个全连接层(前馈神经网络)。
注意力机制主要由矩阵乘法构成,这些是线性操作。为了建模自然语言处理等任务中复杂的非线性输入-输出关系,仅靠线性变换是不够的。
根据神经网络理论,单隐藏层的前馈神经网络是通用函数逼*器。只要隐藏层有足够多的神经元,它可以以任意精度逼*任何函数。因此,在多头注意力层之后添加全连接层,能为整个架构引入非线性,从而极大地增强其表达能力和灵活性。
前馈层的应用方式
值得注意的是,这些全连接层是独立地应用于每个标记(Token)的。假设经过注意力层后,我们得到序列表示 Z,其维度为 T x d_m(T 个标记,每个是 d_m 维向量)。
对于序列中的第 j 个标记 Z_j(一个 d_m 维向量),我们对其单独应用前馈网络。一个典型的单隐藏层前馈网络操作如下:
Z_j' = σ( Z_j * W_1 + b_1 ) * W_2 + b_2
其中:
σ是非线性激活函数(如 ReLU, Sigmoid, Tanh)。W_1,b_1,W_2,b_2是可学*参数。W_1的维度通常是d_m x d_ff(d_ff是隐藏层维度),W_2的维度是d_ff x d_m。- 输出
Z_j'的维度与输入Z_j相同,均为d_m。
这样,我们对 T 个标记分别进行变换,得到新的表示序列 Z',其维度仍为 T x d_m。
输出层与概率分布生成
经过前馈网络层处理后,我们得到了每个标记的最终表示 Z_j‘。接下来,我们需要将这些表示转换为对词汇表中所有可能标记的概率分布,这是生成任务的核心。
线性投影到词汇表空间
首先,通过一个线性投影层,将 d_m 维的标记表示映射到词汇表大小 V 维的空间:
S_j = Z_j' * W_p + b_p
其中 W_p 是可学*参数矩阵,维度为 d_m x V。S_j 是一个 V 维的实值向量。
通过Softmax生成概率分布
这个 V 维向量 S_j 包含了模型对于下一个标记是词汇表中每个词的可能性“分数”。为了将其转化为一个合法的概率分布(即所有概率之和为1),我们应用 softmax 函数:
ŷ_j = softmax( S_j )
ŷ_j 的第 i 个分量 ŷ_{j,i} 表示在给定上下文和当前位置 j 的情况下,下一个标记是词汇表中第 i 个词的概率。
通过这种方式,Transformer架构的最终输出,对于输入序列中的每一个位置 j,都给出了一个在完整词汇表上的离散概率分布 ŷ_j。在训练时,我们可以通过比较这个预测分布与真实的下一个标记(使用交叉熵损失)来优化模型参数。在推理(生成)时,我们可以从这个分布中采样或取最可能的词,作为模型生成的输出。


总结
本节课我们一起学*了Transformer架构中确保自回归特性的因果注意力机制,以及提升模型表达能力的多头注意力设计。我们还了解了前馈神经网络层如何为模型引入必要的非线性变换。最后,我们看到了模型如何通过线性投影和softmax函数,将内部的连续表示转化为对词汇表的概率分布,从而完成生成任务的核心步骤。这些组件共同构成了现代生成式AI模型,特别是解码器(Decoder)部分的基础。
059:跳跃连接与归一化

在本节课中,我们将学*Transformer架构中的两个关键组件:跳跃连接(或称残差连接)与层归一化。理解它们的作用和实现方式,对于掌握现代生成式AI模型至关重要。
概述
在深入探讨如何训练自回归模型之前,有必要先描述几个重要的模型组件。其中之一就是跳跃连接与层归一化。本节将详细解释这两个概念。
跳跃连接
跳跃连接,或称残差连接,是深度神经网络中的一种设计。其核心思想是在网络的某一层或某个模块中,将输入直接“跳过”该层的处理,并与该层的输出相加。
以下是其基本公式:
公式:
y = x + F(x)
其中:
x是输入。F(x)是当前层或模块对输入x进行的变换操作。y是最终输出。
这种设计也被称为残差连接,并构成了如ResNet等架构的基础。
跳跃连接的目的在于确保网络架构具备显式地学*恒等映射的能力。观察上述公式,整个模块的输出是“恒等操作(即输入 x)”加上“该模块预期执行的操作 F(x)”。在某些情况下,如果我们希望网络的后续阶段不受当前模块影响,或者当前模块无需对特定特征进行变换,跳跃连接就提供了一条直接的路径,让输入可以原封不动地传递到输出。这相当于在架构层面施加了一种正则化,强制网络学*恒等映射。
在实践中,跳跃连接解决了深度网络训练中的一个难题。研究表明,单纯增加网络深度并不总能带来更好的学*效果,有时甚至会导致性能下降或过拟合。跳跃连接的引入,确保了即使在增加深度时,网络也能稳定地学*,因为它保留了将输入直接传递的可能性,从而缓解了梯度消失等问题。在基于Transformer的架构中,跳跃连接被广泛使用。
层归一化
层归一化是一种对神经网络中每一层的特征向量进行标准化的技术。其目的是稳定训练过程,使数据分布保持相对稳定。
以下是层归一化的计算过程:
公式:
给定一个维度为 D_m 的输入向量 x:
- 计算均值
μ:
μ = (1 / D_m) * Σ_{k=1}^{D_m} x_k - 计算方差
σ²:
σ² = (1 / D_m) * Σ_{k=1}^{D_m} (x_k - μ)² - 进行归一化:
x_norm = (x - μ) / √(σ² + ε)
(其中ε是一个很小的数,如0.01,用于防止除以零的错误) - 应用可学*的缩放和平移参数:
y = γ * x_norm + β
其中 γ 和 β 是可学*的参数。
层归一化的思想很简单:它确保给定向量经过处理后,具有零均值和单位方差。具体做法是计算向量在所有维度上的均值和方差,然后进行标准化。
那么,参数 γ 和 β 的作用是什么呢?虽然将特征向量中心化并归一化到单位方差通常是个好主意,但有时这可能并不合适。为了确保模型能够自主选择是否应用归一化效果,我们引入了这两个可学*的参数。从数学上很容易证明,通过选择合适的 γ 和 β 值,可以“撤销”括号内的归一化操作。如果模型认为需要层归一化,γ 可以趋*于1,β 趋*于0;如果模型认为不需要,它可以通过学* γ 和 β 来恢复原始输入 x 的分布。这就是Transformer中典型的层归一化过程。
在Transformer中的结合应用
在Transformer架构中,跳跃连接和层归一化通常结合使用。
以下是其结合方式:
公式:
输出 = LayerNorm( 输入 + Sublayer(输入) )
其中 Sublayer 可以代表架构中的一个块,例如注意力块或前馈全连接层块。
这个公式的含义是:
输入 + Sublayer(输入)部分实现了跳跃连接(残差连接)。- 然后,对这个相加后的结果应用层归一化
LayerNorm。
这种设计(残差连接后接层归一化)是Transformer每个子层(如自注意力层和前馈网络层)的标准配置,它极大地促进了深层网络的稳定和高效训练。
总结


本节课我们一起学*了Transformer架构中的两个核心组件:跳跃连接与层归一化。跳跃连接通过将输入直接加到输出上,帮助网络学*恒等映射并缓解深度网络训练中的梯度问题。层归一化则通过标准化每层的激活值来稳定训练过程,其引入的可学*参数 γ 和 β 赋予了模型灵活选择是否应用归一化的能力。在Transformer中,这两者通常结合使用,构成了 输出 = LayerNorm(输入 + Sublayer(输入)) 的标准子层结构,为构建强大的生成式AI模型奠定了坚实的基础。
生成式AI的数学基础:P60:Transformer架构中的位置嵌入

在本节课中,我们将学*Transformer架构中的一个关键组成部分:位置嵌入。我们将了解为什么需要它,它是如何工作的,以及它在整个Transformer模型中的位置。
概述
Transformer模型在处理输入序列时,面临一个根本性问题:它本身无法感知序列中元素的顺序。因为输入通常被表示为独立的向量,模型无法知道哪个词先出现,哪个词后出现。为了解决这个问题,我们需要向模型注入关于序列顺序的信息,这就是位置嵌入的作用。
上一节我们介绍了Transformer的基本注意力机制,本节中我们来看看如何让模型“记住”顺序。
为什么需要位置嵌入?
Transformer的输入层将每个词元(token)编码为一个向量。然而,这些向量本身不包含任何关于该词元在原始句子中位置的信息。模型无法区分“猫追老鼠”和“老鼠追猫”,因为词向量“猫”和“老鼠”是相同的,无论它们出现在哪里。
简而言之,模型需要一种方法来保留输入序列的顺序信息。
什么是位置嵌入?
位置嵌入是一个与输入词元向量维度相同的固定向量。这个向量被加到对应的词元向量上,从而将位置信息编码到输入表示中。这个过程也称为位置编码。
这与我们在扩散模型中看到的思路类似,都是将时间或顺序信息作为额外输入提供给模型。
有多种方法可以实现位置编码,其中一种著名的方法是使用正弦余弦编码。
正弦余弦位置编码公式
假设我们有一个序列,其中每个位置用索引 j 表示。模型的嵌入维度是 D_model。那么,位置 j 的编码向量 P(j) 定义如下:
对于向量中偶数索引(i = 0, 2, 4, ...)的元素:
P(j, 2i) = sin( j / (10000^(2i / D_model) ) )
对于向量中奇数索引(i = 1, 3, 5, ...)的元素:
P(j, 2i+1) = cos( j / (10000^(2i / D_model) ) )
代码描述:
import numpy as np
def get_positional_encoding(position, d_model):
angle_rates = 1 / np.power(10000, (2 * (np.arange(d_model)//2)) / np.float32(d_model))
angle_rads = position * angle_rates
# 对偶数索引应用sin,奇数索引应用cos
pos_encoding = np.zeros(d_model)
pos_encoding[0::2] = np.sin(angle_rads[0::2]) # 偶数索引
pos_encoding[1::2] = np.cos(angle_rads[1::2]) # 奇数索引
return pos_encoding
这个公式为每个位置 j 生成一个唯一的、固定长度的向量 P(j)。这个向量会被加到对应位置的输入词元嵌入向量上。
在Transformer架构中的位置
现在,让我们看看位置嵌入如何整合到完整的Transformer架构中。
以下是Transformer的一个基础架构流程:
- 输入嵌入:首先,输入的词元(通常是one-hot向量)通过一个可学*的嵌入矩阵,被转换为稠密的词嵌入向量
X。 - 添加位置编码:将计算得到的位置嵌入向量
P加到词嵌入向量X上。即,对于序列中位置j的输入,其最终输入表示为X(j) + P(j)。 - 多头注意力块:带有位置信息的输入向量被送入多头注意力模块进行处理。
- 残差连接与层归一化:注意力模块的输出会与该模块的原始输入(即
X(j) + P(j))进行相加(残差连接),然后进行层归一化。这有助于稳定训练。 - 前馈神经网络:归一化后的结果通过一个全连接层(前馈网络)。
- 再次残差连接与层归一化:全连接层的输出会与进入全连接层之前的向量再次进行残差连接和层归一化。
- 线性层与Softmax:最后,经过处理的向量通过一个线性层和Softmax函数,输出最终的预测结果(例如,下一个词的概率分布)。
这个基础块(多头注意力 + 前馈网络,各自带有残差连接和归一化)可以被堆叠多次,构成更深的Transformer模型。
总结
本节课中我们一起学*了Transformer模型中的位置嵌入机制。我们了解到:
- 目的:为了解决Transformer模型无法感知输入序列顺序的问题。
- 方法:为序列中的每个位置生成一个独特的、固定的向量(位置嵌入),并将其加到对应的词元嵌入向量上。
- 经典实现:使用正弦和余弦函数生成位置编码,确保模型能够捕捉到相对和绝对位置信息。
- 架构整合:位置嵌入在输入处理阶段被加入,随后数据流经包含多头注意力、前馈网络、残差连接和层归一化的标准Transformer块。


位置嵌入是Transformer能够成功处理序列任务(如机器翻译、文本生成)的基础组件之一。
061:Transformer的训练与推理

概述
在本节课中,我们将学*如何训练和推理基于Transformer架构的自回归生成模型。我们将首先介绍使用“教师强制”方法的训练过程,然后探讨几种常见的推理策略,包括贪婪解码、采样以及它们的变体。
训练:使用教师强制法
上一节我们介绍了Transformer的架构,本节中我们来看看如何使用这种架构来训练一个自回归生成模型。
我们被给定一个标记序列:x1, x2, ..., xT,其中每个 xt 属于词汇表 V。在自回归模型中,我们的目标是学*整个序列的联合概率,并将其建模为条件概率的乘积:
P(x1, ..., xT) = ∏ Pθ(xt | x<t)
这里的 Pθ(xt | x<t) 正是Transformer模型在位置 t 的输出所表示的概率分布。
为了以无监督的方式训练模型,我们采用一种称为“教师强制”的方法。其核心思想如下:
- 输入标记:我们取序列
x1, x2, ..., xT-1(丢弃最后一个标记)。 - 目标标记:我们取序列
x2, x3, ..., xT(即输入标记向右移动一位)。
通过这种方式,我们隐式地要求Transformer模型根据之前的所有标记 x<t 来预测当前标记 xt。损失函数定义为负对数似然:
L = -∑ log(ŷt, xt)
其中,ŷt, xt 是Transformer在时间步 t 输出的、对应于标记 xt 的概率。具体来说,ŷt 是一个在词汇表 V 上的softmax分布:
ŷt = softmax(žt)
因此,ŷt, xt 就等于模型预测标记 xt 为正确下一个标记的概率。我们通过最小化这个损失函数来训练模型。
这种训练是完全无监督的。虽然也可以用于有监督的序列对任务,但在标准的语言模型预训练中,目标标记是通过这种“教师强制”方式自动生成的。
推理:生成新序列
训练完成后,我们进入推理阶段,即使用训练好的模型生成新的序列。自回归模型的推理有多种方式,以下是几种常见的方法。
贪婪解码
这是一种最简单直接的方法。在每一个时间步 t,模型会输出一个在词汇表上的概率分布 ŷt。贪婪解码选择概率最高的那个标记作为输出:
y*_t = argmax_v ŷt, v
以下是贪婪解码的特点:
- 优点:速度非常快,计算简单,并且是确定性的(相同的输入总是产生相同的输出)。
- 缺点:缺乏多样性,输出可能过于保守和重复,不适合需要“创造性”输出的场景。
完全采样
与贪婪解码的确定性相反,完全采样从模型输出的整个概率分布中进行随机采样:
y*_t ~ Categorical(ŷt)
以下是完全采样的特点:
- 优点:能产生更多样化的输出。
- 缺点:由于完全随机,可能会采样到低概率、不连贯的标记,导致生成内容质量不稳定。
Top-k 采样
为了在多样性和质量之间取得平衡,Top-k采样是一种折衷方案。它只从概率最高的k个标记构成的子集中进行采样。
具体步骤如下:
- 从分布
ŷt中选出概率最高的前k个标记,构成集合Vk。 - 将
ŷt在这k个标记上的概率重新归一化,得到一个新的分布Pk。 - 从这个新的分布
Pk中进行采样:y*_t ~ Categorical(Pk)
这种方法通过限制采样范围,降低了生成低质量标记的风险,同时保留了随机性。
核采样(Top-p 采样)
核采样是Top-k采样的一种自适应变体。它不固定采样标记的数量k,而是设定一个概率阈值p(例如0.9)。
具体步骤如下:
- 将词汇表标记按概率从高到低排序。
- 从概率最高的标记开始累加,直到累积概率超过阈值p。
- 用这些被选中的标记构成一个集合,并将其概率重新归一化。
- 从这个新的分布中进行采样。
这种方法能根据当前上下文输出的分布动态调整采样池的大小,更加灵活。
温度缩放
温度缩放是一种在采样前调整输出分布“平滑度”的技术。它在进行softmax操作之前,对模型的logits(žt)进行缩放:
ž‘_t = žt / T
其中,T 是一个称为“温度”的超参数。调整温度会影响最终的输出分布:
- T < 1:会使分布变得更“尖锐”(概率差异增大),降低随机性,输出更接*贪婪解码。
- T > 1:会使分布变得更“平坦”(概率差异减小),增加随机性,输出更多样。
然后,我们基于缩放后的logits计算softmax:ŷt = softmax(ž‘_t),再从这个分布中进行采样或贪婪解码。
除了上述方法,还有束搜索等更复杂的推理算法,但本节课不展开讨论。
总结
本节课中我们一起学*了Transformer模型在自回归生成任务中的关键流程。
- 训练:我们使用“教师强制”方法,通过将输入序列偏移一位作为目标来定义损失函数,以无监督的方式训练模型预测下一个标记。
- 推理:我们探讨了多种从训练好的模型中生成序列的策略,包括确定性的贪婪解码、完全随机的采样,以及平衡两者的Top-k采样、核采样和温度缩放技术。


以Transformer为骨干的自回归模型构成了当今大多数主流生成式AI(如GPT、Gemini等)的基础。从下一节课开始,我们将转向新的主题:如何利用强化学*技术,对这些在大规模数据上预训练的模型进行对齐和微调,使其更符合人类偏好。我们将重点学*策略梯度算法以及PPO、DPO等算法。
062:DDPM噪声估计的实现 🎼



欢迎来到本教程。在本节中,我们将理解DDPM的另一种视角:将其视为对添加噪声的回归。
在上一节教程中,我们已经了解了U-Net架构,以及训练和推理过程的实现方式。本节将介绍一种替代解释,即DDPM作为噪声回归模型。我们将直接通过代码来理解这一观点,并观察它与之前版本的最小差异。
概述
在本节中,我们将学*如何实现一个将DDPM视为噪声回归模型的版本。我们将逐步解析代码,理解如何修改训练目标,从预测原始图像转向预测在扩散过程中添加的噪声。
代码结构与准备
当前代码的结构与之前预测图像本身的“原始”DDPM版本略有不同。代码中包含了大量注释,以便作为实现其他替代视角(如均值预测或分数预测)的基础。
以下是必要的库导入和基本参数设置。

# 基本参数
image_size = 32
batch_size = 128
epochs = 25
learning_rate = 1e-3
timesteps = 600

首先,我们需要定义噪声调度表 beta。beta 是 alpha 的修改版本,其中 alpha = 1 - beta。
# 定义线性beta调度
def linear_beta_schedule(timesteps, start=0.0001, end=0.02):
return torch.linspace(start, end, timesteps)
betas = linear_beta_schedule(timesteps)
alphas = 1. - betas
alphas_cumprod = torch.cumprod(alphas, axis=0) # 累积乘积 α_bar_t
sqrt_alphas_cumprod = torch.sqrt(alphas_cumprod) # sqrt(α_bar_t)
sqrt_one_minus_alphas_cumprod = torch.sqrt(1. - alphas_cumprod) # sqrt(1 - α_bar_t)
我们预先计算这些常量列表,以便在后续方程中方便地获取特定时间步 t 的值。
U-Net模型架构
接下来,我们查看用于噪声估计的U-Net模型。其结构与之前类似,但输出通道数为1(因为MNIST是单通道图像),并且时间嵌入维度为32。
class UNet(nn.Module):
def __init__(self, image_channels=1, down_channels=(64, 128, 256),
up_channels=(256, 128, 64), out_dim=1, time_emb_dim=32):
super().__init__()
# 时间嵌入层
self.time_mlp = nn.Sequential(
SinusoidalPositionEmbeddings(time_emb_dim),
nn.Linear(time_emb_dim, time_emb_dim),
nn.ReLU()
)
# 初始卷积层
self.conv0 = nn.Conv2d(image_channels, down_channels[0], 3, padding=1)
# 下采样模块
self.downs = nn.ModuleList([
Block(down_channels[i], down_channels[i+1], time_emb_dim)
for i in range(len(down_channels)-1)
])
# 上采样模块
self.ups = nn.ModuleList([
Block(up_channels[i], up_channels[i+1], time_emb_dim, up=True)
for i in range(len(up_channels)-1)
])
# 最终输出层
self.output = nn.Conv2d(up_channels[-1], out_dim, 1)
def forward(self, x, timesteps):
# 获取时间嵌入
t = self.time_mlp(timesteps)
# 初始卷积
x = self.conv0(x)
# 存储残差连接
residual_inputs = []
# 下采样过程
for down in self.downs:
x = down(x, t)
residual_inputs.append(x)
# 上采样过程
for up in self.ups:
residual_x = residual_inputs.pop()
# 拼接特征图
x = torch.cat((x, residual_x), dim=1)
x = up(x, t)
# 最终输出
return self.output(x)
模型中的 Block 类负责处理卷积、批量归一化、激活函数以及时间嵌入的融合。根据是下采样还是上采样,它会选择不同的卷积操作。
前向扩散过程
前向扩散过程根据以下公式,将噪声逐步添加到原始图像 x0 上,得到 t 时刻的噪声图像 xt:
公式: xt = sqrt(α_bar_t) * x0 + sqrt(1 - α_bar_t) * ε
其中 ε 是从标准正态分布中采样的噪声。
以下是该过程的实现:
def forward_diffusion_sample(x0, t, device):
"""
根据给定时间步t,对图像x0添加噪声。
返回噪声图像xt和所使用的噪声ε。
"""
noise = torch.randn_like(x0) # 采样噪声 ε ~ N(0, I)
sqrt_alphas_cumprod_t = get_index_from_list(sqrt_alphas_cumprod, t, x0.shape)
sqrt_one_minus_alphas_cumprod_t = get_index_from_list(sqrt_one_minus_alphas_cumprod, t, x0.shape)
# 根据公式计算 xt
xt = sqrt_alphas_cumprod_t * x0 + sqrt_one_minus_alphas_cumprod_t * noise
return xt, noise
def get_index_from_list(vals, t, x_shape):
"""
从预计算的常量列表vals中,获取对应批次中每个样本在时间步t的值。
并调整形状以匹配输入x。
"""
batch_size = t.shape[0]
out = vals.gather(-1, t.cpu())
return out.reshape(batch_size, *((1,) * (len(x_shape) - 1))).to(t.device)
训练循环
训练过程的核心是让U-Net模型学会预测在前向扩散过程中添加到图像上的噪声 ε。
以下是训练循环的关键步骤:



- 从数据加载器中获取一批原始图像
x0。 - 为批次中的每个图像随机采样一个时间步
t。 - 通过
forward_diffusion_sample函数,得到噪声图像xt和真实噪声noise。 - 将
xt和时间步t输入U-Net模型,得到预测的噪声predicted_noise。 - 计算预测噪声与真实噪声之间的均方误差(MSE)作为损失。
- 反向传播并更新模型参数。



# 训练步骤伪代码
for epoch in range(epochs):
for batch in dataloader:
x0 = batch[0].to(device) # 原始图像
t = torch.randint(0, timesteps, (batch_size,), device=device).long() # 随机时间步
xt, noise = forward_diffusion_sample(x0, t, device) # 加噪图像和真实噪声
predicted_noise = model(xt, t) # 模型预测的噪声
loss = F.mse_loss(noise, predicted_noise) # 损失函数
optimizer.zero_grad()
loss.backward()
optimizer.step()

反向采样(推理)过程


在推理阶段,我们从纯噪声 xT 开始,逐步去噪以生成图像。每一步,模型预测当前噪声图像 xt 中的噪声,然后根据以下公式计算 t-1 时刻的图像 x_{t-1}:




公式: x_{t-1} = (1 / sqrt(α_t)) * (xt - ((1 - α_t) / sqrt(1 - α_bar_t)) * predicted_noise) + σ_t * z


其中,当 t > 0 时,z ~ N(0, I),当 t = 0 时,z = 0。σ_t 是后验方差。
以下是反向采样循环的实现:

@torch.no_grad()
def sample(model, image_size, batch_size=16, channels=1):
# 从纯噪声开始
x = torch.randn((batch_size, channels, image_size, image_size), device=device)
for i in reversed(range(0, timesteps)):
t = torch.full((batch_size,), i, device=device, dtype=torch.long)
predicted_noise = model(x, t) # 预测噪声
# 根据公式计算均值
sqrt_recip_alphas_t = get_index_from_list(torch.sqrt(1. / alphas), t, x.shape)
sqrt_one_minus_alphas_cumprod_t = get_index_from_list(sqrt_one_minus_alphas_cumprod, t, x.shape)
# x_{t-1}的均值部分
model_mean = sqrt_recip_alphas_t * (x - ((1. - alphas[t]) / sqrt_one_minus_alphas_cumprod_t) * predicted_noise)
if i > 0:
# 添加随机噪声
noise = torch.randn_like(x)
posterior_variance_t = get_index_from_list(betas, t, x.shape)
x = model_mean + torch.sqrt(posterior_variance_t) * noise
else:
x = model_mean
# 将像素值限制在[0,1]范围内
x = torch.clamp(x, 0.0, 1.0)
return x

结果与练*
运行上述代码可以生成MNIST数字图像。需要注意的是,当前实现生成的图像前景(数字)清晰,但背景可能并非理想的纯黑色,这与标准的MNIST数据集有所不同。
这留作一个有趣的练*:你可以尝试调整超参数(如学*率、训练轮数、时间步数 timesteps 或 beta 调度表的起止值),或者微调模型架构,以获得更清晰的背景和更高质量的图像。
总结
在本节中,我们一起学*了如何将DDPM实现为一个噪声回归模型。我们详细分析了代码的各个部分:
- 定义了线性
beta调度并计算了相关常量。 - 构建了用于噪声预测的U-Net模型。
- 实现了前向扩散过程,将图像与噪声混合。
- 设置了训练循环,其目标是让模型最小化预测噪声与真实添加噪声之间的MSE损失。
- 实现了反向采样过程,从噪声中逐步生成图像。
这种将DDPM视为噪声估计的观点是理解其工作原理的核心方式之一。在接下来的教程中,我们将基于相同的代码结构,探索DDPM的其他解释,如均值预测和显式分数估计,届时我们将只关注目标函数的修改,而模型架构和前向过程将保持不变。




063:DDIM的实现 🎼


欢迎来到本教程,我们将探讨如何实现DDIM。在本节中,我们将专注于使用DDIM方法进行推理。我们不会进行任何训练,因为从理论课可知,如果在DDPM中训练一个回归网络,它实际上隐含地训练了DDIM所需的一系列模型。我们将使用之前教程中介绍过的噪声预测模型作为基础模型,然后在此基础上通过DDIM过程生成样本。
概述
在本节中,我们将学*如何实现去噪扩散隐式模型(DDIM)的采样过程。我们将基于一个预训练的噪声预测模型,编写代码执行DDIM采样,从而从随机噪声生成图像。整个过程只涉及推理,不涉及训练。
DDIM采样过程回顾
首先,让我们回顾一下DDIM的采样公式。根据讨论,采样过程包含三个部分,需要将它们相加。我们将分别计算这三个部分,最后进行求和。
以下是DDIM采样的核心公式:

公式:
x_{t-1} = μ_θ(x_t, t) + σ_t * z

其中:
μ_θ(x_t, t)是预测的均值。σ_t是方差项。z是标准高斯噪声。
具体而言,μ_θ(x_t, t) 可以通过预测的噪声 ε_θ(x_t, t) 和预测的原始图像 x̂_0 来计算。在我们的实现中,我们将使用噪声预测模型。
代码实现步骤
现在,让我们来看具体的代码实现。我们将逐步解析DDIM采样的关键步骤。
1. 初始化与参数设置
首先,我们设置采样所需的参数,包括生成样本的数量、采样步数以及参数 η。
代码:
num_samples = 16
timesteps = 30
eta = 0
我们将模型设置为评估模式,并准备时间步序列。由于在任意时刻,我们都需要当前时间步和下一个时间步,因此我们将时间步处理成对的形式。


2. 采样循环
接下来,我们进入主要的采样循环。对于每一对时间步(当前步 t 和下一步 t_next),我们执行以下操作。
首先,我们从标准高斯分布中采样初始噪声 x_t。
代码:
img = torch.randn((num_samples, 3, image_size, image_size), device=device)
然后,我们将当前图像 img 和时间步 t 输入噪声预测模型,得到预测的噪声 pred_noise。
代码:
pred_noise = model(img, t)
3. 计算预测均值 μ_θ
根据公式,我们可以通过预测的噪声来计算预测的原始图像 x̂_0,进而得到预测的均值 μ_θ。
公式:
x̂_0 = (x_t - √(1 - ᾱ_t) * ε_θ) / √ᾱ_t
在我们的代码中,α_t 始终表示累积乘积 ᾱ_t,这与文献和之前训练过程保持一致,以确保兼容性。
代码:
pred_x0 = (img - sqrt(1 - alpha_bar[t]) * pred_noise) / sqrt(alpha_bar[t])
pred_x0 = torch.clamp(pred_x0, -1., 1.)
pred_x0 即为我们预测的 x̂_0。然后,我们可以计算方向指向 x_t 的项。

4. 计算方向项和噪声项
采样公式中的方向项由预测的噪声和系数组成。
公式:
direction_to_xt = √(1 - ᾱ_{t_next} - σ_t^2) * ε_θ

方差项 σ_t 的计算如下:

公式:
σ_t = η * √((1 - ᾱ_{t-1})/(1 - ᾱ_t)) * √(1 - ᾱ_t/ᾱ_{t-1})
在代码中,我们整体计算了平方根内的乘积。当 η = 0 时,σ_t 项为零,这对应于确定性采样(DDIM)。当 η = 1 时,它则包含随机噪声。

代码:
# 计算方向项
dir_xt = sqrt(1 - alpha_bar[t_next] - sigma_t**2) * pred_noise
# 计算噪声项(当eta=0时此项为0)
noise = sigma_t * torch.randn_like(img) if eta > 0 else 0
5. 更新图像

最后,我们将预测均值、方向项和噪声项相加,得到前一个时间步的图像 x_{t-1}。

公式:
x_{t-1} = √ᾱ_{t_next} * x̂_0 + direction_to_xt + noise


代码:
img = sqrt(alpha_bar[t_next]) * pred_x0 + dir_xt + noise
img = torch.clamp(img, -1., 1.)

重复此过程,遍历所有的时间步对,最终得到生成的图像。

结果与比较


以上是通过DDIM采样生成的图像。它们质量尚可,但并非最佳。通过调整参数,例如使用完整的训练时间步数(例如600步)并将 η 设置为1,你可以比较不同设置下的结果,并可能获得更清晰、更锐利的图像。
使用DDIM的主要优势在于采样效率。例如,如果模型使用600个时间步训练,DDIM可以只采样30步,实现了20倍的加速,这是一个非常高效的操作。

总结

在本节课中,我们一起学*了DDIM(去噪扩散隐式模型)的实现方法。我们回顾了DDIM采样的核心公式,并逐步解析了如何利用预训练的噪声预测模型进行推理生成图像。关键点包括:
- 参数设置:确定了采样步数和
η参数。 - 噪声预测:使用模型预测当前噪声。
- 均值与方向计算:根据公式计算预测均值和指向
x_t的方向。 - 图像更新:结合均值、方向和随机噪声(若
η>0)更新图像。
通过将采样步数从训练时的数百步减少到几十步,DDIM显著提高了生成效率。建议你尝试修改参数(如 timesteps 和 eta),观察并比较生成图像质量的变化。
在接下来的教程中,我们将探讨自回归模型的相关内容。




064:引导DDPM的实现 🧠


在本教程中,我们将学*如何实现分类器引导的扩散模型采样。我们将基于一个噪声预测模型,结合一个噪声分类器,来生成特定类别的图像。
概述 📋
上一节我们介绍了基础的噪声预测模型。本节中,我们将看看如何通过一个额外的分类器来引导扩散过程,从而生成指定类别的图像。核心在于,我们将一个训练好的无条件扩散模型与一个能对含噪图像进行分类的模型结合起来。
代码结构与准备 ⚙️
以下是实现所需的基本代码结构和准备工作。这与之前教程中的设置类似。
# 必要的包导入
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import datasets, transforms
import numpy as np
import matplotlib.pyplot as plt
# 设置运行设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 定义线性beta调度和前向扩散的辅助函数
def linear_beta_schedule(timesteps):
# ... 具体实现
return betas
def forward_diffusion(x0, t, sqrt_alphas_cumprod, sqrt_one_minus_alphas_cumprod):
# ... 具体实现
return noisy_image
我们使用了与之前相同的正弦时间嵌入、U-Net块和基础模块。这些部分没有变化。

噪声分类器 🎯
引导扩散需要的一个关键组件是噪声分类器。其核心思想是:在给定的时间步,输入一个含噪图像和时间戳,模型应能预测该图像对应的类别标签。
以下是噪声分类器的结构:

class NoisyClassifier(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.time_embedding = nn.Embedding(1000, 128) # 可学*的时间嵌入
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(2)
self.fc = nn.Linear(32 * 7 * 7, num_classes) # 假设输入为28x28图像
def forward(self, x, t):
# x: 含噪图像, t: 时间步
t_emb = self.time_embedding(t) # 获取时间嵌入
x = F.relu(self.conv1(x))
x = self.pool(x)
x = F.relu(self.conv2(x))
# 将时间嵌入扩展到与特征图匹配的维度并相加
t_emb = t_emb.unsqueeze(-1).unsqueeze(-1).expand(-1, -1, x.shape[2], x.shape[3])
x = x + t_emb
x = x.flatten(start_dim=1)
logits = self.fc(x)
return logits

训练过程:
- 首先,像之前一样训练一个无条件噪声预测模型。
- 然后,训练这个噪声分类器。关键点在于,训练时输入的不是干净图像
x0,而是经过前向扩散过程得到的含噪图像xt。损失函数使用标准的交叉熵损失。



引导采样:结合模型 🧩

当无条件扩散模型和噪声分类器都训练好后,核心问题是如何在采样过程中利用分类器的信息来生成指定类别的图像。
以下是引导采样的关键步骤:
def guided_sample(model, classifier, target_class, scale=2.0, timesteps=1000):
model.eval()
classifier.eval()
# 1. 从纯噪声开始
xt = torch.randn((batch_size, 1, 28, 28)).to(device)
# 目标类别标签
y = torch.full((batch_size,), target_class, device=device)
for t in reversed(range(timesteps)):
# 2. 预测噪声
predicted_noise = model(xt, torch.tensor([t], device=device))
# 3. 计算无条件均值
# 根据公式:μ_uncond = (1 / sqrt(α_t)) * (xt - ( (1-α_t) / sqrt(1-α_t_bar) ) * predicted_noise)
sqrt_recip_alphas_t = torch.sqrt(1.0 / alphas[t])
sqrt_one_minus_alphas_bar_t = torch.sqrt(1 - alphas_cumprod[t])
mu_uncond = sqrt_recip_alphas_t * (xt - ( (1 - alphas[t]) / sqrt_one_minus_alphas_bar_t ) * predicted_noise)
# 4. 计算分类器梯度
xt.requires_grad_(True) # 开启梯度以计算分类器对xt的梯度
logits = classifier(xt, torch.tensor([t], device=device))
log_probs = F.log_softmax(logits, dim=-1)
# 获取目标类别的对数概率
target_log_probs = log_probs[range(batch_size), y]
# 计算梯度:∇_xt log p(y | xt)
gradient = torch.autograd.grad(target_log_probs.sum(), xt)[0]
xt.requires_grad_(False)
# 5. 调整均值:μ_guided = μ_uncond + scale * Σ_t * ∇_xt log p(y | xt)
# 其中 Σ_t 是后验方差
posterior_variance_t = (1 - alphas_cumprod[t-1]) / (1 - alphas_cumprod[t]) * (1 - alphas[t]) if t > 0 else 0
mu_guided = mu_uncond + scale * posterior_variance_t * gradient
# 6. 重参数化采样,得到前一时间步的图像
if t > 0:
noise = torch.randn_like(xt)
xt = mu_guided + torch.sqrt(posterior_variance_t) * noise
else:
xt = mu_guided # 最后一步直接取均值
# 可选:将像素值裁剪到合理范围
xt = torch.clamp(xt, -1.0, 1.0)
return xt
核心公式:
引导采样调整了反向扩散的均值。无条件均值 μ_uncond 根据噪声预测模型计算。然后,我们加上一个由分类器梯度引导的项:
μ_guided = μ_uncond + s * Σ_t * ∇_xt log p(y | xt)
其中:
s是引导尺度,控制分类器影响的强度。Σ_t是时间步t的后验方差。∇_xt log p(y | xt)是分类器关于输入xt的梯度,它指示了如何改变xt以增加其属于目标类别y的概率。
结果与总结 🎨
通过上述过程,我们成功地将无条件扩散模型与一个噪声分类器相结合,实现了分类器引导的采样。在本例中,我们设定目标类别为数字“9”,并生成了相应的图像。虽然生成的图像质量有提升空间,但这清晰地展示了引导生成的基本原理。
本节课中我们一起学*了:
- 噪声分类器的概念与实现,它能够对含噪图像进行分类。
- 如何分别训练无条件扩散模型和噪声分类器。
- 引导采样的核心算法:在反向扩散过程中,利用分类器关于输入图像的梯度来调整采样均值,从而将生成过程导向指定的类别。
- 引导尺度
s的作用,它平衡了无条件生成与分类器引导之间的强度。


你可以基于提供的代码框架,通过更长时间的训练、调整模型架构或优化超参数来获得更好的生成效果。在接下来的教程中,我们将探讨更高级的扩散模型变体。
065:强化学*概述 🧠

在本节课中,我们将讨论用于语言模型对齐的强化学*方法。我们将从强化学*的基础概念入手,了解其如何应用于调整大型语言模型的行为,使其更符合人类的期望。
概述
在之前的课程中我们看到,自回归语言模型学*的是来自词汇表的一系列标记 X_T 的分布。具体来说,它学*在给定先前出现的标记序列 X_{<t} 的条件下,预测下一个标记 X_t 的概率。
这个条件分布 P(X_t | X_{<t}) 通常使用如Transformer这类具有注意力机制的架构来建模。
由于语言模型是在大量无标签数据上训练的,对齐的目标是确保语言模型的行为与期望的、预定义的行为(由人工标注或固定规则指定)保持一致。这一点至关重要,因为大型语言模型的训练数据(例如来自互联网)通常未经任何监管。模型在这种无监督方式下训练,其行为可能不符合人类期望。为了确保模型的行为方式符合人类预期,模型必须与人类的期望对齐。
一种方法是以完全监督的方式重新训练模型,即提供符合人类用户期望的输入-输出对。然而,这类数据极难获取。相对容易的是,对于一个已训练的语言模型,给定一个输入,可以生成多个候选输出,然后由人类评判员或其他基于机器的机制对这些输出进行评级。基于这些评级或判断,语言模型需要被重新训练或对齐,以确保未来模型的输出能与人类做出的判断保持一致。这就是为什么在模型训练后需要进行对齐的直观目标。
总结来说,大型语言模型在未经监管的数据上训练,这些数据可能包含各种类型的标记。为了确保语言模型的输出以符合人类行为的方式受到监管,需要使用某些方法重新训练模型,使其输出与人类用户的期望兼容。为了实现这一目标,人们提出了许多基于强化学*的算法,它们非常适用于语言模型的对齐领域。
因此,让我们从强化学*的介绍开始。
强化学*简介
请注意,强化学*本身是一个非常广阔的领域。本课程的目标并非专门教授强化学*,我们只会概述强化学*的基本概念,并简要了解其中一种用于语言模型对齐的变体。
统计模型学*有三种主要范式:
- 监督学*范式:我们拥有数据和对应的标签,目标是让模型学*标签基于数据的后验分布。
- 无监督学*范式:我们拥有从特定分布采样的数据,但没有对应的标签。我们建模的是给定数据的边缘分布。
- 强化学*范式:其思想是模仿人类的学*方式。人类既没有像监督学*中那样确切的标签,也并非像无监督学*那样完全没有环境反馈。人类的学*方式介于严格的监督学*范式和无任何标签的无监督学*范式之间。
观察人类如何学*:我们执行一个特定动作,基于我们执行的动作,我们所处的环境会给出反馈。这个反馈可以是正面的或负面的。根据收到的反馈类型,我们在未来面对完全相同的情境时会改变行为方式。如果是正面反馈,我们的行为会得到强化;如果是负面反馈,我们则会改变该行为,转向不同的行为方式。这就是人类学*的基本思想,也是强化学*范式所模仿的。
强化学*的数学形式化
在强化学*范式中,我们有一个智能体和一个环境。
智能体在每个离散时间步 t 处于一个特定状态并执行一个特定动作。智能体可能处于的状态来自一个有限的状态空间 S,智能体可以执行的动作来自动作空间 A。
在每个时间步 t,智能体根据一个称为策略的函数采取特定动作。一旦智能体执行了一个动作,环境会通过提供一个反馈来响应,该反馈由一个称为奖励函数 R 的函数量化。奖励函数从状态空间和动作空间的笛卡尔积映射到一个实数。也就是说,当智能体在状态 S_t 采取动作 A_t 时,环境返回奖励 R(S_t, A_t)。
环境给出奖励后,智能体根据另一个称为转移核的分布转移到新状态 S_{t+1}。一旦进入状态 S_{t+1},它再次根据策略采取另一个动作,这个过程不断重复。
其思想是以某种方式学*或调整此策略,以优化累积奖励的某种度量。
重复一下设置:有一个智能体在不同的状态下执行一系列因果动作。每次智能体执行一个特定动作,环境都会通过给予反馈来响应,该反馈由奖励函数量化。智能体收到反馈后,根据转移核转移到不同的状态。一旦处于不同的状态,它根据其遵循的策略采取另一个动作。这个过程持续进行。强化学*的目标是更新或学*智能体所遵循的策略,以优化某种累积奖励的概念。这是基本思想。
从数学上讲,我刚才描述的整个过程由一个称为马尔可夫决策过程 的概念形式化。MDP由以下五元组定义:(S, A, P, R, γ, ρ)。
让我们逐一查看它们的定义:
S定义了智能体可能处于的状态集合。它可以是一个包含M个元素的有限集。集合S称为MDP的状态空间。A定义了可能的动作集合。这也称为动作空间。- 转移核记为
P(S_{t+1} | S_t, A_t)。这是在时间t处于状态S_t并执行动作A_t时,在时间t+1转移到状态S_{t+1}的概率。 - 奖励函数
R是从状态空间和动作空间的笛卡尔积到实数的函数。R(S_t, A_t)表示在状态S_t采取动作A_t所获得的奖励。请注意,奖励的概念是固定的,它取决于环境,我们无法控制环境如何给出奖励。 γ是折扣因子,是一个介于0和1之间的标量。稍后将描述为什么需要折扣因子的概念。ρ是初始状态分布。由于我们有一个随机离散时间随机过程来模拟智能体的行为,因此需要初始状态分布向量ρ。
此外,我们还有策略 π_θ,如前所述,它是在给定状态 S 下动作的分布,即 π_θ(A_t | S_t)。该策略由参数 θ 参数化。我们的想法是根据收到的奖励类型来改变这个策略。
很容易注意到,智能体的行为完全取决于这个特定的随机策略分布,它会告诉我们处于特定状态时应采取什么动作。这由 θ 参数化。
轨迹与目标函数
当智能体根据策略 π_θ 行动时,我们会得到一条轨迹,记为 τ。这是一组交替的状态和动作:τ = (S_0, A_0, S_1, A_1, S_2, A_2, ...)。
一旦有了这条轨迹,我们就可以定义轨迹的分布,称为轨迹分布。在策略 π_θ 下的轨迹分布 P_θ(τ) 由以下公式给出:
P_θ(τ) = ρ(S_0) * ∏_{t=0}^{T-1} [ π_θ(A_t | S_t) * P(S_{t+1} | S_t, A_t) ]
这只是概率的链式法则。我们所做的是写出了在策略 π_θ 下给定轨迹 τ 的分布。智能体从 S_0 开始,其概率由初始状态分布 ρ 给出。然后在每个时间步 t,智能体根据策略行动,这意味着它在状态 S_t 下执行动作 A_t。根据智能体在状态 S_t 下执行的动作,它根据转移核 P 转移到状态 S_{t+1}。这就是特定轨迹的概率。
定义了轨迹之后,我们需要定义强化学*的目标。目标是学*一个好的策略。问题是如何定义“好”的策略。为此,我们需要定义另一个称为折扣回报的量。
一条轨迹 τ 的折扣回报 R(τ) 定义如下:
R(τ) = ∑_{t=0}^{T-1} γ^t * R(S_t, A_t)
这里发生的是,我们本可以只取智能体在遍历该特定轨迹时获得的所有奖励之和。然而,人们通常采用折扣奖励,原因是随着我们远离给定时间 t,奖励函数对策略改变的影响应该减小。因为更接*给定时间步的动作对策略的影响应该比之后采取的动作更大,这是通过将奖励乘以常数 γ^t 来体现的。折扣因子 γ 是0到1之间的标量。因此,随着我们离给定的 t 越来越远,γ^t 会减小,这就是我们乘以折扣因子的原因。
现在,强化学*中的目标或目标函数是最大化期望折扣回报。数学上,目标函数 J(θ) 是:
J(θ) = E_{τ ∼ P_θ} [ R(τ) ] = ∫ P_θ(τ) * R(τ) dτ
我们想要做的是学*策略 π_θ,以最大化期望回报。这个优化是针对所有可能的此类策略。这就是强化学*的目标函数。
总结
让我们快速回顾一下我们所做的内容。
当智能体与环境交互时,我们获得一条轨迹,它是一组交替的状态和动作。我们可以使用特定表达式定义在策略 π_θ 下的轨迹分布。强化学*算法的目标是学*一个好的策略。定义策略好坏的一种方法是定义折扣回报,它本质上是整个轨迹上经过加权的奖励。一旦有了这个折扣回报,目标就是最大化这个期望回报,即在所有可能轨迹的分布下(进而也是在策略的分布下)该折扣回报的期望值。
这是任何强化学*算法的主要目标函数。解决这个优化问题后,我们得到一个能最大化期望折扣回报的策略,这是定义好策略的一种方式。


在本节课中,我们一起学*了强化学*的基本框架及其在语言模型对齐中的应用背景。我们介绍了马尔可夫决策过程的核心组件,包括状态、动作、策略、奖励函数和转移核,并定义了通过最大化期望折扣回报来学*最优策略的目标。在接下来的课程中,我们将探讨如何利用这个框架具体实现语言模型的对齐。
066:策略梯度定理 🎼


在本节课中,我们将学*强化学*中一个核心的优化方法——策略梯度定理。我们将了解如何通过梯度下降来优化策略,并推导出计算目标函数梯度的关键公式。这对于后续理解如何将强化学*应用于大语言模型的对齐任务至关重要。
优化问题的多种解法
强化学*中的优化问题有多种解决方法。为了本次讨论,我们将关注其中一种流行的方法:策略梯度优化。
策略梯度优化的核心思想
其核心思想如下:假设我们优化的策略可以用可微函数(例如神经网络)表示。一个具体的例子是我们正在考虑的情况,即策略由类似Transformer的架构(一种长波模型)表示。在这种情况下,此优化问题可以使用常见的随机梯度方法求解。
梯度下降与目标函数梯度
为了实现这一点,我们需要获得特定目标函数的梯度。因为梯度下降法要求我们沿着目标函数的梯度方向更新策略参数。更新公式如下:
θ_new = θ_old + η * ∇_θ J(θ)
其中 η 是学*率,∇_θ J(θ) 是目标函数关于策略参数 θ 的梯度。
策略梯度定理的必要性
只有当我们可以获得目标函数关于策略参数 θ 的梯度时,才能进行这种基于梯度下降的策略更新。为此,我们需要借助一个非常强大且优美的结果——策略梯度定理。这个定理使我们能够找到解决上述优化问题所需的目标函数梯度。
策略梯度定理的推导
现在,让我们看看策略梯度定理具体是什么。我们需要计算以下目标函数的梯度:
∇_θ J(θ) = ∇_θ E_{τ∼p_θ(τ)} [R(τ)]
其中,目标函数是期望折扣回报 R(τ),期望是关于从分布 p_θ(τ) 中采样的轨迹 τ 计算的。
第一步:将梯度移入期望
由于梯度是线性算子,我们可以将其移入积分(或期望)内部:
∇_θ J(θ) = ∫ ∇_θ [p_θ(τ) * R(τ)] dτ = ∫ [∇_θ p_θ(τ)] * R(τ) dτ
这里,R(τ) 独立于 θ。
第二步:应用对数导数技巧
我们需要计算 ∇_θ p_θ(τ)。这里可以使用对数导数技巧,将其表达为:
∇_θ p_θ(τ) = p_θ(τ) * ∇_θ log p_θ(τ)
这个技巧成立是因为 ∇ log f(x) = (∇ f(x)) / f(x)。将其代入上式,得到:
∇_θ J(θ) = ∫ p_θ(τ) * [∇_θ log p_θ(τ)] * R(τ) dτ = E_{τ∼p_θ(τ)} [∇_θ log p_θ(τ) * R(τ)]
第三步:分解轨迹概率
现在,让我们考虑 ∇_θ log p_θ(τ) 这一项。我们知道轨迹概率定义为:
p_θ(τ) = ρ(s_0) * Π_{t=0}^{∞} [π_θ(a_t|s_t) * p(s_{t+1}|s_t, a_t)]
其中 ρ(s_0) 是初始状态分布,p(s_{t+1}|s_t, a_t) 是环境转移概率。对上述取对数并求梯度:
∇_θ log p_θ(τ) = Σ_{t=0}^{∞} ∇_θ log π_θ(a_t|s_t)
因为 ρ(s_0) 和转移概率 p 均与策略参数 θ 无关,在求梯度时为零。
第四步:得到基本形式
将上述结果代回梯度表达式:
∇_θ J(θ) = E_{τ∼p_θ(τ)} [ Σ_{t=0}^{∞} ∇_θ log π_θ(a_t|s_t) * R(τ) ]
这就是策略梯度定理最基本的形式。它指出,用于更新策略的目标函数的梯度由此特定期望给出。
从基本形式到实用形式
上一节我们推导了策略梯度定理的基本形式,本节中我们来看看如何将其转化为更高效、方差更低的实用形式。
引入“未来回报”
在上述基本形式中,整个轨迹的总回报 R(τ) 被应用于所有时间步 t,无论动作是何时采取的。我们可以通过定义从时间 t 开始的未来折扣回报 R_t 来使其更高效:
R_t = Σ_{k=0}^{∞} γ^k * r_{t+k}
其中 γ 是折扣因子。这样,总回报 R(τ) 就等于 R_0。梯度可以等价地重写为:
∇_θ J(θ) = E_{τ∼p_θ(τ)} [ Σ_{t=0}^{∞} ∇_θ log π_θ(a_t|s_t) * R_t ]
现在,每个时间步 t 乘的是从该时刻开始的未来回报 R_t,而不是整个轨迹的总回报。
引入基线以减少方差
上述梯度估计量的方差通常很大。为了降低方差,常用的方法是从回报 R_t 中减去一个仅依赖于状态 s_t 的基线函数 b(s_t)。可以证明,这不会改变梯度的期望值,但能有效减少方差。
因此,引入基线后的新梯度估计为:
∇_θ J(θ) = E_{τ∼p_θ(τ)} [ Σ_{t=0}^{∞} ∇_θ log π_θ(a_t|s_t) * (R_t - b(s_t)) ]
选择价值函数作为基线
文献中的多种算法给出了不同的基线函数选择。一个著名且高效的选择是使用价值函数 V^π(s) 作为基线。
以下是相关函数的定义:
- 价值函数:
V^π(s) = E_{τ∼π} [ Σ_{k=0}^{∞} γ^k * r_{t+k} | s_t = s ]- 表示从状态
s开始,遵循策略π所能获得的期望折扣回报。
- 表示从状态
- 动作价值函数(Q函数):
Q^π(s, a) = E_{τ∼π} [ Σ_{k=0}^{∞} γ^k * r_{t+k} | s_t = s, a_t = a ]- 表示从状态
s开始并执行动作a,然后遵循策略π所能获得的期望折扣回报。
- 表示从状态
- 优势函数:
A^π(s, a) = Q^π(s, a) - V^π(s)- 表示在状态
s下采取特定动作a,相比于遵循策略π的平均表现,所带来的额外期望回报。
- 表示在状态
连接优势函数
一个关键结论是:未来回报 R_t 与价值函数 V^π(s_t) 的差值,是优势函数 A^π(s_t, a_t) 的一个无偏估计量。
证明如下:
E [ R_t - V^π(s_t) | s_t, a_t ] = E [ R_t | s_t, a_t ] - V^π(s_t) = Q^π(s_t, a_t) - V^π(s_t) = A^π(s_t, a_t)
因此,(R_t - V^π(s_t)) 可以看作是优势函数的一个单样本估计。
策略梯度定理的最终形式
综合以上步骤,我们得到了策略梯度定理最终常用的形式:
∇_θ J(θ) = E_{τ∼p_θ(τ)} [ Σ_{t=0}^{∞} ∇_θ log π_θ(a_t|s_t) * A^π(s_t, a_t) ]
这个表达式被大多数先进的、基于策略梯度的算法所采用,也包括用于大语言模型对齐的算法。
遗留问题与展望
到目前为止,我们一起学*了策略梯度定理的完整推导和演变。最后,还有两个关键问题需要回答:
- 如何将自回归大语言模型嵌入到这个强化学*框架中? 我们需要将语言模型的生成过程(根据上文生成下一个词)视作一个顺序决策过程。
- 在实践中如何计算优势函数? 通常需要训练一个参数化的奖励模型来估计回报。在拥有奖励模型后,可以使用诸如广义优势估计(GAE)等方法来高效计算优势函数。
我们将在后续课程中探讨如何训练参数化奖励模型,并详细讲解如何将大语言模型与策略梯度优化方法结合起来,从而完成对齐的闭环。


总结


本节课中,我们一起学*了策略梯度定理。我们从优化策略的基本目标出发,推导了目标函数梯度的基本表达式。为了提升学*效率并降低估计方差,我们引入了未来回报、基线函数等概念,并最终将梯度表达为策略对数梯度与优势函数乘积的期望。这个最终形式 ∇_θ J(θ) = E [ Σ_t ∇_θ log π_θ(a_t|s_t) * A^π(s_t, a_t) ] 构成了现代策略梯度算法(如用于大模型对齐的RLHF方法)的核心数学基础。
067:将自回归语言模型表达为强化学*策略

在本节中,我们将学*如何将自回归语言模型(AR-LM)的生成过程,形式化地表达为一个强化学*(RL)策略。这是应用强化学*理论来优化语言模型的关键第一步。
概述
上一节我们介绍了强化学*中的策略梯度定理。为了将该理论应用于语言模型,我们首先需要将自回归语言模型表述为一个强化学*策略。本节将详细阐述这一对应关系的建立过程。
将语言模型视为策略
在自回归语言模型中,我们建模的是给定历史词元(tokens)序列后,下一个词元的条件概率分布。其核心操作是预测:
P_θ(x_t | x_{<t})
其中,θ 是模型参数,x_t 是当前要预测的词元,x_{<t} 是之前所有已生成的词元。
在强化学*的框架下,我们将这个语言模型本身视为一个策略 π_θ。策略的功能是:在给定当前状态 s_t 时,选择动作 a_t。对于语言模型,这个对应关系非常直接:
π_θ(a_t | s_t) = P_θ(x_t | x_{<t})
这意味着,语言模型根据当前已生成的文本(状态),生成下一个词元(动作)的概率分布,就是我们的策略。
构建语言模型的轨迹
为了应用策略梯度等强化学*算法,我们需要定义语言模型生成过程中的“轨迹”。轨迹由一系列状态和动作交替组成:τ = (s_0, a_0, s_1, a_1, ...)。
以下是语言模型中轨迹的构建步骤:
-
初始状态
s_0:初始状态是提供给语言模型的输入,通常称为“提示”(prompt)。 -
第一个动作
a_0:语言模型根据初始提示s_0,通过某种推理过程(如采样或贪婪解码)生成第一个输出词元。这个词元即为第一个动作a_0。 -
后续状态
s_1:将初始提示s_0和第一个动作a_0拼接起来,构成新的文本序列,这个序列就是下一个状态s_1。 -
后续动作与状态:语言模型基于新状态
s_1生成第二个词元,作为动作a_1。再将a_1拼接到s_1之后,形成状态s_2。以此类推,不断重复,直到生成结束标志或达到长度限制。
通过这种方式,语言模型的一次完整文本生成过程,就被构建成了一条强化学*所需的轨迹 τ。
应用策略梯度优化
一旦我们将语言模型定义为策略 π_θ,并将其生成过程构建为轨迹 τ,强化学*的框架便完全适用。我们可以使用策略梯度定理来计算目标函数(例如期望奖励)关于模型参数 θ 的梯度。
梯度公式的核心形式为:
∇_θ J(θ) ≈ E_{τ∼π_θ} [ Σ_t (∇_θ log π_θ(a_t | s_t)) * R(τ) ]
其中,R(τ) 是评估整个生成轨迹 τ 质量的奖励函数。通过梯度上升法更新参数 θ,我们可以优化策略(即语言模型),使其生成的文本能获得更高的奖励。
总结


本节课中,我们一起学*了将自回归语言模型转化为强化学*策略的关键步骤。我们明确了语言模型的条件概率分布即为策略本身,并详细说明了如何将文本生成过程构建成状态-动作轨迹。这为后续直接应用策略梯度等强化学*算法来优化语言模型的生成质量奠定了坚实的理论基础。下一节,我们将探讨如何设计合适的奖励函数 R(τ) 来指导语言模型的优化方向。
068:*端策略优化(PPO) 🎯

概述
在本节中,我们将学*两种用于将语言模型与人类偏好对齐的策略梯度算法的改进版本。第一种是*端策略优化算法,第二种是直接偏好优化算法。这两种是目前用于对齐自回归语言模型与特定人类偏好数据的非常流行的算法。
从策略梯度到重要性采样
上一节我们介绍了策略梯度算法的基础。在强化学*框架中,语言模型被视为一个参数化的策略。策略梯度算法用于对齐该策略。参数更新遵循梯度上升,目标函数J(θ)的梯度估计为:
公式:
∇J(θ) = E[∇log π_θ(a_t|s_t) * A_t]
其中,期望是关于从策略π_θ中采样的轨迹。我们可以使用样本平均来*似这个期望,得到梯度估计:
公式:
Ĝ = (1/B) Σ_{t=0}^{T-1} [∇log π_θ(a_t|s_t) * Â_t]
这里的动作a_t和状态s_t来自当前策略的“展开”。在语言模型中,这对应于给定提示并生成响应。
然而,这个估计器是同策略的,这意味着每次参数更新都需要从正在优化的同一策略中采集新的样本。这个过程计算成本高昂,并且由于每次更新后都丢弃旧轨迹,可能导致高方差。我们希望复用从旧策略收集的轨迹数据来更新当前策略,即转向异策略学*。这可以通过一种经典的统计技术——重要性采样——来实现。
重要性采样原理
重要性采样是一种用于评估随机变量函数期望的通用方法。问题在于计算函数f(x)关于分布p(x)的期望:
公式:
E_{x~p}[f(x)] = ∫ p(x) f(x) dx
假设我们有另一个分布q(x)。我们可以将上述期望重写为:
公式:
E_{x~p}[f(x)] = ∫ [p(x)/q(x)] f(x) q(x) dx = E_{x~q}[(p(x)/q(x)) f(x)]
这样,关于p(x)的期望就被转换成了关于q(x)的期望,只需在函数f(x)上乘以两个密度的比值p(x)/q(x)。这个技术就是重要性采样。
在策略梯度中应用重要性采样
现在,让我们看看如何将重要性采样的思想应用到策略梯度中。我们的目标是计算梯度∇J(θ),其期望是关于当前策略π_θ的。
假设我们从另一个策略π_{θ_old}中采样轨迹。应用重要性采样,我们可以将梯度期望重写为:
公式:
∇J(θ) = E_{(s_t, a_t)~π_{θ_old}} [ (π_θ(a_t|s_t) / π_{θ_old}(a_t|s_t)) * ∇log π_θ(a_t|s_t) * A_t ]
可以证明,上式中的项(π_θ/π_{θ_old}) * ∇log π_θ 等于 ∇(π_θ/π_{θ_old})。因此,我们定义一个新的替代损失函数:
公式:
L_{IS}(θ) = E_{(s_t, a_t)~π_{θ_old}} [ (π_θ(a_t|s_t) / π_{θ_old}(a_t|s_t)) * A_t ]
这个替代损失函数的梯度与原始目标函数J(θ)的梯度相同。现在,期望是关于固定的旧策略π_{θ_old}的,这允许我们复用旧策略采集的数据进行多次更新,从而提高了数据效率。
*端策略优化的核心思想
然而,上述重要性采样公式存在一个问题:如果新策略π_θ与旧策略π_{θ_old}偏离太多,重要性权重(π_θ/π_{θ_old})的方差会变得非常大,导致训练不稳定。
为了避免这个问题,PPO算法对目标函数施加了约束,以限制新策略偏离旧策略的程度。这是通过引入一个分布散度度量(通常是KL散度)作为约束来实现的。
PPO没有直接使用带约束的优化,而是提出了两种主要的替代目标函数形式,它们在实践中更容易优化:
1. PPO-裁剪
这种方法通过裁剪重要性权重比率来防止过大的更新。
公式:
L^{CLIP}(θ) = E_t [ min( r_t(θ) * Â_t, clip(r_t(θ), 1-ε, 1+ε) * Â_t ) ]
其中, r_t(θ) = π_θ(a_t|s_t) / π_{θ_old}(a_t|s_t)
2. PPO-自适应KL惩罚
这种方法在目标函数中添加了一个基于KL散度的自适应惩罚项。
公式:
L^{KLPEN}(θ) = E_t [ (π_θ(a_t|s_t) / π_{θ_old}(a_t|s_t)) * Â_t - β * KL[π_{θ_old}(·|s_t), π_θ(·|s_t)] ]
其中,系数β会根据当前KL散度与目标值的比较进行自适应调整。
以下是PPO算法(裁剪版本)的核心步骤概述:
算法步骤:
- 使用当前策略π_θ与环境交互,收集一批轨迹数据(状态、动作、奖励)。
- 使用广义优势估计等方法,计算每个时间步的优势函数估计值Â_t。
- 对于多个epoch(例如,K次):
- 计算重要性权重 r_t(θ) = π_θ(a_t|s_t) / π_{θ_old}(a_t|s_t)。
- 计算裁剪后的目标函数 L^{CLIP}(θ)。
- 使用梯度上升法更新策略参数θ,以最大化 L^{CLIP}(θ)。
- 用更新后的策略π_θ覆盖旧策略π_{θ_old}。
- 重复步骤1-4。
总结


本节课我们一起学*了*端策略优化算法。我们从标准的策略梯度算法出发,指出了其同策略学*导致的数据效率低下问题。通过引入重要性采样技术,我们将梯度估计转换为基于旧策略的期望,从而能够复用数据。最后,为了控制新策略的更新幅度并保证训练稳定性,PPO通过裁剪重要性权重或添加KL散度惩罚项,构造了一个易于优化且稳健的替代目标函数。这使得PPO成为对齐大型语言模型等任务中非常有效且流行的算法。
069:信任区域策略优化(TRPO)与*端策略优化(PPO) 🧠

在本节课中,我们将学*两种重要的策略优化算法:信任区域策略优化(TRPO)和*端策略优化(PPO)。我们将了解它们如何通过约束策略更新的幅度来稳定训练过程,并详细解析PPO算法的目标函数构成。
概述
上一节我们介绍了策略梯度定理的基本思想。本节中,我们来看看如何在实际应用中稳定策略更新。直接应用策略梯度可能导致更新步幅过大,使新策略与旧策略差异巨大,进而导致训练不稳定甚至崩溃。为了解决这个问题,研究者提出了信任区域策略优化(TRPO)及其更实用的变体——*端策略优化(PPO)。
信任区域策略优化(TRPO)
TRPO算法的核心思想是:在更新策略时,确保新策略与旧策略之间的差异不会太大。它通过约束新旧策略之间的KL散度来实现这一点。
TRPO的目标函数如下:
目标函数:
[
\max_{\theta} \mathbb{E}{s, a \sim \pi{\theta_{old}}} \left[ \frac{\pi_{\theta}(a|s)}{\pi_{\theta_{old}}(a|s)} A^{\pi_{\theta_{old}}}(s, a) \right]
]
约束条件:
[
\mathbb{E}{s \sim \pi{\theta_{old}}} \left[ D_{KL}(\pi_{\theta_{old}}(\cdot|s) | \pi_{\theta}(\cdot|s)) \right] \leq \delta
]
其中,(\delta) 是一个超参数。这个约束确保了新旧策略的KL散度被限制在一个“信任区域”内,从而实现了平缓、稳定的更新。
然而,直接求解这个带约束的优化问题在计算和实现上并非易事。正是由于这个挑战,催生了更易于实现的*端策略优化算法。
*端策略优化(PPO)
由于TRPO的约束优化问题求解困难,PPO提出了一种替代方案。它不显式计算KL散度,而是通过“裁剪”目标函数来间接约束策略更新。
首先,我们定义重要性采样比率 (r_t(\theta)):
[
r_t(\theta) = \frac{\pi_{\theta}(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}
]
PPO的核心是使用一个裁剪后的替代目标函数。以下是PPO目标函数的第一部分(裁剪目标):
裁剪替代目标:
[
L^{CLIP}(\theta) = \mathbb{E}_{t} \left[ \min\left( r_t(\theta) \hat{A}_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t \right) \right]
]
其中,(\hat{A}_t) 是优势函数的估计值,(\epsilon) 是一个小的超参数(例如0.1或0.2)。
这个公式的工作原理如下:
- 当优势函数 (\hat{A}_t) 为正时,我们希望增加该动作的概率(即希望 (r_t(\theta)) 增大)。但如果 (r_t(\theta)) 增长过大(超过 (1+\epsilon)),
clip操作会将其值限制在 (1+\epsilon),从而防止新策略与旧策略偏离太远。 - 当优势函数 (\hat{A}_t) 为负时,我们希望减少该动作的概率。但如果 (r_t(\theta)) 减小过多(低于 (1-\epsilon)),
clip操作会将其值限制在 (1-\epsilon)。 min操作确保了在裁剪生效时,我们采用更保守的更新目标,从而进一步稳定训练。
完整的PPO目标函数
一个完整的PPO算法目标通常包含三个部分,旨在同时优化策略、价值函数并鼓励探索。
完整目标函数:
[
L^{PPO}(\theta) = \mathbb{E}{t} \left[ L^{CLIP}t(\theta) - c_1 (V(s_t) - R_t)^2 + c_2 H(\pi(\cdot|s_t)) \right]
]
以下是该目标函数三个组成部分的详细说明:
- 策略目标((L^{CLIP})):即上文所述的裁剪替代目标,用于在信任区域内改进策略。
- 价值函数损失:这是一个回归任务,(V_{\theta}(s_t)) 是价值网络对状态 (s_t) 的估值,(R_t) 是实际获得的回报(或目标值)。最小化其均方误差可以训练一个更准确的价值函数,而准确的价值函数对于估计优势函数 (\hat{A}_t) 至关重要。
- 策略熵奖励:(H(\pi_{\theta}(\cdot|s_t))) 是策略在状态 (s_t) 下的熵。最大化熵(即公式中减去负熵)可以鼓励策略保持一定的随机性,从而促进探索,避免过早收敛到次优策略。(c_1) 和 (c_2) 是控制各部分权重的系数。
总结
本节课中我们一起学*了稳定策略梯度更新的两种关键方法。
- 我们首先介绍了信任区域策略优化(TRPO),它通过显式地约束新旧策略之间的KL散度来限制更新幅度。
- 接着,我们探讨了*端策略优化(PPO),它通过一个巧妙的裁剪目标函数来隐式实现类似的约束,从而避免了TRPO中复杂的约束优化问题,使其更易于实现和应用。
- 最后,我们解析了完整的PPO目标函数,它集成了策略改进、价值函数拟合和探索鼓励三项任务。


目前,整个算法流程中还缺少一个关键环节:奖励函数 (R_t) 是如何得到的? 在基于人类反馈的强化学*等场景中,这个奖励通常由一个独立的神经网络来建模,这个过程称为“奖励建模”。这将是我们在下一节中要探讨的核心内容。
070:奖励建模 🎯

在本节课中,我们将要学*强化学*中一个至关重要的组成部分:奖励建模。我们将探讨为什么直接获取奖励数据很困难,并介绍一种更实用的替代方法——使用偏好数据来训练奖励模型。
概述
在基于策略梯度的强化学*算法中,奖励函数建模至关重要,因为优势函数和价值函数都基于奖励,而这些函数会出现在所有策略梯度优化算法的损失函数中。
奖励函数的作用
奖励函数是一个定义在状态空间和动作空间笛卡尔积上的函数,它将一个状态-动作对映射到一个实数。
公式:R(s, a) -> r
从实践角度看,它接收一个状态 s 和一个动作 a 作为输入,并输出一个实数 r。这个数值代表了在给定状态下执行该动作的“好坏”程度。
监督学*方法的挑战
乍一看,如果我们拥有标注了奖励值的数据,那么在经典的监督学*框架下训练奖励模型似乎是简单的,因为这本质上是一个回归问题。
以下是所需的数据格式和训练思路:
数据格式:(s, a, reward)
训练方法:
- 奖励模型
R_φ通常由一个神经网络参数化。 - 可以通过经验风险最小化(ERM)框架,即基于梯度下降的目标最小化方法进行学*。
然而,获取这种标注了精确奖励值的数据在实践中往往非常困难,尤其是在涉及大规模数据集的情况下。此外,奖励的概念具有很强的主观性。对于相同的输入,一个人认为好的响应,另一个人可能不认同。因此,获取一个能在不同标注者间标准化的标量分数并非易事。
偏好数据:一种替代方案
为了解决上述问题,研究者们开始寻求不同的数据格式和奖励模型训练方法。其中一种被广泛使用的数据称为“偏好数据”。
以下是偏好数据的格式:
数据格式:(x, y_W, y_L)
x:输入(在RL中可以是状态,在LLM中可以是提示)。y_W:被偏好的输出或响应。y_L:不被偏好的输出或响应。
与为每个响应获取一个标量奖励相比,收集这种相对比较的数据要容易得多。例如,一些商业大语言模型会生成两个回答,并让用户选择更喜欢哪一个,这正是在收集用户偏好数据。由于是相对比较,它也避免了标度不一致的问题。
基于偏好数据构建奖励模型
现在,核心问题是:给定偏好数据,我们如何构建一个奖励模型?
有多种方法可以回答这个问题。接下来,我们将介绍一种经典且著名的模型——布拉德利-特里模型。
布拉德利-特里模型
该模型的核心思想是:对给定的输入 x,模型 y_W 优于 y_L 的概率,可以通过两个响应所获奖励的差异来计算。
数学模型:
P(y_W ≻ y_L | x) = σ( R_φ(x, y_W) - R_φ(x, y_L) )
其中,σ 是逻辑函数(Sigmoid函数):
公式:σ(t) = 1 / (1 + e^{-t})
这个公式非常直观:如果 y_W 的奖励远高于 y_L 的奖励,那么 y_W 被偏好的概率就接*1;如果两者奖励相*,概率就接*0.5。
训练奖励模型
我们的目标是找到一个奖励模型 R_φ,使得它对数据集中所有偏好对 (y_W, y_L) 的预测概率最大化。
因此,我们可以定义奖励模型的损失函数为负对数似然,并对其进行最小化:
损失函数:
L(φ) = - E_{(x, y_W, y_L) ~ D} [ log σ( R_φ(x, y_W) - R_φ(x, y_L) ) ]
训练目标:
找到最优参数 φ*,使得上述损失函数最小化(或等价地,使对数似然最大化)。
通过使用梯度下降法优化这个目标,我们可以训练出奖励模型 R_φ。这个模型学会了为更受偏好的响应分配更高的奖励分数。
总结


本节课我们一起学*了奖励建模的关键知识。我们首先了解了奖励函数在策略梯度算法中的核心作用,以及直接使用标量奖励数据进行监督学*所面临的困难。接着,我们引入了更易于获取的“偏好数据”作为解决方案。最后,我们详细讲解了如何使用布拉德利-特里模型,利用偏好数据来训练一个有效的奖励模型。训练好的奖励模型随后可以被集成到策略梯度优化算法中,用于指导智能体的学*。
071:直接偏好优化(DPO)

概述
在本节课中,我们将要学*一种名为“直接偏好优化”(Direct Preference Optimization, DPO)的算法。该算法的核心目标是:在不依赖显式奖励模型的情况下,仅使用偏好数据来优化策略(例如语言模型)。我们将从DPO的动机出发,逐步推导其数学原理,并解释它为何比传统的PPO等方法更高效。
动机与问题定义
上一节我们介绍了基于人类反馈的强化学*(RLHF),它通常需要先训练一个奖励模型,再使用PPO等策略梯度算法来更新策略。这种方法存在两个主要问题:
- 策略更新的质量严重依赖于奖励模型的性能。
- 训练一个好的奖励模型本身就是一个计算成本高昂的优化问题。
因此,DPO提出的核心问题是:能否绕过奖励模型,直接使用偏好数据来优化策略?
理论基础:带约束的策略优化
首先,我们回顾带约束的策略优化目标。其目标是最大化期望奖励,同时最小化新策略 π_θ 与某个参考策略 π_ref 之间的KL散度。这可以表述为以下优化问题:
目标:max_π E_(x,y)~π [r(x, y)] - β * D_KL(π(y|x) || π_ref(y|x))
其中,β 是拉格朗日乘子,用于控制与参考策略的偏离程度。
可以严格证明,上述优化问题的最优策略 π* 具有以下解析形式(即玻尔兹曼分布):
π*(y|x) = (1 / Z(x)) * π_ref(y|x) * exp( (1/β) * r(x, y) )
这里,Z(x) 是归一化常数,定义为对所有可能的响应 y 求和:Z(x) = Σ_y π_ref(y|x) * exp( (1/β) * r(x, y) )。
然而,由于响应空间 y 通常极其庞大(例如所有可能的文本序列),Z(x) 是难以计算的(intractable)。
从最优策略推导最优奖励
虽然无法直接计算最优策略,但我们可以利用上述关系,反推出最优奖励函数 r* 的表达式。对最优策略的公式两边取对数并整理,可以得到:
r*(x, y) = β * log( π*(y|x) / π_ref(y|x) ) + β * log Z(x)
这个公式表明,最优奖励函数可以通过最优策略与参考策略的对数比值来表示(加上一个与 y 无关的常数项 β * log Z(x))。
核心推导:绕过奖励模型
接下来,我们将这个最优奖励 r* 的表达式,代入之前用于训练奖励模型的布拉德利-特里(Bradley-Terry)偏好模型中。布拉德利-特里模型假设,给定输入 x,偏好 y_w 优于 y_l 的概率为:
P(y_w ≻ y_l | x) = σ( r(x, y_w) - r(x, y_l) ) = exp(r(x, y_w)) / [ exp(r(x, y_w)) + exp(r(x, y_l)) ]
将 r* 的表达式代入上述模型。经过一系列代数运算(此处省略推导细节),我们可以得到一个关键结果:偏好概率可以完全用策略参数 θ 来表示,而不再显式包含奖励函数 r。
具体地,代入后得到:
P(y_w ≻ y_l | x) = σ( β * log( π_θ(y_w|x) / π_ref(y_w|x) ) - β * log( π_θ(y_l|x) / π_ref(y_l|x) ) )
DPO目标函数
我们的目标是找到策略参数 θ,使得模型预测的偏好概率与人类提供的偏好数据 D = { (x, y_w, y_l) } 尽可能一致。这等价于最大化这些偏好数据对的似然。
因此,DPO的最终目标函数是最大化以下对数似然:
**L_DPO(π_θ; π_ref) = E_(x, y_w, y_l)~D [ log σ( β * log( π_θ(y_w|x) / π_ref(y_w|x) ) - β * log( π_θ(y_l|x) / π_ref(y_l|x) ) ) ]`
以下是DPO目标函数的关键组成部分:
π_θ(y|x):待优化的策略(例如,我们想要对齐的语言模型)。π_ref(y|x):参考策略(通常是在大规模无监督数据上预训练得到的初始模型)。β:控制与参考策略偏离程度的超参数。σ(·):逻辑函数(sigmoid)。(x, y_w, y_l):来自偏好数据集D的样本,其中y_w是优于y_l的响应。
DPO的优势与总结
本节课中我们一起学*了直接偏好优化(DPO)的原理。
核心优势:DPO通过巧妙的数学变换,将策略优化问题转化为一个仅依赖于策略本身和偏好数据的目标函数。它完全绕过了训练独立奖励模型的步骤,从而:
- 简化了训练流程:避免了奖励模型训练不准带来的误差传播。
- 提升了计算效率:减少了训练环节和总体计算开销。
- 保持了稳定性:目标函数直接针对最终策略进行优化,通常更稳定。
应用场景:DPO非常适用于拥有大量成对偏好数据(即标注了哪个回答更好)的场景,是当前对齐大型语言模型(LLM)与人类价值观的主流高效方法之一。


补充说明:并非所有任务都适合DPO。对于拥有可验证奖励的任务(例如代码生成,可以通过测试用例通过率定义奖励),传统的基于PPO的策略梯度方法可能更合适。在实际的LLM训练中(如GPT系列),通常的流程是:首先在大规模无监督语料上通过“下一个词预测”目标预训练一个基础模型;然后收集大量人类偏好数据;最后使用DPO(或类似的RLHF方法)对这个基础模型进行对齐微调。
072:状态空间模型 🧠

在本节课中,我们将学*状态空间模型。这是一种用于处理序列数据的重要模型家族,旨在解决Transformer模型在处理长序列时计算量过大的问题。
概述
我们处理的大量数据本质上是序列化的,例如构成基础模型核心的自然语言文本。建模序列,尤其是长序列,在这些场景中至关重要。Transformer一直是处理此类序列数据的首选模型。然而,当上下文长度(即序列长度)增长时,Transformer所需的计算量会呈平方级增长。这是因为在处理每个标记时,都需要计算它与序列中所有其他标记的注意力分数。对于一个长度为 L 的序列,训练和推理都需要 O(L²) 的计算量。
为了解决这个问题,研究社区考虑了一种称为状态空间模型的替代模型家族。在这种模型中,训练和推理所需的计算量不会随序列长度呈平方级增长,而是线性增长。
状态空间模型的数学定义
上一节我们介绍了序列数据建模的挑战,本节我们来看看状态空间模型是如何定义的。
一个状态空间模型由以下方程定义。首先,我们假设输入和输出序列都是一维的。设 u(t) 和 y(t) 分别表示输入和输出序列。两者都是一维信号。
给定一维的输入和输出序列,状态空间模型定义如下:
dx(t)/dt = A * x(t) + B * u(t)
y(t) = C * x(t) + D * u(t)
这是状态空间模型的定义。其中:
x(t)是一个n维的隐藏状态向量。A, B, C, D是可学*的系统参数(模型参数)。dx(t)/dt表示x(t)对时间的导数。
这个模型描述的是:给定一个一维输入信号 u(t),它被投影到一个 n 维的状态空间(或潜在空间)x(t) 上。状态空间中的时间动态演化由第一个方程通过矩阵 A 和 B 学*得到。输出 y(t) 通过矩阵 C 与状态空间相连,而矩阵 D 直接将输出与输入 u(t) 相连。
这与循环神经网络有相似之处,区别在于循环神经网络中连接这些量的关系是非线性的,而在线性状态空间模型中,这些关系是线性的。
在文献中,通常省略 D 矩阵,因为它可以通过一个可学*的跳跃连接来实现。因此,最终连续时间状态空间模型由以下方程给出:
dx(t)/dt = A * x(t) + B * u(t)
y(t) = C * x(t)
其中 A, B, C 是可学*的模型参数。
模型的离散化
我们刚刚定义了连续时间信号的状态空间模型。但在实践中,我们处理的是离散序列(例如文本数据)。因此,应用此模型的第一步是离散化。
离散化意味着将连续时间信号转换为离散时间信号,通常通过采样实现。假设我们有一个离散输入序列 u[k],其中 u[k] = u(k * Δ),Δ 是步长。
离散化后,状态空间方程的参数会发生变化,具体形式取决于所采用的离散化方法。一种常用的方法是双线性变换(或图斯汀方法)。离散化后,参数变换如下:
Ā = (I - Δ/2 * A)^(-1) * (I + Δ/2 * A)
B̄ = (I - Δ/2 * A)^(-1) * Δ * B
C̄ = C
其中 I 是适当大小的单位矩阵,Δ 是离散化所用的步长。
由此,我们得到离散序列的状态空间模型方程:
x[k] = Ā * x[k-1] + B̄ * u[k]
y[k] = C̄ * x[k]
上述方程定义了一个序列到序列的映射(从输入序列 u 到输出序列 y),而连续时间模型定义的是函数到函数的映射。这是所有现代状态空间模型实践应用的基础。
训练离散状态空间模型
我们已经得到了离散状态空间方程,接下来的目标是通过训练数据找到参数 Ā, B̄, C̄。
观察这些方程,它们定义了当前状态与先前状态之间的递归关系。这种递归关系可以用来推导出计算高效的训练算法。
假设初始状态向量为 0(即 x[-1] = 0),展开状态空间模型会得到以下递推关系:
以下是展开后的状态和输出序列:
x[0] = B̄ * u[0]x[1] = Ā * B̄ * u[0] + B̄ * u[1]x[2] = ² * B̄ * u[0] + Ā * B̄ * u[1] + B̄ * u[2]y[0] = C̄ * B̄ * u[0]y[1] = C̄ * Ā * B̄ * u[0] + C̄ * B̄ * u[1]y[2] = C̄ * ² * B̄ * u[0] + C̄ * Ā * B̄ * u[1] + C̄ * B̄ * u[2]
可以将其推广为:
y[k] = C̄ * Ā^k * B̄ * u[0] + C̄ * Ā^(k-1) * B̄ * u[1] + ... + C̄ * B̄ * u[k]
仔细观察,这个形式类似于一个我们熟悉的操作——卷积。如果我们定义一个长度为 L 的卷积核向量 K̄:
K̄ = [C̄*B̄, C̄*Ā*B̄, C̄*²*B̄, ..., C̄*Ā^(L-1)*B̄]
那么输出 y[k] 就是输入序列 u 与该卷积核 K̄ 的卷积结果:
y = K̄ * u (其中 * 表示卷积操作)
这是一个关键结论:输出序列中的第 k 个标记,不过是输入序列 u 与一个固定卷积核 K̄ 的卷积积。这个卷积核是模型参数的函数。这意味着,一旦计算出这个核 K̄,对任何输入进行推理就只需计算该输入与这个预计算核的卷积。
在频域中实现高效计算
我们转向状态空间模型框架是为了更高效地处理长序列。将输出序列表示为卷积形式的意义在于,根据信号处理原理,时域(或序列域)中的卷积等价于频域中的乘法。
由于存在快速算法(如快速傅里叶变换,FFT)可以将序列转换到频域,状态空间模型文献中的核心思想就是:在频域中计算卷积核,通过频域中的乘法运算计算输出,然后通过逆傅里叶变换将输出转换回时域。因为傅里叶变换和逆变换可以利用FFT算法在对数时间内完成,这被认为比Transformer中的平方级计算更快。
因此,接下来的目标是在频域中计算这个卷积核 K̄。
计算卷积核:生成函数方法
在S4等著名的状态空间模型方法中,卷积核是使用生成函数在频域中计算的。
回顾一下,卷积核 K̄ 的表达式为:K̄[i] = C̄ * Ā^i * B̄。其截断生成函数(Z变换)K̂_L(z) 定义为:
K̂_L(z) = Σ_{i=0}^{L-1} K̄[i] * z^{-i} = Σ_{i=0}^{L-1} (C̄ * Ā^i * B̄) * z^{-i}
通过矩阵几何级数公式,并当 L 足够大时,可以证明:
K̂(z) = C̄ * (I - Ā * z^{-1})^{-1} * B̄
这就是卷积核生成函数的最终表达式。需要计算的就是这个式子。然而,直接计算这个表达式并不容易,因此不同的模型会对矩阵 Ā 施加特定的结构,使核计算变得可行。
施加结构:对角矩阵与对角加低秩矩阵
到目前为止,我们尚未对矩阵 Ā 的形式做任何特殊假设。不同的方法通过施加不同的结构来使核计算易于处理。我们将在本模块中研究两种特殊情况。
第一种情况:Ā 为对角矩阵
当 Ā 是对角矩阵 Λ 时,可以很容易地证明,核的生成函数可以写成多个柯西核的和的形式:
K̂(z) = Σ_{i=1}^n (C̄_i * B̄_i) / (1 - Λ_i * z^{-1})
这种形式下,不再需要计算矩阵 Ā 的幂,只需计算这些标量项(柯西核),大大简化了计算。
第二种情况:Ā 为对角加低秩矩阵
让 Ā 仅为对角矩阵限制性太强。一个更一般的情况是让 Ā 具有“对角加低秩”的结构:
Ā = Λ - P * Q^T
其中 Λ 是对角矩阵,P 和 Q 是可学*的秩为1的矩阵。应用矩阵求逆引理(Woodbury恒等式)后,可以证明最终的核函数仍然是多个柯西核的线性组合。这意味着在整个核计算中,同样不涉及矩阵 Ā 的幂运算。
在S4模型中,矩阵 Ā 并非随机初始化,而是初始化为一个特殊的矩阵,称为HiPPO矩阵。经验表明,这种结构能带来更好的泛化性能。
扩展到多维数据与训练
到目前为止,我们的分析都假设输入和输出变量是一维的。对于实践中常见的多维数据,状态空间模型是按维度独立应用的。这意味着数据的每个维度都使用一个独立的一维状态空间模型进行处理。
多个状态空间模型可以像神经网络层一样堆叠起来。一旦构建好序列模型,就可以使用均方根误差或交叉熵等损失函数,并通过梯度下降来更新状态空间模型的参数。这可以通过有监督或无监督的方式完成。
Mamba:选择性状态空间模型
S4模型的一个已知缺点是卷积核是固定的,因为模型参数 A, B, C 不随时间变化。这导致一些简单任务(如选择性复制序列中的特定标记)无法完成。
为了解决这个问题,Mamba(或称选择性状态空间模型)将参数变为时间依赖的。这是Mamba与S4等之前模型的最大区别。数学上,模型变为:
x[k+1] = A(u[k]) * x[k] + B(u[k]) * u[k]
y[k] = C(u[k]) * x[k]
注意,A, B, C 现在是输入序列 u 的函数。
这种变化带来了一个后果:由于参数随时间变化,无法像S4那样推导出一个固定的卷积核并在频域进行高效计算。因此,Mamba的模型前向传播和推理不在频域进行,而是在时域本身完成,并使用了更快的并行扫描算法。这种状态空间方程的定义类似于计算“前缀和”,而前缀和可以通过并行扫描算法进行高效并行化。此外,Mamba还结合了一些针对GPU架构的硬件优化技巧。
与S4类似,Mamba也是独立应用于数据的每个维度。
总结


本节课我们一起学*了状态空间模型。我们从其数学定义出发,探讨了如何将连续时间模型离散化以处理实际序列数据。我们了解到,通过展开方程,状态空间模型的输出可以表示为输入与一个固定卷积核的卷积。为了高效处理长序列,该卷积在频域通过快速傅里叶变换实现。为了简化核的计算,模型(如S4)会对参数矩阵施加特定结构(如对角或对角加低秩)。最后,我们介绍了Mamba模型,它通过使参数依赖于输入,增强了模型的表达能力,但计算方式也转向依赖于时域的并行扫描算法。状态空间模型为处理长序列提供了一种计算高效的Transformer替代方案。

浙公网安备 33010602011771号