深度学习|误差逆传播:梯度速解

引言

我们知道训练神经网络模型的核心是以损失函数为基准来调整优化网络参数,使得网络的输出尽可能接近真实标签。在神经网络中,优化网络参数需要计算每个权重参数的梯度,不同的网络结构,计算梯度的方式和复杂度往往大不相同,有没有一种算法,即可以有效囊括所有类型的网络结构的梯度计算,又足以保证梯度计算的高效性?答案就是我们今天要讲的误差逆传播算法。

在这里插入图片描述

链式法则

要理解误差逆传播算法,需要先了解微分中链式法则的概念。链式法则是微分中的基本法则,可用于求解复合函数的导数。

如果某个函数由复合函数表示,则该复合函数的导数可以用构成该复合函数的各个函数的导数的乘积表示。

以式 1 所示的复合函数为例:

z = t 2 t = x + y (1) z = t^2 \\ t = x + y \tag{1} z=t2t=x+y(1)

通过链式法则求解 ∂ z ∂ x \frac{\partial{z}}{\partial{x}} xz

∂ z ∂ x = ∂ z ∂ t ∂ t ∂ x = 2 t × 1 = 2 ( x + y ) \frac{\partial{z}}{\partial{x}} = \frac{\partial{z}}{\partial{t}} \frac{\partial{t}}{\partial{x}} = 2t \times 1 = 2(x + y) xz=tzxt=2t×1=2(x+y)

可见一个复杂函数的求导问题可以分解为组成该复杂函数的局部函数的求导问题,我们完全可以将复杂函数的导数等价的表示为其所有局部函数的导数的乘积。

在神经网络中,误差逆传播算法就是利用链式法则来计算网络中每个参数的梯度。

误差逆传播

在前文「深度学习|模型训练:手写 SimpleNet」中,我们演示了使用数值微分方式求梯度的过程,数值微分的方式求梯度简单、易于理解与实现,但它的问题是计算效率很低。在 SimpleNet 的示例中,我们使用数值微分法训练所需时间长达 27.7 小时,几乎是不可用的状态。

那么有没有更高效的替代方式呢?终于轮到神经网络的主角算法误差逆传播出场了!

参考上文求复合函数(式 1)关于 x 的导数 ∂ z ∂ x \frac{\partial{z}}{\partial{x}} xz 的求解过程,给定 x 与 y,按照函数式求解 z 的正向计算的过程,就好比神经网络的前向传播Forward Propagation),而沿着函数正向计算的链路,从最末端逆向计算每个局部函数的导数,最终相乘从而得到该复杂函数的导数,就好比神经网络的逆传播Backward Propagation)。

前向传播(Forward Propagation):将输入数据通过网络进行运算,得到网络的输出、输出与目标值之间的误差
逆传播(Backward Propagation):从输出层开始,将误差逆传播到隐藏层,直到输入层。逆传播过程可以计算每个权重的梯度,即误差相对于每个权重的偏导数。

误差逆传播error BackPropagation,简称 BP)就是基于数学推导的解析性(相对于数值微分的数值性)梯度计算方法(符号微分Symbolic Differentiation),按照数学中求导的链式法则,局部导数会按正向传播的反方向传递。

以求解 ∂ z ∂ x \frac{\partial{z}}{\partial{x}} xz 为例,我们可以用如下图 1 中红色箭头所指过程表示该求导过程:

在这里插入图片描述

图 1 所示从左到右是复合函数的正向传播过程,表示的是 t = x + y t = x + y t=x+y z = t 2 z = t^2 z=t2 的正向计算过程。

