深度学习入门(三):神经网络的学习

本文为《深度学习入门 基于Python的理论与实现》的部分读书笔记
代码以及图片均参考此书

损失函数(loss function)

为何要设定损失函数

在进行神经网络的学习时,不能将识别精度作为指标。因为如果以识别精度为指标,则参数的导数在绝大多数地方都会变为0。假设某个神经网络正确识别出了100 笔训练数据中的32 笔,此时识别精度为32%。如果以识别精度为指标,即使稍微改变权重参数的值,识别精度也仍将保持在32%,不会出现变化。也就是说,仅仅微调参数,是无法改善识别精度的。即便识别精度有所改善,它的值也不会像32.0123 . . .%这样连续变化,而是变为33%、34%这样的不连续的、离散的值。而如果把损失函数作为指标,则当前损失函数的值可以表示为0.92543 . . . 这样的值。并且,如果稍微改变一下参数的值,对应的损失函数也会像0.93432 . . . 这样发生连续性的变化。识别精度对微小的参数变化基本上没有什么反应,即便有反应,它的值也是不连续地、突然地变化。作为激活函数的阶跃函数也有同样的情况。出于相同的原因,如果使用阶跃函数作为激活函数,神经网络的学习将无法进行。如图4-4 所示,阶跃函数的导数在绝大多数地方(除了0 以外的地方)均为0。也就是说,如果使用了阶跃函数,那么即便将损失函数作为指标,参数的微小变化也会被阶跃函数抹杀,导致损失函数的值不会产生任何变化。
在这里插入图片描述

均方误差(mean squared error)

在这里插入图片描述

  • yk 是表示神经网络的输出,tk 表示监督数据(one-hot表示),k 表示数据的维数

交叉熵误差(cross entropy error)

在这里插入图片描述

  • tk中只有正确解标签的索引为1,其他均为0(one-hot 表示)。因此,上式实际上只计算对应正确解标签的输出的自然对数,即 E = -logyi ( ti == 1 ) 。该损失函数值尽量小时,yi在(0, 1)的范围内尽量大,即输出正确标签的概率尽量大

mini-batch学习

使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。也就是说,如果训练数据有100个的话,我们就要把这100个损失函数的总和作为学习的指标。如果要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式子:
在这里插入图片描述

  • 通过这样的平均化,可以获得和训练数据的数量无关的统一指标。比如,即便训练数据有1000个或10000个,也可以求得单个数据的平均损失函数。
  • 然而,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch, 小批量),然后对每个mini-batch 进行学习。比如,从60000 个训练数据中随机选择100笔,再用这100笔数据进行学习。这种利用一小部分样本数据来近似地计算整体的学习方式称为mini-batch学习
  • 计算所有训练数据的损失进行梯度下降称为batch gradient descent,取全部训练集中的一部分计算损失进行梯度下降称为mini-batch gradient descent。在进行batch gradient descent时,所有训练数据的损失(loss)一定是单调递减的,而进行mini-batch gradient descent时不能保证每一次更新后所有训练数据的损失都下降,但总体的趋势一定是下降的。

利用np.random.choice实现从训练数据中的随机抽取mini-batch:

train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

下面用代码实现mini-batch版的损失函数:

  • 均方误差(mean squared error)
def mean_squared_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    batch_size = y.shape[0]

    # 转成one-hot标签
    if t.size != y.size:
        tmp = t
        t = np.zeros_like(y)
        t[np.arange(batch_size), tmp.astype('int64')] = 1

    return 0.5 * np.sum((y - t)**2) / batch_size
  • 交叉熵误差(cross entropy error)
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
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

数值微分

  • 中心差分
def numerical_diff(f, x):
	h = 1e-4 # 0.0001
	return (f(x+h) - f(x-h)) / (2*h)
  • 梯度
    在这里插入图片描述
    像上式这样的由全部变量的偏导数汇总而成的向量称为梯度(gradient)
def _numerical_gradient_1d(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    for idx in range(x.size):
        tmp_val = x[idx]

        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 还原值
        
    return grad

def numerical_gradient_2d(f, X):
    if X.ndim == 1:
        return _numerical_gradient_1d(f, X)
    else:
        grad = np.zeros_like(X)
        
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_1d(f, x)
        
        return grad

# 支持任意维数
def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    # np.nditer: numpy迭代器对象
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]

        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 还原值
        it.iternext()   
        
    return grad
  • 梯度指示的方向是各点处的函数值减小最多的方向

