上一章介绍了神经网络的概要,重点关注了神经网络在识别时进行的处理
链接
这一章的“学习”是指从训练数据中自动获取最优权重参数的过程,为了能够学习,导入损失函数这一指标。我们学习的目的就是以该损失函数为基准,找出能使它的值达到最小的权重参数。

从数据中学习

神经网络的特征就是可以从数据中学习,指的是由数据自动决定权重参数的值。
之前感知机对照真值表人工设定了3个参数值,而实际的神经网络参数的数量成千上万甚至过亿,想用人工决定参数的值就不可能了。这一章就是介绍神经网络的学习,即利用数据决定参数值的方法。

数据驱动

数据是机器学习的核心。机器学习的方法是极力避免人为介入,尝试从收集到的数据中发现答案。例如设计一个识别5的程序,人可以很简单的识别出,但却很难明确说出是基于何种规律而识别出5。可以通过数据来解决这个问题:

一种方案

先从图像中提取特征量,再用机器学习技术学习这些特征量的模式。这里的特征量是指可以从输入数据中准确提取本质数据的转换器。使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的SVM,KNN等分类器进行学习。

注意点:将图像转换为向量时使用的特征量仍然由人设计。对于不同的问题,必须设计专门的特征量,才能得到好的结果。

如图神经网络直接学习图像本身,第二个路线中特征量仍是由人工设计的。神经网络的优点是对所有的问题都可以用同样的流程来解决,通过不断学习所提供的数据,尝试发现待求解的问题的模式。

训练数据和测试数据

机器学习中,一般将数据分为训练数据测试数据两部分来进行学习和实验等。首先使用训练数据学习,寻找最优的参数,然后使用测试数据评价训练得到的模型的实际能力。之所以这样做就是追求模型的泛化能力,此外训练数据也可以称为监督数据。

获得泛化能力是机器学习的最终目标。仅通过一个数据集去学习和评价参数,会导致对某个数据集过度拟合的过拟合状态。

损失函数

神经网络的学习通过某个指标表示现在的状态,然后以这个指标为基准,寻找最优权重参数。所用的指标称为损失函数(loss function)。这个损失函数可以使用任意函数,一般用均方误差和交叉熵误差

均方误差

可用作损失函数的函数有很多,其中最有名的是均方误差(mean squared error)

y k y_k yk表示神经网络的输出, t k t_k tk表示监督数据, k k k表示数据的维数
均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,再求总和。

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

实际计算

import numpy as np

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

y = [0,0,1,0,0,0,0]
t = [0.1,0.05,0.6,0.0,0.05,0.1,0.0]

print(mean_squared_error(np.array(y),np.array(t)))

交叉熵误差

交叉熵误差(cross entropy error)也经常被用作损失函数
log表示以e为底的自然对数, y k y_k yk是神经网络的输出, t k t_k tk是正确解标签(我个人觉得 l o g y k logy_k logyk写成 l n y k lny_k lnyk更眼熟一点

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

计算np.log时,加上了一个微小值delta,防止负无限大的发生
简单计算:

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

y = [0,0,1,0,0,0,0]
t = [0.1,0.05,0.6,0.0,0.05,0.1,0.0]

print(cross_entropy_error(np.array(y),np.array(t)))

mini-batch学习

机器学习使用训练数据进行学习,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。所以计算损失函数时必须将所有的训练数据作为对象。

之前的例子都是针对单个数据的损失函数,若要所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成以下形式

t n k t_nk tnk表示第n个数据的第k个元素的值,式子看起来有点复杂,其实只是把求单个数据的损失函数的式扩大到了N份数据,不过最后还是要除以N进行正规化。这样就能得到单个数据的“平均损失函数”。通过这样的平均化,可以获得和训练数据的数量无关的统一指标。

另外,MNIST数据集的训练数据有60000个,如果以全部数据为对象求损失函数的和,计算会花费很多时间,遇上几千万以上的数据量更难求。因此需要我们从全部数据中选出一部分,作为全体数据的“近似”。神经网络的学习就是从训练数据中选出一部分数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。例如从60000个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习

读入MNIST数据集的代码:

import sys,os
sys.path.append(os.pardir)
import numpy as np
from dateset.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) 

通过设定参数one_hot_label = True,可以得到one_hot表示(仅正确解标签为1,其余为0的数据结构),代码中的两行注释表示训练数据有60000个,输入数据是784维(28×28)的图像数据,监督数据是10维的数据。

从这个训练数据中随机抽取10笔数据,可以使用numpy的np.random.choice(),写成如下形式:

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

使用np.random.choice()可以从指定的数字中随机选择想要的数字,可以得到一个包含被选数据的索引的数组,然后只需指定这些随机选出的索引,取出mini-batch,然后使用这个mini-batch计算损失函数即可。

