Flow Models 详解

Flow Models 详解

生成模型概览

​ 在探索模型结构的多样性时,主流的生成模型大致可分为几类:GAN (生成对抗网络)、VAE (变分自编码器)、Flow (流模型)、Diffusion (扩散模型)以及 AR (自回归模型)等。它们各有特点,致力于从不同角度解决数据生成的问题。

生成模型概览图1

包含AR模型的概览图

基于流的生成模型 (Flow-based Generative Model)

​ 首先需要回顾一下生成模型的核心目标:最大化观测数据的对数似然。简单来说,我们希望模型学习到的数据分布 pθ(x)p_\theta(\boldsymbol x) 尽可能地接近真实数据的分布 pdata(x)p_{data}(\boldsymbol x)

​ 然而,直接对高维复杂数据的对数似然 logpθ(x)\log p_\theta(\boldsymbol x) 进行建模和优化通常非常困难。因此,像 VAE 和扩散模型等方法,往往通过优化对数似然的一个下界 (Evidence Lower Bound, ELBO) 来间接达到目的。虽然这在实践中取得了成功,但优化下界终究与直接优化似然本身存在一定的差距。

基于流 (Flow-based) 的模型则提供了一条直接优化对数似然的路径。 其核心思想在于构建一个从真实数据分布到某个简单、易于处理的先验分布(例如标准正态分布)的可逆映射 (双射)。借助数学中的变量替换定理 (Change of Variables Theorem),这个可逆映射可以将复杂数据分布下的概率密度计算,转换为在简单先验分布下的概率密度计算,从而实现对数据对数似然的直接建模和优化

​ 具体来说,我们的优化目标是最大化训练数据集 {x1,x2,,xm}\{\boldsymbol x_1,\boldsymbol x_2,\ldots,\boldsymbol x_m \} (假设其独立同分布采样自 pdatap_{data}) 的总对数似然:

J(θ)=maxθi=1mlogpθ(xi)J(\theta)=\max_\theta\sum_{i=1}^m \log p_\theta(\boldsymbol x^i)

现在,我们引入隐变量 z\boldsymbol z,其服从一个我们已知的简单先验分布 π(z)\pi(\boldsymbol z) (例如 N(0,I)\mathcal{N}(\boldsymbol 0, \boldsymbol I))。我们希望学习一个可逆的映射函数 Gθ:ZXG_\theta: \mathcal{Z} \to \mathcal{X},它可以将隐变量 z\boldsymbol z 映射到数据空间,即 x=Gθ(z)\boldsymbol x = G_\theta(\boldsymbol z)。相应地,其逆映射为 Gθ1:XZG_\theta^{-1}: \mathcal{X} \to \mathcal{Z},即 z=Gθ1(x)\boldsymbol z = G_\theta^{-1}(\boldsymbol x)

根据变量替换定理,由 GθG_\theta 定义的数据分布 pθ(x)p_\theta(\boldsymbol x) 可以通过隐变量的概率密度 π(z)\pi(\boldsymbol z) 表示为:

pθ(x)=π(Gθ1(x))det(Gθ1(x)x)p_\theta(\boldsymbol x) = \pi(G_\theta^{-1}(\boldsymbol x)) \cdot \left| \det \left( \frac{\partial G_\theta^{-1}(\boldsymbol x)}{\partial \boldsymbol x} \right) \right|

其中,Gθ1(x)x\frac{\partial G_\theta^{-1}(\boldsymbol x)}{\partial \boldsymbol x} 是逆映射 Gθ1G_\theta^{-1} 在点 x\boldsymbol x 处的雅可比矩阵 (Jacobian Matrix),而 det()\left| \det \left( \dots \right) \right| 表示其行列式的绝对值。为了简洁,我们将其记为 detJGθ1(x)\left| \det J_{G_\theta^{-1}}(\boldsymbol x) \right|。于是,单一样本的对数似然 logpθ(x)\log p_\theta(\boldsymbol x) 可以写为:

logpθ(x)=logπ(Gθ1(x))+logdetJGθ1(x)\log p_\theta(\boldsymbol x) = \log \pi(G_\theta^{-1}(\boldsymbol x)) + \log \left| \det J_{G_\theta^{-1}}(\boldsymbol x) \right|

将此表达式代入我们最初的优化目标 J(θ)J(\theta),得到:

