完整代码: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)
# 划分为59000张训练集和1000张验证集
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}")

测试,绘图等等这里就不赘述了。