误差反向传播的python实现(简单高效计算梯度值)

NN的学习中需要计算权重和偏置参数的梯度,对于梯度的计算,很容易想到数值导数,即前向差分
d x = f ( x + h ) − f ( x ) h dx=\frac{f(x+h)-f(x)}{h} dx=hf(x+h)f(x)

或者改进一点,用中心差分(更接近准确的导数值)
d x = f ( x + h ) − f ( x − h ) 2 h dx=\frac{f(x+h)-f(x-h)}{2h} dx=2hf(x+h)f(xh)
h取一个接近0的数值,如0.0001

但这样计算是比较慢的,尤其是大型网络,参数众多的情况,所以我们用一种更快的方法计算参数的梯度值——反向传播。

反向传播的精髓,本质,核心是链式法则chain rule

正是链式法则的应用简化了梯度计算,而且链式法则和反向传播都特别简单,非常好理解。

由于太简单,本文跳过原理叙述直接写代码,如果想仔细学习BP可以看斋藤康毅的《Deep Learning from Scratch》(很火的红色封面,有一条灰色的鱼,CSDN下载链接,我下载看过,非常清晰)中第五章关于反向传播的讲解,极为透彻!!!用计算图作为示例,一点一点,推导加法器,乘法器,RELU,sigmoid,softmax,除法器,exp运算,log运算等,总之所有的NN中用到的运算模块的反向导数,还推导了softmax-with-cross_entropy_error层的反向导数求解,得到了极为漂亮的结果,也正是这个结果使得梯度的计算如此简单。

NN的每一层都实现为一个类,包括Affine层,Sigmoid层,Relu层,Softmax-with-Loss层(loss用cross-entropy-error),类中定义前向传播和反向传播的计算方法。

1. Relu层