J(θ)=maxθi=1m[logπ(Gθ1(xi))+logdetJGθ1(xi)]J(\theta) = \max_\theta \sum_{i=1}^m \left[ \log \pi(G_\theta^{-1}(\boldsymbol x^i)) + \log \left| \det J_{G_\theta^{-1}}(\boldsymbol x^i) \right| \right]

​ 这个公式是流模型训练的核心。它由两部分组成:第一项 logπ(Gθ1(xi))\log \pi(G_\theta^{-1}(\boldsymbol x^i)) 鼓励模型将数据点 xi\boldsymbol x^i 映射到先验分布 π\pi 下具有较高概率的隐变量;第二项 logdetJGθ1(xi)\log \left| \det J_{G_\theta^{-1}}(\boldsymbol x^i) \right| 则是雅可比行列式的对数,它衡量了从数据空间到隐空间的映射过程中发生的体积变化

可逆变换的数学本质

​ 流模型成功的关键在于精心设计的可逆变换函数 f:XZf:\mathcal{X}\to \mathcal{Z} (在上述讨论中对应 Gθ1G_\theta^{-1})。通过这个变换及其雅可比行列式,我们可以精确地转换概率密度:

logpX(x)=logpZ(f(x))+logdetJf(x)\log p_{\mathcal{X}}(\boldsymbol x) = \log p_{\mathcal{Z}}(f(\boldsymbol x)) + \log\left|\det J_f(\boldsymbol x)\right|

这里有几个关键点需要强调:

  1. 维度保持特性:与 VAE 等模型可能将数据压缩到低维隐空间不同,流模型中的可逆变换通常要求输入 X\mathcal{X} 和输出 Z\mathcal{Z} 的维度严格一致。这既是一个约束,也使得模型能够保留数据的全部信息。
  2. 参数共享的映射:Flow 模型的核心是学习一个可逆映射 fθ:XZf_\theta: \mathcal{X} \leftrightarrow \mathcal{Z}。无论是从数据空间 X\mathcal{X} 到隐空间 Z\mathcal{Z} 的编码过程(即 fθf_\theta,对应前文的 Gθ1G_\theta^{-1}),还是从隐空间 Z\mathcal{Z} 到数据空间 X\mathcal{X} 的生成过程(即 fθ1f_\theta^{-1},对应前文的 GθG_\theta),都由同一组参数 θ\theta 控制。因此,不像 VAE 那样需要分别训练编码器和解码器网络,Flow 模型通过优化上述对数似然来学习这单一可逆映射的参数

​ 一个重要的观察是:在流模型中,隐空间 Z\mathcal{Z} 的维度通常与原始数据空间 X\mathcal{X} 的维度保持一致。 如果我们随意设计一般的可逆变换 fkf_k,计算其雅可比行列式 detJfk\det J_{f_k} 的复杂度可能是 O(D3)\mathcal{O}(D^3)DD 是数据维度),这对于高维数据(如图像)是难以承受的。

因此,对变换 fkf_k 的设计有如下关键要求:

  • 必须可逆:这是流模型的根本。
  • 雅可比行列式易于计算:这是模型实用性的保证。通常希望计算复杂度为 O(D)\mathcal{O}(D) 或更低。这引导我们设计具有特定结构的变换,例如那些雅可比矩阵是三角矩阵的变换。
模型 前向过程 反向过程
Normalizing Flow 通过显式的可学习变换将样本分布变换为标准高斯分布 从标准高斯分布采样,并通过上述变换的逆变换得到生成的样本
Diffusion Model 通过不可学习的 schedule 对样本进行加噪,多次加噪变换为标准高斯分布 从标准高斯分布采样,通过模型隐式地学习反向过程的噪声,去噪得到生成样本

归一化流 (Normalizing Flow)

​ “归一化流”这一名称强调了模型将复杂数据分布“归一化”为一个标准、简单的目标分布(通常是标准正态分布)的过程。这与 VAE 中强制后验分布逼近标准正态先验有相似之处

归一化流示意图
由于单个可逆变换 GG 的表达能力可能有限(特别是为了保证可逆性和雅可比行列式易算性而引入的结构约束),实践中我们通常将多个简单可逆变换(称为“流层”或“仿射耦合层”等)串联起来,形成一个更强大、更具表达能力的复合变换:

