查看全课大纲(第 89 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

VAE

变分自编码器(VAE)

小象实战讲义 · 基于深度学习的计算机视觉

在进入 VAE 之前,我们先回顾经典的自编码器:它能够学习图像的压缩表示并重建图像,但无法用于生成全新的图像。本节我们将学习变分自编码器(Variational Autoencoder, VAE),它通过在编码过程中引入概率分布约束,将离散的潜在空间变为连续且平滑的,从而能够从随机噪声中生成新的、可理解的图像。学完本节,你将理解VAE的核心思想、其损失函数的构成,以及“重参数化”这一关键训练技巧。

💡 核心导读

  • 从AE到VAE的跨越:经典自编码器的潜在空间是离散且不连续的,无法用于生成。VAE通过强制潜在向量服从高斯分布,解决了这一问题。
  • VAE的核心机制:编码器不再输出一个确定的向量,而是输出一个高斯分布的参数(均值和方差),解码器则从该分布中采样进行重建。
  • 损失函数的两部分:VAE的损失由重构损失(衡量重建质量)和KL散度损失(衡量潜在分布与标准正态分布的相似度)共同构成。
  • 重参数化技巧:为了使采样过程可导,VAE采用“重参数化”技术,将随机采样转化为确定性计算,使得误差能够反向传播。

经典自编码器的局限

我们先快速回顾一下经典自编码器(Autoencoder, AE)的结构。它由编码器(Encoder)和解码器(Decoder)两部分组成。

输入图像 X --> [编码器:卷积网络] --> 潜在向量 Z (低维) --> [解码器:反卷积网络] --> 重建图像 X'

训练目标很简单:最小化原始输入图像 X 与重建输出图像 X' 之间的差异(如均方误差MSE)。训练完成后,我们可以保存某张图像的编码向量 Z,之后随时将其输入解码器,就能重建出该图像。

然而,一个根本性的问题出现了:我们能否凭空创造一个潜在向量 Z,输入解码器后生成一张全新的、合理的图像?

遗憾的是,对于经典AE,答案通常是否定的。原因在于,经典AE学习到的潜在空间(Latent Space)通常是不连续且未被充分覆盖的。编码器只会为训练集中出现过的图像生成对应的 Z 值。潜在空间中大量区域是“空白”的,如果将一个随机向量放入这些空白区域,解码器输出的往往是无意义的噪声。

import numpy as np

# 模拟经典AE的潜在空间:只有少数离散点有定义
def classic_ae_decoder(z):
    # 假设潜在空间只有两个有效点对应猫和狗
    valid_points = {0: 'cat_image', 1: 'dog_image'}
    # 如果z接近有效点,则返回对应图像标签;否则返回噪声
    if np.abs(z - 0) < 0.1:
        return valid_points[0]
    elif np.abs(z - 1) < 0.1:
        return valid_points[1]
    else:
        return 'noise_image'

# 测试
test_vectors = [-0.5, 0.05, 0.5, 1.05, 2.0]
for zv in test_vectors:
    print(f"潜在向量 z={zv:.2f} -> 输出: {classic_ae_decoder(zv)}")
潜在向量 z=-0.50 -> 输出: noise_image
潜在向量 z=0.05 -> 输出: cat_image
潜在向量 z=0.50 -> 输出: noise_image
潜在向量 z=1.05 -> 输出: dog_image
潜在向量 z=2.00 -> 输出: noise_image

可以看到,只有 z 在0或1附近时,输出才有意义。这正是经典AE无法用于生成的原因。

VAE的基本思想:引入分布约束

VAE的核心思想非常直观:给编码网络加上一个约束,强制它生成的潜在向量 Z 的分布服从标准正态分布(单位高斯分布)。

具体来说,VAE的编码器不再直接输出一个潜在向量 Z,而是输出定义了一个概率分布的两个参数:

  • 均值向量 μ
  • 对数方差向量 log(σ²)(通常输出对数方差以保证数值稳定性)

我们假设潜在变量 Z 服从以 μ 为均值、σ² 为方差的对角高斯分布,即 Z ~ N(μ, σ²I),并且我们希望这个分布尽量接近标准正态分布 N(0, I)。

这样做的好处是什么?

  1. 连续且平滑的潜在空间:由于整个潜在空间都被一个连续的概率分布所填充,从该分布中任意采样一个点 Z,解码器都能生成一张有意义的图像。
  2. 隐式特征解耦:理想情况下,潜在空间的不同维度可能对应图像的不同语义特征(如笑容程度、肤色、发型等),并且这些特征是连续变化的。这使得我们可以在潜在空间中进行有意义的插值。
import numpy as np

# 模拟VAE的编码器:输出分布参数
np.random.seed(42) # 固定随机种子,保证输出确定
def vae_encoder(x):
    # 假设输入x经过网络,得到分布的参数
    mu = np.array([0.2])  # 均值
    log_var = np.array([-1.0]) # 对数方差
    return mu, log_var

# 从参数定义的分布中采样得到潜在向量z
def sample_z(mu, log_var):
    sigma = np.exp(0.5 * log_var) # 计算标准差
    eps = np.random.randn(*mu.shape) # 从标准正态分布采样噪声
    z = mu + eps * sigma # 重参数化技巧
    return z

# 模拟VAE的解码器
def vae_decoder(z):
    # 解码器将z映射回数据空间
    # 这里简化为:z的值越大,生成的“笑容程度”越高
    smile_intensity = 1 / (1 + np.exp(-5 * (z - 0.5))) # sigmoid函数模拟
    return f"生成图像,笑容程度: {smile_intensity[0]:.2f}"

# VAE前向过程模拟
mu, log_var = vae_encoder("输入图像")
print(f"编码器输出 -> 均值 μ: {mu[0]:.3f}, 对数方差 log_var: {log_var[0]:.3f}")

# 采样两次,得到不同的z
for i in range(2):
    z_sampled = sample_z(mu, log_var)
    output = vae_decoder(z_sampled)
    print(f"采样 {i+1}: z = {z_sampled[0]:.3f}, {output}")
编码器输出 -> 均值 μ: 0.200, 对数方差 log_var: -1.000
采样 1: z = 0.501, 生成图像,笑容程度: 0.50
采样 2: z = 0.116, 生成图像,笑容程度: 0.13

注意,对于同一张输入图像,由于采样了不同的噪声 ε,我们得到了不同的潜在向量 Z,从而解码出略有差异的图像。这体现了VAE的概率生成特性。

VAE的损失函数

VAE的训练目标是双重的:

  1. 重建图像要像(Reconstruction Loss)。
  2. 学到的潜在分布要像标准正态分布(Regularization Loss)。

因此,其损失函数是这两部分的加权和:

总损失 = 重构损失 + KL散度损失

1. 重构损失

衡量解码器重建的图像 X' 与原始图像 X 的差异。对于图像数据,常用均方误差(MSE) 或二元交叉熵(BCE)。其作用是迫使编码器保留输入数据的关键信息。

L_{recon} = MSE(X, X') = ||X - X'||^2

2. KL散度损失

衡量编码器产生的潜在分布 q(Z|X) = N(μ, σ²) 与先验分布 p(Z) = N(0, I) 之间的差异。使用KL散度(Kullback-Leibler Divergence)进行度量。其作用是规范潜在空间,使其连续、平滑且易于采样。

对于对角高斯分布,其KL散度有一个漂亮的解析解:

L_{KL} = D_{KL}[q(Z|X) || p(Z)] = -0.5 * Σ (1 + log(σ²) - μ² - σ²)

其中求和是对潜在向量的每个维度进行的。

import numpy as np

# 计算VAE损失函数的示例
def compute_vae_loss(x_original, x_reconstructed, mu, log_var):
    """
    计算VAE的总损失。
    参数:
        x_original: 原始图像数据 (展平向量)
        x_reconstructed: 重建图像数据 (展平向量)
        mu: 均值向量
        log_var: 对数方差向量
    """
    # 1. 重构损失 (MSE)
    mse_loss = np.mean((x_original - x_reconstructed) ** 2)
    
    # 2. KL散度损失 (解析解)
    # KL = -0.5 * sum(1 + log(sigma^2) - mu^2 - sigma^2)
    kl_loss = -0.5 * np.sum(1 + log_var - np.power(mu, 2) - np.exp(log_var))
    # 通常对KL损失取平均,使其与重构损失尺度相近
    kl_loss = kl_loss / x_original.shape[0]
    
    # 3. 总损失
    total_loss = mse_loss + kl_loss
    return mse_loss, kl_loss, total_loss

# 模拟数据
np.random.seed(42)
# 假设一个4维的潜在空间,批量大小为2
batch_size = 2
latent_dim = 4
# 原始数据 (例如,2张8x8灰度图展平为64维向量)
x_orig = np.random.randn(batch_size, 64)
# 重建数据
x_recon = x_orig + np.random.randn(batch_size, 64) * 0.1  # 加一点噪声模拟重建误差
# 编码器输出的分布参数
mu = np.random.randn(batch_size, latent_dim) * 0.5
log_var = np.random.randn(batch_size, latent_dim) * 0.3 - 1  # 使方差较小

# 计算损失
recon_loss, kl_loss, total_loss = compute_vae_loss(x_orig, x_recon, mu, log_var)
print(f"重构损失 (MSE): {recon_loss:.6f}")
print(f"KL散度损失: {kl_loss:.6f}")
print(f"VAE总损失: {total_loss:.6f}")
重构损失 (MSE): 0.010036
KL散度损失: 1.813922
VAE总损失: 1.823958

重参数化技巧

训练VAE时,我们需要从分布 N(μ, σ²) 中采样 Z,然后将 Z 送入解码器。然而,“采样”是一个随机操作,不可导,这会阻断误差从解码器向编码器参数(μ, σ)的反向传播。

重参数化技巧(Reparameterization Trick) 完美地解决了这个问题。其核心思想是将随机性从计算图中分离出去。

具体做法:

  1. 从标准正态分布中采样一个噪声变量:ε ~ N(0, I)
  2. 通过一个确定性变换得到 Z:Z = μ + ε ⊙ σ (其中 ⊙ 表示逐元素相乘)

这样,Z 依然是随机的(因为 ε 是随机的),但随机性来源于一个与模型参数无关的输入 ε。计算图变成了 μ, σ, ε -> Z,而对 μ 和 σ 的求导路径是确定且可导的。

import numpy as np

# 对比直接采样与重参数化
np.random.seed(42)
latent_dim = 3
mu = np.array([0.2, -0.1, 0.5])
log_var = np.array([-1.0, 0.0, -2.0]) # 对应方差为 exp(log_var)
sigma = np.exp(0.5 * log_var)

print(f"均值 μ: {mu}")
print(f"标准差 σ: {sigma}")

# 方法1: 直接采样(在自动微分框架里,采样节点没有梯度)
z_direct = np.random.randn(latent_dim) * sigma + mu
print(f"直接采样 Z: {z_direct}")

# 方法2: 重参数化 (可导)
# 两行数值形式相同,区别在计算图:重参数化把随机性移到常数 ε 上,Z 对 μ、σ 可导
epsilon = np.random.randn(latent_dim) # 从标准正态采样,这个节点在反向传播时被视为常数输入
z_reparam = mu + epsilon * sigma # 确定性计算
print(f"重参数化 Z: {z_reparam}")
print(f"使用的噪声 ε: {epsilon}")
# 注意:z_direct 和 z_reparam 都是来自同一分布 N(μ, σ²) 的有效样本,只是生成方式不同。
均值 μ: [ 0.2 -0.1  0.5]
标准差 σ: [0.60653066 1.         0.36787944]
直接采样 Z: [ 0.50127236 -0.2382643   0.7382713 ]
重参数化 Z: [ 1.1237643  -0.33415337  0.41386583]
使用的噪声 ε: [ 1.52302986 -0.23415337 -0.23413696]

在反向传播时,损失 L 对 μ 和 σ 的梯度可以顺利计算:

  • ∂L/∂μ = ∂L/∂z * ∂z/∂μ = ∂L/∂z * 1
  • ∂L/∂σ = ∂L/∂z * ∂z/∂σ = ∂L/∂z * ε

VAE的训练流程与效果

结合以上所有部分,VAE完整的训练流程如下所示:

输入 X
    |
    v
[编码器网络]
    |          输出两个向量:
    |-----> 均值 μ
    |-----> 对数方差 log(σ²)
    |
    |          重参数化:
    |-----> 采样 ε ~ N(0, I)
    |-----> 计算 Z = μ + ε ⊙ σ
    |
    v
[解码器网络]
    |
    v
重建输出 X'
    |
    v
计算损失 L = L_recon(X, X') + L_KL(N(μ,σ²) || N(0,I))
    |
    v
反向传播,更新编码器和解码器参数

VAE生成的图像质量在早期模型中表现尚可,但存在一个普遍问题:图像往往比较模糊。这主要是因为重构损失使用了MSE。MSE惩罚的是像素级的差异,它倾向于让模型输出所有可能图像的平均值,从而导致细节丢失,图像变得平滑模糊。

尽管如此,VAE为深度生成模型奠定了重要基础。它明确地将概率图模型与神经网络结合,引入了潜在变量和变分推断的思想。其“编码-解码”的框架和重参数化技巧也被后续许多模型所借鉴。

📝 动手练一练

  1. 概念辨析:经典自编码器(AE)与变分自编码器(VAE)在潜在向量的处理上有何根本区别?这种区别导致了它们在什么能力上的差异?
👉 点击查看参考答案

根本区别:

  • 经典AE的编码器直接将输入映射为一个确定的、离散的潜在向量 Z。
  • VAE的编码器将输入映射为一个概率分布的参数(均值 μ 和方差 σ²),潜在向量 Z 是从这个分布中采样得到的随机变量。

能力差异:

  • 由于潜在空间离散且未被充分覆盖,经典AE无法可靠地生成新图像,随机向量输入解码器常产生噪声。
  • VAE通过强制潜在分布连续且平滑(接近标准正态),使得从该分布中任意采样的点,解码后都能生成一张有意义的图像,从而具备了生成新样本的能力。
  1. 手算理解:假设一个VAE模型的潜在空间只有1维。对于某张输入图像,编码器输出 μ = 0.5, log(σ²) = -2。请计算: a) 方差 σ² 和标准差 σ。 b) 该分布与标准正态分布 N(0,1) 之间的KL散度 L_KL。
👉 点击查看参考答案

a) 计算方差和标准差: log(σ²) = -2 => σ² = exp(-2) ≈ 0.1353 σ = sqrt(σ²) = exp(-1) ≈ 0.3679

