本文主要参考《深度学习入门-基于python的理论与实现》一书。
“手写数字识别” 可以说是机器学习界的“hello world”了,本文将简单说下如何不使用成熟的机器学习库,来手动实现一个神经网络。
首先,我们需要导入训练集和测试集:
"""load_mnist(normalize=True, flatten=True, one_hot_label=False)
读入MNIST数据集
Parameters
----------
normalize : 将图像的像素值正规化为0.0~1.0
one_hot_label :
one_hot_label为True的情况下,标签作为one-hot数组返回
one-hot数组是指[0,0,1,0,0,0,0,0,0,0]这样的数组
flatten : 是否将图像展开为一维数组
Returns
-------
(训练图像, 训练标签), (测试图像, 测试标签)
"""
(x_train, t_train), (x_test, t_test) = load_mnist(normalize = True, one_hot_label = True)
接着,我们要实现一个双层网络,它应该接收一系列输入,并经过若干个隐藏层的内部处理,最终识别出数字为0-9中的哪一个。
上面提到的过程叫做前向传播,我们通过前向传播来得出一个结果。但是,这个结果不一定对,所以,我们同样需要一个叫做反向传播的东西,来修正学习模型。
在我们的例子中,会设计4个层,分别是Affine层,Relu层,Affine层,SoftmaxWithLoss层。
- Affine层:在Affine层我们会对输入矩阵进行形如:y = ax+b的计算,反映到矩阵上就像一次线性变换和一次平移,所以叫做仿射变换层
- Relu层:激活函数层,在这里会将上一层输出的数据进行一次运算,输出0-1之间的一个数。至于为什么需要激活层而不是直接把结果传递下去,可以参考 https://zhuanlan.zhihu.com/p/165194685
- SoftmaxWithLoss层:如果不训练ANN,那么这层是不需要的。softmax的作用就是将输出正规化(输出值的和为1),以便进行反向传播的计算。
先来看下前向传播的实现:
- Affine层的前向传播:
def forward(self, x):
# 对应张量
self.original_x_shape = x.shape
x = x.reshape(x.shape[0], -1)
self.x = x
out = np.dot(self.x, self.W) + self.b
return out
很简单,就是基本就是形如y = ax + b的形式。
- Relu层的前向传播:
def forward(self, x):
self.mask = (x <= 0)
out = x.copy()
# 等价于out[x <= 0] = 0, 前面计算了self.mask,只是为了把结果存下来给反向传播使用
out[self.mask] = 0
return out
Relu层也很好理解,Relu会把大于0的元素保持不变,小于0的元素变为0。
需要注意,这里的运算用到了numpy数组的特性。
- SoftmaskWithLoss的前向传播:
def forward(self, x, t):
self.t = t
self.y = softmax(x)
self.loss = cross_entropy_error(self.y, self.t)
return self.loss
这里的t是监督数据,通过交叉熵误差计算损失函数,得到损失值。
接着就是前向传播,很简单,依次遍历每个层的forward函数:
def predict(self, x):
for layer in self.layers.values():
x = layer.forward(x)
return x
别忘了,我们前面还提到,要想训练ANN,需要实现反向传播。那么什么叫反向传播?
其实反向传播就是一个对参数求导的过程,目的是使损失函数降到最低。除了反向传播,还有一种使用数值微分求梯度的办法来降低损失函数的值,但这种方法计算量比较大。所以我们选择反向传播。
先来实现以下各个层的反向传播算法:
- Affine层的反向传播:
def backward(self, x):
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
dx = dx.reshape(*self.original_x_shape) # 还原输入数据的形状(对应张量)
return dx
- Relu层的反向传播:
def backward(self, x):
dout[self.mask] = 0
dx = dout
return dx
- SoftmaskWithLoss的反向传播:
def backward(self, x, t):
batch_size = self.t.shape[0]
if self.t.size == self.y.size: # 监督数据是one-hot-vector的情况
dx = (self.y - self.t) / batch_size
else:
dx = self.y.copy()
dx[np.arange(batch_size), self.t] -= 1
dx = dx / batch_size
return dx
接着,我们用这些反向传播来算梯度:
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.lastLayer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {}
grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
return grads
层已经都建好了,开始撸训练的代码:
# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
# 新建一个输入层784神经元、隐藏层50神经元、输出层10神经元的双层神经网络
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
输入层的784来源于28*28,即单个图像的大小,我们会把图像的转成一个一维数组作为输入。隐藏层的大小是随意规定的。
# 更新次数
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
定义一些超参数,也就是人为控制的参数:
- iters_num:要训练的次数
- train_size:训练样本的总数
- batch_size:batch的大小,我们训练会分批进行,因为我们每次输入一个数据——(784,)去运算,那效率就太低了。如果每次输入100个数据去运算,那这个100个数据会以矩阵形式输入——(100 * 784),输出就是(100 * 10),这样可以高效的利用numpy的矩阵运算,极大提升训练效率。这种训练法被称为mini-batch。
- learning_rate:学习率,也就是我们每次以多大幅度去沿着梯度下降的方向更新参数
train_loss_list = []
train_acc_list = []
test_acc_list = []
这些主要是为了记录训练结果。
iter_per_epoch = max(train_size / batch_size, 1)
在训练过程中,因为我们的数据可能只是某一类数据,也就是我们的训练样本可能不具备训练所需要的全部特征,比如我们用10000个草书写的数字训练的神经网络,去测楷书写的数字,准确率会大打折扣。这种现象称为过拟合,为了在训练过程中及时察觉这种情况,通常需要准备两组数据,一组用来训练,一组用来测试。
我们定义iter_per_epoch 这个变量,就是每隔一段时间,去对比下训练数据的准确率,和测试数据的准确率,来看下他们偏离大不大。
训练的完整代码如下:
# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
# 新建一个输入层784神经元、隐藏层50神经元、输出层10神经元的双层神经网络
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 = []
# 所有训练数据均被使用过一次时的更新次数,假如有1w个训练数据,batch为100,那每100次就是一个epoch
iter_per_epoch = max(train_size / batch_size, 1)
for i in range(iters_num):
# 挑选100个索引
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)