深度学习入门(鱼书)学习记录 - 第5章 误差反向传播法

前言:上一章通过数值微分计算神经网络的权重参数的梯度,这种方法比较简单但比较耗时。所以现在介绍另外一种比较高效的方法 -- 误差反向传播法

目录

计算图

举例

为什么用计算图求解

计算图的优点

链式法则

链式求导

反向传播

加法节点的反向传播

乘法节点的反向传播

简单层的实现

乘法层实现 

加法层的实现

激活函数层的实现

ReLU层

Sigmoid层

Affine/Softmax层的实现

Affine层

Softmax-with-Loss 层


计算图

计算图通过节点和箭头表示计算过程。

举例

问题1:太郎在超市买了2个100日元一个的苹果,消费税是10%,请计算支付金额

计算图改进:将苹果和消费税标在圆圈外面

问题2:太郎在超市买了2个苹果、3个橘子。其中,苹果每个100日元, 橘子每个150日元。消费税是10%,请计算支付金额

综上,用计算图解题的情况下,需要按如下流程进行。

1.构建计算图。

2.在计算图上,从左向右进行计算。

第2步 从左往右计算的过程就是正向传播,反之从右往左反向传播

前面计算图中 各个节点处的计算都是局部计算,只需进行与自己有关的计算,不用考虑全局。

为什么用计算图求解

使用计算图最大的原因是,可以通过反向传播高效计算导数

上述问题1补充 求“支付金额关于苹果的价格的导数”,这个导数的值表示当苹果的价格稍微上涨时,支付金额会增加多少

图5-5 反向传播使用与正方向相反的箭头(粗线)表示。反向传播传递“局部导数”,将导数的值写在箭头的下方。在这个例子中,反向传播从右向左传递导数的值(1 → 1.1 → 2.2)。从这个结果中可知,“支付金额关于苹果的价格的导数”的值是2.2。这意味着,如果苹果的价格上涨1日元, 最终的支付金额会增加2.2日元(严格地讲,如果苹果的价格增加某个微小值, 则最终的支付金额将增加那个微小值的2.2倍)。

计算图的优点

可以通过正向传播和反向传播高效地计算各个变量的导数

链式法则

假设存在 y = f(x)的计算,这个计算的反向传播如图5-6所示

反向传播的计算顺序是,将信号E乘以节点的局部导数  \frac{\partial y}{\partial x},然后将结果传递给下一个节点。

链式求导

计算图的反向传播是基于链式法则成立的。

复合函数求导一般用链式法则,这里是高数基础知识就不赘述了。比如  z = (x+y){_{}}^{2}可以看作

z = t{_{}}^{2} 和 t = x + y两个式子构成。求z对x的导数 \frac{\partial z}{\partial x} = \frac{\partial z}{\partial t} \frac{\partial t}{\partial x} = 2t \cdot 1 = 2(x+y) 

用计算图表示如下,**2表示平方运算。

反向传播

加法节点的反向传播

以 z = x + y 为例观察其反向传播,\frac{\partial z}{\partial x} = 1,\frac{\partial z}{\partial y} = 1  用计算图表示。

加法节点的反向传播只是将输入信号输出到下一个节点。例如 15 = 10 + 5,反向传播会从上游传来1.3

乘法节点的反向传播

z = xy 对x和y分别求导 \frac{\partial z }{\partial x} = y, \frac{\partial z }{\partial y} = x

乘法的反向传播会将上游的值乘以正向传播时的输入信号的“翻转值” 后传递给下游。翻转值表示一种翻转关系,如图5-12所示,正向传播时信号 是x的话,反向传播时则是y;正向传播时信号是y的话,反向传播时则是x。

其中 6.5 = 1.3 * 5, 13 = 1.3 * 10

小结:加法的反向传播只是将上游的值传给下游, 并不需要正向传播的输入信号。但是,乘法的反向传播需要正向传播时的输入信号值。因此,实现乘法节点的反向传播时,要保存正向传播的输入信号。

简单层的实现

python实现上述购买苹果的例子,计算图的乘法节点称为“乘法层”(MulLayer),加法节点称为“加法层” (AddLayer)。

乘法层实现 

例如 z = xy

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

    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y

        return out

    def backward(self, dout):
        dx = dout * self.y
        dy = dout * self.x

        return dx, dy

backward()将从上游传来的导数(dout)乘以正向传播的翻转值,然后传给下游。

买苹果例子的正向和反向传播代码实现如下

apple = 100
apple_num = 2
tax = 1.1

mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)

# backward
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dTax:", dtax)

执行结果和上图黑色加粗箭头表示的数字一致 

 price: 220
dApple: 2.2
dApple_num: 110
dTax: 200

加法层的实现

以 z = x + y 为例,backward()将上游传来的导数(dout)原封不动地传递给下游

