"> "> 深度学习-生成对抗网络 | Yufei Luo's Blog

深度学习-生成对抗网络

简介

生成式对抗网络(Generative Adversarial Network,GAN)基于博弈论场景,共包含两部分:判别器和生成器,其中生成器网络需要与一个判别器网络进行竞争。生成器网络\(g\)直接产生样本\(\boldsymbol{x}=g(\boldsymbol{z};\boldsymbol{\theta^{(g)}})\)\(\boldsymbol{\theta^{(g)}}\)指生成器网络的参数),其中\(\boldsymbol{z}\)为一个向量,通常是按照均匀分布或者正态分布生成的随机噪声;而它的对手判别器网络\(d\)则试图区分从训练数据抽取的样本和从生成器抽取的样本,判别器计算得到由\(d(\boldsymbol{x};\boldsymbol{\theta}^{(d)})\)\(\boldsymbol{\theta}^{(d)}\)为判别器网络的参数)给出的概率值,指示\(\boldsymbol{x}\)是真实训练样本而不是从模型抽取的伪造样本的概率。

img

GAN网络的目标函数如下: \[ \arg \min_{g}\max_{d} V(\boldsymbol{\theta}^{(g)},\boldsymbol{\theta}^{(d)}) \] 其中\(V(\boldsymbol{\theta}^{(g)},\boldsymbol{\theta}^{(d)})\)的表达式为: \[ V(\boldsymbol{\theta}^{(g)},\boldsymbol{\theta}^{(d)})=E_{\boldsymbol{x}\sim P_{data}}[\log d(\boldsymbol{x})]+E_{\boldsymbol{x}\sim P_{G}}[\log (1-d({\boldsymbol{x}}))] \]

或者也可以理解为生成网络的损失为\(\log (1-d(g(\boldsymbol{z})))\),而判别网络的损失为\(-\log (1-d(g(\boldsymbol{z})))-\log (d(\boldsymbol{x}))\)。我们可以从这一表达式解释对抗,判别器去试图学习将样本正确地分类为真的或者伪造的,即希望\(d(\boldsymbol{x})\)接近于1,而\(d(g(\boldsymbol{z}))\)接近于0;同时生成器试图欺骗分类器让其相信样本是真实的,它试图使得\(d(g(\boldsymbol{z}))\)接近于1。在训练过程中,二者互相对抗,构成一个动态的博弈过程。

在网络收敛时,会达成如下的纳什均衡局面:生成器的样本与实际数据不可区分,也即\(P_{data}\)\(P_{G}\)的分布完全相同,此时判别器处处输出\(\frac{1}{2}\)。然后,就可以丢掉判别器,留下生成器作为最终得到的生成模型。

数学原理

假设我们数据集的概率分布是\(P_{data}(\boldsymbol{x})\),而我们定义生成网络\(G\)所对应的概率分布为\(P_{G}(\boldsymbol{x})\)。生成网络的学习目标则是要让\(P_{G}(\boldsymbol{x})\)\(P_{data}(\boldsymbol{x})\)尽可能地接近,或者说使二者的差异尽可能地小。因此,生成网络的训练目标可以写成: \[ G^*=\arg \min_{G}\text{Diff}(P_{data}||P_{G}) \] 其中,\(\text{Diff}(P_{data}||P_{G})\)是衡量两个概率分布之间差异的函数。