b) 计算KL散度(使用单维公式): L_KL = -0.5 * (1 + log(σ²) - μ² - σ²) = -0.5 * (1 + (-2) - (0.5)² - 0.1353) = -0.5 * (1 - 2 - 0.25 - 0.1353) = -0.5 * (-1.3853) ≈ 0.6927

因此,KL散度损失约为 0.6927。这个正值表示学到的分布 N(0.5, 0.1353) 与标准正态分布 N(0,1) 之间存在差异。

本章小结

本节我们深入探讨了变分自编码器(VAE),它是连接自编码器与生成式模型的重要桥梁。VAE通过为潜在变量引入概率分布约束,构建了一个连续、平滑的潜在空间,从而实现了从随机噪声生成新图像的能力。其巧妙的两部分损失函数(重构损失+KL散度损失)和重参数化技巧,是模型得以成功训练的关键。

今天,VAE 仍是生成建模与表示学习的基础组件,后续发展出 β-VAE、VQ-VAE 等变体;其重参数化与 ELBO(证据下界)思想也广泛用于变分推断、扩散模型与多模态表示学习。

📋 行动清单

  • 理解VAE的动机:明确经典AE在生成任务上的缺陷,以及VAE通过分布约束如何解决该问题。
  • 掌握VAE损失函数:能写出并解释重构损失和KL散度损失的具体形式及其作用。
  • 应用重参数化技巧:理解为什么需要重参数化,并能用公式 Z = μ + ε ⊙ σ 表达采样过程。

—— 小象教研组