生成对抗网络

    生成对抗网络GAN(Generative Adversarial Networks)是由蒙特利尔大学Ian Goodfellow在2014年提出的机器学习架构,与之前介绍的神经网络不同,GAN最初是作为一种无监督的机器学习模型,生成对抗网络的变体也有很多,如GAN、DCGAN、CGAN、ACGAN等,无论对抗生成网络形式为何种,对抗生成网络都由两部分组成:判别器(Discriminator)常用D表示;另一个称为生成器(Generator)用G表示。判别器与生成器的博弈过程是生成对抗网络学习过程,判别器通过不断学习提高自身的识别能力,而生成器利用判别器不断提升生成样本能力,当判别器对生成器生成的样本判断真伪概率为50%时,生成器训练完时说明生成器生成的样本达到了以假乱真的效果了,生成对抗网络整个训练过程类似金庸小说《神雕侠侣》中的周伯通,通过左右互搏术提高自身的武学修为。

一、生成对抗网络训练过程

1.1、判别器与生成器相互博弈过程

    以最简单的GAN为例,用xi代表判别器输入数据,xi有两种可能,当xi来自真实样本数据输入时标记为yi=1,而当xi来自生成器输入时数据标记为yi=0,训练判别器使用交叉熵作为损失函数:

交叉熵.png 

上式中pi代表真实的概率分布,在①式中,pi等于0或1,即pi等于yi;qi代表判别器模拟的概率分布,可用qi=D(xi)表示,有了这些设定后,①式可写为:

交叉熵2.png 

由于yi取值只有0,1两种可能,所以上面表达式交叉熵3.png最终形式是p1.png(yi=1),要么是p2.png(yi=0),公式②是两分类问题的交叉熵函数。在训练判别器过程中往往是输入一条真实样本,再输入一条来自生成器生成数据,输入数据xi来自真实样本或生成器的概率为50%,将公式②两边同乘以1/2有:

推导1.png 

推导过程使用数学期望的离散公式:

数学期望公式.png

判别器训练过程为求③交叉熵最小值,换言之,是求下面方程的最大值:

公司4.png 

④中函数V(D)中的D代表判别器中未知参数集合。通过上面训练过程,判别器能逐渐识别出生成数据的真伪。当判别器输入xi来自生成器时,G将噪音数据zi处理后得到输入xi,用代数式表达为:

gg.png

生成器目标是使得生成的图像G(z)与真实的数据没有差异,即使得D(G(z))函数值变大,代入④后有:

函数.png

上式中的G代表生成器中未知参数集合,D(G(z))函数值变大时,log1.png变小,所以生成器G的训练目标是让公式④变小。综合起来看,对于函数V(D,G)先将G的参数集合视为符号常量,判别器的参数集合D视为变量求函数V最大值,该最大值是含G的符号常量代数式,此时再求函数最小值即可获得生成器的最优参数值,整个训练过程用公式表示为:

vd.png

这个最优点叫做鞍点,svm支持向量机详解一篇中曾详细介绍过,如下图所示:

v函数.jpg

上图中曲线代表函数V(D,G*),其含义为参数集合G固定为G*,以D为自变量生成的一个函数,求V(D,G*)最大值,参数集合G取不同值就相应的生成无数个曲线,这曲线最大值形成一个马鞍形背脊线:

最优点.jpg

背脊线上最小值为鞍点,即为参数集合G和D的最优解。GAN的训练过程也可以归纳为:第一步max过程,训练判别器使D能最大程度的识别输入数据真伪;第二步min过程,利用逐渐提升的判别器来训练生成器,使生成器G能生成以假乱真的数据。先max后min反应出判别器与生成器相互博弈的过程,而最优解是纳什均衡点。

1.2、实现GAN网络

    利用上面推导可以实现一个简单的生成对抗网络GAN,下面的代码中利用minist数据集生成0到9的图片,判别器和生成器都是简单的全连接神经网络,判别器输出是一个0到1的小数,代表判别器识别样本真伪的概率;生成器输入以一组标准正态分布噪音,输出是一个28*28的图片,利用GAN生成器最后能生成较为逼真的手写数字图片。

minist数据集下载地址:minist数据集下载,下载后放入程序目录下的dataset目录中。

=imgdataset.py=:图片加载辅助类

