使用Paddle Paddle构建手写数字识别模型
对于构建一个深度学习的模型往往由如下步骤去实现,本小白在此文中使用经典的mnist
数据集来构建手写数字识别模型为例来入门深度学习
数据处理
MNIST数据集以json格式存储在本地,其数据又分为train_set(训练集)
,val_set(验证集)
和test_set(测试集)
- 训练集:用于确定模型的参数
- 验证集:用于调节模型超参数
- 测试集:用于估计实际的应用效果
数据说明:MNIST中的训练集共
5000
多张,每一张图像大小为784
,表示的是28*28
像素的灰度值
加载数据
datafile = './work/mnist.json.gz'
# 加载数据集文件
mnistdata = json.load(gzip.open(datafile))
# 将读取到的数据分为训练集,验证集和测试集
train_set, val_set, eval_set = mnistdata
# 训练数据集数量
imgs, labels = train_set[0], train_set[1]
print("训练数据集数量:", len(imgs))
训练样本乱序、生成批次数据
实验数据表明,模型会对最后出现的数据印象更深刻,约到最后,模型的参数所受的影响越大。所以为了避免模型记忆影响训练效果,需要进行样本乱序操作。
# 前文曾有提过,每一张图像数据是用长784的向量表示,需要将其reshape为28*28的格式
IMG_ROWS = 28
IMG_COLS = 28
# 定义数据集每个数据的序号,可以根据序号读取数据
index_list = list(range(imgs_length))
# 设置批次大小
BATCHSIZE = 100
# 训练样本乱序
random.shuffle(index_list)
# 定义数据生成器,返回批次数据
def data_generator():
imgs_list = []
labels_list = []
for i in index_list:
# 将数据处理成类型为float32,shape为[1, 28, 28]
img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32')
label = np.reshape(labels[i], [1]).astype('int64') # 后面会用到softmax函数
imgs_list.append(img)
labels_list.append(label)
if len(imgs_list) == BATCHSIZE:
# 获得一个batchsize的数据,并返回
yield np.array(imgs_list), np.array(labels_list)
# 清空数据读取列表
imgs_list = []
labels_list = []
# 如果剩余数据的数目小于BATCHSIZE,
# 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch
if len(imgs_list) > 0:
yield np.array(imgs_list), np.array(labels_list)
return data_generator
网络设计
基于全连接网络的实现
全连接神经网络包含四层:输入层、两个隐含层和输出层。输入层用于数据的输入;隐含层可以增加网络深度和复杂度,隐含层的节点数越多,神经网络表示能力越强,参数量也会增加;输出层为输出网络计算结果。以MNIST的数据为例,输入层是尺度为28×28的像素值,隐含层可以设置为为10×10的结构。输出层是一个数字,尺寸为1。
# 多层全连接神经网络实现
class MNIST(fluid.dygraph.Layer):
def __init__(self):
super(MNIST, self).__init__()
# 定义两层全连接隐含层,输出维度是10,激活函数为sigmoid
self.fc1 = Linear(input_dim=784, output_dim=10, act='sigmoid') # 隐含层节点为10,可根据任务调整
self.fc2 = Linear(input_dim=10, output_dim=10, act='sigmoid')
# 定义一层全连接输出层,输出维度是1,使用softmax激活函数,原因后面以及
self.fc3 = Linear(input_dim=10, output_dim=1, act='softmax')
# 定义网络的前向计算
def forward(self, inputs, label=None):
inputs = fluid.layers.reshape(inputs, [inputs.shape[0], 784])
outputs1 = self.fc1(inputs)
outputs2 = self.fc2(outputs1)
outputs_final = self.fc3(outputs2)
return outputs_final
使用卷积神经网络
对于计算机视觉问题,效果最好的模型仍然是卷积神经网络。
# 多层卷积神经网络实现
class MNIST(fluid.dygraph.Layer):
def __init__(self):
super(MNIST, self).__init__()
# 定义卷积层,输出特征通道num_filters设置为20,卷积核的大小filter_size为5,卷积步长stride=1,padding=2
# 激活函数使用relu
self.conv1 = Conv2D(num_channels=1, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
# 定义池化层,池化核pool_size=2,池化步长为2,选择最大池化方式
self.pool1 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
# 定义卷积层,输出特征通道num_filters设置为20,卷积核的大小filter_size为5,卷积步长stride=1,padding=2
self.conv2 = Conv2D(num_channels=20, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
# 定义池化层,池化核pool_size=2,池化步长为2,选择最大池化方式
self.pool2 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
# 定义一层全连接层,输出维度是1,不使用激活函数
self.fc = Linear(input_dim=980, output_dim=1, act=None)
# 定义网络前向计算过程,卷积后紧接着使用池化层,最后使用全连接层计算最终输出
def forward(self, inputs):
x = self.conv1(inputs)
x = self.pool1(x)
x = self.conv2(x)
x = self.pool2(x)
x = fluid.layers.reshape(x, [x.shape[0], -1])
x = self.fc(x)
return x
训练过程之损失函数
- 对于分类模型,最常使用的是
Softmax
函数,它可以将原始输出转变成对应标签的概率。 - 在模型输出为分类标签的概率时,直接以标签和概率做比较也不够合理,人们更习惯使用交叉熵误差作为分类问题的损失衡量。对于损失函数的选择,交叉熵误差损失函数常用于分类问题,均方误差常用于回归问题。
with fluid.dygraph.guard():
model = MNIST()
model.train()
#调用加载数据的函数
train_loader = load_data('train')
optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01, parameter_list=model.parameters())
EPOCH_NUM = 5
for epoch_id in range(EPOCH_NUM):
for batch_id, data in enumerate(train_loader()):
#准备数据,变得更加简洁
image_data, label_data = data
image = fluid.dygraph.to_variable(image_data)
label = fluid.dygraph.to_variable(label_data)
#前向计算的过程
predict = model(image)
#计算损失,使用交叉熵损失函数,取一个批次样本损失的平均值
loss = fluid.layers.cross_entropy(predict, label)
avg_loss = fluid.layers.mean(loss)
#每训练了200批次的数据,打印下当前Loss的情况
if batch_id % 200 == 0:
print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy()))
#后向传播,更新参数的过程
avg_loss.backward()
optimizer.minimize(avg_loss)
model.clear_gradients()
#保存模型参数
fluid.save_dygraph(model.state_dict(), 'mnist')
训练过程之优化算法
在深度学习神经网络模型中,通常使用标准的随机梯度下降算法更新参数,学习率代表参数更新幅度的大小,即步长。当学习率最优时,最终能达到的效果最好。对于学习率的选择不能太小也不能太大,学习率越小,损失函数的变化速度越慢,我们就需要花费更长的时间进行收敛(图一);而学习率越大,参数在最优解附近震荡,损失难以收敛(图二)。
在训练过程中可以尝试调小或调大,通过观察Loss下降的情况判断并选择合理的学习率。修改学习率只需要这是训练过程中 fluid.optimizer.SGDOptimizer
函数的learning_rate
参数即可。
有时候我们发现loss
值下降速度特别慢,我们就需要调节它的学习率了。在Paddle Paddle API
中提供了多种预先定义的常用的学习率衰减策略来动态生成的学习率,我们可以直接使用。 学习率调度器
调整学习率是一件非常麻烦的事情,需要不断的根据loss
的变化来调整步长,我们也可以选择合适的优化算法来求得最优解,目前比较成熟的四种优化算法:SGD、Momentum、AdaGrad和Adam。具体内容参考:优化器
训练过程之资源配置
在训练过程的部分代码前三行中改为如下几行,可以设置在GPU上进行训练,use_gpu
为True
表示在GPU上,否则在CPU上
use_gpu = False
# 设置使用GPU资源训神经网络,默认使用服务器的第一个GPU卡。"0"是GPU卡的编号
place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()
with fluid.dygraph.guard(place):
...
...
训练过程之调试与优化
下图为回归模型的过拟合,理想和欠拟合状态的表现
对于样本量有限、但需要使用强大模型的复杂任务,模型很容易出现过拟合的表现,即在训练集上的损失小,在验证集或测试集上的损失较大。如下图
反之,如果模型在训练集和测试集上均损失较大,则称为欠拟合,欠拟合表示模型还不够强大,还没有很好的拟合已知的训练样本,此类问题只需不断使用更强大的模型即可。更多的是去关注过拟合问题。
在深度学习中,过拟合发生的原因通常为如下两点原因:
- 训练数据存在噪音,导致模型学到了噪音,而不是真实规律。
- 使用强大模型(表示空间大)的同时训练数据太少,导致在训练数据上表现良好的候选假设太多,锁定了一个“虚假正确”的假设。
对于第一种,我们使用数据清洗和修正来解决。对于第二种,我们或者限制模型表示能力,或者收集更多的训练数据。在实际项目中,更快、更低成本可控制过拟合的方法,只有限制模型的表示能力。
为了防止模型过拟合,在没有扩充样本量的可能下,只能降低模型的复杂度,在优化器中设置regularization
参数即可实现
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=0.01, regularization=fluid.regularizer.L2Decay(regularization_coeff=0.1),parameter_list=model.parameters())
模型的保存与加载
训练过程往往都是比较耗时的,如果发生导致训练过程主动或被动的中断的情况,再从初始状态重新训练是不可接受的,飞桨支持从上一次保存状态开始继续训练,随时保存训练过程中的模型状态。
友情提示: 对于数据量比较大的训练,我们可以设置每过几个 epoch 保存一下模型,防止中断。
# 保存模型参数和优化器的参数
fluid.save_dygraph(model.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))
fluid.save_dygraph(optimizer.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))
对于恢复训练,飞桨提供如下API:
- 使用
model.state_dict()
获取模型参数。 - 使用
optimizer.state_dict()
获取优化器和学习率相关的参数。 - 调用
fluid.save_dygraph()
将参数保存到本地
如果模型参数文件和优化器参数文件是相同的,我们可以使用load_dygraph同时加载这两个文件
params_dict, opt_dict = fluid.load_dygraph(params_path)
完整代码
import os
import random
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Conv2D, Pool2D, Linear
import numpy as np
from PIL import Image
import gzip
import json
# 定义数据集读取器
def load_data(mode='train'):
# 数据文件
datafile = './work/mnist.json.gz'
print('loading mnist dataset from {} ......'.format(datafile))
data = json.load(gzip.open(datafile))
train_set, val_set, eval_set = data
# 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS
IMG_ROWS = 28
IMG_COLS = 28
if mode == 'train':
imgs = train_set[0]
labels = train_set[1]
elif mode == 'valid':
imgs = val_set[0]
labels = val_set[1]
elif mode == 'eval':
imgs = eval_set[0]
labels = eval_set[1]
imgs_length = len(imgs)
assert len(imgs) == len(labels), \
"length of train_imgs({}) should be the same as train_labels({})".format(
len(imgs), len(labels))
index_list = list(range(imgs_length))
# 读入数据时用到的batchsize
BATCHSIZE = 100
# 定义数据生成器
def data_generator():
if mode == 'train':
random.shuffle(index_list)
imgs_list = []
labels_list = []
for i in index_list:
img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32')
label = np.reshape(labels[i], [1]).astype('int64')
imgs_list.append(img)
labels_list.append(label)
if len(imgs_list) == BATCHSIZE:
yield np.array(imgs_list), np.array(labels_list)
imgs_list = []
labels_list = []
# 如果剩余数据的数目小于BATCHSIZE,
# 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch
if len(imgs_list) > 0:
yield np.array(imgs_list), np.array(labels_list)
return data_generator
#调用加载数据的函数
train_loader = load_data('train')
# 定义模型结构
class MNIST(fluid.dygraph.Layer):
def __init__(self):
super(MNIST, self).__init__()
# 定义一个卷积层,使用relu激活函数
self.conv1 = Conv2D(num_channels=1, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
# 定义一个池化层,池化核为2,步长为2,使用最大池化方式
self.pool1 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
# 定义一个卷积层,使用relu激活函数
self.conv2 = Conv2D(num_channels=20, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
# 定义一个池化层,池化核为2,步长为2,使用最大池化方式
self.pool2 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
# 定义一个全连接层,输出节点数为10
self.fc = Linear(input_dim=980, output_dim=10, act='softmax')
# 定义网络的前向计算过程
def forward(self, inputs, label):
x = self.conv1(inputs)
x = self.pool1(x)
x = self.conv2(x)
x = self.pool2(x)
x = fluid.layers.reshape(x, [x.shape[0], 980])
x = self.fc(x)
if label is not None:
acc = fluid.layers.accuracy(input=x, label=label)
return x, acc
else:
return x
#在使用GPU机器时,可以将use_gpu变量设置成True
use_gpu = False
place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()
with fluid.dygraph.guard(place):
model = MNIST()
model.train()
EPOCH_NUM = 5
BATCH_SIZE = 100
# 定义学习率,并加载优化器参数到模型中
total_steps = (int(60000//BATCH_SIZE) + 1) * EPOCH_NUM
lr = fluid.dygraph.PolynomialDecay(0.01, total_steps, 0.001)
# 使用Adam优化器
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=lr, parameter_list=model.parameters())
for epoch_id in range(EPOCH_NUM):
for batch_id, data in enumerate(train_loader()):
#准备数据,变得更加简洁
image_data, label_data = data
image = fluid.dygraph.to_variable(image_data)
label = fluid.dygraph.to_variable(label_data)
#前向计算的过程,同时拿到模型输出值和分类准确率
predict, acc = model(image, label)
avg_acc = fluid.layers.mean(acc)
#计算损失,取一个批次样本损失的平均值
loss = fluid.layers.cross_entropy(predict, label)
avg_loss = fluid.layers.mean(loss)
#每训练了200批次的数据,打印下当前Loss的情况
if batch_id % 200 == 0:
print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(),avg_acc.numpy()))
#后向传播,更新参数的过程
avg_loss.backward()
optimizer.minimize(avg_loss)
model.clear_gradients()
# 保存模型参数和优化器的参数
fluid.save_dygraph(model.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))
fluid.save_dygraph(optimizer.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))
params_path = "./checkpoint/mnist_epoch0"
#在使用GPU机器时,可以将use_gpu变量设置成True
use_gpu = False
place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()
with fluid.dygraph.guard(place):
# 加载模型参数到模型中
params_dict, opt_dict = fluid.load_dygraph(params_path)
model = MNIST()
model.load_dict(params_dict)
EPOCH_NUM = 5
BATCH_SIZE = 100
# 定义学习率,并加载优化器参数到模型中
total_steps = (int(60000//BATCH_SIZE) + 1) * EPOCH_NUM
lr = fluid.dygraph.PolynomialDecay(0.01, total_steps, 0.001)
# 使用Adam优化器
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=lr, parameter_list=model.parameters())
optimizer.set_dict(opt_dict)
for epoch_id in range(1, EPOCH_NUM):
for batch_id, data in enumerate(train_loader()):
#准备数据,变得更加简洁
image_data, label_data = data
image = fluid.dygraph.to_variable(image_data)
label = fluid.dygraph.to_variable(label_data)
#前向计算的过程,同时拿到模型输出值和分类准确率
predict, acc = model(image, label)
avg_acc = fluid.layers.mean(acc)
#计算损失,取一个批次样本损失的平均值
loss = fluid.layers.cross_entropy(predict, label)
avg_loss = fluid.layers.mean(loss)
#每训练了200批次的数据,打印下当前Loss的情况
if batch_id % 200 == 0:
print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(),avg_acc.numpy()))
#后向传播,更新参数的过程
avg_loss.backward()
optimizer.minimize(avg_loss)
model.clear_gradients()