数值微分

梯度法使用梯度的信息决定前进的方向

导数

导数表示的是某个瞬间的变化量,定义成以下的式子:

左边符号表示f(x)关于x的导数,即f(x)相对于x的变化程度

# 不好的示例实现
def numerical_diff(f,x):
    h = 10e-50
    return (f(x+h)-f(x))/h

这个函数有两个参数,函数 f 和传给函数 f 的参数 x ,这段代码两处地方需要改进,想把尽可能小的值赋给 h,所以采用了10e-50这个微小值,但会产生舍入误差。(因省略小数的精细部分的数值而造成最终的计算结果上的误差)

如果用32位的浮点数来表示1e-50,就会变成0.0,无法正确表示,使用过小的值会造成计算上的问题,可以将h改成10^-4

第二个要改进的地方与 f 的差分有关,真的导数对应函数在x处的斜率(切线),但在代码实现上是(x+h)和 x 之间的斜率,两者得到的导数值在严格意义上并不一致,这个差异的出现是因为 h 不可能无限接近0

所以计算 f 在(x+h)和(x-h)之间的差分,也成为中心差分

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

数值微分的例子

尝试用上述的数值微分对简单函数进行求导

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

绘制图像

import numpy as np
import matplotlib.pyplot as plt

x= np.arange(0.0,20.0,0.1)  # 0到20,0.1为单位
y= func1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x,y)
plt.show()


来计算一下这个函数在x=5和x=10的导数

print(numerical_diff(func1,5))
print(numerical_diff(func1,10))


得出的结果与真的导数误差极为小,基本上可以看作相等

画出x=5的切线,完整代码如下:

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

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

import numpy as np
import matplotlib.pyplot as plt

x= np.arange(0.0,20.0,0.1)  # 0到20,0.1为单位
y= func1(x)
plt.xlabel("x")
plt.ylabel("f(x)")


def tangent_line(f, x):
    d = numerical_diff(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y

tf = tangent_line(func1, 5)
y2 = tf(x)

plt.plot(x,y)
plt.plot(x,y2)
plt.show()

偏导数


具有两个变量的函数

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

画一下这个函数的图像

来求该函数的导数,但是式中有两个变量,所以要区分对 x 0 x_0 x0还是 x 1 x_1 x1进行求导数,此外将有多个变量的函数的导数称为偏导数,用数学式可以写成

求解两个关于偏导数的例子

题目一

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

def func_t1(x0):
    return x0*x0 + 4.0**2.0

print(numerical_diff(func_t1,3.0))

题目二

def func_t2(x1):
    return 3.0**2.0 + x1*x1  

print(numerical_diff(func_t2,4.0))


与解析解的导数基本一致,偏导数与单变量的导数一样,都是求某个某个地方的斜率。不过偏导数需要将多个变量中的某一变量定为目标变量,并将其他变量固定为某个值。

梯度

上面的例子中计算了 x 0 x_0 x0 x 1 x_1 x1的偏导数,而一起计算两个的偏导数

此外,像这样由全部变量的偏导数汇总而成的向量称为梯度(gradient)

import numpy as np

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

函数numerical_gradient(f,x)执行的处理和求单变量的数值微分基本没有区别,参数 f 为函数,x为numpy数组,该函数对数组 x 的各个元素求数值微分。现在用它来求点(3,4),(0,2),(3,0)处的梯度

print(numerical_gradient(func2,np.array([3.0,4.0])))
print(numerical_gradient(func2,np.array([0.0,2.0])))
print(numerical_gradient(func2,np.array([3.0,0.0])))



将该函数的梯度画在图上

# coding: utf-8
# cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D

def _numerical_gradient_no_batch(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(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)
        
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)
        
        return grad

def function_2(x):
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)

def tangent_line(f, x):
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y
     
if __name__ == '__main__':
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)
    
    X = X.flatten()
    Y = Y.flatten()
    
    grad = numerical_gradient(function_2, np.array([X, Y]) )
    
    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.legend()
    plt.draw()
    plt.show()


如图该函数的梯度呈现为有向向量(箭头),指向了最低处。实际上梯度会指向各点处的函数值降低的方向。更严格的讲,梯度指示的方向是各点处的函数值减小最多的方向。(重要性质)

梯度法

机器学习的主要任务就是在学习中寻找最优参数(权重和偏置),这里的最优参数就是损失函数取最小值时的参数。但通常情况下损失函数很复杂,参数空间庞大,不知道在何处取最小值,只能通过巧妙使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。

就如上面的重要性质讲的一样,梯度指示的方向是各点处的函数值减小最多的方向。所以无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。虽然梯度的方向并不一定指向最小值,但沿着它的方向能最大限度地减小函数的值。