import os
import numpy as np
import gzip
import torch.utils.data as Data
class DataSet(Data.Dataset):
    """
        读取数据、初始化数据
    """
    def __init__(self, folder, data_name, label_name,transform=None):
        (train_set, train_labels) = self.load_data(folder, data_name, label_name) # 其实也可以直接使用torch.load(),读取之后的结果为torch.Tensor形式
        self.train_set = train_set
        self.train_labels = train_labels
        self.transform = transform
        pass

    def load_data(self,data_folder, data_name, label_name):
        """
            data_folder: 文件目录
            data_name: 数据文件名
            label_name:标签数据文件名
        """
        with gzip.open(os.path.join(data_folder, label_name), 'rb') as lbpath:  # rb表示的是读取二进制数据
            y_train = np.frombuffer(lbpath.read(), np.uint8, offset=8)

        with gzip.open(os.path.join(data_folder, data_name), 'rb') as imgpath:
            x_train = np.frombuffer(
                imgpath.read(), np.uint8, offset=16).reshape(len(y_train), 28, 28)
        return (x_train, y_train)

    def __getitem__(self, index):

        img, target = self.train_set[index], int(self.train_labels[index])
        if self.transform is not None:
            img = self.transform(img)
        return img, target

    def __len__(self):
        return len(self.train_set)

=GAN_Net.py=:判别器与生成器

import torch
import torch.nn as nn

class D_Net(nn.Module):
    def __init__(self):
        super().__init__()

        self.dnet = nn.Sequential(
            nn.Linear(784,512),
            nn.ReLU(),
            nn.Linear(512,256),
            nn.ReLU(),
            nn.Linear(256,1),
            nn.Sigmoid()
        )

    def forward(self,x):
        x = self.dnet(x)
        return x

class G_Net(nn.Module):
    def __init__(self,noise_size):
        super().__init__()
        self.gnet = nn.Sequential(
            nn.Linear(noise_size,256),
            nn.ReLU(),
            nn.Linear(256,512),
            nn.ReLU(),
            nn.Linear(512,784)
        )

    def forward(self,x):
        x = self.gnet(x)
        return x

=GAN_Train.py=训练过程代码

from GAN_Net import D_Net,G_Net
import torch.utils.data as Data
from imgdataset import DataSet
import torch
import torch.nn as nn
from torchvision.datasets import MNIST
from torchvision import transforms
from torch.utils import data
from torchvision.utils import save_image
import os
dataPath='dataset'
def loadDataset():
    trainDataset = DataSet(dataPath,
                                   "train-images-idx3-ubyte.gz",
                                   "train-labels-idx1-ubyte.gz",
                                   transform=transforms.ToTensor())

    testDataset = DataSet(dataPath,
                                  "t10k-images-idx3-ubyte.gz",
                                  "t10k-labels-idx1-ubyte.gz",
                                  transform=transforms.ToTensor())
    return trainDataset, testDataset

def gnloader(loadbatsize):
    trainDataset, testDataset=loadDataset()
    # 训练数据和测试数据的装载
    train_loader = Data.DataLoader(
        dataset=trainDataset,
        batch_size=loadbatsize,
        shuffle=True
    )

    test_loader = Data.DataLoader(
        dataset=testDataset,
        batch_size=loadbatsize,
        shuffle=True,
    )
    return train_loader,test_loader
class Trainer:
    def __init__(self):
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.loss_fn = nn.BCELoss()
    def train(self):
        if not os.path.exists("./gan_img"):
            os.mkdir("./gan_img")
        if not os.path.exists("./gan_params"):
            os.mkdir('./gan_params')
        BATCH_SIZE = 100
        NUM_EPOCHS = 150
        INPUT_SIZE=128
        train_loader, test_loader=gnloader(BATCH_SIZE)
        d_net = D_Net().to(self.device)
        g_net = G_Net(INPUT_SIZE).to(self.device)
        d_opt = torch.optim.Adam(d_net.parameters())
        g_opt = torch.optim.Adam(g_net.parameters())

        for epochs in range(NUM_EPOCHS):
            for i,(x,y) in enumerate(train_loader):
                #训练判别器
                N = x.size(0)
                real_img = x.to(self.device).reshape(N,-1)
                real_label = torch.ones(N,1).to(self.device)
                fake_label = torch.zeros(N,1).to(self.device)
                real_out = d_net(real_img)
                d_real_loss = self.loss_fn(real_out,real_label)
                z = torch.randn(N,INPUT_SIZE).to(self.device)
                fake_img = g_net(z)
                fake_out = d_net(fake_img)
                d_fake_loss = self.loss_fn(fake_out,fake_label)
                d_loss = d_real_loss+d_fake_loss
                d_opt.zero_grad()
                d_loss.backward()
                d_opt.step()

                #训练生成器
                z = torch.randn(N,INPUT_SIZE).to(self.device)
                fake_img = g_net(z)
                fake_out = d_net(fake_img)
                g_loss = self.loss_fn(fake_out,real_label)
                g_opt.zero_grad()
                g_loss.backward()
                g_opt.step()

                if i % 500 == 0:
                    print("Epoch:{}/{},d_loss:{:.3f},g_loss:{:.3f},"
                          "d_real:{:.3f},d_fake:{:.3f}".
                          format(epochs, NUM_EPOCHS, d_loss.item(), g_loss.item(),
                                 real_out.data.mean(), fake_out.data.mean()))

                    real_image = real_img.cpu().data.reshape([-1, 1, 28, 28])
                    save_image(real_image, "./gan_img/real_img.jpg", nrow=10, normalize=True, scale_each=True)
                    fake_image = fake_img.cpu().data.reshape([-1, 1, 28, 28])
                    save_image(fake_image, "./gan_img/fake_img.jpg", nrow=10, normalize=True, scale_each=True)
                    torch.save(d_net.state_dict(), "gan_params/d_net.pth")
                    torch.save(g_net.state_dict(), "gan_params/g_net.pth")
