第12篇 Fast AI深度学习课程——DarkNet、GAN

本节课程将介绍很火的对抗生成网络。由于这一网络结构很新,目前(课程发布时,18年4月份)Fast.AI尚未提供相应的封装,因此需要使用Pytorch的数据结构来构建。在构建GAN之前,我们将在CIFAR10数据上,仅使用Pytorch的数据结构,构建结构较简单的Darknet,以展示利用Pytorch搭建网络的思路。

一、Darknet

1. 数据准备

下载后解压。由于train文件夹下各类数据都在一起,要将之按照类别进行移动至相应子文件夹下。

trn_path = PATH/'train/'
cls_path = {}
for cls in classes:
    os.makedirs(trn_path/cls, exist_ok=True)
    cls_path[cls] = trn_path/cls

for f in trn_path.glob('*.png'):
    cls = f.name.split(".")[0].split('_')[-1]
    if cls == 'automobile': cls = "car"
    if cls == "airplane": cls = "plane"
    f.replace(cls_path[cls]/f.name)

由于使用test文件作为验证集,因此也需要按照train文件夹下的结构做整理。

CIFAR数据集中的图片尺寸为32x32,不太适合做旋转变换,因此定义如下变换实现数据修饰:

tfms = tfms_from_stats(stats, sz, aug_tfms=[RandomFlip()], pad=sz//8)
data = ImageClassifierData.from_paths(PATH, val_name='test', tfms=tfms, bs=bs)

其中设置pad=4,使得图片被扩展为40x40,然后进行随机的裁剪,以恢复32x32的尺寸。需要说明的是,在图片扩展时,使用的是对称反射的方式,而非补零扩展,这样效果较好。

2. 网络构建

Darknet的网络结构类似于ResNet,主体是由ResBlock构成。在此,使用模块化构建方法。首先定义一个卷积模块,该模块结构为Conv-Normalize-LeakyReLU

def conv_layer(ni, nf, ks=3, stride=1):
    return nn.Sequential(
        nn.Conv2d(ni, nf, kernel_size=ks, bias=False, stride=stride, padding=ks//2),
        nn.BatchNorm2d(nf, momentum=0.01),
        nn.LeakyReLU(negative_slope=0.1, inplace=True))

其中LeakyReLU的参数inplace设置为True,可以减少内存(或显存)的开销,又不影响梯度的传递。(如LeakyReLU,仅需根据输出值的正负,即可判定梯度值。与此相同的还有均匀池化层等。而有些需要保留原值来计算梯度值的单元,就不能使用原位操作。)

接下来定义ResBlock模块,其结构如下:

图 1.ResBlock结构
<!--![在这里插入图片描述](https://img-blog.csdnimg.cn/20190104202205244.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N1cmVkaWVk,size_16,color_FFFFFF,t_70)-->

代码如下:

class ResLayer(nn.Module):
    def __init__(self, ni):
        super().__init__()
        self.conv1=conv_layer(ni, ni//2, ks=1)
        self.conv2=conv_layer(ni//2, ni, ks=3)
        
    def forward(self, x): 
        return x.add(self.conv2(self.conv1(x)))

注意其中的两个卷积层构成了BottleNeck形式,即中间特征数减少。

ResBlock的基础上,就可定义Darknet了。Darknet主体由若干层组构成,每个层组又由y一个卷积层和若干ResBlock组成。最终添加一个池化层、一个全连接层进行输出。其结构图如下(哈哈,符号是我瞎画的)。

图 2.Darknet结构图

代码如下:

class Darknet(nn.Module):
    def make_group_layer(self, ch_in, num_blocks, stride=1):
        return [conv_layer(ch_in, ch_in*2,stride=stride)
               ] + [(ResLayer(ch_in*2)) for i in range(num_blocks)]

    def __init__(self, num_blocks, num_classes, nf=32):
        super().__init__()
        layers = [conv_layer(3, nf, ks=3, stride=1)]
        for i,nb in enumerate(num_blocks):
            layers += self.make_group_layer(nf, nb, stride=2-(i==1))
            nf *= 2
        layers += [nn.AdaptiveAvgPool2d(1), Flatten(), nn.Linear(nf, num_classes)]
        self.layers = nn.Sequential(*layers)
    
    def forward(self, x): return self.layers(x)

值得说明的是nn.AdaptiveAvgPool2d()函数,其参数为所输出的特征的尺寸。本例中,单个特征的尺寸由32x32逐渐变化为1x1

3. 损失函数

由网络构建学习器模型,并设置损失函数为交互熵。

lr = 1.3
learn = ConvLearner.from_model_data(m, data)
learn.crit = nn.CrossEntropyLoss()
learn.metrics = [accuracy]
wd=1e-4
%time learn.fit(lr, 1, wds=wd, cycle_len=30, use_clr_beta=(20, 20, 
                0.95, 0.85))

fit()中的use_clr_beta参数为一个四元组,其中前两个参数的意义与use_clr(参见上一课)相同。后两个参数设置了动量系数的最高最低值。

图 3.use_clr参数

二、GAN

有关生成对抗网络的介绍不再赘述,可参考CS231n课程的相关内容。本部分将关注于WGAN的实现。

  1. 数据集

本部分将在样本图片的基础上,生成卧室场景图片。所用数据集可使用如下代码下载、解压、转换。

curl 'http://lsun.cs.princeton.edu/htbin/download.cgi?tag=latest&category=bedroom&set=train' -o bedroom.zip
unzip bedroom.zip
pip install lmdb
python lsun-data.py {PATH}/bedroom_train_lmdb --out_dir {PATH}/bedroom

其中lsun-data.py为文件夹lsun-scripts下的脚本。

由于原数据集太大,还可使用Kaggle上提供的按20%的比例抽取的样本
此外,还可使用人脸表情数据集CelebA

  1. 构建网络
  • 判别网络Discriminator
    首先定义卷积模块。

    class ConvBlock(nn.Module):
        def __init__(self, ni, no, ks, stride, bn=True, pad=None):
            super().__init__()
            if pad is None: pad = ks//2//stride
            self.conv = nn.Conv2d(ni, no, ks, stride, padding=pad, bias=False)
            self.bn = nn.BatchNorm2d(no) if bn else None
            self.relu = nn.LeakyReLU(0.2, inplace=True)
        
        def forward(self, x):
            x = self.relu(self.conv(x))
            return self.bn(x) if self.bn else x
    

    注意其中BatchNorm层与ReLU层的顺序,与自定义的DarkNet正好相反,其实也没那么讲究。

    读取图像,然后输出是真是假的分值。因此,其网络结构为:输入图像首先经过一个跨立度为2的卷积,尺寸缩小为源图像的一半(卷积结果的特征维度由输入ndf参数限定);然后经过若干层维持尺寸及特征维度不变的卷积层,再经过一系列跨力度为2的卷积模块,继续缩小尺寸,同时特征维度每次变为上一步的2倍;最终得到尺寸不大于4x4的特征,然后取均值进行输出。

    class DCGAN_D(nn.Module):
        def __init__(self, isize, nc, ndf, n_extra_layers=0):
            super().__init__()
            assert isize % 16 == 0, "isize has to be a multiple of 16"
    
            self.initial = ConvBlock(nc, ndf, 4, 2, bn=False)
            csize,cndf = isize/2,ndf
            self.extra = nn.Sequential(*[ConvBlock(cndf, cndf, 3, 1)
                                        for t in range(n_extra_layers)])
    
            pyr_layers = []
            while csize > 4:
                pyr_layers.append(ConvBlock(cndf, cndf*2, 4, 2))
                cndf *= 2; csize /= 2
            self.pyramid = nn.Sequential(*pyr_layers)
            
            self.final = nn.Conv2d(cndf, 1, 4, padding=0, bias=False)
    
        def forward(self, input):
            x = self.initial(input)
            x = self.extra(x)
            x = self.pyramid(x)
            return self.final(x).mean(0).view(1)
    

    其中isize表示image size,为输入图像的尺寸;csize表示conv-size,表示卷积后图像的尺寸。

  • 生成网络Generator
    首先定义转置卷积模块(有关转置卷积的定义可参见CS231n课程的相关内容)。

    class DeconvBlock(nn.Module):
        def __init__(self, ni, no, ks, stride, pad, bn=True):
            super().__init__()
            self.conv = nn.ConvTranspose2d(ni, no, ks, stride, padding=pad, bias=False)
            self.bn = nn.BatchNorm2d(no)
            self.relu = nn.ReLU(inplace=True)
            
        def forward(self, x):
            x = self.relu(self.conv(x))
            return self.bn(x) if self.bn else x
    

    事实上,可能使用nn.Upsample替代nn.ConvTranspose2d会更合适。

    接下来定义生成网络。生成网络接受一个随机向量,输出一幅图像。网络将输入的随机向量视为1x1的特征,然后进行多次转置卷积,将之扩展为NxN3通道数组,以视为图像。生成网络的结构与判别网络的大致相反,代码如下。

    class DCGAN_G(nn.Module):
        def __init__(self, isize, nz, nc, ngf, n_extra_layers=0):
            super().__init__()
            assert isize % 16 == 0, "isize has to be a multiple of 16"
    
            cngf, tisize = ngf//2, 4
            while tisize!=isize: cngf*=2; tisize*=2
            layers = [DeconvBlock(nz, cngf, 4, 1, 0)]
    
            csize, cndf = 4, cngf
            while csize < isize//2:
                layers.append(DeconvBlock(cngf, cngf//2, 4, 2, 1))
                cngf //= 2; csize *= 2
    
            layers += [DeconvBlock(cngf, cngf, 3, 1, 1) for t in range(n_extra_layers)]
            layers.append(nn.ConvTranspose2d(cngf, nc, 4, 2, 1, bias=False))
            self.features = nn.Sequential(*layers)
    
        def forward(self, input): return F.tanh(self.features(input))
    

    其中nz表示输入的随机向量的维度。第一个while循环是为了获取合适的特征维度,以保证和判别网络的特征维度相对应。

  • 训练过程
    通用的训练流程是:

    set_trainable_status()
    while(iter_epoch < epochs):
        while(iter_dl < n_dl):
            batch = grab_batch()
            loss = forward(batch)
            loss.backward()
            optimizer.step()
            zero_grad()
    

    而对于GAN,一个额外的流程控制是:在训练过程中,要先训练Discriminator,此时利用总体的损失函数计算梯度值,同时更新DiscriminatorGenerator的参数。然后固定Discriminator(设置Discriminatortrainable状态为假),计算损失函数,计算梯度,然后仅更新Generator的参数。注意,要多训练Discriminator,其原因参见CS231n课程的相关内容。

    另外需要注意的是需要保持GeneratorDiscriminator的系数处于-0.01~0.01区间内,以使得模型可正常工作。

  • 需要说明的问题

    • WGAN的优化器使用的是RMSProp;使用较大的学习速率,或者使用带冲量的优化器,会导致模型训练失效。解释如下:Discriminator的损失函数并不稳定(也是,Generator就是一个天马行空的捣乱的,谁知道它喂给Discriminator的数据是啥样的),如果采取基于动量的优化策略,前后两次的优化方向可能偏差很大,造成优化效果的不稳定;采用较大的学习速率也会如此。

    • 目前并无对GAN网络的进行效果评估的有效手段。比如GAN是否过拟合,GAN是否存在模态坍缩(仅输出极为有限的生成图片),生成的图片是否是数据集中某张图片的复制。

三、Cycle GAN

考虑一下这样的应用场景:如何将一匹马变成斑马?问题的难点在于我们并没有马和斑马的匹配图片作为训练样本。

一个天才的想法是:我们可以训练一个Generator,把某马变为某斑马。为保证变出来的斑马确实像斑马,我们训练一个Discriminator,来区分斑马的真假。为了保证变出的斑马又和原马很相似,我们再将该斑马变为马(这是另一个Generator),并判断新马和原马的差异的大小。如果变出的斑马和原马不像,那么由斑马变出的新马将和原马有很大的差异。将同样的操作应用于把某斑马变为某马的过程。

图 4.Cycle GAN机理
以下为代码说明,大部分代码是从`Cycle GAN`的`Github`库里"借鉴"过来的。
1. 数据加载器

数据下载:
wget https://people.eecs.berkeley.edu/~taesung_park/CycleGAN/datasets/horse2zebra.zip

数据加载器是由CreateDataLoader()函数创建的。其返回一个CustomDatasetDataLoader类,该类派生自BaseDataLoader。而在CustomDatasetDataLoader的初始化函数initialize()中,将使用PytorchDataloader,其需要一个Dataset对象,该对象又是由CreateDataset()函数创建。查看该函数的定义,并结合本例的实际情形,所需的DatasetUnalignedDataset对象。

UnalignedDataset对象的__getitem__()函数中,主要完成的是获取某个索引的图片,做一定的变换,然后将图片返回。

2. 网络模型

网络模型是由create_model()函数创建的,其返回一个CycleGANModel对象。在CycleGANModel的初始化函数中,将定义两个Generator,定义损失函数,定义优化器。

一些有用的链接

猜你喜欢

转载自blog.csdn.net/suredied/article/details/85797718
今日推荐