但是在上式中,\(P_G\)\(P_{data}\)的公式都是未知的,因此这里需要借助一个判别器\(D\)去判断二者的差异。我们可以从\(P_{data}\)\(P_G\)两个分布中采集一些样本出来,对于\(P_{data}\),我们可以从数据集中直接抽样出一些样本;而对于\(P_G\),则是生成一些随机向量,然后让生成器根据这些随机向量去生成一些样本。接下来,便可以让判别器去计算\(P_G\)\(P_{data}\)的相似程度,判别器的目标函数\(V(G,D)\)为: \[ V(G,D)= E_{\boldsymbol{x}\sim P_{data}(\boldsymbol{x})}[\log D(\boldsymbol{x})]+E_{\boldsymbol{x}\sim P_{G}(\boldsymbol{x})}[\log(1-D(\boldsymbol{x}))] \] 也就是说,判别器的优化目标为: \[ \begin{aligned} D^*=&\arg\max_{D} \left\{ E_{\boldsymbol{x}\sim P_{data}(\boldsymbol{x})}[\log D(\boldsymbol{x})]+E_{\boldsymbol{x}\sim P_{G}(\boldsymbol{x})}[\log(1-D(\boldsymbol{x}))]\right\} \\ =&\arg\max_{D} \sum_{i=1}^{N} \left[ P_{data}(\boldsymbol{x}_i)\log D(\boldsymbol{x}_i)+P_{G}(\boldsymbol{x}_i)\log(1-D(\boldsymbol{x}_i)) \right] \end{aligned} \] 我们将\(P_{data}\)表示为\(a\)\(P_G\)表示为\(b\),则上式累加号内的部分可简写为: \[ f(D)=a\log(D)+b\log(1-D) \] 对齐求导,并令其结果等于0,可得: \[ D^*=\frac{a}{a+b}=\frac{P_{data}}{P_{data}+P_{G}} \] 而将其代回到判别器的目标函数中,可得: \[ \begin{aligned} &\sum_{i=1}^{N} \left[ P_{data}(\boldsymbol{x}_i)\log D(\boldsymbol{x}_i)+P_{G}(\boldsymbol{x}_i)\log(1-D(\boldsymbol{x}_i)) \right] \\ =&\sum_{i=1}^{N} \left[ P_{data}(\boldsymbol{x}_i)\log \frac{P_{data}}{P_{data}+P_{G}}+P_{G}(\boldsymbol{x}_i)\log\frac{P_{G}}{P_{data}+P_{G}} \right] \\ =&\sum_{i=1}^{N} \left[ P_{data}(\boldsymbol{x}_i)\log \frac{P_{data}}{(P_{data}+P_{G})/2}+P_{G}(\boldsymbol{x}_i)\log\frac{P_{G}}{(P_{data}+P_{G})/2} -2\log 2 \right] \\ =& -2\log 2+KL(P_{data}||\frac{P_{data}+P_{G}}{2})+KL(P_{G}||\frac{P_{data}+P_{G}}{2}) \\ =& -2\log 2+JSD(P_{data}||P_G) \end{aligned} \] 即最大化判别器的目标函数的过程,就是求解分布\(P_{data}\)\(P_G\)的Jenson-Shannon Divergence的过程。

综上,我们可以整理得到GAN网络的数学原理如下:

  1. 我们的目标是找到一个最优生成器\(G^*\),使得\(P_{data}\)\(P_G\)的差异最小,即:\(G^*=\arg \min_{G}\text{Diff}(P_{data}||P_{G})\)
  2. 但是由于我们不知道\(P_{data}\)\(P_G\)的具体表达式,因此需要借助一个判别器\(D\)来计算两个分布之间的差异:\(D^*=\arg\max_{D}V(D,G)\)
  3. 因此,最终的优化目标为:\(G^*=\arg\min_{G}\max_{D}V(D,G)\)

训练过程

根据上面的推导,在初始化生成器与判别器的参数\(\boldsymbol{\theta^{(g)}}\)\(\boldsymbol{\theta^{(d)}}\)之后,网络的训练可以按照如下步骤进行:

  1. 从数据集\(P_{data}\)中抽样出\(m\)个样本\(\{\boldsymbol{x}_1,\boldsymbol{x}_2,\dots,\boldsymbol{x}_m\}\),其中\(m\)为超参数,需要自己调整
  2. 从一个概率分布(均匀分布、高斯分布等)随机生成\(m\)个向量\(\{\boldsymbol{z}_1,\boldsymbol{z}_2,\dots,\boldsymbol{z}_m\}\),并以此作为生成器的输入,生成\(m\)个假数据\(\{\hat{\boldsymbol{x}}_1,\hat{\boldsymbol{x}}_2,\dots,\hat{\boldsymbol{x}}_m\}\),其中\(\hat{\boldsymbol{x}}_i=g(\boldsymbol{z}_i)\)
  3. 根据判别器的损失函数,计算判别网络中参数的梯度\(\nabla_{\boldsymbol{\theta^{(d)}}}\frac{1}{m}\sum_{i=1}^{m}[-\log (1-d(\hat{\boldsymbol{x}_i}))-\log (d(\boldsymbol{x}_i))]\),并更新判别器的参数

