Deep Learning-深度学习(五)
深度学习入门
1、手写数字识别-优化算法
1.1 前提条件
依旧是之前对于数据的处理以及网络模型的设计,包括对数据的各个数据集的分类,以及对数据集的卷积神经网络处理等。
1 # 加载相关库 2 import os 3 import random 4 import paddle 5 from paddle.nn import Conv2D, MaxPool2D, Linear 6 import numpy as np 7 from PIL import Image 8 import gzip 9 import json 10 11 # 定义数据集读取器 12 def load_data(mode='train'): 13 # 读取数据文件 14 datafile = './work/mnist.json.gz' 15 print('loading mnist dataset from {} ......'.format(datafile)) 16 data = json.load(gzip.open(datafile)) 17 # 读取数据集中的训练集,验证集和测试集 18 train_set, val_set, eval_set = data 19 20 # 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS 21 IMG_ROWS = 28 22 IMG_COLS = 28 23 # 根据输入mode参数决定使用训练集,验证集还是测试 24 if mode == 'train': 25 imgs = train_set[0] 26 labels = train_set[1] 27 elif mode == 'valid': 28 imgs = val_set[0] 29 labels = val_set[1] 30 elif mode == 'eval': 31 imgs = eval_set[0] 32 labels = eval_set[1] 33 # 获得所有图像的数量 34 imgs_length = len(imgs) 35 # 验证图像数量和标签数量是否一致 36 assert len(imgs) == len(labels), \ 37 "length of train_imgs({}) should be the same as train_labels({})".format( 38 len(imgs), len(labels)) 39 40 index_list = list(range(imgs_length)) 41 42 # 读入数据时用到的batchsize 43 BATCHSIZE = 100 44 45 # 定义数据生成器 46 def data_generator(): 47 # 训练模式下,打乱训练数据 48 if mode == 'train': 49 random.shuffle(index_list) 50 imgs_list = [] 51 labels_list = [] 52 # 按照索引读取数据 53 for i in index_list: 54 # 读取图像和标签,转换其尺寸和类型 55 img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32') 56 label = np.reshape(labels[i], [1]).astype('int64') 57 imgs_list.append(img) 58 labels_list.append(label) 59 # 如果当前数据缓存达到了batch size,就返回一个批次数据 60 if len(imgs_list) == BATCHSIZE: 61 yield np.array(imgs_list), np.array(labels_list) 62 # 清空数据缓存列表 63 imgs_list = [] 64 labels_list = [] 65 66 # 如果剩余数据的数目小于BATCHSIZE, 67 # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch 68 if len(imgs_list) > 0: 69 yield np.array(imgs_list), np.array(labels_list) 70 71 return data_generator 72 73 # 定义模型结构 74 import paddle.nn.functional as F 75 # 多层卷积神经网络实现 76 class MNIST(paddle.nn.Layer): 77 def __init__(self): 78 super(MNIST, self).__init__() 79 80 # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2 81 self.conv1 = Conv2D(in_channels=1, out_channels=20, kernel_size=5, stride=1, padding=2) 82 # 定义池化层,池化核的大小kernel_size为2,池化步长为2 83 self.max_pool1 = MaxPool2D(kernel_size=2, stride=2) 84 # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2 85 self.conv2 = Conv2D(in_channels=20, out_channels=20, kernel_size=5, stride=1, padding=2) 86 # 定义池化层,池化核的大小kernel_size为2,池化步长为2 87 self.max_pool2 = MaxPool2D(kernel_size=2, stride=2) 88 # 定义一层全连接层,输出维度是10 89 self.fc = Linear(in_features=980, out_features=10) 90 91 # 定义网络前向计算过程,卷积后紧接着使用池化层,最后使用全连接层计算最终输出 92 # 卷积层激活函数使用Relu,全连接层激活函数使用softmax 93 def forward(self, inputs): 94 x = self.conv1(inputs) 95 x = F.relu(x) 96 x = self.max_pool1(x) 97 x = self.conv2(x) 98 x = F.relu(x) 99 x = self.max_pool2(x) 100 x = paddle.reshape(x, [x.shape[0], -1]) 101 x = self.fc(x) 102 return x
1.2 设置学习率
在进行深度学习的时候,我们使用的是随机梯度下降的方法,在这个方法当中需要对每一次参数修正有一个幅度,这就是我们所说的学习率。当我们能够利用最为合适的学习率时,我们就能够使得整个模型的效果达到最佳。对于学习率的设置需要注意的是:
- 学习率不是越小越好。学习率越小,损失函数的变化速度越慢,意味着我们需要花费更长的时间进行收敛,如下左图所示。
- 学习率不是越大越好。只根据总样本集中的一个批次计算梯度,抽样误差会导致计算出的梯度不是全局最优的方向,且存在波动。在接近最优解时,过大的学习率会导致参数在最优解附近震荡,损失难以收敛,如下右图所示。
所以我们需要通过对Loss的下降过程进行分析,来调试出更加合适的学习率:
代码为:
1 #仅优化算法的设置有所差别 2 def train(model): 3 model.train() 4 #调用加载数据的函数 5 train_loader = load_data('train') 6 7 #设置不同初始学习率 8 opt = paddle.optimizer.SGD(learning_rate=0.001, parameters=model.parameters()) 9 # opt = paddle.optimizer.SGD(learning_rate=0.0001, parameters=model.parameters()) 10 # opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters()) 11 12 EPOCH_NUM = 10 13 for epoch_id in range(EPOCH_NUM): 14 for batch_id, data in enumerate(train_loader()): 15 #准备数据 16 images, labels = data 17 images = paddle.to_tensor(images) 18 labels = paddle.to_tensor(labels) 19 20 #前向计算的过程 21 predicts = model(images) 22 23 #计算损失,取一个批次样本损失的平均值 24 loss = F.cross_entropy(predicts, labels) 25 avg_loss = paddle.mean(loss) 26 27 #每训练了100批次的数据,打印下当前Loss的情况 28 if batch_id % 200 == 0: 29 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 30 31 #后向传播,更新参数的过程 32 avg_loss.backward() 33 # 最小化loss,更新参数 34 opt.step() 35 # 清除梯度 36 opt.clear_grad() 37 38 #保存模型参数 39 paddle.save(model.state_dict(), 'mnist.pdparams') 40 41 #创建模型 42 model = MNIST() 43 #启动训练过程 44 train(model)
当学习率为0.01时:
当学习率为0.001时:
当学习率为0.0001时:
可以发现当学习率为0.0001时,对于Loss的下降是很慢的,此外也需要明白太大了也是不好的。
1.3 学习率的主流优化算法
目前四种比较成熟的优化算法:SGD、Momentum、AdaGrad和Adam,效果如图所示。
其中中心点就是最佳的点。
每个批次的数据含有抽样误差,导致梯度更新的方向波动较大。如果我们引入物理动量的概念,给梯度下降的过程加入一定的“惯性”累积,就可以减少更新路径上的震荡,即每次更新的梯度由“历史多次梯度的累积方向”和“当次梯度”加权相加得到。历史多次梯度的累积方向往往是从全局视角更正确的方向,这与“惯性”的物理概念很像,也是为何其起名为“Momentum”的原因。类似不同品牌和材质的篮球有一定的重量差别,街头篮球队中的投手(擅长中远距离投篮)喜欢稍重篮球的比例较高。一个很重要的原因是,重的篮球惯性大,更不容易受到手势的小幅变形或风吹的影响。
-
AdaGrad: 根据不同参数距离最优解的远近,动态调整学习率。学习率逐渐下降,依据各参数变化大小调整学习率。
-
Adam: 由于动量和自适应学习率两个优化思路是正交的,因此可以将两个思路结合起来,这就是当前广泛应用的算法。
对四种模型的运用,可以通过paddle paddle的库进行 一 一 运行进行查看:
1 #仅优化算法的设置有所差别 2 def train(model): 3 model.train() 4 #调用加载数据的函数 5 train_loader = load_data('train') 6 7 #四种优化算法的设置方案,可以逐一尝试效果 8 opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters()) 9 # opt = paddle.optimizer.Momentum(learning_rate=0.01, momentum=0.9, parameters=model.parameters()) 10 # opt = paddle.optimizer.Adagrad(learning_rate=0.01, parameters=model.parameters()) 11 # opt = paddle.optimizer.Adam(learning_rate=0.01, parameters=model.parameters()) 12 13 EPOCH_NUM = 3 14 for epoch_id in range(EPOCH_NUM): 15 for batch_id, data in enumerate(train_loader()): 16 #准备数据 17 images, labels = data 18 images = paddle.to_tensor(images) 19 labels = paddle.to_tensor(labels) 20 21 #前向计算的过程 22 predicts = model(images) 23 24 #计算损失,取一个批次样本损失的平均值 25 loss = F.cross_entropy(predicts, labels) 26 avg_loss = paddle.mean(loss) 27 28 #每训练了100批次的数据,打印下当前Loss的情况 29 if batch_id % 200 == 0: 30 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 31 32 #后向传播,更新参数的过程 33 avg_loss.backward() 34 # 最小化loss,更新参数 35 opt.step() 36 # 清除梯度 37 opt.clear_grad() 38 39 #保存模型参数 40 paddle.save(model.state_dict(), 'mnist.pdparams') 41 42 #创建模型 43 model = MNIST() 44 #启动训练过程 45 train(model)
其中,SGD的效果在上面已经得到,下面均以学习率为0.01进行查看,
Momentum:
Adagrad:
Adam:
可以看见几种优化算法的效果都是很明显的下降的。
2、资源配置
因为对于较为简单的神经网络优化模型,会比较快就能够训练好,但是如果数据量一旦过大,就需要运算速度更高的硬件。因此,对于资源配置的优化,也是比较重要的一环。
2.1 单GPU训练
通过paddle.device.set_device API,设置在GPU上训练还是CPU上训练。
paddle.device.set_device (device)
参数 device (str):此参数确定特定的运行设备,可以是cpu
、 gpu:x
或者是xpu:x
。其中,x
是GPU或XPU的编号。当device是cpu
时, 程序在CPU上运行;当device是gpu:x
时,程序在GPU上运行。
1 #仅优化算法的设置有所差别 2 def train(model): 3 #开启GPU 4 use_gpu = True 5 paddle.device.set_device('gpu:0') if use_gpu else paddle.device.set_device('cpu') 6 model.train() 7 #调用加载数据的函数 8 train_loader = load_data('train') 9 10 #设置不同初始学习率 11 opt = paddle.optimizer.Adam(learning_rate=0.01, parameters=model.parameters()) 12 13 EPOCH_NUM = 5 14 for epoch_id in range(EPOCH_NUM): 15 for batch_id, data in enumerate(train_loader()): 16 #准备数据,变得更加简洁 17 images, labels = data 18 images = paddle.to_tensor(images) 19 labels = paddle.to_tensor(labels) 20 21 #前向计算的过程 22 predicts = model(images) 23 24 #计算损失,取一个批次样本损失的平均值 25 loss = F.cross_entropy(predicts, labels) 26 avg_loss = paddle.mean(loss) 27 28 #每训练了100批次的数据,打印下当前Loss的情况 29 if batch_id % 200 == 0: 30 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 31 32 #后向传播,更新参数的过程 33 avg_loss.backward() 34 opt.step() 35 opt.clear_grad() 36 37 #保存模型参数 38 paddle.save(model.state_dict(), 'mnist.pdparams') 39 #创建模型 40 model = MNIST() 41 #启动训练过程 42 train(model)
结果为:
2.2 分布式训练
2.2.1 模型并行
模型并行是将一个网络模型拆分为多份,拆分后的模型分到多个设备上(GPU)训练,每个设备的训练数据是相同的。模型并行的实现模式可以节省内存,一般适用于如下两个场景:
-
模型架构过大: 完整的模型无法放入单个GPU。
-
网络模型的结构设计相对独立: 当网络模型的设计结构可以并行化时,采用模型并行的方式。
注意这里的网络模型拆分概念,即可以进行横向按层划分、纵向跨层划分和模型随机划分。划分之后的各层实际上是通过某一个节点进行连接,每一层通过一个点进行联系起来的,而不是完全的孤立起来。
2.2.2 数据并行
数据并行与模型并行不同,数据并行每次读取多份数据,读取到的数据输入给多个设备(GPU)上的模型,每个设备上的模型是完全相同的。
这里需要注意的是,如果每个相同的模型在通过不同的数据输入进行训练过后,其实是在最终的参数上会有差距的,因此需要一个梯度同步机制,保证每个设备的梯度是完全相同的。梯度同步有两种方式:PRC通信方式和NCCL2通信方式。
①PRC:通常用于CPU分布式训练,它有两个节点:参数服务器Parameter server和训练节点Trainer,结构如图:
parameter server收集来自每个设备的梯度更新信息,并计算出一个全局的梯度更新。Trainer用于训练,每个Trainer上的程序相同,但数据不同。当Parameter server收到来自Trainer的梯度更新请求时,统一更新模型的梯度。即由sever来进行统一。
②NCCL2通信方式:相比PRC通信方式,使用NCCL2(Collective通信方式)进行分布式训练,不需要启动Parameter server进程,每个Trainer进程保存一份完整的模型参数,在完成梯度计算之后通过Trainer之间的相互通信,Reduce梯度数据到所有节点的所有设备,然后每个节点在各自完成参数更新。
2.2.3 单机多卡程序
加速神经网络模型的训练最简单直接的办法是使用 GPU,往往一块 GPU 的显存容量是有限的,如果遇到了比较大的模型或者是参数量巨大的情况下会直接爆显存,最简单粗暴的办法就是加 GPU,一块不够我就使用两块,两块还不够我就再加两块。这会有一个核心的问题,为了使用多块 GPU 进行训练,我们必须想一个办法在多个 GPU 上进行分发数据和模型,并且协调训练的过程。
3、训练调优与优化
训练过程优化思路主要有如下五个关键环节:
1. 计算分类准确率,观测模型训练效果。
交叉熵损失函数只能作为优化目标,无法直接准确衡量模型的训练效果。准确率可以直接衡量训练效果,但由于其离散性质,不适合做为损失函数优化神经网络。
2. 检查模型训练过程,识别潜在问题。
如果模型的损失或者评估指标表现异常,通常需要打印模型每一层的输入和输出来定位问题,分析每一层的内容来获取错误的原因。
3. 加入校验或测试,更好评价模型效果。
理想的模型训练结果是在训练集和验证集上均有较高的准确率,如果训练集的准确率低于验证集,说明网络训练程度不够;如果训练集的准确率高于验证集,可能是发生了过拟合现象。通过在优化目标中加入正则化项的办法,解决过拟合的问题。
4. 加入正则化项,避免模型过拟合。
飞桨框架支持为整体参数加入正则化项,这是通常的做法。此外,飞桨框架也支持为某一层或某一部分的网络单独加入正则化项,以达到精细调整参数训练的效果。
5. 可视化分析。
用户不仅可以通过打印或使用matplotlib库作图,以及其他更专业的可视化分析工具(如VisualDL),提供便捷的可视化分析方法。
3.1 计算模型的分类准确率
准确率是一个直观衡量分类模型效果的指标,由于这个指标是离散的,因此不适合作为损失来优化。通常情况下,交叉熵损失越小的模型,分类的准确率也越高。
class paddle.metric.Accuracy 为飞桨提供的计算分类准确率API,可以直接计算准确率。
现在,在模型前向计算过程forward函数中计算分类准确率,并在训练时打印每个批次样本的分类准确率。
完整代码:
1 # 加载相关库 2 import os 3 import random 4 import paddle 5 import numpy as np 6 from PIL import Image 7 import gzip 8 import json 9 10 11 # 定义数据集读取器 12 def load_data(mode='train'): 13 14 # 读取数据文件 15 datafile = './work/mnist.json.gz' 16 print('loading mnist dataset from {} ......'.format(datafile)) 17 data = json.load(gzip.open(datafile)) 18 # 读取数据集中的训练集,验证集和测试集 19 train_set, val_set, eval_set = data 20 21 # 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS 22 IMG_ROWS = 28 23 IMG_COLS = 28 24 # 根据输入mode参数决定使用训练集,验证集还是测试 25 if mode == 'train': 26 imgs = train_set[0] 27 labels = train_set[1] 28 elif mode == 'valid': 29 imgs = val_set[0] 30 labels = val_set[1] 31 elif mode == 'eval': 32 imgs = eval_set[0] 33 labels = eval_set[1] 34 # 获得所有图像的数量 35 imgs_length = len(imgs) 36 # 验证图像数量和标签数量是否一致 37 assert len(imgs) == len(labels), \ 38 "length of train_imgs({}) should be the same as train_labels({})".format( 39 len(imgs), len(labels)) 40 41 index_list = list(range(imgs_length)) 42 43 # 读入数据时用到的batchsize 44 BATCHSIZE = 100 45 46 # 定义数据生成器 47 def data_generator(): 48 # 训练模式下,打乱训练数据 49 if mode == 'train': 50 random.shuffle(index_list) 51 imgs_list = [] 52 labels_list = [] 53 # 按照索引读取数据 54 for i in index_list: 55 # 读取图像和标签,转换其尺寸和类型 56 img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32') 57 label = np.reshape(labels[i], [1]).astype('int64') 58 imgs_list.append(img) 59 labels_list.append(label) 60 # 如果当前数据缓存达到了batch size,就返回一个批次数据 61 if len(imgs_list) == BATCHSIZE: 62 yield np.array(imgs_list), np.array(labels_list) 63 # 清空数据缓存列表 64 imgs_list = [] 65 labels_list = [] 66 67 # 如果剩余数据的数目小于BATCHSIZE, 68 # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch 69 if len(imgs_list) > 0: 70 yield np.array(imgs_list), np.array(labels_list) 71 72 return data_generator 73 # 定义模型结构 74 import paddle.nn.functional as F 75 from paddle.nn import Conv2D, MaxPool2D, Linear 76 77 # 多层卷积神经网络实现 78 class MNIST(paddle.nn.Layer): 79 def __init__(self): 80 super(MNIST, self).__init__() 81 82 # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2 83 self.conv1 = Conv2D(in_channels=1, out_channels=20, kernel_size=5, stride=1, padding=2) 84 # 定义池化层,池化核的大小kernel_size为2,池化步长为2 85 self.max_pool1 = MaxPool2D(kernel_size=2, stride=2) 86 # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2 87 self.conv2 = Conv2D(in_channels=20, out_channels=20, kernel_size=5, stride=1, padding=2) 88 # 定义池化层,池化核的大小kernel_size为2,池化步长为2 89 self.max_pool2 = MaxPool2D(kernel_size=2, stride=2) 90 # 定义一层全连接层,输出维度是10 91 self.fc = Linear(in_features=980, out_features=10) 92 93 # 定义网络前向计算过程,卷积后紧接着使用池化层,最后使用全连接层计算最终输出 94 # 卷积层激活函数使用Relu,全连接层激活函数使用softmax 95 def forward(self, inputs, label): 96 x = self.conv1(inputs) 97 x = F.relu(x) 98 x = self.max_pool1(x) 99 x = self.conv2(x) 100 x = F.relu(x) 101 x = self.max_pool2(x) 102 x = paddle.reshape(x, [x.shape[0], 980]) 103 x = self.fc(x) 104 if label is not None: 105 acc = paddle.metric.accuracy(input=x, label=label) 106 return x, acc 107 else: 108 return x 109 110 #调用加载数据的函数 111 train_loader = load_data('train') 112 113 #在使用GPU机器时,可以将use_gpu变量设置成True 114 use_gpu = True 115 paddle.set_device('gpu:0') if use_gpu else paddle.set_device('cpu') 116 117 #仅优化算法的设置有所差别 118 def train(model): 119 model = MNIST() 120 model.train() 121 122 #四种优化算法的设置方案,可以逐一尝试效果 123 # opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters()) 124 # opt = paddle.optimizer.Momentum(learning_rate=0.01, momentum=0.9, parameters=model.parameters()) 125 # opt = paddle.optimizer.Adagrad(learning_rate=0.01, parameters=model.parameters()) 126 opt = paddle.optimizer.Adam(learning_rate=0.01, parameters=model.parameters()) 127 128 EPOCH_NUM = 5 129 for epoch_id in range(EPOCH_NUM): 130 for batch_id, data in enumerate(train_loader()): 131 #准备数据 132 images, labels = data 133 images = paddle.to_tensor(images) 134 labels = paddle.to_tensor(labels) 135 136 #前向计算的过程 137 predicts, acc = model(images, labels) 138 139 #计算损失,取一个批次样本损失的平均值 140 loss = F.cross_entropy(predicts, labels) 141 avg_loss = paddle.mean(loss) 142 143 #每训练了100批次的数据,打印下当前Loss的情况 144 if batch_id % 200 == 0: 145 print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(), acc.numpy())) 146 147 #后向传播,更新参数,消除梯度的过程 148 avg_loss.backward() 149 opt.step() 150 opt.clear_grad() 151 152 #保存模型参数 153 paddle.save(model.state_dict(), 'mnist.pdparams') 154 155 #创建模型 156 model = MNIST() 157 #启动训练过程 158 train(model)
结果为:
3.2 过拟合现象、正则化
对于样本量有限、但需要使用强大模型的复杂任务,模型很容易出现过拟合的表现,即在训练集上的损失小,在验证集或测试集上的损失较大,如图:
反之,如果模型在训练集和测试集上均损失较大,则称为欠拟合。
为了防止过拟合现象,就需要运用到正则化:在模型的优化目标(损失)中人为加入对参数规模的惩罚项。当参数越多或取值越大时,该惩罚项就越大。通过调整惩罚项的权重系数,可以使模型在“尽量减少训练损失”和“保持模型的泛化能力”之间取得平衡。泛化能力表示模型在没有见过的样本上依然有效。正则化项的存在,增加了模型在训练集上的损失。
代码为:
1 def train(model): 2 model.train() 3 4 #各种优化算法均可以加入正则化项,避免过拟合,参数regularization_coeff调节正则化项的权重 5 opt = paddle.optimizer.Adam(learning_rate=0.01, weight_decay=paddle.regularizer.L2Decay(coeff=1e-5), parameters=model.parameters()) 6 7 EPOCH_NUM = 5 8 for epoch_id in range(EPOCH_NUM): 9 for batch_id, data in enumerate(train_loader()): 10 #准备数据,变得更加简洁 11 images, labels = data 12 images = paddle.to_tensor(images) 13 labels = paddle.to_tensor(labels) 14 15 #前向计算的过程,同时拿到模型输出值和分类准确率 16 predicts, acc = model(images, labels) 17 #计算损失,取一个批次样本损失的平均值 18 loss = F.cross_entropy(predicts, labels) 19 avg_loss = paddle.mean(loss) 20 21 #每训练了100批次的数据,打印下当前Loss的情况 22 if batch_id % 200 == 0: 23 print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(), acc.numpy())) 24 25 #后向传播,更新参数的过程 26 avg_loss.backward() 27 opt.step() 28 opt.clear_grad() 29 30 #保存模型参数 31 paddle.save(model.state_dict(), 'mnist_regul.pdparams') 32 33 model = MNIST() 34 train(model)
结果为:
利用已经有的模型:
1 def evaluation(model):
2 print('start evaluation .......')
3 # 定义预测过程
4 params_file_path = 'mnist.pdparams'
5 # 加载模型参数
6 param_dict = paddle.load(params_file_path)
7 model.load_dict(param_dict)
8
9 model.eval()
10 eval_loader = load_data('eval')
11
12 acc_set = []
13 avg_loss_set = []
14 for batch_id, data in enumerate(eval_loader()):
15 images, labels = data
16 images = paddle.to_tensor(images)
17 labels = paddle.to_tensor(labels)
18 predicts, acc = model(images, labels)
19 loss = F.cross_entropy(input=predicts, label=labels)
20 avg_loss = paddle.mean(loss)
21 acc_set.append(float(acc.numpy()))
22 avg_loss_set.append(float(avg_loss.numpy()))
23
24 #计算多个batch的平均损失和准确率
25 acc_val_mean = np.array(acc_set).mean()
26 avg_loss_val_mean = np.array(avg_loss_set).mean()
27
28 print('loss={}, acc={}'.format(avg_loss_val_mean, acc_val_mean))
29
30 model = MNIST()
31 evaluation(model)
结果为:
3.3 可视化分析
即使用相关绘图工具绘制损失随训练下降的曲线图,这里用到的是Matplotlib库。步骤为:
①:将训练的批次编号作为X轴坐标,该批次的训练损失作为Y轴坐标。训练开始前,声明两个列表变量存储对应的批次编号(iters=[])和训练损失(losses=[])。
1 iters=[] 2 losses=[] 3 for epoch_id in range(EPOCH_NUM): 4 """start to training"""
②:随着训练的进行,将iter和losses两个列表填满。
1 import paddle.nn.functional as F 2 3 iters=[] 4 losses=[] 5 for epoch_id in range(EPOCH_NUM): 6 for batch_id, data in enumerate(train_loader()): 7 images, labels = data 8 predicts, acc = model(images, labels) 9 loss = F.cross_entropy(predicts, label = labels.astype('int64')) 10 avg_loss = paddle.mean(loss) 11 # 累计迭代次数和对应的loss 12 iters.append(batch_id + epoch_id*len(list(train_loader())) 13 losses.append(avg_loss)
③:训练结束后,将两份数据以参数形式导入PLT的横纵坐标。
1 plt.xlabel("iter", fontsize=14),plt.ylabel("loss", fontsize=14)
④:调用plt.plot()函数即可完成作图。
1 plt.plot(iters, losses,color='red',label='train loss')
详细代码为:
1 #引入matplotlib库 2 import matplotlib.pyplot as plt 3 4 def train(model): 5 model.train() 6 7 opt = paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters()) 8 9 EPOCH_NUM = 10 10 iter=0 11 iters=[] 12 losses=[] 13 for epoch_id in range(EPOCH_NUM): 14 for batch_id, data in enumerate(train_loader()): 15 #准备数据,变得更加简洁 16 images, labels = data 17 images = paddle.to_tensor(images) 18 labels = paddle.to_tensor(labels) 19 20 #前向计算的过程,同时拿到模型输出值和分类准确率 21 predicts, acc = model(images, labels) 22 #计算损失,取一个批次样本损失的平均值 23 loss = F.cross_entropy(predicts, labels) 24 avg_loss = paddle.mean(loss) 25 26 #每训练了100批次的数据,打印下当前Loss的情况 27 if batch_id % 100 == 0: 28 print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(), acc.numpy())) 29 iters.append(iter) 30 losses.append(avg_loss.numpy()) 31 iter = iter + 100 32 33 #后向传播,更新参数的过程 34 avg_loss.backward() 35 opt.step() 36 opt.clear_grad() 37 38 #保存模型参数 39 paddle.save(model.state_dict(), 'mnist.pdparams') 40 41 return iters, losses 42 43 model = MNIST() 44 iters, losses = train(model) 45 46 #画出训练过程中Loss的变化曲线 47 plt.figure() 48 plt.title("train loss", fontsize=24) 49 plt.xlabel("iter", fontsize=14) 50 plt.ylabel("loss", fontsize=14) 51 plt.plot(iters, losses,color='red',label='train loss') 52 plt.grid() 53 plt.show()
结果为:
4、模型加载与恢复
有时候会存在运行了很久的模型因为一些因素而中断,但是这种情况下是很需要能够恢复中断的训练,这样可以减少很多不必要的时间成本的浪费。而飞桨平台则提供了这样的功能。
从恢复训练的损失变化来看,加载模型参数继续训练的损失函数值和正常训练损失函数值是相差不多的,总结为:
①:保存模型时同时保存模型参数和优化器参数
1 paddle.save(opt.state_dict(), 'model.pdopt') 2 paddle.save(model.state_dict(), 'model.pdparams')
②:恢复参数时同时恢复模型参数和优化器参数
1 model_dict = paddle.load("model.pdparams") 2 opt_dict = paddle.load("model.pdopt") 3 4 model.set_state_dict(model_dict) 5 opt.set_state_dict(opt_dict)
详情见参考资料:https://aistudio.baidu.com/aistudio/projectdetail/4389171
5、动转静部署
深度学习框架实现一个新的模式,同时具备动态图高易用性与静态图高性能的特点。
动态图转静态图训练:原理是通过分析Python代码,将动态图代码转写为静态图代码,并在底层自动使用静态图执行器运行。其基本使用方法十分简便,只需要在要转化的函数前添加一个装饰器 @paddle.jit.to_static即可实现动态图转静态图模式运行,进行模型训练或者推理部署。
①:构建了仅有一层全连接层的手写字符识别网络。在forward函数之前加了装饰器@paddle.jit.to_static
,要求模型在静态图模式下运行:
1 # 定义手写数字识别模型
2 class MNIST(paddle.nn.Layer):
3 def __init__(self):
4 super(MNIST, self).__init__()
5
6 # 定义一层全连接层,输出维度是1
7 self.fc = paddle.nn.Linear(in_features=784, out_features=10)
8
9 # 定义网络结构的前向计算过程
10 @paddle.jit.to_static # 添加装饰器,使动态图网络结构在静态图模式下运行
11 def forward(self, inputs):
12 outputs = self.fc(inputs)
13 return outputs
②:飞桨实现动转静的功能是在内部完成的,对使用者来说,动态图的训练代码和动转静模型的训练代码是完全一致的:
1 import paddle
2 import paddle.nn.functional as F
3 # 确保从paddle.vision.datasets.MNIST中加载的图像数据是np.ndarray类型
4 paddle.vision.set_image_backend('cv2')
5
6 # 图像归一化函数,将数据范围为[0, 255]的图像归一化到[-1, 1]
7 def norm_img(img):
8 batch_size = img.shape[0]
9 # 归一化图像数据
10 img = img/127.5 - 1
11 # 将图像形式reshape为[batch_size, 784]
12 img = paddle.reshape(img, [batch_size, 784])
13
14 return img
15
16 def train(model):
17 model.train()
18 # 加载训练集 batch_size 设为 16
19 train_loader = paddle.io.DataLoader(paddle.vision.datasets.MNIST(mode='train'),
20 batch_size=16,
21 shuffle=True)
22 opt = paddle.optimizer.SGD(learning_rate=0.001, parameters=model.parameters())
23 EPOCH_NUM = 10
24 for epoch in range(EPOCH_NUM):
25 for batch_id, data in enumerate(train_loader()):
26 images = norm_img(data[0]).astype('float32')
27 labels = data[1].astype('int64')
28
29 #前向计算的过程
30 predicts = model(images)
31
32 # 计算损失
33 loss = F.cross_entropy(predicts, labels)
34 avg_loss = paddle.mean(loss)
35
36 #每训练了1000批次的数据,打印下当前Loss的情况
37 if batch_id % 1000 == 0:
38 print("epoch_id: {}, batch_id: {}, loss is: {}".format(epoch, batch_id, avg_loss.numpy()))
39
40 #后向传播,更新参数的过程
41 avg_loss.backward()
42 opt.step()
43 opt.clear_grad()
44
45
46 model = MNIST()
47
48 train(model)
49
50 paddle.save(model.state_dict(), './mnist.pdparams')
51 print("==>Trained model saved in ./mnist.pdparams")
结果为:
6、总结
通过本周的学习,首先对整个手写数字识别的神经网络模型有了一个较为清晰的认识,整个流程的实现以及优化。但是对于其中的很多细节还是比较晦涩的,例如模型恢复、加载的点没有更深入的了解,还有熵的学习也还是比较难以理解,特别是卷积神经网络,这是需要不断的去学习和理解的。
7、参考资料
网络模型拆分:https://www.cnblogs.com/JamesDYX/p/10085746.html
单机单卡,单机多卡了解:https://blog.csdn.net/love1005lin/article/details/116404049
改为单机多卡程序:https://aistudio.baidu.com/aistudio/projectdetail/4388894
模型加载以及恢复训练:https://aistudio.baidu.com/aistudio/projectdetail/4389171