if __name__ == '__main__':
    t = Trainer()
    t.train()

程序中每次迭代选择100组数据,如果来自真实的样本则以维度为[100,1]、值全为1的张量real_label 作为标签输入到判别器中;如果输入来自生成器,则以[100,1]、值全为0的张量fake_label 作为标签输入到判别器中,通过优化过程提升判别器识别能力,这对应上述推导过程中max阶段。随后利用升级后的判别器训练生成器,优化生成器参数从而不断生成能以假乱真的图片,这对应推导中min阶段。训练过程中只是简单标注数据的真伪,没有判断每个真实图片对应是哪个数字的图片,可见GAN是一种无监督的机器学习结构,代码生成后图片如下:

生成图片png.png

二、DCGAN

    上面GAN网络生成器和判别器都是简单全连接神经网络,DCGAN的网络部分为卷积网络,具体来说DCGAN的判别器是一个卷积网络,输出仍然是一个0到1之间的实数,代表判别器判断数据的真伪概率;而生成器是一个反卷积网络输出的是一个具体的数据,本篇中生成器生成的是一个28*28的单通道图。判别器利用卷积网络实现降维提取数据主要特征,而反卷积常称作上采样,是增维的过程。

    在卷积神经网络一章中详细介绍过卷积网络的训练过程,设输入尺寸为i,卷积核为k方阵,步长s,补padding层数为p,卷积之后的尺寸为:

卷积输出.png 

公式⑤中[]代表取不大于该小数的整数,如[1.4]=1,[2.8]=2等,卷积输出尺寸公式很好理解:

1618907677763047397.jpg

i+2p-k可以理解为上图绿色右边框'行程',s可以理解为速度,i+2p-k除以s为绿色右边框走完全程一共所需要'时间',每一格时间代表输出的一个信息,特殊一些,这个'时间'必须是整数。同时如果p=0代表valid模式,p>0代表full或same模式,几种卷积模式如下图:

a)valid模式,padding=0

1618908168399031507.png

b)full模式,开始时卷积核右下角与图片左上角重叠,结束时卷积核左上角与图片右下角重叠。

1618908204946052170.png

c)same模式,卷积核的中心始终和原图的像素重合,如果步长为1,输出尺寸等输入图片尺寸。

1618908220726022843.png

再来看反卷积,反卷积也称转置卷积,如果一个卷积操作将5*5图片变为3*3图片,那么反卷积可实现将3*3图片恢复到5*5的图片,也是说反卷积能实现卷积在尺寸上逆操作,需要强调的是反卷积只是在尺寸上还原,并不能实现将信息全部还原回去,反卷积后输出尺寸可以通过公式⑤利用换元法推导出来,分几种情况讨论。

1、设原卷积过程步长为1,padding=0,卷积核大小为k,根据公式⑤,得卷积后输出尺寸o

ip1.png 

对于反卷积而言,上式中o对应输入尺寸,i对应反卷积的输出,不妨设i'为反卷积的输入尺寸,o'为反卷积输出尺寸,显然有:

等式1.png

上面方程组代入⑥有

ip2.png (6.1)

为了能把上式写成和卷积公式⑤一样的形式,即卷积输出.png,(6.1)可变换为:

从.png

通过换元法将反卷积变成了卷积过程,同时这种情况下反卷积步长s',padding p',核大小k',输出o'有以下对应关系:

标一.png表1

下图是该情况下一个例子:

列1.png

2、卷积过程步长s>1,padding层数 p>0,输入尺寸i,且i+2p-k能被步长s整除

由于i+2p-k能被步长s整除,利用公式⑤计算卷积输出尺寸时,依然能脱去取整符号[]:

vd2.png

同样,利用代数分解法将上式变为公式⑤形式:

从2.png

将上面推导结果对号入座,换算表如下:

标而.png表2

下面是情形二的一个例子:

离2.png

当卷积过程步长s>1时,做反卷积时会在输入图像相邻两列插入s-1列0,这也是卷积步长大于1时反卷积的一个特点,通过插入0值列才能使得反卷积步长始终为1。

3、卷积过程步长s>1,padding层数 p>0,输入尺寸i,且i+2p-k不能被步长s整除

此时i+2p-k不能被步长s整除,不妨设a为i+2p-k取余s的结果,即a=(i+2p-k) mod s,由整数取余性质可知,此时i+2p-k-a就可以被s整除了,引入a后即可脱去取整符号[]:

屠刀1.png

与情形二类似,利用多项式分解可得:

从44.png

上式中k-p-1为反卷积padding层数,需要主要的是a的含义,a是在在输入数据所有外层加k-p-1层padding后,再在最上面和最右面添加padding层数,在tensorflow和pytorch中叫做outpadding,下面给出一个该情形下的例子:

列4.png

可以看到反卷积时现在外层加了p'=3-1-1=1层padding,而a=(6+2-3) mod 2=1,所以在图像的上侧和右侧又加了1层padding,outpadding的引入是为了得到较为标准的输出,举一个例子,利用一个3*3的卷积核对输入尺寸7*7图像做步长2,padding为0的卷积,利用公式5可得到卷积的结果尺寸为[(7-3)/2]+1=3,而同样方法对输入尺寸8*8图像做卷积输出也是3:[(8-3)/2]+1=3,这样一来对于反卷积而言,对一个3*3的图像做反卷积会有两个结果,这显然不是我们希望发生的结果,有了outpadding后,反卷积的结果总是原卷积步长的整数倍,这就消除了歧义使反卷积的输出尺寸统一。情形三换算表如下:

标3.png表3

下面是一组利用DCGAN实现绘制数字的代码,其中图片加载类imgdataset.py可复制之前的实例代码。

=DCGAN_Net.py=生成器和判别器

import torch
import torch.nn as nn


class D_Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.dnet = nn.Sequential(
            nn.Conv2d(1, 128, 5, 2, 2),
            nn.LeakyReLU(0.2),  # 14*14
            nn.Conv2d(128, 256, 5, 2, 2),
            nn.BatchNorm2d(256),  # 7*7
            nn.LeakyReLU(0.2),
            nn.Conv2d(256, 512, 5, 2, 1),  # 3*3
            nn.BatchNorm2d(512),
            nn.LeakyReLU(0.2),
            nn.Conv2d(512, 1, 3, 1, 0), # 1*1
            nn.Sigmoid()
        )

    def forward(self, x):
        x = self.dnet(x)
        return x


class G_Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.gnet = nn.Sequential(
            nn.ConvTranspose2d(128, 512, 3, 1, 0) # 3*3,
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.ConvTranspose2d(512, 256, 5, 2, 1),# 7*7
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.ConvTranspose2d(256, 128, 5, 2, 2, 1),# 14*14
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.ConvTranspose2d(128, 1, 5, 2, 2, 1),# 28*28
            nn.Tanh()
        )

    def forward(self,x):
        x = self.gnet(x)
        return x

=DCGAN_Train.py=训练DCGAN代码

import torch
import torch.nn as nn
from torchvision.datasets import MNIST
from torch.utils import data
import os
from torchvision.utils import save_image
from torchvision import transforms
from DCGAN_Net import D_Net,G_Net
import torch.utils.data as Data
from imgdataset import DataSet
dataPath='dataset'
def loadDataset():
    trainDataset = DataSet(dataPath,
                                   "train-images-idx3-ubyte.gz",
                                   "train-labels-idx1-ubyte.gz",
                                   transform=transforms.ToTensor())

    testDataset = DataSet(dataPath,
                                  "t10k-images-idx3-ubyte.gz",
                                  "t10k-labels-idx1-ubyte.gz",
                                  transform=transforms.ToTensor())
    return trainDataset, testDataset