步骤1~3为判别器的训练过程,这一部分可重复多次

  1. 从一个概率分布(均匀分布、高斯分布等)随机生成\(m\)个向量\(\{\boldsymbol{z}_1,\boldsymbol{z}_2,\dots,\boldsymbol{z}_m\}\),这里随机生成的向量无需与第2步保持一致
  2. 根据生成器的损失函数,计算生成网络中参数的梯度\(\nabla_{\boldsymbol{\theta^{(g)}}}\frac{1}{m}\sum_{i=1}^{m}[\log (1-d(g({\boldsymbol{z}_i})))]\),并以此更新生成器的参数

步骤4、5为生成器的训练过程,这一部分只需要做一次

经典变种

DCGAN

相比于原始的GAN,DCGAN的主要改进是在网络结构上,它使用两个卷积神经网络作为生成器和判别器。同时也对卷积神经网络的结构做了一些改变,以提高样本的质量和收敛的速度,主要的改进有:

  • 取消所有的池化层,生成网络中使用转置卷积进行上采样,而判别网络中通过改变卷积操作的步长来代替池化
  • 在生成网络和判别网络中都使用BatchNorm
  • 去掉了全连接层,网络变为全卷积网络
  • 在生成网络中使用ReLU激活函数(最后一层用tanh),而判别网络使用LeakyReLU作为激活函数

在DCGAN的研究中,生成网络的输入信号\(z\)可以被看作是生成图像的另一种表示。文章中通过对图片A的输入\(z_A\)和图片B的输入\(z_B\)之间做插值,可以让生成的图像自然地从A过渡到B。同时,通过对输入信号\(z\)进行向量运算,也可以得到对应含义的图像。例如\(z_1\)对应的图像为露出笑容的女性,\(z_2\)对应的图像为女性,\(z_3\)对应的图像为男性,那么\(z_1-z_2+z_3\)可以得到露出笑容的男性图像。

WGAN

简单地说,原始的GAN存在以下两个问题:

  1. 最原始的损失函数是:\(-E_{\boldsymbol{x}\sim P_{data}(\boldsymbol{x})}[\log D(\boldsymbol{x})]-E_{\boldsymbol{x}\sim P_{G}(\boldsymbol{x})}[\log(1-D(\boldsymbol{x}))]\)。对于这一损失函数来说,判别器越好,生成器梯度消失越严重。

    根据原始GAN定义的判别器损失函数,当判别器越好时,最终的结果也就越近似于最大化\(p_G\)\(p_{data}\)之间的JS散度。而问题就出在这里,我们希望优化JS散度使得\(p_G\)\(p_{data}\)之间越来越接近,这个希望在两个分布有所重叠的时候是有可能实现的。但是如果两个分布完全没有重叠部分,或者重叠部分可以忽略,它们的JS散度便接近于定值\(\log 2\),而这也意味着此时的梯度接近于0。此时,生成器无法得到梯度信息,也就无法被优化。

    而实际上,\(p_G\)\(p_{data}\)之间不重叠或者重叠部分可忽略的可能性非常大。考虑到GAN中的生成器一般是从某个低维(比如100维)的随机分布中采样出一个编码向量,再经过一个神经网络生成出一个高维样本(比如64x64的图片就有4096维)。当生成器的参数固定时,生成样本的概率分布虽然是定义在4096维的空间上,但它本身所有可能产生的变化已经被那个100维的随机分布限定了,其本质维度就是100。再考虑到神经网络带来的映射降维,最终可能比100还小,所以生成样本分布的支撑集就在4096维空间中构成一个最多100维的低维流形(即高维空间中的曲线、曲面概念的推广),“撑不满”整个高维空间。而这也就会导致真实分布与生成分布所对应的流形几乎很难有不可忽略的重叠部分。

  2. 而另外一种损失函数(也被称为-log D trick)的表达式为:\(-E_{\boldsymbol{x}\sim P_{data}(\boldsymbol{x})}[\log D(\boldsymbol{x})]-E_{\boldsymbol{x}\sim P_{G}(\boldsymbol{x})}[-\log D(\boldsymbol{x})]\)

    对这一损失函数来说,优化\(E_{\boldsymbol{x}\sim P_{G}(\boldsymbol{x})}[-\log D(\boldsymbol{x})]\)等价于最小化\(KL(P_{G}||P_{data})-2JS(P_{data}||P_{G})\)。这一等价最小化目标存在两个严重的问题,一是要同时最小化生成分布与真实分布的KL散度,却又要最大化二者的JS散度,这会导致梯度不稳定。

    同时,KL散度是一种非对称的衡量标准。如果\(P_{G}\rightarrow 0,P_{data}\rightarrow 1\),则KL散度的值接近于0;而如果\(P_{G}\rightarrow 1,P_{data}\rightarrow 0\),则KL散度的值接近于无穷。因此,生成器宁可去多生成一些重复但是比较“安全”的样本,也不愿意去生成多样性的样本。这就产生了模式坍缩的现象。

