生成对抗网络(Generative Adversarial Networks,GAN)初探

1. 从纳什均衡(Nash equilibrium)提及

咱们先来看看纳什均衡的经济学定义:php

所谓纳什均衡,指的是参与人的这样一种策略组合,在该策略组合上,任何参与人单独改变策略都不会获得好处。换句话说,若是在一个策略组合上,当全部其余人都不改变策略时,没有人会改变本身的策略,则该策略组合就是一个纳什均衡css

B站上有一个关于”海滩2个兄弟卖雪糕“造成纳什均衡的 视频,讲的很生动。
无论系统中的双方一开始处于什么样的状态,只要系统中参与竞争的个体都是”理性经济人“,即 每一个人在考虑其余人的可能动做的基础上,出于最大化本身的我的利益做为下一步行动的考虑,那么最终系统都必定会进入纳什均衡状态,这个状态也许对于系统来讲不是全局最优的,可是对于系统中的每个个体来讲都是理论最优的
这样可能仍是有一些抽象,咱们用几个例子来讲明。

0x1:价格战中的纳什均衡

市场上有2家企业A和B,都是卖纸的,纸的成本都是2元钱,A和B都卖5块钱。在最开始,A、B企业都是盈利3块,这种状态叫”社会最优解(Social optimal solution)“。但问题是,社会最优解是一个不稳定的状态,就如同下图中这个优化曲面上那个红球点同样,虽然该小球目前处于曲面最高点,可是只要施加一些轻微的扰动,小球就会马上向山下滑落:html

如今企业A和B准备开展商业竞争:git

  • 有一天,A企业率先降价到4块钱,因而A销量大增,B销量大减。
  • B看到了后,降价到3块钱,因而B销量大增,A销量大减。
  • ......

但若是价格战一直这样打下去,这个过程显然不可能无限迭代下去。当A和B都降价到了3块时,双方都达到了成本的临界点,既不敢涨价,也不敢降价。涨价了市场就丢了,降价了,就赚不到钱甚至赔钱。因此A和B都不会再去作改变,这就是纳什均衡。github

A和B怎样可以得到最大利润呢,就是A和B坐到一块儿商量,同时把价格提升,这就叫共谋,但法律为了保障消费者利益,禁止共谋。补充一句,共谋在机器学习中被称做”模型坍塌“,指的对对抗的模型双方都进入了一个互相承认的局部最优区而再也不变化,具体的技术细节咱们后面会讨论。web

0x2:囚徒困境中的纳什均衡

囚徒困境是说:有两个小偷集体做案,而后被警察捉住。警察对两我的分别审讯,而且告诉他们政策:算法

  • 若是两我的都交代坦白,就能够定罪,两我的各判八年。
  • 若是一我的交代另外一个不交代,那么同样能够定罪。可是交代的人从宽处罚,批评教育就释放。不交代的人从严处罚,判十年。
  • 若是两我的都不交代,无法定罪,每一个人判一年意思一下。

两我的的收益状况以下所示:shell

由于A和B是不能互相通讯的,所以这是一个静态不彻底信息博弈,咱们分别考虑双方的决策面:网络

  • A的决策。A会想,我如何才能得到更大收益呢?
    • 先考虑最坏的状况:若是B坦白了,那么我坦白就会判8年,我抗拒就会判十年,我应该坦白;
    • 再考虑最好的状况:若是B抗拒了,我坦白会判0年,我抗拒会判1年,我仍是应该坦白;
    • 因此最终A会选择坦白。
  • 一样,B也会这样想。

所以最终纳什均衡点在两我的都坦白,各判八年这里。架构

显然,集体最优解在两我的都抗拒,这样一来每一个人都判一年就出来了。可是,纳什均衡点却不在这里。并且,在纳什均衡点上,任何一我的都没有改变本身决策的动力。由于一旦单方面改变决策,那我的的收益就会降低。

0x3:开车加塞现象的纳什均衡

咱们知道,在国内开车夹塞很常见。若是你们都不夹塞,是总体的最优解,可是按照纳什均衡理论,任何一个司机都会考虑,不管别人是否夹塞,我夹塞均可以使本身的收益变大。因而最终你们都会夹塞,加重拥堵,反而不如你们都不加塞走的快。

那么,有没有办法使我的最优变成集体最优呢?方法就是共谋。两个小偷在做案以前能够说好,我们若是进去了,必定都抗拒。若是你这一次敢反悔,那么之后道上的人不再会有人跟你一块儿了。也就是说,在屡次博弈过程当中,共谋是可能的。可是若是这个小偷想干完这一票就走,共谋就是不牢靠的。