梯度法寻找函数的最小值

  • 神经网络必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数取最小值时的参数。通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。这里需要注意的是,梯度表示的是各点处的函数值减小最多的方向。因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。

函数的极小值、最小值以及被称为鞍点(saddle point)的地方,梯度为0。极小值是局部最小值,也就是限定在某个范围内的最小值。鞍点是从某个方向上看是极大值,从另一个方向上看则是极小值的点。虽然梯度法是要寻找梯度为0 的地方,但是那个地方不一定就是最小值(也有可能是极小值或者鞍点)。此外,当函数很复杂且呈扁平状时,学习可能会进入一个(几乎)平坦的地区,陷入被称为“学习高原”的无法前进的停滞期。

  • 虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地减小函数的值。因此,在寻找函数的最小值(或者尽可能小的值)的位置的任务中,要以梯度的信息为线索,决定前进的方向。

  • 在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进.寻找最小值的梯度法称为梯度下降法(gradient descent method)

  • 用数学式来表示梯度法
    在这里插入图片描述
    η 表示更新量,在神经网络的学习中,称为学习率(learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。过大会发散,过小学习速度慢。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。这种人工设定的值称为超参数

def numerical_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

下面测试一下:

if __name__ == '__main__':
    def function_2(x):
        return x[0]**2 + x[1]**2

    init_x = np.array([-3.0, 4.0])    

    lr = 0.1
    step_num = 100
    x = numerical_gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

    print(x)

代码输出:

[-6.11110793e-10  8.14814391e-10]

可以看出非常接近f取最小值时的参数(0, 0)

神经网络的梯度

这里所说的梯度是指损失函数关于权重参数的梯度。
在这里插入图片描述
下面以单层神经网络为例,求出权重参数的梯度

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

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t) # w为伪参数,是为了与之前定义的numerical_gradient兼容
dW = numerical_gradient(f, net.W)

print(dW)

学习算法的实现

  1. 选出mini-batch
  2. 计算梯度
  3. 更新参数
  4. 不断重复上述步骤
  • 下面以一个二层神经网络在mnist数据集上训练为例进行代码实现

神经网络的学习中,必须确认是否能够正确识别训练数据以外的其他数据,即确认是否会发生过拟合。过拟合是指,虽然训练数据中的数字图像能被正确辨别,但是不在训练数据中的数字图像却无法被识别的现象。

下面的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个
epoch,我们都会记录下训练数据和测试数据的识别精度。

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

代码同时通过pickle模块进行参数的存储

import sys
file_path = __file__.replace('\\', '/')
dir_path = file_path[: file_path.rfind('/')] # 当前文件夹的路径
pardir_path = dir_path[: dir_path.rfind('/')]
sys.path.append(pardir_path) # 添加上上级目录到python模块搜索路径

import numpy as np
from func.gradient import numerical_gradient 
from func.activation import sigmoid, softmax, cross_entropy_error, sigmoid_grad
import matplotlib.pyplot as plt

