文章目录

  • (一) 问题描述
  • (二) 设计简要描述
  • (三) 程序清单
  • (四) 结果分析
  • (五) 调试报告
  • (六) 实验小结


(一) 问题描述

不使用任何机器学习框架,仅仅通过Numpy库构建一个最简单的全连接前馈神经网络,并用该网络识别mnist提供的手写数字体。

(二) 设计简要描述

机器学习的三个基本步骤——

PyTorch mnist手写识别数字 模型推理 numpy手写数字识别_数据


程序设计思路——(此图放大可看清)

PyTorch mnist手写识别数字 模型推理 numpy手写数字识别_数据_02

(三) 程序清单

import numpy as np
from mnist import load_mnist


# sigmoid函数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))


# softmax函数
def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a, axis=1, keepdims=True)
    y = exp_a / sum_exp_a
    return y


# 损失函数
# y是神经网络的输出,t是正确解标签
def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y + delta))


# 计算并返回数值微分值的代码
def numerical_gradient(f, x):
    # f是函数,x是自变量
    h = 1e-4  # 0.0001
    grad = np.zeros_like(x)  # 生成和x形状相同的数组
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])  # 高效的多维迭代器对象来遍历数组
    while not it.finished:
        idx = it.multi_index  # 表示进入下一次迭代
        tmp_val = x[idx]

        # f(x+h)的计算
        x[idx] = tmp_val + h
        fh1 = f(x)

        # f(x-h)的计算
        x[idx] = tmp_val - h
        fh2 = f(x)
        grad[idx] = (fh1 - fh2) / (2 * h)

        x[idx] = tmp_val  # 还原,因为这里只是计算一下梯度,需要暂时修改x矩阵
        it.iternext()

    return grad


class TwoLayerNet:
    # 模型初始化
    # size是神经元数量
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # 初始化权重
        self.params = {}

        # 第一层的权重
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)  # rand函数根据给定维度生成[0,1)?

        # 第一层的偏置
        self.params['b1'] = np.zeros(hidden_size)

        # 请添加网络第二层的权重和偏置的初始化代码
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    # 模型前向传播过程
    def forward(self, x):
        # 请从参数字典获取网络参数
        W1, W2, b1, b2 = self.params['W1'], self.params['W2'], self.params['b1'], self.params['b2']
        # 实现第一层的运算
        z1 = np.dot(x, W1) + b1
        h1 = sigmoid(z1)
        # 请实现第二层的运算
        h2 = np.dot(h1, W2) + b2  # 注意,第二层的输入x是第一层的输出h1
        y = softmax(h2)  # 第二层的激活函数是softmax而不是sigmoid
        return y

    # 定义损失函数
    def loss(self, x, t):  # x:输入数据, t:正确标签作为监督数据
        y = self.forward(x)
        return cross_entropy_error(y, t)

    # 计算预测的准确率
    def accuracy(self, x, t):  # 假定输入的数据x和标签t都是mini-batch形式的
        # 请补充实现模型前向输出的代码
        y = self.forward(x)
        # 请补充提取模型预测类别和标签真实类别的代码
        y = np.argmax(y, axis=1)  # argmax()函数的作用是沿着一个轴返回最大值的索引
        t = np.argmax(t, axis=1)
        # 请补充计算并返回模型类别预测准确率的代码
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    # 通过数值微分的方式计算模型参数的梯度
    def gradient(self, x, t):
        # 定义字典形式的参数梯度
        grads = {}

        # 请定义需传入numerical_gradient函数的需求梯度的函数
        loss_W = lambda W: self.loss(x, t)
        # 请补充通过数值微分的方法计算参数W1、W2、b1、b2的梯度的代码
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])

        # 返回计算出的梯度
        return grads


if __name__ == '__main__':
    # 获得MNIST数据集
    (x_train, t_train), (x_test, t_test) = load_mnist(one_hot_label=True)

    # 定义训练循环迭代次数
    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 = []

    # 计算一个epoch所需的训练迭代次数(一个epoch定义为所有训练数据都遍历过一次所需的迭代次数)
    iter_per_epoch = 1
    MyTwoLayerNet = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)  # 实例化TwoLayerNet类创建MyTwoLayerNet对象
    #训练循环的代码
    for i in range(iters_num):
        # 在每次训练迭代内部选择一个批次的数据
        batch_choose = np.random.choice(train_size, batch_size)  # choice()函数的作用是从train_size中随机选出batch_size个
        x_batch = x_train[batch_choose]
        t_batch = t_train[batch_choose]

        # 请补充计算梯度的代码
        grad = MyTwoLayerNet.gradient(x_batch, t_batch)
        # 请补充更新模型参数的代码
        for params in ('W1', 'b1', 'W2', 'b2'):
            MyTwoLayerNet.params[params] -= learning_rate * grad[params]

        # 请补充向train_loss_list列表添加本轮迭代的模型损失值的代码
        loss = MyTwoLayerNet.loss(x_batch, t_batch)
        train_loss_list.append(loss)

        # 判断是否完成了一个epoch,即所有训练数据都遍历完一遍
        if i % iter_per_epoch == 0:
            # 请补充向train_acc_list列表添加当前模型对于训练集预测精度的代码
            train_acc = MyTwoLayerNet.accuracy(x_train, t_train)
            # 请补充向test_acc_list列表添加当前模型对于测试集预测精度的代码
            test_acc = MyTwoLayerNet.accuracy(x_test, t_test)
            train_acc_list.append(train_acc)
            test_acc_list.append(test_acc)
            # 输出一个epoch完成后模型分别在训练集和测试集上的预测精度以及损失值
            print("iteration:{} ,train acc:{}, test acc:{} ,loss:{}".format(i, round(train_acc, 3), round(test_acc, 3),
                                                                            round(loss, 2)))

(四) 结果分析

运行约15个小时后的结果

PyTorch mnist手写识别数字 模型推理 numpy手写数字识别_权重_03


初始精度大约为0.1,因为一共10个数字……

经过如此之长的时间训练精度也只提高到0.2

效果这样差的原因是:

① 没有用上GPU

② 梯度下降算法过于朴素,没有用上反向传播项目结构

PyTorch mnist手写识别数字 模型推理 numpy手写数字识别_数据_04

(五) 调试报告

  1. 权重矩阵的形状不知道如何设置,联想到神经网络的示意图,每层网络之间有多条连线,每个连线对应一个权重,故权重矩阵设置为上一层神经元个数X下一层神经元个数,即
# 第一层的权重
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
# 第二层的权重
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
  1. 训练太慢
    将每一次的训练批数设置为1,即
# 计算一个epoch所需的训练迭代次数(一个epoch定义为所有训练数据都遍历过一次所需的迭代次数)
iter_per_epoch = 1
  1. 写计算梯度的函数时应当注意每算完一个梯度要将矩阵还原

(六) 实验小结

本次实验没有通过深度学习框架,而是主要依靠numpy库构建了一个含有一个隐藏层的神经网络,将交叉熵作为损失函数,在mnist训练集和测试集上分别求精度,更深刻地体会到了神经网络的运作原理,可以改进的地方有:

  1. 梯度下降方式过于朴素,导致每次训练时间开销大
  2. 神经网络的形状是写死的,不利于扩展

此外我加深了对机器学习的认识,无论那种类型的神经网络,本质上都可以看作是一个数学函数,其输入输出由具体情景决定,函数的框架在训练前就已经设定好,训练的本质是是通过输出与目标值的比对,不断修改这个函数的参数值,直到输出与目标的差距很小,就可以认为得到了一个较好的模型。