深度学习(鱼书)day08--误差反向传播(后三节)

深度学习(鱼书)day08--误差反向传播(后三节)

一、激活函数层的实现

这里,我们把构成神经网络的层实现为一个类。先来实现激活函数的ReLU层和Sigmoid层。

  1. ReLU层

    激活函数ReLU(Rectified Linear Unit)由下式表示。
    y={x(x>0)0(x≤0) y = \begin{cases} x & (x > 0) \\ 0 & (x \leq 0) \end{cases} y={x0(x>0)(x≤0)

    y关于x的导数:
    ∂y∂x={1(x>0)0(x≤0) \frac{\partial y}{\partial x} = \begin{cases} 1 & (x > 0) \\ 0 & (x \leq 0) \end{cases} ∂x∂y={10(x>0)(x≤0)

    python 复制代码
    class ReLU:
        def __init__(self):
            self.mask = None
    
        def forward(self, x):
            self.mask = (x <= 0)
            out = x.copy()
            out[self.mask] = 0
    
        def backward(self, dout):
            dout[self.mask] = 0
            dx = dout
            return dx

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

    如果正向传播时的输入值小于等于0,则反向传播的值为0。因此,反向传播中会使用正向传播时保存的mask,将从上游传来的dout的mask中的元素为True的地方设为0。

    ReLU层的作用就像电路中的开关一样。正向传播时,有电流通过的话,就将开关设为 ON;没有电流通过的话,就将开关设为 OFF。反向传播时,开关为ON的话,电流会直接通过;开关为OFF的话,则不会有电流通过。

  2. Sigmoid层

    正向传播:

反向传播:

步骤1

"/"节点表示

,它的导数可以解析性地表示为下式。

反向传播时,会将上游的值乘以-y 2 (正向传播的输出的平方乘以1后的值 )后,再传给下游。计算图如下所示。

步骤2

"+"节点将上游的值原封不动地传给下游。计算图如下所示。

步骤3

"exp"节点表示y = exp(x ),它的导数由下式表示。

上游的值乘以正向传播时的输出(这个例子中是exp(−x))后,再传给下游。

步骤4

"× "节点将正向传播时的值翻转后做乘法运算。因此,这里要乘以*−*1。

集约化的"sigmoid"节点。可以不用在意Sigmoid层中琐碎的细节,而只需要专注它的输入和输出,这一点也很重要。

进一步整理如下:

因此,Sigmoid层的反向传播,只根据正向传播的输出就能计算出来。

代码实现:

python 复制代码
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 * (1.0 - self.out) * self.out
        return dx

二、Affine/Softmax层的实现

  1. Affine层

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

    现在将这里进行的求矩阵的乘积与偏置的和的运算用计算图表示出来。将乘积运算用"dot"节点表示的话,则np.dot(X, W) + B的运算可用下图所示的计算图表示出来。在各个变量的上方标记了它们的形状(计算图上显示了X 的形状为(2,),X ·W的形状为(3,))。

现在我们来考虑图5-24的计算图的反向传播。以矩阵为对象的反向传播,按矩阵的各个元素进行计算时,步骤和以标量为对象的计算图相同。实际写一下的话,可以得到下式:

尝试写出计算图的反向传播,如图所示:


  1. 批版本的Affine层

    前面介绍的Affi ne层的输入X是以单个数据为对象的。现在我们考虑N个数据一起进行正向传播的情况,也就是批版本的Affine层。先给出批版本的Affine层的计算图,如图所示。

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

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

python 复制代码
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)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        return dx

输入数据为张量(四维数据)的情况:

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

    def forward(self, x):
        # 对应张量
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        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)
        
        dx = dx.reshape(*self.original_x_shape)  # 还原输入数据的形状(对应张量)
        return dx
  1. Softmax-with-Loss 层

    softmax函数会将输入值正规化之后再输出。比如手写数字识别时,Softmax层的输出如图所示。

神经网络中进行的处理有推理(inference)和学习 两个阶段。神经网络的推理通常不使用 Softmax层 。比如,用图 5-28的网络进行推理时,会将最后一个 Affine层的输出 作为识别结果。神经网络中未被正规化的输出结果有时被称为"得分 "。也就是说,当神经网络的推理只需要给出一个答案的情况下,因为此时只对得分最大值感兴趣,所以不需要 Softmax层。不过,神经网络的学习阶段则需要 Softmax层。

这里也包含作为损失函数的交叉熵误差(cross entropy error),所以称为"Softmax-with-Loss层"。Softmax-with-Loss层(Softmax函数和交叉熵误差)的计算图如图所示。


  • 这里假设要进行3类分类,从前面的层接收3个输入(得分)。Softmax 层将输入(a1, a2, a3)正规化,输出(y1, y2, y3)。Cross Entropy Error 层接收Softmax的输出(y1, y2, y3)和教师标签(t1, t2, t3),从这些数据中输出损失L。

  • 注意反向传播的结果 :Softmax层的反向传播得到了(y1 − t1, y2 − t2, y3 − t3)这样"漂亮"的结果。由于(y1, y2, y3)是Softmax层的输出,(t1, t2, t3)是监督数据,所以(y1 − t1, y2 − t2, y3 − t3)是Softmax层的输出和教师标签的差分。神经网络的反向传播会把这个差分表示的误差传递给前面的层,这是神经网络学习中的重要性质

  • 考虑一个具体的例子,比如教师标签是 (0, 1, 0) ,Softmax层的输出是 (0.3, 0.2, 0.5)的情形。因为正确解标签处的概率是0.2(20%),这个时候的神经网络未能进行正确的识别。此时,Softmax层的反向传播传递的是(0.3, −0.8, 0.5)这样一个大的误差 。因为这个大的误差会向前面的层传播,所以Softmax层前面的层会从这个大的误差中学习到"大"的内容。

  • 回归问题中输出层使用"恒等函数",损失函数使用"平方和误差",也是出于同样的理由。也就是说,使用"平方和误差"作为"恒等函数"的损失函数,反向传播才能得到(y1 −t1, y2 − t2, y3 − t3)这样"漂亮"的结果。

  • 再举一个例子,比如思考教师标签是 (0, 1, 0) ,Softmax层的输出是 (0.01, 0.99, 0)的情形(这个神经网络识别得相当准确 )。此时Softmax层的反向传播传递的是 (0.01, −0.01, 0)这样一个小的误差 。这个小的误差也会向前面的层传播,因为误差很小,所以Softmax层前面的层学到的内容也很"小"

Softmax-with-Loss层的实现:

python 复制代码
class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None
        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]
        if self.t.size == self.y.size:  # 监督数据是one-hot-vector的情况
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size

        return dx

注意反向传播时,将要传播的值除以批的大小(batch_size)后,传递给前面的层的是单个数据的误差。

三、误差反向传播法的实现

通过像组装乐高积木一样组装上一节中实现的层,可以构建神经网络。本节我们将通过组装已经实现的层来构建神经网络。

  1. 神经网络学习的全貌图

步骤2中,之前我们利用数值微分求得了这个梯度。数值微分虽然实现简单,但是计算要耗费较多的时间。和需要花费较多时间的数值微分不同,误差反向传播法可以快速高效地计算梯度

  1. 对应误差反向传播法的神经网络的实现

    python 复制代码
    import sys, os
    sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
    import numpy as np
    from common.layers import *
    from common.gradient import numerical_gradient
    from collections import OrderedDict
    
    class TwoLayerNet:
    
        def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
            # 初始化权重
            self.params = {}
            self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
            self.params['b1'] = np.zeros(hidden_size)
            self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) 
            self.params['b2'] = np.zeros(output_size)
    
            # 生成层
            self.layers = OrderedDict()
            self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
            self.layers['Relu1'] = Relu()
            self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
    
            self.lastLayer = SoftmaxWithLoss()
            
        def predict(self, x):
            for layer in self.layers.values():
                x = layer.forward(x)
            
            return x
            
        # x:输入数据, t:监督数据
        def loss(self, x, t):
            y = self.predict(x)
            return self.lastLayer.forward(y, t)
        
        def accuracy(self, x, t):
            y = self.predict(x)
            y = np.argmax(y, axis=1)
            if t.ndim != 1 : t = np.argmax(t, axis=1)
            
            accuracy = np.sum(y == t) / float(x.shape[0])
            return accuracy
            
        # x:输入数据, t:监督数据
        def numerical_gradient(self, x, t):
            loss_W = lambda W: self.loss(x, t)
            
            grads = {}
            grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
            grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
            grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
            grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
            
            return grads
            
        def gradient(self, x, t):
            # forward
            self.loss(x, t)
    
            # backward
            dout = 1
            dout = self.lastLayer.backward(dout)
            
            layers = list(self.layers.values())
            layers.reverse()
            for layer in layers:
                dout = layer.backward(dout)
    
            # 设定
            grads = {}
            grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
            grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
    
            return grads