xf1z1f2z2zK1fKzK=z\boldsymbol x \xrightarrow{f_1} \boldsymbol z_1 \xrightarrow{f_2} \boldsymbol z_2 \xrightarrow{\dots} \boldsymbol z_{K-1} \xrightarrow{f_K} \boldsymbol z_K = \boldsymbol z

其中 x\boldsymbol x 是输入数据,z\boldsymbol z 是最终的隐变量,每一个 fkf_k 都是一个可逆变换。

多层流变换示意图

​ 对于这样的复合变换,根据链式法则,总的雅可比行列式是对每一层雅可比行列式的连乘。因此,总的对数似然贡献也是各层对数雅可比行列式之和:

logpX(x)=logpZ(z)+k=1KlogdetJfk(zk1)\log p_{\mathcal{X}}(\boldsymbol x) = \log p_{\mathcal{Z}}(\boldsymbol z) + \sum_{k=1}^K \log \left| \det J_{f_k}(\boldsymbol z_{k-1}) \right|

其中 z0=x\boldsymbol z_0 = \boldsymbol xzk=fk(zk1)\boldsymbol z_k = f_k(\boldsymbol z_{k-1})

​ 此时,优化目标变为:

J(θ)=maxθi=1m[logπ(zi)+k=1KlogdetJfk(zk1i)]J(\theta) = \max_\theta \sum_{i=1}^m \left[ \log \pi(\boldsymbol z^i) + \sum_{k=1}^K \log \left| \det J_{f_k}(\boldsymbol z_{k-1}^i) \right| \right]

其中 zi\boldsymbol z^i 是样本 xi\boldsymbol x^i 经过整个流变换序列后的最终隐表示

Coupling Blocks:仿射耦合层 (Affine Coupling Layer)

​ 为了满足 flow 模型可逆且雅可比矩阵便于计算的要求,有多种巧妙的层设计,其中仿射耦合层是 RealNVP、NICE 和 Glow 等模型的核心组件。

其基本思想是将输入向量 x\boldsymbol x 分成两部分,例如 x=(xA,xB)\boldsymbol x = (\boldsymbol x_A, \boldsymbol x_B)。变换时,一部分 (xA\boldsymbol x_A) 保持不变,而另一部分 (xB\boldsymbol x_B) 则通过一个仿射变换进行更新,该仿射变换的参数 (尺度 ss 和平移 tt) 由保持不变的那部分 (xA\boldsymbol x_A) 计算得出。

仿射耦合层示意图

具体地,对于一个从 x\boldsymbol xy\boldsymbol y 的耦合层变换 GG:

  1. 将输入 x\boldsymbol x 沿某个维度(例如通道维度)切分为两半:xA,xB\boldsymbol x_A, \boldsymbol x_B
  2. 第一部分保持不变:yA=xA\boldsymbol y_A = \boldsymbol x_A
  3. 第二部分经过仿射变换:yB=xBexp(s(xA))+t(xA)\boldsymbol y_B = \boldsymbol x_B \odot \exp(s(\boldsymbol x_A)) + t(\boldsymbol x_A)
    其中 \odot 表示逐元素相乘。尺度参数 ss 和平移参数 tt 都是通过神经网络(例如几层全连接或卷积层)作用于 xA\boldsymbol x_A 得到的。exp(s(xA))\exp(s(\boldsymbol x_A)) 确保尺度因子为正。

这个变换的雅可比矩阵 JG(x)J_G(\boldsymbol x) 具有如下形式(假设 xA\boldsymbol x_A 是前 dd 维,xB\boldsymbol x_B 是后 DdD-d 维):

Jf(x)=[Id0d×(Dd)yBxAdiag(exp(s(xA)))]\boldsymbol{J}_f(\boldsymbol x) = \begin{bmatrix} \mathbb{I}_d & \boldsymbol{0}_{d\times(D-d)} \\ \frac{\partial\boldsymbol{y}_B} {\partial\boldsymbol{x}_A} & \mathrm{diag}(\exp(s(\boldsymbol{x}_A))) \end{bmatrix}

雅可比行列式计算图示

这是一个下三角矩阵(或上三角,取决于分割和更新的顺序),其行列式就是对角线元素的乘积:

det(JG(x))=jexp(sj(xA))=exp(jsj(xA))\det(J_G(\boldsymbol x)) = \prod_j \exp(s_j(\boldsymbol x_A)) = \exp\left(\sum_j s_j(\boldsymbol x_A)\right)

因此,对数雅可比行列式可以非常高效地计算:

