一、阶梯化学习速率
在前述课程中,我们使用了重启学习速率、三角化学习速率等技巧,以实现更快的收敛、更稳定的泛化。上述技巧均是通过设置相应参数,来实现整个训练过程的学习速率的变化。事实上,一个更通用的方法,是在不同的训练阶段(训练阶段由epoch
序列指明)使用指定的学习速率。(这一想法可通过调用多次fit()
函数,每次使用不同的学习速率来达到;但更便捷的方式是提供一套API
。)Fast.AI
提供了实现这种机制的TrainingPhase API
。一个训练阶段Training Phase
可调节的参数有:
- 持续的
epoch
数。 - 优化策略。
- 学习速率(可为数值,可为数组)。
- 学习速率变化方式。
- 动量(对
Adam
方法,即为beta1
)。 - 动量衰减方法。
其使用示例如下:
phases = [TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = 1e-2),
TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = (1e-2,1e-3), lr_decay=DecayType.LINEAR),
TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = 1e-3)]
learn.fit_opt_sched(phases)
其中未设定的动量值的默认值为0.9
。而DecayType
可选的有LINEAR
、COSINE
、EXPONETIAL
、POLYNOMIAL
(可指定阶数)。使用learn.sched.plot_lr()
可绘制学习速率和冲量的曲线。通过TrainingPhase API
可实现重启学习速率
、三角化学习速率
等学习速率的定制。
另外,还可通过TrainingPhase API
实现学习速率的搜索。这可通过设置fit_opt_sched(stop_div=True)
来实现,当损失函数过大时,训练过程就中止了。
TrainingPhase API
还支持中途更换数据集的操作。这是通过指定fit_opt_sched()
函数中的data_list
参数来实现的。
二、ResBlock
的修改Inception
在Inception-ResNet-v2
中,新的网络结构单元Inception
被提出,用于取代ResBlock
,图示如下:

- 相比于
ResBlock
,Inception
多了第三条通路,即Conv(1x1)-Conv(1x7)-Conv(7x1)
,且这条通路的输出和那条两层卷积通路的输出,会沿特征维度的方向堆叠在一起(而非相加)。这种思路实际上在DenseNet
中也有体现。DenseNet
的结构单元和ResBlock
相近,但其对原输入的使用,不是使之和两层卷积通路的输出相加,而是相拼接。 Inception
的第三条通路的Conv(1x7)-Conv(7x1)
部分,实际相当于Conv(7x7)
,可以视为一个可做两维分解的7x7
卷积核的作用。这样可以大幅度减少变量数目(由49
变为14
)。另外,这样所取得的效果,也往往会优于直接使用Conv(7x7)
,原因是实际图像中往往存在沿行、沿列方向的结构特征。
三、风格迁移
风格迁移的目标是:输入一张内容图像和一张风格图像,输出一张内容和内容图像接近、风格和风格图像接近的图像。基本思路是:构建一个评估输出图像和内容图像以及风格图像的差异的损失函数,从一张随机噪声图像开始,利用梯度下降法更新图像,直至图像满足设定的条件。
1. 内容损失函数
首先考虑内容损失函数。一个最直接的想法是使用源图像和生成图像的在像素空间的欧氏距离。但这样就限定了输出图像与源图像很接近。为了给输出图像更大的自由度,可使用两张图像在特征空间的欧氏距离。而要获得图像特征,就可使用神经网络。更高层级的特征,就可赋予生成图像更大的自由度。
2. 特征提取
由上述分析,我们首先使用已训练好的网络,提取图像特征。本例中使用VGG16
网络,可使用如下语句获取:
m_vgg = to_gpu(vgg16(True)).eval()
vgg16(True)
中调用了torch.utils.model_zoo.load_url()
来下载模型,模型的默认存储位置为~/.torch/models/
。由于训练过程中需要更新的是生成图像的像素,不需要训练网络参数,仅需通过网络提取图像特征,因此,使网络处于eval()
状态。
使用如下语句获取需要对输入的内容图像要做的变换:
trn_tfms,val_tfms = tfms_from_model(vgg16, sz)
其中val_tfms
是所需要的。tfms_from_model()
函数,实际上通过vgg16
这一模型参数,找到该模型所依赖的数据集的统计信息,然后调用tfms_from_stats()
函数返回所要做的变换。val_tfms
包含的变换为fastai.transforms
定义的Scale
、CenterCrop
、Normalize
、ChannelOrder
,即缩放、裁剪、归一化、调整通道顺序。
将源图像整理为批形式,进行上述变换,然后取VGG
中的某一层的输出作为图片特征:
mg_tfm = val_tfms(img)
targ_t = m_vgg(VV(img_tfm[None]))
m_vgg = nn.Sequential(*children(m_vgg)[:37])
其中img_tfm[None]
即将一幅图像增加维度,变为一个batch
。
3. 仅使用内容损失函数,更新图像,以测试网络
定义一个随机噪声图片并平滑滤波,将之通过val_tfms
所包含的变换,转换成Pytorch
的Variable
类型,并标记对齐计算梯度(requires_grad=True
)。
opt_img = np.random.uniform(0, 1, size=img.shape).astype(np.float32)
opt_img = scipy.ndimage.filters.median_filter(opt_img, [8,8,1])
opt_img = val_tfms(opt_img)/2
opt_img_v = V(opt_img[None], requires_grad=True)
定义内容损失函数,定义优化器,定义每步的更新策略,然后开始训练。
def actn_loss(x): return F.mse_loss(m_vgg(x), targ_v)*1000
optimizer = optim.LBFGS([opt_img_v], lr=0.5)
def step(loss_fn):
global n_iter
optimizer.zero_grad()
loss = loss_fn(opt_img_v)
loss.backward()
n_iter+=1
if n_iter%show_iter==0: print(f'Iteration: {n_iter}, loss: {loss.data[0]}')
return loss
while n_iter <= max_iter: optimizer.step(partial(step,actn_loss))
上述语句使用了新的优化策略LBFGS
,名称中的BFGS
是四个发明人姓名的首字母,L
表示Limited Memory
。这个优化策略使用了二阶导数Hessian
矩阵。二阶导数标识着一阶导数Jacobian
向量的变化的快慢。如果一阶导数变化较慢,在使用梯度下降法时,可一步跨越较大的距离;反之,应使用较小的学习速率。然而,如果使用解析方法计算Hessian
矩阵,计算量过大。可考虑使用数值方法。而LBFGS
则是仅记录最近的10~20
次梯度值,然后用之计算二阶导数。
训练后可得图片:

