作者:云不见 链接:https://blog.csdn.net/Walk_OnTheRoad/article/details/107837003 编辑:王萌 澳门城市大学(深度学习冲鸭公众号)

此笔记是基于《深度学习入门》这本书的重点知识汇总。

接着《深度学习入门:基于Python的理论与实现》学习笔记(1)继续学习笔记的记载。

目录:

梯度 神经网络的梯度 学习算法的实现步骤: mini-batch的实现(为了提高学习的效率!) 利用误差反向传播法求梯度(高速) 加法节点的反向传播 乘法节点的反向传播 log对数的反向传播 指数e的反向传播 激活函数层反向传播的实现 1、Relu层的反向传播 2、sigmoid函数的反向传播 Affine层的反向传播 3、Softmax-with-Loss层的反向传播 误差反向传播法的实现 使用误差反向传播法的神经网络的学习

本文重点知识点:

  • 梯度
    
  • 神经网络计算梯度的实现(利用数值微分)
  • 学习算法的实现步骤
  • 利用误差反向传播法求梯度(更高速)
  • 激活函数层反向传播的实现
    
  • 利用误差反向传播法实现两层神经网络的学习
    

如有细节处没有写到的,请继续精读《深度学习入门:基于Python的理论与实现》,对小白来说真的是非常好的深度学习的入门书籍,通俗易懂。(书中的例子主要是基于CV的)

梯度

为什么要计算梯度?

  • 寻找函数的最小值
    

前文我们已经说过,用损失函数来描述神经网络的好坏。损失函数越小,神经网络越好。

那么如何去寻找这个损失函数的最小值讷?也就是说如何找一个函数的最小值。

  • 通过数值微分计算梯度。梯度下降的地方,就是函数在变小的地方。直到梯度下降为0,这个函数的局部最小值也就找到啦。
    

这种方法就叫梯度下降法(SGD):寻找最小值的梯度法。

数学式表示梯度法

学习率lr (η):梯度下降的速度。需要自己设定,不能太大,不能太小。

Python来实现数值微分计算梯度的梯度下降法:

def gradient_descent(f, init_x, lr=0.01, step_num=100): # step_num为更新次数
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad
    return x

缺点: 但是通过数值微分计算梯度比较费时间,之后介绍的误差反向传播法会更高效。

神经网络的梯度

即求损失函数L关于权重参数的梯度

损失函数L关于权重参数w的数值微分法计算梯度

以下是利用数值微分求梯度,实现简单神经网络的代码:

import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient
class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3) # 用高斯分布进行初始化(正态分布)
    def predict(self, x):
        return np.dot(x, self.W)
    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)
        return loss

学习算法的实现步骤

也就是梯度下降法逐渐更新函数的过程

  • 前提
    
    神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为“学习”。

神经网络的学习分成下面4个步骤。

  • 步骤1(mini-batch)
    

    从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们的目标是减小mini-batch的损失函数的值。

  • 步骤2(计算梯度) 为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。

  • 步骤3(更新参数) 将权重参数沿梯度方向进行微小更新。

  • 步骤4(重复) 重复步骤1、步骤2、步骤3。

2层神经网络的实现代码:

import sys, os
sys.path.append(os.pardir)
from common.functions import *
from common.gradient import numerical_gradient
class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size,
                 weight_init_std=0.01):
        # 1、初始化权重
        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)
        
    # 2、神经网络的推理过程(已知权重参数,进行前向传播)
    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        return y
        
    # 3、损失函数的选择
    # x:输入数据, t:监督数据
    def loss(self, x, t):
        y = self.predict(x)
        return cross_entropy_error(y, t)
        
    # 4、网络的识别精度,判断网络的精确度
    def accuracy(self, x, t):  # 
        y = self.predict(x)
        y = np.argmax(y, axis=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

mini-batch的实现(为了提高学习的效率!)


就是从训练数据中随机选择一部分数据(称为mini-batch),再以这些mini-batch为对象,使用梯度法更新参数的过程。

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_
laobel = True)
train_loss_list = []

# 超参数
iters_num = 10000  # 使用随机梯度下降法(SGD)更新参数、循环10000次
train_size = x_train.shape[0]
batch_size = 100  # 随机选择100笔mini-batch数据
learning_rate = 0.1

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
for i in range(iters_num):
    # 获取mini-batch
    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)

记录损失函数不断拟合的过程

通过数值微分计算梯度比较费时间,误差反向传播法更高效。

利用误差反向传播法求梯度(高速)


计算图:将计算过程用图形表示出来。就像流程图一样。由节点,输入输出和箭头来表示他们之间的关系。 图 基于计算图来求解买苹果需要多少钱的问题

图中所示就是一个简单的计算图,每个圈圈相当于一个节点,通过加减乘除等运算来表示输入之间的关系;箭头上的数字表示输入的权重。比如输入为“苹果”,价格(权重)为100元一个;还有一条输入是“苹果的个数”,个数(权重)为两个,得到第一个乘法节点的输出为200;以此类推加上消费税输入,经过多个节点计算,得到最终买苹果的价格(输出)。 图 计算图的反向传播

