神经网络的学习
在线性可分的与非门、或门的感知机模型中,我们可以根据真值表人工设定参数来实现,而此时的参数只有3个。然而,在实际的神经网络中,参数的数量成千上万,甚至上亿,很显然,人工设定参数是不可能的。从数据中学习就成了解决上面问题的关键。
- “学习”就是指从训练数据中自动获取最优权重参数的过程
- 为了能够进行学习,需要引入损失函数
- 学习的目的:使损失函数达到最小值的权重参数
- 学习过程可以使用梯度法
损失函数
神经网络的学习需要通过某个指标来表现现在的状态,然后,以这个指标为基准,寻找最优权重参数。这个指标便是“损失函数”,这个损失函数可以死任意函数,但一般用均方误差和交叉熵误差。
损失函数是表示神经网络性能的“恶劣程度”的指标,即当前的神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。
- 均方误差
其中, 表示神经网络的输出, 表示监督数据(标签数据), 表示数据的维度
# 均方误差损失函数
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]
# 预测为"2"的概率最高
y1 = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
# 预测为"7"的概率最高
y2 = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
# 求均方误差
print(mean_squared_error(np.array(y1),np.array(t)))
print(mean_squared_error(np.array(y2),np.array(t)))
输出为:
0.09750000000000003
0.5975
结果我们发现, 的损失函数的值更小,和监督数据之间的误差较小,也就是 的输出结果与监督数据更吻合。
- 交叉熵误差
其中, 表示神经网络的输出, 表示正确解标签, 表示数据的维度
中只有正确解标签的索引为1,其他为0(one-hot表示)
# 交叉熵误差
def cross_entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))
# 在计算log时,加上了一个微小值,防止出现计算np.log(0)变为负无穷大
# 测试
# 设"2"为正确解
t = [0,0,1,0,0,0,0,0,0,0]
# "2"的概率最高
y1 = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
# "7"的概率最高
y2 = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
# 求交叉熵误差
print(cross_entropy_error(np.array(y1),np.array(t)))
print(cross_entropy_error(np.array(y2),np.array(t)))
输出为:
0.510825457099338
2.302584092994546
此结果与前面均方误差讨论的内容一样
mini-batch学习(小批量学习)
使用训练数据进行学习,就是针对训练数据计算损失函数的值,找到是该值尽可能小的参数,因此计算损失函数的值的时候必须将所有训练数据作为对象,也就是要把所有训练数据的损失函数值的总和作为学习的指标。
前面提到的均方误差和交叉熵误差都是针对单个数据的损失函数,如果要求所有孙训练数据的损失函数的总和,这里仅以交叉熵误差为例,数学表达式可以写为:
其中,假设数据有 个, 表示第n个数据的第k个元素的值,是监督数据, 是神经网络的输出
在数据量特别大的时候,如果我们以全部数据为对象来计算,计算过程需要花费很长的时间
因此,我们需要从全部数据中选出一部分,作为全部数据的“近似”
比如,从60000个训练数据中随机选择100个数据,再用这100个数据进行学习,这种学习方式就称为mini-batch学习
- 如何选出小批量数据
import numpy as np
import mnist
# load_mnist用于读入MNIST数据集函数
# 读入时设定one_hot_label=True,可以得到one-hot表示(即仅正确解标签为1,其他为0)
(X_train, T_train),(X_test, T_test) = mnist.load_mnist(normalize=False, one_hot_label=True)
print(X_train.shape) # (60000, 784)
print(T_train.shape) # (60000, 10)
train_size = X_train.shape[0] # 训练数据的数量
batch_size = 10 # 设置批量数为10
# 使用np.random.choice()可以从指定数字中随机选择想要的数字
# 比如,np.random.choice(60000, 10)会从0-59999之间随机选择10个数字
batch_mask = np.random.choice(train_size, batch_size)
x_batch = X_train[batch_mask]
t_batch = T_train[batch_mask]
- 同时处理单个数据和批量数据的交叉熵误差的实现
# 可以同时处理单个数据和批量数据
# 监督数据为非one-hot表示
def cross_entropy_error(y, t):
# y为1维时,即
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
np.arange(5)表示生成一个NumPy数组[0, 1, 2, 3, 4],假设监督数据为[2, 7, 0, 9, 4]
y[np.arange(5), t]会生成[y[0, 2], y[1, 7], y[2, 0], y[3, 9], y[4, 4]]
导数、数值微分和偏导数
- 导数数学表达式如下:
关于导数,可自行参考高等数学
- 数值微分
def numerical_diff(f,x):
h = 1e-4 # 0.0001
return (f(x+h) - f(x-h)) / (2 * h) # 这边使用的是中心差分
根据导数的定义,我们可以向h中赋入一个微小值,让h无限接近0,但是不能太小,太小可能会导致舍入误差(指因省略小数的精细部分的数值,小数点第8位以后的数值),导致最终结果的计算上的误差
- 举例
import numpy as np
import matplotlib.pylab as plt
def function_1(x):
return 0.01*x**2 + 0.1*x
x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)
plt.show()
print(numerical_diff(function_1, 5)) # 0.1999999999990898
print(numerical_diff(function_1, 10)) # 0.2999999999986347
上面函数的导数为:
在x=5和10处,“真的导数为”0.2和0.3,和上面的结果相比,误差很小。
- 偏导数
上面我们只是针对单一变量的函数的导数,如果有多个变量的函数的导数,我们称为偏导数。
用数学式表示为:
对于多个变量的函数求导,我们可以取一个变量,然后固定其他变量来求偏导数,例如:
def function_temp1(x0):
return x0*x0 + 4.0**2
print(numerical_diff(function_temp1, 3.0)) # 6.00000000000378
这里定义一个固定 的新函数,然后只有变量 的函数应用了求数值微分的函数;
对 的偏导数为:
时,对 的偏导数为6.0
我们可以发现,上面两个结果同样很接近
梯度
对于多变量的函数,计算全部变量的偏导数汇总而成的向量就称为梯度。比如上面的两个变量的函数,梯度为:
- python实现
# f(x0,x1) = x0^2 + x_1^2
def function_2(x):
return x[0]**2 + x[1]**2
# 梯度
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 生成和x形状相同的0数组
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
# 测试
print(numerical_gradient(function_2, np.array([3.0, 4.0])))
print(numerical_gradient(function_2, np.array([0.0, 2.0])))
print(numerical_gradient(function_2, np.array([3.0, 0.0])))
输出为:
[6. 8.]
[0. 4.]
[6. 0.]
梯度法
机器学习的主要任务是在学习时寻找最优参数。同样的,神经网络也必须在学习时找到最优参数(权重和偏置),这里所说的最优参数就是使损失函数取最小值时的参数。
一般而言,损失函数很复杂,参数空间庞大,我们不知道它在何处能取最小值,但是我们可以通过巧妙地使用梯度来寻找函数的最小值(或者尽可能小的值),这种方法就称为梯度法。
梯度表示的是各点处的函数值减小最多的方向,因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂函数中,梯度指示的方向基本上都不是函数最小值处。
虽然梯度方向不一定指向函数最小值方向,但是沿着梯度方向能够最大限度地减小函数值,因此,在寻找函数最小值的位置任务中,要以梯度的信息为线索,决定前进的方向。
梯度法的数学表达式:
其中, 为学习率,决定在一次学习中,应该学习多少,以及在多大程度上更新参数。
上式表示一次更新,这个步骤会反复执行
- python实现梯度下降法
# 梯度下降法(f为要进行最优化的函数, init_x为初始值,lr是学习率,step_num为梯度法的重复次数)
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
for i in range(step_num):
# 调用之前的numerical_gradient()函数求梯度
grad = numerical_gradient(f, x)
x -= lr * grad
return x
# 测试,用梯度下降法求函数f(x0,x1) = x0^2 + x_1^2最小值
# f(x0,x1) = x0^2 + x_1^2
def function_2(x):
return x[0]**2 + x[1]**2
init_x = np.array([-3.0, 4.0]) # 设置初始值为[-3.0, 4.0]
print(gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100))
输出为:
[-6.11110793e-10 8.14814391e-10]
结果非常接近(0,0),实际上,真实的最小值就是(0,0),所以说通过梯度法我们基本得到了正确的结果。
如果我们改变学习率,我们再来看看结果:
# 学习率过大
print(gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100))
# 结果为:[-2.58983747e+13 -1.29524862e+12]
# 学习率过小
print(gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100))
# 结果为:[-2.99999994 3.99999992]
由结果我们可以发现,学习率过大,会发散成一个很大的数;学习率过小,基本上没怎么更新就结束了
所以,设定合适的学习率是一个很重要的问题
学习率这样的参数称为超参数
- 神经网络的梯度
假设我们有一个神经网络权重参数 如下:
梯度可以表示为:
- python实现神经网络梯度
import numpy as np
# softmax函数
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的情况下,转换为正确解标签的索引
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
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): # 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()
print("权重参数:\n", net.W)
f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)
print("梯度:\n", dW)
输出为:
权重参数:
[[-0.76324663 -0.94168079 -0.10280071]
[-0.12308627 -0.0498545 -0.02240215]]
梯度:
[[ 0.16727439 0.16053045 -0.32780484]
[ 0.25091159 0.24079568 -0.49170727]]
dW是一个2*3的二维数组,其中第一行第一列的数大约为0.167,这表明如果 增加h,那么损失函数的值就会增加0.167h;再如,第一行第三列的数大约为-0.328,这表明如果 增加h,那么损失函数的值将减小0.328h
因此,从减小损失函数值的观点来看, 应该往正方向更新, 应该往反方向更新
总结
- 本篇主要介绍了神经网络的学习过程
- 涉及到如何从整个训练数据中获取批量数据、损失函数、梯度法
- 最后实现了一个简单的神经网络的权重参数的梯度求解
- 求解出神经网络的梯度后,接下来只需要使用梯度法,更新参数即可,这将在下篇通过python实现