本节课程将介绍很火的对抗生成网络。由于这一网络结构很新,目前(课程发布时,18年4月份)Fast.AI
尚未提供相应的封装,因此需要使用Pytorch
的数据结构来构建。在构建GAN
之前,我们将在CIFAR10
数据上,仅使用Pytorch
的数据结构,构建结构较简单的Darknet
,以展示利用Pytorch
搭建网络的思路。
Darknet
下载后解压。由于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
的尺寸。需要说明的是,在图片扩展时,使用的是对称反射的方式,而非补零扩展,这样效果较好。
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 *= 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
。
由网络构建学习器模型,并设置损失函数为交互熵。
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//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
的特征,然后进行多次转置卷积,将之扩展为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, 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
,此时利用总体的损失函数计算梯度值,同时更新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
),并判断新马和原马的差异的大小。如果变出的斑马和原马不像,那么由斑马变出的新马将和原马有很大的差异。将同样的操作应用于把某斑马变为某马的过程。
数据下载:
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__()
函数中,主要完成的是获取某个索引的图片,做一定的变换,然后将图片返回。
网络模型是由create_model()
函数创建的,其返回一个CycleGANModel
对象。在CycleGANModel
的初始化函数中,将定义两个Generator
,定义损失函数,定义优化器。
WGAN
论文。DCGAN
,提出GAN的论文。Github
代码库。