ReflectionPad2d、InstanceNorm2d详解及实现

这两天研究快速风格迁移,pytorch的实现中有几个平时不常见的Layer在里面,第一个是ReflectionPad2d

这个名字虽然一看就知道是用来对输入数据进行扩边的,可是pad操作不都是放在卷积层里面作为一部分吗?单独拿出来作为一层的话,反向传播时应该怎么处理?带着这些疑问用pytorch试验了一下,先看以下代码:

>>> import torch
>>> import torch.nn as nn
>>> pad = nn.ReflectionPad2d(1)
>>> a = torch.arange(16, dtype=torch.float32).reshape(1,1,4,4)
>>> a
tensor([[[[ 0.,  1.,  2.,  3.],
          [ 4.,  5.,  6.,  7.],
          [ 8.,  9., 10., 11.],
          [12., 13., 14., 15.]]]])
>>> pad(a)
tensor([[[[ 5.,  4.,  5.,  6.,  7.,  6.],
          [ 1.,  0.,  1.,  2.,  3.,  2.],
          [ 5.,  4.,  5.,  6.,  7.,  6.],
          [ 9.,  8.,  9., 10., 11., 10.],
          [13., 12., 13., 14., 15., 14.],
          [ 9.,  8.,  9., 10., 11., 10.]]]])

使用的填充宽度是1,填充结果和原来的数组相比宽、高都要加2,这一点是没问题的,只不过并不是使用0来填充。如果仔细观察的话,可以发现使用的是镜像填充的方法,以边界的一行或者一列为对称轴,就可以发现两规律了

ReflectionPad2d的forward操作是很好实现的,但是在backward的中要怎么处理传回来的梯度呢?再看两个例子:

>>> a = torch.ones(1,1,3,3, requires_grad=True)
tensor([[[[1., 1., 1.],
          [1., 1., 1.],
          [1., 1., 1.]]]], requires_grad=True)

这里创建一个张量a,形如上

>>> def printGrad(grad):
	print(grad)

>>> b = pad(a)
>>> b.register_hook(printGrad)

b是a经过ReflectionPad2d.forward后的输出结果,不用看,5行5列全是1,而printGrad是注册的一个hook函数,这个函数会在接下来进行backward时梯度传到norm这一层时调用,功能就是将传进来的梯度打印出来而已

>>> c = b.sum()
>>> c.backward()
tensor([[[[1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1.]]]])

当调用c.backward()时,正常打印了传给norm的梯度,也全是1

简单说一下,这里的梯度就是 \nabla_{b} c=\frac{\partial c}{\partial b} ,b是矩阵,c是所有元素之和,为标量。所以c对b的梯度就是c对b的每一个元素求偏导,自然就得到上面的结果了。然后是

>>> a.grad
tensor([[[[1., 3., 1.],
          [3., 9., 3.],
          [1., 3., 1.]]]])

以上是c对a的梯度,也就是 \nabla_{a} c=\frac{\partial c}{\partial a}=\frac{\partial c}{\partial b}\frac{\partial b}{\partial a} ,其中 \frac{\partial c}{\partial b} 是已知的,那么剩下的就是要弄明白 \frac{\partial b}{\partial a} 的值,或者所norm.backward中对传进来的梯度进行了怎样的变换

光看上面还看不出什么结果,为了进行验证,再看一个例子:

>>> a = torch.ones(1,1,5,5, requires_grad=True)
>>> b = pad(a)
>>> b.register_hook(printGrad)
>>> b.sum().backward()
tensor([[[[1., 1., 1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1., 1., 1.],
          [1., 1., 1., 1., 1., 1., 1.]]]])
>>> a.grad
tensor([[[[1., 2., 1., 2., 1.],
          [2., 4., 2., 4., 2.],
          [1., 2., 1., 2., 1.],
          [2., 4., 2., 4., 2.],
          [1., 2., 1., 2., 1.]]]])

有这个例子作为对比,相信应该都很清楚norm.backward的所作所为了吧。简单的来说就是:在forward中所扩充的元素是哪个位置的镜像,在backward中就把其梯度叠加到该位置上。为了更好的说明这个过程,下面使用numpy实现一遍:

>>> import numpy as np
>>> x = np.ones((7,7))
>>> top = bottom = left = right = 1
>>> x
array([[1., 1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1., 1.]])