在社会领域,共谋是靠法律完成的。你们约定的共谋结论就是法律,若是有人不按照约定作,就会受到法律的惩罚。经过这种方式保证最终决策从我的最优的纳什均衡点变为集体最优势。

另一方面,如今不少汽车厂商提出了车联网的概念,在路上的每一辆车都经过物联网连成一个临时网络,全部车按照一个最优的协同算法共同协定最优的行车路线、行车速度、路口等待等行为,这样总体交通能够达到一个总体最优,全部人都节省了时间。

0x3:枪手博弈

彼此痛恨的甲、乙、丙三个枪手准备决斗,他们各自的水平以下:

  • 甲枪法最好,十发八中;
  • 乙枪法次之,十发六中;
  • 丙枪法最差,十发四中;

1. 场景一:三人同时开枪,而且每人只发一枪。每一轮枪战后,谁活下来的机会大一些?

首先明确一点,这是一个静态不彻底信息博弈,每一个抢手在开枪前都不知道其余对手的策略,只能在猜想其余对手策略的基础上,选择对本身最优的策略。

咱们来分析一下第一轮枪战各个枪手的策略。

  • 枪手甲必定要对枪手乙先开枪。由于乙对甲的威胁要比丙对甲的威胁更大,甲应该首先干掉乙,这是甲的最佳策略。
  • 一样的道理,枪手乙的最佳策略是第一枪瞄准甲。乙一旦将甲干掉,乙和丙进行对决,乙胜算的几率天然大不少。
  • 枪手丙的最佳策略也是先对甲开枪。乙的枪法毕竟比甲差一些,丙先把甲干掉再与乙进行对决,丙的存活几率仍是要高一些。

第一轮枪战事后,有几种可能的结果:

  • 甲乙双亡,丙获胜
  • 甲亡,乙丙存活
  • 乙亡,甲丙存活

如今进入第二轮枪战:

除非第一轮甲乙双亡,不然丙就必定处于劣势,由于不论甲或乙,他们的命中率都比丙的命中率为高。

这就是枪手丙的悲哀。能力不行的丙玩些花样虽然能在第一轮枪战中暂时获胜。可是,若是甲乙在第一轮枪战中没有双亡的话,在第二轮枪战结束后,丙的存活的概率就必定比甲或乙为低。

这彷佛说明,能力差的人在竞争中耍弄手腕能赢一时,但最终每每不能成事。

2. 场景二:三人轮流开枪,没人只发一枪。丙最后发枪。

咱们如今改变游戏规则,假定甲乙丙不是同时开枪,而是他们轮流开一枪。先假定开枪的顺序是甲、乙、丙,咱们来分析一下枪战过程:

  • 甲一枪将乙干掉后(80%的概率),就轮到丙开枪,丙有40%的概率一枪将甲干掉。
  • 乙躲过甲的第一枪(20%概率),轮到乙开枪,乙仍是会瞄准枪法最好的甲开枪,即便乙这一枪干掉了甲(60%概率),下一轮仍然是轮到丙开枪(40%概率)。不管是甲或者乙先开枪,乙都有在下一轮先开枪的优点。

若是是丙先开枪,状况又如何呢?

3. 场景三:三人轮流开枪,没人只发一枪。丙第一个发枪。

  • 丙能够向甲先开枪(40%概率),
    • 即便丙打不中甲,甲的最佳策略仍然是向乙开枪。
    • 可是,若是丙打中了甲,下一轮可就是乙开枪打丙了。
  • 所以,丙的最佳策略是胡乱开一枪,只要丙不打中甲或者乙,在下一轮射击中他就处于有利的形势(先发优点)。

咱们经过这个例子,能够理解人们在博弈中可否获胜,不单纯取决于他们的实力,更重要的是取决于博弈方实力对比所造成的关系

在上面的例子中,乙和丙其实是一种联盟关系,先把甲干掉,他们的生存概率都上升了。咱们如今来判断一下,乙和丙之中,谁更有可能背叛,谁更可能忠诚?

任何一个联盟的成员都会时刻权衡利弊,一旦背叛的好处大于忠诚的好处,联盟就会破裂。在乙和丙的联盟中,乙是最忠诚的。这不是由于乙自己具备更加忠诚的品质,而是利益关系使然。只要甲不死,乙的枪口就必定会瞄准甲。但丙就不是这样了,丙不瞄准甲而胡乱开一枪显然违背了联盟关系,丙这样作的结果,将使乙处于更危险的境地。