OrderedDict是有序字典 ,"有序"是指它可以记住向字典里添加元素的顺序。因此,神经网络的正向传播只需按照添加元素的顺序调用各层的forward()方法就可以完成处理,而反向传播只需要按照相反的顺序调用各层即可。因为Affine层和ReLU层的内部会正确处理正向传播和反向传播,所以这里要做的事情仅仅是以正确的顺序连接各层,再按顺序(或者逆序)调用各层

  1. 误差反向传播法的梯度确认

    在确认误差反向传播法的实现是否正确时,是需要用到数值微分的。数值微分的优点是实现简单,一般情况下不太容易出错。而误差反向传播法的实现很复杂,容易出错。所以,经常会比较数值微分的结果和误差反向传播法的结果 ,以确认误差反向传播法的实现是否正确 。确认数值微分求出的梯度结果和误差反向传播法求出的结果是否一致(严格地讲,是非常相近)的操作称为梯度确认(gradient check)。

    python 复制代码
    import sys, os
    sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
    import numpy as np
    from dataset.mnist import load_mnist
    from two_layer_net import TwoLayerNet
    
    # 读入数据
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
    
    network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
    
    x_batch = x_train[:3]
    t_batch = t_train[:3]
    
    grad_numerical = network.numerical_gradient(x_batch, t_batch)
    grad_backprop = network.gradient(x_batch, t_batch)
    
    for key in grad_numerical.keys():
        diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
        print(key + ":" + str(diff))

