WGAN与WGAN-GP

省流

*生成对抗网络(GAN)在图像生成等任务上表现出色,但传统的 GAN 训练存在诸多问题,如模式崩溃(Mode Collapse)梯度消失或爆炸等。为了解决这些问题,WGAN(Wasserstein GAN)及其改进版本 WGAN-GP(WGAN with Gradient Penalty)被提出,使得 GAN 的训练更加稳定,并能够生成质量更高的样本。*

传统的GAN模型的很多问题来自于它的损失函数的数学缺陷上。使用原损失函数其实是等价于去优化一个JS散度,假如两个分布完全没有重合的情况下,损失函数值是一个常数log2,这个时候梯度为0,这一点对于模型训练是致命的,会导致梯度消失的问题,那么如何修改这个损失函数,才能使得缓解这个问题呢,所以研究人员提出了使用 Wasserstein 距离来替代原设计。

从直观上来看,Wasserstein 好像就是将原损失函数的取对数的操作取消掉,成为

  • 批评器的损失函数
    $$ L_D = E[D(X)] - E[D(G(Z))] $$
  • 生成器的损失函数
    $$ L_G = -E[D(G(Z))] $$

为了使得这一替换有效,模型必须满足 1-李普希兹 函数条件,这一点需要使用 梯度裁剪梯度惩罚 的操作。

最终证明效果很好。

WGAN 简介

WGAN(Wasserstein GAN)由 Martin Arjovsky 等人在 2017 年提出,并在论文《Wasserstein GAN》中进行了详细介绍。WGAN 通过引入Wasserstein 距离(也称 Earth Mover’s Distance, EMD) 来度量真实分布和生成分布之间的差距,从而改善训练稳定性。

WGAN 的改进点

相较于传统 GAN,WGAN 主要有以下改进:

  1. 引入 Wasserstein 距离
    • 传统 GAN 使用 JS 散度(Jensen-Shannon Divergence)度量真实分布和生成分布的差异,但容易导致梯度消失。
    • WGAN 采用 Wasserstein 距离(EMD),它可以提供更稳定的训练信号。
  2. 去掉 Sigmoid 及交叉熵损失
    • WGAN 直接使用神经网络的输出值作为衡量分布距离的指标,不再使用 Sigmoid 激活函数。
  3. 使用权重裁剪(Weight Clipping)
    • WGAN 要求判别器(Critic)的参数保持在一定范围(如 [-0.01, 0.01]),以满足 1-Lipschitz 条件。
  4. 不再使用传统的判别器(Discriminator),改用批评器(Critic)
    • 批评器不再输出样本是真假,而是输出 Wasserstein 距离的估计值。

WGAN 结构

生成器(Generator)

  • 生成器的结构与普通 GAN 类似,输入为随机噪声 Z,输出为生成的图像 G(Z)
  • 目标是最小化 Wasserstein 距离,使生成样本的分布接近真实分布。

批评器(Critic)

  • 取代传统判别器,输入为真实图像 X 或生成图像 G(Z)
  • 输出一个任意实数,其值用于衡量真实数据和生成数据的 Wasserstein 距离。
  • 训练时优化 Wasserstein 损失,使得批评器可以正确衡量两者的距离。

WGAN 损失函数

  • 批评器的损失函数
    $$ L_D = E[D(X)] - E[D(G(Z))] $$
  • 生成器的损失函数
    $$ L_G = -E[D(G(Z))] $$

WGAN-GP 简介

虽然 WGAN 通过权重裁剪保证了 1-Lipschitz 条件,但这种方法存在一定的弊端,如梯度消失、参数空间受限。为了解决这些问题,Gulrajani 等人提出了 WGAN-GP(WGAN with Gradient Penalty),用**梯度惩罚(Gradient Penalty)**取代权重裁剪。

WGAN-GP 的改进点

  1. 去掉权重裁剪,改用梯度惩罚
    • 直接限制批评器的梯度范数,使其满足 1-Lipschitz 条件。
  2. 更稳定的训练
    • 由于不再强制限制参数范围,模型可以更自由地学习复杂分布。

WGAN-GP 的损失函数

  • 批评器的损失函数
    $$ L_D = E[D(X)] - E[D(G(Z))] + \lambda E[(||\nabla_{\hat{x}} D(\hat{x})||_2 - 1)^2] $$
    其中:

    • ( \lambda ) 是梯度惩罚项的权重,一般设为 10。
    • ( \hat{x} ) 是真实样本和生成样本之间的插值。
    • 目标是让梯度范数接近 1,以满足 1-Lipschitz 条件。
  • 生成器的损失函数(与 WGAN 相同):
    $$ L_G = -E[D(G(Z))] $$

WGAN-GP 训练过程

  1. 批评器训练
    • 使用真实数据 X 和生成数据 G(Z) 计算 Wasserstein 距离。
    • 计算梯度惩罚项,调整批评器的参数。
  2. 生成器训练
    • 生成器更新参数,使 D(G(Z)) 尽可能大,即最小化 Wasserstein 距离。
  3. 交替训练
    • 通常训练 5 次批评器,再训练 1 次生成器

WGAN 和 WGAN-GP 的对比

WGAN WGAN-GP
Lipschitz 约束 权重裁剪 梯度惩罚
训练稳定性 相对稳定 更加稳定
适用于高维数据 可能受限 适用于更复杂的数据
生成质量 良好 更高质量

WGAN / WGAN-GP 的应用

WGAN 及 WGAN-GP 由于训练稳定,广泛应用于图像生成、风格迁移、数据增强等任务。例如:

  • 高质量人脸生成(如 CelebA 数据集)
  • 医学影像合成(用于补充训练数据)
  • 图像超分辨率