反向传播,顾名思义,由输出传回输入,也就是说已知输出,求出输入。

上图反向传播的计算顺序:

  • 将信号 E × 节点的局部导数(偏导数),再传递给下一节点
    

对于简单层

加法节点的反向传播


加法节点的反向传播原封不动的传给下一个节点。

乘法节点的反向传播


乘法节点的反向传播:反向输入的偏导乘以另一条正向输入的值

log对数的反向传播


指数e的反向传播


激活函数层反向传播的实现

1、Relu层的反向传播


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

总结:ReLU层像电路中的开关

  • 正向传播时,有电流通过的话,开关设为ON;没有电流通过,开关设为OFF。
    
  • 反向传播时,开关为ON,电流会直接通过;开关为OFF,则不会有电流通过
    

2、sigmoid函数的反向传播


最终得到Sigmoid的反向传播:

已知sigmoid的y=1/(1+exp(-x)),带入式子化简一下得到下式,即为sigmoid层的反向传播:

用Python实现反向传播的Sigmoid层

#(实现的代码在 common/layers.py中)
class Sigmoid:
    def __init__(self):
        self.out = None
    def forward(self, x):
        out = 1 / (1 + np.exp(-x))
        self.out = out
        return out  # 正向传播时将输出 y 保存在了实例变量 out中
    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out
        return dx

Affine层的反向传播


Affine仿射变换:y = xw+b。就是神经网络的正向传播中进行的矩阵的乘积运算

  • 包括一次线性变换和一次平移,分别对应神经网络的加权和运算与加偏置运算,将进行该仿射变换的处理实现为“Affine层”
    

其中:W^T的T表示转置。 这里还要注意矩阵的形状,为什么要注意矩阵的形状呢?因为矩阵的乘积运算要求对应维度的元素个数保持一致,所以也就能推导出上式的乘积顺序为什么一个在前,一个在后了。因为只有这样才能实现矩阵乘法,得到正确的矩阵形状。

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(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

3、Softmax-with-Loss层的反向传播


中间的过程推导很长,在书中的附录A中可参考,但是这里我们只需要记住这个看起来特别简单的结论即可。

Softmax-with-Loss层的反向传播代码实现

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None # 损失
        self.y = None    # softmax的输出
        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]
        dx = (self.y - self.t) / batch_size  # 反向传播
        return dx



误差反向传播法的实现

通过像组装乐高积木一样组装每一层,可以最终构建神经网络。

接下来可以通过组装已经实现的层来构建神经网络。

神经网络学习的步骤如下所示(又出现了,这个很重要):

  • 前提
    
    神经网络中有合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为学习。神经网络的学习分为下面4个步骤。
  •  步骤1(mini-batch)
    
    从训练数据中随机选择一部分数据。
  •   步骤2(计算梯度)
    
    计算损失函数关于各个权重参数的梯度。(计算梯度:数值微分法和误差反向传播法)
  •  步骤3(更新参数)
    
    将权重参数沿梯度方向进行微小的更新。
  •  步骤4(重复)
    
    重复步骤1、步骤2、步骤3。

和需要花费较多时间的数值微分不同,误差反向传播法可以快速高效地计算梯度。

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

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'] = \  # w1*x + b1
            Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = \  # w2*x + b2
            Affine(self.params['W2'], self.params['b2'])
        self.lastLayer = SoftmaxWithLoss()  #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'] = self.layers['Affine1'].dW
        grads['b1'] = self.layers['Affine1'].db
        grads['W2'] = self.layers['Affine2'].dW
        grads['b2'] = self.layers['Affine2'].db
        return grads

以上做的的事情仅仅是以正确的顺序连接各层,再按顺序(或者逆序)调用各层。

如果想另外构建一个神经网络(比如5层、10层、20层……的大的神经网络)时,只需像组装乐高积木那样添加必要的层就可以了。

数值微分的实现简单,因此,一般情况下不太容易出错。而误差反向传播法的实现很复杂,容易出错。

如何检查误差反向传播法梯度算对没有?:

  • 可以利用梯度确认(gradient check),确认数值微分求出的梯度结果和误差反向传播法求出的结果是否一致。
    

梯度确认的代码实现如下所示:

# (源代码在 ch05/gradient_check.py中)。
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))

使用误差反向传播法的神经网络的学习

这个学习过程中只是求梯度的方法与之前不同。

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.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)

总结

我们学过的层有 ReLU 层、Softmax-with-Loss 层、Affine 层、Softmax 层等,这些层中实现了 forward和 backward方法,通过将数据正向和反向地传播,可以高效地计算权重参数的梯度。

通过使用层进行模块化,神经网络中可以自由地组装层,轻松构建出自己喜欢的网络。

注:如有细节处没有写到的,请继续精读《深度学习入门》,对小白来说真的是非常通俗易懂的深度学习入门书籍。(书中的例子主要是基于CV的)