Toriskia 's Blog

13 篇文章 · 12 个标签 · 6 个友链

← 返回文章列表

2026.03.31

Normalizing Flow

包含 AI 辅助生成内容

一、从生成模型出发#

生成模型的目标是学习数据分布 pdata(x)p_{\text{data}}(x),并能够从模型中采样出与真实数据相似的样本。

一种常见思路是先定义一个简单的潜变量分布:

z0π(z)z_0 \sim \pi(z)

其中 π(z)\pi(z) 通常取标准高斯分布 N(0,I)\mathcal N(0, I)。然后用一个生成器 GG 把简单分布映射到复杂的数据空间:

x=G(z0)x = G(z_0)

这对应了一个非常直观的生成过程:

  1. 从简单先验 π(z)\pi(z) 中采样一个噪声变量 z0z_0
  2. 通过映射 GG 得到样本 xx

生成模型示意图

这里的关键问题是:如果只知道采样过程,能不能显式写出生成后的概率密度 p(x)p(x)

对于一般生成器,答案往往是否定的;但如果 GG可逆且可微的,那么就可以用变量变换公式精确计算密度。这正是 Normalizing Flow 的基础。


二、变量变换公式#

1. 一维情形#

x=G(z),z=G1(x)x = G(z), \quad z = G^{-1}(x)

在一个很小的区间内,概率质量守恒:

p(x)Δx=π(z)Δzp(x') \Delta x = \pi(z') \Delta z

因此有:

p(x)=π(z)dzdxp(x') = \pi(z') \left| \frac{\mathrm dz}{\mathrm dx} \right|

也就是:

p(x)=π(G1(x))dG1(x)dxp(x) = \pi(G^{-1}(x)) \left| \frac{\mathrm d G^{-1}(x)}{\mathrm dx} \right|

2. 多维情形#

在多维空间中,导数要推广为雅可比矩阵。若 G:RdRdG : \mathbb R^d \to \mathbb R^d 可逆且可微,则:

p(x)=π(z)det(JG1(x)),z=G1(x)p(x) = \pi(z) \left| \det\left(J_{G^{-1}}(x)\right) \right|, \quad z = G^{-1}(x)

其中:

JG1(x)=G1(x)xJ_{G^{-1}}(x) = \frac{\partial G^{-1}(x)}{\partial x}

这个式子的本质仍然是“概率质量守恒”,只是:

  • 一维里的长度缩放,变成了多维里的体积缩放
  • 体积缩放由雅可比行列式刻画

于是对数密度可以写成:

logp(x)=logπ(G1(x))+logdet(JG1(x))\log p(x) = \log \pi(G^{-1}(x)) + \log \left| \det\left(J_{G^{-1}}(x)\right) \right|

这一步把生成模型训练转化成了一个可计算的最大似然问题


三、Normalizing Flow#

1. 为什么叫 Flow#

Normalizing Flow 的核心做法是:不直接用一个复杂的大映射,而是把它拆成很多个简单、可逆、且雅可比行列式容易计算的变换。

设:

zk=fk(zk1),k=1,2,,Kz_k = f_k(z_{k-1}), \quad k = 1, 2, \dots, K

则整体变换为:

x=zK=fKfK1f1(z0)x = z_K = f_K \circ f_{K-1} \circ \cdots \circ f_1(z_0)

其中 z0π(z)z_0 \sim \pi(z)

因为每一层都可逆,所以整体也可逆:

z0=f11f21fK1(x)z_0 = f_1^{-1} \circ f_2^{-1} \circ \cdots \circ f_K^{-1}(x)

这类“逐层流动”的可逆变换就叫做 flow


2. 对数似然分解#

对单层变换 zk=fk(zk1)z_k = f_k(z_{k-1}),变量变换公式给出:

logpk(zk)=logpk1(zk1)logdet(fkzk1)\log p_k(z_k) = \log p_{k-1}(z_{k-1}) - \log \left| \det\left( \frac{\partial f_k}{\partial z_{k-1}} \right) \right|

