深度学习入门学习笔记(四)(下)

深度学习入门学习笔记(四)(下)

一。 梯度
1.梯度法

下面我们用数学式来表示梯度法。
x 0    =    x 0    −    η ∂ f ∂ x 0 x_0\;=\;x_0\;-\;\eta\frac{\partial f}{\partial x_0} x0=x0ηx0f
x 1    =    x 1    −    η ∂ f ∂ x 1 x_1\;=\;x_1\;-\;\eta\frac{\partial f}{\partial x_1} x1=x1ηx1f
η表示更新量,在神经网络的学习中,称为学习率(learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。
上式表示更新一次的式子,这个步骤会反复执行。也就是说,每次都如此更新变量的值,通过反复执行此步骤,逐渐减小损失函数的值。虽然这里只展示了有两个变量时的更新过程,但是即便增加变量的数量,也可以通过对该值的偏导进行更新。
学习率需要事先确定为某个值,比如0.01或者0.001。一般而言,这个值过大或者过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。
下面是梯度下降法的实现:

def gradient_decent(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是梯度法的重复次数。numerical_gradient(f, x)会求函数的梯度,用该梯度乘以学习率得到的值进行更新操作,由step_num指定重复的次数。
使用这个函数就可以求函数的极小值,顺利的话,还可以求函数的最小值。下面,我们就来尝试解决下面这个问题。

在这里插入图片描述

这里,设初始值为(-3.0, 4.0),开始使用梯度法寻找最小值。最终的结果是(-6.1e-10, 8.1e-10),非常接近(0,0)。实际上,真的最小值就是(0,0),所以说通过梯度法我们基本得到了正确结果。如果用图来表示梯度法的更新过程,则如下图所示。可以发现,原点处是最低的地方,函数的取值一点点在向其靠近。
在这里插入图片描述

此图的源代码为:

import numpy as np
import matplotlib.pylab as plt
from gradient_2d import numerical_gradient

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append( x.copy() )

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)
#       (1, 2)  (2, 20)

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

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

lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)
print(x_history.shape) # x_history的维度为[20,2]
plt.plot( [-5, 5], [0, 0], '--b') # '--b'蓝色破折线
plt.plot( [0, 0], [-5, 5], '--b')
plt.plot(x_history[:0], x_history[:1], 'o') # 'o' 画圆圈

plt.xlim(-3.5, 3.5) # 横坐标范围
plt.ylim(-4.5, 4.5) # 纵坐标范围
plt.xlabel("x0")    # 横坐标标记为x0
plt.ylabel("x1")    # 纵坐标标记为x1
plt.show()
print(x_history.shape)
print(x_history[:,1].shape)
# x_history的维度为step_num行2列 即[20, 2]
# x_history[:,0].shape = (20,) 取第1列20个元素,即x0
# x_history[:,1].shape = (20,) 取第2列20个元素,即x1

另外,学习率过大,会发散成一个很大的值。如果学习率过小,基本上没怎么更新就结束了。也就是找到适合的学习率是个重要的问题。学习率这样的参数称为超参数。超参数是人工设定的,需要尝试多个值,来找到一种可以使得学习顺利进行的设定。
比如下面两个例子:

# 学习率过大的例子:lr=10.0 
>>> init_x = np.array([-3.0, 4.0]) 
>>> gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100) 
array([ -2.58983747e+13,  -1.29524862e+12])

# 学习率过小的例子:lr=1e-10 
>>> init_x = np.array([-3.0, 4.0]) 
>>> gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100) 
array([-2.99999994,  3.99999992])

2.神经网络的梯度

神经网络的学习也要求梯度。这里所说的梯度是指损失函数关于权重参数的梯度。比如,由一个只有一个形状为2×3的权重W的神经网络,损失函数用L表示。此时,梯度可以用
∂ L ∂ W \frac{\partial L}{\partial W} WL
表示。用数学式的话,如下表示:
W    =    ( w 11 w 12 w 13 w 21 w 22 w 23 ) W\;=\;\begin{pmatrix}w_{11}&w_{12}&w_{13}\\w_{21}&w_{22}&w_{23}\end{pmatrix} W=(w11w21w12w22w13w23)