def gnloader(loadbatsize):
    trainDataset, testDataset=loadDataset()
    # 训练数据和测试数据的装载
    train_loader = Data.DataLoader(
        dataset=trainDataset,
        batch_size=loadbatsize,
        shuffle=True
    )

    test_loader = Data.DataLoader(
        dataset=testDataset,
        batch_size=loadbatsize,
        shuffle=True,
    )
    return trainDataset, testDataset,train_loader,test_loader


def loadMNIST(batch_size):  # MNIST图片的大小是28*28
    return gnloader(batch_size)

class Trainer:
    def __init__(self):
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.loss_fn = nn.BCELoss()

    def train(self):
        if not os.path.exists("./dcgan_img"):
            os.mkdir("./dcgan_img")
        if not os.path.exists("./dcgan_params"):
            os.mkdir("./dcgan_params")
        BATCH_SIZE = 100
        NUM_EPOCHS = 150
        trainset, testset, train_loader, test_loader = loadMNIST(BATCH_SIZE)
        d_net = D_Net().to(self.device)
        g_net = G_Net().to(self.device)
        d_opt = torch.optim.Adam(d_net.parameters(),lr=0.001,betas=(0.5,0.999))
        g_opt = torch.optim.Adam(g_net.parameters(),lr=0.001,betas=(0.5,0.999))

        for epochs in range(NUM_EPOCHS):
            for i,(x,y) in enumerate(train_loader):
                N = x.size(0)
                real_img = x.to(self.device)
                fake_label = torch.zeros(N,1,1,1).to(self.device)
                real_label = torch.ones(N,1,1,1).to(self.device)
                real_out = d_net(real_img)
                d_real_loss = self.loss_fn(real_out,real_label)

                z = torch.randn(N,128,1,1).to(self.device)
                fake_img = g_net(z)
                fake_out = d_net(fake_img)
                d_fake_loss = self.loss_fn(fake_out,fake_label)

                d_loss = d_fake_loss+d_real_loss
                d_opt.zero_grad()
                d_loss.backward()
                d_opt.step()

                z = torch.randn(N,128,1,1).to(self.device)
                fake_img = g_net(z)
                fake_out = d_net(fake_img)
                g_loss = self.loss_fn(fake_out,real_label)
                g_opt.zero_grad()
                g_loss.backward()
                g_opt.step()

                if i%50 == 0:
                    print("epochs:{}/{},d_loss:{:.3f},,g_loss:{:.3f},"
                          "d_real:{:.3f},d_fake:{:.3f}".format(
                        epochs,NUM_EPOCHS,d_loss.item(),g_loss.item(),
                        real_out.data.mean(), fake_out.data.mean()))
                    real_image = real_img.cpu().data
                    save_image(real_image, "./dcgan_img/epoh{0}-iteration{1}-real_img.jpg".
                               format(epochs ,i), nrow=10, normalize=True, scale_each=True)
                    fake_image = fake_img.cpu().data
                    save_image(fake_image, "./dcgan_img/epoh{0}-iteration{1}-fake_img.jpg".
                               format(epochs,i), nrow=10, normalize=True, scale_each=True)

if __name__ == '__main__':
    t = Trainer()
    t.train()

DCGAN一个难点在于参数的设定,上面的代码如果不使用GPU,神经网络训练收敛很慢。DCGAN的训练过程与GAN没有区别,重点看DCGAN的生成器与判别器,判别器的输入是28*28的图片

nn.Conv2d(1, 128, 5, 2, 2)

卷积核大小为5,步长为2,padding层数为2,由公式⑤可得输出为

c1.png

按此规则向下分析,判别器最终输出是一个1维实数,通过sigmod函数将此实数变为0到1之间小数。

    生成器过程与判别器执行路径倒序对应,代表着利用反卷积将一维数据逐步还原为28*28图片大小的过程,生成器第一行代码是一个反卷积:

 nn.ConvTranspose2d(128, 512, 3, 1, 0)

这行代码对应判别器代码的最后一行,是核大小k=3,步长s=1,p=0的卷积过程:

nn.Conv2d(512, 1, 3, 1, 0)

通过换算表1可以得到反卷积输出尺寸:

c2.png

生成器第二次反卷积代码:

nn.ConvTranspose2d(512, 256, 5, 2, 1),

对应于判别器卷积过程,这是输入i=7,核大小k=5,步长为s=2,p=1的卷积过程

nn.Conv2d(256, 512, 5, 2, 1)

步长大于1时,先计算i+2p-k是否能被步长s整除,i+2p-k=7+2-5=4,显然能被步长2整除,反卷积输出要使用换算表2

-请登陆后阅读余下文章-
登录|注册
上一篇  LSTM-循环神经网络(下)
评论区