class TwoLayerNet:
    """
    2 Fully Connected layers
    softmax with cross entropy error
    """
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        self.params = {}
        self.params['w1'] = np.random.randn(input_size, hidden_size) * weight_init_std
        self.params['b1'] = np.zeros(hidden_size)
        self.params['w2'] = np.random.randn(hidden_size, output_size) * weight_init_std
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):
        a1 = np.dot(x, self.params['w1']) + self.params['b1']
        z1 = sigmoid(a1)
        a2 = np.dot(z1, self.params['w2']) + self.params['b2']
        y = softmax(a2)

        return y

    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 = y.argmax(axis=1)
        t = t.argmax(axis=1)

        accuracy = np.sum(y == t) / x.shape[0]
        return accuracy

    def numerical_gradient(self, x, t):
        loss = lambda w: self.loss(x, t)

        grads = {}
        grads['w1'] = numerical_gradient(loss, self.params['w1'])
        grads['b1'] = numerical_gradient(loss, self.params['b1'])
        grads['w2'] = numerical_gradient(loss, self.params['w2'])
        grads['b2'] = numerical_gradient(loss, self.params['b2'])

        return grads

    def gradient(self, x, t):
        W1, W2 = self.params['w1'], self.params['w2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['w2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['w1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads


if __name__ == '__main__':
    from dataset.mnist import load_mnist
    import pickle
    import os

    (x_train, t_train),  (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=True)
    
    # hyper parameters
    lr = 0.1
    batch_size = 100
    iters_num = 10000

    # setting
    train_flag = 1 # 进行训练还是预测
    pretrain_flag = 0 # 加载上一次训练的参数
    
    pkl_file_name = dir_path + '/two_layer_net.pkl'
    train_size = x_train.shape[0]
    train_loss_list = []
    train_acc_list = []
    test_acc_list = []
    best_acc = 0

    iter_per_epoch = max(int(train_size / batch_size), 1)

    net = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

    if (pretrain_flag == 1 or train_flag == 0) and os.path.exists(pkl_file_name):
        with open(pkl_file_name, 'rb') as f:
            net.params = pickle.load(f)
            print('params loaded!')

    if train_flag == 1:
        print('start training!')
        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]

            # 计算梯度
            # grads_numerical = net.numerical_gradient(x_batch, t_batch)
            grads = net.gradient(x_batch, t_batch)

            # 更新参数
            for key in ('w1', 'b1', 'w2', 'b2'):
                net.params[key] -= lr * grads[key]
            
            train_loss_list.append(net.loss(x_batch, t_batch))

            # 记录学习过程
            if i % iter_per_epoch == 0:
                train_acc_list.append(net.accuracy(x_train, t_train))
                test_acc_list.append(net.accuracy(x_test, t_test))
                print("train acc, test acc | ", train_acc_list[-1], ", ", test_acc_list[-1])

                if test_acc_list[-1] > best_acc:
                    best_acc = test_acc_list[-1]
                    with open(pkl_file_name, 'wb') as f:
                        pickle.dump(net.params, f)
                        print('net params saved!')

        # 绘制图形
        fig, axis = plt.subplots(1, 1)

        x = np.arange(len(train_acc_list))
        axis.plot(x, train_acc_list, 'r', label='train acc')
        axis.plot(x, test_acc_list, 'g--', label='test acc')
        
        markers = {'train': 'o', 'test': 's'}
        axis.set_xlabel("epochs")
        axis.set_ylabel("accuracy")
        axis.set_ylim(0, 1.0)
        axis.legend(loc='best')
        plt.show()
    else:
        print(net.accuracy(x_train[:], t_train[:]))

因为数值微分速度实在是太慢了,所以先用误差反向传播来进行梯度下降(该内容在下一篇讲),下面是运行一段时间后的代码输出,可以看到在测试集上精度不断上升,说明代码工作正常:

start training!
train acc, test acc |  0.09863333333333334 ,  0.0958
net params saved!
train acc, test acc |  0.78535 ,  0.7914
net params saved!
train acc, test acc |  0.8755833333333334 ,  0.8829
net params saved!
train acc, test acc |  0.8984333333333333 ,  0.902
net params saved!
train acc, test acc |  0.9081333333333333 ,  0.9125
net params saved!
train acc, test acc |  0.9149166666666667 ,  0.9181
net params saved!
train acc, test acc |  0.9202666666666667 ,  0.9222
net params saved!
train acc, test acc |  0.9244 ,  0.9271
net params saved!
train acc, test acc |  0.92815 ,  0.9273
net params saved!
train acc, test acc |  0.9319166666666666 ,  0.9323
net params saved!
train acc, test acc |  0.9342666666666667 ,  0.9351
net params saved!
train acc, test acc |  0.9372833333333334 ,  0.9365
net params saved!
train acc, test acc |  0.9393 ,  0.9386
net params saved!
train acc, test acc |  0.9423333333333334 ,  0.9404
net params saved!
train acc, test acc |  0.94355 ,  0.9424
net params saved!
train acc, test acc |  0.9461166666666667 ,  0.9437
net params saved!
train acc, test acc |  0.9478666666666666 ,  0.9455
net params saved!
  • 可以看出网络现在没有出现过拟合现象
    在这里插入图片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章