总结

WGAN 通过 Wasserstein 距离提高了 GAN 训练的稳定性,而 WGAN-GP 进一步优化了 Lipschitz 约束,使得训练更加稳定,生成效果更好。两者的主要改进点包括:

  • WGAN 引入 Wasserstein 距离,改进损失函数,但仍需权重裁剪。
  • WGAN-GP 用梯度惩罚替代权重裁剪,提高稳定性
  • WGAN-GP 适用于更复杂的数据分布,效果更优。

WGAN 和 WGAN-GP 的提出极大地推动了 GAN 的发展,后续许多生成模型(如 StyleGAN)都在其基础上进行改进。

DCGAN

生成对抗网络(GAN)是一种强大的生成模型,而深度卷积生成对抗网络(DCGAN, Deep Convolutional GAN)是GAN的一个改进版本,它引入了**深度卷积神经网络(CNN)**来增强图像生成的能力,使得生成的图像更加清晰、稳定。

DCGAN 简介

DCGAN 由 Radford 等人在 2015 年提出,并在论文《Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks》中详细介绍。它主要改进了 GAN 的网络结构,通过使用卷积层和去卷积层(反卷积层)来替代传统的全连接层,从而提高生成器的表现力和稳定性。

DCGAN 的改进点

相较于标准 GAN,DCGAN 主要有以下改进:

  1. 使用卷积层 取代全连接层,使网络能够更好地学习图像的空间特征。
  2. 使用批归一化(Batch Normalization) 来稳定训练,防止梯度消失或爆炸。
  3. 去掉池化层(Pooling),改用步长为 2 的卷积和反卷积操作来实现下采样和上采样。
  4. 在生成器中使用 ReLU 激活函数(最后一层使用 Tanh),提高梯度流动性。
  5. 在判别器中使用 LeakyReLU 激活函数,避免梯度完全消失。

DCGAN 结构

DCGAN 仍然由**生成器(Generator)判别器(Discriminator)**组成,它们的架构有所调整,以更好地适应图像数据。

生成器(Generator)

生成器的任务是将一个随机噪声向量 Z 转换为逼真的图像。

生成器的结构

  • 输入:随机噪声 Z(通常服从标准正态分布)。
  • 通过一系列**转置卷积(反卷积)**层,将低维噪声转换为高维图像。
  • 每一层使用批归一化(Batch Normalization),避免训练不稳定。
  • 隐藏层使用 ReLU 激活函数,最后一层使用 Tanh 激活函数,确保输出像素值在 [-1,1] 之间。

生成器的优化目标

生成器的目标是欺骗判别器,使其认为生成的图像是真实的,即最小化以下损失函数:

$$
L_G = -E[\log D(G(Z))]
$$

判别器(Discriminator)

判别器的任务是区分输入图像是真实的还是由生成器生成的。

判别器的结构

  • 输入:一张图像(可能是真实的,也可能是生成的)。
  • 通过多个卷积层提取特征,每一层都使用 LeakyReLU 激活函数。
  • 最后通过全连接层 + Sigmoid 激活函数,输出 01,表示假图像或真图像。

判别器的优化目标

判别器的目标是正确区分真实图像 X 和生成图像 G(Z),即最大化以下损失函数:

$$
L_D = -E[\log D(X)] - E[\log(1 - D(G(Z)))]
$$

DCGAN 训练过程

训练 DCGAN 需要交替优化生成器和判别器,使二者不断进步,最终生成高质量的图像。

训练步骤

  1. 判别器训练

    • 使用真实图像 X 计算 D(X),并最大化 log(D(X))
    • 使用生成器生成的图像 G(Z) 计算 D(G(Z)),并最大化 log(1 - D(G(Z)))
    • 计算损失 L_D,更新判别器参数。
  2. 生成器训练

    • 生成器生成图像 G(Z)
    • 计算 D(G(Z)),希望让判别器将其判定为真实,即最大化 log(D(G(Z)))
    • 计算损失 L_G,更新生成器参数。
  3. 交替训练

    • 通常先训练判别器几步,再训练生成器一步。
    • 经过多个 epoch 的训练后,生成器可以生成高质量的图像。

训练技巧

  • 使用批归一化(BatchNorm):避免训练不稳定。
  • 调整学习率:通常使用 Adam 优化器,学习率设为 0.0002。
  • 避免判别器过强:如果判别器训练得太好,生成器可能无法学习到有效的特征。
  • 使用标签平滑(Label Smoothing):真实样本标签用 0.9 代替 1.0,避免梯度消失问题。

DCGAN 的应用

DCGAN 被广泛应用于图像生成任务,如:

  • 人脸生成:如 CelebA 数据集训练的 DCGAN 可生成逼真的人脸。
  • 动漫风格生成:使用 DCGAN 训练动漫数据集,可生成风格化的角色。
  • 数据增强:可以用于补充稀缺数据,提高模型的泛化能力。
  • 艺术创作:可用于生成风格化的艺术作品。

总结

DCGAN 通过引入深度卷积网络(CNN)结构,使 GAN 训练更加稳定,生成的图像更加逼真。其主要改进包括:

  • 去掉全连接层,使用卷积层和反卷积层
  • 批归一化稳定训练,提高模型收敛性
  • 改进激活函数,使用 ReLU 和 LeakyReLU 提高梯度流动性
  • 去掉池化层,使用步长控制特征提取和生成过程

由于这些改进,DCGAN 在图像生成任务上表现优秀,并成为后续许多生成模型(如 StyleGAN、BigGAN)的基础。

