标签 GAN 下的文章

摘要

基于WGAN提出了一个攻击黑盒分类器的自然对抗样本生成框架。这些自然对抗样本可以帮助解释黑盒模型的决策行为和评估的准确性。最后在图像分类、文本蕴含和机器翻译上进行了测试。

网络训练

相比于GAN/WGAN,NaturalGAN多了一个部件逆变器(Inverter$I$).逆变器的作用是将原始样本x映射回低维的稠密向量空间.

这样做的原因在于:GAN生成器的输入是随机的低维高斯噪声z(比如dim=100),通过生成器G映射到高维空间,这个分布在高维空间是低维流形,也就是所谓的“撑不满”整个高维空间(这个在之前在WGAN中提到过).换句话说生成的对抗样本$x'$的分布其实很大程度上取决于低维噪声的分布.而GAN直接拿随机噪声作为G的输入,而忽略了真实样本x的分布$p_r$,尽管通过设置损失函数(JS散度或EM距离)可以拉近$p_r$和$p_g$,但是缺少了原始样本x的特征信息,会导致最终生成的对抗样本不够自然.

在AdvGAN中,是通过输入样本x或样本x的特征图来解决上述不足

image-20230412110629371

回到NatualGAN,训练共分两个步骤.首先是按照WGAN的策略,训练好生成器G和判别器C.这里G的输入暂时还是随机的高斯噪声.最大最小博弈函数为(EM距离)

$$ \operatorname*{min}_{\theta}\operatorname*{max}_{\omega}\mathbb{E}_{x\rightarrow p_{x(x)}}\left[C_{\omega}(x)\right]-\mathbb{E}_{z\sim p_{z(z)}}\left[C_{\omega}(G_{\theta}(z))\right] $$

训练第二步即上图中的黄色部分,固定判别器C,训练逆变器$I$和生成器G.$I$的输入是原始样本$x$,输出是和噪声$z$在同一维度空间的$z'$.对于判别器$I$,我们想得到的效果是:

  • 输入原始样本$x$产生的输出$z'$,将其输入到$G$中生成的新样本$x'$,应该和$x$足够接近(保证自然).这一部分可以称为重建误差(reconstruction error).
  • 对于某一噪声$z$经$G$生成的样本$x'$,将其输入到$I$中得到的输出$z'$.$z$和$z'$的分布应该足够接近.这一部分是训练逆变器$I$的映射能力.

综上两点可以得到$I$(也包括$G$)的损失函数.

$$ \operatorname*{min}_{\gamma}\mathbb{E}_{x\sim p_{x}(x)}\|G_{\theta}(I_{\gamma}(x))-x\|+\lambda\cdot\mathbb{E}_{z\sim p_{z}(z)}[{\mathcal{L}}(z,I_{\gamma}(G_{\theta}(z)))] $$

当距离$\mathcal{L}$使用$L_2 distance$时,$\lambda$=0.1(for images)

当距离$\mathcal{L}$使用$Jensen-Shannon distance$时,$\lambda$=1(for text data)

对抗样本生成

$G,C,I$都训练结束后,可以定义对抗样本为

$$ x^{*}=G_{\theta}(z^{*}){\mathrm{~where~}}z^{*}=\arg\operatorname*{min}_{\tilde{z}}\parallel\tilde{z}-\mathbb{Z}_{\gamma}(x)\parallel\operatorname{s.t.}f(G_{\theta}({\tilde{z}}))\neq f(x) $$

首先将原始样本x输入到逆变器$I$中,得到低维向量$z'$,随后对$z'$产生一定扰动生成$\tilde z$.找到扰动最小的$z$,使$f(G(\tilde z))\neq f(x) $.

扰动最小自然是为了保证对抗样本足够自然

image-20230412114336821

关于$\tilde z$的搜索,论文给了两种算法.一种是逐渐增加扰动的暴搜算法,另一个是基于二分的粗粒度到细粒度的搜索算法.具体看图~

image-20230412114707401