通过数值微分和误差反向传播法求出的梯度的差非常小。所以误差反向传播法求出的梯度是正确的。

  1. 使用误差反向传播法的学习

    和之前的实现相比,不同之处仅在于通过误差反向传播法求梯度这一点。

    python 复制代码
    # coding: utf-8
    import sys, os
    sys.path.append(os.pardir)
    
    import numpy as np
    from dataset.mnist import load_mnist
    from two_layer_net import TwoLayerNet
    
    # 读入数据
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
    
    network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
    
    iters_num = 10000
    train_size = x_train.shape[0]
    batch_size = 100
    learning_rate = 0.1
    
    train_loss_list = []
    train_acc_list = []
    test_acc_list = []
    
    iter_per_epoch = max(train_size / batch_size, 1)
    
    for i in range(iters_num):
        batch_mask = np.random.choice(train_size, batch_size)
        x_batch = x_train[batch_mask]
        t_batch = t_train[batch_mask]
        
        # 梯度
        #grad = network.numerical_gradient(x_batch, t_batch)
        grad = network.gradient(x_batch, t_batch)
        
        # 更新
        for key in ('W1', 'b1', 'W2', 'b2'):
            network.params[key] -= learning_rate * grad[key]
        
        loss = network.loss(x_batch, t_batch)
        train_loss_list.append(loss)
        
        if i % iter_per_epoch == 0:
            train_acc = network.accuracy(x_train, t_train)
            test_acc = network.accuracy(x_test, t_test)
            train_acc_list.append(train_acc)
            test_acc_list.append(test_acc)
            print(train_acc, test_acc)
相关推荐
风象南10 分钟前
Claude Code这个隐藏技能,让我告别PPT焦虑
人工智能·后端
曲幽33 分钟前
FastAPI压力测试实战:Locust模拟真实用户并发及优化建议
python·fastapi·web·locust·asyncio·test·uvicorn·workers
Mintopia1 小时前
OpenClaw 对软件行业产生的影响
人工智能
陈广亮1 小时前
构建具有长期记忆的 AI Agent:从设计模式到生产实践
人工智能
会写代码的柯基犬2 小时前
DeepSeek vs Kimi vs Qwen —— AI 生成俄罗斯方块代码效果横评
人工智能·llm
Mintopia2 小时前
OpenClaw 是什么?为什么节后热度如此之高?
人工智能
爱可生开源社区2 小时前
DBA 的未来?八位行业先锋的年度圆桌讨论
人工智能·dba
叁两5 小时前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
敏编程5 小时前
一天一个Python库:jsonschema - JSON 数据验证利器
python