CONTENTS
- 四、神经网络的学习
- 4.1 从数据中学习
- 4.2 Loss function
- 4.3 数值微分
- 4.4 梯度
- 4.5 学习算法的实现
四、神经网络的学习
4.1 从数据中学习
神经网络的特征就是可以从数据中学习。所谓“从数据中学习”,是指可以由数据自动决定权重参数的值。利用特征量和机器学习的方法中,特征量仍是由人工设计的,而在神经网络中,连图像中包含的重要特征量也都是由机器来学习的。
机器学习中,一般将数据分为训练数据和测试数据两部分来进行学习和实验等。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力。为什么需要将数据分为训练数据和测试数据呢?因为我们追求的是模型的泛化能力。为了正确评价模型的泛化能力,就必须划分训练数据和测试数据。另外,训练数据也可以称为监督数据。
泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。获得泛化能力是机器学习的最终目标。只对某个数据集过度拟合的状态称为过拟合( o v e r f i t t i n g over\ fitting over fitting)。避免过拟合也是机器学习的一个重要课题。
4.2 Loss function
神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基准,寻找最优权重参数。神经网络的学习中所用的指标称为损失函数( l o s s f u n c t i o n loss\ function loss function)。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。
均方误差如下式所示,其中, y k y_k yk表示神经网络的输出, t k t_k tk表示监督数据, k k k表示数据的维数,均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,再求总和:
使用Python实现代码如下:
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
交叉熵误差如下式所示,其中, y k y_k yk表示神经网络的输出, t k t_k tk表示正确解标签,且 t k t_k tk中只有正确解标签的索引为 1 1 1,其他均为 0 0 0( o n e − h o t one-hot one−hot表示):
该式实际上只计算对应正确解标签的输出的自然对数。比如,假设正确解标签的索引是 “ 2 ” “2” “2”,与之对应的神经网络的输出是 0.6 0.6 0.6,则交叉熵误差是 − l o g 0.6 = 0.51 -log\ 0.6=0.51 −log 0.6=0.51;若 “ 2 ” “2” “2”对应的输出是 0.1 0.1 0.1,则交叉熵误差为 − l o g 0.1 = 2.30 -log\ 0.1=2.30 −log 0.1=2.30。也就是说,交叉熵误差的值是由正确解标签所对应的输出结果决定的。
使用Python实现代码如下:
def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))
函数内部在计算np.log
时,加上了一个微小值delta
。这是因为,当出现np.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
使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。计算损失函数时必须将所有的训练数据作为对象。计算损失函数时必须将所有的训练数据作为对象,如果要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式子:
假设数据有 N N N个, t n k t_{nk} tnk表示第 n n n个数据的第 k k k个元素的值( y n k y_{nk} ynk是神经网络的输出, t n k t_{nk} tnk是监督数据)。
由于MNIST数据集的训练数据有
60000
60000
60000个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为
m
i
n
i
−
b
a
t
c
h
mini-batch
mini−batch,小
批量),然后对每个
m
i
n
i
−
b
a
t
c
h
mini-batch
mini−batch进行学习。
假设要从这个训练数据中随机抽取10笔数据,可以使用如下代码,使用np.random.choice()
可以从指定的数字中随机选择想要的数字。比如,np.random.choice(60000, 10)
会从
0
0
0到
59999
59999
59999之间随机选择
10
10
10个数字:
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]
改良交叉熵误差代码,实现一个可以同时处理单个数据和批量数据(数据作为 b a t c h batch 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
当监督数据是标签形式(非 o n e − h o t one-hot one−hot表示,而是像 “ 2 ” , “ 7 ” “2”,“7” “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
# 假设batch_size = 5, t = [2, 7, 0, 9, 4]
# 则y[np.arange(batch_size), t] = [y[0, 2], y[1, 7], y[2, 0], y[3, 9], y[4, 4]]
4.3 数值微分
导数就是表示某个瞬间的变化量,它可以定义成下面的式子:
但是该式子计算时存在误差,因为真的导数是对应函数在 x x x处的斜率(称为切线),但上述实现中计算的导数对应的是 ( x + h ) (x+h) (x+h)和 x x x之间的斜率(如下图所示)。为了减小这个误差,我们可以计算函数 f f f在 ( x + h ) (x+h) (x+h)和 ( x − h ) (x-h) (x−h)之间的差分。因为这种计算方法以 x x x为中心,计算它左右两边的差分,所以也称为中心差分(而 ( x + h ) (x+h) (x+h)和 x x x之间的差分称为前向差分)。
使用Python实现代码如下:
def numerical_diff(f, x):
h = 1e-4 # 0.0001
return (f(x + h) - f(x - h)) / (2 * h)
现在假设我们有一个一元二次函数: y = 0.01 x 2 + 0.1 x y=0.01x^2+0.1x y=0.01x2+0.1x,使用Python实现该式如下:
def function_1(x):
return 0.01 * x ** 2 + 0.1 * x
我们来计算一下这个函数在 x = 5 x=5 x=5 和 x = 10 和x=10 和x=10处的导数:
numerical_diff(function_1, 5) # 0.1999999999990898
numerical_diff(function_1, 10) # 0.2999999999986347
接下来我们来看另一个函数: 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,和上例不同的是,这里有两个变量,使用Python实现该式如下:
def function_2(x):
return x[0] ** 2 + x[1] ** 2
# 或者return np.sum(x ** 2)
因为该式有两个变量,因此有必要区分对哪个变量求导数,有多个变量的函数的导数称为偏导数。用数学式表示的话,可以写成 ∂ f ∂ x 0 , ∂ f ∂ x 1 \frac {\partial f}{\partial x_0},\frac {\partial f}{\partial x_1} ∂x0∂f,∂x1∂f。偏导数需要将多个变量中的某一个变量定为目标变量,并将其他变量固定为某个值(即常数)。
例如求 x 0 = 3 , x 1 = 4 x_0=3,x_1=4 x0=3,x1=4时,关于 x 0 x_0 x0的偏导数 ∂ f ∂ x 0 \frac {\partial f}{\partial x_0} ∂x0∂f:
def function_tmp1(x0):
return x0 * x0 + 4.0 ** 2.0
numerical_diff(function_tmp1, 3.0) # 6.00000000000378
4.4 梯度
像 ( ∂ f ∂ x 0 , ∂ f ∂ x 1 ) (\frac {\partial f}{\partial x_0},\frac {\partial f}{\partial x_1}) (∂x0∂f,∂x1∂f)这样的由全部变量的偏导数汇总而成的向量称为梯度( g r a d i e n t gradient gradient),梯度指示的方向是各点处的函数值减小最多的方向。梯度可以像下面这样来实现:
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 会生成一个形状和x相同,所有元素都为0的数组
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] = float(tmp_val) - h
fxh2 = f(x) # f(x - h)
grad[idx] = (fxh1 - fxh2) / (2 * h)
x[idx] = tmp_val # 还原值
it.iternext()
return grad
神经网络必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数取最小值时的参数,通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。由于梯度表示的是各点处的函数值减小最多的方向,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地减小函数的值。
现在,我们尝试用数学式来表示梯度法,如下式所示:
式中的 η \eta η表示更新量,在神经网络的学习中,称为学习率( l e a r n i n g r a t e learning\ rate learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。
学习率需要事先确定为某个值,比如 0.01 0.01 0.01或 0.001 0.001 0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。
使用Python实现梯度下降法如下,其中参数 f f f是要进行最优化的函数, i n i t _ x init\_x init_x是初始值, l r lr lr是学习率 l e a r n i n g r a t e learning\ rate learning rate, s t e p _ n u m step\_num step_num是梯度法的重复次数:
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
实验结果表明,学习率过大的话,会发散成一个很大的值;反过来,学习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率是一个很重要的问题。
神经网络中的梯度是指损失函数关于权重参数的梯度:
我们以一个简单的神经网络为例,来实现求梯度的代码。为此,我们要实现一个名为simpleNet
的类:
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
试着用一下simpleNet
:
net = simpleNet()
x = np.array([0.6, 0.9])
p = net.predict(x)
print(p) # [1.05414809, 0.63071653, 1.1328074]
np.argmax(p) # 2,最大值的索引
t = np.array([0, 0, 1]) # 正确解标签
net.loss(x, t) # 0.92806853663411326
接下来求梯度:
def f(W):
return net.loss(x, t)
dW = numerical_gradient(f, net.W)
Python中如果定义的是简单的函数,可以使用 l a m b d a lambda lambda表示法。使用 l a m b d a lambda lambda的情况下,上述代码可以如下实现:
f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)
4.5 学习算法的实现
我们先来确认一下神经网络的学习步骤,顺便复习一下这些内容:
- (1) m i n i − b a t c h mini-batch mini−batch:从训练数据中随机选出一部分数据;
- (2)计算梯度:为了减小 m i n i − b a t c h mini-batch mini−batch的损失函数的值,需要求出各个权重参数的梯度;
- (3)更新参数:将权重参数沿梯度方向进行微小更新;
- (4)重复:重复以上三个步骤。
神经网络的学习按照上面 4 4 4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的 m i n i − b a t c h mini-batch mini−batch数据,所以又称为随机梯度下降法。
下面,我们来实现手写数字识别的
2
2
2层神经网络(隐藏层为
1
1
1层)。我们将这个
2
2
2层神经网络实现为一个名为TwoLayerNet
的类,实现过程如下所示:
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
类为对象,使用MNIST数据集进行学习:
import sys
sys.path.append('D:\VS Code Project\Deep Learning')
import numpy as np
import matplotlib.pylab 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)
network = TwoLayerNet(input_size = 784, hidden_size = 50, output_size = 10)
# 超参数
iters_num = 10000 # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1 # 学习率
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
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)
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()
神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包含在训练数据中的数据。上述的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个 e p o c h epoch epoch,我们都会记录下训练数据和测试数据的识别精度。 e p o c h epoch epoch是一个单位。一个 e p o c h epoch epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于 10000 10000 10000笔训练数据,用大小为 100 100 100笔数据的 m i n i − b a t c h mini-batch mini−batch进行学习时,重复随机梯度下降法 100 100 100次,就相当于所有的训练数据就都被“看过”了。
绘图结果如下图所示:
如上图所示,随着 e p o c h epoch epoch的前进(学习的进行),我们发现使用训练数据和测试数据评价的识别精度都提高了,并且,这两个识别精度基本上没有差异(两条线基本重叠在一起)。因此,可以说这次的学习中没有发生过拟合的现象。
下一节:【学习笔记】深度学习入门:基于Python的理论与实现-误差反向传播法。