image-20230412114757180

算法二先通过二分确定扰动范围(<$\delta r$),然后再爆搜.算法效率是算法一的四倍,效果可以持平算法一

生成结果

先看一对有意思的图,图片(a)是数据x的分布情况,图(b)是逆变器的输出结果,可以看到$z'$基本满足正态分布,图(c)显示了二元分类器的决策边界以及NatualGAN生成的对抗样本$x^*(Our)$的位置.而通过FGSM生成的对抗样本可能在右侧.虽然两者都使辨别器犯错,但是$x^*(Our)$在蓝色样本的流形上,无疑会更加自然.

image-20230412115228352

最后是在MNIST手写数字数据集上的对比测试.

image-20230412115741285

简介

本篇论文主要是对AdvGAN进行了一些小改进,证明了在非定向攻击中,潜在特征作为对抗生成的先验比整个输入图像更好,同时消除了对生成器遵循基于编码器-解码器的架构的需要,从而减少了训练/推理开销。
论文地址:只有4页的AdvGAN++

网络框架

跟AdvGAN相比,主要区别在于生成器G的输入产生了变化,从原来的原始图像x变成了图像x的特征图与噪声向量的级联。而图像x的特征图是通过目标网络M的特征提取器f得到的。这两点就是AdvGAN和原始版本的最大区别。

image-20230410170801954

损失函数

与AdvGAN类似,损失函数为

$$ L(G,D)=L_{GAN}+\alpha L_{adv}+\beta L_{pert} $$

其中

