神经网络是手写数字识别中常用的机器学习模型。它由许多神经元组成,每个神经元接收输入并生成输出。在前向传递过程中,神经元计算一些权重和偏移量的线性组合,并将其输入到一个非线性的激活函数中,从而生成神经元的输出。输出层通常使用softmax函数,将神经网络的输出映射到每个数字类别的概率。训练神经网络通常使用反向传播算法,该算法用于计算网络中每个权重和偏移量的梯度,并用梯度下降算法调整这些参数以最小化损失函数。在训练完成后,手写数字图像可以通过前向传递神经网络来进行分类。梯度下降法是寻找损失函数最小值的常用方法,包括批量梯度下降、随机梯度下降和小批量梯度下降。选择哪种梯度下降法取决于具体的算法和任务需求
本项目的源代码已放在文章末尾,如果它对您有帮助,请点个赞或收藏支持一下哦~。
一个完整的神经网络通常由多个基本的网络层堆叠而成。本项目中的三层全连接神经网络由三个全连接层构成,在每两个全连接层之间插入 ReLU 激活函数以引入非线性变换,最后使用 Softmax 层计算交叉熵损失,如图所示。因此本项目中使用的基本单元包括全连接层、ReLU 激活函数、Softmax 损失函数
MNIST数据集是一个手写数字图像数据集,由60,000个训练图像和10,000个测试图像组成。每个图像都是28x28像素的灰度图像,表示0到9之间的一个数字。该数据集常用于机器学习中的图像分类任务。
获取MNIST数据集的步骤如下:
mian.py
# coding=utf-8
import numpy as np
import struct
import os
import time
import matplotlib.pyplot as plt
from layers_1 import FullyConnectedLayer, ReLULayer, SoftmaxLossLayer
# 画图时的中文支持
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
# 定义了 MNIST 数据集的存储路径和文件名
MNIST_DIR = "./mnist_data"
TRAIN_DATA = "train-images-idx3-ubyte"
TRAIN_LABEL = "train-labels-idx1-ubyte"
TEST_DATA = "t10k-images-idx3-ubyte"
TEST_LABEL = "t10k-labels-idx1-ubyte"
# 显示矩阵的形状和均值和标准差
def show_matrix(mat, name):
# print(name + str(mat.shape) + ' mean %f, std %f' % (mat.mean(), mat.std()))
pass
# 该类包括多个函数,用于加载数据集、建立模型、初始化模型、训练和评估
# 定义一个类名为 MNIST_MLP,表示一个多层感知机(Multilayer Perceptron)模型
class MNIST_MLP(object):
# 初始化函数,传入了多个参数用于初始化神经网络
def __init__(self, batch_size=90, input_size=784, hidden1=32, hidden2=16, out_classes=10, lr=0.01, max_epoch=2,
print_iter=100):
self.batch_size = batch_size # 每次训练迭代所使用的数据批次大小
self.input_size = input_size # 输入层神经元数量(即MNIST图像大小)
self.hidden1 = hidden1 # 第一层隐藏层神经元数量
self.hidden2 = hidden2 # 第二层隐藏层神经元数量
self.out_classes = out_classes # 输出层神经元数量(即分类数量)
self.lr = lr # 学习率,用于控制模型训练时每次参数的更新幅度
self.max_epoch = max_epoch # 最大训练轮数
self.print_iter = print_iter # 训练过程中每隔print_iter次输出一次日志信息
self.accuracy_list = [] # 存储每次迭代的准确率
self.loss_list = [] # 存储每次迭代损失
# 该函数用于读取 MNIST 数据集的图像和标记,并将其存储为 numpy 数组,其中 is_images 参数为 True 表示读取图像,为 False 表示读取标记
# 定义一个函数来读取mnist数据集
def load_mnist(self, file_dir, is_images='True'):
# 打开二进制文件并读取其中的数据
bin_file = open(file_dir, 'rb')
bin_data = bin_file.read()
bin_file.close()
# 解析文件头
if is_images:
# 如果是图像数据集,读取图像格式的文件头
fmt_header = '>iiii'
magic, num_images, num_rows, num_cols = struct.unpack_from(fmt_header, bin_data, 0)
else:
# 如果是标签数据集,读取标签格式的文件头
fmt_header = '>ii'
magic, num_images = struct.unpack_from(fmt_header, bin_data, 0)
num_rows, num_cols = 1, 1
# 计算数据的大小
data_size = num_images * num_rows * num_cols
# 解析数据
mat_data = struct.unpack_from('>' + str(data_size) + 'B', bin_data, struct.calcsize(fmt_header))
# 将数据重塑为一个矩阵
mat_data = np.reshape(mat_data, [num_images, num_rows * num_cols])
# 打印读取数据的相关信息
print('Load images from %s, number: %d, data shape: %s' % (file_dir, num_images, str(mat_data.shape)))
# 返回读取到的数据
return mat_data
# 调用 load_mnist 函数读取和存储 MNIST 中训练数据和测试数据的图像和标记
def load_data(self):
# 调用函数 load_mnist 读取和预处理 MNIST 中训练数据和测试数据的图像和标记
# print('Loading MNIST data from files...')
print('加载 MNIST 数据集 ...')
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)
# print(train_images.shape)
# print(train_labels.shape)
# print(test_images.shape)
# print(test_labels.shape)
# 将训练数据集打乱,以便更好地训练
def shuffle_data(self):
print('随机混洗 MNIST 数据...')
np.random.shuffle(self.train_data)
# 建立多层感知机神经网络,并定义各层的数量及其之间的连接关系
def build_model(self): # 建立网络结构
print('构建多层感知机模型...')
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]
# 初始化神经网络参数
def init_model(self):
# print('Initializing parameters of each layer in MLP...')
print('初始化各层参数权值...')
for layer in self.update_layer_list:
layer.init_param()
# 从文件加载神经网络参数(即权重和偏差)
def load_model(self, param_dir): # 加载神经网络权值
print('从文件加载参数权值:' + param_dir)
params = np.load(param_dir, allow_pickle=True).item()
self.fc1.load_param(params['w1'], params['b1'])
self.fc2.load_param(params['w2'], params['b2'])
self.fc3.load_param(params['w3'], params['b3'])
# 将神经网络的参数(即权重和偏差)保存到文件中以备后续调用
def save_model(self, param_dir):
print('保存权值和偏置到: ' + param_dir)
params = {}
params['w1'], params['b1'] = self.fc1.save_param()
params['w2'], params['b2'] = self.fc2.save_param()
params['w3'], params['b3'] = self.fc3.save_param()
np.save(param_dir, params)
# 神经网络的前向传播,即将输入数据通过神经网络传递,得出预测结果
def forward(self, input): # 神经网络的前向传播
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)
# 使用反向传播和梯度下降训练多层感知机神经网络,使其逐渐适应数据集
def train(self):
# 计算每个batch中的样本数
max_batch = self.train_data.shape[0] // self.batch_size # [0]表示数据集的第一维,也就是数据集中样本的数量
print('开始训练...')
# 对于每个epoch
for idx_epoch in range(3):
# 将训练数据集随机打乱
self.shuffle_data()
# 对于每个batch
for idx_batch in range(max_batch):
# 从训练数据中取出当前batch的图片和标签
batch_images = self.train_data[idx_batch * self.batch_size:(idx_batch + 1) * self.batch_size, :-1]
batch_labels = self.train_data[idx_batch * self.batch_size:(idx_batch + 1) * self.batch_size, -1]
# 使用前向传播算法计算预测概率
prob = self.forward(batch_images)
# 使用Softmax交叉熵函数计算损失
loss = self.softmax.get_loss(batch_labels)
# 使用反向传播算法进行梯度下降更新模型参数
self.backward()
self.update(self.lr)
# 如果当前batch的序号可以整除打印迭代步数的间隔,打印该batch的损失
if idx_batch % self.print_iter == 0:
print('Epoch %d, iter %d, loss: %.6f' % (idx_epoch, idx_batch, loss))
# 计算当前batch的预测准确率并存储下来
pred_results = np.zeros([batch_labels.shape[0]])
for idx in range(batch_labels.shape[0] // self.batch_size):
# 从当前batch中取出样本进行预测
batch_images_temp = batch_images[idx * self.batch_size:(idx + 1) * self.batch_size, :]
prob = self.forward(batch_images_temp)
pred_labels = np.argmax(prob, axis=1)
pred_results[idx * self.batch_size:(idx + 1) * self.batch_size] = pred_labels
# 计算准确率
acc = np.mean(pred_results == batch_labels)
self.accuracy_list.append(acc) # 存储当前batch的预测准确率
self.loss_list.append(loss) # 存储当前batch的预测准确率
# 在测试数据集上对神经网络进行推断,并计算分类准确率
def evaluate(self): # 推断函数
pred_results = np.zeros([self.test_data.shape[0]])
start_time = time.time()
for idx in range(self.test_data.shape[0] // self.batch_size):
batch_images = self.test_data[idx * self.batch_size:(idx + 1) * self.batch_size, :-1]
prob = self.forward(batch_images)
end = time.time()
# print(prob.shape)
pred_labels = np.argmax(prob, axis=1)
pred_results[idx * self.batch_size:(idx + 1) * self.batch_size] = pred_labels
print("测试总时间: %f" % (time.time() - start_time))
accuracy = np.mean(pred_results == self.test_data[:, -1])
print('测试集准确率: %f' % accuracy)
plt.plot(range(len(self.accuracy_list)), self.accuracy_list)
plt.xlabel('迭代次数')
plt.ylabel('准确率')
plt.title('训练过程中准确率变化')
plt.show()
plt.plot(range(len(self.loss_list)), self.loss_list)
plt.xlabel('迭代次数')
plt.ylabel('准确率')
plt.title('训练过程中损失变化')
plt.show()
# 创建 MNIST_MLP 类的实例,并对参数进行初始化、加载数据、建立模型、初始化模型、训练、保存模型、加载模型和评估的一系列操作
if __name__ == '__main__':
h1, h2, e = 32, 16, 1
mlp = MNIST_MLP(hidden1=h1, hidden2=h2, max_epoch=e)
mlp.load_data()
mlp.build_model()
mlp.init_model()
start_time = time.time()
mlp.train()
print("训练总时间: %f" % (time.time() - start_time))
mlp.save_model('mlp-%d-%d-%depoch.npy' % (h1, h2, e))
mlp.load_model('mlp-%d-%d-%depoch.npy' % (h1, h2, e))
mlp.evaluate()
这是一个使用多层感知机(MLP)进行手写数字分类的Python代码。
# coding=utf-8 # 代码文件编码方式
import numpy as np # 引入numpy库
import struct # 引入struct库
import os # 引入os库
import time # 引入time库
# 实现了一个全连接层类,num_input是输入维度,num_output是输出维度
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))
# 初始化参数,std是初始化参数的标准差
def init_param(self, std=0.01):
self.weight = np.random.normal(loc=0.0, scale=std, size=(self.num_input, self.num_output))
self.bias = np.zeros([1, self.num_output])
# 前向传播计算, input是输入
def forward(self, input):
start_time = time.time()
self.input = input
# 全连接层的前向传播,计算输出结果
self.output = np.dot(self.input, self.weight) + self.bias
return self.output
# 反向传播计算,top_diff是损失函数对输出的导数
def backward(self, top_diff):
# 计算参数梯度和本层损失
self.d_weight = np.dot(self.input.T, top_diff)
self.d_bias = np.sum(top_diff, axis=0, keepdims=True)
bottom_diff = np.dot(top_diff, self.weight.T)
return bottom_diff
# 利用参数进行参数更新,lr是学习率
def update_param(self, lr):
self.weight -= lr * self.d_weight
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
# 实现了一个ReLU激活函数层类
class ReLULayer(object):
def __init__(self):
print('\tReLU layer.')
# 计算前向传播的输出结果
def forward(self, input):
start_time = time.time()
self.input = input
# ReLU层的前向传播,计算输出结果
output = np.maximum(0, input)
return output
# 计算反向传播输出的损失结果
def backward(self, top_diff):
# ReLU层的反向传播,计算本层损失
bottom_diff = top_diff.copy()
bottom_diff[self.input<0] = 0
return bottom_diff
# 实现了一个softmax激活函数层类
class SoftmaxLossLayer(object):
def __init__(self):
print('\tSoftmax loss layer.')
# 计算前向传播的输出结果
def forward(self, input):
# 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):
# softmax 损失层的反向传播,计算本层损失
bottom_diff = (self.prob - self.label_onehot) / self.batch_size
return bottom_diff
这段代码实现了一个简单的神经网络,包括全连接层、ReLU激活函数层和softmax损失函数层。其中,全连接层用于将输入数据进行线性变换,ReLU激活函数层用于增加网络的非线性能力,softmax损失函数层用于计算分类问题中的损失函数。代码中实现了前向传播和反向传播的计算,并且提供了参数初始化、参数更新、参数加载和保存等功能,方便模型的重新训练。
项目源码: https://gitee.com/TGY1817/handwritten-digit-recognition