课程中使用了一个钩子类来封装有关钩子的操作。
class SaveFeatures():
features=None
def __init__(self, m): self.hook = m.register_forward_hook(self.hook_fn)
def hook_fn(self, module, input, output): self.features = output
def close(self): self.hook.remove()
然后选择VGG
模型中每个Max Pooling
层之前的某层,进行输出的保存。
block_ends = [i-1 for i,o in enumerate(children(m_vgg))
if isinstance(o,nn.MaxPool2d)]
sf = SaveFeatures(children(m_vgg)[block_ends[3]])
m_vgg(VV(img_tfm[None]))
targ_v = V(sf.features.clone())
使用上述方法,就可逐个测试各层的输出,然后选取一个较为合适的特征。
若选取32
层输出的特征,则可得训练结果:

5. 风格损失函数
考虑使用风格图像经过VGG
网络后输出的特征,构建图像纹理特征。图像纹理特征,需要去除像素的空间信息。一个做法是使用图像特征的相关矩阵(又称Gram
矩阵)。其解释如下:设图像的一个特征向量
x
⃗
\vec{x}
x表示色彩亮度分布,另一个特征向量
y
⃗
\vec{y}
y表示物体边缘,那么如果二者对应位置同相(同正负),且取值较大,则说明图像特征为边缘处较亮。而
x
⃗
⋅
y
⃗
\vec{x}\cdot\vec{y}
x⋅y则是一个综合的结果,说明了整幅图像中边缘较亮的情形出现的强度,这实际就是去除了像素空间信息的纹理特征的表达。
风格损失函数就定义为生成图像和风格图像的Gram
矩阵(在所有特征输出层都计算Gram
矩阵)的欧氏距离。
sfs = [SaveFeatures(children(m_vgg)[idx]) for idx in block_ends]
def gram(input):
b,c,h,w = input.size()
x = input.view(b*c, -1)
return torch.mm(x, x.t())/input.numel()*1e6
def gram_mse_loss(input, target): return F.mse_loss(gram(input), gram(target))
def style_loss(x):
m_vgg(opt_img_v)
outs = [V(o.features) for o in sfs]
losses = [gram_mse_loss(o, s) for o,s in zip(outs, targ_styles)]
return sum(losses)
然后将内容损失和风格损失结合起来,就可训练风格迁移网络了。
一些有用的链接
- 课程wiki: 本节课程的一些相关资源,包括课程笔记、课上提到的博客地址等。
- Deep Painterly Harmonization: 使用神经网络实现无缝融合的论文。
- Stochastic Weight Averaging: 讲述如何在
Fast.AI
代码中添加SWA
支持的技术博客。 - Progressive Growing of GANs for Improved Quality, Stability, and Variation: 讲述分辨率递进的
GAN
的论文。