完整代码:https://github.com/JIANG54864/ml_exp/tree/main/exp3
数据集较大,请通过网盘下载。
【实验内容及说明】
MNIST数据集是一个大型手写数字数据集。通常用于训练各种图像处理系统。该数据集还广泛用于机器学习领域的培训和测试。MNIST手写数字识别模型的主要任务是:输入一张手写数字的图像,然后识别图像中手写的是哪个数字。
mnist_train.csv文件包含60,000个训练实例和标签。mnist_test.csv包含10,000个测试例子和标签。数据集中每一行由785个值组成:第一个值是标签(从0到9的数字),其余784个值是每个位置的像素值(从0到255的数字)。
可将训练样本划分为59000 张样本的训练集和1000 张样本的验证集,测试集可只取1000 张测试样本。其中每个样本表示 28×28 像素的灰度照片,所有照片分属10个不同的类别。
(1) 用神经网络对给定的数据集进行分类,画出loss图,给出在测试集上的精确度;
(2) 不能使用 pytorch 等框架,也不能使用库函数,所有算法都要自己实现;
(3)整个神经网络包括 3 层——输入层,隐藏层,输出层。输入层有 28*28个神经元,隐藏层有 50个神经元,输出层有 10 个神经元(对应 10 个类别)。
(4) 附加:可以试着修改隐藏层神经元数,层数,学习率,正则化权重等参数探究参数对实验结果的影响。
【正文】
先处理一下数据,测试集同理
1 2 3 4 5 6 7 8 9
| data_file = open("input/mnist_train.csv", "r") data_list = data_file.readlines()[1:] data_file.close() np.random.shuffle(data_list) train_data = data_list[:59000] validation_data = data_list[59000:60000]
|
重点:定义一个神经网络的类。每个样本表示 28×28 像素的灰度照片,故输入层应该有 28*28个神经元。所有照片分属10个不同的类别(10个数字),故输出层应该有 10 个神经元。隐藏层神经元可以灵活一点,先用题目给的50个好了。除此之外还应该有激活函数、权重矩阵等等。激活函数我分别用经典的sigmod和relu函数做了测试。链接权重,使用了正态分布随机数进行初始化,均值为 0,标准差为隐藏层节点数的平方根的倒数,确保权重值范围与隐藏层节点数成反比,避免梯度爆炸或消失。最后一个参数为矩阵形状。
1 2 3 4 5 6 7 8
| def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate): self.inodes = inputnodes self.hnodes = hiddennodes self.onodes = outputnodes self.lr = learningrate self.activation_func = lambda x: 1 / (1 + np.exp(-x)) self.wih = np.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes)) self.who = np.random.normal(0.0, pow(self.hnodes, -0.5), (self.onodes, self.hnodes))
|
在一次计算中,通过前向传播计算隐藏层和输出层的输出,接着计算输出误差和损失,并通过反向传播更新权重,其中y * (1 - y)为sigmoid函数y(x)的导数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| def train(self, input_list, target_list): inputs = np.array(input_list, ndmin=2).T targets = np.array(target_list, ndmin=2).T hidden_inputs = np.dot(self.wih, inputs) hidden_outputs = self.activation_func(hidden_inputs) final_inputs = np.dot(self.who, hidden_outputs) final_output = self.activation_func(final_inputs) output_errors = targets - final_output loss = np.sqrt(np.sum(np.square(final_output - targets)) / len(final_output)) hidden_errors = np.dot(self.who.T, output_errors) self.who += self.lr * np.dot((output_errors * final_output * (1.0 - final_output)), np.transpose(hidden_outputs)) self.wih += self.lr * np.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), np.transpose(inputs)) return loss
|
如果使用relu函数,相应部分修改为:
1 2 3 4 5
| output_gradient = final_output > 0 hidden_gradient = hidden_outputs > 0
self.who += self.lr * np.dot((output_errors * output_gradient),np.transpose(hidden_outputs)) self.wih += self.lr * np.dot((hidden_errors * hidden_gradient), np.transpose(inputs))
|
一轮训练,将输入数据归一化,targets将正确数字对应位置值设为0.99,其余元素设为0.01,并计算此轮训练的平均损失函数
1 2 3 4 5 6 7 8 9 10
| epoch_loss = 0 for record in train_data: all_values = record.split(',') inputs = (np.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01 targets = np.zeros(output_nodes) + 0.01 targets[int(all_values[0])] = 0.99 loss = mynet.train(inputs, targets) epoch_loss += loss avg_loss = epoch_loss / len(train_data) epoch_losses.append(avg_loss)
|
每轮训练后,在验证集上评估模型,借此可以调整超参数以及决定何时停止训练。我修改隐藏层神经元数和学习率,显然,隐藏层神经元数越多,拟合能力越强,收敛速度越慢,资源消耗也越大;学习率越高,收敛速度越快;如果隐藏层神经元数太多,会导致过拟合的问题,学习率太高,可能会振荡而无法收敛。训练10轮后已经达到很高的准确率,此后再训练到20轮提升很小。增加一层隐藏层,学习率需要降低一个数量级,否则直接就跳过最优解了。
1 2 3 4 5 6 7 8 9 10 11 12 13
| correct = 0 for record in validation_data: all_values = record.split(',') inputs = (np.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01 targets = np.zeros(output_nodes) + 0.01 targets[int(all_values[0])] = 0.99 predict_result_percent = mynet.query(inputs) predict_result = predict_result_percent.argmax() if predict_result == int(all_values[0]): correct += 1 accuracy = correct / len(validation_data) validation_accuracies.append(accuracy) print(f"Epoch {i+1}, Loss: {avg_loss:.4f}, Validation Accuracy: {accuracy:.4f}")
|
测试,绘图等等这里就不赘述了。