KK 层连起来,得到:

logpK(zK)=logp0(z0)k=1Klogdet(fkzk1)\log p_K(z_K) = \log p_0(z_0) - \sum_{k=1}^K \log \left| \det\left( \frac{\partial f_k}{\partial z_{k-1}} \right) \right|

如果把 p0p_0 取成简单先验 π(z)\pi(z),并记最终输出 zK=xz_K = x,那么:

logp(x)=logπ(z0)k=1Klogdet(fkzk1)\log p(x) = \log \pi(z_0) - \sum_{k=1}^K \log \left| \det\left( \frac{\partial f_k}{\partial z_{k-1}} \right) \right|

其中 z0z_0 由对 xx 做逆变换得到。

因此,flow 的训练目标通常就是最大化数据的对数似然:

maxθxDlogpθ(x)\max_\theta \sum_{x \in \mathcal D} \log p_\theta(x)

这和 GAN 的一个关键区别在于:flow 是显式似然模型,可以直接做最大似然训练。


四、可逆变换的设计要求#

Normalizing Flow 能不能训练得好,取决于每个子变换 fkf_k 的设计。理想的层要同时满足三点:

  1. 可逆
  2. 逆变换易于计算
  3. 雅可比行列式易于计算

否则虽然理论上可以写出变量变换公式,但计算成本会非常高。

下面是一些常见结构的复杂度对比:

类型逆复杂度行列式复杂度
全连接O(d3)O(d^3)O(d3)O(d^3)
对角O(d)O(d)O(d)O(d)
三角O(d2)O(d^2)O(d2)O(d^2)
块对角O(c3d)O(c^3 d)O(c3d)O(c^3 d)
LU 分解O(d2)O(d^2)O(d)O(d)
空间卷积O(dlogd)O(d \log d)O(d)O(d)
1×11 \times 1 卷积O(c3+c2d)O(c^3 + c^2 d)O(c3)O(c^3)

Normalizing Flow 的结构设计是在表达能力与可计算性之间做平衡。


五、耦合层(Coupling Layer)#

耦合层是 flow 中最经典的一类结构。它的思想很简单:把输入分成两部分,只变换其中一部分,另一部分作为条件控制参数。

1. 定义#

把输入拆成:

x=(xA,xB)x = (x^A, x^B)

定义变换:

f(x)=[xAf^(xBθ(xA))]f(x) = \begin{bmatrix} x^A \\ \hat f(x^B \mid \theta(x^A)) \end{bmatrix}

含义是:

  • xAx^A 原样保留
  • xBx^B 在条件 θ(xA)\theta(x^A) 控制下发生变换

这样设计的好处是,雅可比矩阵天然是块三角形式。

耦合层示意图


2. 雅可比与行列式#

它的雅可比矩阵为:

Jf=[I0f^xAJf^]J_f = \begin{bmatrix} I & 0 \\ \frac{\partial \hat f}{\partial x^A} & J_{\hat f} \end{bmatrix}

块三角矩阵的行列式等于对角块行列式之积,因此:

det(Jf)=det(Jf^)\det(J_f) = \det(J_{\hat f})

这个结果非常关键,因为它意味着:

  • 不需要计算完整雅可比矩阵的行列式
  • 只需要计算被变换那一部分的雅可比行列式

于是高维情况下也能高效训练。


3. 常见耦合变换#

i. 加性耦合(NICE, 2014)#

定义:

f^(xt)=x+t\hat f(x \mid t) = x + t

于是:

yA=xA,yB=xB+t(xA)y^A = x^A, \quad y^B = x^B + t(x^A)

它的优点是逆变换简单:

xB=yBt(yA)x^B = y^B - t(y^A)

但缺点也很明显:局部体积不变,因此:

logdetJ=0\log |\det J| = 0

表达能力相对有限。

ii. 仿射耦合(RealNVP, 2016)#

定义:

f^(xs,t)=sx+t\hat f(x \mid s, t) = s \circ x + t