先创建一个(7,7)的矩阵,代表上面b的梯度,top,bottom,left,right是四个边的扩展宽度,然后再来演示norm.backward的过程:

>>> x[top+1:2*top+1] += x[top-1::-1]
>>> x[-1-2*bottom:-1-bottom] += x[:-bottom-1:-1]
>>> x
array([[1., 1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1., 1.],
       [2., 2., 2., 2., 2., 2., 2.],
       [1., 1., 1., 1., 1., 1., 1.],
       [2., 2., 2., 2., 2., 2., 2.],
       [1., 1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1., 1.]])

先把第1行和最后一行分别加到第3和第5行上

>>> x[...,left+1:2*left+1] += x[...,left-1::-1]
>>> x[...,-1-2*right:-1-right] += x[...,:-right-1:-1]
>>> x
array([[1., 1., 2., 1., 2., 1., 1.],
       [1., 1., 2., 1., 2., 1., 1.],
       [2., 2., 4., 2., 4., 2., 2.],
       [1., 1., 2., 1., 2., 1., 1.],
       [2., 2., 4., 2., 4., 2., 2.],
       [1., 1., 2., 1., 2., 1., 1.],
       [1., 1., 2., 1., 2., 1., 1.]])

同样的,对列元素也这样操作一遍

>>> x[top:-bottom, left:-right]
array([[1., 2., 1., 2., 1.],
       [2., 4., 2., 4., 2.],
       [1., 2., 1., 2., 1.],
       [2., 4., 2., 4., 2.],
       [1., 2., 1., 2., 1.]])

最后,剪裁正中间的那部分矩形,就得到了想要的结果。


第二个是InstanceNorm2d,这个是一个归一化层,官网上有比较详细的说明,其公式如下:

z=\beta \frac{x-\overline{x}}{\sqrt{\sigma^2+\epsilon}}+\gamma

其中 \beta,\gamma 是仿射系数(仿射简单的说就是线性变换加平移),另外,设

y= \frac{x-\overline{x}}{\sqrt{\sigma^2+\epsilon}}

x 是一个矩阵, \overline{x}x 的平均值, \sigma 是其标准差, \epsilon 是一个非常小的值(默认是1e-5),是防止标准差为0时产生除0的异常。为了方便计算,这里不要它,直接用:

y= \frac{x-\overline{x}}{\sigma}

而且标准差为0,就意味着x的所有元素的值都一样,整个式子直接为0,下面用numpy来实现其forward和backward的计算:

def forward(self, x):
    self.std = x.std()
    if self.std == 0:
        x[...] = 0
        return x
    self.m = x.mean()
    self.y = (x - self.m) / self.std
    return self.y * self.beta + self.gamma

为了详细说明backward的实现,先推导其梯度。设从上一层传回来的梯度为 \frac{\partial L}{\partial z} ,backward的目的是为了计算出 \frac{\partial L}{\partial x} 继续往回传。根据链式法则可知:

\frac{\partial L}{\partial x}=\frac{\partial L}{\partial z}\frac{\partial z}{\partial y}\frac{\partial y}{\partial x}

所以要求的就是 \frac{\partial z}{\partial y}\frac{\partial y}{\partial x} 。首先,显然有:

\frac{\partial z}{\partial y}=\beta

\frac{\partial y}{\partial x} 就比较复杂一点,这里先求 \frac{\partial y}{\partial \sigma}\frac{\partial y}{\partial \overline{x}} (一大波公式正在接近)

\frac{\partial y}{\partial \sigma}=(x-\overline{x}) \frac{\partial}{\partial \sigma} \sigma^{-1}=(\overline{x}-x) \sigma^{-2}=\frac{\overline{x}-x}{\sigma^{2}}

因为标准差受平均值的影响,所以可以把标准差看做是关于平均值的函数

\frac{\partial y}{\partial \overline{x}}=\frac{(\overline{x}-x)^{\prime} \sigma-(\overline{x}-x) \sigma^{\prime}}{\sigma^{2}}

