目录
前面的话
阅前必须看!~~~
这个理解方式是石林老师教的,我觉得很通俗,若不是做课程设计的读者需要读完正文再去看题目,另外这个题目的答案我都是分步骤给的,解释在后面,希望能够起到帮助大家更深刻理解神经网络。完整代码放在了后面的文章
目的是抛砖引玉,不太建议直接抄。
为爱发电,多点赞可能会导致我更新未来你想要的内容。
正文
1、实验目的
掌握神经网络的设计原理,熟练掌握神经网络的训练和使用方法,能够使用Python语言,针对手
写数字分类的训练和使用,实现一个三层全连接神经网络模型。具体包括:
1)实现三层神经网络模型来进行手写数字分类,建立一个简单而完整的神经网络工程。通过本实
验理解神经网络中基本模块的作用和模块间的关系,为后续建立更复杂的神经网络实验奠定基础。
2)利用Python实现神经网络基本单元的前向传播(正向传播)和反向传播,加深对神经网络中基
本单元的理解,包括全连接层、激活函数、损失函数等基本单元。
3)利用Python实现神经网络的构建和训练,实现神经网络所使用的梯度下降算法,加深对神经网
络训练过程的理解。
2、背景知识
3.1 神经网络的组成
一个完整的神经网络通常由多个基本的网络层堆叠而成。本实验中的三层全连接神经网络由三
个全连接层构成,在每两个全连接层之间插入 ReLU 激活函数以引入非线性变换,最后使用 Softmax 层
计算交叉熵损失,如图 3.1 所示。因此本实验中使用的基本单元包括全连接层、 ReLU 激活函数、
Softmax 损失函数。
1. 全连接层
全连接层以一维向量作为输入,输入与权重相乘后再与偏置相加得到输出向量。假设全连接层
的输入为 m 维列向量 x,输出为 n 维列向量 y。
图 3.1 用于手写数字分类的三层全连接神经网络
全连接层的权重 W 是二维矩阵,维度为 m×n,偏置 b 是 n 维列向量。前向传播时,全连接层的输
出的计算公式为(注意偏置可以是向量,计算每一个输出使用不同的值;偏置也可以是一个标量,计算
同一层的输出使用同一个值)
实际应用中通常使用批量(batch)随机梯度下降算法,即选择若干个样本同时计算。假设选择的样
本量为 p,此时输入变为二维矩阵 X, 维度为 p× m, 每行代表一个样本,每个样本对应公式(3.1)中的
𝒙𝑻。输出也变为二维矩阵 Y,维度为 p× n。 此时全连接层的前向传播计算公式由公式(3.1)变为(需要
说明的是,批量处理时输入 X 和输出 Y 的每一行代表一个样本,分别对应公式(3.1)中的 x 和 y 的转
置,因此公式(33.3)和公式(3.4)的形式与公式(3.1)和(3.2)略有不同)
2. ReLU 激活函数
ReLU 激活函数是按元素运算操作,输出向量 y 的维度与输入向量 x 的维度相同。在前向传播中,
如果输入 x 中的元素小于 0, 输出为 0, 否则输出等于输入。因此 ReLU 的计算公式为:
3. Softmax 损失层
Softmax 损失层是目前多分类问题中最常用的损失函数,假设 Softmax 损失层的输入为向量𝑥, 维度为
𝑘。 其中𝑘对应分类的类别数, 例如对手写数字进行分类时, 𝑘 = 10。 在前向传播的计算过程中,首先对𝑥计
算𝑒指数并进行行归一化,从而得到 Softmax 分类概率。
3.2 神经网络训练
神经网络训练通过调整网络层的参数来使神经网络计算出来的结果与真实结果(标记)尽量接近。神
经网络训练通常使用随机梯度下降算法,通过不断的迭代计算每层参数的梯度,利用梯度对每层参数进行
更新。具体而言,给定当前迭代的训练样本(包含输入数据及标记信息),首先进行神经网络的前向传播处
理,输入数据和权重相乘再经过激活函数计算出隐层,隐层与下一层的权重相乘再经过激活函数得到下一
个隐层,通过逐层迭代计算出神经网络的输出结果。随后利用输出结果和标记信息计算出损失函数值。然
后进行神经网络的反向传播处理,从损失函数开始逆序逐层计算损失函数对权重和偏置的偏导(即梯度),
最后利用梯度对相应的参数进行更新。更新参数 W 的计算公式为
图 3.2 两层神经网络示例
其中 h、 z 分别是第一、第二层全连接层的输出。 Softmax 损失的损失值 L 为:
3、 示例代码
1) 数据集
数据集采用 MNIST 手写数字库(老师直接提供,也可在 http://yann.lecun.com/exdb/mnist/自行下
载)。 该数据集包含一个训练集和一个测试集,其中训练集有 60000 个样本,测试集有 10000 个样本。
每个样本都由灰度图像(即单通道图像)及其标记组成,图像大小为 28×28。 MNIST 数据集包含 4 个
文件,分别是训练集图像、训练集标记、测试集图像、测试集标记。
2) 总体设计
设计一个三层神经网络实现手写数字图像分类。该网络包含两个隐层和一个输出层,其中输入神
经元个数由输入数据维度决定,输出层的神经元个数由数据集包含的类别决定,两个隐层的神经元个数
可以作为超参数自行设置。对于手写数字图像的分类问题,输入数据为手写数字图像,原始图像一般
可表示为二维矩阵(灰度图像)或三维矩阵(彩色图像) ,在输入神经网络前会将图像矩阵调整为一维
向量作为输入。待分类的类别数一般是提前预设的,如手写数字包含 0 至 9 共 10 个类别,则神经网络
的输出神经元个数为 10。
为了便于迭代开发,工程实现时采用模块化的方式来实现整个神经网络的处理,共划分为5大
模块:
1)数据加载模块:从文件中读取数据,并进行预处理,其中预处理包括归一化、维度变换等
处理。如果需要人为对数据进行随机数据扩增,则数据扩增处理也在数据加载模块中实现。
2)基本单元模块:实现神经网络中不同类型的网络层的定义、前向传播、反向传播等功能。
3)网络结构模块:利用基本单元模块建一个完整的神经网络。
4)网络训练( training)模块:用训练集对神经网络进行训练。对建立的神经网络结构,实
现神经网络的前向传播、神经网络的反向传播、对神经网络进行参数更新、保存神经网络参数等
基本操作,以及训练函数主体。
5)网络推断( inference)模块:使用训练得到的网络模型,对测试样本进行预测(也称为
测试或推断)。具体操作包括加载训练得到的模型参数、神经网络的前向传播等。
3) 数据加载模块
本实验釆用的数据集是MNIST手写数字库。该数据集中的图像数据和标记数据采用表2.1中的
IDX文件格式存放。图像的像素值按行优先顺序存放,取值范围为[0,255] ,其中0表示黑色, 255
表示白色。
表 3.1 MNIST 数据集 IDX 文件格式
首先编写读取 MNIST 数据集文件并预处理的子函数,程序示例如图 3.3 所示。然后调用该子函数
对 MN1ST 数据集中的 4 个文件分别进行读取和预处理,并将处理过的训练和测试数据存储在 NumPy
矩阵中(训练模型时可以快速读取该矩阵中的数据),实现该功能的程序示例如图 3.4 所示。
1. def load_mnist(self, file_dir, is_images = 'True'):
2. bin_file = open(file_dir, 'rb')
3. bin_data = bin_file.read()
4. bin_file.close()
5. # 分析文件头部
6. if is_images: # 读取图像数据
7. fmt_header = '>iiii'
8. magic, num_images, num_rows, num_cols = struct.unpack_from(fmt_header, bin_data, 0
)
9. else: # 读取标记数据
10. fmt_header = '>ii'
11. magic, num_images = struct.unpack_from(fmt_header, bin_data, 0)
12. num_rows, num_cols = 1, 1
13. data_size = num_images * num_rows * num_cols
14. mat_data = struct.unpack_from('>' + str(data_size) + 'B', bin_data, struct.calcsize(fm
t_header))
15. mat_data = np.reshape(mat_data, [num_images, num_rows * num_cols])
16. print('Load images from %s, number: %d, data shape: %s' % (file_dir, num_images, str(m
at_data.shape)))
17. return mat_data
图 3.3 MNIST 数据集文件的读取和预处理
1. def load_data(self):
2. # TODO: 调用函数 load_mnist 读取和预处理 MNIST 中训练数据和测试数据的图像和标记
3. print('Loading MNIST data from files...')
4. train_images = self.load_mnist(os.path.join(MNIST_DIR, TRAIN_DATA), True)
5. train_labels = _____________________________
6. test_images = _____________________________
7. test_labels = _____________________________
8. self.train_data = np.append(train_images, train_labels, axis=1)
9. self.test_data = np.append(test_images, test_labels, axis=1)
图 3.4 MNIST 子数据集的读取和预处理
TODO 提示:代码中已有如下定义,直接按照 train_images 的代码套用即可:
TRAIN_DATA = "train-images-idx3-ubyte"
TRAIN_LABEL = "train-labels-idx1-ubyte"
TEST_DATA = "t10k-images-idx3-ubyte"
TEST_LABEL = "t10k-labels-idx1-ubyte"
4) 基本单元模块
本实验采用图 3.1 中的三层神经网络,主体是三个全连接层。在前两个全连接层之后使用 ReLU 激活函
数层引入非线性变换,本实验采用 ReLU 层作为激活函数层。在神经网络的最后添加 Softmax 层计算交叉熵
损失。因此,本实验中需要实现的基本单元模块包括全连接层、 ReLU 层和 Softmax 损失层。
在神经网络实现中,通常同类型的层用一个类来定义,多个同类型的层用类的实例来实现,层中的计
算用类的成员函数来定义。类的成员函数通常包括层的初始化、参数的初始化、前向传播计算、反向传播
计算、参数的更新、参数的加载和保存等。其中层的初始化函数一般会根据实例化层时的输入系数确定该
层的超参数,例如该层的输入神经元数量和输出神经元数量等。参数的初始化函数会对该层的参数(如全
连接层中的权重和偏置)分配存储空间,并填充初始值。前向传播函数利用前一层的输出作为本层的输入,
计算本层的输出结果、反向传播函数根据链式法则逆序逐层计算损失函数对权重和偏置的梯度。参数的更
新函数利用反向传播函数计算的梯度对本层的参数进行更新。参数的加载函数从给定的文件中加载参数的
值,参数的保存函数将当前层参数的值保持到指定的文件中。有些层(如激活函数层)可能没有参数,就
不需要定义参数的初始化、更新、加载和保存函数。有些层(如激活函数层和损失函数层)的输出维度由
输入维度决定,不需要人工设定,因此不需要层的初始化函数。
以下便是全连接层、 ReLU 层和 Softmax 损失层的具体实现步骤。
1) 全连接层:程序示例如图 3.5 所示,定义了以下成员函数。
•层的初始化:需要确定该全连接层的输入神经元个数(即输入二维矩阵中每个行向量的维度)和输出
神经元个数(即输出二维矩阵中每个行向量的维度)。
•参数初始化;全连接层的参数包括权重和偏置。根据输入向量的维度 m 和输出向量的维度 n 可以确定
权重 W 的维度为 m× n,偏置 b 的维度为 n。在对权重和偏置进行初始化时,通常利用高斯随机数初始化
权重的值,而将偏置的所有值初始化为 0。
•前向传播计算:全连接层的前向传播计算公式为(3.3),可以通过输入矩阵与权重矩阵相乘再与偏置相加
实现。
•反向传播计算: 全连接层的反向传播的计算公式为(3.4)。给定损失函数对本层输出的偏导𝛻𝑦𝐿, 利用矩
阵相乘计算权重和偏置的梯度𝛻𝑊𝐿、 𝛻𝑏𝐿以及损失函数对本层输入的偏导𝛻𝑥𝐿。
•参数更新:给定学习率𝜂,可利用反向传播计算得到的权重梯度𝛻𝑊𝐿和偏置梯度𝛻𝑏𝐿对本层的权重 W 和偏
置 b 进行更新:
𝑊 = 𝑊 - 𝜂𝛻𝑊𝐿
𝑏 = 𝑏 - 𝜂𝛻𝑏𝐿
•参数加载: 从该函数的输入中读取本层的权重 W 和偏置 b。
•参数保存: 返回本层当前的权重 W 和偏置 b。
1. class FullyConnectedLayer(object):
2. def __init__(self, num_input, num_output): # 全连接层初始化
3. self.num_input = num_input
4. self.num_output = num_output
5. print('\tFully connected layer with input %d, output %d.' % (self.num_input, self.
num_output))
6. def init_param(self, std=0.01): # 参数初始化
7. self.weight = np.random.normal(loc=0.0, scale=std, size=(self.num_input, self.num_
output))
8. self.bias = np.zeros([1, self.num_output])
9. def forward(self, input): # 前向传播计算
10. start_time = time.time()
11. self.input = input
12. # TODO:全连接层的前向传播,计算输出结果 提示:公式 3.1,矩阵乘法 np.matmul
13. self.output = _____________________________
14. return self.output
15. def backward(self, top_diff): # 反向传播的计算 大水漫灌, top_diff 就是上一层房间的海水
16. # TODO:全连接层的反向传播,计算参数梯度和本层损失
17. self.d_weight = _____________________________ # 提示: 公式 3.4
18. self.d_bias = _____________________________
19. bottom_diff = _____________________________
20. return bottom_diff # 大水漫灌, bottom_diff 就是传到下一层房间的海水
21. def update_param(self, lr): # 参数更新
22. # TODO:对全连接层参数利用参数进行更新 提示:公式 3.14
23. self.weight = _____________________________
24. self.bias = _____________________________
25. def load_param(self, weight, bias): # 参数加载
26. assert self.weight.shape == weight.shape
27. assert self.bias.shape == bias.shape
28. self.weight = weight
29. self.bias = bias
30. def save_param(self): # 参数保存
31. return self.weight, self.bias
图 3.5 全连接层的实现示例
2) ReLU 层:不包含参数,因此实现中没有参数初始化、参数更新、参数的加载和保存相关的函数。
ReLU 层的程序示例如图 3.6 所示,定义了以下成员函数。
•前向传播计算: 根据公式(3.5)可以计算 ReLU 层前向传播的结果。在工程实现中, 可以对整个输入矩阵
使用 maximum 函数, maximum 函数会进行广播,计算输入矩阵的每个元素与 0 的最大值。
•反向传播计算: 根据公式(3.6)可以计算损失函数对输入的偏导。在工程实现中,可以获取 x(i)<0 的位置
索引,将 y 中对应位置的值置为 0。
1. class ReLULayer(object):
2. def __init__(self):
3. print('\tReLU layer.')
4. def forward(self, input): # 前向传播的计算
5. start_time = time.time()
6. self.input = input
7. # TODO: ReLU 层的前向传播,计算输出结果 提示:公式 3.5
8. output = _____________________________
9. return output
10. def backward(self, top_diff): # 反向传播的计算
11. # TODO: ReLU 层的反向传播,计算本层损失 提示:公式 3.6
12. bottom_diff = _____________________________
13. bottom_diff[self.input<0] = 0
14. return bottom_diff
图 3.6 ReLU 层的实现示例
3) Softmax 损失层:同样不包含参数,因此实现中没有参数初始化、更新、加载和保存相关的函数。
但该层需要额外计算总的损失函数值,作为训练时的中间输出结果,帮助判断模型的训练进程。 Softmax 损
失层的程序示例如图 3.7 所示,定义了以下成员函数。
•前向传播计算:使用公式(3.11)计算,该公式为确保数值稳定,会在求 e 指数前先进行减最大值处理。
•损失函数计算;使用公式(3.12)计算,釆用批量随机梯度下降法训练时损失值是 batch 内的所有样本的
损失值的均值。需要注意的是, MNIST 手写数字库的标记数据读入的是 0 至 9 的类别编号, 计算损失时
需要先将类别编号转换为 one-hot 向量。
•反向传播计算: 可以使用公式(3.13)计算,计算时同样需要对样本数量取平均。
1. class SoftmaxLossLayer(object):
2. def __init__(self):
3. print('\tSoftmax loss layer.')
4. def forward(self, input): # 前向传播的计算
5. # TODO: softmax 损失层的前向传播,计算输出结果 提示: 公式 3.7
6. input_max = np.max(input, axis=1, keepdims=True)
7. input_exp = np.exp(input - input_max)
8. self.prob = _____________________________
9. return self.prob
10. def get_loss(self, label): # 计算损失
11. self.batch_size = self.prob.shape[0]
12. self.label_onehot = np.zeros_like(self.prob)
13. self.label_onehot[np.arange(self.batch_size), label] = 1.0
14. loss = -np.sum(np.log(self.prob) * self.label_onehot) / self.batch_size
15. return loss
16. def backward(self): # 反向传播的计算
17. # TODO: softmax 损失层的反向传播,计算本层损失 提示:公式 3.13
18. bottom_diff = _____________________________
19. return bottom_diff
图 3.7 Softmax 损失层的实现示例
5) 网络结构模块
网络结构模块利用己经实现的神经网络的基本单元来建立一个完整的神经网络。在工程实现
中通常用一个类来定义一个神经网络,用类的成员函数来定义神经网络的初始化、建立神经网络
结构、对神经网络进行参数初始化等基本操作。本实验中三层神经网络的网络结构模块的程序示
例如图3.8所示,定义了以下成员函数。
•神经网络初始化:确定神经网络相关的超参数,例如网络中每个隐层的神经元个数。
•建立网络结构:定义整个神经网络的拓扑结构,实例化基本単元模块中定义的层并将这些层
进行堆叠。例如本实验使用的三层神经网络包含三个全连接层,并且在前两个全连接层后跟随有
ReLU层,神经网络的最后使用了Softmax损失层。
•神经网络参数初始化:对于神经网络中包含参数的层,依次调用这些层的参数初始化函数,
从而完成整个神经网络的参数初始化。本实验使用的三层神经网络中,只有三个全连接层包含参
数,依次调用其参数初始化函数即可。
1. class MNIST_MLP(object):
2. def __init__(self, batch_size=100, input_size=784, hidden1=32, hidden2=16, out_classes
=10, lr=0.01, max_epoch=2, print_iter=100):
3. # 神经网络初始化
4. self.batch_size = batch_size
5. self.input_size = input_size
6. self.hidden1 = hidden1
7. self.hidden2 = hidden2
8. self.out_classes = out_classes
9. self.lr = lr
10. self.max_epoch = max_epoch
11. self.print_iter = print_iter
12. def build_model(self): # 建立网络结构
13. # TODO:建立三层神经网络结构
14. print('Building multi-layer perception model...')
15. self.fc1 = FullyConnectedLayer(self.input_size, self.hidden1)
16. self.relu1 = ReLULayer()
17. _____________________________
18. self.fc3 = FullyConnectedLayer(self.hidden2, self.out_classes)
19. self.softmax = SoftmaxLossLayer()
20. self.update_layer_list = [self.fc1, self.fc2, self.fc3]
21.
22. def init_model(self):
23. print('Initializing parameters of each layer in MLP...')
24. for layer in self.update_layer_list:
25. layer.init_param()
图 3.8 三层神经网络的网络结构模块实现示例
6) 网络训练( training)模块
神经网络训练流程如图3.9所示。在完成数据加载模块和网络结构模块实现之后,需要实现训
练模块。本实验中三层神经网络的网络训练模块程序示例如图3.10所示。神经网络的训练模块通
常拆解为若干步骤,包括神经网络的前向传播、神经网络的反向传播、神经网络参数更新、神经
网络参数保存等基本操作。这些网络训练模块的基本操作以及训练主体用神经网络类的成员函数
来定义:
图3.9 神经网络训练流程
神经网络的前向传播:根据神经网络的拓扑顺序,顺序调用每层的前向传播函数。以输入数
据作为第一层的输入,之后每层的输出作为其后一层的输入顺序计算每一层的输出,最后得到损
失函数层的输出。
•神经网络的反向传播:根据神经网络的拓扑顺序,逆序调用每层的反向传播函数。釆用链式
法则逆序逐层计算损失函数对每层参数的偏导,最后得到神经网络所有层的参数梯度。
•神经网络参数更新:对神经网络中包含参数的层,依次调用各层的参数更新函数,来对整个
神经网络的参数进行更新。本实验中的三层神经网络仅其中的三个全连接层包含参数,依次更新
三个全连接层的参数即可。
•神经网络参数保存:对神经网络中包含参数的层,依次收集这些层的参数并存储到文件中。
•神经网络训练主体:在该函数中,( 1)确定训练的一些超参数,如使用批量梯度下降算法
时的批量大小、学习率大小、迭代次数(或训练周期次数)、可视化训练过程时每迭代多少次屏
幕输出一次当前的损失值等等。( 2)开始迭代训练过程。每次迭代训练开始前,可以根据需要对
数据进行随机打乱,一般是一个训练周期(即当整个数据集的数据都参与一次训练过程)后对数
据集进行随机打乱。每次迭代训练过程中,先选取当前迭代所使用的数据和对应的标记,再进行
整个网络的前向传播,随后计算当前迭代的损失值,然后进行整个网络的反向传播来获得整个网
络的参数梯度,最后对整个网络的参数进行更新。完成一次迭代后可以根据需要在屏幕上输出当
前的损失值,以供实际应用中修改模型作参考。完成神经网络的训练过程后,通常会将训练得到的
神经网络模型参数保存到文件中。
1. def forward(self, input): # 神经网络的前向传播
2. # TODO:神经网络的前向传播
3. h1 = self.fc1.forward(input)
4. h1 = self.relu1.forward(h1)
5. _____________________________
6. prob = self.softmax.forward(h3)
7. return prob
8.
9. def backward(self): # 神经网络的反向传播
10. # TODO:神经网络的反向传播
11. dloss = self.softmax.backward()
12. _____________________________
13. dh1 = self.relu1.backward(dh2)
14. dh1 = self.fc1.backward(dh1)
15.
16. def update(self, lr): # 神经网络的参数更新
17. for layer in self.update_layer_list:
18. 19. | layer.update_param(lr) |
20. def train(self): # 训练函数
21. max_batch = self.train_data.shape[0] // self.batch_size
22. print('Start training...')
23. for idx_epoch in range(self.max_epoch):
24. self.shuffle_data()
25. for idx_batch in range(max_batch):
26. batch_images = self.train_data[idx_batch*self.batch_size:(idx_batch+1)*self.ba
tch_size, :-1]
27. batch_labels = self.train_data[idx_batch*self.batch_size:(idx_batch+1)*self.ba
tch_size, -1]
28. prob = self.forward(batch_images)
29. loss = self.softmax.get_loss(batch_labels)
30. self.backward()
31. self.update(self.lr)
32. if idx_batch % self.print_iter == 0:
33. print('Epoch %d, iter %d, loss: %.6f' % (idx_epoch, idx_batch, loss))
图 3.10 三层神经网络的网络训练模块实现示例
7) 网络推断(inference)模块
整个神经网络推断流程如图3.11所示。完成神经网络的训练之后,可以用训练得到的模型对
测试数据进行预测,以评估模型的精度。本实验中三层神经网络的网络推断模块程序示例如图
3.12所示。工程实现中同样常将一个神经网络的推断模块拆解为若干步骤,包括神经网络模型参
数加载、前向传播、精度计算等基本操作。这些网络推断模块的基本操作以及推断主体用神经网
络类的成员函数来定义:
•神经网络的前向传播:网络推断模块中的神经网络前向传播操作与网络训练模块中的前向传
播操作完全一致,因此可以直接调用网络训练模块中的神经网络前向传播函数。
•神经网络参数加载:读取神经网络训练模块保存的模型参数文件,并加载有参数的网络层的
参数值。
•神经网络推断函数主体:在进行神经网络推断前,需要从模型参数文件中加载神经网络的参
数。在神经网络推断过程中,循环每次读取一定批量的测试数据,随后进行整个神经网络的前向
传播计算得到神经网络的输出结果。得到整个测试数据集的输出结果后,与测试数据集的标记进
行比对,利用相关的评价函数计算模型的精度,如手写数字分类问题使用分类平均正确率作为模
型的评函数。
图 3.11 神经网络推断流程
1. def load_model(self, param_dir): # 加载神经网络权值
2. print('Loading parameters from file ' + param_dir)
3. params = np.load(param_dir, allow_pickle=True).item()
4. self.fc1.load_param(params['w1'], params['b1'])
5. self.fc2.load_param(params['w2'], params['b2'])
6. self.fc3.load_param(params['w3'], params['b3'])
7.
8. def evaluate(self): # 推断函数
9. pred_results = np.zeros([self.test_data.shape[0]])
10. start_time = time.time()
11. for idx in range(self.test_data.shape[0]//self.batch_size):
12. batch_images = self.test_data[idx*self.batch_size:(idx+1)*self.batch_size, :-1]
13. prob = self.forward(batch_images)
14. end = time.time()
15. pred_labels = np.argmax(prob, axis=1)
16. pred_results[idx*self.batch_size:(idx+1)*self.batch_size] = pred_labels
17. print("All evaluate time: %f"%(time.time()-start_time))
18. accuracy = np.mean(pred_results == self.test_data[:,-1])
19. print('Accuracy in test set: %f' % accuracy)
图 3.12 三层神经网络的网络推断模块实现示例
8) 完整实验流程
完成神经网络的各个模块之后,调用这些模块就可以实现用三层神经网络进行手写数字图像
分类的完整流程。本实验中三层神经网络的完整流程的程序示例如图3.13所示。首先实例化三层
神经网络对应的类,指定神经网络的超参数,如每层的神经元个数。其次进行数据的加载和预处
理。再调用网络结构模块建立神经网络,随后进行网络初始化,在该过程中网络结构模块会自动
调用基本单元模块实例化神经网络中的每个层。然后调用网络训练模块训练整个网络,之后将训
练得到的模型参数保存到文件中。最后从文件中读取训练得到的模型参数,之后调用网络推断模
块测试网络的精度。
1. if __name__ == '__main__':
2. h1, h2, e = 32, 16, 1
3. mlp = MNIST_MLP(hidden1=h1, hidden2=h2, max_epoch=e)
4. mlp.load_data()
5. mlp.build_model()
6. mlp.init_model()
7. start_time = time.time()
8. mlp.train()
9. print("All train time: %f"%(time.time()-start_time))
10. mlp.save_model('mlp-%d-%d-%depoch.npy' % (h1, h2, e))
11. mlp.load_model('mlp-%d-%d-%depoch.npy' % (h1, h2, e))
12. mlp.evaluate()
图 3.13 三层神经网络的完整流程实现示例
9) 实验评估
在图像分类任务中,通常使用测试集的平均分类正确率判断分类结果的精度。假设共有N个图
像样本(MNIST手写数据集中共包含10000张测试图像,此时N=10000) ,𝑏𝑚𝑝𝑖为神经网络输出的第i
张图像的预测结果, 𝑝𝑖为一个向量,取其中最大分量对应的类别作为预测类别。假设第i张图像的
标记为𝑦𝑖,即第i张图像属于类别𝑦𝑖,则计算平均分类正确率R的公式为
其中1(𝑎𝑟𝑔𝑚𝑎𝑥(𝑝𝑖) = 𝑦𝑖)代表当𝑝𝑖中的最大分量对应的类别编号与𝑦𝑖相等时值为1,否则值为
0。
4、实验内容
1) 请在代码中有TODO的地方填空,将程序补充完整,在报告中写出相应代码,并给出自己的
理解。
2) mlp.load_data()执行到最后时, train_images、 train_labels、 test_images、 test_labels 的
维度是多少?即多少行多少列,用(x,y)来表示。 self.train_data 和 self.test_data 的维度是多少?
3) 本案例中的神经网络一共有几层?每层有多少个神经元?如果要增加或减少层数,应该怎么
做(简单描述即可不用编程)?如果要增加或减少某一层的节点,应该怎么做(简单描述)?如果要
把 softmax 换成 sigmoid,应该怎么做(简单描述) ? 这种替换合理么?
4) 在 train()函数中, max_batch = self.train_data.shape[0] // self.batch_size 这一句的意义
是什么? self.shuffle_data()的意义是什么?
5) 最终 evaluate()函数输出的 Accuracy in test set 是多少?请想办法提高该数值。 本小题的评
估标准设定如下:
• 60 分标准:给定全连接层、 ReLU 层、 Softmax 损失层的前向传播的输入矩阵、参数值、反向传
播的输入,可以得到正确的前向传播的输出矩阵、反向传播的输出和参数梯度。
• 80 分标准:实现正确的三层神经网络,并进行训练和推断,使最后训练得到的模型在 MNIST
测试数据集上的平均分类正确率高于 92%。
• 90 分标准:实现正确的三层神经网络,并进行训练和推断,调整和训练相关的超参数,使最后
训练得到的模型在 MNIST 测试数据集上的平均分类正确率高于 95%。
• 100 分标准:在三层神经网络基础上设计自己的神经网络结构,并进行训练和推断,使最后训
练得到的模型在 MN1ST 测试数据集上的平均分类正确率高于 98%。
题目答案
1) 请在代码中有 TODO 的地方填空, 将程序补充完整, 在报告中写出相应代码, 并给出自
己的理解。
答:
def load_data(self):
# TODO: 调用函数 load_mnist 读取和预处理 MNIST 中训练数据和测试数据
的图像和标记
print('Loading MNIST data from files...')
train_images = self.load_mnist(os.path.join(MNIST_DIR, TRAIN_DATA), True)
train_labels = self.load_mnist(os.path.join(MNIST_DIR, TRAIN_LABEL), False)
test_images = self.load_mnist(os.path.join(MNIST_DIR, TEST_DATA), True)
test_labels = self.load_mnist(os.path.join(MNIST_DIR, TEST_LABEL), False)
self.train_data = np.append(train_images, train_labels, axis=1)
self.test_data = np.append(test_images, test_labels, axis=1)
这一部分是数据预处理的填空代码, 目的是读取 MNIST 数据集文件并预处理,
MNIST_DIR 是程序所在根目录下的 mnist_data 路径, TRAIN_DATA 和 TRAIN_LABEL 代表训
练集合的数据和标识, TEST_DATA 和 TEST_LABEL 代表的是测试集的。 联系 load_mnist 函
数的实现, 需要确定文件是数据还是标识才能进行预处理, 所以还需要传改路径对应的文
件是否是 images 的 bool 值, 帮助函数进行 struct.unpack_from 打包处理。
class FullyConnectedLayer(object):
def __init__(self, num_input, num_output): # 全连接层初始化
self.num_input = num_input
self.num_output = num_output
print('\tFully connected layer with input %d, output %d.' % (self.num_input,
self.num_output))
def init_param(self, std=0.01): # 参数初始化
self.weight self.num_output)) | = | np.random.normal(loc=0.0, | scale=std, | size=(self.num_input, |
self.bias = np.zeros([1, self.num_output])
def forward(self, input): # 前向传播计算
start_time = time.time()
self.input = input
# TODO: 全连接层的前向传播, 计算输出结果
self.output = np.matmul(input,self.weight)+self.bias
return self.output
def backward(self, top_diff): # 反向传播的计算
# TODO: 全连接层的反向传播, 计算参数梯度和本层损失
self.d_weight = np.matmul(self.input.T,top_diff)
self.d_bias = np.sum(top_diff,axis=0)
bottom_diff = np.matmul(top_diff,self.weight.T)
return bottom_diff
def update_param(self, lr): # 参数更新
# TODO: 对全连接层参数利用参数进行更新
self.weight = self.weight - lr * self.d_weight
self.bias = self.bias - lr * self.d_bias
def load_param(self, weight, bias): # 参数加载
assert self.weight.shape == weight.shape
assert self.bias.shape == bias.shape
self.weight = weight
self.bias = bias
def save_param(self): # 参数保存
return self.weight, self.bias
这一部分是全连接层类的代码。
①对于前向传播, 使用公式 3.1:
公式中的 y 表示这一层的输出对应 output, x 是输入对应 input, W 是权值对应 weight,
b 是偏差对应 bias, 调用 np.matmul 函数进行运算, 实现公式 3.1。
②对于反向传播, 使用公式 3.4:
公式是权值的更新公式, 偏差也类似。 两者的实现就像公式代入就行。
class ReLULayer(object):
def __init__(self):
print('\tReLU layer.')
def forward(self, input): # 前向传播的计算
start_time = time.time()
self.input = input
# TODO: ReLU 层的前向传播, 计算输出结果
output = input.copy()
output[(input < 0)] = 0
return output
def backward(self, top_diff): # 反向传播的计算
# TODO: ReLU 层的反向传播, 计算本层损失
bottom_diff = top_diff
bottom_diff[self.input<0] = 0
return bottom_diff
这是 ReLU 层, 前向传播使用了公式 3.5:
这是 ReLU 函数的基本思想增强一部分数据, 摒弃一部分数据, 将小于 0 的数据都舍
弃, 置 0。 故我在实现的时候, 先复制 input 然后将其中小于 0 的置 0。
反向传播使用了公式 3.6:
原理和前向传播差不多, 由 x 决定偏导的取值。
class SoftmaxLossLayer(object):
def __init__(self):
print('\tSoftmax loss layer.')
def forward(self, input): # 前向传播的计算
# TODO: softmax 损失层的前向传播, 计算输出结果
input_max = np.max(input, axis=1, keepdims=True)
input_exp = np.exp(input - input_max)
self.prob = input_exp / np.sum(input_exp, axis=1, keepdims=True)
return self.prob
def get_loss(self, label): # 计算损失
self.batch_size = self.prob.shape[0]
self.label_onehot = np.zeros_like(self.prob)
self.label_onehot[np.arange(self.batch_size), label] = 1.0
loss = -np.sum(np.log(self.prob) * self.label_onehot) / self.batch_size
return loss
def backward(self): # 反向传播的计算
# TODO: softmax 损失层的反向传播, 计算本层损失
bottom_diff = (self.prob - self.label_onehot) / self.batch_size
return bottom_diff
这是 Softmax 损失层, 对于前向传播, 使用更加适合实际工程的公式 3.11:
input_max 和 input_exp 已经算好了, 我们只需要带入就可以了, 分子是 input_exp,
分母是 input_exp.sum, keepdims 参数是保持矩阵形状, axis=1 是对列求和。
对于反向传播使用公式 3.13 计算:
def build_model(self): # 建立网络结构
# TODO: 建立三层神经网络结构
print('Building multi-layer perception model...')
self.fc1 = FullyConnectedLayer(self.input_size, self.hidden1)
self.relu1 = ReLULayer()
self.fc2 = FullyConnectedLayer(self.hidden1, self.hidden2)
self.relu2 = ReLULayer()
self.fc3 = FullyConnectedLayer(self.hidden2, self.out_classes)
self.softmax = SoftmaxLossLayer()
self.update_layer_list = [self.fc1, self.fc2, self.fc3]
这是网络结构模块, 实验使用的是三层神经网络包含三个全连接层, 并且在前两个全
连接层后跟随有 ReLU 层, 神经网络的最后使用了 Softmax 损失层。 故需要调用三次
FullyConnectedLayer, 两次 ReLULayer, 一次 SoftmaxLossLayer。
def forward(self, input): # 神经网络的前向传播
# TODO: 神经网络的前向传播
h1 = self.fc1.forward(input)
h1 = self.relu1.forward(h1)
h2 = self.fc2.forward(h1)
h2 = self.relu2.forward(h2)
h3 = self.fc3.forward(h2)
prob = self.softmax.forward(h3)
return prob
def backward(self): # 神经网络的反向传播
# TODO: 神经网络的反向传播
dloss = self.softmax.backward()
dh3 = self.fc3.backward(dloss)
dh2 = self.relu2.backward(dh3)
dh2 = self.fc2.backward(dh2)
dh1 = self.relu1.backward(dh2)
dh1 = self.fc1.backward(dh1)
def update(self, lr): # 神经网络的参数更新
for layer in self.update_layer_list:
layer.update_param(lr)
这是网络训练模块: 对于前向传播: 根据神经网络的拓扑顺序, 顺序调用每层的前向
传播函数。 以输入数据作为第一层的输入, 之后每层的输出作为其后一层的输入顺序计算
每一层的输出, 最后得到损失函数层的输出。 故需要先后调用三次 FullyConnectedLayer、
两次 ReLULayer、 一次 SoftmaxLossLayer 的 forward。
对于反向传播: 根据神经网络的拓扑顺序, 逆序调用每层的反向传播函数。 与前向相
反, 就不赘述了
2) mlp.load_data() 执行 到最 后时, train_images、 train_labels、 test_images 、
test_labels 的维度是多少? 即多少行多少列, 用(x,y)来表示。 self.train_data 和
self.test_data 的维度是多少?
答:
mlp.load_data() 函 数 执 行 完 毕 后 , train_images 的 维 度 是 (60000,784) ,
train_labels 的维度是(60000,1), test_images 的维度是(10000,784), test_labels 的
维度是(10000,1), 这些都取决于文件设定:
self.train_data 的维度是(60000, 785), self.test_data 的维度是(10000, 785)。
数据和标记按列合并了。
3) 本案例中的神经网络一共有几层? 每层有多少个神经元? 如果要增加或减少层数, 应
该怎么做(简单描述即可不用编程) ? 如果要增加或减少某一层的节点, 应该怎么做(简
单描述) ? 如果要把 softmax 换成 sigmoid, 应该怎么做(简单描述) ? 这种替换合理
么?
答:
本案例中的神经网络一共有三层, 输入神经元包括 784 个, 输出神经元为 10 个, 两
个隐藏层的神经元可以自己设计, 在这里是 32 和 16。
如果要增加或减少层数, 应该首先更改 MNIST_MLP 类中有关神经元设置的参数, 然后
修改 build_model 函数, 删去或者增加对应的层, 然后修改 load_model 和 save_model,
同样加载对应层数的神经网络权值, 对于 forward 和 backward 也需要修改, 具体是调用
方法需要按照对应神经的拓扑结构。
如果要增加或减少某一层的节点, 应该在类初始化的时候设置好初始值, 对每一层的
节点进行初始化。
如果要把 softmax 换成 sigmoid, 应该创建一个新的 sigmoid 层的类, 然后在主函
数上进行调用, 函数的结构要和 softmax 类相似, 但是前向传递要改成:反向传递也要改成:
这种替换不合理, sigmoid 函数用于多标签问题, 选取多个标签作为正确答案, 它是
将任意实数值归一化映射到[0-1]之间, 并不是不同概率之间的相互关联, 且由于远离 0
的部分梯度较小, 容易出现梯度消失现象。 softmax 函数用于多分类问题, 即从多个分类
中选取一个正确答案。 softmax 综合了所有输出值的归一化, 因此得到的是不同概率之间
的相互关联。 对于这个实验, 一幅图片对应的数字只有一种, 我们不是处理多标签问题。
4) 在 train()函数中, max_batch = self.train_data.shape[0] // self.batch_size 这
一句的意义是什么? self.shuffle_data()的意义是什么?
答:
self.train_data.shape[0]的意思是 train_data 中的数据量, 也就是行数, 在这里
是 60000 条数据的意思, self.batch_size 是手动设定的, 表示一次数据更新用到的数据
量是 100 个数据量, max_batch 表示数据更新的次数, 也表示最大的分组号。
self.shuffle_data()的意义是打乱数据, 保证实验的准确性。
5) 最终 evaluate()函数输出的 Accuracy in test set 是多少? 请想办法提高该数值。
答:
最终的 Accuracy in test set: 0.932200。
首先想到的是提高训练的次数, 将参数 e 修改为 10, 结果是 0.964100
之后再通过提高训练次数对于结果也不会有比较高的提升的, 那就需要思考方法的优
化了。 首先想到的是再次增加层数, 但是在增加到 4 层之后, 得到的结果是训练时间变长
了但是效果反而降低了准确率甚至都没有 96%了, 每次在训练之初误差会特别大, 分析是
因为权值和偏差的设定都是随机的, 在训练中需要调整的参数过多。 多次改变参数对三层
神经网络进行测试, 得出结论神经元的节点和训练次数对结果准确率的影响是高于层数的。
修改代码:
def __init__(self, batch_size=100, input_size=784, hidden1=64, hidden2=32, hidden3=16,
out_classes=10, lr=0.01, max_epoch=2, print_iter=100): #初始化
self.hidden3 = hidden3#在参数传到类中那部分代码添加
self.fc3 = FullyConnectedLayer(self.hidden2, self.hidden3) #在建立网络结构模块中添加
self.relu3 = ReLULayer()
self.fc4.load_param(params['w4'], params['b4']) # 在加载神经网络权值模块添加
params['w4'], params['b4'] = self.fc4.save_param()# save_model 模块添加
def forward(self, input): # 神经网络的前向传播
# TODO: 神经网络的前向传播
h1 = self.fc1.forward(input)
h1 = self.relu1.forward(h1)
h2 = self.fc2.forward(h1)
h2 = self.relu2.forward(h2)
h3 = self.fc3.forward(h2)
h3 = self.relu3.forward(h3)
h4 = self.fc4.forward(h3)
prob = self.softmax.forward(h4)
return prob
def backward(self): # 神经网络的反向传播
# TODO: 神经网络的反向传播
dloss = self.softmax.backward()
dh4 = self.fc4.backward(dloss)
dh3 = self.relu3.backward(dh4)
dh3 = self.fc3.backward(dh3)
dh2 = self.relu2.backward(dh3)
dh2 = self.fc2.backward(dh2)
dh1 = self.relu1.backward(dh2)
dh1 = self.fc1.backward(dh1)
if __name__ == '__main__':
h1, h2,h3, e = 512, 128, 128, 1
op = 0
mlp = MNIST_MLP(hidden1=h1, hidden2=h2,hidden3=h3, max_epoch=e)
mlp.load_data()
mlp.build_model()
mlp.init_model()
start_time1 = time.time()
start_time = time.time()
mlp.train()
op = op + e
while(mlp.evaluate()<=0.98):
mlp.train()
op = op + e
print("符合要求, 共",op,"次")
print("All train time: %f"%(time.time()-start_time1))
mlp.save_model('mlp-%d-%d-%d-%depoch.npy' % (h1, h2, h3, e))
mlp.load_model('mlp-%d-%d-%d-%depoch.npy' % (h1, h2, h3, e))
将三层神经网络, 第一层、 第二层、 第三层节点分别为 256, 128, 128, 学习率为 0.1
的情况下, 准确率通常需要经过 50 秒以下、 总共低于十五次的训练就能达到要求, 每次
训练完一次就进行评价会增多一部分时间但是综合考虑比较小可以省略。
最后的话
:)看完点赞让我更快更新到你想要的内容。