∂ L ∂ W    =    ( ∂ L ∂ w 11 ∂ L ∂ w 12 ∂ L ∂ w 13 ∂ L ∂ w 21 ∂ L ∂ w 22 ∂ L ∂ w 23 ) \frac{\partial L}{\partial W}\;=\;\begin{pmatrix}\frac{\partial L}{\partial w_{11}}&\frac{\partial L}{\partial w_{12}}&\frac{\partial L}{\partial w_{13}}\\\frac{\partial L}{\partial w_{21}}&\frac{\partial L}{\partial w_{22}}&\frac{\partial L}{\partial w_{23}}\end{pmatrix} WL=(w11Lw21Lw12Lw22Lw13Lw23L)

∂ L ∂ W \frac{\partial L}{\partial W} WL
的元素由各个元素关于W的偏导数构成。比如,第一行第一列的元素表示当w11稍微变化时,损失函数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): # predict方法用于得到预测值
        return np.dot(x, self.W)

    def loss(self, x, t): # 求损失函数值
        z = self.predict(x)
        y = softmax(z) # softmax
        loss = cross_entropy_error(y, t) # y为预测值 t为标签

        return loss
    

这里使用了common/functions.py中的softmax和cross_entropy_error方法,以及common/gradient.py中的numerical_gradient方法。simpleNet类只有一个实例变量,即形状为2×3的权重系数。他有两个方法,一个是用于预测的predict(x),另一个是用于求损失函数值得loss(x, t)。这里参数x接收输入数据,t接收正确解标签。现在来试着用一下这个simpleNet。

>>> net = simpleNet()
>>> print(self.W) # 权重参数
[[ 0.13544053  1.19424436  0.61833916]
 [-0.00295785 -0.39919273  0.78907848]]
>>> 
>>> x = np.array([0.6, 0.9])
>>> p = net.predict(x)
>>> print(p)
[0.07860225 0.35727316 1.08117413]
>>> np.argmax(p) # 最大值得索引
2
>>> 
>>> t = np.array([0, 0, 1]) # 正确解标签
>>> net.loss(x, t)
0.6161534496048704

由于以上得权重值是随机得,故结果很可能不一致。
接下来求梯度。和前面一样,我们使用numerical_gradient(f, x)即数值微分来求梯度(这里定义得函数f(W)的参数W是一个伪参数。因为numerical_gradient(f, x)会在内部执行f(x), 为了与之兼容定义了f(W))。

>>> def f(W):
···     return net.loss(x, t)
···
>>> dW = numercial_gradient(f, net.W)
>>> print(dW)
[[ 0.11889062  0.15709879 -0.27598942]
 [ 0.17833594  0.23564819 -0.41398413]]

numerical_gradient(f, x) 的参数f是函数,x是传给函数f的参数。因此,这里参数x取net.W,并定义一个计算损失函数的新函数f,然后把这个新定义的函数传递给numerical_gradient(f, x)。
numerical_gradient(f, net.W)的结果是dW,一个形状为2×3的二维数组。观察一下dW的内容。例如,dW中对W11的偏导数的值大约是0.1,这表示如果将W11增加h,那么损失函数的值会增加0.1h。反之,偏导结果为负数代表会减小对应的倍数,偏导的绝对值代表贡献大小。
另外,在上面的代码中,定义新函数时使用了“def f(x):···”的形式。实际上,Python中如果定义的是简单的函数,可以使用lambda表示法。使用lambda的情况下,上述代码可以如下实现。

>>> f = lambda w : net.loss(x, t)
>>> dW = numerical_gradient(f, net.W)
二. 学习算法的实现

神经网络的学习步骤为:
前提
神经网络存在合适的权重和偏置,调整权重(weight)和偏置(bias)以便拟合训练数据的过程称为“学习”。神经网络的学习分为下面4个步骤。

  • 步骤1(mini-batch)

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

  • 步骤2(计算梯度)

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

  • 步骤3(更新参数)

    将权重参数沿梯度方向进行微小更新。

  • 步骤4(重复)

    重复步骤1、步骤2、步骤3。

