【深度学习入门:基于python的理论与实现】第四章神经网络的学习笔记

前言

这个笔记可以用作复习《深度学习入门:基于python的理论与实现》,笔记内容大概包含了所有重点知识,但并不完善。

第四章神经网络的学习

4.1 从数据中学习

神经网络的学习和机器学习不同,它不需要人为设置特征量,它可以将数据直接作为原始数据,进行“端对端”的学习,因此他对所有问题都可以用相同的流程来解决。
在这里插入图片描述

训练数据、测试数据、泛化能力和过拟合:

  • 在机器学习中,数据分为训练数据测试数据以正确评价模型的泛化能力(处理未被观察过的数据的能力)。

  • 只对某个数据集过度拟合的状态称为过拟合,这会导致模型可以顺利的处理某个数据集,但无法处理其他数据集。


4.2 损失函数

神经网络的学习中所用的指标称为损失函数。损失函数可以使用任意函数,但一般使用均方误差和交叉熵误差等。

4.2.1 均方误差

均方误差的公式为:

M S E = 1 2 ∑ k = 1 n ( y k − t k ) 2 MSE = \frac{1}{2} \sum_{k=1}^{n} (y_k - {t}_k)^2 MSE=21k=1n(yktk)2

这里, y k y_k yk是表示神经网络的输出, t k t_k tk表示监督数据,k表示数据的维数。

代码实现:

def mean_squared_error(y, t):
    return 0.5 * np.sum((y-t)**2)

两个例子:

>>> # 设“2”为正确解
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
>>>
>>> # 例1:“2”的概率最高的情况(0.6)
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> mean_squared_error(np.array(y), np.array(t))
0.097500000000000031
>>>
>>> # 例2:“7”的概率最高的情况(0.6)
>>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
>>> mean_squared_error(np.array(y), np.array(t))
0.59750000000000003

均方误差方法常用于one-hot表示,并且可以发现均方误差越小代表输出与实际结果越吻合。

交叉熵误差

L ( y , t ) = − ∑ i = 1 k t i log ⁡ ( y i ) L(y, t) = - \sum_{i=1}^{k} t_i \log(y_i) L(y,t)=i=1ktilog(yi)

这里,log表示以e为底的自然对数, y k y_k yk是神经网络的输出, t k t_k tk是正确解标签(one-hot表示)。可以发现交叉熵其实就是计算正确的输出y的自然对数值。

python代码实现:

def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y + delta))

这里在计算np.log时加上了一个微笑值delta,为了防止log(0)的出现。

两个例子:

 >>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
 >>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
 >>> cross_entropy_error(np.array(y), np.array(t))
 0.51082545709933802
 >>>
 >>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
 >>> cross_entropy_error(np.array(y), np.array(t))
 2.3025840929945458

可以发现依然遵守误差值越大表示与实际结果相差越大。

4.2.3 mini-batch学习

机器学习其实就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,
计算损失函数时必须将所有的训练数据作为对象。因此如果要求计算所有训练数据损失函数的总和,以交叉熵误差为例,可以写成下式:

E = − 1 N ∑ n ∑ k t n k log ⁡ ( y n k ) E = -\frac{1}{N}\sum_{n} \sum_{k} t_{nk} \log(y_{nk}) E=N1nktnklog(ynk)

式子和上面的交叉熵误差没有什么区别只是将单个数据的损失函数扩大到了N份数据,最后除以N进行正规化。

但是对于大量的数据,以全部数据为对象学习并不现实,因此我们从全部数据中选出一部分,,作为全部数据的“近似”进行学习,这种学习方式称为mini-batch学习

数据导入部分python代码实现:

 import sys, os
 sys.path.append(os.pardir)
 import numpy as np
 from dataset.mnist import load_mnist
 (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
 print(x_train.shape) # (60000, 784)
 print(t_train.shape) # (60000, 10)
 train_size = x_train.shape[0] # 60000
 batch_size = 10
 batch_mask = np.random.choice(train_size, batch_size)
 x_batch = x_train[batch_mask]
 t_batch = t_train[batch_mask]

使用np.random.choice()可以从指定的数字中随机选择想要的数字。比如,np.random.choice(60000, 10)会从0到59999之间随机选择10个数字。

mini-batch版交叉熵误差实现

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size # np.sum会计算所有元素

y的维度为1时,即求单个数据的交叉熵误差时,需要改变数据的形状,否则y.shape[0]会出现错误。

顺便一提,当监督数据是标签形式(“2”,“7”这样的标签)交叉熵误差可以通过如下代码实现。

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

np.arange (batch_size)会生成一个从0到batch_size-1的数组。比如当batch_size为5时,np.arange(batch_size)会生成一个NumPy 数组[0, 1, 2, 3, 4]。

因为t中标签是以[2, 7, 0, 9, 4]的形式存储的,所以y[np.arange(batch_size), t]能抽出各个数据的正确解标签对应的神经网络的输出(在这个例子中,y[np.arange(batch_size), t] 会生成NumPy数组[y[0,2], y[1,7], y[2,0], y[3,9], y[4,4]])。

还记得之前讲过交叉熵函数其实就是计算正确输出的自然对数值。


4.3 数值微分

导数

导数的定义不多介绍,但在python中的实现需要注意。它使用如下公式:

lim ⁡ h → 0 f ( x + h ) − f ( x − h ) 2 h \lim_{h \to 0}\frac {f(x+h) - f(x-h)}{2h} h0lim2hf(x+h)f(xh)

python的代码实现:

def numerical_diff(f, x):
    h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)