ACGAN

在之前的博客中,我们介绍了通用的GAN代码实现,它能够生成图像,但无法生成指定类别的图像。ACGAN(Auxiliary Classifier GAN)弥补了这一缺陷,通过在生成器和判别器中引入类别信息,使模型能够生成特定类别的数据。

ACGAN的生成器接收一个随机噪声和一个图像标签作为输入,并输出一张符合该类别的图像。而判别器不仅要判断输入图像的真假,还要同时输出图像的类别标签

ACGAN简介

ACGAN的最大特点是既能生成图像,又能进行分类。它是对传统GAN的扩展,由Ian Goodfellow等人在2014年提出的GAN基础上进一步发展而来。ACGAN通过引入条件控制,使生成过程受到额外信息的指导,从而能够生成具有特定属性或风格的数据。

ACGAN的原理

ACGAN的基本原理依然基于GAN的对抗训练框架,包括一个生成器(Generator)和一个判别器(Discriminator),但两者都接受额外的类别信息(Conditional Information),例如类别标签、文本描述等。

  • 生成器(Generator)

    • 接收随机噪声向量 Z 和条件信息 C(如类别标签)。
    • 结合 ZC 生成符合指定类别的数据样本。
  • 判别器(Discriminator)

    • 输入一张图像,判断其是真实的还是生成的。
    • 额外输出该图像的类别标签。

训练过程

  • 对抗训练:生成器不断优化,使其生成的样本足够真实,能够欺骗判别器;判别器则不断学习,以更准确地区分真实数据与生成数据。
  • 损失函数
    • 生成器的损失 = GAN 损失 + 分类损失(生成样本的类别正确性)。
    • 判别器的损失 = 真实样本的真假判断损失 + 生成样本的真假判断损失 + 分类损失。

这种设计不仅提高了GAN的生成质量,还能确保生成的样本具有正确的类别信息。

ACGAN的关键机制——辅助分类器(Auxiliary Classifier)

GAN的传统机制可以概括为:输入随机噪声,输出伪造样本。然而,这样的生成方式缺乏约束,就像火车没有轨道一样,生成结果的方向不可控。为了解决这个问题,研究者提出了CGAN(Conditional GAN),通过向GAN添加辅助标签,使生成过程更加精准。

ACGAN是CGAN的扩展,在判别器中增加了辅助分类器(Auxiliary Classifier),用于预测输入数据的类别信息。这使得模型不仅能够判断数据真假,还能进一步分类,提高生成数据的可控性。

辅助分类器的结构

辅助分类器是一种神经网络组件,通常嵌入判别器中,负责预测输入数据的类别。其一般结构如下:

  1. 输入层:接收图像数据及其类别标签(或其他条件信息)。
  2. 特征提取层:使用卷积层(对于图像数据)或全连接层(对于其他数据)提取特征。
  3. 条件融合层:在某些模型中,特征与条件信息会进一步融合(如拼接、元素乘法或注意力机制)。
  4. 分类层:采用全连接层输出类别预测结果,通常使用 softmax 计算类别概率分布。
  5. 损失函数:使用交叉熵损失衡量预测类别与真实类别之间的差距。

判别器的整体结构

ACGAN的判别器由两个分支组成:

  • 主分类器分支:判断输入数据是真实的还是生成的。
  • 辅助分类器分支:预测输入数据的类别。

判别器的总损失是主分类器损失与辅助分类器损失的加权组合,从而保证它既能正确区分真假数据,又能准确分类。

ACGAN的条件分类实现过程

ACGAN的核心目标是通过条件信息控制数据的生成过程,使模型能够按照给定的类别生成特定的数据。

生成器(Generator)

  1. 输入
    • 随机噪声向量 Z(通常来自高斯分布)。
    • 条件向量 C(通常是 one-hot 编码的类别标签)。
  2. 条件嵌入
    • C 通过嵌入层转换为与 Z 维度相同的向量。
    • ZC 进行合并,形成新的输入向量。
  3. 生成数据
    • 通过深度神经网络(如卷积层、反卷积层等)生成数据样本。

判别器(Discriminator)

  1. 输入
    • 真实数据样本或生成器生成的数据样本。
    • 条件向量 C
  2. 特征提取
    • 通过多个卷积层提取特征。
  3. 两个输出分支
    • 真假判断分支:判断样本是真实的还是生成的。
    • 辅助分类器分支:预测样本的类别。
  4. 损失函数
    • 对抗损失:用于真假判断。
    • 分类损失:用于类别预测。

训练流程

  1. 判别器训练
    • 使用真实数据计算对抗损失和分类损失。
    • 使用生成数据计算对抗损失和分类损失。
    • 计算总损失并优化判别器。
  2. 生成器训练
    • 生成数据,试图欺骗判别器,使其误判为真实数据。
    • 计算生成样本的分类损失。
    • 计算总损失并优化生成器。
  3. 交替训练
    • 通常先更新判别器几次,再更新生成器一次。

训练优化建议

  • 条件信息合并方式:直接拼接或使用注意力机制。
  • 损失权重调整:对抗损失和分类损失需要适当平衡。
  • 训练稳定性:调整学习率、批量大小等超参数,提高训练稳定性。

总结

ACGAN通过辅助分类器的引入,使得生成器能够生成带有特定类别属性的数据,同时保持GAN的对抗训练优势。这使得ACGAN在图像生成、风格迁移、数据增强等领域具备广泛的应用价值。

支持向量机(SVM)详解