在梯度法中,函数的取值从当前位置沿着梯度方向前进一段距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断沿梯度方向前进,逐渐减少函数值的过程就是梯度法

尝试用数学式来表示梯度法“

η \eta η表示更新量,也称为学习率(learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。

上面的数学式表示更新一次的式子,这个步骤会反复执行,逐渐减少函数值。学习率需要事先确定为某个值,比如0.01或者0.001。为了达到合适的值,一般都会边改变值边确认学习是否正确进行。

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 是学习率,step_num是梯度法的重复次数。
numerical_gradient(f,x)是之前的求函数梯度,用该梯度乘以学习率得到的值进行更新操作。

使用这个函数可以求极小值

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

init_x = np.array([-3.0,4.0])
print(gradient_descent(func2,init_x=init_x,lr=0.1,step_num=100))


设置初始值为(-3.0,4.0),最终结果为(-6.1e-10,8.1e-10),非常接近(0,0),接近真的最小值。用图来表示梯度法的更新过程,原点处是最低的地方,函数的取值一点点在向其靠近。

# coding: utf-8
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)


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)

plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()


学习率过大过小都会影响最后的结果

init_x = np.array([-3.0,4.0])
print(gradient_descent(func2,init_x=init_x,lr=10.0,step_num=100))   # 学习率为10
print(gradient_descent(func2,init_x=init_x,lr=1e-10,step_num=100))  # 学习率为1e-10



过大会发散为一个很大的值,学习率过小的话,基本上没更新就结束了。学习率这种超参数
需要人工设定尝试多个值。

神经网络的梯度

神经网络的学习也要求梯度。这里的梯度是指损失函数关于权重参数的梯度。有一个形状为2*3的权重 W 的神经网络,损失函数用 L L L 表示


的元素由各个元素关于 W 的偏导数构成。 例如第一行第一列元素表示当 w w w 11稍微变化时,损失函数 L L L 会发生多大变化。

下面以一个简单的神经网络为例,来实现求梯度的代码,为此实现一个名为 simpleNet 的类

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

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

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x) # 溢出对策
    return np.exp(x) / np.sum(np.exp(x))

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_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    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

学习算法的实现

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

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

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

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

步骤4(重复)
重复步骤1,步骤2,步骤3

这个学习方法通过梯度下降法更新参数,使用的数据是随机选择的mini batch数据,所以又称为随机梯度下降法(stochastic gradient descent)。一般由一个名为SGD的函数来实现。下面来实现手写数字识别的神经网络,这里以2层神经网络(隐藏层为1层的网络)为对象,使用MNIST数据集进行学习。

2层神经网络的类

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

# coding: utf-8
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):
        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
        
    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

TwoLayerNet类有params和grads两个字典型实例变量,前者保存神经网络的参数,后者保存梯度的参数。TwoLayerNet的方法实现,首先是__init__(self,input_size,hidden_size,output_size)方法,是类的初始化方法(初始化就是生成TwoLayerNet实例时被调用的方法)。从第一个参数开始,依次表示输入层的神经元数、隐藏层的神经元数、输出层的神经元数。

进行手写数字识别时,输入图像大小为784(28×28),输出为10个类别,所以指定参数input_size=784、output_size=10,将隐藏层的个数hidden_size设置为一个合适的值即可。

predict(self,x)和accuracy(self,x,t)的实现和上一章的神经网络的推理过程基本一样。loss(self,x,t)是计算损失函数值的方法。这个方***基于predict()的结果和正确解标签,计算交叉熵误差。剩下的numerical_gradient(self,x,t)方***计算各个参数的梯度。根据数值微分,计算各个参数相对于损失函数的梯度。另外,gradient(self,x,t)是下一章要实现的地方,该方法使用误差反向传播法高效的计算梯度。

mini-batch的实现

神经网络的学习就是mini-batch学习,就是从训练数据中随机选择一部分数据,在意这些mini-batch为对象,使用梯度法更新参数的过程。

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)

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)
    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)更新参数。

基于测试数据的评价

通过反复学习可以使损失函数的值逐渐减小,不过这个损失函数的值,严格讲是“对训练数据的某个mini-batch的损失函数”的值。训练数据的损失函数值减小,光有这个结果还不能说明该神经网络在其他数据集上也一定能有同等程度的表现。

神经网络需要防止过拟合,要掌握泛化能力。对代码进行修改,下面的代码在进行学习的过程中会定期地对训练数据和测试数据记录识别精度。每经过一个 epoch ,都会记录训练数据和测试数据的识别精度。

修改的代码:

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)

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):
    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)
    # 计算每个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的识别精度,是因为如果for语句的循环中一只计算识别精度,会花费太多时间。并且也没有必要频繁记录识别精度,所以才会经过一次epoch就记录一次训练数据的识别精度。