文章目录
(一) 问题描述(二) 设计简要描述(三) 程序清单(四) 结果分析(五) 调试报告(六) 实验小结(一) 问题描述
不使用任何机器学习框架,仅仅通过Numpy库构建一个最简单的全连接前馈神经网络,并用该网络识别mnist提供的手写数字体。
(二) 设计简要描述
机器学习的三个基本步骤——
程序设计思路——(此图放大可看清)
(三) 程序清单
import numpy as npfrom 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_areturn y# 损失函数# y是神经网络的输出,t是正确解标签def cross_entropy_error(y, t):delta = 1e-7return -np.sum(t * np.log(y + delta))# 计算并返回数值微分值的代码def numerical_gradient(f, x):# f是函数,x是自变量h = 1e-4 # 0.0001grad = 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 + hfh1 = f(x)# f(x-h)的计算x[idx] = tmp_val - hfh2 = f(x)grad[idx] = (fh1 - fh2) / (2 * h)x[idx] = tmp_val # 还原,因为这里只是计算一下梯度,需要暂时修改x矩阵it.iternext()return gradclass 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) + b1h1 = sigmoid(z1)# 请实现第二层的运算h2 = np.dot(h1, W2) + b2 # 注意,第二层的输入x是第一层的输出h1y = softmax(h2) # 第二层的激活函数是softmax而不是sigmoidreturn 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 gradsif __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 = 1MyTwoLayerNet = 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个小时后的结果
初始精度大约为0.1,因为一共10个数字……
经过如此之长的时间训练精度也只提高到0.2
效果这样差的原因是:
① 没有用上GPU
② 梯度下降算法过于朴素,没有用上反向传播
项目结构
(五) 调试报告
权重矩阵的形状不知道如何设置,联想到神经网络的示意图,每层网络之间有多条连线,每个连线对应一个权重,故权重矩阵设置为上一层神经元个数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,即
# 计算一个epoch所需的训练迭代次数(一个epoch定义为所有训练数据都遍历过一次所需的迭代次数)iter_per_epoch = 1
写计算梯度的函数时应当注意每算完一个梯度要将矩阵还原
(六) 实验小结
本次实验没有通过深度学习框架,而是主要依靠numpy库构建了一个含有一个隐藏层的神经网络,将交叉熵作为损失函数,在mnist训练集和测试集上分别求精度,更深刻地体会到了神经网络的运作原理,可以改进的地方有:
梯度下降方式过于朴素,导致每次训练时间开销大神经网络的形状是写死的,不利于扩展
此外我加深了对机器学习的认识,无论那种类型的神经网络,本质上都可以看作是一个数学函数,其输入输出由具体情景决定,函数的框架在训练前就已经设定好,训练的本质是是通过输出与目标值的比对,不断修改这个函数的参数值,直到输出与目标的差距很小,就可以认为得到了一个较好的模型。