支持向量机(Support Vector Machine, SVM)是一种常用于分类和回归的机器学习算法,因其优秀的泛化能力和数学理论基础,在数据挖掘、文本分类、人脸识别等任务中广泛应用。

1. SVM 基本概念

SVM 的核心思想是找到一个最优的超平面(Hyperplane),用于划分数据,使不同类别的数据点尽可能分开,并最大化间隔(Margin)

  • 超平面:在二维空间中,超平面是一条直线;在三维空间中,它是一个平面;在更高维度的空间中,仍称之为超平面。
  • 支持向量(Support Vectors):距离超平面最近的点,这些点决定了最优超平面的位置。
  • 间隔(Margin):支持向量到超平面的最小距离,SVM 试图最大化间隔,以提高泛化能力。

2. SVM 的数学原理

假设我们的数据集是线性可分的,给定训练样本 ( (x_i, y_i) ),其中 ( x_i ) 是特征向量,( y_i \in {-1, 1} ) 表示类别标签。

超平面的方程可以表示为:
[ w \cdot x + b = 0 ]
其中 ( w ) 是法向量,决定了超平面的方向,( b ) 是偏置项。

为了找到最优超平面,我们需要最大化间隔 ( \frac{2}{||w||} ),等价于最小化 ( ||w||^2 ),同时保证所有样本点被正确分类:

[ y_i (w \cdot x_i + b) \geq 1, \quad \forall i ]

这就是硬间隔 SVM 的优化问题。

2.1 软间隔 SVM

在现实数据中,可能存在部分噪声数据,使得严格的线性可分难以实现。因此,我们引入松弛变量 ( \xi_i ) 来允许一定的误分类:

[ \min \frac{1}{2} ||w||^2 + C \sum \xi_i ]

其中 ( C ) 是超参数,控制间隔最大化与误分类的权衡。

2.2 核函数(Kernel Trick)

当数据是非线性可分时,SVM 使用核函数(Kernel Function) 将数据映射到高维特征空间,使其在高维空间中线性可分。

常见核函数包括:

  • 线性核函数:( K(x_i, x_j) = x_i \cdot x_j )
  • 多项式核函数:( K(x_i, x_j) = (x_i \cdot x_j + c)^d )
  • 高斯径向基核(RBF 核):( K(x_i, x_j) = e^{-\gamma ||x_i - x_j||^2} )

3. SVM 的优缺点

优点

  • 适用于高维数据,尤其是文本分类等任务。
  • 通过核技巧处理非线性问题。
  • 具有良好的泛化能力。

缺点

  • 对于大规模数据集,训练时间较长。
  • 需要选择合适的核函数,否则可能导致过拟合。
  • 对噪声较敏感。

4. SVM 的 Python 实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score

# 加载数据集
iris = datasets.load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 训练 SVM 模型
clf = SVC(kernel='rbf', C=1.0, gamma='scale')
clf.fit(X_train, y_train)

# 预测 & 评估
y_pred = clf.predict(X_test)
print("Accuracy:", accuracy_score(y_test, y_pred))

逻辑回归(Logistic Regression)详解

逻辑回归(Logistic Regression)是一种常见的分类算法,尽管名字中带有“回归”,但它主要用于二分类问题,也可以扩展到多分类任务。

1. 逻辑回归的基本原理

逻辑回归的核心思想是使用**逻辑函数(Sigmoid)**将线性回归的输出转换为概率值:

[
\sigma(z) = \frac{1}{1 + e^{-z}}
]

其中,( z ) 是线性回归的结果:

[
z = w_1x_1 + w_2x_2 + … + w_nx_n + b
]

通过 Sigmoid 函数,逻辑回归可以输出一个介于 0 和 1 之间的概率值,并根据设定的阈值(通常为 0.5)将样本分类为 0 或 1。

2. 逻辑回归的决策边界

逻辑回归的决策边界是由权重和偏置确定的超平面。例如:

  • 二维数据中,决策边界是一条直线
  • 三维数据中,决策边界是一个平面
  • 在更高维数据中,决策边界是一个超平面

3. 逻辑回归的损失函数

为了优化逻辑回归模型,我们使用对数损失函数(Log Loss),即交叉熵损失(Cross-Entropy Loss)

[
L = - \frac{1}{m} \sum_{i=1}^{m} [ y_i \log \hat{y}_i + (1 - y_i) \log (1 - \hat{y}_i) ]
]

其中:

  • ( y_i ) 是真实标签(0 或 1)。
  • ( \hat{y}_i ) 是预测的概率值。
  • ( m ) 是样本总数。

4. 逻辑回归的优化方法

逻辑回归使用**梯度下降(Gradient Descent)**来最小化损失函数,更新权重 ( w ) 和偏置 ( b ):

[
w = w - \alpha \frac{\partial L}{\partial w}, \quad b = b - \alpha \frac{\partial L}{\partial b}
]

其中 ( \alpha ) 是学习率。

常见优化方法:

  • 批量梯度下降(BGD)
  • 随机梯度下降(SGD)
  • 小批量梯度下降(Mini-batch SGD)

5. 逻辑回归的 Python 实现

使用 scikit-learn 进行逻辑回归建模:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
from sklearn.metrics import accuracy_score

# 生成数据集
X, y = make_classification(n_samples=1000, n_features=2, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 训练逻辑回归模型
model = LogisticRegression()
model.fit(X_train, y_train)

# 预测
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"模型准确率: {accuracy:.2f}")

6. 逻辑回归的优缺点

优点

  • 计算简单,易于实现。
  • 训练速度快,适用于大规模数据。
  • 结果可解释性强,能提供特征权重。

