引入
为什么传统的自编码器不能生成新的图像呐?
传统自编码器接收图像后,会将其压缩为 10-100 维的低维向量,再通过该向量重建原始图像。
这种低维向量所处的空间更紧凑、易解释,被称为潜在空间,即便原始图像可能包含上千个像素。
编辑
现在咱们有个训练好的解码器,它能根据图像的潜在表示把图重建出来。那要生成新图,很自
然就会想:从潜在空间里随便挑些点,再让解码器跑一遍不就行了?但这里有个问题 —— 大部分
时候这么做没用。因为潜在空间通常没什么规律,乱糟糟的,所以空间里大部分区域,解码器根本
没法把它变成有意义的图像。
编辑
另一种方法可能是取一种编码后的图像,然后采样其潜在表示附近的点。这应该会给我们原始图像
附近的图像。这样也不完全的正确,潜在空间的表示很差,以至于附近的点也不能对应原始图像附
近有意义的变化。如果我们采样接近原始图像的点,我们可能只会得到原始图像相同的重构。
编辑
原理介绍
那么解决方案是什么啊?理想情况下,我们希望有一个组织良好的潜在空间。在那里采样点会
导致连贯的新图像。下面让我们看看VAE是如何做到的。
下面快速说下贝叶斯的基本符号和属性。假设有个随机变量 X,它能取 0 到 10 之间的任何
值;如果某个事件的发生概率是按 X 的分布来的,就叫 “从 X 里抽样”。还有个叫 “概率密度函数”
的东西,用 p (x) 表示,它能告诉我们从 X 里抽到 0 到 10 中某个值的可能性有多大。另外,E (x)
代表这个分布的 “期望值”,简单说就是从 X 里抽很多次,这些结果的平均大概是多少,这个平均
值能通过概率密度函数的积分算出来。
编辑
现在看 X 和 Z,3D 图里的是它们的联合概率分布,意思就是每对可能的 X 和 Z 同时发生的概率。
当然 X 和 Z 各自也有自己的概率分布,叫边缘概率分布,分别用 P (X) 和 P (Z) 表示。有意思的
是,用联合分布能算出这两个边缘分布,这个过程叫边缘化,得对另一个变量积分才行 —— 比如
想知道 X 取某个值的概率,就把所有可能的 Z 值代入联合分布积分;想知道 Z 取某个值的概率,
就把所有可能的 X 值代入联合分布积分。还有个概念叫条件概率,其实就是从联合分布里 “切出一
片”,再用 Z 值抽样的概率做归一化处理。
编辑
我们的目标是从数据集(比如图像)的分布 P (x) 里生成新数据,但我们不知道 P (x) 具体长什么
样,只能拿到一些样本。所以我们引入了低维空间的潜在分布 P (Z),用 Z 向量捕捉数据核心特
征;为了连接 P (x) 和 P (Z),需要两个映射:后验分布(看图像 X 生成 Z 的概率)和似然分布
(看 Z 重构出 X 的概率)—— 理论上从后验抽 Z 再重构成 X,就能生成新数据,但问题是我们也
不知道 P (Z) 的具体形式,计算根本没法做。于是我们假设 P (Z) 是正态分布,这样就能算似然 P
(X|Z) 了,可还是缺后验 P (Z|X);这时候变分自编码器就用一个带可学习参数的高斯分布 q (z|x)
来近似真实后验,这个学习过程就是变分贝叶斯优化。
编辑
我们用自编码器从图像里估计两个参数,再让解码器根据抽样出的潜在变量 Z 重构图像。要实现
自编码器近似后验并重构图像,得先通过贝叶斯公式推导出训练目标 —— 这个目标由两部分组
成,第一部分是 “一致性项”,用来衡量用 Z 重构原图像 X 的效果,因为我们做了假设,它能简化
成 L2 损失(均方误差),算的时候只要用解码器重构图像,再对比重构图和原图的 L2 差距就
行;第二部分是 KL 散度,它能测近似后验和先验分布 P (Z) 的距离,而我们选的 P (Z) 是正态分
布,所以优化时能让近似后验也接近正态分布。总的来说,变分自编码器的训练目标 L 就是带正
则化的重构损失,既靠 L2 损失保证重构效果,又靠 KL 散度让潜在空间符合正态分布,最终确保
生成的样本和原始数据分布 p (x) 一致。
编辑
手动计算
网络结构
现在咱们从理论转到实际,自编码器是这么实现这些分布的:普通自编码器把输入压缩成潜在
空间的一个点,再解码回输入空间,但变分自编码器不一样 —— 它的编码器会把输入转换成一个
高斯分布的两个参数(均值和方差),用这个分布来代表输入,而不是一个点;之后从这个高斯分
布里随机抽点,让解码器把这些点转回输入空间,就能算 L 损失的各个部分,再反向传播优化。不
过抽样操作本身没法反向传播,这就需要重新参数化技巧:先从标准正态分布里抽个随机点,用编
码器输出的方差缩放它、用均值移动它,这样既相当于从后验分布抽样,又能让过程对参数可微
分,支持反向传播。典型的 VAE 流程和普通自编码器类似,先拿图像生成重构图,对比原图算重
构效果,但它还会输出后验分布的参数,再算这个后验和先验(标准正态分布)的 KL 散度,而两
个高斯分布的 KL 散度有现成的计算公式可以直接用。
编辑
手算模拟
假设我们有这样一个任务:
输入特征 D=4 (例如:身高、体重、年龄、收入)
潜在空间 L=2 (例如:抽象的“健康度”和“财富值”)
首先我们把输入展平,然后经过8个神经元的线性层,得到的(1*8)的输出,然后再经过一个激
活函数,得到(1*8)的输出。
编辑
分别通过两个2个神经元的先行层,得到(1*2)的均值参数和方差的对数参数。
编辑
通过重参数化得到潜在变量Z
编辑
潜在变量Z,通过8个神经元的线性层,Relu激
BCE = F.mse_loss(recon_x, x.view(-1, D_INPUT), reduction='mean') # 重构损失(MSE) KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) # KL散度损失 return BCE, KLD, BCE + KLD # 分别返回各损失
活函数,4个神经元的线性层,最终得到1*4的输出。
编辑
损失函数计算
编辑
代码实现
import os os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" # 放在所有库导入前 import torch import torch.nn as nn import torch.nn.functional as F import numpy as np import matplotlib.pyplot as plt from matplotlib.patches import Ellipse # 设置中文字体支持 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False # --- 设定参数 --- D_INPUT = 4 # 输入维度 H_HIDDEN = 8 # 隐藏层维度 L_LATENT = 2 # 潜在空间维度 # 假设的输入数据 (D=4) X_input = torch.tensor([4.0, 3.0, 1.0, 2.0], dtype=torch.float32).unsqueeze(0) print("原始输入形状:", X_input.shape) # 输出形状:[1, 4](批量大小=1,特征数=4) # --- VAE 模型定义(增加中间结果返回) --- class VAE(nn.Module): def __init__(self, D_in, H, L_lat): super(VAE, self).__init__() # 编码器层 self.fc1 = nn.Linear(D_in, H) # 输入→隐藏层 self.fc_mu = nn.Linear(H, L_lat) # 隐藏层→均值 self.fc_logvar = nn.Linear(H, L_lat) # 隐藏层→对数方差 # 解码器层 self.fc3 = nn.Linear(L_lat, H) # 潜在变量→隐藏层 self.fc4 = nn.Linear(H, D_in) # 隐藏层→输出 # 初始化权重(固定随机种子,保证结果可复现) nn.init.normal_(self.fc1.weight, mean=0.0, std=0.1) nn.init.constant_(self.fc1.bias, 0.0) nn.init.normal_(self.fc_mu.weight, mean=0.0, std=0.1) nn.init.constant_(self.fc_mu.bias, 0.0) nn.init.normal_(self.fc_logvar.weight, mean=0.0, std=0.1) nn.init.constant_(self.fc_logvar.bias, 0.0) nn.init.normal_(self.fc3.weight, mean=0.0, std=0.1) nn.init.constant_(self.fc3.bias, 0.0) nn.init.normal_(self.fc4.weight, mean=0.0, std=0.1) nn.init.constant_(self.fc4.bias, 0.0) def encode(self, x): # 编码器完整流程:输入→fc1→ReLU→输出均值和对数方差 h1 = self.fc1(x) # 第一层线性变换 h1_act = F.relu(h1) # ReLU激活 mu = self.fc_mu(h1_act) # 计算均值 logvar = self.fc_logvar(h1_act) # 计算对数方差 return h1, h1_act, mu, logvar # 返回中间结果 def reparameterize(self, mu, logvar): std = torch.exp(0.5 * logvar) # 标准差 = exp(0.5*log方差) eps = torch.randn_like(std) # 从标准正态分布采样噪声 z = mu + eps * std # 重参数化得到潜在变量 return std, eps, z def decode(self, z): # 解码器完整流程:潜在变量→fc3→ReLU→fc4→输出 h3 = self.fc3(z) # 第一层线性变换 h3_act = F.relu(h3) # ReLU激活 recon_x = self.fc4(h3_act) # 输出重构结果 return h3, h3_act, recon_x # 返回中间结果 def forward(self, x): # 完整前向传播,返回所有中间结果 h1, h1_act, mu, logvar = self.encode(x.view(-1, D_INPUT)) std, eps, z = self.reparameterize(mu, logvar) h3, h3_act, recon_x = self.decode(z) return { 'h1': h1, 'h1_act': h1_act, # 编码器中间结果 'mu': mu, 'logvar': logvar, 'std': std, 'eps': eps, 'z': z, # 潜在空间相关 'h3': h3, 'h3_act': h3_act, # 解码器中间结果 'recon_x': recon_x # 最终重构结果 } def vae_loss_function(recon_x, x, mu, logvar): BCE = F.mse_loss(recon_x, x.view(-1, D_INPUT), reduction='mean') # 重构损失(MSE) KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) # KL散度损失 return BCE, KLD, BCE + KLD # 分别返回各损失 # --- 主程序(详细输出每一层结果) --- if __name__ == "__main__": print("=" * 80) print("VAE 每一层详细计算过程") print("=" * 80) # 实例化模型 model = VAE(D_INPUT, H_HIDDEN, L_LATENT) # 前向传播(获取所有中间结果) with torch.no_grad(): # 关闭梯度计算,仅做推理 outputs = model(X_input) # 提取结果 h1 = outputs['h1'] h1_act = outputs['h1_act'] mu = outputs['mu'] logvar = outputs['logvar'] std = outputs['std'] eps = outputs['eps'] z = outputs['z'] h3 = outputs['h3'] h3_act = outputs['h3_act'] recon_x = outputs['recon_x'] # 计算损失 L_rec, L_kld, Total_Loss = vae_loss_function(recon_x, X_input, mu, logvar) # -------------------------- # 1. 输入数据 # -------------------------- print("\n【1. 输入层】") print(f"输入数据 X (形状: {X_input.shape}): {X_input.squeeze().numpy().round(4)}") # -------------------------- # 2. 编码器计算过程 # -------------------------- print("\n【2. 编码器】") print(f" a. 输入→fc1线性变换 (形状: {h1.shape}): {h1.squeeze().numpy().round(4)}") print(f" b. ReLU激活后 (形状: {h1_act.shape}): {h1_act.squeeze().numpy().round(4)}") print(f" c. 均值 μ (形状: {mu.shape}): {mu.squeeze().numpy().round(4)}") print(f" d. 对数方差 log(σ²) (形状: {logvar.shape}): {logvar.squeeze().numpy().round(4)}") # -------------------------- # 3. 重参数化过程 # -------------------------- print("\n【3. 重参数化】") print(f" a. 标准差 σ (形状: {std.shape}): {std.squeeze().numpy().round(4)}") print(f" b. 随机噪声 ε (形状: {eps.shape}): {eps.squeeze().numpy().round(4)}") print(f" c. 潜在变量 z = μ + ε×σ (形状: {z.shape}): {z.squeeze().numpy().round(4)}") # -------------------------- # 4. 解码器计算过程 # -------------------------- print("\n【4. 解码器】") print(f" a. z→fc3线性变换 (形状: {h3.shape}): {h3.squeeze().numpy().round(4)}") print(f" b. ReLU激活后 (形状: {h3_act.shape}): {h3_act.squeeze().numpy().round(4)}") print(f" c. 重构结果 X̂ (形状: {recon_x.shape}): {recon_x.squeeze().numpy().round(4)}") # -------------------------- # 5. 损失计算 # -------------------------- print("\n【5. 损失计算】") print(f" a. 重构损失 (MSE): {L_rec.item():.6f}") print(f" b. KL散度损失: {L_kld.item():.6f}") print(f" c. 总损失 (ELBO): {Total_Loss.item():.6f}") print("\n" + "=" * 80)
编辑
可视化
import os os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" # 放在所有库导入前 import torch import torch.nn as nn import torch.nn.functional as F import numpy as np import matplotlib.pyplot as plt from matplotlib.patches import Ellipse # 设置中文字体支持 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False # --- 设定参数 --- D_INPUT = 4 # 输入维度 H_HIDDEN = 8 # 隐藏层维度 L_LATENT = 2 # 潜在空间维度 # 假设的输入数据 (D=4) X_input = torch.tensor([4.0, 3.0, 1.0, 2.0], dtype=torch.float32).unsqueeze(0) # --- VAE 模型定义 --- class VAE(nn.Module): def __init__(self, D_in, H, L_lat): super(VAE, self).__init__() self.fc1 = nn.Linear(D_in, H) self.fc_mu = nn.Linear(H, L_lat) self.fc_logvar = nn.Linear(H, L_lat) self.fc3 = nn.Linear(L_lat, H) self.fc4 = nn.Linear(H, D_in) def encode(self, x): h1 = F.relu(self.fc1(x)) return self.fc_mu(h1), self.fc_logvar(h1) def reparameterize(self, mu, logvar): std = torch.exp(0.5 * logvar) eps = torch.randn_like(std) return mu + eps * std def decode(self, z): h3 = F.relu(self.fc3(z)) return self.fc4(h3) def forward(self, x): mu, logvar = self.encode(x.view(-1, D_INPUT)) z = self.reparameterize(mu, logvar) return self.decode(z), mu, logvar, z def vae_loss_function(recon_x, x, mu, logvar): BCE = F.mse_loss(recon_x, x.view(-1, D_INPUT), reduction='mean') KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) return BCE, KLD, BCE + KLD # --- 潜在空间可视化函数 --- def visualize_latent_space(mu, logvar, z_latent, num_samples=200): """可视化VAE的潜在空间分布""" fig, axes = plt.subplots(1, 2, figsize=(16, 7)) # 计算标准差 std = torch.exp(0.5 * logvar).squeeze().numpy() mu_np = mu.squeeze().numpy() z_np = z_latent.squeeze().numpy() # 生成多个采样点 samples = [] for _ in range(num_samples): eps = np.random.randn(L_LATENT) z_sample = mu_np + std * eps samples.append(z_sample) samples = np.array(samples) # 左图:VAE潜在空间分布 axes[0].scatter(samples[:, 0], samples[:, 1], alpha=0.4, s=40, c='lightblue', edgecolors='blue', linewidth=0.5, label='采样点') axes[0].scatter(mu_np[0], mu_np[1], c='red', s=300, marker='*', edgecolors='darkred', linewidths=2, label='均值 μ', zorder=10) axes[0].scatter(z_np[0], z_np[1], c='purple', s=200, marker='D', edgecolors='darkviolet', linewidths=2, label='当前采样 z', zorder=10) # 绘制1σ和2σ椭圆 ellipse_1sigma = Ellipse((mu_np[0], mu_np[1]), width=2*std[0], height=2*std[1], fill=False, edgecolor='red', linewidth=2, linestyle='--', label='1σ 范围', alpha=0.8) ellipse_2sigma = Ellipse((mu_np[0], mu_np[1]), width=4*std[0], height=4*std[1], fill=False, edgecolor='orange', linewidth=1.5, linestyle=':', label='2σ 范围', alpha=0.6) axes[0].add_patch(ellipse_1sigma) axes[0].add_patch(ellipse_2sigma) axes[0].set_xlabel('z₁ (第一潜在维度)', fontsize=13, fontweight='bold') axes[0].set_ylabel('z₂ (第二潜在维度)', fontsize=13, fontweight='bold') axes[0].set_title('VAE 潜在空间分布', fontsize=15, fontweight='bold', pad=15) axes[0].legend(loc='upper right', fontsize=11, framealpha=0.9) axes[0].grid(True, alpha=0.3, linestyle='--') axes[0].axhline(y=0, color='k', linewidth=0.8, alpha=0.3) axes[0].axvline(x=0, color='k', linewidth=0.8, alpha=0.3) # 添加统计信息 info_text = f'μ = [{mu_np[0]:.3f}, {mu_np[1]:.3f}]\nσ = [{std[0]:.3f}, {std[1]:.3f}]' axes[0].text(0.02, 0.98, info_text, transform=axes[0].transAxes, fontsize=10, verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) # 右图:与标准正态分布对比 standard_samples = np.random.randn(num_samples, L_LATENT) axes[1].scatter(standard_samples[:, 0], standard_samples[:, 1], alpha=0.3, s=30, c='lightgreen', edgecolors='green', linewidth=0.5, label='标准正态 N(0,I)') axes[1].scatter(samples[:, 0], samples[:, 1], alpha=0.4, s=40, c='lightblue', edgecolors='blue', linewidth=0.5, label='VAE 潜在分布') axes[1].scatter(mu_np[0], mu_np[1], c='red', s=300, marker='*', edgecolors='darkred', linewidths=2, label='VAE 均值 μ', zorder=10) axes[1].scatter(0, 0, c='green', s=300, marker='*', edgecolors='darkgreen', linewidths=2, label='标准正态均值', zorder=10) axes[1].set_xlabel('z₁ (第一潜在维度)', fontsize=13, fontweight='bold') axes[1].set_ylabel('z₂ (第二潜在维度)', fontsize=13, fontweight='bold') axes[1].set_title('潜在分布 vs 标准正态分布', fontsize=15, fontweight='bold', pad=15) axes[1].legend(loc='upper right', fontsize=11, framealpha=0.9) axes[1].grid(True, alpha=0.3, linestyle='--') axes[1].axhline(y=0, color='k', linewidth=0.8, alpha=0.3) axes[1].axvline(x=0, color='k', linewidth=0.8, alpha=0.3) # 添加说明 explanation = 'KL散度损失使VAE的潜在分布逐渐接近标准正态分布N(0,I)这样可以确保潜在空间的连续性和可插值性' axes[1].text(0.5, -0.15, explanation, transform=axes[1].transAxes, fontsize=10, ha='center', style='italic', bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.7)) plt.suptitle('VAE 潜在空间可视化 - 理解编码器的输出分布', fontsize=17, fontweight='bold', y=0.98) plt.tight_layout() return fig # --- 主程序 --- if __name__ == "__main__": print("=" * 60) print("VAE 潜在空间可视化") print("=" * 60) # 实例化模型 model = VAE(D_INPUT, H_HIDDEN, L_LATENT) # 前向传播 with torch.no_grad(): recon_batch, mu, logvar, z_latent = model(X_input) L_rec, L_kld, Total_Loss = vae_loss_function(recon_batch, X_input, mu, logvar) # 打印结果 print(f"\n输入 X (4D): {X_input.squeeze().numpy().round(3)}") print(f"均值 μ (2D): {mu.squeeze().numpy().round(3)}") print(f"Log方差 log(σ²) (2D): {logvar.squeeze().numpy().round(3)}") print(f"标准差 σ (2D): {torch.exp(0.5 * logvar).squeeze().numpy().round(3)}") print(f"采样的潜在编码 z (2D): {z_latent.squeeze().numpy().round(3)}") print(f"重建 X̂ (4D): {recon_batch.squeeze().numpy().round(3)}") print("-" * 60) print(f"重构损失 (MSE): {L_rec.item():.4f}") print(f"KL 散度损失: {L_kld.item():.4f}") print(f"总损失 (ELBO): {Total_Loss.item():.4f}") print("=" * 60) # 创建潜在空间可视化 print("\n正在生成潜在空间可视化...") fig = visualize_latent_space(mu, logvar, z_latent, num_samples=200) plt.savefig('vae_latent_space.png', dpi=150, bbox_inches='tight') print("✓ 可视化图表已保存为 'vae_latent_space.png'") plt.show() print("\n可视化完成!")
编辑
编辑