合做才能对抗强敌。只有乙丙合做,才能把甲先干掉。若是,乙丙不和,乙或丙单独对甲都不占优,必然被甲前后解决。、

1966年经典电影《黄金三镖客》中的最后一幕,三个主人公手持枪杆站在墓地中,为了宝藏随时准备决一雌雄。为了活着拿到宝藏,幸存下来的最优策略是什么呢? 

0x4:蒙古联合南宋灭金

当时,蒙古军事实力最强,金国次之,南宋武力最弱。原本南宋应该和金国结盟,帮助金国抵御蒙古的入侵才是上策,或者至少保持中立。可是,当时的南宋采起了和蒙古结盟的政策。南宋当局先是糊涂地赞成了拖雷借道宋地伐金。1231年,蒙古军队在宋朝的先遣队伍引导下,借道四川等地,北度汉水歼灭了金军有生力量。

1233年,南宋军队与蒙古军队合围蔡州,金朝最后一个皇帝在城破后死于乱兵,金至此灭亡。1279年,南宋正式亡于蒙古。

若是南宋当政者有战略眼光,捐弃前嫌,与世仇金结盟对抗最强大的敌人蒙古,宋和金都不至于那么快就前后灭亡了。

0x5:智猪博弈

猪圈里面有两只猪, 一只大,一只小。猪圈很长,一头有一个踏板,另外一头是饲料的出口和食槽。每踩一下踏板,在远离踏板的猪圈的另外一边的投食口就会落下少许的食物。若是有一只猪去踩踏板,另外一只猪就有机会抢先吃到另外一边落下的食物。

  • 当小猪踩动踏板时,大猪会在小猪跑到食槽以前恰好吃光全部的食物;
  • 如果大猪踩动了踏板,则还有机会在小猪吃完落下的食物以前跑到食槽,争吃到另外一半残羹。

那么,两只猪各会采起什么策略?使人出乎意料的是,答案竟然是:小猪将选择“搭便车”策略,也就是舒舒服服地等在食槽边;而大猪则为一点残羹不知疲倦地奔忙于踏板和食槽之间。

缘由何在呢?咱们来分析一下,首先这是一个静态不彻底信息博弈

  • 小猪踩踏板:小猪将一无所得,不踩踏板反而能吃上食物。对小猪而言,不管大猪是否踩动踏板,不踩踏板老是好的选择。
  • 反观大猪,已明知小猪是不会去踩动踏板的,本身亲自去踩踏板总比不踩强吧,因此只好亲力亲为了。

“智猪博弈”的结论彷佛是,在一个双方公平、公正、合理和共享竞争环境中,有时占优点的一方最终获得的结果却有悖于他的初始理性。这种状况在现实中比比皆是。

好比,在某种新产品刚上市,其性能和功用还不为人所熟识的状况下,若是进行新产品生产的不只是一家小企业,还有其余生产能力和销售能力更强的企业。那么,小企业彻底没有必要做出头鸟,本身去投入大量广告作产品宣传,只要采用跟随战略便可。

“智猪博弈”告诉咱们,谁先去踩这个踏板,就会造福全体,但多劳却并不必定多得。

在现实生活中,不少人都只想付出最小的代价,获得最大的回报,争着作那只不劳而获的小猪。“一个和尚挑水喝,两个和尚抬水喝,三个和尚没水喝”说的正是这样一个道理。这三个和尚都想作“小猪”,却不想付出劳动,不肯承担起“大猪”的义务,最后致使每一个人都没法得到利益。

0x6:证券市场中的“智猪博弈”

金融证券市场是一个群体博弈的场所,其真实状况很是复杂。在证券交易中,其结果不只依赖于单个参与者自身的策略和市场条件,也依赖其余人的选择及策略。

在“智猪博弈”的情景中,大猪是占据比较优点的,可是,因为小猪别无选择,使得大猪为了本身能吃到食物,不得不辛勤忙碌,反而让小猪搭了便车,并且比大猪还得意。这个博弈中的关键要素是猪圈的设计, 即踩踏板的成本。

证券投资中也是有这种情形的。例如,当庄家在底位买入大量股票后,已经付出了至关多的资金和时间成本,若是不等价格上升就撤退,就只有接受亏损。

因此,基于和大猪同样的贪吃本能,只要大势不是太糟糕,庄家通常都会抬高股价,以求实现手中股票的增值。这时的中小散户,就能够对该股追加资金,当一只聪明的“小猪”,而让 “大猪”庄家力抬股价。固然,这种股票的发觉并不容易,因此当“小猪”所须要的条件,就是发现有这种状况存在的猪圈,并冲进去。这样,你就成为一只聪明的“小猪”。

