深度学习|模型训练:手写 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
在此基础上我们可以推广到对多组参数 X 求函数 f 梯度的方法:
def numerical_gradient_2d(f, X):
"""梯度函数(批量)
用数值微分求导法,求 f 关于 n 组参数的 n 个梯度。
Args:
f: 损失函数
X: 参数(n 组参数,2 维数组)
Returns:
grad: n 组梯度(2 维数组)
"""
if X.ndim == 1:
return _numerical_gradient_1d(f, X)
else:
grad = np.zeros_like(X)
for idx, x in enumerate(X):
grad[idx] = _numerical_gradient_1d(f, x)
return grad
numerical_gradient_2d 实现了对多组参数 X 同时求函数 f 梯度,这将可以直接应用于下文关于模型训练的 mini-batch 梯度计算当中。
SimpleNet 类
有了激活函数、损失函数、梯度计算函数,我们就可以实现简单的神经网络 SimpleNet 类了。
权重参数
首先 SimpleNet 类包含了神经网络的权重参数 W 与偏置参数 B,我们在 SimpleNet 类的 __init__
方法定义和保存这些权重参数:
class SimpleNet(object):
"""一个简单的演示神经网络 SimpleNet,用于演示神经网络对手写数字图像识别任务的自动学习和推理过程。
Attributes:
params: 存放 SimpleNet 网络权重参数与偏置参数
W1: 第 1 层网络的权重参数
b1: 第 1 层网络的偏置参数
W2: 第 2 层网络的权重参数
b2: 第 2 层网络的偏置参数
"""
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
"""SimpleNet 的初始化函数
Args:
input_size: 输入层(第 0 层)神经元个数(神经网络入参个数)
hidden_size: 隐藏层(第 1 层)神经元个数
output_size: 输出层(第 2 层)神经元个数(神经网络出参个数)
weight_init_std: 用于初始化权重参数的高斯分布的标准差
"""
# 初始化权重
self.params = {}
self.params['W1'] = weight_init_std * \
np.random.randn(input_size, hidden_size) # 用高斯分布进行 W1 参数的随机初始化
self.params['b1'] = np.zeros(hidden_size) # 用 0 进行 b1 参数的初始化
self.params['W2'] = weight_init_std * \
np.random.randn(hidden_size, output_size) # 用高斯分布进行 W2 参数的随机初始化
self.params['b2'] = np.zeros(output_size) # 用 0 进行 b2 参数的初始化
模型推理
参见前文「深度学习|模型推理:端到端任务处理」,我们可以根据输入 x 和 SimpleNet 的权重参数(W、B)计算出推理结果 y:
import numpy as np
import sigmoid, softmax
class SimpleNet(object):
# ...
def predict(self, x):
"""推理函数
识别数字图像代表的数值。
Args:
x: 图像像素值数组(图像数据)
Returns:
y: 推理结果,图像代表的数值
"""
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
s1 = np.dot(x, W1) + b1
a1 = sigmoid(s1)
s2 = np.dot(a1, W2) + b2
y = softmax(s2)
return y
损失计算
结合监督数据 t 与 SimpleNet 的推理函数 predict 得到的推理结果 y,我们可以使用上文的交叉熵函数
实现损失函数:
import cross_entropy_error
class SimpleNet(object):
# ...
def loss(self, x, t):
"""损失函数(交叉熵误差)
Args:
x: 输入数据,即图像数据
t: 监督数据,即正确解标签
Returns:
loss: 推理的损失值
"""
y = self.predict(x)
return cross_entropy_error(y, t)