神经网络的学习按照上面4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的mini-batch数据,所以又称为随机梯度下降法(stochastic gradient descent, SGD)。深度学习的很多框架中,随机梯度下降法一般由名为SGD的函数来实现。SGD来源于随机梯度下降法的英文名称的首字母。
下面来实现手写数字识别的神经网络。这里以2层神经网络(隐藏层为一层的网络)为对象,使用MNIST数据集进行学习。

1. 2层神经网络的类

首先,我们将这个2层神经网络实现为一个名为TwoLayerNet的类,实现过程如下所示。

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 = {}  # 空字典存放可训练参数
        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为图像数据
        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

我们把这个类中用到的变量和方法整理以下:
在这里插入图片描述

在这里插入图片描述

TwoLayerNet类中有params和grads两个字典型实例变量。params变量中保存了权重参数,比如params[‘W1’]以NumPy数组的形式保存了第1层的权重参数。此外,第1层的偏置可以通过param[‘b1’]进行访问。这里来看一个例子。

net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10)
net.params['W1'].shape # (784, 100)
net.params['b1'].shape # (100,)
net.params['W2'].shape # (100, 10)
net.params['b2'].shape # (10,)

如上所示,params变量中保存了该神经网络所需的全部参数。并且,params变量中保存的权重参数会用在推理处理(前向处理)中。推理的实现如下所示。

x = np.random.randn(100, 784) # 为输入数据(100笔,即100张图片)
y = net.predict(x)

此外,与params变量对应,grads变量中保存了各个参数的梯度。如下所示,使用
numerical_gradient()方法计算出梯度之后,梯度的信息将保存在grads字典型变量中。

x = np.random.randn(100, 784)
t = np.random.randn(100, 10)

grads = net.numerical_gradient(x, t) # 计算梯度
grads['W1'].shape  # (784, 100)
grads['b1'].shape  # (100,)
grads['W2'].shape  # (100, 10)
grads['b2'].shape  # (10,)

接着,我们看一下TwoLayerNet方法的实现。首先是__init__(self, input_size, hidden_size, output_size)方法,它是类的初始化方法(所谓初始化方法,就是生成TwoLayerNet实例时被调用的方法。从第一个参数开始,依次表示输入层的神经元数、隐藏层的神经元数、输出层的神经元数。另外,因为机型手写数字识别时,输入图像的大小为784(28×28,通道数为1的灰度图像),输出为10个类别,所以指定参数input_size=784、output_size=10,将隐藏层的个数hidden_size设置为一个合适的值即可。(超参数)。
此外,这个初始化方法会对权重参数进行初始化。如何设置权重参数的初始值这个问题时关系到神经网络能否成功学习的重要问题。后面会详细讨论权重参数的初始化,这里只需要知道,权重使用符合高斯分布的随机数进行初始化(np.random.randn),偏置使用0进行初始化。另外,loss(self, x, t)是计算损失函数值得方法。这个方法会基于predict()得结果和正确解标签,计算交叉熵误差。
剩下得numerical_gradient(self, x, t)方法会计算各个参数的梯度。根据前面讲到的数值微分,计算各个参数对于损失函数得梯度。另外,gradient(self, x, t)是下一部分要实现的方法,该方法使用误差反向传播法高效地计算梯度。

用误差反向传播法求到的梯度和数值微分的结果基本一致,但可以高速地进行处理。

2. mini-batch的实现

神经网络的学习的实现使用的是前面介绍过的mini-batch学习。所谓mini-batch学习,就是从训练数据中随机选择一部分数据(称为mini-batch),再以这些mini-batch为对象,使用梯度法更新参数的过程。下面,我们就以TwoLayerNet类为对象,使用MNIST数据集进行学习。

import numpy as np
from dataset.minist 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)

# 超参数
iters_num = 10000  # 迭代次数为10000
train_size = x_train.shape[0]  # 训练集大小,即图片张树
batch_size = 100  # mini-batch大小为100(超参数)
learning_rate = 0.1  # 学习率

network = TwoLayerNet(input_size=784, hidden_size, output_size=10)

for i in range(iters_num):
    # 获取mini-batch数量的训练集
    batch_mask = np.randon.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)

