DCGAN手写数字生成实验报告


DCGAN手写数字生成

1. 实验内容

本题的主要内容是对原始的GAN模型进行改进,使用更为稳定的DCGAN模型生成手写数字。

DCGAN的全称是Deep Convolution Generative Adversarial Networks(深度卷积生成对抗网络),是一种将CNN与GAN结合的无监督学习模型,用来解决GAN训练不稳定的问题。

2. 设计思路及方案

2.1 GAN原理

GAN的基本原理其实非常简单,这里以生成图片为例进行说明。假设我们有两个网络,G(Generator)和D(Discriminator)。正如它的名字所暗示的那样,它们的功能分别是:

(1) G是一个生成网络,它接收一个随机的噪声z,通过这个噪声生成图片,记做G(z)。

(2) D是一个判别网络,判别一张图片是不是“真实的”。它的输入参数是x,x代表一张图片,输出D(x)代表x为真实图片的概率,如果为1,就代表100%是真实的图片,而输出为0,就代表不可能是真实的图片。

在训练过程中,生成网络G的目标就是尽量生成真实的图片去欺骗判别网络D。而D的目标就是尽量把G生成的图片和真实的图片分别开来。这样,G和D构成了一个动态的“博弈过程”。

最后博弈的结果是什么?在最理想的状态下,G可以生成足以“以假乱真”的图片G(z)。对于D来说,它难以判定G生成的图片究竟是不是真实的,因此D(G(z)) = 0.5。

GAN原理如图1所示。

图1

2.2 算法描述

判别器的学习:

在每一次训练迭代中,从数据集中随机抽取m个样本\(x^1,x^2,...,x^m\)?,与m个服从正态分布的噪声样本,\(z^1,z^2,...,z^m?\)获取生成的数据\(\tilde{x}^1,\tilde{x}^2,...,\tilde{x}^m=G(z^i)?\),更新判别器的参数\(\theta_d?\)来最大化

\[\widetilde{V}=\frac{1}{m}\sum_{i=1}^{m}\log D(x^i)+\frac{1}{m}\sum_{i=1}^{m}\log (1-D(\tilde{x}^i))\\ \theta_d \leftarrow \theta_d+\eta \nabla\widetilde{V}(\theta_d) \]

生成器的学习:

随机抽取m个服从正态分布的噪声样本\(z^1,z^2,...,z^m\),更新生成器的参数\(\theta_g\)来最大化

来最大化

\[\widetilde{V}=\frac{1}{m}\sum_{i=1}^{m}\log D(G(z^i))\\ \theta_g \leftarrow \theta_g+\eta \nabla\widetilde{V}(\theta_g) \]

2.3 DCGAN架构

DCGAN的生成器网络结构如图2所示,相较原始的GAN,DCGAN几乎完全使用了卷积层代替全连接层,判别器几乎是和生成器对称的,从图中我们可以看到,整个网络没有pooling层和上采样层的存在,具体

图2

DCGAN能改进GAN训练稳定的原因主要有:

  1. 使用步长卷积代替上采样层,卷积在提取图像特征上具有很好的作用,并且使用卷积代替全连接层。

  2. 生成器G和判别器D中几乎每一层都使用batchnorm层,将特征层的输出归一化到一起,加速了训练,提升了训练的稳定性。(生成器的最后一层和判别器的第一层不加batchnorm)

  3. 在判别器中使用LeakyReLU激活函数,而不是RELU,防止梯度稀疏,生成器中仍然采用RELU,但是输出层采用Tanh。

  4. 使用Adam优化器训练,并且学习率最好是0.0002。


图3 生成器、判别器网络架构

3. 编程实现

3.1 代码

import argparse
import os
import numpy as np
import math
import torchvision.transforms as transforms
from torchvision.utils import save_image
from torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch

parser = argparse.ArgumentParser()
parser.add_argument("--n_epochs", type=int, default=200, help="number of epochs of training")
parser.add_argument("--batch_size", type=int, default=64, help="size of the batches")
parser.add_argument("--lr", type=float, default=0.0002, help="adam: learning rate")
parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient")
parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient")
parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation")
parser.add_argument("--latent_dim", type=int, default=100, help="dimensionality of the latent space")
parser.add_argument("--img_size", type=int, default=32, help="size of each image dimension")
parser.add_argument("--channels", type=int, default=1, help="number of image channels")
parser.add_argument("--sample_interval", type=int, default=400, help="interval between image sampling")
opt = parser.parse_args()
print(opt)

def weights_init_normal(m):
    classname = m.__class__.__name__
    if classname.find("Conv") != -1:
        torch.nn.init.normal_(m.weight.data, 0.0, 0.02)
    elif classname.find("BatchNorm2d") != -1:
        torch.nn.init.normal_(m.weight.data, 1.0, 0.02)
        torch.nn.init.constant_(m.bias.data, 0.0)
        
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.init_size = opt.img_size // 4
        self.l1 = nn.Sequential(nn.Linear(opt.latent_dim, 128 * self.init_size ** 2))
        self.conv_blocks = nn.Sequential(
            nn.BatchNorm2d(128), # 对128个输入特征进行批标准化操作,是每个节点的输入均值为0,方差为1
            nn.Upsample(scale_factor=2), # 上采样,默认是使用最近邻差值法
            nn.Conv2d(128, 128, 3, stride=1, padding=1), 
            # 二维卷积层,输入的通道数128,输出的通道数128,卷积核的尺寸3,卷积步长1,padding=1保证了输入输出的图像形状大小相同
            nn.BatchNorm2d(128, 0.8), # 对128个输入进行批标准化操作,将eps设置为0.8
            nn.ReLU(inplace=True)
            nn.Upsample(scale_factor=2), # 上采样
            nn.Conv2d(128, 64, 3, stride=1, padding=1), # 二维卷积层,输出的通道数64
            nn.BatchNorm2d(64, 0.8),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, opt.channels, 3, stride=1, padding=1),
            nn.Tanh(),
        )
    def forward(self, x):
        out = self.l1(x)
        out = out.view(out.shape[0], 128, self.init_size, self.init_size)
        img = self.conv_blocks(out)
        return img