股市中,散户投资者与小猪的命运有类似之处,没有能力承担炒做成本,因此就应该充分利用资金灵活、成本低和不怕被套的优点,发现并选择那些机构投资者已经或可能坐庄的股票,等着大猪们为本身服务。

由此看到,散户和机构的博弈中,散户并非总没有优点的,关键是找到有大猪的那个食槽,并等到对本身有利的游戏规则造成时再进入。

0x7:纳什均衡博弈与GAN网络的关系

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 

 

2. GAN网络的思想起源

GAN的起源之做鼻祖是 Ian Goodfellow 在 2014 年发表在 ICLR 的论文:Generative Adversarial Networks”。

按照笔者的理解,提出GAN网络的出发点有以下几个:

  • 最核心的做用是提升分类器的鲁棒能力,由于生成器不断生成”尽可能逼近真实样本“的伪造图像,而分类器为了能正确区分出伪造和真实的样本,就须要不断地挖掘样本中真正蕴含的潜在几率信息,而抛弃无用的多余特征,这就起到了提升鲁棒和泛化能力的做用。从某种程度上来讲,GAN起到了和正则化约束的效果。
  • 基于随机扰动,有针对性地生成新样本。可是要注意的一点是,GAN生成的样本并非彻底的未知新样本,GAN的generator生成的新样本更多的侧重点是经过增长可控的扰动来尝试躲避discriminator的检测。实际上,GAN对生成0day样本的能力颇有限。

为了清楚地阐述这个概念,笔者先从对抗样本这个话题开始提及。

0x1:对抗样本(adversarial example)

对抗样本(adversarial example)是指通过精心计算获得的用于误导分类器的样本。例以下图就是一个例子,左边是一个熊猫,可是添加了少许随机噪声变成右图后,分类器给出的预测类别倒是长臂猿,但视觉上左右两幅图片并无太大改变。

出现这种状况的缘由是什么呢?

简单来讲,就是预测器发生了过拟合。图像分类器本质上是高维空间的一个复杂的决策函数,在高维空间上,图像分类器过度考虑了全像素区间内的细节信息,致使预测器对图像的细节信息太敏感,微小的扰动就可能致使预测器的预测行为产生很大的变化。

关于这个话题,笔者在另外一篇文章中对过拟合现象以及规避方法进行了详细讨论。

除了添加”随机噪声驱动的像素扰动”这种方法以外,还能够经过图像变形的方式,使得新图像和原始图像视觉上同样的状况下,让分类器获得有很高置信度的错误分类结果。这种过程也被称为对抗攻击(adversarial attack)

0x2:有监督驱动的无监督学习

人类经过观察和体验物理世界来学习,咱们的大脑十分擅长预测,不须要显式地通过复杂计算就能够获得正确的答案。监督学习的过程就是学习数据和标签之间的相关关系。

可是在非监督学习中,数据并无被标记,并且目标一般也不是对新数据进行预测。

在现实世界中,标记数据是十分稀有和昂贵的。生成对抗网络经过生成伪造的/合成的数据并尝试判断生成样本真伪的方法学习,这本质上至关于采用了监督学习的方法来作无监督学习。作分类任务的判别器在这里是一个监督学习的组件,生成器的目标是了解真实数据的模样(几率分布),并根据学到的知识生成新的数据。

Relevant Link:  

https://www.jiqizhixin.com/articles/2018-03-05-4

 

3. GAN网络基本原理

GAN网络发展到现在已经有不少的变种,在arxiv上天天都会有大量的新的研究论文被提出。可是笔者这里不许备枚举全部的网络结构,而是仅仅讨论GAN中最核心的思想,经过笔者本身的论文阅读,将我认为最精彩的思想和学术创新提炼出来给你们,从此咱们也能够根据本身的理解,将其余领域的思想交叉引入进来,继续不断创新发展。

0x1:GAN的组成

 

经典的GAN网络由两部分组成,分别称之为判别器D生成器G,两个网络的工做原理能够以下图所示,

D 的目标就是判别真实图片和 G 生成的图片的真假,而 G 是输入一个随机噪声来生成图片,并努力欺骗 D。

简单来讲,GAN 的基本思想就是一个最小最大定理,当两个玩家(D 和 G)彼此竞争时(零和博弈),双方都假设对方采起最优的步骤而本身也以最优的策略应对(最小最大策略),那么结果就会进入一个肯定的均衡状态(纳什均衡)。

0x2:损失函数分析

1. 生成器(generator)损失函数