logdet(Jf(x))=jsj(xA)\log |\det(J_f(\boldsymbol x))| = \sum_j s_j(\boldsymbol x_A)

这个变换的逆变换也容易计算:

  1. xA=yA\boldsymbol x_A = \boldsymbol y_A
  2. xB=(yBt(yA))exp(s(yA))\boldsymbol x_B = (\boldsymbol y_B - t(\boldsymbol y_A)) \odot \exp(-s(\boldsymbol y_A))

为了让所有维度都能得到更新,通常会交替地将不同部分的维度作为 xA\boldsymbol x_A(保持不变的部分)。例如,在一个耦合层中,前一半维度不变,后一半更新;在下一个耦合层中,后一半维度不变,前一半更新(通过一个固定的排列操作,如翻转,来实现)。

堆叠耦合层示意图

通过堆叠多个这样的耦合层,并可能在它们之间加入维度重排(如1x1卷积或固定置换),模型可以学习到非常复杂和灵活的数据变换。

Autoregressive Flows:自回归流

自回归流 (Autoregressive Flow) 是另一类重要的流模型,在自回归流中,数据点 x=(x1,x2,,xD)\boldsymbol x = (x_1, x_2, \ldots, x_D) 的每个维度 xix_i 到对应隐变量 ziz_i (或者反过来) 的变换,都依赖于 x\boldsymbol x (或 z\boldsymbol z) 的前 i1i-1 个维度

对于从 z\boldsymbol zx\boldsymbol x 的变换 GG:

xi=τ(zi;hi(z<i))x_i = \tau(z_i; \boldsymbol{h}_i(\boldsymbol z_{<i}))

其中 τ\tau 是一个关于 ziz_i 的可逆标量函数,其参数(例如仿射变换中的尺度 αi\alpha_i 和偏置 βi\beta_i)由一个条件网络 hi\boldsymbol{h}_i 根据 z<i=(z1,,zi1)\boldsymbol z_{<i} = (z_1, \ldots, z_{i-1}) 计算得到。这意味着 xix_i 的生成依赖于 ziz_i 以及所有在 ii 之前的隐变量 z1,,zi1z_1, \ldots, z_{i-1}

autoregressiv flow

这种结构的关键优势在于其雅可比矩阵的特性。对于上述从 z\boldsymbol zx\boldsymbol x 的变换 GG,其雅可比矩阵 JG(z)=xzJ_G(\boldsymbol z) = \frac{\partial \boldsymbol x}{\partial \boldsymbol z} 是一个下三角矩阵:

Jf(z)=(x1z100x2z1x2z20xDz1xDz2xDzD)J_f(\boldsymbol z) = \begin{pmatrix} \frac{\partial x_1}{\partial z_1} & 0 & \cdots & 0 \\ \frac{\partial x_2}{\partial z_1} & \frac{\partial x_2}{\partial z_2} & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial x_D}{\partial z_1} & \frac{\partial x_D}{\partial z_2} & \cdots & \frac{\partial x_D}{\partial z_D} \end{pmatrix}

这是因为 xix_i 的计算只依赖于 z1,,ziz_1, \ldots, z_i,而不依赖于 zjz_{j} 其中 j>ij>i。因此,xizj=0\frac{\partial x_i}{\partial z_j} = 0 对于 j>ij>i

三角矩阵的行列式就是其对角线元素的乘积:

det(JG(z))=i=1Dxizi\det(J_G(\boldsymbol z)) = \prod_{i=1}^D \frac{\partial x_i}{\partial z_i}

如果变换 τ\tau 是一个仿射变换,例如 xi=αi(z<i)zi+βi(z<i)x_i = \alpha_i(\boldsymbol z_{<i}) z_i + \beta_i(\boldsymbol z_{<i}),那么对角线元素就是尺度参数 αi(z<i)\alpha_i(\boldsymbol z_{<i})。因此,对数雅可比行列式可以简单地计算为:

logdet(Jf(z))=i=1Dlogαi(z<i)\log |\det(J_f(\boldsymbol z))| = \sum_{i=1}^D \log |\alpha_i(\boldsymbol z_{<i})|

