This framework can yield specific training algorithms for many kinds of model and optimization
algorithm. In this article, we explore the special case when the generative model generates samples
by passing random noise through a multilayer perceptron, and the discriminative model is also a
multilayer perceptron. We refer to this special case as adversarial nets.该框架能够为多种模型和优化算法生成特定的训练算法。在本文中,我们探讨了生成模型通过多层感知机传递随机噪声来生成样本,而判别模型也是多层感知机的这种特殊情况。我们将这种特殊情况称为对抗网络。
GANs 由两个核心部分组成:
这两个网络是对抗训练的:生成器努力“骗过”判别器,生成更真实的数据;而判别器努力提高自己的分辨能力。这种对抗过程最终让生成器学会生成非常逼真的图像。
以生成 MNIST 数据集中的手写数字为例,逐步实现
首先,我们需要准备训练数据。MNIST 数据集包含 6 万张 28x28 像素的灰度手写数字图像。我们会用这些真实图像来训练判别器,并作为生成器的学习目标。
生成器的任务是从随机噪声生成图像。假设输入是一个 100 维的随机噪声向量,输出是一个 28x28 的图像(展开为 784 维向量)。一个简单的生成器结构可能是:
判别器的任务是判断图像的真假。输入是 28x28 的图像(展开为 784 维向量),输出是一个概率值。一个简单的判别器结构可能是:
GANs 的训练是一个博弈过程,包含以下步骤:
这个过程不断重复,直到生成器生成的图像足够逼真。
下面是用 PyTorch 实现的一个简单 GAN 示例。
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
# 设置超参数
batch_size = 64 # 每批次处理的图像数量
lr = 0.0002 # 学习率
num_epochs = 100 # 训练轮数
noise_dim = 100 # 噪声向量的维度
# 数据加载和预处理
transform = transforms.Compose([
transforms.ToTensor(), # 转换为张量
transforms.Normalize((0.5,), (0.5,)) # 标准化到 [-1, 1]
])
mnist = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
data_loader = torch.utils.data.DataLoader(mnist, batch_size=batch_size, shuffle=True)
# 定义生成器
class Generator(nn.Module):
def __init__(self):
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, 784),
nn.Tanh() # 输出范围 [-1, 1]
)
def forward(self, z):
return self.model(z)
# 定义判别器
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(
nn.Linear(784, 512),
nn.LeakyReLU(0.2), # Leaky ReLU 防止梯度消失
nn.Linear(512, 256),
nn.LeakyReLU(0.2),
nn.Linear(256, 1),
nn.Sigmoid() # 输出概率 [0, 1]
)
def forward(self, img):
img_flat = img.view(img.size(0), -1) # 将图像展平为 784 维向量
return self.model(img_flat)
# 实例化模型
generator = Generator()
discriminator = Discriminator()
# 定义损失函数和优化器
criterion = nn.BCELoss() # 二元交叉熵损失
optimizer_g = optim.Adam(generator.parameters(), lr=lr)
optimizer_d = optim.Adam(discriminator.parameters(), lr=lr)
# 训练循环
for epoch in range(num_epochs):
for i, (real_imgs, _) in enumerate(data_loader):
batch_size = real_imgs.size(0)
# 训练判别器
optimizer_d.zero_grad()
real_labels = torch.ones(batch_size, 1) # 真实图像标签为 1
fake_labels = torch.zeros(batch_size, 1) # 假图像标签为 0
# 用真实图像训练判别器
real_output = discriminator(real_imgs)
d_loss_real = criterion(real_output, real_labels)
# 用生成图像训练判别器
z = torch.randn(batch_size, noise_dim) # 随机噪声
fake_imgs = generator(z)
fake_output = discriminator(fake_imgs.detach()) # detach 防止梯度传回生成器
d_loss_fake = criterion(fake_output, fake_labels)
# 总判别器损失
d_loss = d_loss_real + d_loss_fake
d_loss.backward()
optimizer_d.step()
# 训练生成器
optimizer_g.zero_grad()
z = torch.randn(batch_size, noise_dim)
fake_imgs = generator(z)
fake_output = discriminator(fake_imgs)
g_loss = criterion(fake_output, real_labels) # 目标是让判别器认为是真的
g_loss.backward()
optimizer_g.step()
# 每轮打印损失
print(f'Epoch [{epoch+1}/{num_epochs}], d_loss: {d_loss.item():.4f}, g_loss: {g_loss.item():.4f}')
数据加载:
torchvision
加载 MNIST 数据集。transforms.Normalize((0.5,), (0.5,))
将像素值从 [0, 1] 标准化到 [-1, 1],与生成器的 Tanh 输出一致。生成器:
Tanh
激活函数,确保输出范围与数据匹配。判别器:
LeakyReLU
防止梯度消失,Sigmoid
输出概率。训练过程:
Adam
优化器和二元交叉熵损失(BCELoss
)。输出:
d_loss
)和生成器损失(g_loss
),观察训练进展。通过这个例子,可以看到 GANs 的具体实现其实是将生成器和判别器的对抗思想转化为代码。生成器从噪声生成图像,判别器判断真假,两者交替训练,最终生成器能生成逼真的手写数字。如果运行这段代码,经过足够多的轮次(比如 100 轮),会发现生成的图像越来越接近真实的 MNIST 数字。
附:论文中提到了马尔可夫链(Markov Chain)和近似推理网络(Unrolled Approximate Inference Networks),它们是什么?
马尔可夫链是一种随机过程,用来描述系统在不同状态之间转换的规律。它的核心特点是无记忆性,也就是说,未来的状态只依赖于当前状态,而与更早的状态历史无关。这种性质也被称为“马尔可夫性质”。
想象一个简单的天气预测场景:
在这个例子中,天气的状态(晴天或雨天)形成了一个马尔可夫链。
在生成模型中,马尔可夫链常被用来从复杂的概率分布中抽取样本。例如,马尔可夫链蒙特卡罗(MCMC)方法利用马尔可夫链逐步调整样本,使其逐渐逼近目标分布(比如真实图像的分布)。不过,这种方法需要运行很多步才能稳定,计算成本较高,尤其是在高维数据(如图像)上,收敛速度较慢。
下面是一个简单的Python代码,模拟基于马尔可夫链的天气变化:
import numpy as np
# 状态转移矩阵
# 行是当前状态,列是下一状态
P = np.array([[0.7, 0.3], # 晴天 -> 晴天: 0.7, 晴天 -> 雨天: 0.3
[0.5, 0.5]]) # 雨天 -> 晴天: 0.5, 雨天 -> 雨天: 0.5
# 初始状态:0 = 晴天
current_state = 0
# 模拟 10 天的天气
for day in range(1, 11):
next_state = np.random.choice([0, 1], p=P[current_state])
print(f"第 {day} 天: {'晴天' if next_state == 0 else '雨天'}")
current_state = next_state
输出示例(结果随机):
第 1 天: 晴天
第 2 天: 晴天
第 3 天: 雨天
第 4 天: 晴天
...
代码解释:
P
是状态转移矩阵,定义了从当前状态到下一状态的概率。np.random.choice
根据概率随机选择下一状态。在生成模型中,例如变分自编码器(VAE),我们需要推断隐藏变量(也叫潜在变量)的分布。推理网络是一个神经网络,用来近似这个后验分布。简单来说,给定观测数据(比如一张图片),推理网络预测潜在变量的可能分布(比如图片的特征)。
“展开的”推理网络指的是在训练或推理过程中,网络通过多步迭代来更精确地逼近真实的后验分布,而不是只进行一次预测。这种方法类似于反复优化答案,每次都比上一次更接近正确结果。
假设你在猜一个谜语:
在VAE中,普通的推理网络通过单次前向传播预测潜在变量的分布。而展开的推理网络会进行多步优化(比如通过梯度下降或更复杂的网络结构),提高对后验分布的近似精度。这样可以生成更高质量的样本,但也会增加计算复杂度和训练时间。
下面是一个简单的VAE推理网络实现(基于PyTorch),并示意如何“展开”它:
import torch
import torch.nn as nn
# 定义推理网络(编码器)
class InferenceNetwork(nn.Module):
def __init__(self, input_dim, hidden_dim, latent_dim):
super(InferenceNetwork, self).__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc2_mean = nn.Linear(hidden_dim, latent_dim)
self.fc2_logvar = nn.Linear(hidden_dim, latent_dim)
def forward(self, x):
h = torch.relu(self.fc1(x))
mean = self.fc2_mean(h)
logvar = self.fc2_logvar(h)
return mean, logvar
# 定义生成网络(解码器)
generative_net = nn.Sequential(
nn.Linear(10, 256),
nn.ReLU(),
nn.Linear(256, 784),
nn.Sigmoid()
)
# 简单的VAE模型
class VAE(nn.Module):
def __init__(self, inference_net, generative_net):
super(VAE, self).__init__()
self.inference_net = inference_net
self.generative_net = generative_net
def forward(self, x):
mean, logvar = self.inference_net(x)
z = self.reparameterize(mean, logvar)
recon_x = self.generative_net(z)
return recon_x, mean, logvar
def reparameterize(self, mean, logvar):
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mean + eps * std
# 实例化模型
inference_net = InferenceNetwork(input_dim=784, hidden_dim=256, latent_dim=10)
vae = VAE(inference_net, generative_net)
代码解释:
InferenceNetwork
是一个神经网络,输入是数据(比如展平的28×28图像,维度为784),输出是潜在变量的均值和方差。reparameterize
是VAE中的重参数化技巧,用于从分布中采样潜在变量 z
。generative_net
将潜在变量 z
转换回重构数据。普通推理网络只运行一次 forward
,而展开的推理网络可以通过多步迭代优化潜在变量。例如:
def unrolled_inference(x, inference_net, generative_net, num_steps=5, learning_rate=0.01):
mean, logvar = inference_net(x)
z = mean # 初始估计
for _ in range(num_steps):
recon_x = generative_net(z)
loss = ((recon_x - x) ** 2).sum() # 重构损失
grad = torch.autograd.grad(loss, z, retain_graph=True)[0]
z = z - learning_rate * grad # 梯度下降更新 z
return z
# 示例调用
x = torch.randn(1, 784) # 假设输入数据
z = unrolled_inference(x, inference_net, generative_net)
代码解释:
mean
和 logvar
,并以 mean
作为潜在变量 z
的起点。z
,使重构数据 recon_x
更接近输入 x
。马尔可夫链
展开的近似推理网络
这两个概念在生成模型中很重要,但侧重点不同:马尔可夫链关注采样,展开的推理网络关注推理精度。
先验分布(Prior Distribution)是贝叶斯统计中的一个核心概念,它是指在没有看到任何数据之前,我们对未知参数的初步假设或信念。这个信念用概率分布的形式来表达,反映了我们在数据到来之前对参数可能取值的看法。
为了更好地理解先验分布,我们可以用一个简单的例子来说明。假设你在猜测一枚硬币是否公平:
在贝叶斯统计中,先验分布通常用 p ( θ ) p(\theta) p(θ) 表示,其中 θ \theta θ 是我们想要估计的未知参数。
先验分布在贝叶斯统计中之所以重要,是因为它通过贝叶斯定理与数据结合,更新我们的信念。贝叶斯定理的公式是:
p ( θ ∣ x ) = p ( x ∣ θ ) ⋅ p ( θ ) p ( x ) p(\theta | x) = \frac{p(x | \theta) \cdot p(\theta)}{p(x)} p(θ∣x)=p(x)p(x∣θ)⋅p(θ)
其中:
先验分布可以根据我们对参数的了解程度分为两种:
先验分布在贝叶斯统计中有以下几个关键作用:
简单来说,先验分布是贝叶斯统计中在看到数据之前对未知参数的初步假设,用概率分布来表达我们的信念。通过贝叶斯定理,先验分布与数据(似然函数)结合,更新为更准确的后验分布。它既体现了主观性(基于我们的知识或猜测),又为统计推断提供了一个起点。
后验分布是贝叶斯统计中的一个核心概念。简单来说,它是在给定观测数据的情况下,对未知参数的概率分布进行更新后的结果。它结合了我们事先的信念(先验分布)和新的数据信息,反映了参数更真实的可能性。
在贝叶斯统计中,我们通常会从以下步骤理解后验分布:
假设你在猜测一个硬币是否公平:
贝叶斯定理告诉我们如何计算后验分布:
p ( θ ∣ x ) = p ( x ∣ θ ) ⋅ p ( θ ) p ( x ) p(\theta | x) = \frac{p(x | \theta) \cdot p(\theta)}{p(x)} p(θ∣x)=p(x)p(x∣θ)⋅p(θ)
后验分布非常重要,因为它可以用来:
在像变分自编码器(VAE)这样的模型中,后验分布也有类似的应用。假设我们用VAE生成手写数字图像: