本文主要是介绍第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/clsfor 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模块,其结构如下:
代码如下:
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组成。最终添加一个池化层、一个全连接层进行输出。其结构图如下(哈哈,符号是我瞎画的)。
代码如下:
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 *= 2layers += [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(参见上一课)相同。后两个参数设置了动量系数的最高最低值。
二、GAN
有关生成对抗网络的介绍不再赘述,可参考CS231n课程的相关内容。本部分将关注于WGAN的实现。
- 数据集
本部分将在样本图片的基础上,生成卧室场景图片。所用数据集可使用如下代码下载、解压、转换。
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。
- 构建网络
-
判别网络
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//strideself.conv = nn.Conv2d(ni, no, ks, stride, padding=pad, bias=False)self.bn = nn.BatchNorm2d(no) if bn else Noneself.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,ndfself.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 /= 2self.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的特征,然后进行多次转置卷积,将之扩展为NxN的3通道数组,以视为图像。生成网络的结构与判别网络的大致相反,代码如下。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, 4while tisize!=isize: cngf*=2; tisize*=2layers = [DeconvBlock(nz, cngf, 4, 1, 0)]csize, cndf = 4, cngfwhile csize < isize//2:layers.append(DeconvBlock(cngf, cngf//2, 4, 2, 1))cngf //= 2; csize *= 2layers += [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,此时利用总体的损失函数计算梯度值,同时更新Discriminator和Generator的参数。然后固定Discriminator(设置Discriminator的trainable状态为假),计算损失函数,计算梯度,然后仅更新Generator的参数。注意,要多训练Discriminator,其原因参见CS231n课程的相关内容。另外需要注意的是需要保持
Generator和Discriminator的系数处于-0.01~0.01区间内,以使得模型可正常工作。 -
需要说明的问题
-
WGAN的优化器使用的是RMSProp;使用较大的学习速率,或者使用带冲量的优化器,会导致模型训练失效。解释如下:Discriminator的损失函数并不稳定(也是,Generator就是一个天马行空的捣乱的,谁知道它喂给Discriminator的数据是啥样的),如果采取基于动量的优化策略,前后两次的优化方向可能偏差很大,造成优化效果的不稳定;采用较大的学习速率也会如此。 -
目前并无对
GAN网络的进行效果评估的有效手段。比如GAN是否过拟合,GAN是否存在模态坍缩(仅输出极为有限的生成图片),生成的图片是否是数据集中某张图片的复制。
-
三、Cycle GAN
考虑一下这样的应用场景:如何将一匹马变成斑马?问题的难点在于我们并没有马和斑马的匹配图片作为训练样本。
一个天才的想法是:我们可以训练一个Generator,把某马变为某斑马。为保证变出来的斑马确实像斑马,我们训练一个Discriminator,来区分斑马的真假。为了保证变出的斑马又和原马很相似,我们再将该斑马变为马(这是另一个Generator),并判断新马和原马的差异的大小。如果变出的斑马和原马不像,那么由斑马变出的新马将和原马有很大的差异。将同样的操作应用于把某斑马变为某马的过程。
1. 数据加载器
数据下载:
wget https://people.eecs.berkeley.edu/~taesung_park/CycleGAN/datasets/horse2zebra.zip
数据加载器是由CreateDataLoader()函数创建的。其返回一个CustomDatasetDataLoader类,该类派生自BaseDataLoader。而在CustomDatasetDataLoader的初始化函数initialize()中,将使用Pytorch的Dataloader,其需要一个Dataset对象,该对象又是由CreateDataset()函数创建。查看该函数的定义,并结合本例的实际情形,所需的Dataset是UnalignedDataset对象。
在UnalignedDataset对象的__getitem__()函数中,主要完成的是获取某个索引的图片,做一定的变换,然后将图片返回。
2. 网络模型
网络模型是由create_model()函数创建的,其返回一个CycleGANModel对象。在CycleGANModel的初始化函数中,将定义两个Generator,定义损失函数,定义优化器。
一些有用的链接
- 课程wiki: 本节课程的一些相关资源,包括课程笔记、课上提到的博客地址等。
- Wasserstein GAN
WGAN论文。 - Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks:
DCGAN,提出GAN的论文。 - Bedroom数据集的20%抽样。
- Deconvolution and Checkerboard Artifacts: 一个转置卷积可视化以及讲解棋盘效应的博客。
- Cycle GAN:
Github代码库。 - MutiModal GAN: 可以一次生成多种图像。
这篇关于第12篇 Fast AI深度学习课程——DarkNet、GAN的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!