y = { x , x > 0 0 , x ≤ 0 y=\left\{ \begin{aligned} x,x>0\\ 0,x\leq0\\ \end{aligned} \right. y={x,x>00,x0

导数自然就是:
∂ y ∂ x = { 1 , x > 0 0 , x ≤ 0 \frac{\partial y}{\partial x}=\left\{ \begin{aligned} 1,x>0\\ 0,x\leq0\\ \end{aligned} \right. xy={1,x>00,x0
在这里插入图片描述
代码:

# relu层的类
class Relu:
    def __init__(self):
        self.mask = None

    # 前向传播的计算
    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()  # out就等于x
        out[self.mask] = 0

        return out

    # 反向传播的计算
    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout

        return dx

在这里插入图片描述
这个比喻可以说是很入骨了···

2. Sigmoid层

y = 1 1 + e x p ( − x ) y=\frac{1}{1+exp(-x)} y=1+exp(x)1
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
代码:

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

    def forward(self, x):
        out = 1 / 1 + np.exp(-x)
        self.out = out

        return out

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

        return dx

3. Affine层

NN中前向传播需要用矩阵乘法,在每个学习层都会遇到这样的计算:
Y = X . W + B Y=X.W+B Y=X.W+B
运算里有缩放和平移,在几何学上称为仿射变换(affine),所以我们把这个计算专门定义为一层,叫做affine层,这样每一个学习层都可以重复调用这个类。
在这里插入图片描述在这里插入图片描述在这里插入图片描述
上2图对于理解代码的backward()方法非常重要!!!

5-25是单个输入经过affine层的正向和反向计算,但5-27展示的批版本的才是实用价值更大的,加快计算速度,而且不难,只要理解矩阵乘法就行。

黑色反向粗箭头下面写的是导数值,一点一点,依据链式法则从输出传到输入,L是假设的整个NN的最终输出的损失函数,由于affine层只是NN的一个part,所以反向传播由 ∂ L ∂ Y \frac{\partial L}{\partial Y} YL开始:

  • 经加法节点导数不变,只是对偏置求导时,由于示例中偏置是1行3列的,而 X . W X.W X.W是N行3列的,实际上N行都加了一样的偏置,所以对偏置矩阵B(1行3列)的导数是 ∂ L ∂ Y \frac{\partial L}{\partial Y} YL列方向的和;(第0轴是列方向,第1轴是行方向,第2轴是Z方向)
  • 经过乘法节点,翻转相乘,得到对X和W的导数,具体为啥左乘右乘还转置从矩阵乘法的合法性上(维度是否可乘)很好理解。

经过乘法节点,翻转相乘的解释:
z = x y z=xy z=xy
∂ z ∂ x = y \frac{\partial z}{\partial x}=y xz=y
∂ z ∂ y = x \frac{\partial z}{\partial y}=x yz=x
在这里插入图片描述
经过加法节点,导数不变的解释:
z = x + y z=x+y z=x+y
∂ z ∂ x = 1 \frac{\partial z}{\partial x}=1 xz=1
∂ z ∂ y = 1 \frac{\partial z}{\partial y}=1 yz=1
在这里插入图片描述

代码:

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(x, self.w) + self.b
        
        return out
    
    
    def backward(self,dout):
        dx = np.dot(dout, self.w.T) 
        # 权重经过的是乘法器单元,对数据x求导则让输出dout乘以权重
        # 对权重求导则让dout乘以数据x
        # 偏置经过加法器单元,对b求导就等于对dout求导
        self.dw = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        
        return dx 

4. Softmax-with-Loss层

如图,这是一个3层NN(2个隐层)
affine层用于完成每层的加权和偏置计算
隐层用relu激活函数
输出层用softmax激活函数(因为这是分类任务,回归任务用恒等函数作为激活函数)对结果进行正规化(输出的和调整为1),正规化之前的NN输出通常被称为“得分”

在这里插入图片描述

注意:NN中有两个阶段,学习和推理。
softmax层只对NN的学习有用,学完之后投入推理阶段后,这一层是可有可无的,毕竟直接选择得分最大的输出作为分类结果也是可以的,所以推理阶段通常不用softmax层。

分类任务中,通常隐层会使用relu居多(比sigmoid的使用更多),而输出层affine完了后是一定会使用softmax激活函数的,并且一定要搭配交叉熵误差损失函数cross-entropy-error,因为这样搭配起来会使得Softmax-with-Loss层得到最简单漂亮的反向导数:
在这里插入图片描述
这个图看似很复杂,实际并不难,示例的Softmax-with-Loss层(输出层)有3个输出unit, 即这是个三元分类任务,正向的传播很简单,略过。
反向导数计算:
很显然,L是交叉熵损失,最开始的导数是 ∂ L ∂ L = 1 \frac{\partial L}{\partial L}=1 LL=1

  • 经过乘法节点,翻转乘,得-1
  • 经过加法节点,导数不变
  • 经过log节点,导数除以对应的y值
    在这里插入图片描述
  • 后面以此类推,最终得到反向导数就是 y n − t n y_n-t_n yntn的简单结果,非常有利于我们高速计算梯度!而不是像文首那样用数值法逼近,计算量更大。

在这里插入图片描述
代码:

def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a-c) # 防溢出
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a

    return y


def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    # 监督数据是one-hot-vector的情况下,转换为正确解标签的索引
    if t.size == y.size:
        t = t.argmax(axis=1)

    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size


class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None
        self.t = None  # one-hot vector


    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]
        dy = (self.y - self.t) / batch_size
        # 注意反向传播时,将要传播的值除以批的大小(batch_size)后,
        # 传递给前面的层的是单个数据的误差。

        return dy
误差反向传播法是一种用于计算神经网络中参数梯度的方法。它通过将正向传播和反向传播结合起来,通过链式法则来计算每个参数对于整体损失函数的梯度。在误差反向传播法中,首先通过正向传播计算出网络的输出结果,然后根据损失函数计算出网络的损失。接着,通过反向传播将损失从输出层向输入层逐层传递,计算出每个参数对于损失的贡献,最终得到参数的梯度。 在Python实现误差反向传播法,可以使用NumPy库来进行矩阵和向量操作。以下是一个简单的示例代码实现了一个简单的神经网络的误差反向传播过程的代码片段: ```python # 假设网络有两个全连接层和一个输出层 import numpy as np # 正向传播 def forward_propagation(X, parameters): # 假设第一层的激活函数为ReLU Z1 = np.dot(parameters['W1'], X) + parameters['b1'] A1 = np.maximum(0, Z1) # 假设第二层的激活函数为ReLU Z2 = np.dot(parameters['W2'], A1) + parameters['b2'] A2 = np.maximum(0, Z2) # 假设输出层的激活函数为sigmoid Z3 = np.dot(parameters['W3'], A2) + parameters['b3'] A3 = 1 / (1 + np.exp(-Z3)) return A3 # 反向传播 def backward_propagation(X, Y, parameters, cache): m = X.shape[1] # 计算输出层的误差 dZ3 = cache['A3'] - Y # 计算第二层的误差 dW3 = 1 / m * np.dot(dZ3, cache['A2'].T) db3 = 1 / m * np.sum(dZ3, axis=1, keepdims=True) dA2 = np.dot(parameters['W3'].T, dZ3) dZ2 = np.multiply(dA2, np.int64(cache['A2'] > 0)) # 计算第一层的误差 dW2 = 1 / m * np.dot(dZ2, cache['A1'].T) db2 = 1 / m * np.sum(dZ2, axis=1, keepdims=True) dA1 = np.dot(parameters['W2'].T, dZ2) dZ1 = np.multiply(dA1, np.int64(cache['A1'] > 0)) # 计算输入层的误差 dW1 = 1 / m * np.dot(dZ1, X.T) db1 = 1 / m * np.sum(dZ1, axis=1, keepdims=True) gradients = { 'dW1': dW1, 'db1': db1, 'dW2': dW2, 'db2': db2, 'dW3': dW3, 'db3': db3 } return gradients ``` 以上代码片段中的`forward_propagation`函数实现了神经网络的正向传播过程,`backward_propagation`函数实现了神经网络的反向传播过程。其中,`parameters`是神经网络的参数,`cache`是正向传播过程中保存的中间结果,`X`是输入数据,`Y`是标签数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值