生成器网络以随机的噪声z做为输入并试图生成样本数据,并将生成的伪造样本数据提供给判别器网络D,

能够看到,G 网络的训练目标就是让 D(G(z)) 趋近于 1,即彻底骗过判别器(判别器将生成器生成的伪造样本所有误判为真)。G 网络经过接受 D 网络的反馈做为梯度改进方向,经过BP过程反向调整本身的网络结构参数。

2. 判别器(discriminator)

判别器网络以真实数据x或者伪造数据G(z)做为输入,并试图预测当前输入是真实数据仍是生成的伪造数据,并产生一个【0,1】范围内的预测标量值。

D 网络的训练目标是区分真假数据,D 网络的训练目标是让 D(x) 趋近于 1(真实的样本判真),而 D(G(z)) 趋近于0(伪造的样本判黑)。D 网络同时接受真实样本和 G 网络传入的伪造样本做为梯度改进方向,,经过BP过程反向调整本身的网络结构参数。

3. 综合损失函数

生成器和判别器网络的损失函数结合起来就是生成对抗网络(GAN)的综合损失函数:

两个网络相互对抗,彼此博弈,如上所示,综合损失函数是一个极大极小函数;

  • 损失函数第一项:会驱使判别器尽可能将真实样本都判真
  • 损失函数第二项:会驱使判别器尽可能将伪造样本都判黑。但同时,生成器G会对抗这个过程

整个相互对抗的过程,Ian Goodfellow 在论文中用下图来描述:

 

 黑色曲线表示输入数据 x 的实际分布,绿色曲线表示的是 G 网络生成数据的分布,紫色的曲线表示的是生成数据对应于 D 的分布的差别距离(KL散度)

GAN网络训练的目标是但愿着实际分布曲线x,和G网络生成的数据,两条曲线能够相互重合,也就是两个数据分布一致(达到纳什均衡)。

  • a图:网络刚开始训练,D 的分类能力还不是最好,所以有所波动,而生成数据的分布也天然和真实数据分布不一样,毕竟 G 网络输入是随机生成的噪声;
  • b图:随着训练的进行,D 网络的分类能力就比较好了,能够看到对于真实数据和生成数据,它是明显能够区分出来,也就是给出的几率是不一样的;
  • c图:因为 D 网络先行提升的性能,随后 G 网络开始追赶,G 网络的目标是学习真实数据的分布,即绿色的曲线,因此它会往蓝色曲线方向移动。由于 G 和 D 是相互对抗的,当 G 网络提高,也会影响 D 网络的分辨能力;
  • d图:当假设 G 网络不变(G已经优化到收敛状态),继续训练 D 网络,最优的状况会是,也就是当生成数据的分布趋近于真实数据分布的时候,D 网络输出的几率会趋近于 0.5(真实样本和伪造样本各占一半,生成器没法再伪造了,判别器也没法再优化了,也能够说对于判别器来讲其没法从样本中区分中真实样本和伪造样本),这也是最终但愿达到的训练结果,这时候 G 和 D 网络也就达到一个平衡状态。

0x3:算法伪码流程

论文给出的算法实现过程以下所示:

一些细节须要注意:

  • 首先 G 和 D 是同步训练,但二者训练次数不同,一般是 D 网络训练 k 次后,G 训练一次。主要缘由是 GAN 刚开始训练时候会很不稳定,须要让判别器D尽快先进入收敛区间;
  • D 的训练是同时输入真实数据和生成数据来计算 loss,而不是采用交叉熵(cross entropy)分开计算。不采用 cross entropy 的缘由是这会让 D(G(z)) 变为 0,致使没有梯度提供给 G 更新,而如今 GAN 的作法是会收敛到 0.5;
  • 实际训练的时候,做者是采用来代替,这是但愿在训练初始就能够加大梯度信息,这是由于初始阶段 D 的分类能力会远大于 G 生成足够真实数据的能力,但这种修改也将让整个 GAN 再也不是一个完美的零和博弈。

0x4:算法的优势

GAN的巧妙之处在于其目标函数的设定,由于此,GAN有以下几个优势:

  • GAN 中的 G 做为生成模型,不须要像传统图模型同样,须要一个严格的生成数据的几率表达式。这就避免了当数据很是复杂的时候,复杂度过分增加致使的不可计算。
  • GAN 不须要 inference 模型中的一些庞大计算量的求和计算。它惟一的须要的就是,一个噪音输入,一堆无标准的真实数据,两个能够逼近函数的网络。

0x5:算法的挑战与缺陷