class AddLayer:
    def __init__(self):
        pass

    def forward(self, x, y):
        out = x + y

        return out

    def backward(self, dout):
        dx = dout * 1
        dy = dout * 1

        return dx, dy

python实现上面5-17的计算图,这里有3个乘法1个加法对应3个乘法层和1个加法层的实例。

虽然代码比较长,但是逻辑简单,按顺序传入参数。

apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1

# layer
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)  # (1)
orange_price = mul_orange_layer.forward(orange, orange_num)  # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price)  # (3)
price = mul_tax_layer.forward(all_price, tax)  # (4)

# backward
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice)  # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)  # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)  # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)  # (1)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dOrange:", dorange)
print("dOrange_num:", int(dorange_num))
print("dTax:", dtax)

计算的结果如下,和上图黑色加粗箭头表示的数字一致 

price: 715
dApple: 2.2
dApple_num: 110
dOrange: 3.3000000000000003
dOrange_num: 165
dTax: 650

激活函数层的实现

ReLU层

y是分段函数,这里根据导数定义 求x = 0处的导数 ,因为x = 0处左右两边的导数值不同,所以x = 0处应该不可导。这里不知道原作者是如何考虑的,有同样疑问的小伙伴欢迎留言。

图5.8 如果正向传播时的输入x大于0,则反向传播会将上游的值原封不动地传给下游;如果正向传播时的x小于等于0,则反向传播中传给下游的信号将停在此处。计算图如下

python实现Relu层

class Relu:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0 

        return out

    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout

        return dx

mask是由True/False构成的NumPy数组,它会把正向传播时的输入x的元素中小于等于0的地方保存为True,其他地方(大于0的元素)保存为False。

out = x.copy()
out[mask] = 0
print(out)

out数组将mask数组中值为True的(x <= 0)元素设置为0 

[[1. 0.]
 [0. 3.]]

Sigmoid层

图5-19的计算图的反向传播

 

Sigmoid层的计算图进一步简化为下图

Sigmoid层代码实现: 

class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = sigmoid(x)
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out

        return dx

Affine/Softmax层的实现

前情回顾:使用Numpy的np.dot()实现矩阵的乘法

这里,X、W、B 分别是形状为(2,)、(2, 3)、(3,)的多维数组。这样一 来,神经元的加权和可以用Y = np.dot(X, W) + B计算出来。然后,Y 经过激活函数转换后,传递给下一层。这就是神经网络正向传播的流程。

注意:X和W 的乘积必须使对应维度的元素个数一致

Affine层

神经网络的正向传播中进行的矩阵的乘积运算在几何学领域被称为“仿射变换”。因此,这里将进行仿射变换的处理实现为“Affine层”

注意X、W、B是矩阵(多维数组),各个结点之间传播的是矩阵

推导后得到下面两个式子

📢注意:矩阵的乘积运算要求对应维度的元素个数保持一致,所以要注意矩阵的形状(维数)

批版本的Affine层的计算图

加上偏置时,需要特别注意。正向传播时,偏置被加到X·W的各个数据上。比如,N = 2(数据为2个)时,偏置会被分别加到这2个数据(各自的计算结果)上,具体的例子如下所示 

正向传播时,偏置会被加到每一个数据(第1个、第2个……)上。因此, 反向传播时各个数据的反向传播的值需要汇总为偏置的元素。用代码表示的话,如下所示。

这里使用了np.sum()对第0轴(以数据为单位的轴,axis=0)方向上的元素进行求和, 每一列元素相加

Affine层代码实现

class Affine:
    def __init__(self, W, b):
        self.W =W
        self.b = b
    
        self.x = None
        # 权重和偏置参数的导数
        self.dW = None
        self.db = None

    def forward(self, x):
        self.x = x
        out = np.dot(self.x, self.W) + self.b

        return out

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        
        return dx

Softmax-with-Loss 层

Softmax函数和交叉熵误差,这个计算图最好自己推导一下(看着复杂,推导完就很清晰也就知道怎么来的了,主要是加法和乘法的反向传播以及链式求导。都是前面的知识,动手推一下记忆更深刻)

简易版如下 

注意的是反向传播的结果。Softmax层的反向传播得到了 (y1 − t1, y2 − t2, y3 − t3)这样“漂亮”的结果。

Softmax-with-Loss层的实现

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None #损失
        self.y = None # softmax的输出
        self.t = None # 监督数据

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        
        return self.loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        
        return dx

总结:目前介绍了两种求梯度的方法

1、基于数值微分的方法(耗费时间)

2、误差反向传播法,即使存在大量的参数,也可以高效地计算梯度。

以后都使用误差反向传播法求梯度。

个人体会:这一章的计算图最好自己推导一下就明白了,总的来说难度不大,都是基础的高数求导知识(复合函数 链式求导法则、常见的求导公式)。计算图搞明白了,对应的代码实现就比较简单了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值