更常见的写法是让网络输出 logs\log s,再指数化保证尺度非零:

yA=xA,yB=exp(α(xA))xB+t(xA)y^A = x^A, \quad y^B = \exp(\alpha(x^A)) \circ x^B + t(x^A)

逆变换为:

xB=(yBt(yA))exp(α(yA))x^B = \bigl(y^B - t(y^A)\bigr) \circ \exp\bigl(-\alpha(y^A)\bigr)

此时雅可比是对角的,因此:

logdetJ=iαi(xA)\log |\det J| = \sum_i \alpha_i(x^A)

这比加性耦合更灵活,也是现代 flow 中非常常见的基本模块。


4. 耦合层的局限#

单个耦合层并不会同时变换所有维度,因为总有一部分变量被原样保留。为了让所有通道都能逐步混合起来,通常需要在耦合层之间加入:

  • 维度置换
  • 通道重排
  • 可逆线性变换

Glow 中的可逆 1×11 \times 1 卷积就是为了解决这个问题。


六、可逆 1×11 \times 1 卷积#

1. 为什么需要它#

早期模型常用固定置换来打乱通道顺序,但固定置换表达能力有限。Glow 的做法是学习一个可逆的线性变换,让不同通道之间能够更充分地混合。

设输入特征图:

hRc×H×Wh \in \mathbb R^{c \times H \times W}

用一个权重矩阵:

WRc×cW \in \mathbb R^{c \times c}

对每个空间位置的通道向量做同一个线性变换,输出维度不变。

只要 WW 可逆,这个卷积就是可逆的。


2. 行列式计算#

对于每个空间位置,变换的雅可比都是 WW;而空间上一共有 H×WH \times W 个位置,因此整体对数行列式为:

logdet(dconv2D(h;W)dh)=HWlogdet(W)\log \left| \det\left( \frac{\mathrm d \operatorname{conv2D}(h; W)}{\mathrm dh} \right) \right| = H \cdot W \cdot \log |\det(W)|

这个公式非常漂亮:

  • 图像越大,重复次数越多
  • 真正需要计算的只是一个 c×cc \times c 矩阵的行列式

3. LU 分解重参数化#

为了进一步降低计算开销,可以把 WW 参数化为:

W=PL(U+diag(s))W = P L (U + \operatorname{diag}(s))

其中:

  • PP 是固定置换矩阵
  • LL 是对角线为 1 的下三角矩阵
  • UU 是严格上三角矩阵
  • diag(s)\operatorname{diag}(s) 提供对角项

因为三角矩阵的行列式等于对角线元素之积,所以:

logdet(W)=ilogsi\log |\det(W)| = \sum_i \log |s_i|

这样就把原本昂贵的矩阵行列式计算,转成了对角元素求和,复杂度降到 O(c)O(c)


七、离散数据与去量化(Dequantization)#

很多真实数据是离散的,例如 8-bit 图像像素只取 00255255 的整数值。但 flow 建模的是连续密度 p(x)p(x),如果直接拿连续模型去拟合离散点,会出现奇异问题:

  • 模型可能把概率质量集中在离散点附近
  • 对应的密度可以趋于无穷大
  • 最大似然训练因此变得不合理

解决办法是先给离散数据加上连续噪声,把它变成连续变量。设离散观测为 yy,加入噪声 uu 后得到:

x=y+ux = y + u

对应的离散概率可以写成:

pd(y)=pmodel(y+u)p(u)du1Kk=1Kpmodel(y+uk)p_d(y) = \int p_{\text{model}}(y + u) p(u)\,\mathrm du \approx \frac{1}{K}\sum_{k=1}^K p_{\text{model}}(y + u_k)

最常见的选择是均匀去量化:

uUniform([0,1)d)u \sim \operatorname{Uniform}([0,1)^d)

这样每个离散像素都会扩展成一个连续的小立方体,flow 就是在这些连续区域上建模密度。


八、Glow#

