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