关于 神 经 网 络 学 习 的 基 础 知 识,到 这 里 就 全 部 介 绍 完 了。“损 失 函 数” “mini-batch” “梯度” “梯度下降法”等关键词已经陆续登场。详情请见前两篇笔记:笔记(一),笔记(二)
学习算法的实现
这里我们来确认一下神经网络的学习步骤,顺便复习一下这些内容。神经网络的学习步骤如下所示:
前提
神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为“学习”。神经网络的学习分成下面 4 个步骤。
步骤 1(mini-batch)
从训练数据中随机选出一部分数据,这部分数据称为 mini-batch。我们的目标是减小 mini-batch 的损失函数的值。
步骤 2(计算梯度)
为了减小 mini-batch 的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。
步骤 3(更新参数)
将权重参数沿梯度方向进行微小更新。
步骤 4(重复)
重复步骤 1、步骤 2、步骤 3。
神经网络的学习按照上面 4 个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的 mini batch 数据,所以又称为随机梯度下降法(stochastic gradient descent)--SGD。下面,我们来实现手写数字识别的神经网络。这里以 2 层神经网络(隐藏层为 1 层的网络)为对象,使用 MNIST 数据集进行学习。
import numpy as np
from net import sigmoid
from learn2_net import softmax, cross_entropy_error
import numpy as np
#修改计算梯度的函数,使其适配二维tensor的参数x
def numerical_gradient(f, x):
h = 1e-4 # 步长(很小的值,用于计算数值梯度)
grad = np.zeros_like(x) # 创建一个与 x 形状相同的零数组,用于存储梯度
if x.ndim == 1:
# 处理一维数组
for idx in range(x.size):
tmp_val = x[idx] # 保存当前 x 的值
# 计算 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 # 恢复原始的 x 值
elif x.ndim == 2:
# 处理二维数组
for i in range(x.shape[0]):
for j in range(x.shape[1]):
tmp_val = x[i, j] # 保存当前 x 的值
# 计算 f(x+h)
x[i, j] = tmp_val + h
fxh1 = f(x)
# 计算 f(x-h)
x[i, j] = tmp_val - h
fxh2 = f(x)
# 使用中心差分法计算梯度
grad[i, j] = (fxh1 - fxh2) / (2 * h)
x[i, j] = tmp_val # 恢复原始的 x 值
else:
raise ValueError("输入数组的维度不支持")
return grad # 返回梯度
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
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
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
代码说明:首先,我们将这个 2 层神经网络实现为一个名为 TwoLayerNet 的类。TwoLayerNet 类有 params 和 grads 两个字典型实例变量:
params
保存神经网络的参数的字典型变量(实例变量)。
params['W1']是第 1 层的权重,params['b1']是第 1 层的偏置。
params['W2']是第 2 层的权重,params['b2']是第 2 层的偏置
# 创建 TwoLayerNet 实例
net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10)
# 打印权重和偏置的形状
print(net.params['W1'].shape) # (784, 100)
print(net.params['b1'].shape) # (100,)
print(net.params['W2'].shape) # (100, 10)
print(net.params['b2'].shape) # (10,)
上述代码实现了params的参数的初始化,下面是参数在predict函数中的调用:
# 生成伪输入数据(100笔)
x = np.random.rand(100, 784)
# 预测
y = net.predict(x)
grads
保存梯度的字典型变量(numerical_gradient()方法的返回值)。
grads['W1']是第 1 层权重的梯度,grads['b1']是第 1 层偏置的梯度。
grads['W2']是第 2 层权重的梯度,grads['b2']是第 2 层偏置的梯度
此外,与 params变量对应,grads变量中保存了各个参数的梯度。如下所示,
使用 numerical_gradient() 方法计算梯度后,梯度的信息将保存在 grads 变
量中。
# 生成伪正确标签(100笔)
t = np.random.rand(100, 10)
# 计算梯度
grads = net.numerical_gradient(x, t)
# 打印梯度的形状
print(grads['W1'].shape) # (784, 100)
print(grads['b1'].shape) # (100,)
print(grads['W2'].shape) # (100, 10)
print(grads['b2'].shape) # (10,)
输出结果:
接着,我们来看一下 TwoLayerNet 的方法的实现。首先是 __init__(self,input_size, hidden_size, output_size) 方法,它是类的初始化方法(所谓初始化方法,就是生成 TwoLayerNet 实例时被调用的方法)。从第 1 个参数开始,依次表示输入层的神经元数、隐藏层的神经元数、输出层的神经元数。另外,因为进行手写数字识别时,输入图像的大小是 784(28 × 28),输出为 10 个类别,所以指定参数 input_size=784、output_size=10,将隐藏层的个数 hidden_size设置为一个合适的值即可。
- 此外,这个初始化方法会对权重参数进行初始化。如何设置权重参数的初始值这个问题是关系到神经网络能否成功学习的重要问题。后面我们会详细讨论权重参数的初始化,这里只需要知道,权重使用符合高斯分布 的 随 机 数 进 行 初 始 化,偏 置 使 用 0 进行 初 始 化
- 另外,loss(self, x, t)是计算损失函数值的方法。这个方法会基于 predict() 的结果和正确解标签,计算交叉熵误差。
- numerical_gradient(self, x, t)基于数值微分计算参数的梯度。下一章,我们会介绍一个高速计算梯度的方法,称为误差反向传播法。用误差反向传播法求到的梯度和数值微分的结果基本一致,但可以高速地进行处理。
mini-batch 的实现
所谓mini-batch 学习,就是从训练数据中随机选择一部分数据(称为 mini-batch),再以这些 mini-batch 为对象,使用梯度法更新参数的过程。下面,我们就以TwoLayerNet 类为对象,使用 MNIST 数据集进行学习:
(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):
# 1.获取mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 2.计算梯度
grad = network.numerical_gradient(x_batch, t_batch)
# grad = network.gradient(x_batch, t_batch) # 高速版!
# 3.更新参数
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。每更新一次,都对训练数据计算损失函数的值,并把该值添加到数组中。用图像来表示这个损失函数的值的推移,如图 所示。(代码会运行很久)
观察图 4-11,可以发现随着学习的进行,损失函数的值在不断减小。这是学习正常进行的信号,表示神经网络的权重参数在逐渐拟合数据。也就是说,神经网络的确在学习!通过反复地向它浇灌(输入)数据,神经网络正在逐渐向最优参数靠近。
基于测试数据的评价
根据图 4-11 呈现的结果,我们确认了通过反复学习可以使损失函数的值逐渐减小这一事实。不过这个损失函数的值,严格地讲是“对训练数据的某个 mini-batch 的损失函数”的值。训练数据的损失函数值减小,虽说是神经网络的学习正常进行的一个信号,但光看这个结果还不能说明该神经网络在其他数据集上也一定能有同等程度的表现。
神经网络的学习中,必须确认是否能够正确识别训练数据以外的其他数据,即确认是否会发生过拟合。过拟合是指,虽然训练数据中的数字图像能被正确辨别,但是不在训练数据中的数字图像却无法被识别的现象。神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛
化能力,就必须使用不包含在训练数据中的数据。下面的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个epoch,我们都会记录下训练数据和测试数据的识别精度。
epoch 是一个单位。一个 epoch 表示学习中所有训练数据均被使用过
一次时的更新次数。比如,对于 10000 笔训练数据,用大小为 100
笔数据的 mini-batch 进行学习时,重复随机梯度下降法 100 次,所
有的训练数据就都被“看过”了 。此时,100 次就是一个 epoch。
#测试集性能测试
(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)
# 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,就对所有的训练数据和测试数据计算识别精度,并记录结果。之所以要计算每一个 epoch 的识别精度,是因为如果在 for 语句的循环中一直计算识别精度,会花费太多时间。
图 4-12 中,实线表示训练数据的识别精度,虚线表示测试数据的识别精度。如图所示,随着epoch 的前进(学习的进行),我们发现使用训练数据和测试数据评价的识别精度都提高了,并且,这两个识别精度基本上没有差异(两条线基本重叠在一起)。因此,可以说这次的学习中没有发生过拟合的现象。
小结
• 机器学习中使用的数据集分为训练数据和测试数据。
• 神经网络用训练数据进行学习,并用测试数据评价学习到的模型的
泛化能力。
• 神经网络的学习以损失函数为指标,更新权重参数,以使损失函数
的值减小。
• 利用某个给定的微小值的差分求导数的过程,称为数值微分。
• 利用数值微分,可以计算权重参数的梯度。
• 数值微分虽然费时间,但是实现起来很简单。下一章中要实现的稍
微复杂一些的误差反向传播法可以高速地计算梯度。
本章完整代码如下:
from dataprocess import load_mnist
from net import sigmoid
from learn2_net import softmax, cross_entropy_error
import numpy as np
#修改计算梯度的函数,使其适配二维tensor的参数x
def numerical_gradient(f, x):
h = 1e-4 # 步长(很小的值,用于计算数值梯度)
grad = np.zeros_like(x) # 创建一个与 x 形状相同的零数组,用于存储梯度
if x.ndim == 1:
# 处理一维数组
for idx in range(x.size):
tmp_val = x[idx] # 保存当前 x 的值
# 计算 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 # 恢复原始的 x 值
elif x.ndim == 2:
# 处理二维数组
for i in range(x.shape[0]):
for j in range(x.shape[1]):
tmp_val = x[i, j] # 保存当前 x 的值
# 计算 f(x+h)
x[i, j] = tmp_val + h
fxh1 = f(x)
# 计算 f(x-h)
x[i, j] = tmp_val - h
fxh2 = f(x)
# 使用中心差分法计算梯度
grad[i, j] = (fxh1 - fxh2) / (2 * h)
x[i, j] = tmp_val # 恢复原始的 x 值
else:
raise ValueError("输入数组的维度不支持")
return grad # 返回梯度
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
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
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 实例
net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10)
# 打印权重和偏置的形状
print(net.params['W1'].shape) # (784, 100)
print(net.params['b1'].shape) # (100,)
print(net.params['W2'].shape) # (100, 10)
print(net.params['b2'].shape) # (10,)
# 生成伪输入数据(100笔)
x = np.random.rand(100, 784)
# 预测
y = net.predict(x)
# 生成伪正确标签(100笔)
t = np.random.rand(100, 10)
# 计算梯度
grads = net.numerical_gradient(x, t)
# 打印梯度的形状
print(grads['W1'].shape) # (784, 100)
print(grads['b1'].shape) # (100,)
print(grads['W2'].shape) # (100, 10)
print(grads['b2'].shape) # (10,)
(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):
# 1.获取mini-batch
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 2.计算梯度
grad = network.numerical_gradient(x_batch, t_batch)
# grad = network.gradient(x_batch, t_batch) # 高速版!
# 3.更新参数
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)
#测试集性能测试
(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)
# 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))