Glow 是一类非常有代表性的 flow 模型。

Glow: Generative Flow with Invertible 1x1 Convolutions

一个典型的 Glow step 通常包含:

  1. ActNorm
  2. 可逆 1×11 \times 1 卷积
  3. 仿射耦合层

其设计逻辑很清楚:

  • ActNorm 负责稳定每个通道的尺度
  • 可逆 1×11 \times 1 卷积负责通道混合
  • 仿射耦合层负责提供非线性、可逆、且易求行列式的变换

Glow 的意义:只要可逆层设计得足够好,flow 可以在图像生成上做到相当强的表达能力,同时保留精确似然。


九、连续时间 Normalizing Flow#

前面的 flow 都是离散层堆叠:

z0z1zKz_0 \to z_1 \to \cdots \to z_K

进一步可以把层数推向无限,把离散变换看成连续时间上的动力系统,这就得到 连续时间标准化流,也常称为 Neural ODE / Continuous Normalizing Flow (CNF)

1. ODE 形式#

设状态随时间演化:

dz(t)dt=f(z(t),t,θ)\frac{\mathrm dz(t)}{\mathrm dt} = f(z(t), t, \theta)

则从 t0t_0 演化到 t1t_1 的结果为:

z(t1)=z(t0)+t0t1f(z(t),t,θ)dtz(t_1) = z(t_0) + \int_{t_0}^{t_1} f(z(t), t, \theta)\,\mathrm dt

这表示样本不再经过若干离散层,而是沿着向量场 ff 连续流动。


2. 瞬时变量变换公式#

离散 flow 的难点之一是每层都要算一个行列式。连续时间情形下,这个问题转化为散度(trace)计算:

dlogp(z(t))dt=Tr(fz(t))=div(f)\frac{\mathrm d \log p(z(t))}{\mathrm dt} = -\operatorname{Tr}\left(\frac{\partial f}{\partial z(t)}\right) = -\operatorname{div}(f)

这是连续版的变量变换公式。它说明:

  • 若向量场在局部把体积“压缩”,密度会上升
  • 若向量场在局部把体积“膨胀”,密度会下降

与离散 flow 相比,这里不再需要直接计算 Jacobian determinant,而是计算 Jacobian 的迹,很多时候会更高效。


3. 训练目标#

对上式从 00 积分到 tt,得到:

logp(z(t))=logp(z(0))0tTr(fz(τ))dτ\log p(z(t)) = \log p(z(0)) - \int_0^t \operatorname{Tr}\left(\frac{\partial f}{\partial z(\tau)}\right)\,\mathrm d\tau

因此负对数似然可以写成:

L(z(t))=logp(z(t))=logp(z(0))+0tTr(fz(τ))dτL(z(t)) = -\log p(z(t)) = -\log p(z(0)) + \int_0^t \operatorname{Tr}\left(\frac{\partial f}{\partial z(\tau)}\right)\,\mathrm d\tau

这个形式和离散 flow 一脉相承,只不过有限求和变成了时间积分。


4. 伴随方法(Adjoint Method)#

连续系统训练时,不能像普通深层网络那样简单保存所有中间状态,因此通常使用伴随方法来反向传播。

定义伴随变量:

a(t)=Lz(t)a(t) = \frac{\partial L}{\partial z(t)}

则它满足反向 ODE:

da(t)dt=a(t)Tfz\frac{\mathrm da(t)}{\mathrm dt} = -a(t)^T \frac{\partial f}{\partial z}

参数梯度则为:

dLdθ=t1t0a(t)Tfθdt\frac{\mathrm dL}{\mathrm d\theta} = -\int_{t_1}^{t_0} a(t)^T \frac{\partial f}{\partial \theta}\,\mathrm dt

这意味着:

  • 前向先解一次 ODE 得到状态轨迹
  • 反向再解一个伴随方程得到梯度

连续时间 flow 因此在理论上很优雅,但训练效率和数值稳定性往往更依赖 ODE 求解器的实现。