从右往左是复合函数的逆传播过程,通过逆传播求函数关于 x 的导数 ∂ z ∂ x \frac{\partial{z}}{\partial{x}} xz,只需要沿着正向计算的链路逆向计算每个局部函数的导数,例如从输出 zz 本身的导数是 ∂ z ∂ z \frac{\partial{z}}{\partial{z}} zz,从 zt 的导数是 ∂ z ∂ t \frac{\partial{z}}{\partial{t}} tz,从 tx 的导数是 ∂ t ∂ x \frac{\partial{t}}{\partial{x}} xt,最终将每个环节的导数相乘即是该复合函数的导数 ∂ z ∂ z ∂ z ∂ t ∂ t ∂ x \frac{\partial{z}}{\partial{z}} \frac{\partial{z}}{\partial{t}} \frac{\partial{t}}{\partial{x}} zztzxt(其中 ∂ z ∂ z \frac{\partial{z}}{\partial{z}} zz 可忽略)。这就是 BP 算法的基本思想。

不难发现,神经网络中的前向传播都是由一些简单的加法、乘法等常用的运算复合而成,而神经网络的逆传播就是求解网络整个“复合函数”关于网络各层中权重参数梯度

我们在了解了 BP 算法的基本思路后,不难得出这些梯度的求解方式:沿着网络的正向运算过程,反向从输出层开始,往前计算每层运算的局部梯度,然后将求解目标参数梯度的完整链路上的所有局部梯度相乘,得到的就是目标参数的梯度。

接下来我们可以找到在逆传播过程中,使用 BP 算法求解加法、乘法等常用运算的梯度的规律。应用这些规律,我们可以在神经网络的逆传播运算过程中高效地计算梯度。

加法的逆传播

z = x + y z = x + y z=x+y 为例,其梯度 ( ∂ z ∂ x \frac{\partial{z}}{\partial{x}} xz ∂ z ∂ y \frac{\partial{z}}{\partial{y}} yz) 永远为 (1, 1)。

因此加法运算在逆传播时,总是将下游梯度乘以 1,即原封不动传递给上游。

我们可以使用 AddLayer 类实现加法运算的前向传播与逆传播:

class AddLayer:
    """
    加法运算的前向传播与逆传播
    """

    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        """
        前向传播

        Args:
            x: 输入 x
            y: 输入 y

        Returns:
            out: 输出
        """

        out = x + y

        return out

    def backward(self, dout):
        """
        逆传播

        Args:
            dout: 上游梯度

        Returns:
            dx: x 的梯度
            dy: y 的梯度
        """

        dx = dout * 1
        dy = dout * 1

        return dx, dy

这里采用了标准层封装的方式来实现加法运算,将加法运算封装成了一个可以被任意结构的神经网络直接复用的小组件,其他如乘法运算激活函数损失函数等我们都将采用这样的实现方式。采用这样的封装方式,我们就可以在组装我们想要的网络时随意选择我们想要的组件(基本运算单元)。在实际的生产级机器学习框架(如 Scikit-learn、TensorFlow、PyTorch 等)中,这些底层运算封装也正是采用了这样的方式实现。

乘法的逆传播

z = x y z = xy z=xy 为例,其梯度 ( ∂ z ∂ x \frac{\partial{z}}{\partial{x}} xz ∂ z ∂ y \frac{\partial{z}}{\partial{y}} yz) = (y, x)。

因此乘法运算的逆传播时,总是将下游梯度乘以上游相乘参数的值(翻转值)。比如 x 与 y 相乘,求关于 x 的偏导数时,y 是 x 的翻转值;求关于 y 的偏导数时,x 是 y 的翻转值。

同上,我们用 MulLayer 类实现乘法运算的前向传播与逆传播:

class MulLayer:
    """
    乘法运算的前向传播与逆传播
    """

    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        """
        前向传播

        Args:
            x: 输入 x
            y: 输入 y

        Returns:
            out: 输出
        """

        self.x = x
        self.y = y
        out = x * y

        return out

    def backward(self, dout):
        """
        逆传播

        Args:
            dout: 上游梯度

        Returns:
            dx: x 的梯度
            dy: y 的梯度
        """

        dx = dout * self.y
        dy = dout * self.x

        return dx, dy

逆传播求梯度

y = x 1 x 2 + x 3 y = x_1x_2 + x_3 y=x1x2+x3 为例,求 ( x 1 , x 2 , x 3 ) (x_1, x_2, x_3) (x1,x2,x3)= (100, 2, 300) 处的梯度。