初代GAN有一些缺点,或者是说挑战,笔者这里介绍以下:
  • 启动及初始化的问题:GAN的训练目标是让生成器和判别器最终达到一个纳什均衡状态,此时两个网络都没法继续再往前作任何优化,优化结束。梯度降低的启动会选择一个减少所定义问题损失的方法,可是并无理论保证GAN必定能够100%进入纳什均衡状态,这是一个高维度的非凸优化目标。网络试图在接下来的步骤中最小化非凸优化目标,可是最终可能致使进入震荡而不是收敛到底层真实目标。
  • GAN 过于自由致使训练难以收敛以及不稳定。
  • 梯度消失问题:原始 G 的损失函数没有意义,它是让 G 最小化 D 识别出本身生成的假样本的几率,但实际上它会致使梯度消失问题,这是因为开始训练的时候,G 生成的图片很是糟糕,D 能够垂手可得的识别出来,这样 D 的训练没有任何损失,也就没有有效的梯度信息回传给 G 去优化它本身,这就是梯度消失了。最后,虽然做者意识到这个问题,在实际应用中改用来代替,这至关于从最小化 D 揪出本身的几率,变成了最大化 D 抓不到本身的几率。虽然直观上感受是一致的,但其实并不在理论上等价,也更没有了理论保证在这样的替代目标函数训练下,GAN 还会达到平衡。这个结果会进一步致使模式奔溃问题。
  • 模型坍塌:基本原理是生成器可能会在某种状况下重复生成彻底一致的图像(也能够理解为梯度消失),这其中的缘由和博弈论中的启动问题相关。咱们能够这样来想象GAN的训练过程,
    • 先从判别器的角度试图最大化,再从生成器的角度试图最小化。若是生成器最小化开始以前,判别器已经彻底最大化,全部工做还能够正常运行;
    • 若是首先最小化生成器,再从判别器的角度试图最大化。若是判别器最大化开始以前,生成器已经彻底最小化,那么工做就没法运行。缘由在于若是咱们保持判别器不变,它会将空间中的某些点标记为最有多是真的而不是假的(由于生成器已经最小化了),这样生成器就会选择将全部的噪声输入映射到那些最可能为真的点上,这就陷入了局部最优的陷阱中了,优化过程就提早中止了。
固然上面提到的不少缺点已经在后续的学术论文中被新提出的修改算法解决了,咱们接下来讨论其主要解决思想。

0x6:提高GAN训练效果的一些方法

1. 中间层特征驱动损失函数

针对GAN不稳定的问题,学者们提出了经过 使用判别器中间层的特征来预测图像,并将结果做为监督信息来反馈给生成器
经过这种方式,训练获得的生成器的生成数据会匹配真实数据的统计特性以及判别器中间层的预期特征值。这样强迫判别器去寻找那些最能很好地判别真实数据的潜在特征,而不是那些由当前模型生成数据的表层特征。

2. 小批量度量输入样本类似度

模型坍塌的问题能够经过 引入额外的度量特征来解决(例如KL散度)。这样判别器每次收到的是一小批样本而不是一个单独样本,判别器能够使用例如KL散度来度量样本之间的距离,这样就很容易检测出当前的生成器是否是已经开始坍塌。从而阻止了生成器继续向局部最大似然点滑落。
整体来讲,小批量样本表现更接近实际,并且能够保证不一样样本之间在空间上有合适的距离。

3. 引入历史平均

历史平均的思想是加入一个惩罚项来惩罚那些和历史平均权重相差过多的权重值。即若是当前参数值和历史上最近t批该参数平均值的距离越近,给予的惩罚越大。
经过这种方式,能够缓解目标函数在收敛后期的震荡。

4. 单侧标签平滑

一般状况下咱们使用标签0表明真实图像,使用1表明伪造图像。咱们还能够使用一些更平滑的标签,例如0.1和0.9,它们能够使得网络在一些对抗的例子中更加健壮。

5. 输入规范化

使用tanh做为生成器最后一层激活函数,能够得到更平滑的收敛效果。

6. 批规范化

在每个批次的数据中标准化前一层的激活项, 即,应用一个维持激活项平均值接近 0,标准差接近 1 的转换。

7. 利用ReLU和MaxPool避免梯度稀疏

若是梯度稀疏,GAN博弈的稳定性会受到很大影响,Leaky ReLU对生成和判别器的梯度稀疏问题都会有缓解做用。

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/

 

4. 从生成模型和判别模型的几率视角看GAN

在阅读了不少GAN衍生论文以及GAN原始论文以后,笔者一直在思考的一个问题是:GAN背后的底层思想是什么?GAN衍生和改进算法的灵感和思路又是从哪里来的?