因此,WGAN使用了Wasserstein距离。Wasserstein距离又叫Earth-Mover距离,它的定义如下: \[ W(P_1,P_2)=\inf_{\gamma\sim \Pi(P_1,P_2)}E_{(\boldsymbol{x},\boldsymbol{y})\sim \gamma}[||\boldsymbol{x}-\boldsymbol{y}||] \] 其中,\(\Pi(P_1,P_2)\)\(P_1\)\(P_2\)组合起来的所有可能的联合分布的集合,\(\gamma\)代表其中可能的联合分布,\((\boldsymbol{x},\boldsymbol{y})\)则是从这个联合分布中得到的采样,\(||\boldsymbol{x}-\boldsymbol{y}||\)代表这对样本的距离。因此,可以计算该联合分布\(\gamma\)下样本对距离的期望值\(E_{(\boldsymbol{x},\boldsymbol{y})\sim \gamma}[||\boldsymbol{x}-\boldsymbol{y}||]\)。在所有可能的联合分布中,能够对这个期望值取得的下界(\(\inf\)符号在数学上代表取下界),就定义为Wasserstein距离。

直观上来讲,\(E_{(\boldsymbol{x},\boldsymbol{y})\sim \gamma}[||\boldsymbol{x}-\boldsymbol{y}||]\)可以理解为在\(\gamma\)这个“路径规划”下,将\(P_1\)这堆“沙土”挪动到\(P_2\)位置所需的“消耗”,而\(W(P_1,P_2)\)就是“最优路径规划”下的“最小消耗”,因此叫Earth-Mover距离。

相比于KL散度和JS散度,Wasserstein距离的优越性在于,即使两个分布没有重叠,它仍然可以反映它们的远近。

但是Wasserstein距离中的\(\inf_{\gamma\sim \Pi(P_1,P_2)}\)部分无法直接求解,通过一定的数学变换可以将其变为下面的形式: \[ W(P_1,P_2)=\frac{1}{K}\sup_{||f||_{L}\le K} E_{\boldsymbol{x}\sim P_1}[f(\boldsymbol{x})]-E_{\boldsymbol{x}\sim P_2}[f(\boldsymbol{x})] \] 其中,\(||f||_{L}\)指的是函数\(f\)的Lipschitz常数,而\(K\)为某个特定的常数值。

附:Lipschitz连续与Lipschitz常数的概念

对于一个连续函数\(f\),如果存在一个常数\(K\ge 0\),使得定义域内的任意两个元素\(\boldsymbol{x}_1\)\(\boldsymbol{x}_2\),都满足 \[ |f(\boldsymbol{x}_1)-f(\boldsymbol{x}_2)|\le K|\boldsymbol{x}_1-\boldsymbol{x}_2| \] 那么\(f\)为Lipschitz连续,\(K\)也被称为函数\(f\)的Lipschitz常数。

简单理解,如果\(f\)的定义域是一个实数集合,那么这一要求其实就等价于\(f\)一阶导函数的绝对值不超过\(K\)

