这篇文章将会教你怎样用机器学习来伪造假数据,题材还是人脸,以下六张人脸里面,有两张是假的,猜猜是哪两张😎?

生成假人脸使用的网络是对抗生成网络 (GAN - Generative adversarial network),这个网络与之前介绍的比起来相当特殊,虽然看起来不算复杂,但训练起来极其困难,以下将从基础原理开始一直讲到具体代码,还会引入一些之前没有讲过的组件和训练方法😨。
所谓生成网络就是用于生成文章,音频,图片,甚至代码等数据的机器学习模型,例如我们可以给出一个需求让网络生成一份代码,如果网络足够强大,生成的代码质量足够好并且能满足需求,那码农们就要面临失业了😱。当然,目前机器学习模型可以生成的数据比较有限并且质量都很一般,码农们的饭碗还是能保住一段时间的。
生成网络和普通的模型一样,要求有输入和输出,假设我们可以传入一些条件让网络生成符合条件的图片:
看起来非常好用,但训练这样的模型需要一个庞大的数据集,并且得一张张图片去标记它们的属性,实现起来会累死人。这篇文章介绍的对抗生成网络属于无监督学习,可以完全不需要给数据打标签,你只需要给模型认识一些真实数据,就可以让模型输出类似真实数据的假数据。对抗生成网络分为两部分,第一部分是生成器 (Generator),第二部分是识别器 (Discriminator),生成器负责根据随机条件生成数据,识别器负责识别数据是否为真。
训练对抗生成网络有两大目标,这两大目标是矛盾的,这就是为什么我们叫对抗生成网络:
生成器需要生成骗过识别器 (输出为真) 的数据
识别器需要不被生成器骗过去 (针对生成器生成的数据输出为假,针对真实数据输出为真)
对抗生成网络的训练流程大致如下,需要循环训练生成器和识别器:
简单通俗一点我们可以用造假皮包为例来理解,好理解了吧🤗:
和现实造假皮包一样,生成器会生成越来越接近真实数据的假数据,最后会生成和真实数据一模一样的数据,但这样反而就远离我们构建生成网络的目的了(不如直接用真实数据)。使用生成网络通常是为了达到以下的目的:
要求大量看上去是真的,但稍微不一样的数据
要求没有版权保护的数据 (假数据没来的版权🤒)
生成想要但是现实没有的数据 (需要更进一步的工作)
看以上的流程你可能会发现,因为对抗生成网络是无监督学习,不需要标签,我们只能给模型传入随机的条件来让它生成数据,模型生成出来的数据看起来可能像真的但不一定是我们想要的。如果我们想要指定具体的条件,则需要在训练完成以后分析随机条件对生成结果的影响,例如随机生成的第二个数字代表性别,第六个数字代表年龄,第八个数字代表头发的数量,这样我们就可以调整这些条件来让模型生成想要的图片。
还记得上一篇人脸识别的模型不?人脸识别的模型会把图片转换为某个长度的向量,训练完成以后这个向量的值会代表人物的属性,而这一篇是反过来,把某个长度的向量转换回图片,训练成功以后这个向量同样会代表人物的各个属性。当然,两种的向量表现形式是不同的,把人脸识别输出的向量交给对抗生成网络,生成的图片和原有的图片可能会相差很远,把人脸识别输出的向量还原回去的方法后面再研究吧🤕。
在第八篇介绍 CNN 的文章中,我们了解过卷积层运算 (Conv2d) 的实现原理,CNN 模型会利用卷积层来把图片的长宽逐渐缩小,通道数逐渐扩大,最后扁平化输出一个代表图片特征的向量:
而在对抗生成网络的生成器中,我们需要实现反向的操作,即把向量当作一个 (向量长度, 1, 1) 的图片,然后把长宽逐渐扩大,通道数 (最开始是向量长度) 逐渐缩小,最后变为 (3, 图片长度, 图片宽度) 的图片 (3 代表 RGB)。
实现反向操作需要反卷积层 (ConvTranspose2d),反卷积层简单的来说就是在参数数量相同的情况下,把输出大小的数据还原为输入大小的数据:
要理解反卷积层的具体运算方式,我们可以把卷积层拆解为简单的矩阵乘法:
可以看到卷积层计算的时候可以根据内核参数和输入大小生成一个矩阵,然后计算输入与这个矩阵的乘积来得到输出结果。
而反卷积层则会计算输入与转置 (Transpose) 后的矩阵的乘积得到输出结果:
可以看到卷积层与反卷积层的区别只在于是否转置计算使用的矩阵。此外,通道数量转换的计算方式也是一样的。
测试反卷积层的代码如下:
需要注意的是,不一定存在一个反卷积层可以把卷积层的输出还原到输入,这是因为卷积层的计算是不可逆的,即使存在一个可以把输出还原到输入的矩阵,这个矩阵也不一定有一个等效的反卷积层的内核参数。
接下来我们看一下生成器的定义,原始介绍 GAN 的论文给出了生成 64x64 图片的网络,而这里给出的是生成 80x80 图片的网络,其实区别只在于一开始的输出通道数量 (论文是 4, 这里是 5)
表现如下:
其中批次正规化 (BatchNorm) 用于控制参数值范围,防止层数过多 (后面会结合识别器训练) 导致梯度爆炸问题。
还有一个要点是生成器输出的范围会在 -1 ~ 1,也就是使用 -1 ~ 1 代表 0 ~ 255 的颜色值,这跟我们之前处理图片的时候把值除以 255 使得范围在 0 ~ 1 不一样。使用 -1 ~ 1 可以提升输出颜色的精度 (减少浮点数的精度损失)。
我们再看以下识别器的定义,基本上就是前面生成器的相反流程:
看到这里你可能会有几个疑问:
为什么用 LeakyReLU: 这是为了防止层数叠加次数过多导致的梯度消失问题,参考第三篇,LeakyReLU 对于负数输入不会返回 0,而是返回 <code>输入 * slope</code>,这里的 <code>slope</code> 指定为 0.2
为什么第一层不加批次正规化 (BatchNorm): 原有论文中提到实际测试中,如果在所有层添加批次正规化会让模型训练结果不稳定,生成器的最后一层和识别器的第一层拿掉以后效果会好一些
为什么不加池化层: 添加池化层以后可逆性将会降低,例如识别器针对假数据返回接近 0 的数值时,判断哪些部分导致这个输出的依据会减少
接下来就是训练生成器和识别器,生成器和识别器需要分别训练,训练识别器的时候不能动生成器的参数,训练生成器的时候不能动识别器的参数,使用的代码大致如下:
上述例子应该可以帮助你理解大致的训练流程和只训练识别器或生成器的方法,但是直接这么做效果会很差🤕,接下来我们会看看对抗生成网络的问题,并且给出优化方案,后面的完整代码会跟上述例子有一些不同。
如果对原始论文有兴趣可以参考这里,原始的对抗生成网络又称 DCGAN (Deep Convolutional GAN)。
看完以上的内容你可能会觉得,嘿嘿,还是挺简单的。不🤕,虽然原理看上去挺好理解,模型本身也不复杂,但对抗生成网络是目前介绍过的模型里面训练难度最高的,这是因为对抗生成网络建立在矛盾上,没有一个明确的目标 (之前的模型目标都是针对未学习过的数据预测正确率尽可能接近 100%)。如果生成器生成 100% 可以骗过识别器的数据,那可能代表识别器根本没正常工作,或者生成器生成的数据跟真实数据 100% 相同,没实用价值;而如果识别器 100% 可以识别生成器生成的数据,那代表生成器生成的数据太垃圾,一个都骗不过。本篇介绍的例子使用了最蠢最简单的方法,把每一轮学习后生成器生成的数据输出到硬盘,然后人工鉴定生成的效果怎样🤒,同时还会每 100 轮训练记录一次模型状态,供训练完以后回滚使用 (最后一个模型状态效果不会是最好的,后面会说明)。
另一个问题是识别器和生成器不能同时训练,怎样安排训练过程对训练结果的影响非常大😮,理想的过程是:识别器稍微领先生成器,生成器跟着识别器慢慢的生成越来越精准的数据。举例来说,识别器首先会识别肤色占比较多的图片为人脸,接下来生成器会生成全部都是肤色的图片,然后识别器会识别有两个看上去是眼睛的图片为人脸,接下来生成器会加上两个看上去是眼睛的形状到图片,之后识别器会识别带有五官的图片为人脸,接下来生成器会加上剩余的五官到图片,最后识别器会识别五官和脸形状比较正常的人为人脸,生成器会尽量调整五官和人脸形状接近正常水平。而不理想的过程是识别器大幅领先生成器,例如识别器很早就达到了接近 100% 的正确率,而生成器因为找不到学习的方向正确率会一直原地踏步;另一个不理想的过程是生成器领先识别器,这时会出现识别器找不到学习的方向,生成器也找不到学习的方向而原地转的情况。实现识别器稍微领先生成器,可以增加识别器的训练次数,常见的方法是每训练 n 次识别器就训练 1 次生成器,而本文后面会介绍根据正确率动态调整识别器和生成器学习次数的方法,参考后面的代码吧。
对抗生成网络最大的问题是模式崩溃 (Mode Collapse) 问题,这个问题所有训练对抗生成网络的人都会面对,并且目前没有 100% 的方法避免😭。简单的来说就是生成器学会偷懒作弊,只会输出一到几个与真实数据几乎一模一样的虚假数据,因为生成的数据同质化非常严重,即使可以骗过识别器也没什么实用价值。发生模式崩溃以后的输出例子如下,可以看到很多人脸都非常接近:
为了尽量避免模式崩溃问题,以下几个改进的模型被发明了出来,这就是人民群众的智慧啊😡。
模式崩溃问题的原因之一就是部分模型参数会随着训练固化 (达到本地最优),因为原始的对抗生成网络会让识别器输出尽可能接近 1 或者 0 的值,如果值已经是 0 或者 1 那么参数就不会被调整。WGAN (Wasserstein GAN) 的解决方式是不限制识别器输出的值范围,只要求识别器针对真实数据输出的值大于虚假数据输出的值,和要求生成器生成可以让识别器输出更大的值的数据。
第一个修改是拿掉识别器最后的 Sigmoid,这样识别器输出的值就不会限制在 0 ~ 1 的范围内。
第二个修改是修改计算损失的方式:
这么修改以后会出现一个问题,识别器输出的值范围会随着训练越来越大 (生成器提高虚假数据的输出值,接下来识别器提高真实数据的输出值,循环下去输出值就会越来越大😱),从而导致梯度爆炸问题。为了解决这个问题 WGAN 对识别器参数的可取范围做出了限制,也就是在调整完参数以后裁剪参数,第三个修改如下:
如果有兴趣可以参考 WGAN 的原始论文,里面一大堆数学公式可以把人吓坏😱,但主要的部分只有上面提到的三点。
WGAN 为了防止梯度爆炸问题对识别器参数的可取范围做出了限制,但这个做法比较粗暴,WGAN-GP (Wasserstein GAN Gradient Penalty) 提出了一个更优雅的方法,即限制导函数值的范围,如果导函数值偏移某个指定的值则通过损失给与模型惩罚。
具体实现如下,看起来比较复杂但做的事情只是计算识别器输入数据的导函数值,然后判断所有通道合计的导函数值的 L2 合计与常量 1 相差多少,相差越大就返回越高的损失,这样识别器模型参数自然会控制在某个水平。
然后再修改计算识别器损失的方法:
最后把识别器中的批次正规化 (BatchNorm) 删掉或者改为实例正规化 (InstanceNorm) 就完了。InstanceNorm 和 BatchNorm 的区别在于计算平均值和标准差的时候不会根据整个批次计算,而是只根据各个样本自身计算,关于 BatchNorm 的计算方式可以参考第四篇。
如果有兴趣可以参考 WGAN-GP 的原始论文。
又到完整代码的时间了🤗,这份代码同时包含了原始的 GAN 模型 (DCGAN),WGAN 和 WGAN-GP 的实现,后面还会比较它们之间的效果相差多少。
使用的数据集链接如下,前一篇的人脸识别文章也用到了这个数据集:
https://www.kaggle.com/atulanandjha/lfwpeople
需要注意的是人脸图片数量越多就越容易出现模式崩溃问题,这也是对抗生成网络训练的难点之一🤒,这份代码只会随机选取 2000 张图片用于训练。
这份代码还会根据正确率动态调整生成器和识别器的训练比例,如果识别器比生成器更强则训练 1 次生成器,如果生成器比识别器更强则训练 5 次识别器,这么做可以省去手动调整训练比例的麻烦,经实验效果也不错🥳。
保存代码到 <code>gan.py</code>,然后执行以下命令即可开始训练:
同样训练 2000 轮以后,DCGAN, WGAN, WGAN-GP 输出的样本如下:
DCGAN
WGAN
WGAN-GP
可以看到 WGAN-GP 受模式崩溃问题影响最少,并且效果也更好😤。
WGAN-GP 训练到 3000 次以后输出的样本如下:
WGAN-GP 训练到 10000 次以后输出的样本如下:
随着训练次数增多,WGAN-GP 一样无法避免模式崩溃问题,这就是为什么以上代码会记录每一轮训练后输出的样本,并在每 100 轮训练以后保存单独的模型状态,这样训练结束以后我们可以通过评价输出的样本找到效果最好的批次,然后使用该批次的模型状态。
上述的例子效果最好的状态是训练 3000 次以后的状态。
你可能发现输出的样本中夹杂了一些畸形🥴,这是因为生成器没有覆盖到输入的向量空间,最主要的原因是随机输入中包含了很多接近 0 的值,避免这个问题简单的做法是生成随机输入时限制值必须小于或大于某个值。原则上给反卷积层设置 Bias 也可以避免这个问题,但会更容易陷入模式崩溃问题。
使用训练好的模型生成人脸就比较简单了:
额外的,我做了一个可以动态调整参数捏脸的网页,html 代码如下:
保存到 <code>gan_eval.html</code> 以后执行以下命令即可启动服务器:
浏览器打开 <code>http://localhost:8666</code> 以后会显示以下界面,点击随机生成按钮可以随机生成人脸,拉动左边的参数条可以动态调整参数:
一些捏脸的网站会分析各个参数的含义,看看哪些参数代表肤色,那些参数代表表情,哪些参数代表脱发程度,我比较懒就只给出各个参数的序号了🤒。
又摸完一个新的模型了,跟到这篇的人也越来越少了,估计这个系列再写一两篇就会结束 (VAE, 强化学习)。
前一篇论文我提到了可能会开一个新的系列介绍 .NET 的机器学习,但我决定不开了。经过试验发现没有达到可用的水平,文档基本等于没有,社区气氛也不行 (大会 PPT 倒是做的挺好的)。毕竟语言只是个工具,不是老祖宗,还是看开一点吧。学 python 再做机器学习会轻松很多,就像长远来说学一点基础英语再编程比完全只用中文编程 (先把基础框架类库系统接口的英文全部翻译成中文,再用中文写) 简单很多,对叭😎。