《手写数字识别》神经网络 学习笔记

本文主要参考《深度学习入门-基于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),以便进行反向传播的计算。

先来看下前向传播的实现:

  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的形式。

  1. 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数组的特性。

  1. 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,需要实现反向传播。那么什么叫反向传播?

其实反向传播就是一个对参数求导的过程,目的是使损失函数降到最低。除了反向传播,还有一种使用数值微分求梯度的办法来降低损失函数的值,但这种方法计算量比较大。所以我们选择反向传播。

先来实现以下各个层的反向传播算法:

  1. 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
  1. Relu层的反向传播:
    def backward(self, x):
        dout[self.mask] = 0
        dx = dout
        
		return dx
  1. 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)
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页