机器学习:GAN 生成对抗网络



概述

机器学习算法多数是用于解决回归问题,分类问题,聚类问题,而 GAN 则是用于生成内容,比如生成图片

GAN(Generative Adversarial Nets,生成对抗网络)是 2014 年提出的理论 https://arxiv.org/pdf/1406.2661.pdf

GAN 由一个生成器 Generator 和一个判别器 Discriminator 组成,下面以生成图片为例子
  
    
  
Generator 接收随机的噪声 x,生成图片,y = G(x)
Discriminator 接收图片,判断图片是真的还是生成的,z = D(y),样本 y 部分来自真实图片,部分由 G 生成

我们希望训练 G 使得它能生成尽可能真的图片,使得 D 无法区分真假
同时又训练 D 使得它能尽可能地区别真实图片和生成图片

可以看到 G 和 D 目标相反,互相对抗,所以叫生成对抗网络,在对抗训练中,G 和 D 的能力不断提升

理想情况下,最后 G 生成的图片,D 难以区分真假,即 D(G(x)) = 0.5

训练

轮流训练 G 和 D,可以先训练 G 再训练 D,也可以训练 D 再训练 G

以先训练 D 再训练 G 为例子

训练 D
  固定 G 的参数不改变
  取一批真实图片 xr,通过 G 生成一批假图片 xf = G(noise)
  用 D 判定,yr = D(xr),yf = D(xf)
  计算损失函数 Loss(yr, ones) (真图希望输出 1)、Loss(yf, zeros) (假图希望输出 0)
  应用反向梯度传播算法,更新 D 的参数(G 不参与梯度传播,或者传播但不改变 G 的参数)
  
训练 G
  固定 D 的参数不改变
  通过 G 生成一批假图片 xf = G(noise)
  用 D 判定,yf = D(xf)
  计算损失函数 Loss(yf, ones) (希望假图能骗过 D 使其输出 1)
  应用反向梯度传播算法,更新 G 的参数(D 参与梯度传播,但不改变 D 的参数)
  
训练 G 的迭代次数和训练 D 的迭代次数可以不一样,比如先用 5 个 batch 训练 D,再用 1 个 batch 训练 G,循环反复

DCGAN

https://arxiv.org/abs/1511.06434

最开始的 GAN,没有用 CNN 网络,训练不稳定,G 容易生成糟糕的结果

DCGAN(Deep Convolutional GAN)原理和 GAN 一样,但使用 CNN 构建 G 和 D

DCGAN 的 CNN 的特点
  G 网络使用转置卷积(Transposed Convolutional Layer)做上采样
  D 网络通过设置 stride 代替 Pooling 层
  使用 Batch Normalization
  G 网络使用 ReLU 激活函数,最后一层用 tanh 激活函数,不用全连接层,最后的卷积就是输出
  D 网络使用 LeakyReLU 激活函数,最后一层用 sigmoid 激活函数,不用全连接层,最后的卷积就是输出

转置卷积

和正常的卷积反过来,也叫逆卷积或反卷积

就是将小的特征图,变换成大的特征图,将特征图变成图片,例如下图
  
  
  
G 模型用的就是转置卷积网络,可以看到接收 100 channel 的 noise 最终输出 96 x 96 的 RGB 图片

关于转置卷积如何计算,可以参考 https://zhuanlan.zhihu.com/p/327784795

里面介绍了,插值补零法、交错相加法、小卷积核法,等几种方法

要注意转置卷积是卷积的逆运算,不是卷积的逆过程

代码

参考 https://blog.csdn.net/weixin_45807161/article/details/123776427

import matplotlib.pyplot as plt

import torch
import torch.nn as nn
import torchvision

import tqdm

from torch.autograd import Variable
from torch.utils.data import DataLoader

"""
# 简单测试,理解反卷积
m = nn.ConvTranspose2d(3, 6, kernel_size=3, stride=1, padding=0, bias=False)
input = torch.ones(3, 3, 4)
print(input.shape)   # torch.Size([3, 3, 4])

output = m(input)
print(output.shape)  # torch.Size([6, 5, 6])

noises = Variable(torch.randn(50, 100, 1, 1))
print(noises.shape)  # torch.Size([50, 100, 1, 1])

noises.data.copy_(torch.randn(50, 100, 1, 1))
print(noises.shape)  # torch.Size([50, 100, 1, 1])
"""