这里,mini-batch的大小为100,需要每次从60000个训练数据集中随机选出100个数据。然后,对这100个数据的mini-batch求梯度,使用随机梯度下降法(SGD)更新参数。这里,梯度法的更新次数为10000.每更新一次,都对训练数据计算损失函数的值,并把该值添加到数组中。用图像来表示这个损失函数的值的推移。如下图所示:
在这里插入图片描述

3. 基于测试数据的评价

根据上图呈现的结果,我们确认了通过反复学习可以使损失函数的值逐渐减小这一事实。不过这个损失函数的值,严格的讲是对“训练数据的某个mini-batch的损失函数”的值。训练数据的损失函数值逐渐减小,虽说是神经网络的学习正常进行的一个信号,但光看这个结果还不能说明该神经网络在其他数据集上也一定有同等良好的表现。
神经网络的学习中,必须确认能否正确识别训练数据以外的其他数据,即确认是否会发生过拟合现象。过拟合是指,虽然训练数据的数字图像能够被正确辨别,但是不再训练数据中的数字图像却无法被识别的现象。
神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包括在训练数据中的数据。下面的代码在进行学习的过程中,会定期的对训练数据和测试数据记录识别精度。这里,每经过一个epoch,我们都会记录下来训练数据和测试数据的识别精度。

epoch是一个单位。一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。

import sys, os
sys.path.append(os.pardir)  # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
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)
# 标签使用one-hot形式编码  训练集使用归一化到0.0至1.0之间

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)  # 输入各层的特征值个数

# 超参数
iters_num = 10000  # 适当设定循环次数
train_size = x_train.shaepe[0]  # train_size = 60000
batch_size = 100  # mini-batch = 100
learining_rate = 0.1  # 学习率 = 0.1

train_loss_list = []  # 创建包含训练集损失函数的列表
train_acc_list = []   # 创建训练集精度的列表
test_acc_list = []    # 创建小儿食积精度的列表

# 平均每个epoch的重复次数
iter_per_epoch = max(train_size / batch_size, 1)  # iter_per_epoch = 600

for i in range(iter_num):
    # 随机选取mini-batch
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 利用batch来进行随机梯度下降
    # 计算梯度
    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)
    
    # 计算每个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))
  
# 绘制图像
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel('epochs')  # 横坐标名称
plt.ylabel('accuracy')  # 纵坐标名称
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()


# np.arange()返回一个有终点和起点的固定步长的排列
# 一个参数 默认起点0 步长为1 输出: [0 1 2]
# a = np.arange(3)
#两个参数 默认步长为1 输出[3 4 5 6 7 8]
# a = np.arange(3, 9)
#三个参数 起点为0,终点为3,步长为0.1
# 输出[ 0.   0.1  0.2  0.3  0.4  0.5  0.6  0.7  0.8  0.9  1.   1.1  1.2  1.3  1.4 1.5  1.6  1.7  1.8  1.9  2.   2.1  2.2  2.3  2.4  2.5  2.6  2.7  2.8  2.9]
# a = np.arange(0, 3, 0.1)

在上面的例子中,每经过一个epoch,就对所有的训练数据和测试数据计算识别精度,并记录结果。之所以要计算每个epoch的识别精度,是因为如果在for语句的循环中一直计算识别精度,会花费太多时间。并且,也没有必要那么频繁地记录识别精度(只要从大方向上大致把握识别精度的推移就可以了)。因此,我们才会每经过一个epoch就记录一次训练数据的识别精度。
得到的结果为:
在这里插入图片描述

上图中,虚线表示训练数据的识别精度,虚线表示测试数据的识别精度。随着epoch的前进(学习的进行),我们发现使用训练数据和测试数据评价的识别精度都提高了,并且,这两个识别精度基本上没有差异。因此,这次的学习过程中没有出现过拟合现象。

小结
  • 机器学习中使用的数据集分为训练数据和测试数据。
  • 神经网络用训练数据进行学习,并用测试数据评鉴学习到的模型的泛化能力。
  • 神经网络的学习以损失函数为目标,更新权重参数,以使损失函数的值减小。
  • 利用某个给定的微小值的差分求导数的过程,称为数值微分。
  • 利用数值微分,可以计算权重参数的梯度。
  • 数值微分虽然花费时间较长,但是实现起来简单。反向误差传播复杂一些,但可以高效计算梯度(接下来会介绍)。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值