条件网络 hi\boldsymbol{h}_i 通常使用能够有效处理序列依赖性的架构,如循环神经网络 (RNN) 或更常见的 Masked Autoencoder for Distribution Estimation (MADE) 以及 PixelCNN/WaveNet 中的掩码卷积。

  • 采样 (生成):从 p(z)p(\boldsymbol z) 采样得到 z\boldsymbol z,然后计算 x\boldsymbol x。这个过程是高效的,因为给定 z\boldsymbol z,所有的 xix_i 都可以并行计算(如果 hi\boldsymbol{h}_i 是一个掩码自编码器作用于 z\boldsymbol z
  • 似然计算 (训练):计算 p(x)p(\boldsymbol x) 需要先得到 z=f1(x)\boldsymbol z = f^{-1}(\boldsymbol x)zi=τ1(xi;hi(z<i))z_i = \tau^{-1}(x_i; \boldsymbol{h}_i(\boldsymbol z_{<i})) 的计算是串行的,因为计算 ziz_i 需要 z1,,zi1z_1, \ldots, z_{i-1}。这使得似然评估相对较慢

Residual Flows:残差流

residual flow

残差流 (Residual Flow, ResFlow) 提供了一种构建可逆变换的替代方法,它借鉴了深度残差网络 (ResNet) 的思想。其核心是将变换 f:XYf: \mathcal{X} \to \mathcal{Y} 定义为一个残差块的形式:

y=f(x)=x+g(x;θ)\boldsymbol y = f(\boldsymbol x) = \boldsymbol x + g(\boldsymbol x; \theta)

其中 gg 是一个神经网络(例如一个标准的 ResNet 块),θ\theta 是其参数

JG

可逆性与逆变换

与耦合层或自回归流不同,上述形式的残差变换通常没有解析的逆函数 f1f^{-1}。然而,如果函数 gg 满足特定的 Lipschitz 连续性条件,可以保证 GG 是可逆的,并且可以通过不动点迭代来计算其逆变换。

具体来说,如果 gg 关于 x\boldsymbol x 是 Lipschitz 连续的,且其 Lipschitz 常数 Lip(g)<1\text{Lip}(g) < 1(压缩映射),则 f(x)=x+g(x)f(\boldsymbol x) = \boldsymbol x + g(\boldsymbol x) 是可逆的。其逆变换 x=f1(y)\boldsymbol x = f^{-1}(\boldsymbol y) 可以通过以下不动点迭代求解:

xk+1=yg(xk;θ)\boldsymbol x_{k+1} = \boldsymbol y - g(\boldsymbol x_k; \theta)

从某个初始值 x0\boldsymbol x_0 (例如 x0=y\boldsymbol x_0 = \boldsymbol y) 开始迭代,序列 xk\boldsymbol x_k 会收敛到真实的逆 f1(y)f^{-1}(\boldsymbol y)。在实践中,这个迭代过程会执行固定的步数或直到满足某个收敛准则。Lip(g)<1\mathrm{Lip}(g) < 1 的条件可以通过对 gg 的权重进行谱归一化等技术来近似或强制满足

雅可比行列式的计算

残差变换的雅可比矩阵为 JG(x)=I+Jg(x)J_G(\boldsymbol x) = \boldsymbol I + J_g(\boldsymbol x),其中 Jg(x)J_g(\boldsymbol x)g(x)g(\boldsymbol x) 关于 x\boldsymbol x 的雅可比矩阵。直接计算 det(I+Jg(x))\det(\boldsymbol I + J_g(\boldsymbol x)) 的复杂度通常是 O(D3)\mathcal{O}(D^3),这对于高维数据是不可接受的。

ResFlow 的一个关键贡献是采用了一种无偏的随机估计方法来计算对数雅可比行列式 logdet(JG(x))\log |\det(J_G(\boldsymbol x))|,而无需显式计算整个雅可比矩阵。这通常基于以下公式和 Hutchinso 迹估计:

logdet(I+Jg(x))=Tr(log(I+Jg(x)))\log |\det(\boldsymbol I + J_g(\boldsymbol x))| = \mathrm{Tr}(\log(\boldsymbol I + J_g(\boldsymbol x)))

其中 Tr()\mathrm{Tr}(\cdot) 表示矩阵的迹。log(I+Jg(x))\log(\boldsymbol I + J_g(\boldsymbol x)) 可以通过其泰勒级数展开来近似(前提是 Jg(x)J_g(\boldsymbol x) 的特征值满足一定条件,这与 Lip(g)<1\mathrm{Lip}(g)<1 相关):

log(I+A)=AA22+A33=k=1(1)k+1kAk\log(\boldsymbol I + A) = A - \frac{A^2}{2} + \frac{A^3}{3} - \dots = \sum_{k=1}^\infty \frac{(-1)^{k+1}}{k} A^k

然后,Hutchinson迹估计器被用来估计迹:

Hutchinson 迹估计:

对于任意矩阵 MMTr(M)=Evp(v)[vTMv]\mathrm{Tr}(M) = \mathbb{E}_{\boldsymbol v \sim p(\boldsymbol v)}[\boldsymbol v^T M \boldsymbol v],其中 p(v)p(\boldsymbol v) 是一个均值为0、协方差为单位阵的分布(例如,每个元素独立从 Rademacher 分布 {±1}\{\pm 1\} 或标准正态分布中采样)。

因此,对数行列式可以估计为:

logdet(Jf(x))Evp(v)[vT(k=1K(1)k+1k(Jg(x))k)v]\log |\det(J_f(\boldsymbol x))| \approx \mathbb{E}_{\boldsymbol v \sim p(\boldsymbol v)}\left[\boldsymbol v^T \left(\sum_{k=1}^K \frac{(-1)^{k+1}}{k} (J_g(\boldsymbol x))^k\right) \boldsymbol v\right]

其中 KK 是截断的级数项数。每一项 vT(Jg(x))kv\boldsymbol v^T (J_g(\boldsymbol x))^k \boldsymbol v 都可以通过 kk 次向量-雅可比积 (vector-Jacobian products, VJPs) 或雅可比-向量积 (Jacobian-vector products, JVPs) 高效计算,而无需实例化完整的雅可比矩阵 Jg(x)J_g(\boldsymbol x)。这使得计算复杂度大致为 O(KDCg)\mathcal{O}(K \cdot D \cdot C_g),其中 CgC_g 是计算 gg 的一次前向/反向传播的代价

  • gg 函数的选择非常灵活,可以直接使用标准的深度学习模块(如ResNet块)。相比结构受限的耦合层和自回归层,理论上可以构建更具表达能力的变换(但是严格满足 Lip(g)<1\mathrm{Lip}(g) < 1 可能比较困难,或者可能过度约束模型表达能力)
  • 对数雅可比行列式的估计会给损失函数和梯度带来噪声/方差,可能影响训练的稳定性和收敛速度。逆变换依赖迭代求解,可能需要较多计算步骤,尤其是在生成新样本时

训练与推断

训练 (Training)

流模型的训练遵循严格的极大似然准则。目标是最小化负对数似然 (Negative Log-Likelihood, NLL):

Lflow=Expdata[logpX(x)]=Expdata[logpZ(fθ(x))+k=1KlogdetJfk(zk1)]\mathcal{L}_{\mathrm{flow}} = -\mathbb{E}_{\boldsymbol x \sim p_{\mathrm{data}}}\left[\log p_{\mathcal{X}}(\boldsymbol x)\right] = -\mathbb{E}_{\boldsymbol x \sim p_{\mathrm{data}}}\left[\log p_{\mathcal{Z}}(f_{\theta}(\boldsymbol x)) + \sum_{k=1}^K \log\left|\det J_{f_k}(\boldsymbol z_{k-1})\right|\right]

其中 fθ(x)f_\theta(\boldsymbol x) 是整个流变换序列作用于 x\boldsymbol x 后得到的最终隐变量 zK\boldsymbol z_K,而 fkf_k 是序列中的第 kk 个变换,JfkJ_{f_k} 是其雅可比矩阵,zk1\boldsymbol z_{k-1} 是第 kk 个变换的输入。

这个损失函数包含两个核心组成部分:

  • 先验匹配项: logpZ(fθ(x))\log p_{\mathcal{Z}}(f_{\theta}(\boldsymbol x))。这一项驱动模型学习到的隐空间分布 fθ(x)f_{\theta}(\boldsymbol x) 尽可能逼近我们预设的简单先验分布 pZp_{\mathcal{Z}}(例如,标准正态分布)。
  • 流形校正项 (体积变化项): k=1KlogdetJfk(zk1)\sum_{k=1}^K \log\left|\det J_{f_k}(\boldsymbol z_{k-1})\right|。这一项是所有变换层对数雅可比行列式的总和,它精确地衡量了从数据空间到隐空间的映射过程中,局部体积是如何变化的。

通过梯度下降等优化算法最小化 Lflow\mathcal{L}_{\mathrm{flow}},我们可以学习到变换函数 fθf_\theta 的参数。

推断/生成 (Inference/Generation)

训练完成后,我们可以利用学习到的可逆变换进行多种操作:

  • 密度估计: 对于一个新的数据点 xnew\boldsymbol x_{new},我们可以通过 fθ(xnew)f_\theta(\boldsymbol x_{new}) 计算其在隐空间的表示 znew\boldsymbol z_{new},并利用公式 logpX(xnew)=logpZ(znew)+logdetJfk\log p_{\mathcal{X}}(\boldsymbol x_{new}) = \log p_{\mathcal{Z}}(\boldsymbol z_{new}) + \sum \log|\det J_{f_k}| 来精确计算其对数似然值。这对于异常检测等任务非常有用。
  • 数据生成: 要生成新的样本,我们首先从先验分布 pZp_{\mathcal{Z}} 中采样一个隐变量 zsample\boldsymbol z_{sample},然后通过逆变换 fθ1=fK1f11f_\theta^{-1} = f_K^{-1} \circ \dots \circ f_1^{-1} 将其映射回数据空间,得到新的样本 xsample=fθ1(zsample)\boldsymbol x_{sample} = f_\theta^{-1}(\boldsymbol z_{sample})

以下是一个使用 PyTorch 实现的简化版 RealNVP (一种基于耦合层的流模型) 的示例代码,用于二维数据分布的学习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import sklearn.datasets # 确保导入 sklearn.datasets

# RealNVP中的仿射耦合层
class CouplingLayer(nn.Module):
def __init__(self, input_dim, hidden_dim, parity):
super().__init__()
self.parity = parity # 用于决定哪一半数据被转换,哪一半保持不变
# input_dim 是整个输入的维度,例如2D数据就是2
# 我们将输入对半分,一半 (dim_half) 用于条件,另一半 (dim_half) 被变换
dim_half = input_dim // 2

# s_net 和 t_net 用于从 x_a 计算尺度(s)和平移(t)参数
# 输入是 dim_half (x_a 的维度)
# 输出也是 dim_half (s 和 t 各自的维度,与 x_b 的维度相同)
self.s_net = nn.Sequential(
nn.Linear(dim_half, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, dim_half) # 输出 s
)
self.t_net = nn.Sequential(
nn.Linear(dim_half, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, dim_half) # 输出 t
)

def forward(self, x, reverse=False):
# x 的维度是 input_dim
# 沿最后一个维度对半分
xa, xb = torch.chunk(x, 2, dim=-1)

if self.parity: # 如果 parity 为 True,则交换 xa 和 xb,使得这次变换作用于原始的 xa 部分
xa, xb = xb, xa

# s 和 t 是根据 xa 计算得到的
s = self.s_net(xa)
t = self.t_net(xa)

if reverse: #逆向传播:从隐空间 z 到数据空间 x
yb = (xb - t) * torch.exp(-s) # 注意这里是 -s
log_det_jacobian = -s.sum(dim=-1)
else: # 正向传播:从数据空间 x 到隐空间 z
yb = xb * torch.exp(s) + t
log_det_jacobian = s.sum(dim=-1) # 对数雅可比行列式是 sum(s)

if self.parity: # 如果之前交换过,再交换回来以保持输出顺序
y = torch.cat([yb, xa], dim=-1)
else:
y = torch.cat([xa, yb], dim=-1)

return y, log_det_jacobian

def inverse(self, y):
return self.forward(y, reverse=True)


class NormalizingFlow(nn.Module):
def __init__(self, input_dim=2, hidden_dim=256, num_layers=6):
super().__init__()
self.layers = nn.ModuleList()
for i in range(num_layers):
# 交替 parity,使得每一部分数据都有机会被变换
self.layers.append(CouplingLayer(input_dim, hidden_dim, i % 2 == 0))

def forward(self, x): # 从数据 x 到隐变量 z
log_det_sum = torch.zeros(x.shape[0], device=x.device)
for layer in self.layers:
x, log_det = layer(x)
log_det_sum += log_det
return x, log_det_sum

def inverse(self, z): # 从隐变量 z 到数据 x (生成)
log_det_sum = torch.zeros(z.shape[0], device=z.device) # 对于生成过程,log_det通常不直接用于损失
for layer in reversed(self.layers): # 注意是反向通过各层
z, log_det = layer.inverse(z) # 调用各层的 inverse 方法
log_det_sum += log_det # 这里的log_det是逆变换的log_det
return z, log_det_sum


# 生成一些样本数据,例如双月形数据
def sample_data(n_samples=1024):
data, _ = sklearn.datasets.make_moons(n_samples=n_samples, noise=0.05)
return torch.tensor(data, dtype=torch.float32)

# 定义损失函数:负对数似然
# z 是 x 映射到隐空间的表示
# log_det 是从 x 到 z 变换的对数雅可比行列式
# prior 是隐空间的先验分布,例如标准正态分布
def loss_function(z, log_det_jacobian, prior):
# log p(z) = prior.log_prob(z)
# log p(x) = log p(z) + log_det_jacobian
# 我们希望最大化 log p(x),等价于最小化 -log p(x)
log_likelihood = prior.log_prob(z).sum(dim=-1) + log_det_jacobian
return -log_likelihood.mean()


def train_flow(dim=2, num_epochs=10000, batch_size=512, lr=1e-3):
flow_model = NormalizingFlow(input_dim=dim, num_layers=8, hidden_dim=128)
# 定义先验分布为标准正态分布
prior = torch.distributions.Normal(torch.zeros(dim), torch.ones(dim))
optimizer = optim.Adam(flow_model.parameters(), lr=lr)

print("开始训练 Flow 模型...")
for epoch in range(num_epochs):
data = sample_data(batch_size) # 获取一批数据

optimizer.zero_grad()

# 正向传播:将数据 x 映射到隐变量 z,并计算 log_det_jacobian
z, log_det_jacobian = flow_model(data)

# 计算损失
loss = loss_function(z, log_det_jacobian, prior)

loss.backward()
torch.nn.utils.clip_grad_norm_(flow_model.parameters(), 1.0) # 梯度裁剪防止梯度爆炸
optimizer.step()

if (epoch + 1) % 500 == 0:
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}")