通过一段时间思考以及和同行同窗讨论后,我得出了一些思考,这里分享以下,但愿对读者朋友有帮助。

咱们先来看什么是判别模型和生成模型:

  • 判别式模型学习某种分布下的条件几率p(y|x),即在特定x条件下y发生的几率。判别器模型十分依赖数据的质量,几率分布p(y|x)能够直接将一个特定的x分类到某个标签y上。以逻辑回归为例,咱们所须要作的是最小化损失函数。
  • 生成式模型学习的是联合分布几率p(x,y),x是输入数据,y是所指望的分类。一个生成模型能够根据当前数据的假设生成更多新样本。

从几率论的视角来看,咱们来看一下原始GAN网络的架构:

  • 生成器本质上是一个由输入向量和生成器结构所表明的向量组成的联合几率分布P(v_input, v_G_structure)
    • v_input:表明一种输入向量,能够是随机噪声向量z
    • v_G_structure:网络本质上是对输入向量进行线性和非线性变化,由于能够将其抽象为一个动态变化的向量函数
  • 判别器本质上是一个由(真实样本,伪造样本)做为输入x,进行后验预测p(y|x)的几率模型

遵循这种框架进行思考,CGAN只是将v_input中的随机噪声z替换成了另外一种向量(文本或者标签向量),而Pix2pixGAN是将一个图像向量做为v_input输入GAN网络。

 

5. 从原始GAN网络中衍生出的流行GAN架构

GAN的发展离不开goodfellow后来的学者们不断的研究与发展,目前已经提出了不少优秀的新GAN架构,而且这个发展还在继续。为了让本博文能保持必定的环境独立性,笔者这里不作完整的罗列与枚举,相反,笔者但愿从两条脉络来展开讨论:

  • 解决问题导向:为了解决原始GAN或者当前学术研究中发现的关于GAN网络的性能和架构问题而提出的新理论与新框架
  • 新场景应用导向:为了将GAN应用在新的领域中而提出的新的GAN架构

0x1:DCGAN(Deep Convolutional Generative Adversarial Networks)

Alec Radford,Luke Metz,Soumith Chintala等人在“Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks”提出了DCGAN。这是GAN研究的一个重要里程碑,由于它提出了一个重要的架构变化来解决训练不稳定,模式崩溃和内部协变量转换等问题。从那时起,基于DCGAN的架构就被应用到了许多GAN架构。

DCGAN的提出主要是为了解决原始GAN架构的原生架构问题,咱们接下来来讨论下。

1. 生成器的架构优化

生成器从潜在空间中获得100维噪声向量z,经过一系列卷积上采样操做,将z映射到一个像素矩阵对应的空间中,以下图:

DCGAN经过下面的一些架构性约束来固化网络: 

  • 在判别器中使用步数卷积来取代池化层,在生成器中使用小步数卷积来取代池化层;
  • 在生成器和判别器中均使用批规范化,批规范化是一种经过零均值和单位方差的方法进行输入规范化使得学习过程固话的技术。这项技术在实践中被证明能够在许多场合提高训练速度,减小初始化不佳带来的启动问题,而且一般能产生更准确的结果;
  • 消除原架构中较深的全链接隐藏层,而且在最后只使用简单的平均值池化;
  • 在生成器输出层使用tanh,在其它层均使用ReLU激发;
  • 在判别器的全部层中都使用Leaky ReLU激发;

2. 模型训练

生成器和判别器都是经过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产生的手写数字输出

0x2:CGAN(Conditional GAN,CGAN)

1. 有输入条件约束的生成器网络架构 

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,同时链接了判别器和生成器网络。

 

2. 训练过程

在训练过程,咱们将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手写数字图像

0x3:CycleGAN(Cycle Consistent GAN,循环一致生成网络)

CycleGANs 由Jun-Yan Zhu,Taesung Park,Phillip Isola和Alexei A. Efros在题为“Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks”的论文中提出

CycleGAN用来实现不须要其余额外信息,就能将一张图像从源领域映射到目标领域的方法,例如将照片转换为绘画,将夏季拍摄的照片转换为冬季拍摄的照片,或将马的照片转换为斑马照片,或者相反。总结来讲,CycleGAN常备用于不一样的图像到图像翻译。

 

1. 循环网络架构

CycleGAN背后的核心思想是两个转换器F和G,其中:

  • F会将图像从域A转换到域B;
  • G会将图像从域B转换到域A;

