所谓纳什均衡,指的是参与人的这样一种策略组合,在该策略组合上,任何参与人单独改变策略都不会得到好处。换句话说,如果在一个策略组合上,当所有其他人都不改变策略时,没有人会改变自己的策略,则该策略组合就是一个纳什均衡。
市场上有2家企业A和B,都是卖纸的,纸的成本都是2元钱,A和B都卖5块钱。在最开始,A、B企业都是盈利3块,这种状态叫”社会最优解(Social optimal solution)“。但问题是,社会最优解是一个不稳定的状态,就如同下图中这个优化曲面上那个红球点一样,虽然该小球目前处于曲面最高点,但是只要施加一些轻微的扰动,小球就会立刻向山下滑落:
现在企业A和B准备开展商业竞争:
但如果价格战一直这样打下去,这个过程显然不可能无限迭代下去。当A和B都降价到了3块时,双方都达到了成本的临界点,既不敢涨价,也不敢降价。涨价了市场就丢了,降价了,就赚不到钱甚至赔钱。所以A和B都不会再去做改变,这就是纳什均衡。
A和B怎样能够获得最大利润呢,就是A和B坐到一起商量,同时把价格提高,这就叫共谋,但法律为了保障消费者利益,禁止共谋。补充一句,共谋在机器学习中被称作”模型坍塌“,指的对对抗的模型双方都进入了一个互相认可的局部最优区而不再变化,具体的技术细节我们后面会讨论。
囚徒困境是说:有两个小偷集体作案,然后被警察捉住。警察对两个人分别审讯,并且告诉他们政策:
两个人的收益情况如下所示:
因为A和B是不能互相通信的,因此这是一个静态不完全信息博弈,我们分别考虑双方的决策面:
因此最终纳什均衡点在两个人都坦白,各判八年这里。
显然,集体最优解在两个人都抗拒,这样一来每个人都判一年就出来了。但是,纳什均衡点却不在这里。而且,在纳什均衡点上,任何一个人都没有改变自己决策的动力。因为一旦单方面改变决策,那个人的收益就会下降。
我们知道,在国内开车夹塞很常见。如果大家都不夹塞,是整体的最优解,但是按照纳什均衡理论,任何一个司机都会考虑,无论别人是否夹塞,我夹塞都可以使自己的收益变大。于是最终大家都会夹塞,加剧拥堵,反而不如大家都不加塞走的快。
那么,有没有办法使个人最优变成集体最优呢?方法就是共谋。两个小偷在作案之前可以说好,咱们如果进去了,一定都抗拒。如果你这一次敢反悔,那么以后道上的人再也不会有人跟你一起了。也就是说,在多次博弈过程中,共谋是可能的。但是如果这个小偷想干完这一票就走,共谋就是不牢靠的。
在社会领域,共谋是靠法律完成的。大家约定的共谋结论就是法律,如果有人不按照约定做,就会受到法律的惩罚。通过这种方式保证最终决策从个人最优的纳什均衡点变为集体最优点。
另外一方面,现在很多汽车厂商提出了车联网的概念,在路上的每一辆车都通过物联网连成一个临时网络,所有车按照一个最优的协同算法共同协定最优的行车路线、行车速度、路口等待等行为,这样整体交通可以达到一个整体最优,所有人都节省了时间。
彼此痛恨的甲、乙、丙三个枪手准备决斗,他们各自的水平如下:
首先明确一点,这是一个静态不完全信息博弈,每个抢手在开枪前都不知道其他对手的策略,只能在猜测其他对手策略的基础上,选择对自己最优的策略。
我们来分析一下第一轮枪战各个枪手的策略。
第一轮枪战过后,有几种可能的结果:
现在进入第二轮枪战:
除非第一轮甲乙双亡,否则丙就一定处于劣势,因为不论甲或乙,他们的命中率都比丙的命中率为高。
这就是枪手丙的悲哀。能力不行的丙玩些花样虽然能在第一轮枪战中暂时获胜。但是,如果甲乙在第一轮枪战中没有双亡的话,在第二轮枪战结束后,丙的存活的几率就一定比甲或乙为低。
这似乎说明,能力差的人在竞争中耍弄手腕能赢一时,但最终往往不能成事。
我们现在改变游戏规则,假定甲乙丙不是同时开枪,而是他们轮流开一枪。先假定开枪的顺序是甲、乙、丙,我们来分析一下枪战过程:
如果是丙先开枪,情况又如何呢?
我们通过这个例子,可以理解人们在博弈中能否获胜,不单纯取决于他们的实力,更重要的是取决于博弈方实力对比所形成的关系。
在上面的例子中,乙和丙实际上是一种联盟关系,先把甲干掉,他们的生存几率都上升了。我们现在来判断一下,乙和丙之中,谁更有可能背叛,谁更可能忠诚?
任何一个联盟的成员都会时刻权衡利弊,一旦背叛的好处大于忠诚的好处,联盟就会破裂。在乙和丙的联盟中,乙是最忠诚的。这不是因为乙本身具有更加忠诚的品质,而是利益关系使然。只要甲不死,乙的枪口就一定会瞄准甲。但丙就不是这样了,丙不瞄准甲而胡乱开一枪显然违背了联盟关系,丙这样做的结果,将使乙处于更危险的境地。
合作才能对抗强敌。只有乙丙合作,才能把甲先干掉。如果,乙丙不和,乙或丙单独对甲都不占优,必然被甲先后解决。、
1966年经典电影《黄金三镖客》中的最后一幕,三个主人公手持枪杆站在墓地中,为了宝藏随时准备决一死战。为了活着拿到宝藏,幸存下来的最优策略是什么呢?
当时,蒙古军事实力最强,金国次之,南宋武力最弱。本来南宋应该和金国结盟,帮助金国抵御蒙古的入侵才是上策,或者至少保持中立。但是,当时的南宋采取了和蒙古结盟的政策。南宋当局先是糊涂地同意了拖雷借道宋地伐金。1231年,蒙古军队在宋朝的先遣队伍引导下,借道四川等地,北度汉水歼灭了金军有生力量。
1233年,南宋军队与蒙古军队合围蔡州,金朝最后一个皇帝在城破后死于乱兵,金至此灭亡。1279年,南宋正式亡于蒙古。
如果南宋当政者有战略眼光,捐弃前嫌,与世仇金结盟对抗最强大的敌人蒙古,宋和金都不至于那么快就先后灭亡了。
猪圈里面有两只猪, 一只大,一只小。猪圈很长,一头有一个踏板,另一头是饲料的出口和食槽。每踩一下踏板,在远离踏板的猪圈的另一边的投食口就会落下少量的食物。如果有一只猪去踩踏板,另一只猪就有机会抢先吃到另一边落下的食物。
那么,两只猪各会采取什么策略?令人出乎意料的是,答案居然是:小猪将选择“搭便车”策略,也就是舒舒服服地等在食槽边;而大猪则为一点残羹不知疲倦地奔忙于踏板和食槽之间。
原因何在呢?我们来分析一下,首先这是一个静态不完全信息博弈:
“智猪博弈”的结论似乎是,在一个双方公平、公正、合理和共享竞争环境中,有时占优势的一方最终得到的结果却有悖于他的初始理性。这种情况在现实中比比皆是。
比如,在某种新产品刚上市,其性能和功用还不为人所熟识的情况下,如果进行新产品生产的不仅是一家小企业,还有其他生产能力和销售能力更强的企业。那么,小企业完全没有必要作出头鸟,自己去投入大量广告做产品宣传,只要采用跟随战略即可。
“智猪博弈”告诉我们,谁先去踩这个踏板,就会造福全体,但多劳却并不一定多得。
在现实生活中,很多人都只想付出最小的代价,得到最大的回报,争着做那只坐享其成的小猪。“一个和尚挑水喝,两个和尚抬水喝,三个和尚没水喝”说的正是这样一个道理。这三个和尚都想做“小猪”,却不想付出劳动,不愿承担起“大猪”的义务,最后导致每个人都无法获得利益。
金融证券市场是一个群体博弈的场所,其真实情况非常复杂。在证券交易中,其结果不仅依赖于单个参与者自身的策略和市场条件,也依赖其他人的选择及策略。
在“智猪博弈”的情景中,大猪是占据比较优势的,但是,由于小猪别无选择,使得大猪为了自己能吃到食物,不得不辛勤忙碌,反而让小猪搭了便车,而且比大猪还得意。这个博弈中的关键要素是猪圈的设计, 即踩踏板的成本。
证券投资中也是有这种情形的。例如,当庄家在底位买入大量股票后,已经付出了相当多的资金和时间成本,如果不等价格上升就撤退,就只有接受亏损。
所以,基于和大猪一样的贪吃本能,只要大势不是太糟糕,庄家一般都会抬高股价,以求实现手中股票的增值。这时的中小散户,就可以对该股追加资金,当一只聪明的“小猪”,而让 “大猪”庄家力抬股价。当然,这种股票的发觉并不容易,所以当“小猪”所需要的条件,就是发现有这种情况存在的猪圈,并冲进去。这样,你就成为一只聪明的“小猪”。
股市中,散户投资者与小猪的命运有相似之处,没有能力承担炒作成本,所以就应该充分利用资金灵活、成本低和不怕被套的优势,发现并选择那些机构投资者已经或可能坐庄的股票,等着大猪们为自己服务。
由此看到,散户和机构的博弈中,散户并不是总没有优势的,关键是找到有大猪的那个食槽,并等到对自己有利的游戏规则形成时再进入。
GAN的主要灵感来源于博弈论中零和博弈的思想。
应用到深度学习神经网络上来说,就是通过生成网络G(Generator)和判别网络D(Discriminator)不断博弈,进而使 G 学习到数据的分布,同时时 D 获得更好的鲁棒性和泛化能力。
举个例子:用在图片生成上,我们想让最后的 G 可以从一段随机数中生成逼真的图像:
上图中:
G是一个生成式的网络,它接收一个随机的噪声 z(随机数),然后通过这个噪声生成图像。
D是一个判别网络,判别一张图片是不是 “真实的”。它的输入是一张图片,输出的 D(x) 代表 x 为真实图片的概率,如果为 1,就代表 100% 是真实的图片,而输出为 0,就代表不可能是真实的图片。
那么这个训练的过程是什么样子的呢?在训练中:
G 的目标就是尽量生成真实的图片去欺骗判别网络 D。
D的目标就是尽量辨别出G生成的假图像和真实的图像。
这样,G 和 D 就构成了一个动态的“博弈过程”,最终的平衡点即纳什均衡点。
Relevant Link:
https://baijiahao.baidu.com/s?id=1611846467821315306&wfr=spider&for=pc https://www.jianshu.com/p/fadba906f5d3
GAN的起源之作鼻祖是 Ian Goodfellow 在 2014 年发表在 ICLR 的论文:Generative Adversarial Networks”。
按照笔者的理解,提出GAN网络的出发点有如下几个:
为了清楚地阐述这个概念,笔者先从对抗样本这个话题开始说起。
对抗样本(adversarial example)是指经过精心计算得到的用于误导分类器的样本。例如下图就是一个例子,左边是一个熊猫,但是添加了少量随机噪声变成右图后,分类器给出的预测类别却是长臂猿,但视觉上左右两幅图片并没有太大改变。
出现这种情况的原因是什么呢?
简单来说,就是预测器发生了过拟合。图像分类器本质上是高维空间的一个复杂的决策函数,在高维空间上,图像分类器过分考虑了全像素区间内的细节信息,导致预测器对图像的细节信息太敏感,微小的扰动就可能导致预测器的预测行为产生很大的变化。
关于这个话题,笔者在另一篇文章中对过拟合现象以及规避方法进行了详细讨论。
除了添加”随机噪声驱动的像素扰动”这种方法之外,还可以通过图像变形的方式,使得新图像和原始图像视觉上一样的情况下,让分类器得到有很高置信度的错误分类结果。这种过程也被称为对抗攻击(adversarial attack)。
人类通过观察和体验物理世界来学习,我们的大脑十分擅长预测,不需要显式地经过复杂计算就可以得到正确的答案。监督学习的过程就是学习数据和标签之间的相关关系。
但是在非监督学习中,数据并没有被标记,而且目标通常也不是对新数据进行预测。
在现实世界中,标记数据是十分稀有和昂贵的。生成对抗网络通过生成伪造的/合成的数据并尝试判断生成样本真伪的方法学习,这本质上相当于采用了监督学习的方法来做无监督学习。做分类任务的判别器在这里是一个监督学习的组件,生成器的目标是了解真实数据的模样(概率分布),并根据学到的知识生成新的数据。
Relevant Link:
https://www.jiqizhixin.com/articles/2018-03-05-4
GAN网络发展到如今已经有很多的变种,在arxiv上每天都会有大量的新的研究论文被提出。但是笔者这里不准备枚举所有的网络结构,而是仅仅讨论GAN中最核心的思想,通过笔者自己的论文阅读,将我认为最精彩的思想和学术创新提炼出来给大家,今后我们也可以根据自己的理解,将其他领域的思想交叉引入进来,继续不断创新发展。
经典的GAN网络由两部分组成,分别称之为判别器D和生成器G,两个网络的工作原理可以如下图所示,
D 的目标就是判别真实图片和 G 生成的图片的真假,而 G 是输入一个随机噪声来生成图片,并努力欺骗 D。
简单来说,GAN 的基本思想就是一个最小最大定理,当两个玩家(D 和 G)彼此竞争时(零和博弈),双方都假设对方采取最优的步骤而自己也以最优的策略应对(最小最大策略),那么结果就会进入一个确定的均衡状态(纳什均衡)。
生成器网络以随机的噪声z作为输入并试图生成样本数据,并将生成的伪造样本数据提供给判别器网络D,
可以看到,G 网络的训练目标就是让 D(G(z)) 趋近于 1,即完全骗过判别器(判别器将生成器生成的伪造样本全部误判为真)。G 网络通过接受 D 网络的反馈作为梯度改进方向,通过BP过程反向调整自己的网络结构参数。
判别器网络以真实数据x或者伪造数据G(z)作为输入,并试图预测当前输入是真实数据还是生成的伪造数据,并产生一个【0,1】范围内的预测标量值。
D 网络的训练目标是区分真假数据,D 网络的训练目标是让 D(x) 趋近于 1(真实的样本判真),而 D(G(z)) 趋近于0(伪造的样本判黑)。D 网络同时接受真实样本和 G 网络传入的伪造样本作为梯度改进方向,,通过BP过程反向调整自己的网络结构参数。
生成器和判别器网络的损失函数结合起来就是生成对抗网络(GAN)的综合损失函数:
两个网络相互对抗,彼此博弈,如上所示,综合损失函数是一个极大极小函数;
整个相互对抗的过程,Ian Goodfellow 在论文中用下图来描述:
黑色曲线表示输入数据 x 的实际分布,绿色曲线表示的是 G 网络生成数据的分布,紫色的曲线表示的是生成数据对应于 D 的分布的差异距离(KL散度)
GAN网络训练的目标是希望着实际分布曲线x,和G网络生成的数据,两条曲线可以相互重合,也就是两个数据分布一致(达到纳什均衡)。
论文给出的算法实现过程如下所示:
一些细节需要注意:
GAN的巧妙之处在于其目标函数的设定,因为此,GAN有如下几个优点:
Relevant Link:
https://arxiv.org/pdf/1406.2661.pdf https://juejin.im/post/5bdd70886fb9a049f912028d http://www.iterate.site/2018/07/27/gan-%E7%94%9F%E6%88%90%E5%AF%B9%E6%8A%97%E7%BD%91%E7%BB%9C%E4%BB%8B%E7%BB%8D/
在阅读了很多GAN衍生论文以及GAN原始论文之后,笔者一直在思考的一个问题是:GAN背后的底层思想是什么?GAN衍生和改进算法的灵感和思路又是从哪里来的?
经过一段时间思考以及和同行同学讨论后,我得出了一些思考,这里分享如下,希望对读者朋友有帮助。
我们先来看什么是判别模型和生成模型:
从概率论的视角来看,我们来看一下原始GAN网络的架构:
遵循这种框架进行思考,CGAN只是将v_input中的随机噪声z替换成了另一种向量(文本或者标签向量),而Pix2pixGAN是将一个图像向量作为v_input输入GAN网络。
GAN的发展离不开goodfellow后来的学者们不断的研究与发展,目前已经提出了很多优秀的新GAN架构,并且这个发展还在继续。为了让本博文能保持一定的环境独立性,笔者这里不做完整的罗列与枚举,相反,笔者希望从两条脉络来展开讨论:
Alec Radford,Luke Metz,Soumith Chintala等人在“Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks”提出了DCGAN。这是GAN研究的一个重要里程碑,因为它提出了一个重要的架构变化来解决训练不稳定,模式崩溃和内部协变量转换等问题。从那时起,基于DCGAN的架构就被应用到了许多GAN架构。
DCGAN的提出主要是为了解决原始GAN架构的原生架构问题,我们接下来来讨论下。
生成器从潜在空间中得到100维噪声向量z,通过一系列卷积和上采样操作,将z映射到一个像素矩阵对应的空间中,如下图:
DCGAN通过下面的一些架构性约束来固化网络:
生成器和判别器都是通过binary_crossentropy作为损失函数来进行训练的。之后的每个阶段,生成器产生一个MNIST图像,判别器尝试在真实MNIST图像和生成图像的数据集中进行学习。
经过一段时间后,生成器就可以自动学会如何制作伪造的数字。
from __future__ import print_function, division from keras.datasets import mnist from keras.layers import Input, Dense, Reshape, Flatten, Dropout from keras.layers import BatchNormalization, Activation, ZeroPadding2D from keras.layers.advanced_activations import LeakyReLU from keras.layers.convolutional import UpSampling2D, Conv2D from keras.models import Sequential, Model from keras.optimizers import Adam import matplotlib.pyplot as plt import sys import numpy as np class DCGAN(): def __init__(self): # Input shape self.img_rows = 28 self.img_cols = 28 self.channels = 1 self.img_shape = (self.img_rows, self.img_cols, self.channels) self.latent_dim = 100 optimizer = Adam(0.0002, 0.5) # Build and compile the discriminator self.discriminator = self.build_discriminator() self.discriminator.compile( loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'] ) # Build the generator self.generator = self.build_generator() # The generator takes noise as input and generates imgs z = Input(shape=(self.latent_dim,)) img = self.generator(z) # For the combined model we will only train the generator self.discriminator.trainable = False # The discriminator takes generated images as input and determines validity valid = self.discriminator(img) # The combined model (stacked generator and discriminator) # Trains the generator to fool the discriminator self.combined = Model(z, valid) self.combined.compile(loss='binary_crossentropy', optimizer=optimizer) def build_generator(self): model = Sequential() model.add(Dense(128 * 7 * 7, activation="relu", input_dim=self.latent_dim)) model.add(Reshape((7, 7, 128))) model.add(UpSampling2D()) model.add(Conv2D(128, kernel_size=3, padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(Activation("relu")) model.add(UpSampling2D()) model.add(Conv2D(64, kernel_size=3, padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(Activation("relu")) model.add(Conv2D(self.channels, kernel_size=3, padding="same")) model.add(Activation("tanh")) model.summary() noise = Input(shape=(self.latent_dim,)) img = model(noise) return Model(noise, img) def build_discriminator(self): model = Sequential() model.add(Conv2D(32, kernel_size=3, strides=2, input_shape=self.img_shape, padding="same")) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(64, kernel_size=3, strides=2, padding="same")) model.add(ZeroPadding2D(padding=((0,1),(0,1)))) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(128, kernel_size=3, strides=2, padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(256, kernel_size=3, strides=1, padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(1, activation='sigmoid')) model.summary() img = Input(shape=self.img_shape) validity = model(img) return Model(img, validity) def train(self, epochs, batch_size=128, save_interval=50): # Load the dataset (X_train, _), (_, _) = mnist.load_data() # Rescale -1 to 1 X_train = X_train / 127.5 - 1. X_train = np.expand_dims(X_train, axis=3) # Adversarial ground truths valid = np.ones((batch_size, 1)) fake = np.zeros((batch_size, 1)) for epoch in range(epochs): # --------------------- # Train Discriminator # --------------------- # Select a random half of images idx = np.random.randint(0, X_train.shape[0], batch_size) imgs = X_train[idx] # Sample noise and generate a batch of new images noise = np.random.normal(0, 1, (batch_size, self.latent_dim)) gen_imgs = self.generator.predict(noise) # Train the discriminator (real classified as ones and generated as zeros) d_loss_real = self.discriminator.train_on_batch(imgs, valid) d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake) d_loss = 0.5 * np.add(d_loss_real, d_loss_fake) # --------------------- # Train Generator # --------------------- # Train the generator (wants discriminator to mistake images as real) g_loss = self.combined.train_on_batch(noise, valid) # Plot the progress print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss)) # If at save interval => save generated image samples if epoch % save_interval == 0: self.save_imgs(epoch) def save_imgs(self, epoch): r, c = 5, 5 noise = np.random.normal(0, 1, (r * c, self.latent_dim)) gen_imgs = self.generator.predict(noise) # Rescale images 0 - 1 gen_imgs = 0.5 * gen_imgs + 0.5 fig, axs = plt.subplots(r, c) cnt = 0 for i in range(r): for j in range(c): axs[i,j].imshow(gen_imgs[cnt, :,:,0], cmap='gray') axs[i,j].axis('off') cnt += 1 fig.savefig("images/mnist_%d.png" % epoch) plt.close() if __name__ == '__main__': dcgan = DCGAN() dcgan.train(epochs=4000, batch_size=32, save_interval=50)
DCGAN产生的手写数字输出
CGAN由Mehdi Mirza,Simon Osindero在论文“Conditional Generative Adversarial Nets”中首次提出。
在条件GAN中,生成器并不是从一个随机的噪声分布中开始学习,而是通过一个特定的条件或某些特征(例如一个图像标签或者一些文本信息)开始学习如何生成伪造样本。
在CGAN中,生成器和判别器的输入都会增加一些条件变量y,这样判别器D(x,y)和生成器G(z,y)都有了一组联合条件变量。
我们将CGAN的目标函数和GAN进行对比会发现:
GAN目标函数
CGAN目标函数
GAN和CGAN的损失函数区别在于判别器和生成器多出来一个参数y,架构上,CGAN相比于GAN增加了一个输入层条件向量C,同时连接了判别器和生成器网络。
在训练过程,我们将y输入给生成器和判别器网络。
from __future__ import print_function, division from keras.datasets import mnist from keras.layers import Input, Dense, Reshape, Flatten, Dropout, multiply from keras.layers import BatchNormalization, Activation, Embedding, ZeroPadding2D from keras.layers.advanced_activations import LeakyReLU from keras.layers.convolutional import UpSampling2D, Conv2D from keras.models import Sequential, Model from keras.optimizers import Adam import matplotlib.pyplot as plt import numpy as np class CGAN(): def __init__(self): # Input shape self.img_rows = 28 self.img_cols = 28 self.channels = 1 self.img_shape = (self.img_rows, self.img_cols, self.channels) self.num_classes = 10 self.latent_dim = 100 optimizer = Adam(0.0002, 0.5) # Build and compile the discriminator self.discriminator = self.build_discriminator() self.discriminator.compile( loss=['binary_crossentropy'], optimizer=optimizer, metrics=['accuracy'] ) # Build the generator self.generator = self.build_generator() # The generator takes noise and the target label as input # and generates the corresponding digit of that label noise = Input(shape=(self.latent_dim,)) label = Input(shape=(1,)) img = self.generator([noise, label]) # For the combined model we will only train the generator self.discriminator.trainable = False # The discriminator takes generated image as input and determines validity # and the label of that image valid = self.discriminator([img, label]) # The combined model (stacked generator and discriminator) # Trains generator to fool discriminator self.combined = Model([noise, label], valid) self.combined.compile(loss=['binary_crossentropy'], optimizer=optimizer) def build_generator(self): model = Sequential() model.add(Dense(256, input_dim=self.latent_dim)) model.add(LeakyReLU(alpha=0.2)) model.add(BatchNormalization(momentum=0.8)) model.add(Dense(512)) model.add(LeakyReLU(alpha=0.2)) model.add(BatchNormalization(momentum=0.8)) model.add(Dense(1024)) model.add(LeakyReLU(alpha=0.2)) model.add(BatchNormalization(momentum=0.8)) model.add(Dense(np.prod(self.img_shape), activation='tanh')) model.add(Reshape(self.img_shape)) model.summary() noise = Input(shape=(self.latent_dim,)) label = Input(shape=(1,), dtype='int32') label_embedding = Flatten()(Embedding(self.num_classes, self.latent_dim)(label)) model_input = multiply([noise, label_embedding]) img = model(model_input) return Model([noise, label], img) def build_discriminator(self): model = Sequential() model.add(Dense(512, input_dim=np.prod(self.img_shape))) model.add(LeakyReLU(alpha=0.2)) model.add(Dense(512)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.4)) model.add(Dense(512)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.4)) model.add(Dense(1, activation='sigmoid')) model.summary() img = Input(shape=self.img_shape) label = Input(shape=(1,), dtype='int32') label_embedding = Flatten()(Embedding(self.num_classes, np.prod(self.img_shape))(label)) flat_img = Flatten()(img) model_input = multiply([flat_img, label_embedding]) validity = model(model_input) return Model([img, label], validity) def train(self, epochs, batch_size=128, sample_interval=50): # Load the dataset (X_train, y_train), (_, _) = mnist.load_data() # Configure input X_train = (X_train.astype(np.float32) - 127.5) / 127.5 X_train = np.expand_dims(X_train, axis=3) y_train = y_train.reshape(-1, 1) # Adversarial ground truths valid = np.ones((batch_size, 1)) fake = np.zeros((batch_size, 1)) for epoch in range(epochs): # --------------------- # Train Discriminator # --------------------- # Select a random half batch of images idx = np.random.randint(0, X_train.shape[0], batch_size) imgs, labels = X_train[idx], y_train[idx] # Sample noise as generator input noise = np.random.normal(0, 1, (batch_size, 100)) # Generate a half batch of new images gen_imgs = self.generator.predict([noise, labels]) # Train the discriminator d_loss_real = self.discriminator.train_on_batch([imgs, labels], valid) d_loss_fake = self.discriminator.train_on_batch([gen_imgs, labels], fake) d_loss = 0.5 * np.add(d_loss_real, d_loss_fake) # --------------------- # Train Generator # --------------------- # Condition on labels sampled_labels = np.random.randint(0, 10, batch_size).reshape(-1, 1) # Train the generator g_loss = self.combined.train_on_batch([noise, sampled_labels], valid) # Plot the progress print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss)) # If at save interval => save generated image samples if epoch % sample_interval == 0: self.sample_images(epoch) def sample_images(self, epoch): r, c = 2, 5 noise = np.random.normal(0, 1, (r * c, 100)) sampled_labels = np.arange(0, 10).reshape(-1, 1) gen_imgs = self.generator.predict([noise, sampled_labels]) # Rescale images 0 - 1 gen_imgs = 0.5 * gen_imgs + 0.5 fig, axs = plt.subplots(r, c) cnt = 0 for i in range(r): for j in range(c): axs[i,j].imshow(gen_imgs[cnt,:,:,0], cmap='gray') axs[i,j].set_title("Digit: %d" % sampled_labels[cnt]) axs[i,j].axis('off') cnt += 1 fig.savefig("images/%d.png" % epoch) plt.close() if __name__ == '__main__': cgan = CGAN() cgan.train(epochs=20000, batch_size=32, sample_interval=200)
根据输入数字生成对应的MNIST手写数字图像
CycleGANs 由Jun-Yan Zhu,Taesung Park,Phillip Isola和Alexei A. Efros在题为“Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks”的论文中提出
CycleGAN用来实现不需要其他额外信息,就能将一张图像从源领域映射到目标领域的方法,例如将照片转换为绘画,将夏季拍摄的照片转换为冬季拍摄的照片,或将马的照片转换为斑马照片,或者相反。总结来说,CycleGAN常备用于不同的图像到图像翻译。
CycleGAN背后的核心思想是两个转换器F和G,其中:
因此,
和原始的GAN结构相比,由单个G->D的单向开放结构,变成了由两对G<->D组成的双向循环的封闭结构,但形式上依然是G给D输入伪造样本。但区别在于梯度的反馈是双向循环的。
CycleGAN模型有以下两个损失函数:
完整的CycleGAN目标函数如下:
from __future__ import print_function, division import scipy from keras.datasets import mnist from keras_contrib.layers.normalization.instancenormalization import InstanceNormalization from keras.layers import Input, Dense, Reshape, Flatten, Dropout, Concatenate from keras.layers import BatchNormalization, Activation, ZeroPadding2D from keras.layers.advanced_activations import LeakyReLU from keras.layers.convolutional import UpSampling2D, Conv2D from keras.models import Sequential, Model from keras.optimizers import Adam import datetime import matplotlib.pyplot as plt import sys from data_loader import DataLoader import numpy as np import os class CycleGAN(): def __init__(self): # Input shape self.img_rows = 128 self.img_cols = 128 self.channels = 3 self.img_shape = (self.img_rows, self.img_cols, self.channels) # Configure data loader self.dataset_name = 'horse2zebra' self.data_loader = DataLoader( dataset_name=self.dataset_name, img_res=(self.img_rows, self.img_cols) ) # Calculate output shape of D (PatchGAN) patch = int(self.img_rows / 2**4) self.disc_patch = (patch, patch, 1) # Number of filters in the first layer of G and D self.gf = 32 self.df = 64 # Loss weights self.lambda_cycle = 10.0 # Cycle-consistency loss self.lambda_id = 0.1 * self.lambda_cycle # Identity loss optimizer = Adam(0.0002, 0.5) # Build and compile the discriminators self.d_A = self.build_discriminator() self.d_B = self.build_discriminator() self.d_A.compile( loss='mse', optimizer=optimizer, metrics=['accuracy'] ) self.d_B.compile( loss='mse', optimizer=optimizer, metrics=['accuracy'] ) # ------------------------- # Construct Computational # Graph of Generators # ------------------------- # Build the generators self.g_AB = self.build_generator() self.g_BA = self.build_generator() # Input images from both domains img_A = Input(shape=self.img_shape) img_B = Input(shape=self.img_shape) # Translate images to the other domain fake_B = self.g_AB(img_A) fake_A = self.g_BA(img_B) # Translate images back to original domain reconstr_A = self.g_BA(fake_B) reconstr_B = self.g_AB(fake_A) # Identity mapping of images img_A_id = self.g_BA(img_A) img_B_id = self.g_AB(img_B) # For the combined model we will only train the generators self.d_A.trainable = False self.d_B.trainable = False # Discriminators determines validity of translated images valid_A = self.d_A(fake_A) valid_B = self.d_B(fake_B) # Combined model trains generators to fool discriminators self.combined = Model( inputs=[img_A, img_B], outputs=[valid_A, valid_B, reconstr_A, reconstr_B, img_A_id, img_B_id ] ) self.combined.compile( loss=['mse', 'mse', 'mae', 'mae', 'mae', 'mae'], loss_weights=[1, 1, self.lambda_cycle, self.lambda_cycle, self.lambda_id, self.lambda_id], optimizer=optimizer ) def build_generator(self): """U-Net Generator""" def conv2d(layer_input, filters, f_size=4): """Layers used during downsampling""" d = Conv2D(filters, kernel_size=f_size, strides=2, padding='same')(layer_input) d = LeakyReLU(alpha=0.2)(d) d = InstanceNormalization()(d) return d def deconv2d(layer_input, skip_input, filters, f_size=4, dropout_rate=0): """Layers used during upsampling""" u = UpSampling2D(size=2)(layer_input) u = Conv2D(filters, kernel_size=f_size, strides=1, padding='same', activation='relu')(u) if dropout_rate: u = Dropout(dropout_rate)(u) u = InstanceNormalization()(u) u = Concatenate()([u, skip_input]) return u # Image input d0 = Input(shape=self.img_shape) # Downsampling d1 = conv2d(d0, self.gf) d2 = conv2d(d1, self.gf*2) d3 = conv2d(d2, self.gf*4) d4 = conv2d(d3, self.gf*8) # Upsampling u1 = deconv2d(d4, d3, self.gf*4) u2 = deconv2d(u1, d2, self.gf*2) u3 = deconv2d(u2, d1, self.gf) u4 = UpSampling2D(size=2)(u3) output_img = Conv2D(self.channels, kernel_size=4, strides=1, padding='same', activation='tanh')(u4) return Model(d0, output_img) def build_discriminator(self): def d_layer(layer_input, filters, f_size=4, normalization=True): """Discriminator layer""" d = Conv2D(filters, kernel_size=f_size, strides=2, padding='same')(layer_input) d = LeakyReLU(alpha=0.2)(d) if normalization: d = InstanceNormalization()(d) return d img = Input(shape=self.img_shape) d1 = d_layer(img, self.df, normalization=False) d2 = d_layer(d1, self.df*2) d3 = d_layer(d2, self.df*4) d4 = d_layer(d3, self.df*8) validity = Conv2D(1, kernel_size=4, strides=1, padding='same')(d4) return Model(img, validity) def train(self, epochs, batch_size=1, sample_interval=50): start_time = datetime.datetime.now() # Adversarial loss ground truths valid = np.ones((batch_size,) + self.disc_patch) fake = np.zeros((batch_size,) + self.disc_patch) for epoch in range(epochs): for batch_i, (imgs_A, imgs_B) in enumerate(self.data_loader.load_batch(batch_size)): # ---------------------- # Train Discriminators # ---------------------- # Translate images to opposite domain fake_B = self.g_AB.predict(imgs_A) fake_A = self.g_BA.predict(imgs_B) # Train the discriminators (original images = real / translated = Fake) dA_loss_real = self.d_A.train_on_batch(imgs_A, valid) dA_loss_fake = self.d_A.train_on_batch(fake_A, fake) dA_loss = 0.5 * np.add(dA_loss_real, dA_loss_fake) dB_loss_real = self.d_B.train_on_batch(imgs_B, valid) dB_loss_fake = self.d_B.train_on_batch(fake_B, fake) dB_loss = 0.5 * np.add(dB_loss_real, dB_loss_fake) # Total disciminator loss d_loss = 0.5 * np.add(dA_loss, dB_loss) # ------------------ # Train Generators # ------------------ # Train the generators g_loss = self.combined.train_on_batch([imgs_A, imgs_B], [valid, valid, imgs_A, imgs_B, imgs_A, imgs_B]) elapsed_time = datetime.datetime.now() - start_time # Plot the progress print ("[Epoch %d/%d] [Batch %d/%d] [D loss: %f, acc: %3d%%] [G loss: %05f, adv: %05f, recon: %05f, id: %05f] time: %s " \ % ( epoch, epochs, batch_i, self.data_loader.n_batches, d_loss[0], 100*d_loss[1], g_loss[0], np.mean(g_loss[1:3]), np.mean(g_loss[3:5]), np.mean(g_loss[5:6]), elapsed_time)) # If at save interval => save generated image samples if batch_i % sample_interval == 0: self.sample_images(epoch, batch_i) def sample_images(self, epoch, batch_i): if not os.path.exists('images/%s' % self.dataset_name): os.makedirs('images/%s' % self.dataset_name) r, c = 2, 3 imgs_A = self.data_loader.load_data(domain="A", batch_size=1, is_testing=True) imgs_B = self.data_loader.load_data(domain="B", batch_size=1, is_testing=True) # Demo (for GIF) #imgs_A = self.data_loader.load_img('datasets/apple2orange/testA/n07740461_1541.jpg') #imgs_B = self.data_loader.load_img('datasets/apple2orange/testB/n07749192_4241.jpg') # Translate images to the other domain fake_B = self.g_AB.predict(imgs_A) fake_A = self.g_BA.predict(imgs_B) # Translate back to original domain reconstr_A = self.g_BA.predict(fake_B) reconstr_B = self.g_AB.predict(fake_A) gen_imgs = np.concatenate([imgs_A, fake_B, reconstr_A, imgs_B, fake_A, reconstr_B]) # Rescale images 0 - 1 gen_imgs = 0.5 * gen_imgs + 0.5 titles = ['Original', 'Translated', 'Reconstructed'] fig, axs = plt.subplots(r, c) cnt = 0 for i in range(r): for j in range(c): axs[i,j].imshow(gen_imgs[cnt]) axs[i, j].set_title(titles[j]) axs[i,j].axis('off') cnt += 1 fig.savefig("images/%s/%d_%d.png" % (self.dataset_name, epoch, batch_i)) plt.close() if __name__ == '__main__': gan = CycleGAN() gan.train(epochs=200, batch_size=1, sample_interval=200)
苹果->橙子->苹果
有类似架构思想的还有DiscoGAN,相关论文可以在axiv上找到。
StackJANs由Han Zhang,Tao Xu,Hongsheng Li还有其他人在题为“StackGAN: Text to Photo-Realistic Image Synthesis with Stacked Generative Adversarial Networks”的论文中提出。他们使用StackGAN来探索文本到图像的合成,得到了非常好的结果。
一个StackGAN由一对网络组成,当提供文本描述时,可以生成逼真的图像。
pix2pix网络由Phillip Isola,Jun-Yan Zhu,Tinghui Zhou和Alexei A. Efros在他们的题为“Image-to-Image Translation with Conditional Adversarial Networks”的论文中提出。
对于图像到图像的翻译任务,pix2pix也显示出了令人印象深刻的结果。无论是将夜间图像转换为白天的图像还是给黑白图像着色,或者将草图转换为逼真的照片等等,Pix2pix在这些例子中都表现非常出色。
Grigory Antipov,Moez Baccouche和Jean-Luc Dugelay在他们的题为“Face Aging with Conditional Generative Adversarial Networks”的论文中提出了用条件GAN进行面部老化。
面部老化有许多行业用例,包括跨年龄人脸识别,寻找失踪儿童,或者用于娱乐,本质上它属于cGAN的一种场景应用。
Relevant Link:
https://arxiv.org/pdf/1511.06434.pdf https://github.com/hindupuravinash/the-gan-zoo https://github.com/eriklindernoren/Keras-GAN https://zhuanlan.zhihu.com/p/63428113
我们用DNN架构重写原始GAN代码,并使用一批php webshell作为真实样本,尝试用GAN进行伪造样本生成。
from keras.layers import Input, Dense, Reshape, Flatten, Dropout from keras.layers import BatchNormalization, Activation, ZeroPadding2D from keras.layers.advanced_activations import LeakyReLU from keras.layers.convolutional import UpSampling2D, Conv2D from keras.models import Sequential, Model from keras.optimizers import Adam from keras.preprocessing import sequence from sklearn.externals import joblib import re import os import numpy as np # np.set_printoptions(threshold=np.nan) class DCGAN(): def __init__(self): # Input shape self.charlen = 64 self.fileshape = (self.charlen, ) self.latent_dim = 100 self.ENCODER = joblib.load("./CHAR_SEQUENCE_TOKENIZER_INDEX_TABLE_PICKLE.encoder") self.rerange_dim = (len(self.ENCODER.word_index) + 1) / 2. - 0.5 optimizer = Adam(0.0002, 0.5) # Build and compile the discriminator self.discriminator = self.build_discriminator() self.discriminator.compile( loss='binary_crossentropy', optimizer=optimizer, metrics=['accuracy'] ) # Build the generator self.generator = self.build_generator() # The generator takes noise as input and generates imgs z = Input(shape=(self.latent_dim,)) img = self.generator(z) # For the combined model we will only train the generator self.discriminator.trainable = False # The discriminator takes generated images as input and determines validity valid = self.discriminator(img) # The combined model (stacked generator and discriminator) # Trains the generator to fool the discriminator self.combined = Model(z, valid) self.combined.compile(loss='binary_crossentropy', optimizer=optimizer) def build_generator(self): model = Sequential() model.add(Dense(64, activation="relu")) model.add(Dense(128, activation="relu")) model.add(Dense(256, activation="relu")) model.add(Dense(128, activation="relu")) model.add(Dense(self.charlen, activation="relu")) # model.summary() noise = Input(shape=(self.latent_dim,)) img = model(noise) return Model(noise, img) def build_discriminator(self): model = Sequential() model.add(Dense(128, activation="relu")) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.5)) model.add(Dense(256, activation="relu")) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.5)) model.add(Dense(512, activation="relu")) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.5)) model.add(Dense(128, activation="relu")) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.5)) model.add(Dense(1, activation='sigmoid')) # model.summary() img = Input(shape=self.fileshape) validity = model(img) return Model(img, validity) def train(self, epochs, batch_size=64, save_interval=50): # Load the dataset X_train = self.load_webfile_data() # Adversarial ground truths valid = np.ones((batch_size, 1)) fake = np.zeros((batch_size, 1)) for epoch in range(epochs): # --------------------- # Train Discriminator # --------------------- # Select a random half of images idx = np.random.randint(0, X_train.shape[0], batch_size) imgs = X_train[idx] # Sample noise and generate a batch of new images noise = np.random.normal(0, 1, (batch_size, self.latent_dim)) gen_imgs = self.generator.predict(noise) # print gen_imgs # print np.shape(gen_imgs) # Train the discriminator (real classified as ones and generated as zeros) d_loss_real = self.discriminator.train_on_batch(imgs, valid) d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake) d_loss = 0.5 * np.add(d_loss_real, d_loss_fake) # --------------------- # Train Generator # --------------------- # Train the generator (wants discriminator to mistake images as real) g_loss = self.combined.train_on_batch(noise, valid) # Plot the progress print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss)) # If at save interval => save generated image samples if epoch % save_interval == 0: self.save_imgs(epoch) def save_imgs(self, epoch): r, c = 5, 5 noise = np.random.normal(1, 2, (r * c, self.latent_dim)) gen_imgs = self.generator.predict(noise) # Rescale [-1,1] back to [0, ascii_char] range gen_imgs = (gen_imgs + 1.) * self.rerange_dim gen_text_vec = gen_imgs.reshape((np.shape(gen_imgs)[0], self.charlen)) gen_text_vec = gen_text_vec.astype(int) # reconver back to ascii #print "gen_text_vec: ", gen_text_vec gen_text = self.ENCODER.sequences_to_texts(gen_text_vec) #print "gen_text:", gen_text with open('./gen_webfile/{0}.txt'.format(epoch), 'wb') as f: for file_vec in gen_text: fcontent = "" for c in file_vec: fcontent += c fcontent = re.sub(r"\s+", "", fcontent) f.write(fcontent) def load_webfile_data(self): vec_dict = { 'raw_ascii': [] } rootDir = "./webdata" for lists in os.listdir(rootDir): if lists == '.DS_Store': continue webpath = os.path.join(rootDir, lists) with open(webpath, 'r') as fp: fcontent = fp.read() # remove space fcontent = re.sub(r"\s+", " ", fcontent) fcontent_ = "" for c in fcontent: fcontent_ += c + " " vec_dict['raw_ascii'].append(fcontent_) # convert to ascii sequence vec raw_ascii_sequence_vec = self.ENCODER.texts_to_sequences(vec_dict['raw_ascii']) raw_ascii_sequence_vec = sequence.pad_sequences( raw_ascii_sequence_vec, maxlen=self.charlen, padding='post', truncating='post', dtype='float32' ) # reshape to 2d array raw_ascii_sequence_vec = raw_ascii_sequence_vec.reshape((np.shape(raw_ascii_sequence_vec)[0], self.charlen)) # ascii is range in [1, 128], we need Rescale -1 to 1 print "rerange_dim: ", self.rerange_dim raw_ascii_sequence_vec = raw_ascii_sequence_vec / self.rerange_dim - 1. # raw_ascii_sequence_vec = np.expand_dims(raw_ascii_sequence_vec, axis=3) print "np.shape(raw_ascii_sequence_vec): ", np.shape(raw_ascii_sequence_vec) return raw_ascii_sequence_vec if __name__ == '__main__': dcgan = DCGAN() dcgan.train(epochs=8000, batch_size=8, save_interval=20) #print dcgan.load_webfile_data()
实验的结果并不理想,GAN很快遇到了模型坍塌问题,从G生成的样本来看,网络很快陷入了一个局部最优区间中。
关于这个问题,学术界已经有比较多的讨论和分析,笔者这里列举如下:
Sparse reward:adversarial training 没起作用很大的一个原因就在于,discriminator 提供的 reward 具备的 guide signal 太少,Classifier-based Discriminator 提供的只是一个为真或者假的概率作为 reward,而这个 reward 在大部分情况下,是 0。这是因为对于 CNN 来说,分出 fake text 和 real text 是非常容易的,CNN 能在 Classification 任务上做到 99% 的 accuracy,而建模 Language Model 来进行生成,是非常困难的。除此以外,即使 generator 在这样的 reward 指导下有一些提升,此后的 reward 依旧很小。
基本上说,学术界对文本的看法是将其是做一个时序依赖的序列,所以主流方向是使用RNN/LSTM这类模型作为生成器来生成伪造文本序列。而接下要要解决的重点问题是,如何有效地将判别器的反馈有效地传递给生成器。
增加reward signal强度和平滑度:从这一点出发,现有不少工作一方法不再使用简单的 fake/true probability 作为 reward。
离散数据的可导的损失函数:通过改造原始softmax函数,使用新的gumble softmax,它可以代替policy gradient,直接可导了。
policy gradient代替原始gradient,将reward传导回去,这是现在比较主流的做法
Relevant Link:
https://github.com/LantaoYu/SeqGAN https://zhuanlan.zhihu.com/p/25168509 https://tobiaslee.top/2018/09/30/Text-Generation-with-GAN/ https://zhuanlan.zhihu.com/p/36880287 https://www.jianshu.com/p/32e164883eab