上面这个公式的意思就是要求函数\(f\)的Lipschitz常数不超过\(K\)的条件下,对所有可以满足条件的\(f\)\(E_{\boldsymbol{x}\sim P_1}[f(\boldsymbol{x})]-E_{\boldsymbol{x}\sim P_2}[f(\boldsymbol{x})]\)的上界(\(\sup\)在数学上代表取上界的意思)。如果我们用一组参数\(\boldsymbol{w}\)来定义一系列可能的函数\(f_{\boldsymbol{w}}\),此时\(W(P_1,P_2)\)就可以近似变成求解如下形式: \[ K\cdot W(P_1,P_2)\approx \max_{\boldsymbol{w}:||f_{\boldsymbol{w}}||_{L}\le K} E_{\boldsymbol{x}\sim P_1}[f_{\boldsymbol{w}}(\boldsymbol{x})]-E_{\boldsymbol{x}\sim P_2}[f_{\boldsymbol{w}}(\boldsymbol{x})] \] 对于公式中的\(||f_{\boldsymbol{w}}||_{L}\le K\)这一限制,我们其实不用关心具体的\(K\)值,因此它仅仅相当于对\(f_{\boldsymbol{w}}\)的梯度进行限制,而并不影响梯度的方向。一个简单的办法是限制\(f_{\boldsymbol{w}}\)中所有的参数\(w_i\in[-c,c]\),此时\(f_{\boldsymbol{w}}\)的一阶导数也不会超过某个范围,所以一定存在一个\(K\)值满足Lipschitz条件。而在具体算法实现中,只需要在每次更新完参数之后,将\(\boldsymbol{w}\)裁剪到这个范围即可。

而对于GAN而言,\(f_{\boldsymbol{w}}\)就对应于参数为\(\boldsymbol{w}\)的神经网络。根据上述的推导,我们可以构造一个参数为\(\boldsymbol{w}\)、最后一层不是非线性激活层的判别网络\(f_{\boldsymbol{w}}\),在限制\(w_i\in \boldsymbol{w}\)不超过某个范围的情况下,使得 \[ L=E_{\boldsymbol{x}\sim P_{data}}[f_{\boldsymbol{w}}(\boldsymbol{x})]-E_{\boldsymbol{x}\sim P_G}[f_{\boldsymbol{w}}(\boldsymbol{x})] \] 尽可能地取最大值。此时\(L\)就会近似为真实分布与生成分布之间的Wasserstain距离。需要注意的是,由于\(f_{\boldsymbol{w}}\)做的是近似拟合Wasserstein距离,属于回归任务,因此需要将网络最后一层的Sigmoid激活层去掉。

由于Wasserstein距离的优良性质,我们无需担心生成器梯度消失的问题。因此,我们可得到WGAN生成器的损失函数为\(L_{G}=-E_{\boldsymbol{x}\sim P_G}[f_{\boldsymbol{w}}(\boldsymbol{x})]\),判别器的损失函数为\(L_{D}=-E_{\boldsymbol{x}\sim P_{data}}[f_{\boldsymbol{w}}(\boldsymbol{x})]+E_{\boldsymbol{x}\sim P_G}[f_{\boldsymbol{w}}(\boldsymbol{x})]\)

总结上述内容,WGAN与原始GAN相比,修改了下面四点:

  1. 判别器最后一层去掉Sigmoid
  2. 生成器和判别器的损失函数不再取对数
  3. 每次更新判别器的参数之后把它们的绝对值截断到不超过一个固定常数\(c\)
  4. (作者从实际实验中发现)判别器损失函数的梯度不稳定,因此不适合用Adam这类基于动量的优化算法,因此作者推荐使用RMSProp或者SGD