缺点

  • 只能处理线性可分问题,无法解决复杂非线性关系。
  • 对异常值较敏感。
  • 不能自动进行特征选择,可能需要手动筛选。

7. 逻辑回归的扩展

  • 多分类逻辑回归(Softmax 回归):用于多类别分类任务。
  • 正则化逻辑回归(L1/L2 正则化):防止过拟合。
  • 带核的逻辑回归(Kernel Logistic Regression):适用于非线性数据。

WGAN/WGAN-GP 实现手写数字生成

题目

  1. 本次挑战使用的MNIST手写数字数据集,包含60,000张28x28的灰度图像,分为10个类别(数字0-9)。此数据集将用于训练你的生成对抗网络。
  2. 你的任务是使用DCGAN模型,对该数据集进行图像生成。具体要求如下:
    1. 数据集下载:请下载MNIST数据集,并确保数据集中包含训练集和测试集。
    2. 数据预处理:将图像数据进行必要的预处理,使其适合于DCGAN模型的训练。
    3. 模型训练:搭建DCGAN模型,并利用训练数据集进行训练,调整模型参数,尝试生成高质量的数字图像。
    4. 模型评估:在训练过程中,监控生成图像的质量,并可视化不同训练阶段生成的图像。

代码

WGAN

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
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import torchvision.utils as vutils
import os

batch_size = 128
lr = 0.00005 # WGAN采用较小的学习率
noise_dim = 100
epochs = 20
channel_size = 1
critic_iter = 5 # 判别器训练次数
weight_clip = 0.01 # 权重裁剪范围

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
os.makedirs("output_wgan", exist_ok=True)

# 数据预处理
transform = transforms.Compose([
transforms.Grayscale(num_output_channels=1), # 修改默认的图像通道数
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])

# 使用 ImageFolder 读取数据
dataset = datasets.ImageFolder(root='data/mnist_jpg', transform=transform)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)


class Generator(nn.Module):
"""
基于卷积层的生成器
和 DCGAN 相同
"""
def __init__(self, noise_dim, channel_size):
super().__init__()
self.main = nn.Sequential(
nn.ConvTranspose2d(noise_dim, 128, kernel_size=7, stride=1, padding=0),
nn.BatchNorm2d(128),
nn.ReLU(True),

nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),

nn.ConvTranspose2d(64, channel_size, kernel_size=4, stride=2, padding=1),
nn.Tanh()
)

def forward(self, input):
return self.main(input)


class Discriminator(nn.Module):
"""
基于卷积层的判别器
和 DCGAN 相同
"""
def __init__(self, channel_size):
super().__init__()
self.main = nn.Sequential(
nn.Conv2d(channel_size, 64, kernel_size=4, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True),

nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True),
)

self.flatten = nn.Flatten()
self.fc = nn.Linear(128 * 7 * 7, 1)

def forward(self, input):
x = self.main(input)
x = self.flatten(x)
output = self.fc(x)
return output


# 模型,优化器
netG = Generator(noise_dim, channel_size).to(device)
netD = Discriminator(channel_size).to(device)

# 原论文建议使用 RMSprop 优化器
optimizerD = optim.RMSprop(netD.parameters(), lr=lr)
optimizerG = optim.RMSprop(netG.parameters(), lr=lr)

# 训练过程
for epoch in range(epochs):
for i, (data, _) in enumerate(dataloader):
real_imgs = data.to(device)
batch_size = real_imgs.size(0)

# 判别器训练
for _ in range(critic_iter):
netD.zero_grad()
noise = torch.randn(batch_size, noise_dim, 1, 1, device=device)
fake_imgs = netG(noise)

# 计算 Wasserstein 损失
lossD = -netD(real_imgs).mean() + netD(fake_imgs.detach()).mean()
lossD.backward()
optimizerD.step()

# 权重裁剪
for p in netD.parameters():
p.data.clamp_(-weight_clip, weight_clip)

# 生成器训练
netG.zero_grad()
fake_imgs = netG(noise)

# 计算 Wasserstein 损失
lossG = -netD(fake_imgs).mean()
lossG.backward()
optimizerG.step()

if i % 100 == 0:
print(f"Epoch [{epoch + 1}/{epochs}] Batch {i}/{len(dataloader)} Loss_D: {lossD.item():.4f} Loss_G: {lossG.item():.4f}")

# 保存生成结果
with torch.no_grad():
fixed_noise = torch.randn(16, noise_dim, 1, 1, device=device)
fake = netG(fixed_noise)
vutils.save_image(fake, f"output_wgan/fake_samples_epoch_{epoch + 1}.png", normalize=True)

WGAN-GP

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
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import torchvision.utils as vutils
import os

batch_size = 128
lr = 0.0002
noise_dim = 100
epochs = 20
channel_size = 1
lambda_gp = 10 # 梯度惩罚系数
critic_iterations = 5 # 判别器训练次数

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
os.makedirs("output_wgan_gp", exist_ok=True)

# 数据预处理
transform = transforms.Compose([
transforms.Grayscale(num_output_channels=1),
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])

dataset = datasets.ImageFolder(root='data/mnist_jpg', transform=transform)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)


class Generator(nn.Module):
"""
基于卷积层的生成器
和 DCGAN 相同
"""
def __init__(self, noise_dim, channel_size):
super().__init__()
self.main = nn.Sequential(
nn.ConvTranspose2d(noise_dim, 128, kernel_size=7, stride=1, padding=0),
nn.BatchNorm2d(128),
nn.ReLU(True),
nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),
nn.ConvTranspose2d(64, channel_size, kernel_size=4, stride=2, padding=1),
nn.Tanh()
)