#直接对所有的层采用批处理规范化,会导致样本震荡和模型不稳定。通过对生成器的输出层和辨别器的输入层不采用批处理规范化来避免这种情况

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        def discriminator_block(in_filters, out_filters, bn=True):
            block = [nn.Conv2d(in_filters, out_filters, 3, 2, 1), nn.LeakyReLU(0.2, inplace=True), nn.Dropout2d(0.25)]
            # 使用LeakyReLU = max(0,x)+negative_slope?min(0,x),这里将negati_slope设置为0.2,inplace设置为True,表明覆盖运算
            if bn:
                block.append(nn.BatchNorm2d(out_filters, 0.8))
            return block
        self.model = nn.Sequential(
            *discriminator_block(opt.channels, 16, bn=False),
            *discriminator_block(16, 32),
            *discriminator_block(32, 64),
            *discriminator_block(64, 128),
        )
        # The height and width of downsampled image
        ds_size = opt.img_size // 2 ** 4
        self.adv_layer = nn.Sequential(nn.Linear(128 * ds_size ** 2, 1), nn.Sigmoid())
    def forward(self, z):
        out = self.model(z)
        out = out.view(out.shape[0], -1)
        validity = self.adv_layer(out)
        return validity
    
# Loss function
adversarial_loss = torch.nn.BCELoss()

# Initial generator and discriminator
generator = Generator()
discriminator = Discriminator()

# Use GPU
cuda = True if torch.cuda.is_available() else False
if cuda:
    generator.cuda()
    discriminator.cuda()
    adversarial_loss.cuda()
    
# Initial weights
generator.apply(weights_init_normal)
discriminator.apply(weights_init_normal)

os.makedirs("images", exist_ok=True) # images folder is used to store the results
os.makedirs("mnist", exist_ok=True)  # mnist folder is used to store the data

# Config data loader
dataloader = torch.utils.data.DataLoader(
    datasets.MNIST(
        "./mnist",
        train=True,
        download=True,
        transform=transforms.Compose(
            [transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
        ),
    ),
    batch_size=opt.batch_size,
    shuffle=True,
)

# 优化器使用Adam, b1,b2 默认为0.9,0.999,但b1为0.9会导致训练震荡和不稳定,将其减少至0.5可以帮助稳定训练
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2)) 
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor

# ----------
#  Training
# ----------

for epoch in range(opt.n_epochs):
    for i, (imgs, _) in enumerate(dataloader):
        # 生成 ground truth,分别为1.0,0.0,正确则为1.0,错误则为0.0
        valid = Variable(Tensor(imgs.shape[0], 1).fill_(1.0), requires_grad=False)
        fake = Variable(Tensor(imgs.shape[0], 1).fill_(0.0), requires_grad=False)
        # input
        real_imgs = Variable(imgs.type(Tensor))
        # -----------------
        #  Train Generator
        # -----------------
        optimizer_G.zero_grad()
        # Sample noise as generator input
        z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))
        # Generate a batch of images
        gen_imgs = generator(z)
        # Loss measures generator's ability to fool the discriminator
        g_loss = adversarial_loss(discriminator(gen_imgs), valid)
        g_loss.backward()
        optimizer_G.step()
        # ---------------------
        #  Train Discriminator
        # ---------------------
        optimizer_D.zero_grad()
        # Measure discriminator's ability to classify real from generated samples
        real_loss = adversarial_loss(discriminator(real_imgs), valid)
        fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake)
        d_loss = (real_loss + fake_loss) / 2
        d_loss.backward()
        optimizer_D.step()
        print(
            "[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
            % (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item())
        )
        batches_done = epoch * len(dataloader) + i
        if batches_done % opt.sample_interval == 0:
            save_image(gen_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)

3.2 程序运行过程


3.3 程序运行结果

4. 本项目遇到的主要问题及解决方法

主要问题是在于超参数的设定以及损失函数优化器的选择上。举个例子,在Adam优化器的参数设定上,将momentum term \(\beta_1\)的建议值保持在0.9时会导致训练震荡不稳定,故降低至0.5,以帮助稳定训练。还有一开始设置的学习率为0.001,后来设置不同的学习率,发现0.0002时才是最好的。

5. 实践体会

本次实践对我来说收货颇丰,本想以一次竞赛题目来写报告,以往的竞赛主要是图像分类识别任务以及文本挖掘任务等,很少接触到VAE、GAN等生成模型,故借此实践的机会,学习一下生成对抗网络。
起初我先学习了一下李宏毅老师的生成对抗网络,然后通过阅读文献,将代码吃透。通过尝试自己跑不同的生成对抗网络模型,如GAN、DCGAN、InfoGAN、CycleGAN,对这几种网络有了一个大概的轮廓架构。
在跑CycleGAN的时候,大概估算了一下使用一片RTX2080ti显卡跑完需要16个小时,故因设备不足没有办法完成。
InfoGAN虽然跑完了整个代码,但对这个网络模型掌握的还不够深,故最终选择使用GAN改进后的DCGAN来编写此次实践报告。
通过本次机器学习实践,我学到了被称为十年来机器学习领域最有趣的想法的GAN以及它的多种改进版,例如使用CycleGAN实现图像的风格转换是我接触人工智能以来做的最有趣的项目,以后会在生成对抗网络这个领域继续学习探索。

GAN