class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.model = nn.Sequential(
            # 设置输入 channel 和输出 channel,以及 kernel size 和步长等
            # 不会限制长和宽,由输入决定
            # 后面我们用 1x1,这样输出就是 4x4
            # (由输入大小, kernel 大小, stride, padding 决定)
            # (out 大小通过卷积可以得到 in 的大小,由此可以通过 in 大小反推反卷积后的 out 大小)
            # 结合 channel 就是 100 个 1x1 的输入,产生 512 个 4x4 的输出
            # kernel_size, stride, padding 接收两个值(长和宽)组成的 tuple, 如果用 int, 就默认两个值是一样的
            nn.ConvTranspose2d(in_channels=100, out_channels=512, kernel_size=4,
                               stride=1, padding=0, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(True),
            # 输入 512x4x4,输出 256x8x8 (stride = 2, padding = 1)
            nn.ConvTranspose2d(in_channels=512, out_channels=256, kernel_size=4, 
                               stride=2, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(True),
            # 输入 256x8x8,输出 128x16x16
            nn.ConvTranspose2d(in_channels=256, out_channels=128, kernel_size=4, 
                               stride=2, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(True),
            # 输入 128x16x16,输出 64x32x32
            nn.ConvTranspose2d(in_channels=128, out_channels=64, kernel_size=4, 
                               stride=2, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(True),
            # 输入 64x32x32,输出 3x96x96
            nn.ConvTranspose2d(in_channels=64, out_channels=3, kernel_size=5, 
                               stride=3, padding=1, bias=False),
            nn.Tanh()
        )

    def forward(self, x):
        """
            nn.Module 定义了 __call__ 函数

                __call__ : Callable[..., Any] = _call_impl

            而这个 _call_impl 函数里面调用了 self.forward

            由于定义了 __call__ 方法的类可以当作函数调用

            所以下面代码就会调用到 forward

                generator = Generator()
                y = generator(torch.randn(10, 100, 1, 1))

                # torch.randn(10, 100, 1, 1) 随机生成 10x100x1x1 的数据
                # 因为模型是接收批数据的,所以需要指定 4 个值,代表 batch size 为 10,channel 为 100,长宽为 1

                print(y.shape)                    # 输出 torch.Size([10, 3, 96, 96])
                print(y.view(-1).shape)           # 输出 torch.Size([276480])
                                                  # view 相当于 reshape,而 -1 代表由电脑自己计算
                print(y.view(-1, 96, 96).shape)   # 输出 torch.Size([30, 96, 96])
        """
        return self.model(x)


class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.model = nn.Sequential(
            # 如果输入 3x96x96 的图片,输出是 64x32x32 的特征图
            nn.Conv2d(in_channels=3, out_channels=64, kernel_size=5, stride=3, padding=1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # 输入 64x32x32,输出 128x16x16
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2, inplace=True),
            # 输入 128x16x16,输出 256x8x8
            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2, inplace=True),
            # 输入 256x8x8,输出 512x4x4
            nn.Conv2d(in_channels=256, out_channels=512, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(512),
            nn.LeakyReLU(0.2, inplace=True),
            # 输入 512x4x4,输出一个概率
            nn.Conv2d(in_channels=512, out_channels=1, kernel_size=4, stride=1, padding=0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        """
            d = Discriminator()
            y = d(torch.randn(10, 3, 96, 96))    # 随机产生 10 个 channel 为 3,长宽为 96 的数据

            # 如果不用 view(-1) 的话,输出会是 torch.Size([10, 1, 1, 1])
            # 用了 view(-1) 后,输出是 torch.Size([10])
            print(y.shape)
        """
        return self.model(x).view(-1)


def train():
    #########
    # 批大小
    #########
    batch_size = 50

    ##########
    # 导入数据
    ##########
    
    # 定义如何转换图片
    transform = torchvision.transforms.Compose(
        [
            torchvision.transforms.Resize(96),
            torchvision.transforms.CenterCrop(96),
            torchvision.transforms.ToTensor(),
            torchvision.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
        ]
    )

    # 用 transformer 创建数据集,图片要放在 images 下的子目录,比如 images/anime-faces/*.png,有 20000 多张图片
    dataset = torchvision.datasets.ImageFolder('./images', transform=transform)

    # 用 dataset 创建 dataloader, 用于迭代,每次导入一批图片,批大小为 batch_size
    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=4, drop_last=True)

    #######################
    # 判定器和生成器相关设置
    #######################
    generator = Generator()
    discriminator = Discriminator()

    # 优化器,也用于更新参数
    optimizer_g = torch.optim.Adam(generator.parameters(), lr=2e-4, betas=(0.5, 0.999))
    optimizer_d = torch.optim.Adam(discriminator.parameters(), lr=2e-4, betas=(0.5, 0.999))

    # 损失函数
    loss = torch.nn.BCELoss()

    # 标签,值为 1 的标签,和值为 0 的标签,大小为 batch_size
    true_labels = Variable(torch.ones(batch_size))
    fake_labels = Variable(torch.zeros(batch_size))

    # 噪音,大小为 batch_size,channel 为 100
    # fix_noises 不会变化,用于阶段性的测试效果,用于比较不同迭代次数后,同一个噪音输入所产生的图片质量
    # noises 会改变,用于训练,每导入一批真实图片,就用新的噪音,产生新的假图片,每批真假图片的数量都是 batch_size
    fix_noises = Variable(torch.randn(batch_size, 100, 1, 1))
    noises = Variable(torch.randn((batch_size, 100, 1, 1)))

    # CUDA 是否可用,如果可以则使用 CUDA,也就是用 GPU 计算
    # CUDA(Compute Unified Device Architecture),是显卡厂商 NVIDIA 推出的运算平台
    if torch.cuda.is_available():
        print("cuda is available")
        generator.cuda()
        discriminator.cuda()
        loss.cuda()
        true_labels = true_labels.cuda()
        fake_labels = fake_labels.cuda()
        fix_noises = fix_noises.cuda()
        noises = noises.cuda()

    #######
    # 迭代
    #######

    # i 代表第几次迭代
    # 处理完所有的图片就是一次迭代
    for i in range(300):
        print("迭代 " + str(i))

        ###########
        # 分批处理
        ###########

        # j 代表第几批
        # 前面创建 dataloader 的时候已经指定了每批拿多少张图片
        # 由于用的真实图片有 20000 多张,如果 batch_size 设置为 50 的话,每次迭代就是分 400 多个批次处理
        # image 不是一张图片,而是一批图片
        for j, (image, _) in tqdm.tqdm(enumerate(dataloader)):

            #####################
            # 训练 discriminator
            #####################

            # 获取一批真实图片
            real_image = Variable(image)
            if torch.cuda.is_available():
                real_image = real_image.cuda()

            # 清零梯度,避免被前面的迭代批次影响
            optimizer_d.zero_grad()

            # 对真图片做判定,目标是 1
            real_output = discriminator(real_image)
            # 计算损失,目标是 1
            d_loss_real = loss(real_output, true_labels)

            # 产生一批噪音
            noises.data.copy_(torch.randn(batch_size, 100, 1, 1))
            # 生成假图片
            # detach() 使得梯度不会传到 generator,因为训练 discriminator 时需要固定 generator 不变
            fake_image = generator(noises).detach()
            # 对假图片做判定,目标是 0
            fake_output = discriminator(fake_image)
            # 计算损失,目标是 0
            d_loss_fake = loss(fake_output, fake_labels)

            # 合成一个 loss
            d_loss = (d_loss_real + d_loss_fake) / 2
            # 反向梯度传播 (也可以两个 loss 分别做 backward() 然后 step() 更新)
            d_loss.backward()
            # 更新 discriminator 参数
            optimizer_d.step()

            #################
            # 训练 generator
            #################

            # 每 5 个 batch 才训练一次 generator (而 discriminator 是每个 batch 都训练)
            # g 模型和 d 模型的训练次数比例可以自己调,没要求一定这样,也没要求必须先训练 discriminator
            if (j + 1) % 5 == 0:
                # 清零梯度,避免被前面的迭代批次影响
                optimizer_g.zero_grad()

                # 产生噪音
                noises.data.copy_(torch.randn(batch_size, 100, 1, 1))
                # 生成假图片
                fake_image = generator(noises)
                # 对假图片做判断,目标是 1 (希望能欺骗 discriminator)
                fake_output = discriminator(fake_image)
                # 计算损失,目标是 1
                g_loss = loss(fake_output, true_labels)

                # 反向梯度传播
                g_loss.backward()
                # 更新 generator 参数
                optimizer_g.step()

        ##############
        # 观察训练效果
        ##############
        if i in [1, 5, 10, 50, 100, 200]:
            # 用一个在训练过程中保持固定不变的噪音,阶段性的产生图片,用于观察训练的效果
            fix_fake_images = generator(fix_noises)
            fix_fake_images = fix_fake_images.data.cpu()[:64] * 0.5 + 0.5

            fig = plt.figure(1)

            # 将生成的所有小图片放到一张大图中
            k = 1
            for fix_fake_image in fix_fake_images:
                # 这个大图有 8 行 8 列,要添加的小图放到第 k 个位置(从左上角开始算)
                fig.add_subplot(8, 8, eval('%d' % k))
                plt.axis('off')
                plt.imshow(fix_fake_image.permute(1, 2, 0))
                k += 1

            # 调整位置
            plt.subplots_adjust(left=None, right=None, bottom=None, top=None, wspace=0.05, hspace=0.05)
            # 设置标题
            plt.suptitle('第%d次迭代结果' % i, y=0.91, fontsize=15)
            # 保存图片
            plt.savefig("./fake_image/%d-dcgan.png" % i)


if __name__ == '__main__':
    train()

阶段性的生成效果保存在 ./fake_image

下面是第 1,5,10,50,100 次迭代的效果
  
    
  
    
  
    
  
    
  
    

可以看到,同一个输入噪音,生成的效果越来越好
    
关于 detect() 的作用,和不用 detect() 的做法,可以参考
https://zhuanlan.zhihu.com/p/525543542





posted @ 2022-12-08 20:34  moon~light  阅读(362)  评论(0编辑  收藏  举报