手搓神经网络系列之 —— 梯度反传函数具体怎么写?(三)
本文将介绍几种特殊张量运算的反向传播。
众所周知,神经网络中涉及到关于张量的运算不止于加减乘除,还有最大最小值运算等,与常规运算不同,有些特殊运算在数学上甚至是不可微的,但即便如此,我们仍然需要将梯度反传回去。
本系列全部代码见下面仓库:
如有算法或实现方式上的问题,请各位大佬轻喷 + 指正!
本文将介绍 Swapaxes 运算、Max 运算的反向传播。其他特殊运算,就不再一一列举!(主要太懒了)
Swapaxes
与矩阵的转置类似,张量运算 swapaxes 的目的就是交换某两个轴,其定义为:
def swapaxes(self, axis1, axis2):
...axis1、axis2 即为交换的两个轴的序号。稍微想一想容易知道,怎么换过去的就怎么换回来,反传回去的梯度应该就是把梯度的 axis1、axis2 轴交换一下。所以在正向传播时,我们就要把两个交换轴的序号记录在计算图里。
正向传播:
def swapaxes(self, axis1, axis2):
result = Tensor(self.data.swapaxes(axis1, axis2), dtype=self.dtype, requires_grad=self.requires_grad)
if result.grad_enable:
result.children = [(self, axis1, axis2)]
result.grad_fn = SwapaxesBackward()
return result反向传播:
class SwapaxesBackward(BackwardFcn):
def __init__(self):
super(SwapaxesBackward, self).__init__()
def calculate_grad(self, grad, children, place):
axis1, axis2 = children[0][1:]
return grad.swapaxes(axis1, axis2)是不是非常 ez!还有很多其他运算,例如 Transpose 等,也需要记录一些额外的信息。另外,有一些运算虽然并不需要额外信息,但若提供了额外的信息,会对梯度计算有很大的帮助,例如大名鼎鼎的交叉熵函数,各位可以自己推导一下交叉熵函数的梯度,然后思考一下提供什么信息可以加快交叉熵函数梯度的计算。推导过程可以看我这篇文章。
Max
Max 运算在数学上是一个分段的函数,和绝对值函数一样,在分段端点处不可微,在神经网络里经常遇到各种取最大值的运算,常见的例如 MaxPooling。虽然它存在不可微点,但我们仍然有办法搞出一个形式上的梯度,并将其反传回去。
考虑以下 $2\times 3$ 矩阵:
对其做 max 运算有三种情况:1、全局 max;2、对每一行取 max;3、对每一列取 max。对于这些情况,我们的代码中均需要实现梯度反向传播。不过,在写梯度反传函数以前,需要先看一下梯度要怎么传。
第一种情况,对全局取 max,得到的结果为 6:
我们将梯度这个概念还原回最本质的东西,即当某一个量 $x$ 发生微小的变化 $\delta x$ 时,结果产生的变化 $\delta y$ 与这个小改变量 $\delta x$ 比值 $\delta y / \delta x$ 的极限。在这个全局的 max 运算下,根据 max 的逻辑可知,对任何非最大值进行微小的改变,均不会影响到最终的结果;而当最大值所在的位置发生微小的改变时,结果也将产生等值的改变。综合上面的考虑,它的梯度就应该是长这样的:
后两种情况,其实可以统一为一种情况,即对某些轴取 max(可以不止一个轴),这里通过对行取 max 的例子来研究一下规律。
首先需要改变一下说法,所谓对 “行” 取 max,过程是固定矩阵的 “行” 进行遍历,在每一行中找出最大值,相当于在 max 运算中指定 axis=1,而对 “列” 取 max,同理相当于 axis=0,但在高维张量的情况下,就没有行和列的概念了,故以后都将用遍历轴 axis 来说明。
类似地,同样容易得到梯度回传系数为
即,在两个最大值所处的位置上为 1,其余为 0。这也符合常人的理解。
还有一种特殊情况,即存在多个最大值的情况,例如:
这种情况我将其处理为多个最大值按相同权重均分梯度,得到的梯度回传系数如下:
综合以上考虑,我们就可以着手进行代码的实现了。这里我的思路是,将 Max 运算得到的结果以及 axis 值存在计算图中,反向传播时调用这两个量,将运算得到的结果通过复制自身的方式,扩展到与原张量相同。
回到这个例子:
可以先将 Max 得到的结果 $\begin {bmatrix} 3 & 6\end {bmatrix}$ 经过以下两步变换:
- 根据
axis的值添加维度,使结果成为keepdims=True的形式(对keepdims这个参数不太了解的童鞋可以先去了解一下)。 - 根据原张量的形状,在
axis的方向上 tile 自身。
然后将扩展后的结果,与原张量进行比较:
最后沿着所有 axis 进行归一化(和为 1):
处理的过程大概就是上面这样,细节可以看我的 MaxBackward 代码!对于 Min 和 Avg 运算,大体也差不多,故不再重复。
张量的特殊运算还有一大堆,但这里空白太小,我写不下。不过,相信看到这里,推导梯度并将其转换为代码实现对你而言应该不是什么大难题了!接下来的内容将围绕著名的卷积神经网络展开,重点讲解卷积运算的正向、反向传播,还会补充池化运算以及 BN 层的细节。























































































































































































































































































































































































































































.png)
.jpg)
.jpg)
.gif)
.jpg)
.gif)
.jpg)
.jpg)
.gif)
.jpg)
.gif)
.jpg)
.gif)
.jpg)
.png)
.jpg)
.jpg)
.gif)
.jpg)
.jpg)
.jpg)
.gif)
.gif)
.gif)
.jpg)
.gif)
.jpg)
.gif)
.jpg)
.gif)
.gif)
.gif)
.jpg)
.gif)
.jpg)
.gif)
.jpg)
.gif)
.jpg)
.jpg)
.gif)
.jpg)
.jpg)
.gif)
.gif)
.jpg)
.png)
.jpg)
.jpg)
.gif)











































































