print("训练完成!")
return flow_model, prior


def visualize_results(flow_model, prior, data_samples, num_generated_samples=1000):
flow_model.eval() # 设置为评估模式

# 1. 原始数据分布
plt.figure(figsize=(18, 6))
plt.subplot(1, 3, 1)
plt.scatter(data_samples[:, 0], data_samples[:, 1], s=10, alpha=0.5, c='blue')
plt.title("Original Data Distribution (Moons)")
plt.xlabel("x1")
plt.ylabel("x2")
plt.xlim(-2, 3)
plt.ylim(-1.5, 2)


# 2. 数据通过Flow模型映射到隐空间的分布
with torch.no_grad():
z_transformed, _ = flow_model(data_samples)
z_transformed = z_transformed.numpy()
plt.subplot(1, 3, 2)
plt.scatter(z_transformed[:, 0], z_transformed[:, 1], s=10, alpha=0.5, c='green')
plt.title("Data Mapped to Latent Space (Z)")
plt.xlabel("z1")
plt.ylabel("z2")
# 比较理想的情况是接近标准正态分布
# 可以画一个标准正态分布的等高线图作为参考
xx, yy = np.meshgrid(np.linspace(-3, 3, 100), np.linspace(-3, 3, 100))
zz_prior = np.exp(-0.5 * (xx**2 + yy**2)) / (2 * np.pi)
plt.contour(xx, yy, zz_prior, levels=5, alpha=0.3, cmap='gray')
plt.xlim(-4, 4)
plt.ylim(-4, 4)


# 3. 从先验分布采样,并通过逆变换生成新数据
with torch.no_grad():
z_samples = prior.sample((num_generated_samples,))
x_generated, _ = flow_model.inverse(z_samples)
x_generated = x_generated.numpy()

plt.subplot(1, 3, 3)
plt.scatter(x_generated[:, 0], x_generated[:, 1], s=10, alpha=0.5, c='red')
plt.title("Generated Data from Latent Samples")
plt.xlabel("x1")
plt.ylabel("x2")
plt.xlim(-2, 3)
plt.ylim(-1.5, 2)

plt.tight_layout()
plt.show()


if __name__ == '__main__':
# 训练模型
trained_model, model_prior = train_flow(dim=2, num_epochs=10000) # 增加迭代次数以获得更好效果

# 可视化结果
original_data_for_plot = sample_data(1000)
visualize_results(trained_model, model_prior, original_data_for_plot)