$$ L_{GAN}=E_x[\log D(x)+E_xlog(1-D(G(z|f(x)))]\\ L_{adv}=E_x[M_t(G(z|f(x)))]\\ L_{pert}=E_x||x-G(z|f(x))||_2 $$

$L_{adv}$中的$M_t$是指目标模型M将输入识别成类别t的概率(softmax处理后).其他部分与AdvGAN的损失函数一致,这里不再赘述.AdvGAN - JJJYmmm Blog

训练过程

训练过程如算法1所示,跟AdvGAN/GAN的方法一致,使用min-max博弈依次迭代G/D.详见Generative Adversarial Nets - JJJYmmm Blog的理论证明.

image-20230410171717232

总结

AdvGAN++算是一个对原版的小迭代.一句话总结:使用目标模型的特征抽取器抽取原始样本的潜在特征,并将其作为生成器G的输入,取消了G需要是"encoder-decoder"架构的限制,达到了比原版更好的性能和效果.

Reference

论文地址在这[1511.06434] Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks (arxiv.org)

参考博客GAN学习指南:从原理入门到制作生成Demo - 知乎 (zhihu.com)

原理篇

DCGAN的原理和GAN是一样的,这里就不在赘述。它只是把G和D换成了两个卷积神经网络(CNN)。DCGAN对卷积神经网络的结构做了一些改变,以提高样本的质量和收敛的速度,这些改变有:

  • 取消所有pooling层。G网络中使用转置卷积(transposed convolutional layer)进行上采样,D网络中用加入stride的卷积代替pooling。
  • 在D和G中均使用Batch Normalization
  • 去掉FC层,使网络变为全卷积网络
  • G网络中使用ReLU作为激活函数,最后一层使用tanh
  • D网络中使用LeakyReLU作为激活函数

DCGAN中的G网络示意:

image-20230405183038001

数据篇

作为伪二次元,选择参考这篇立本友人的工作Chainerで顔イラストの自動生成 - Qiita

突然发现是15年的帖子,8年前的旧东西力(悲

image-20230406102514776

图片采集

写一个简单爬虫,我们爬这个动漫网站http://konachan.net/. 注意需要翻墙,不然需要人机认证

image-20230406103549591

原理就是遍历网站的不同页面,先把html下载下来,用BeautifulSoup解析html,找到缩略图的src属性,直接下载就好了.也没有针对反爬虫做什么东西(快说谢谢konachan)
import requests
from bs4 import BeautifulSoup
import os
import traceback

def download(url, filename):
    if os.path.exists(filename):
        print('file exists!')
        return
    try:
        r = requests.get(url, stream=True, timeout=60)
        r.raise_for_status()
        with open(filename, 'wb') as f:
            for chunk in r.iter_content(chunk_size=1024):
                if chunk:  # filter out keep-alive new chunks
                    f.write(chunk)
                    f.flush()
        return filename
    except KeyboardInterrupt:
        if os.path.exists(filename):
            os.remove(filename)
        raise KeyboardInterrupt
    except Exception:
        traceback.print_exc()
        if os.path.exists(filename):
            os.remove(filename)


if os.path.exists('../imgs') is False:
    os.makedirs('../imgs')

start = 1
end = 8000
for i in range(start, end + 1):
    url = 'http://konachan.net/post?page=%d&tags=' % i
    print(url)
    html = requests.get(url).text
    soup = BeautifulSoup(html, 'html.parser')
    for img in soup.find_all('img', class_="preview"):
        target_url = img['src']
        filename = os.path.join('../imgs', target_url.split('/')[-1])
        # print(filename)
        download(target_url, filename)
    print('%d / %d' % (i, end))

图片裁剪

下载下来的图片还是太大了,据友人说整张生成的效果不是很好,所以需要用到人脸裁剪工具,把图片裁小一点(正常二次元都拿人头当头像,这很合理).工具同样用用人提到的lbpcascade_animeface,基于opencv的库.

image-20230406103658145

import cv2
import sys
import os.path
from glob import glob

def detect(filename, cascade_file="../lbpcascade_animeface-master/lbpcascade_animeface.xml"):
    if not os.path.isfile(cascade_file):
        raise RuntimeError("%s: not found" % cascade_file)

    cascade = cv2.CascadeClassifier(cascade_file)
    image = cv2.imread(filename)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray = cv2.equalizeHist(gray)

    faces = cascade.detectMultiScale(gray,
                                     # detector options
                                     scaleFactor=1.1,
                                     minNeighbors=5,
                                     minSize=(48, 48))
    for i, (x, y, w, h) in enumerate(faces):
        face = image[y: y + h, x:x + w, :]
        face = cv2.resize(face, (96, 96))
        save_filename = '%s-%d.jpg' % (os.path.basename(filename).split('.')[0], i)
        cv2.imwrite("../faces/" + save_filename, face)


if __name__ == '__main__':
    if os.path.exists('../faces') is False:
        os.makedirs('../faces')
    file_list = glob('../imgs/*.jpg')
    for filename in file_list:
        detect(filename)

最后效果就是这样,很糊是因为每张图片只有64x64.友人用的是96x96.而我为了加快速度就直接用DCGAN本来的64x64了.(好处是可以直接扒Pytorch写的DCGAN)

image-20230406103902973

训练篇

立本友人在博客原文用的是Chainer,我压根没听过的框架.所以我们就用Pytorch吧(笑)

这部分代码我们直接去Pytorch官方库上去找.链接在这examples/dcgan at main · pytorch/examples (github.com)

我们只需要稍微修改一下项目结构和dataset即可.接下来就稍微说一下主要修改的地方吧.

dataset

dataset其实大部分也是抄的Pytorch,比如transform那部分,最后一个Normalize我就第一次见(标准差都是0.5???),虽然很像改成经常见面的ImageNet的数据,但是二次元好像和ImageNet也不搭(

具体来说就是把图片整到img文件夹下,然后直接用ImageFolder创建dataset.这里注意img下面还需要子文件夹放图片,这个子文件夹对于ImageFolder来说就是不同的类别.当然我们这里不需要用到类别标签,因为所有图片的label都是1,意味着是真实图片.

class ReadData():
    def __init__(self,data_path,image_size=64):
        self.root = data_path
        self.image_size = image_size
        self.dataset = self.getdataset()
    
    def getdataset(self):
        dataset = datasets.ImageFolder(
                root=self.root,
                transform=transforms.Compose([
                    transforms.Resize(self.image_size),
                    transforms.CenterCrop(self.image_size),
                    transforms.ToTensor(),
                    transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)),
                ])
            )
        print(f'Total size of dataset:{len(dataset)}')
        return dataset
    
    def getdataloader(self,batch_size=128):
        dataloader = DataLoader(
            self.dataset,
            batch_size=batch_size,
            shuffle=True,
            num_workers=8
        )
        return dataloader
    
if __name__ == '__main__':
    dset = ReadData('./imgs')
    print("ok")
    dloader = dset.getdataloader()

model

GeneratorDiscriminator两个类没什么好说的,都是Pytorch的(甚至变量名都没改) 注意这里还有一个权重初始化函数weights_init

class Generator(nn.Module):
    def __init__(self,nz,ngf,nc):
        super(Generator,self).__init__()
        self.nz = nz
        self.ngf = ngf
        self.nc = nc

        self.main = nn.Sequential(
            # input is Z, going into a convolution
            nn.ConvTranspose2d(self.nz, self.ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(self.ngf * 8),
            nn.ReLU(True),
            # state size. (ngf*8) x 4 x 4
            nn.ConvTranspose2d(self.ngf * 8, self.ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(self.ngf * 4),
            nn.ReLU(True),
            # state size. (ngf*4) x 8 x 8
            nn.ConvTranspose2d(self.ngf * 4, self.ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(self.ngf * 2),
            nn.ReLU(True),
            # state size. (ngf*2) x 16 x 16
            nn.ConvTranspose2d(self.ngf * 2, self.ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(self.ngf),
            nn.ReLU(True),
            # state size. (ngf) x 32 x 32
            nn.ConvTranspose2d(self.ngf, self.nc, 4, 2, 1, bias=False),
            nn.Tanh()
            # state size. (nc) x 64 x 64
        )
    def forward(self,input):
        return self.main(input)


class Discriminator(nn.Module):
    def __init__(self,ndf,nc) -> None:
        super().__init__()
        self.ndf=ndf
        self.nc=nc
        self.main=nn.Sequential(
            # input is (nc) x 64 x 64
            nn.Conv2d(self.nc, self.ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf) x 32 x 32
            nn.Conv2d(self.ndf, self.ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(self.ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*2) x 16 x 16
            nn.Conv2d(self.ndf * 2, self.ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(self.ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*4) x 8 x 8
            nn.Conv2d(self.ndf * 4, self.ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(self.ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*8) x 4 x 4
            nn.Conv2d(self.ndf * 8, 1, 4, 1, 0, bias=False),
            # state size. (1) x 1 x 1
            nn.Sigmoid()
        )
    
    def forward(self,input):
        return self.main(input)
    
def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        nn.init.normal_(m.weight.data,0.0,0.02)
    elif classname.find('BatchNorm') != -1:
        nn.init.normal_(m.weight.data,1.0,0.02)
        nn.init.constant_(m.bias.data, 0)

train

模型训练主要是之前提到的V(G,D)函数的实现,我们其实可以发现它的形式很像二元交叉熵.所以这里直接用BCELoss就可以实现.BCELoss — PyTorch 2.0 documentation

$$ BCELoss = -(y\log(x)+(1-y)\log(1-x)) $$

  • 更新D模型时,需要将$\log(D(x))+\log(1-D(G(z)))$最大化,对于第一项.我们可以选择将$-log(D(x))$最小化,所以我们用BCE时,使用的标签值是1.这样就可以用反向传播使$\log(D(x))$最大化了.即

$$ BCELoss_{D(x)} = -\log(D(x)) $$

  • 第二项同理,对于$D(G(z))$我们使用标签值为0.那么同样可以反向传播使$\log(1-D(G(z)))$最大

$$ BCELoss_{D(G(z))}=-\log(1-D(G(z))) $$

  • 更新G模型时,原论文需要让$\log(1-D(G(z)))$尽可能小,不过如果D模型比较强的话,这个值会很小,会出现梯度小时的情况让G很难训练.所以论文里也提到可以让$\log(D(G(z)))$尽可能大.那么对于$D(G(z))$,我们使用标签值为1(尽管这是一张假图片)

$$ BCELoss_{D(G(z))}=-log(D(G(z))) $$

其他的就没什么不一样啦~大家直接看Pytorch源码即可.

效果篇

  • epoch=20,感觉还可以

result_20

  • epoch=120,挺好的

result_120

  • epoch=150,最好效果

result_150

  • epoch=155,不对劲

result_155

  • epoch=180,寄力

result_180

  • epoch=190,Generator摆烂了

result_190

  • 看一下训练情况.发现D模型Loss已经降到0,且分类效果非常好.而G师傅已经不行了

image-20230406125544922

上述对比警示我们: D模型太好G模型是会摆烂的

最后代码放在仓库里了.JJJYmmm/Pytorch-DCGAN-Anime (github.com)

做了5题蓝桥杯中等题的JJJYmmm身心十分疲倦,决定入门一下生成式对抗网络(GAN).本次看的是开山之作: Ian Goodfellow的Generative Adversarial Nets

摘要

摘要指出本篇工作提出了一个新的框架,通过两个模型相互对抗来提高他们的生成(检测)效果.具体来说:一个是捕获数据分布(这个会在价值函数中体现)的生成模型G,还有一个是判断样本来自真实数据还是G的判别模型D.

G的训练目标是使D出错的概率最大化,最后希望达到的效果就是G生成的数据足够"以假乱真",D无法分别其真实性,在这种情况下,生成器G的分布$P_g=P_{data}$(P为概率密度函数),且D的输出恒为1/2.

这里的G/D可以简单都是用MLP,那么模型训练只需要反向更新即可,也不需要使用马尔科夫链或者近似推理网络.

GAN属于隐式生成网络,它不需要显示表示真实数据的分布$P_{data}$,这个分布蕴含在G中,我们只关注从这个分布采样出来的样本点即可,即G(z).

Introduction

引言基本是摘要的一个补充,主要谈了一下当时深度学习做生成模型效果不太好.之前做生成模型的思想主要是通过一些可学习参数去构造真实的数据分布$P_{data}$,这些参数通过调整尽量使$P_g=P_{data}$,调整的方式就是通过最大化对数似然函数.具体可以见我之前的博客~

本次提出的GAN则属于隐式生成模型,不需要知道$P_g$的具体形式,只需要最后检测模型D分不出来就可以了.

Related Work

讲了一下VAEs/NCE,还有jurgen在92年提到的predictability minimization.据说jurgen作为这篇论文的审稿人review很有意思~

Value Fuction V(G,D)

接下来就讲到价值函数啦,非常关键的一个地方.当然这里还需要对模型的输入进行进一步解释.

当G和D都是MLP的时候,GAN非常容易应用.首先是为了学习生成器在数据x上的分布$P_g$,先要定义输入噪声z的先验分布$p_z(z)$,然后通过MLP得到一个映射关系$G(z;\theta_g)$.接下来对于检验器D,MLP的映射$D(x,\theta_d)$输出一个向量,1表示x来自真实样本,0表示来自G.G的工作是让$log(1-D(G(z)))$尽可能小,而D的工作是让$log(D(x))$尽可能大的同时让$log(1-D(G(z)))$也尽可能大.根据上述描述可以得到价值函数V(G,D)

$$ \mathop{min}\limits_{G}\mathop{max}\limits_{D}V(G,D)=\mathbb{E}_{x\sim p_{data(X)}[logD(x)]} + \mathbb{E}_{z\sim p_{z(z)}log[1-D(G(z))]} $$

GAN的训练过程见算法1,先k步迭代检测器D,在迭代生成器G.

image-20230405160410682

训练过程可以总结为一下四张图,假设数据x和噪声z都是一维向量.x的分布是灰色的正态分布,G(z)是绿色的正态分布,而检测器D是虚线.首先如图b训练D,它对x和g(z)做了一定区分;接下来训练G,让G(z)的分布曲线往x靠近,如图c.最后当两个分布曲线足够贴合时,检测器D无法分辩任何东西,所以是一条直线.

image-20230405160507964

Theoretical Results

理论结果有两个:首先给出固定G的最佳辨别器$D^*$,其次是证明了价值函数的有效性.

Theorem 1.

对于固定的G,最佳分辨器$D^*$为

$$ D^*_G(x) = \frac{p_{data}(x)}{p_{data}(x)+p_g(X)} $$

证明通过对V(G,D)进行变换得到,首先将g(z)换元成x

$$ \begin{align*} V(G,D)&=\int_{x}p_{\mathrm{data}}(x)\log(D(x))d x+\int_{\cdot}p_{z}(z)\log(1-D(g(z)))dz\\ &=\int_{x}p_{\mathrm{data}}(x)\log(D(x))+p_{g}(x)\log(1-D(x)) dx \end{align*} $$

上式则可以转变成以下形式$\ y\to a\log(y)+b\log(1-y)$,其中a,b代表着data和G的数据分布,这个函数是凸函数,可以得到最优点时$y=\frac{a}{a+b}$.那么$D^*$得证

接下来我们可以把$D^*$带入V(G,D)中,得到

$$ \begin{align*} C(G)&=\mathop{max}\limits_{D}V(G,D) \\ \\ &=\mathbb{E}_{x\sim p_{\mathrm{data}}}[\log D_{G}^{*}(x)]+\mathbb{E}_{z\sim p_{z}}[\log(1-D_{G}^{*}(G(z)))]\\ \\ &=\mathbb{E}_{x\sim p_{\mathrm{data}}}[\log D_{G}^{*}(x)]+\mathbb{E}_{x\sim p_{g}}[\log(1-D_{G}^{*}(x))]\\ \\ &=\mathbb{E}_{x\sim p_{\mathrm{{data}}}}\left[\log{\frac{p_{data}(x)}{p_{\mathrm{data}}(x)+p_{g}(x)}}\right]+{\mathbb{E}_{x\sim p_{g}}}\left[\log{\frac{p_{g}(x)}{p_{\mathrm{data}}(x)+p_{g}(x)}}\right] \end{align*} $$

接下来我们考虑在D*的情况下V(G,D)如何达到最小,即G也训练到极致,我们对刚刚的C(G)做如下变形,具体来说就是在分式那个地方乘1/2,使其也变成一个分布.(因为之前两个分布求积分结果是2,如果/2积分结果就是1,可以看成是一个新分布)

再根据KL散度的定义可以把C(G)表示成如下形式,KL>=0,取等条件就是两个分布相等,根据两个KL散度可以得到$p_g=p_{data}$时,C(G)有最小值$-log(4)$

$$ C(G)=-\log(4)+K L\left(p_{\mathrm{data}}\Big|\Big|\frac{p_{\mathrm{data}}+p_{g}}{2}\right)+K L\left(p_{g}\Big|\Big|\frac{p_{\mathrm{data}}+p_{g}}{2}\right) $$

对于这种对称性的KL散度,我们还有一个名称称呼它:琴生-香农散度,所以C(G)进一步表示成如下

$$ C(G)=-\log(4)+2\cdot J S D\left(p_{\mathrm{data}}||p_{g}\right) $$

Theorem 2.

第二个理论证明是假定在G/D具有充分能力的情况下,通过算法1,每次D都能迭代到D*.这样G最终可以使$p_g$收敛到$p_{data}$.这个主要是通过说明函数的凹凸性来证明的.具体来说就是把V(G,D)写成$p_g$的函数,即函数的函数,然后balaba....不是非常懂,直接贴原文了

image-20230405165000254

Experiments

实验就是生成数字/人脸这些....没什么好说的(不过在当时应该算是效果挺好的吧)

image-20230405165301613