def forward(self, input):
return self.main(input)


class Critic(nn.Module):
"""
基于卷积层的判别器
和 DCGAN 相同
"""
def __init__(self, channel_size):
super().__init__()
self.main = nn.Sequential(
nn.Conv2d(channel_size, 64, kernel_size=4, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2, inplace=True)
)
self.flatten = nn.Flatten()
self.fc = nn.Linear(128 * 7 * 7, 1)

def forward(self, input):
x = self.main(input)
x = self.flatten(x)
return self.fc(x)


def compute_gradient_penalty(critic, real_samples, fake_samples):
"""
计算梯度惩罚
"""
# 随机数 alpha作为插值的权重, interpolates 是在真实样本和假样本之间的插值数据
alpha = torch.rand(real_samples.size(0), 1, 1, 1, device=device)
interpolates = (alpha * real_samples + (1 - alpha) * fake_samples).requires_grad_(True)

# 得到判别器的结果
critic_interpolates = critic(interpolates)

# torch.autograd.grad用于计算导数,将梯度计算的结果存储在 gradients 变量中
gradients = torch.autograd.grad(outputs=critic_interpolates, inputs=interpolates,
grad_outputs=torch.ones_like(critic_interpolates),
create_graph=True, retain_graph=True, only_inputs=True)[0]

# 将梯度展平 (Batch, channels, height, width) -> (Batch, channels * height * width)
gradients = gradients.view(gradients.size(0), -1)

# 计算梯度惩罚, 公式为((梯度的L2范数 - 1) ^ 2)的均值
# 理想情况下,Lipschitz常数应该为1,因此梯度的L2范数(gradients.norm(2, dim=1))应该接近1。
# 如果它大于1或小于1,都会给模型带来惩罚,以倒逼判别器的梯度符合要求。
gradient_penalty = ((gradients.norm(2, dim=1) - 1) ** 2).mean()
return gradient_penalty


# 模型,优化器
netG = Generator(noise_dim, channel_size).to(device)
netC = Critic(channel_size).to(device)