\sigma^{\prime}=\frac{\partial \sigma}{\partial \overline{x}}=\frac{\partial}{\partial \overline{x}}\left(\sum_{i}^{m}\left(x_{i}-\overline{x}\right)^{2}\right)^{\frac{1}{2}}=\frac{1}{2}\left(\sum_{i}^{m}\left(x_{i}-\overline{x}\right)^{2}\right)^{-\frac{1}{2}} \sum_{i}^{m} \frac{\partial}{\partial \overline{x}}\left(x_{i}-\overline{x}\right)^{2} =\frac{1}{2 \sigma} \sum_{i}^{m} \frac{\partial}{\partial \overline{x}}\left(x_{i}-\overline{x}\right)^{2}=\frac{1}{\sigma} \sum_{i}^{m} \overline{x}-x_{i}=0

推导了一长串最后得出结果为0我也是惊了,害我还检查了好多遍。最后

\frac{\partial y}{\partial \overline{x}}=\frac{(\overline{x}-x)^{\prime} \sigma}{\sigma^{2}}=-\frac{1}{\sigma}

最后,因为平均值和标准差都随着 x 的变化而变化,所以:

\frac{\partial y}{\partial x_{i}}=\frac{\left(x_{i}-\overline{x}\right)^{\prime} \sigma-\left(x_{i}-\overline{x}\right) \sigma^{\prime}}{\sigma^{2}}=\frac{\left(x_{i}-\overline{x}\right)^{\prime}}{\sigma}-\frac{\left(x_{i}-\overline{x}\right) \sigma^{\prime}}{\sigma^{2}}=\frac{1}{\sigma}+\frac{\partial y}{\partial \overline{x}} \overline{x}^{\prime}+\frac{\partial y}{\partial \sigma} \sigma^{\prime}

\overline{x}^{\prime}=\frac{\partial \overline{x}}{\partial x_{i}}=\frac{1}{m} \frac{\partial}{\partial x_{i}} \sum_{1}^{m} x_{i}=\frac{1}{m}

\sigma^{\prime}=\frac{\partial \sigma}{\partial x_{i}}=\frac{\partial}{\partial x_{i}}\left(\sum\left(x_{i}-\overline{x}\right)^{2}\right)^{\frac{1}{2}}=\frac{1}{2}\left(\sum\left(x_{i}-\overline{x}\right)^{2}\right)^{-\frac{1}{2}} \sum \frac{\partial}{\partial x_{i}}\left(x_{i}-\overline{x}\right)^{2}

=\frac{1}{2 \sigma} \sum \frac{\partial}{\partial x_{i}}\left(x_{i}-\overline{x}\right)^{2}

=\frac{1}{\sigma}\left(\frac{m-1}{m}\left(x_{i}-\overline{x}\right)+\frac{1}{m}\left(\left(\overline{x}-x_{1}\right)+\cdots+\left(\overline{x}-x_{i-1}\right)+\left(\overline{x}-x_{i+1}\right)+\cdots+\left(\overline{x}-x_{m}\right)\right)\right)

=\frac{1}{\sigma}\left(x_{i}-\overline{x}\right)

最终得到:

\frac{\partial y}{\partial x_{i}}=\frac{1}{\sigma}+\frac{\partial y}{\partial \overline{x}} \overline{x}^{\prime}+\frac{\partial y}{\partial \sigma} \sigma^{\prime}=\frac{1}{\sigma}+\frac{\partial y}{\partial \overline{x}} \frac{1}{m}+\frac{\partial y}{\partial \sigma} \frac{1}{\sigma}\left(x_{i}-\overline{x}\right)

=\frac{1}{\sigma}-\frac{1}{m \sigma}-\frac{\left(x_{i}-\overline{x}\right)^{2}}{\sigma^{3}}

因为 y= \frac{x-\overline{x}}{\sigma} ,且 y 也是一个矩阵,令

y_i= \frac{x_i-\overline{x}}{\sigma}

所以,上式可以写作

\frac{\partial y}{\partial x_{i}}=\frac{1}{\sigma}\left(1-\frac{1}{m}-y_{i}^{2}\right)

\frac{\partial L}{\partial x_i}=\frac{\beta}{\sigma}\left(1-\frac{1}{m}-y_{i}^{2}\right)

\frac{\partial z}{\partial x}=\frac{\beta}{\sigma}\left(1-\frac{1}{m}-y^{2}\right)

最后就是根据以上式子套代码了:

def backward(self, eta):
    return eta * (self.beta / self.std) * ((1 - self.m) - self.y**2)

上面的eta就是 \frac{\partial L}{\partial z}

编辑于 2019-05-25 23:26