python中函数的实现例子:

def f(x):
    return 0.01*x**2 + 0.1*x

偏导数

我们直接来看一个例子:

f ( x 0 , x 1 ) = x 0 2 + x 1 2 f(x_0,x_1) = x_0^2 + x_1^2 f(x0,x1)=x02+x12

def function_2(x):
    return x[0]**2 + x[1]**2  

以下是两个求 ∂ f ∂ x 0 \frac{\partial f}{\partial x_0} x0f, ∂ f ∂ x 1 \frac{\partial f}{\partial x_1} x1f的例子:

问题1:求x0=3,x1=4时,关于x0的偏导数
>>> def function_tmp1(x0):
    ...     
        return x0*x0 + 4.0**2.0
    ...
>>> numerical_diff(function_tmp1, 3.0)
6.00000000000378
问题2:求x0=3,x1=4时,关于x1的偏导数
>>> def function_tmp2(x1):
    ...     
        return 3.0**2.0 + x1*x1
    ...
>>> numerical_diff(function_tmp2, 4.0)
7.999999999999119

4.4 梯度

( ∂ f ∂ x 0 , ∂ f ∂ x 1 ) (\frac{\partial f}{\partial x_0},\frac{\partial f}{\partial x_1}) (x0f,x1f)这样的由全部变量的偏导数汇总而成的向量称为梯度(gradient)。

求梯度的python代码实现:

 def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x) # 生成和x形状相同的数组
    for idx in range(x.size):
        tmp_val = x[idx]
        # f(x+h) 的计算
        x[idx] = tmp_val + h
        fxh1 = f(x)
        # f(x-h) 的计算
        x[idx] = tmp_val - h
        fxh2 = f(x)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val # 还原值
    return grad

下面是一个函数梯度的例子。
在这里插入图片描述

可以发现梯度指示的方向是各点处的函数值减小最多的方向

梯度法

神经网络要在学习时找到最优参数,这里的最优参数是损失函数取最小值的参数。通过巧妙地使用梯度来寻找函数最小值(尽可能小的值)的方法就是梯度法

但是需要注意:在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。

在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进。像这样,通过不断地沿梯度方向前进,逐渐减小函数值的过程就是梯度法(gradient method),也叫梯度下降法

梯度下降法的更新公式为:

θ = θ − α ∇ J ( θ ) \theta = \theta - \alpha \nabla J(\theta) θ=θαJ(θ)

其中:

  • θ \theta θ 是模型的参数
  • α \alpha α 是学习率,学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数
  • ∇ J ( θ ) \nabla J(\theta) J(θ) 是损失函数 J J J 对参数 θ \theta θ 的梯度

学习率需要事先确定为某个值,比如0.01或0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。

梯度下降法的python实现:

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad
    return x

参数f是要进行最优化的函数,init_x是初始值,lr是学习率learning rate,step_num是梯度法的重复次数。

神经网络的梯度

神经网络的梯度即,损失函数L对于权重W的偏导(将输入X作为常数),如下所示:

在这里插入图片描述

神经网络的梯度的python代码:

dW = numerical_gradient(loss(x,t), net.W)  # 即求损失函数对W的梯度

4.5 学习算法的具体实现

神经网络学习的步骤:

  1. 取出mini-batch

  2. 计算梯度

  3. 更新参数

  4. 重复1,2,3

因为这里使用的数据是随机选择的mini batch数据,所有又称为随机梯度下降法(SGD)

下面是代码的具体实现:

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):
        # 初始化权重
        self.params = {
    
    }
        # np.random.randn(x0, x1)代码会生成一个x0 * x1的二维数组,且数据在0~1之间
        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)
    def predict(self, x):
        # 推理由x得到y
        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
    # x: 输入数据, t:监督数据
    def loss(self, x, t):
        # 损失函数
        y = self.predict(x)
        return cross_entropy_error(y, t)
    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的实现

 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
 train_size = x_train.shape[0]
 batch_size = 100
 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)
    # 更新参数
    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)

基于测试数据的评价

神经网络学习的目标是掌握泛化能力,必须使用不包含在训练数据中的数据。下面的代码在进行学习的过程中,每经过一个epoch都会对训练数据和测试数据记录识别精度。

  • epoch是一个单位。一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于10000笔训练数据,用大小为100笔数据的mini-batch进行学习时,重复随机梯度下降法100次,所有的训练数据就都被“看过”了(其实不一定)。此时,100次就是一个epoch

加上评价后的python的代码实现:

 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 = []
 train_acc_list = []
 test_acc_list = []
 # 平均每个epoch的重复次数
 iter_per_epoch = max(train_size / batch_size, 1)
 # 超参数
 iters_num = 10000
 batch_size = 100
 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)
    # 更新参数
    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)
 # 计算每个epoch的识别精度
 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 | " + str(train_acc) + ", " + str(test_acc))

为了节约计算时间,所有要经过一个epoch才检查一次,而不是次次都检查。
上面的代码用图表表示如下:
在这里插入图片描述

如图所示,随着epoch的前进(学习的进行),我们发现使用训练数据和测试数据评价的识别精度都提高了,并且,这两个识别精度基本上没有差异(两条线基本重叠在一起)。因此,可以说这次的学习中没有发生过拟合的现象。