optimizerC = optim.Adam(netC.parameters(), lr=lr, betas=(0.5, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(0.5, 0.999))

# 训练过程
for epoch in range(epochs):
for i, (data, _) in enumerate(dataloader):
real_imgs = data.to(device)
batch_size = real_imgs.size(0)

# 训练判别器
for _ in range(critic_iterations):
netC.zero_grad()
noise = torch.randn(batch_size, noise_dim, 1, 1, device=device)
fake_imgs = netG(noise)

# 计算 Wasserstein 损失
lossC_real = -netC(real_imgs).mean() + netC(fake_imgs.detach()).mean()
# 计算梯度惩罚
gradient_penalty = compute_gradient_penalty(netC, real_imgs, fake_imgs.detach())
lossC = lossC_real + lambda_gp * gradient_penalty # 增加梯度惩罚

lossC.backward()
optimizerC.step()

# 训练生成器
netG.zero_grad()
fake_imgs = netG(noise)

# 计算 Wasserstein 损失
lossG = -netC(fake_imgs).mean()
lossG.backward()
optimizerG.step()

if i % 100 == 0:
print(f"Epoch [{epoch + 1}/{epochs}] Batch {i}/{len(dataloader)} Loss_C: {lossC.item():.4f} Loss_G: {lossG.item():.4f}")

# 保存每个 epoch 的生成结果
with torch.no_grad():
fixed_noise = torch.randn(16, noise_dim, 1, 1, device=device)
fake = netG(fixed_noise)
vutils.save_image(fake, f"output_wgan_gp/fake_samples_epoch_{epoch + 1}.png", normalize=True)

结果

10 epoches:
alt text

Wasserstein距离

Wasserstein距离(Wasserstein Distance)

Wasserstein距离,又被称为地球移动者距离(Earth Mover’s Distance, EMD),是一种衡量两个概率分布之间“距离”的方法。直观来说,它可以看作是从一个分布到另一个分布移动“质量”的最小工作量。

直观解释

假设你有两个概率分布,分别表示两个不同的“山脊”(或者“堆积”)。Wasserstein距离试图计算,把一堆质量从一个山脊移动到另一个山脊所需要的最小“成本”。其中,每个“成本”都是根据移动质量的距离来计算的。可以把它看作是把一堆土从一个地方搬到另一个地方所需要的最少工作量。

公式

Wasserstein距离通常定义为最小传输成本。对于两个分布 (P) 和 (Q),它的计算可以用下面的公式表示:

[
W(P, Q) = \inf_{\gamma \in \Gamma(P, Q)} \mathbb{E}_{(x,y) \sim \gamma} [| x - y |]
]

其中:

  • ( \Gamma(P, Q) ) 是所有满足边际条件 (P) 和 (Q) 的联合分布(即对每个 (x) 和 (y) 在分布 (P) 和 (Q) 下的配对情况)。
  • ( | x - y | ) 是计算在空间中从 (x) 到 (y) 的距离。

Wasserstein距离实际上衡量的是从一个分布到另一个分布的最小“成本”或“运输工作量”。

优势

  • 良好的梯度性质:与Kullback-Leibler散度等传统方法不同,Wasserstein距离在分布之间有更平滑的过渡。它不容易出现梯度消失问题,因此它在训练过程中更加稳定。
  • 可用性:Wasserstein距离不仅适用于连续分布,还可以适用于离散分布,这使得它在处理一些复杂的数据分布时特别有用。

在GAN中的应用

在生成对抗网络中,**Wasserstein GAN(WGAN)**引入了Wasserstein距离作为优化目标,解决了传统GAN中的训练不稳定问题。传统GAN在训练时存在判别器输出饱和和梯度消失的问题,而WGAN通过Wasserstein距离的引入,使得生成器和判别器的学习更加稳定,并且生成效果更好。


KL散度

KL散度(Kullback-Leibler Divergence)

KL散度,又叫Kullback-Leibler信息量散度,是一种衡量两个概率分布之间差异的度量。它定义了从分布 (Q) 到分布 (P) 的信息损失,或者说,如果我们用分布 (Q) 来近似真实分布 (P),则产生的误差。

直观解释

KL散度衡量的是使用概率分布 (Q) 来代替概率分布 (P) 时,所丧失的信息量。它并不是对称的,即 (D_{KL}(P \parallel Q)) 不等于 (D_{KL}(Q \parallel P))。可以把KL散度理解为,给定一个真实的分布 (P),如果我们用分布 (Q) 来近似它,那么KL散度就是衡量这种近似误差的大小。

公式

对于离散的概率分布 (P) 和 (Q),KL散度的公式为:

[
D_{KL}(P \parallel Q) = \sum_{i} P(i) \log \frac{P(i)}{Q(i)}
]

对于连续的概率分布 (P) 和 (Q),KL散度的公式为:

[
D_{KL}(P \parallel Q) = \int p(x) \log \frac{p(x)}{q(x)} , dx
]

其中:

  • (P(i)) 和 (Q(i)) 分别是离散分布 (P) 和 (Q) 在样本点 (i) 上的概率。
  • (p(x)) 和 (q(x)) 是连续分布 (P) 和 (Q) 在样本点 (x) 上的概率密度。

优势

  • 易于理解和计算:KL散度的计算相对简单,并且能够直接衡量分布之间的信息丧失。
  • 适用于实际应用:在许多实际机器学习任务中(例如变分推理和自编码器),KL散度经常作为目标函数来进行优化。

缺点

  • 不对称:KL散度不对称,即 (D_{KL}(P \parallel Q) \neq D_{KL}(Q \parallel P)),这可能会导致某些情况下的评估偏差。
  • 无法处理零概率问题:如果分布 (Q) 中某些事件的概率为零,而分布 (P) 中该事件的概率不为零,KL散度会出现无穷大,这在某些情况下会带来计算上的困难。

在GAN中的应用

KL散度在原始GAN的训练过程中并没有直接作为损失函数,但它与生成模型中的**变分自编码器(VAE)**相关。VAE在训练时使用KL散度来衡量潜在变量的分布与标准正态分布之间的差异,因此KL散度在生成模型中经常被用来优化潜在空间的结构。

ACGAN 实现手写数字生成

题目

  1. 本次挑战使用的MNIST手写数字数据集,包含60,000张28x28的灰度图像,分为10个类别(数字0-9)。此数据集将用于训练你的生成对抗网络。
  2. 你的任务是使用DCGAN模型,对该数据集进行图像生成。具体要求如下:
    1. 数据集下载:请下载MNIST数据集,并确保数据集中包含训练集和测试集。
    2. 数据预处理:将图像数据进行必要的预处理,使其适合于DCGAN模型的训练。
    3. 模型训练:搭建DCGAN模型,并利用训练数据集进行训练,调整模型参数,尝试生成高质量的数字图像。
    4. 模型评估:在训练过程中,监控生成图像的质量,并可视化不同训练阶段生成的图像。

代码

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
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import torchvision.utils as vutils
import os

batch_size = 128
lr = 0.0002
noise_dim = 100
epochs = 20
channel_size = 1
num_classes = 10 # 数据集类别数

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
os.makedirs("output_acgan", exist_ok=True)

# 数据预处理和加载
transform = transforms.Compose([
transforms.Grayscale(num_output_channels=1),
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])

dataset = datasets.ImageFolder(root='data/mnist_jpg', transform=transform)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)


class Generator(nn.Module):
def __init__(self, noise_dim, num_classes, channel_size):
"""
基于卷积层的生成器
卷积层的部分和DCGAN完全相同,只是增加了类别嵌入,以学习到类别信息
"""
super().__init__()

# 将离散类别标签映射到连续向量空间
self.label_emb = nn.Embedding(num_classes, noise_dim)

self.main = nn.Sequential(
nn.ConvTranspose2d(noise_dim * 2, 128, kernel_size=7, stride=1, padding=0),
nn.BatchNorm2d(128),
nn.ReLU(True),

nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),

nn.ConvTranspose2d(64, channel_size, kernel_size=4, stride=2, padding=1),
nn.Tanh()
)

def forward(self, noise, labels):
# 将类别嵌入
label_embedding = self.label_emb(labels).unsqueeze(2).unsqueeze(3) # 变形以匹配噪声维度
input = torch.cat([noise, label_embedding], dim=1)
return self.main(input)


class Discriminator(nn.Module):
def __init__(self, channel_size, num_classes):
"""
基于卷积层的判别器
卷积层的部分和DCGAN完全相同
新增加类别分类头
"""
super().__init__()

self.main = nn.Sequential(
nn.Conv2d(channel_size, 64, kernel_size=4, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True),

nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2, inplace=True),
)

self.flatten = nn.Flatten()
self.fc_real_fake = nn.Linear(128 * 7 * 7, 1) # 真假分类
self.fc_class = nn.Linear(128 * 7 * 7, num_classes) # 分类

def forward(self, input):
x = self.main(input)
x = self.flatten(x)