所以,

  • 对于一个在域A的图像x,咱们指望函数G(F(x))的结果与x相同,即 x == G(F(x));
  • 对于一个在域B的图像y,咱们指望函数F(G(y))的结果与y相同,即 y == F(G(y));

和原始的GAN结构相比,由单个G->D的单向开放结构,变成了由两对G<->D组成的双向循环的封闭结构,但形式上依然是G给D输入伪造样本。但区别在于梯度的反馈是双向循环的。

2. 损失函数

CycleGAN模型有如下两个损失函数:

  • 对抗损失(Adversarial Loss):判别器和生成器之间互相对抗的损失,这就是原始GAN网络的损失函数公式:
  • 循环一致损失(Cycle Consistency Loss):综合权衡转换器F和G的损失,F和G之间是编码与解码的对抗关系,不可能同时取到最小值,只能获得总体的平衡最优值:

完整的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上找到。

0x4:StackGAN

StackJANs由Han Zhang,Tao Xu,Hongsheng Li还有其余人在题为“StackGAN: Text to Photo-Realistic Image Synthesis with Stacked Generative Adversarial Networks”的论文中提出。他们使用StackGAN来探索文本到图像的合成,获得了很是好的结果。

一个StackGAN由一对网络组成,当提供文本描述时,能够生成逼真的图像。

0x5:Pix2pix

pix2pix网络由Phillip Isola,Jun-Yan Zhu,Tinghui Zhou和Alexei A. Efros在他们的题为“Image-to-Image Translation with Conditional Adversarial Networks”的论文中提出。

对于图像到图像的翻译任务,pix2pix也显示出了使人印象深入的结果。不管是将夜间图像转换为白天的图像仍是给黑白图像着色,或者将草图转换为逼真的照片等等,Pix2pix在这些例子中都表现很是出色。

0x6:Age-cGAN(Age Conditional Generative Adversarial Networks)

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

 

6. 基于GAN自动生成Webshell样本

0x1:原始GAN结构在NLP领域应用的挑战

咱们用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生成的样原本看,网络很快陷入了一个局部最优区间中。

关于这个问题,学术界已经有比较多的讨论和分析,笔者这里列举以下:

  • 原始GAN主要应用实数空间(连续型数据)上,在生成离散数据(texts)这个问题上并不work。最初的 GANs 仅仅定义在实数领域,GANs 经过训练出的生成器来产生合成数据,而后在合成数据上运行判别器,判别器的输出梯度将会产生梯度反馈,告诉生成器如何经过略微改变合成数据而使其更加现实。通常来讲只有在数据连续的状况下,生成器才能够略微改变合成的数据,而若是数据是离散的,则不能简单的经过改变合成数据。例如,若是你输出了一张图片,其像素值是1.0,那么接下来你能够将这个值改成1.0001。若是输出了一个单词“penguin”,那么接下来就不能将其改变为“penguin + .001”,由于没有“penguin +.001”这个单词。 由于全部的天然语言处理(NLP)的基础都是离散值,如“单词”、“字母”或者“音节”。
  • 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 依旧很小。

  • Search complexity:在以SeqGAN为表明的用RNN做为生成器G的一类的工做中,对于 Reward 的评估都是基于句级别的,也就是会先使用 Monte Carlo Search 的方法将句子进行补全再交给 Discriminator,可是这个采样方法的时间复杂度是 O(nmL2),其中 n 是 batch size,m 是采样的次数,L 是句子的 max len。就 SeqGAN 的实验来讲,每次计算 reward 就会来带很大的开销。

0x2:GAN In NLP的主要发展思路和方向

基本上说,学术界对文本的见解是将其是作一个时序依赖的序列,因此主流方向是使用RNN/LSTM这类模型做为生成器来生成伪造文本序列。而接下要要解决的重点问题是,如何有效地将判别器的反馈有效地传递给生成器

  • 增长reward signal强度和平滑度:从这一点出发,现有很多工做一方法再也不使用简单的 fake/true probability 做为 reward。

    • LeakyGAN(把 CNN 的 feature 泄露给 generator),RankGAN (用 IR 中的排序做为 reward)等工做来提供更加丰富的 reward;

    • 另外一个解决的思路是使用 language model-based discriminator,以提供更多的区分度,北大孙栩老师组的 DP-GAN 在使用了 Languag model discrminator 以后,在 true data 和 fake data 中间架起了一座桥梁:

  • 使用离散数据的可导的损失函数:经过改造原始softmax函数,使用新的gumble softmax,它能够代替policy gradient,直接可导了。
  • 使用RL提供梯度反馈:使用RL的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
相关文章
相关标签/搜索