而WGAN网络的训练可以按照如下步骤进行:

  1. 从数据集\(P_{data}\)中抽样出\(m\)个样本\(\{\boldsymbol{x}_1,\boldsymbol{x}_2,\dots,\boldsymbol{x}_m\}\),其中\(m\)为超参数,需要自己调整
  2. 从一个概率分布(均匀分布、高斯分布等)随机生成\(m\)个向量\(\{\boldsymbol{z}_1,\boldsymbol{z}_2,\dots,\boldsymbol{z}_m\}\),并以此作为生成器的输入,生成\(m\)个假数据\(\{\hat{\boldsymbol{x}}_1,\hat{\boldsymbol{x}}_2,\dots,\hat{\boldsymbol{x}}_m\}\),其中\(\hat{\boldsymbol{x}}_i=g(\boldsymbol{z}_i)\)
  3. 根据判别器的损失函数,计算判别网络中参数的梯度\(\nabla_{\boldsymbol{w}}\frac{1}{m}\sum_{i=1}^{m}[f_{\boldsymbol{w}}(\hat{\boldsymbol{x}_i})-f_{\boldsymbol{w}}(\boldsymbol{x}_i)]\),并更新判别器的参数
  4. 将更新后的参数裁剪到\([-c,c]\)的范围内

步骤1~4为判别器的训练过程,这一部分可重复多次

  1. 从一个概率分布(均匀分布、高斯分布等)随机生成\(m\)个向量\(\{\boldsymbol{z}_1,\boldsymbol{z}_2,\dots,\boldsymbol{z}_m\}\),这里随机生成的向量无需与第2步保持一致

  2. 根据生成器的损失函数,计算生成网络中参数的梯度\(\nabla_{\boldsymbol{\theta^{(g)}}}\frac{1}{m}\sum_{i=1}^{m}[-f_{\boldsymbol{w}}(g({\boldsymbol{z}_i}))]\),并以此更新生成器的参数

步骤5、6为生成器的训练过程,这一部分只需要做一次

WGAN-GP

在WGAN中,将权重限制到一定范围之后,大多数的权重都会位于两个界限附近,这也就意味着网络的大部分权重只有两个可能的数,对于神经网络来说无法充分发挥其拟合能力,造成浪费。并且强制剪切权重也容易导致梯度消失或者梯度爆炸。

而WGAN-GP是WGAN之后的改进版,主要改进了Lipschitz连续性限制条件。在WGAN-GP中,使用了梯度惩罚的方式来满足Lipschitz条件,即判别器的损失函数变为: \[ -E_{\boldsymbol{x}\sim P_{data}}[f_{\boldsymbol{w}}(\boldsymbol{x})]+E_{\tilde{\boldsymbol{x}}\sim P_G}[f_{\boldsymbol{w}}(\tilde{\boldsymbol{x}})]+\lambda E_{\hat{\boldsymbol{x}}}[(||\nabla_{\hat{\boldsymbol{x}}}f_{\boldsymbol{w}}(\hat{\boldsymbol{x}})||_2 -1)^2] \] 其中,\(\hat{\boldsymbol{x}}\)是通过对\(\boldsymbol{x}\)\(\tilde{\boldsymbol{x}}\)插值计算而得,\(\hat{\boldsymbol{x}}=\epsilon \boldsymbol{x}+(1-\epsilon)\tilde{\boldsymbol{x}}\)\(\epsilon\sim U[0,1]\)

优缺点

优点:

  1. 在实际工程中,GAN可以产生看上去比其他模型更好的样本。
  2. 这一网络框架可以训练任何一种生成器网络,对网络结构没有特殊要求。
  3. 无需利用马尔科夫链反复采样,无需在学习过程中进行推断,从而回避了近似计算概率的难题。
  4. 与Pixel RNN相比,生成一个样本所需的时间更短;与VAE相比,没有引入任何决定性的偏置,而变分方法优化的是对数似然的下界,这最终导致VAE生成的实例更模糊;相比玻尔兹曼机、生成随机网络等模型,它的样本可以一次生成,无需反复使用马尔科夫链运算器。

缺点:

  1. 模型的收敛比较困难,判别器与生成器之间需要很好地同步。
  2. 在学习过程中可能会发生模式塌缩问题,即生成器退化,总是生成同样的样本点。GAN的变体WGAN可以一定程度上缓解这一问题。
  3. 可解释性差,生成模型的概率分布没有显式的表达式。

程序示例

下面的程序是使用WGAN生成动漫头像的示例,数据集来自:Anime-Face-Dataset数据集介绍 | 文艺数学君 (mathpretty.com)。在训练过程中,一些尺寸比较小的头像被删除,最终剩下3万多张头像用于模型的训练。