real_fake = torch.sigmoid(self.fc_real_fake(x)) # 需要添加 softmax,因为 BCELoss 需要输入概率值
class_output = self.fc_class(x) # 分类输出(不加 softmax,交叉熵损失自带sigmoid)
return real_fake, class_output

# 模型,优化器,损失函数
netG = Generator(noise_dim, num_classes, channel_size).to(device)
netD = Discriminator(channel_size, num_classes).to(device)

criterion_gan = nn.BCELoss()
criterion_class = nn.CrossEntropyLoss()

optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(0.5, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(0.5, 0.999))

# 训练过程
for epoch in range(epochs):
for i, (data, labels) in enumerate(dataloader):
batch_size = data.size(0)
real_imgs, labels = data.to(device), labels.to(device)

real_labels = torch.ones(batch_size, 1, device=device)
fake_labels = torch.zeros(batch_size, 1, device=device)

# 训练判别器
netD.zero_grad()
# 真实数据损失
real_out, real_class = netD(real_imgs)
lossD_real = criterion_gan(real_out, real_labels)
lossD_real_class = criterion_class(real_class, labels)
# 计算虚假数据损失
# 生成假样本
noise = torch.randn(batch_size, noise_dim, 1, 1, device=device)
fake_labels_input = torch.randint(0, num_classes, (batch_size,), device=device)
fake_imgs = netG(noise, fake_labels_input)
# 假样本损失
fake_out, fake_class = netD(fake_imgs.detach())
lossD_fake = criterion_gan(fake_out, fake_labels)
lossD_fake_class = criterion_class(fake_class, fake_labels_input)
# 总判别器损失
lossD = lossD_real + lossD_fake + lossD_real_class + lossD_fake_class
lossD.backward()
optimizerD.step()

# 训练生成器
netG.zero_grad()
fake_out, fake_class = netD(fake_imgs)
# 生成器希望判别器把假样本判为真实
lossG_fake = criterion_gan(fake_out, real_labels)
lossG_classification = criterion_class(fake_class, fake_labels_input)
# 总生成器损失
lossG = lossG_fake + lossG_classification
lossG.backward()
optimizerG.step()

if i % 100 == 0:
print(f"Epoch [{epoch + 1}/{epochs}] Batch {i}/{len(dataloader)} "
f"Loss_D: {lossD.item():.4f} Loss_G: {lossG.item():.4f}")

# 保存生成结果
with torch.no_grad():
fixed_noise = torch.randn(16, noise_dim, 1, 1, device=device)
fixed_labels = torch.randint(0, num_classes, (16,), device=device)
fake = netG(fixed_noise, fixed_labels)
vutils.save_image(fake, f"output_acgan/fake_samples_epoch_{epoch + 1}.png", normalize=True)

结果

7 epoches:
alt text

批归一化

省流

批归一化就是对于一批数据中所有数据的同一特征维度进行归一化。

归一化的效果不用多说,对于模型训练的稳定性相当重要

比如
一批数据中有6个同学,每个同学有身高,体重,微积分成绩,线性代数成绩四个特征

A B C D E F
180 169 183 175 168 177
70 75 72 68 68 71
87 86 91 69 75 79
61 69 94 82 76 85

那么批归一化会对同一个特征维度进行归一化,比如对于身高|180|169|183|175|168|177|进行归一化

所以它在 CNN 等网络结构中表现良好,但是在特殊的任务(NLP)任务表现不佳,具体原因请见层归一化文章

批量正则化(Batch Normalization)

批量正则化(Batch Normalization,简称BN) 是一种对神经网络的输入数据进行规范化处理的方法,旨在加速网络训练、提高稳定性,并减少模型对初始化的依赖。在GAN中,批量正则化主要应用于生成器和判别器的训练过程中,以提高其收敛速度和训练的稳定性。

原理

在深度神经网络的训练中,随着层数的增加,网络的输入可能会经历很多次的线性变换和非线性激活。这样会导致每一层的输入分布发生变化,这个现象叫做内部协变量偏移(Internal Covariate Shift)。为了避免这种偏移,批量正则化通过对每一层的输入进行标准化,使其均值为0,方差为1,从而加速训练并减少训练的不稳定性。

批量正则化的主要步骤是:

  1. 计算当前批次数据的均值和方差
  2. 用均值和方差对数据进行标准化,使得每个输入特征的均值为0,方差为1。
  3. 使用可学习的缩放因子和偏移量,将标准化后的数据恢复到一定的分布范围内。

公式上,批量正则化的过程是:

[
\hat{x} = \frac{x - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}
]
其中:

  • ( x ) 是当前批次的数据。
  • ( \mu_B ) 和 ( \sigma_B^2 ) 分别是当前批次数据的均值和方差。
  • ( \epsilon ) 是一个很小的常数,用于避免除以零的情况。

然后,标准化后的数据会经过两个可学习的参数:缩放因子(( \gamma ))和偏移量(( \beta )),以便网络能够根据需要恢复原来的数据分布。

在GAN中的作用

  • 提高训练稳定性:由于批量正则化对输入数据进行了标准化,网络训练过程中的梯度更新更加稳定,避免了梯度消失或爆炸的问题。
  • 加速收敛:通过标准化输入数据,使得训练过程更快速地收敛,减少了学习率调节的难度。
  • 缓解模式崩溃:批量正则化有助于防止生成器只生成某一类样本的模式崩溃问题,因为它提供了更为平滑的训练过程,有助于生成器学习更多样的样本。

然而,批量正则化在生成器中的使用会带来一些额外的挑战,尤其是在生成高分辨率图像时,它的效果并不总是理想。因此,研究者们提出了其他的优化方法,像**谱归一化(Spectral Normalization)**等。

|