在这里插入图片描述

y = x 1 x 2 + x 3 y = x_1x_2 + x_3 y=x1x2+x3 的计算链路如图 2,我们可以直接通过上文的 MulLayer 和 AddLayer 进行前向传播求 y,以及逆传播求关于 ( x 1 , x 2 , x 3 ) (x_1, x_2, x_3) (x1,x2,x3) 的梯度。

x1, x2, x3 = 100, 2, 300
mul_layer = MulLayer()
add_layer = AddLayer()

# forward
a = mul_layer.forward(x1, x2)
y = add_layer.forward(a, x3)
print(y)                # 500

# backward
da, dx3 = add_layer.backward(1)
dx1, dx2 = mul_layer.backward(da)
print(dx1, dx2, dx3)    # (x2, x1, 1) = (2, 100, 1)

运行结果与图 2 中所示 ( x 2 , x 1 , 1 ) (x_2, x_1, 1) (x2,x1,1)(输入 x 1 , x 2 , x 3 x_1, x_2, x_3 x1,x2,x3 各自的反向红色箭头是它们各自的梯度)一致,可见逆传播求梯度的结果是符合预期的。

以上逆传播求梯度过程可以直接应用在神经网络中对数组和矩阵的运算上:

x1, x2, x3 = np.array([100, 101, 102]), np.array([2, 3, 4]), np.array([300, 301, 302])
mul_layer = MulLayer()
add_layer = AddLayer()

# forward
a = mul_layer.forward(x1, x2)
y = add_layer.forward(a, x3)
print(y)                # [500 604 710]

# backward
da, dx3 = add_layer.backward(1)
dx1, dx2 = mul_layer.backward(da)
print(dx1, dx2, dx3)    # (x2, x1, 1) = [2 3 4] [100 101 102] 1

SoftmaxWithLoss 层

我们知道神经网络模型的训练过程就是根据损失函数关于权重参数的梯度优化权重参数的过程,其中求解损失函数关于权重参数的梯度是运算的核心。而损失函数往往是神经网络正向传播中的最后一个环节(训练过程的最后一个过程是损失函数,推理过程则一般不需要计算损失),根据 BP 算法的思路,在逆传播过程中,求解损失函数的“局部梯度”就成了求解权重参数梯度的第一步。

由于在多分类任务中,神经网络模型经常使用 Softmax 函数来对最终输出做归一化处理,我们在封装损失函数时,通常会将 Softmax 函数与损失函数结合在一起,这样的结构我们称之为SoftmaxWithLoss层。

下面我们以交叉熵误差为例,通过实现一个 SoftmaxWithLoss 层来演示 BP 算法及其“局部梯度”的求解过程。

假定网络的输出层有 n 个神经元(n 个分类类别),则 SoftmaxWithLoss 层的计算过程如图 3 所示:

在这里插入图片描述

从前面的层输入的是 ( a 1 , a 2 , . . . , a n ) (a_1, a_2, ..., a_n) (a1,a2,...,an),Softmax 层输出的是 ( y 1 , y 2 , . . . , y n ) (y_1, y_2, ..., y_n) (y1,y2,...,yn),实际结果分别是 ( t 1 , t 2 , . . . , t n ) (t_1, t_2, ..., t_n) (t1,t2,...,tn),Cross Entropy Error 输出的损失是 L。

正向传播

Softmax 往往作为网络输出层的激活函数,对网络的输出做最后的归一化处理;而在模型训练时,Softmax 的输出与实际结果作为损失函数的输入,可以计算出模型训练所需的损失值。可见 SoftmaxWithLoss 层实际就是经过了 Softmax 计算和 Loss Function 计算两个过程。

其中 Softmax 计算过程如式 2:

y k = e a k ∑ i = 1 n e a i (2) y_k = \frac{e^{a_k}}{\sum_{i=1}^{n} e^{a_i}} \tag{2} yk=i=1n

评论 50
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三余知行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值