1
2
3
4
5
6
7
8
9
10
11
12
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from torch.optim import RMSprop
from torch.utils.data import DataLoader, Dataset
import os
import cv2
import numpy as np
from PIL import Image
from torchvision.datasets import ImageFolder
import matplotlib.pyplot as plt
1
2
3
4
5
6
7
dataset = ImageFolder(root='./generate/', transform=torchvision.transforms.Compose([
torchvision.transforms.Resize(64),
torchvision.transforms.CenterCrop(64),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5)),
]))
dataloader = DataLoader(dataset, batch_size=128)
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
class Generator(nn.Module):
def __init__(self, input_dim): #产生64*64的图片
super(Generator, self).__init__()
self.input_dim=input_dim
self.network=nn.Sequential(
nn.ConvTranspose2d(input_dim,512,4,1,0,bias=False), #512*4*4
nn.BatchNorm2d(512),
nn.LeakyReLU(inplace=True),
nn.ConvTranspose2d(512,256,4,2,1,bias=False), #256*8*8
nn.BatchNorm2d(256),
nn.LeakyReLU(inplace=True),
nn.ConvTranspose2d(256,128,4,2,1,bias=False), #128*16*16
nn.BatchNorm2d(128),
nn.LeakyReLU(inplace=True),
nn.ConvTranspose2d(128,64,4,2,1,bias=False), #64*32*32
nn.BatchNorm2d(64),
nn.LeakyReLU(inplace=True),
nn.ConvTranspose2d(64,3,4,2,1,bias=False), #3*64*64
nn.Tanh()
)

def forward(self, vec):
vec=vec.reshape((-1, self.input_dim, 1, 1))
fig_tensor=self.network(vec)
return fig_tensor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.network=nn.Sequential(
nn.Conv2d(3,64,4,2,1,bias=False), #输出为64*32*32
nn.LeakyReLU(inplace=True),
nn.Conv2d(64,128,4,2,1,bias=False), #输出为128*16*16
nn.BatchNorm2d(128),
nn.LeakyReLU(inplace=True),
nn.Conv2d(128,256,4,2,1,bias=False), #输出为256*8*8
nn.BatchNorm2d(256),
nn.LeakyReLU(inplace=True),
nn.Conv2d(256,512,4,2,1,bias=False), #输出为512*4*4
nn.BatchNorm2d(512),
nn.LeakyReLU(inplace=True),
nn.Conv2d(512,1,4,1,0,bias=False), #输出为1*1*1
)

def forward(self, x):
return self.network(x).reshape(-1,1)
1
2
3
4
5
6
7
8
9
10
# 学习率需要仔细调整
# 以动漫头像的生成为例:
# 如果学习率太低,生成的图片一直是噪点,训练多轮也不会出现任何图案
# 学习率适中,则训练开始时会先出现一些规则的格子状图案,接着训练则就会出现一些很模糊的头像轮廓。随着训练接着进行,轮廓细节会增加,且颜色也会变得丰富
# 学习率过高,则始终无法形成头像轮廓,输出图片一直是灰色中间夹杂着带有颜色的斑纹
LearningRate=2e-4
Device='cuda' if torch.cuda.is_available() else 'cpu'
#需要把握好判别网络和生成网络做梯度下降的次数比,在WGAN的原论文中,每训练5次判别器,训练一次生成器
#但是训练的时候发现生成网络形成规则图像所需的训练轮数过多,故将比例改为1:1,这样生成头像所需的轮数显著减小
NetGTrainStep=1
1
2
3
4
netG=Generator(256).to(Device)
netD=Discriminator().to(Device)
optimizerG=RMSprop(netG.parameters(),lr=LearningRate)
optimizerD=RMSprop(netD.parameters(),lr=LearningRate)
1
2
3
save_path='./GAN_examples/'+str(LearningRate)+','+str(NetGTrainStep)+'/'
if not os.path.exists(save_path):
os.mkdir(save_path)
1
fixed_noise=torch.randn((64, 256), device=Device)
1
2
3
img_list=[]
G_losses=[]
D_losses=[]
1
2
3
4
5
6
7
8
9
10
def show_and_save_figure(fig_array, show_figure=False, save_figure=False, save_path=save_path, save_figurename=None):
plt.figure(figsize=(15,15))
plt.subplot(1,1,1)
plt.axis('off')
plt.imshow(np.transpose(fig_array.cpu(), (1,2,0)))
if show_figure:
plt.show()
if save_figure:
plt.savefig(save_path+save_figurename)
plt.close()
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
for epoch in range(100):
total_loss_g=0.0
total_loss_d=0.0
num_batch=1
for data, _ in dataloader:
###############################
netD.zero_grad() #在计算G的梯度时,会对D的参数计算其梯度,故需要清空梯度
data=data.to(Device)
batchsize=data.size(0)
output=netD(data)
errD_real=torch.mean(output) #相当于真图片的输出值小,假图片的输出值大
errD_real.backward()

