深度学习|模型训练:手写 SimpleNet
引言
从前文「深度学习|梯度下降法:误差最小化的权重参数」,我们知道了神经网络的学习就是“找寻使损失函数的值尽可能小的权重参数”的过程,又掌握了找寻的方法(梯度下降法)。凭借这些信息,我们可以以纯手写 Python 代码的方式,实现一个简单的神经网络 SimpleNet,使用这个 SimpleNet 来演示神经网络的整个训练过程,并验证它的推理效果。
SimpleNet 网络结构
我们依旧以手写数字识别
为任务目标,实现一个可用于该任务的形如图 1 所示的 SimpleNet
,亲身体验一下神经网络学会
识别这些图片所代表数字的数学过程。
如图 1 所示,SimpleNet 是一个两层神经网络,它的输入层有 784 个神经元,分别代表 28 28 个像素值,第 1 层隐层有 50 个功能神经元,输出层有 10 个神经元,分别代表预测结果为 0 ~ 9 的概率。
从前文对神经网络的介绍中我们知道,要实现一个神经网络的基本功能,除了要确定神经网络的结构
,我们还需要确定它每一层所使用的激活函数
,以及在进行梯度下降法
优化权重参数
时所使用的损失函数
以及梯度函数
。
激活函数
SimpleNet 的第 1 层隐层我们使用 Sigmoid 函数作为激活函数,Sigmoid 函数是一个 S 型函数,它将输入值映射到 0 到 1 之间,有助于神经网络的非线性表达。输出层我们使用 Softmax 函数作为激活函数,Softmax 函数将输入值映射成 0 到 1 之间的概率值,它输出值归一化,使得输出值之和为 1,用于此类多分类任务正好合适:
import numpy as np
def sigmoid(x):
"""S 型函数"""
return 1 / (1 + np.exp(-x))
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))
关于激活函数的更多详细介绍可以参见前文「深度学习|激活函数:网络表达增强」。
损失函数
SimpleNet 的损失函数我们可以选择使用交叉熵误差
(cross entropy error
),交叉熵误差可用于评估类别的概率分布,常用于此类多分类任务。
在多分类任务中,交叉熵误差计算的是对应正确解神经元的输出的自然对数,用式 1 表示:
其中 表示神经网络输出层的第 k 个神经元的输出值, 表示监督数据的 one-hot
表示。
因为 中只有正确解索引位的值为 1,其他均为 0,式 1 实际只计算了对应正确解神经元输出的自然对数。
因此交叉熵误差的图形可以等价于自然对数函数图形:
以 y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
,t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
为例,推理结果 y 相对实际结果 t 的交叉熵误差
为:
更一般的,我们可以将单个 (y, t) 数据样例的交叉熵误差计算推广到求一批包含 n 个样例的训练集的交叉熵误差计算(用于 mini-batch 的梯度计算),用式 2 表示:
如下是交叉熵误差函数的 Python 实现:
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
梯度计算
前文「深度 学习|梯度下降法:误差最小化的权重参数」中我们介绍了对一组参数 x 求函数 f 梯度的方法:
import numpy as np
def _numerical_gradient_1d(f, x):
"""梯度函数
用数值微分求导法,求 f 关于 1 组参数的 1 个梯度。
Args:
f: 损失函数
x: 参数(1 组参数,1 维数组)
Returns:
grad: 1 组梯度(1 维数组)
"""
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 生成和 x 形状相同的数组,用于存放梯度(所有变量的偏导数)
for idx in range(x.size): # 挨个遍历所有变量
xi = x[idx] # 取第 idx 个变量
x[idx] = float(xi) + h
fxh1 = f(x) # 求第 idx 个变量增大 h 所得计算结果
x[idx] = xi - h
fxh2 = f(x) # 求第 idx 个变量减小 h 所得计算结果
grad[idx] = (fxh1 - fxh2) / (2*h) # 求第 idx 个变量的偏导数
x[idx] = xi # 还原第 idx 个变量的值
return grad