第13篇 Fast AI深度学习课程——风格迁移

一、阶梯化学习速率

在前述课程中,我们使用了重启学习速率、三角化学习速率等技巧,以实现更快的收敛、更稳定的泛化。上述技巧均是通过设置相应参数,来实现整个训练过程的学习速率的变化。事实上,一个更通用的方法,是在不同的训练阶段(训练阶段由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可选的有LINEARCOSINEEXPONETIALPOLYNOMIAL(可指定阶数)。使用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,图示如下:

图 1.Inception结构
其中有几点需要说明。
  • 相比于ResBlockInception多了第三条通路,即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定义的ScaleCenterCropNormalizeChannelOrder,即缩放、裁剪、归一化、调整通道顺序。

将源图像整理为批形式,进行上述变换,然后取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所包含的变换,转换成PytorchVariable类型,并标记对齐计算梯度(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次梯度值,然后用之计算二阶导数。

训练后可得图片:

图 2.采用深层特征训练所得结果
##### 4. 选取最合适的特征 上述只是测试了某一层的特征的效果。如何选择合适特征呢?可以使用`Pytorch`提供的钩子机制,在前向传播的过程中,把感兴趣层的输出保存下来。对于`Pytorch`中的`module`对象,其可通过`register_forward_hook(module, input, output) -> None`和`register_backward_hook(module, grad_input, grad_output) -> Tensor or None`函数注册钩子。注册后,每次前向传播或后向传播结束后,就会调用钩子函数。钩子函数不应该改变`module`的状态,而且在使用后要及时删除,以避免不必要的运行。

课程中使用了一个钩子类来封装有关钩子的操作。

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层输出的特征,则可得训练结果:

图 3.采用较浅层特征训练所得结果
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)

然后将内容损失和风格损失结合起来,就可训练风格迁移网络了。

一些有用的链接

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值