noise=torch.randn(batchsize, 256, device=Device)
fake=netG(noise)
output=netD(fake)
errD_fake=torch.mean(output)*(-1)
errD_fake.backward()
errD=errD_real+errD_fake
nn.utils.clip_grad_value_(netD.parameters(), 0.01)
optimizerD.step()

################################
if num_batch%NetGTrainStep==(NetGTrainStep-1):
netG.zero_grad() #清空上一步计算D的梯度时,反向传播到G上面的梯度
noise=torch.randn(batchsize, 256, device=Device)
fake=netG(noise)
output=netD(fake)
errG=torch.mean(output)
errG.backward()
nn.utils.clip_grad_value_(netG.parameters(), 0.01)
optimizerG.step()
total_loss_g+=errG.detach().cpu().numpy()*batchsize

################################
total_loss_d+=errD.detach().cpu().numpy()*batchsize
num_batch+=1

with torch.no_grad():
fake=netG(fixed_noise).detach().cpu()
img_list.append((torchvision.utils.make_grid(fake,padding=2,normalize=True)))
G_losses.append(total_loss_g/32647)
D_losses.append(total_loss_d/32647)
print('epoch:',epoch, ' lossG:',G_losses[-1],' lossD:',D_losses[-1])
show_and_save_figure(img_list[-1], save_figure=True, save_figurename=str(epoch)+'.jpg')

下面为WGAN生成的头像展示,学习率为0.0002:

GAN0
image-20210711122320249
image-20210711122329827
image-20210711122337090

上面的4幅图分别对应了WGAN网络在使用上面的代码(即学习率为0.0002,生成器训练1轮对应于判别器训练1轮)训练1,10,50,100轮之后的结果,训练的轮数较少时就能看到生成的图片中有明显的头像状图案。

在训练1轮之后,只能看到极其模糊的头像轮廓,而且图片的颜色比较单一;在训练10轮之后,头像仍然较模糊,但是已经可以看到不同颜色的头发和不同形状的脸部特征;而随着训练继续进行,生成的头像变得更加清晰,而且其中颜色更加丰富,但是也会看到部分头像的五官和脸部显得扭曲。

参考

  1. 深度学习,Goodfellow
  2. 通俗理解GAN(一):把GAN给你讲得明明白白 - 知乎 (zhihu.com)
  3. 生成对抗网络(GAN) - 知乎 (zhihu.com)
  4. 生成对抗网络(GAN) 背后的数学理论 - 知乎 (zhihu.com)
  5. GAN:生成式对抗网络介绍和其优缺点以及研究现状_Bixiwen_liu的博客-CSDN博客
  6. DCGAN的原文:1511.06434.pdf (arxiv.org)
  7. 令人拍案叫绝的Wasserstein GAN - 知乎 (zhihu.com)
  8. GAN的数学原理:1701.04862.pdf (arxiv.org)
  9. WGAN原文:1701.07875.pdf (arxiv.org)
  10. 【数学】Wasserstein Distance - 知乎 (zhihu.com)
  11. 关于WGAN-GP的理解 - 知乎 (zhihu.com)
  12. https://lilianweng.github.io/lil-log/2017/08/20/from-GAN-to-WGAN.html
  13. https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html
  14. https://github.com/martinarjovsky/WassersteinGAN
  15. https://www.zhihu.com/question/265241118