基于飞桨实现手写数字识别2

参考课程笔记:https://aistudio.baidu.com/aistudio/projectdetail/728143

上篇https://mp.csdn.net/console/editor/html/108276229写了主要思想和实现流程;这篇针对手写数字识别进行具体的实现,并对各个环节进行优化以达到最优的训练效果。

1. 数据处理

数据处理通常包括读取文件、预处理及数据异步三部分。在工业实践中,我们面临的任务和数据环境千差万别,通常需要自己编写适合当前任务的数据处理程序,一般涉及如下五个环节:

  • 读入数据
  • 划分数据集
  • 生成批次数据
  • 训练样本集乱序
  • 校验数据的有效性

首先要加载飞桨和数据处理库:

# 加载飞桨和相关数据处理的库
import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Linear
import numpy as np
#OS用于操作和处理文件路径,执行与操作系统相关命令
import os
#压缩、创建、解压相关文件
import gzip
#JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写
import json
import random

1.1 读入数据并划分数据集

在实际应用中,保存到本地的数据存储格式多种多样,如MNIST数据集以json格式存储在本地,其数据存储结构如 图3所示

图3:MNIST数据集的存储结构
  • train_set(训练集):用于确定模型参数。包括50000条训练样本,每个样本包含手写数字图片和对应的标签。

                                           train_set包含两个元素的列表:train_images(28*28灰度图片)train_labels

  • val_set(验证集):用于调节模型超参数(如多个网络结构、正则化权重的最优选择)。
  • test_set(测试集):用于估计应用效果(没有在模型中应用过的数据,更贴近模型在真实场景应用的效果)。
  • 小建议:当几个模型的准确率在测试集上差距不大时,尽量选择网络结构相对简单的模型。往往越精巧设计的模型和方法,越不容易在不同的数据集之间迁移。
#———————————— 声明数据文件的位置 ————————————————
datafile = './data/data17155/mnist.json.gz'
print('loading mnist dataset from {} .....'.format(datafile))
#———————————— 加载json文件 ————————————————————
data = json.load(gzip.open(datafile))
print('mnist dataset load done')
#————————————— 读取数据区分训练集、验证集、测试集 ——————————
train_set, val_set, eval_set = data

# ———————————— 数据集相关参数 ——————————————————
#图片高度IMG_ROWS, 图片宽度IMG_COLS
IMG_ROWS = 28
IMG_COLS = 28

#———————— 打印数据信息 ——————————————
# 观察训练集数量
imgs, labels = train_set[0], train_set[1]
# len() 方法返回对象(字符、列表、元组等)长度或项目个数
print("训练数据集数量: ", len(imgs))
# 观察验证集数量
imgs, labels = val_set[0], val_set[1]
print("验证数据集数量: ", len(imgs))
# 观察测试集数量
imgs, labels = val= eval_set[0], eval_set[1]
print("测试数据集数量: ", len(imgs))

1.2 训练样本乱序、生成批次数据

  • 训练样本乱序: 先将样本按顺序进行编号,建立ID集合index_list。然后将index_list乱序,最后按乱序后的顺序读取数据。
  • 说明:

    通过大量实验发现,模型对最后出现的数据印象更加深刻。训练数据导入后,越接近模型训练结束,最后几个批次数据对模型参数的影响越大。为了避免模型记忆影响训练效果,需要进行样本乱序操作。

  • 生成批次数据: 先设置合理的batch_size,再将数据转变成符合模型输入要求的np.array格式返回。同时,在返回数据时将Python生成器设置为yield模式,以减少内存占用。


一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。

yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。

https://www.runoob.com/w3cnote/python-yield-used-analysis.html


#_________________ 训练样本乱序 ____________________
#获取训练数据集长度
imgs_length = len(imgs1)
#定义数据集每个数据的序号根据序号读取数据
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(imgs1[i], [1, IMG_ROWS, IMG_COLS]).astype('float32')
        label = np.reshape(labels1[i], [1]).astype('float32')
        imgs_list.append(img)
        labels_list.append(label)
        if len(imgs_list) == BATCHSIZE:
            #获得一个batchsize的数据,并返回
            #带有 yield 的函数在 Python 中被称之为 generator(生成器)
            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 = data_generator
# 以迭代的形式读取数据
for batch_id, data in enumerate(train_loader()):
    image_data, label_data = data
    if batch_id == 0:
        # 打印数据shape和类型
        print("打印第一个batch数据的维度及数据类型:")
        print("图像维度: {}, 标签维度: {}, 图像数据类型: {}, 标签数据类型: {}".format(image_data.shape, label_data.shape, type(image_data), type(label_data)))
    break

1.3 校验数据有效性

在实际应用中,原始数据可能存在标注不准确、数据杂乱或格式不统一等情况。因此在完成数据处理流程后,还需要进行数据校验,一般有两种方式:

1.3.1 机器校验

加入一些校验和清理数据的操作。本文主要对比数据集中图片数量和标签数量,使用assert语句来实现。

#———————————————————— 机器校验 ————————————————————————
assert len(imgs1) == len(labels1),   \
                    "length of train_imgs({}) should be the same as train_labels({})".format(len(imgs1), len(labels1))

1.3.2 人工校验

先打印数据输出结果,观察是否是设置的格式;再从训练的结果验证数据处理和读取的有效性。(观察数据的shape与类型是否与函数中设置的一致)

#———————————————————— 人工校验 ————————————————————————
# 声明数据读取函数,从训练集中读取数据
train_loader = data_generator
# 以迭代的形式读取数据
for batch_id, data in enumerate(train_loader()):
    image_data, label_data = data
    if batch_id == 0:
        # 打印数据shape和类型
        print("打印第一个batch数据的维度,以及数据的类型:")
        print("图像维度: {}, 标签维度: {}, 图像数据类型: {}, 标签数据类型: {}".format(image_data.shape, label_data.shape, type(image_data), type(label_data)))
    break

 1.4 封装数据读取与处理函数

将上述的整套数据流程封装到一个函数中,以便训练时候调用

#———————————————————————————— 封装数据读取与处理函数 ——————————————————————————————————————————————
def load_data(mode='train'):
    datafile = './data/data17155/mnist.json.gz'
    print('loading mnist dataset from {} ......'.format(datafile))
    # 加载json数据文件
    data = json.load(gzip.open(datafile))
    print('mnist dataset load done')
   
    # 读取到的数据区分训练集,验证集,测试集
    train_set, val_set, eval_set = data
    if mode=='train':
        # 获得训练数据集
        imgs, labels = train_set[0], train_set[1]
    elif mode=='valid':
        # 获得验证数据集
        imgs, labels = val_set[0], val_set[1]
    elif mode=='eval':
        # 获得测试数据集
        imgs, labels = eval_set[0], eval_set[1]
    else:
        raise Exception("mode can only be one of ['train', 'valid', 'eval']")
    print("训练数据集数量: ", len(imgs))
    
    # 校验数据
    assert len(imgs) == len(labels), \
          "length of train_imgs({}) should be the same as train_labels({})".format(len(imgs), len(label))
    
    # 获得数据集长度
    imgs_length = len(imgs)
    
    # 定义数据集每个数据的序号,根据序号读取数据
    index_list = list(range(imgs_length))
    # 读入数据时用到的批次大小
    BATCHSIZE = 100
    
    # 定义数据生成器
    def data_generator():
        if mode == 'train':
            # 训练模式下打乱数据
            random.shuffle(index_list)
        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')
            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

 1.5 异步读取

  • 上述所示方法为同步读取,此时数据读取和模型训练串行,模型训练开始于数据读取结束之后。
  • 当样本量较大,数据读取较慢时需采用异步读取的方式。异步读取时,数据读取和模型训并行,读取到的数据不断放入缓存区,以牺牲小部分内存来换取数据读取率的上升,在数据量较大时效果较明显。二者关系见图4:
  • 异步队列:数据读取和模型训练交互的仓库,二者均可以从仓库中读取数据,它的存在使得两者的工作节奏可以解耦。
图4: 同步读取和异步读取示意图

 与同步数据读取相比,异步数据读取仅增加了三行代码,如下所示:

place = fluid.CPUPlace()
# 定义数据读取后存放的位置,CPU或者GPU
# place = fluid.CUDAPlace(0) 时,数据才读取到GPU上

data_loader = fluid.io.DataLoader.from_generator(capacity=5, return_list=True) 
# 创建一个DataLoader对象用于加载Python生成器产生的数据。数据会由Python线程预先读取,并异步送入一个队列中。
#capacity表数据容量

data_loader.set_batch_generator(train_loader, places=place) 
# 迭代的读取数据并打印数据的形状
# 用创建的DataLoader对象设置一个数据生成器set_batch_generator,输入的参数是一个Python数据生成器train_loader和服务器资源类型place(标明CPU还是GPU)

fluid.io.DataLoader.from_generator参数名称和含义如下:

  • feed_list:仅在PaddlePaddle静态图中使用,动态图中设置为“None”,本教程默认使用动态图的建模方式;
  • capacity:表示在DataLoader中维护的队列容量,如果读取数据的速度很快,建议设置为更大的值;
  • use_double_buffer:是一个布尔型的参数,设置为“True”时,Dataloader会预先异步读取下一个batch的数据并放到缓存区;
  • iterable:表示创建的Dataloader对象是否是可迭代的,一般设置为“True”;
  • return_list:在动态图模式下需要设置为“True”。

 

2. 网络设计

2.1 全连接神经网络

包含输入层、隐含层和输出层。隐含层的存在是为了增加网络的深度和复杂度,提高网络的表示能力。通过引入非线性激活函数增加了神经网络的非线性能力。另外,隐含层的节点数是可以调整的,节点数越多,神经网络的表达能力越强,一般来说隐含层的节点数目是低于输入层的,以便于对关键信息做抽象。

图5: 该任务中使用的全连接神经网络结构

Sigmoid激活函数y=\frac{1}{1+e^{-x}}

#多层全连接神经网络的实现
class MNIST(fluid.dygraph.Layer):
    def __init__(self):
        super(MNIST, self).__init__()
        #定义两层全连接隐含层(节点设置为10),输出维度是10,激活函数为Sigmoid
        self.fc1 = Linear(input_dim=784, output_dim=10, act='sigmoid')
        self.fc2 = Linear(input_dim=10, output_dim=10, act='sigmoid')
         # 定义一层全连接输出层,输出维度是1,不使用激活函数
        self.fc3 = Linear(input_dim=10, output_dim=1, act=None)
    #定义向前计算
    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

#————————————网络结构部分之后的代码,保持不变————————————
with fluid.dygraph.guard():
    model = MNIST()
    model.train()
    #调用加载数据的函数,获得MNIST训练数据集
    train_loader = load_data('train')
    #使用SGD优化器,learning_rate设置为0.01
    #parameter_list用来设置参与训练的参数
    optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01, parameter_list=model.parameters())
    # 训练5轮
    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.square_error_cost(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')

2.2 卷积神经网络

对于常见的视觉问题,常用的且效果较好的模型仍然是卷积神经网络。卷积神经网络由卷积层和池化层组成,如图6所示。卷积层负责对输入对象进行扫描以生成更抽象的特征表示,池化层则是对这些特征进行过滤,以保留最关键的特征信息。

卷积的思想主要是局部思想和参数共享。对于目标对象,只关注其在的局部特征;另外对于同一图片的各个部分处理方式是类似的,因此针对同一目的只需要同一卷积核对整张图像的各个部分进行处理。

图6: 卷积神经网络
#多层卷积神经网络的实现
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):
         #inputs = fluid.layers.reshape(inputs, [inputs.shape[0], 784])
         outputs1 = self.conv1(inputs)
         outputs2 = self.pool1(outputs1)
         outputs3 = self.conv2(outputs2)
         outputs4 = self.pool2(outputs3)
         outputs5 = fluid.layers.reshape(outputs4, [outputs4.shape[0], -1])
         outputs_final = self.fc(outputs5)
         return outputs_final

3. 损失函数

分类任务希望最终输出的是一个与类别数一致的维度的向量,向量的每个元素对应属于该分类标签的概率。因此需要引入Softmax函数,它可以将原始输出转变成对应标签的概率,公式如下,其中CCC是标签类别个数。

softmax(x_{i})=\frac{e^{x_{i}}}{\sum_{j=0}^{N}{e_{j}^{x}}}, i=0,...,C-1

对于二分类问题,使用两个输出接入softmax作为输出层,等价于使用单一输出接入Sigmoid函数。

交叉熵损失函数的设计是基于最大似然思想,求在已知测试数据的情况下模型参数发生的概率的最大等价于最小化交叉熵损失函数,交叉熵损失函数公式如下:(其中,log为以e为底的自然对数,y_{k}代表模型的输出,t_{k}代表各个标签。)

L=-[\sum_{k=1}^{N}t_{k}logy_{k}+(1-t_{k}log(1-y_{k}))]

#数据处理部分,需修改标签变量格式
label = np.reshape(labels[i], [1]).astype('int64')
#网络定义部分需修改输出层结构
label = np.reshape(labels[i], [1]).astype('int64')
#损失函数修改为交叉熵损失
loss = fluid.layers.cross_entropy(predict, label)

4. 算法优化

4.1 设置学习率

学习率代表参数更新幅度的大小,即步长。当学习率最优时,模型的有效容量最大,最终能达到的效果最好。学习率与具体的应哟场景有关,选取要适当。可以通过观察损失的变化来调整学习率,代码如下:

#设置不同初始学习率
optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01, parameter_list=model.parameters())
# optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001, parameter_list=model.parameters())
# optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.1, parameter_list=model.parameters())

常见的学习率主流优化算法有:SGD(随机梯度下降,震荡明显)、Momentum(引入“动量”的概念,受历史多次梯度的累计方向的影响,视之为惯性减少震荡)、AdaGrad(根据参数距离最优解的远近调节步长)和Adam(结合前两种方法,当前运用广泛的方法),效果如图。

图7: 学习率算法示意图
#四种优化算法的设置方案,可以逐一尝试效果
optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01, parameter_list=model.parameters())
#optimizer = fluid.optimizer.MomentumOptimizer(learning_rate=0.01, momentum=0.9, parameter_list=model.parameters())
#optimizer = fluid.optimizer.AdagradOptimizer(learning_rate=0.01, parameter_list=model.parameters())
#optimizer = fluid.optimizer.AdamOptimizer(learning_rate=0.01, parameter_list=model.parameters())

4.2 分布式训练

单GPU训练,飞桨动态图通过fluid.dygraph.guard(place=None)里的place参数,设置在GPU上训练还是CPU上训练。

use_gpu = False
place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()
#CPUPlace设置使用CPU资源训神经网络
#CUDAPlace(0)设置使用GPU资源训神经网络,默认使用服务器的第一个GPU卡
#"0"是GPU卡的编号,比如一台服务器有的四个GPU卡,编号分别为0、1、2、3
with fluid.dygraph.guard(place):

分布式训练常见的两种实现模式是:模型并行和数据并行。模型并行适用于模型架构过大(如AlexNet)或结构设计相对独立的情形;当前GPU硬件技术发展比较快,主流GPU内存已满足相应需求了,因此多采用数据并行,数据并行时需注意的是需要实现梯度同步,保证每个设备的梯度更新机制相同。梯度同步有两种方式:PRC通信方式和NCCL2通信方式(Nvidia Collective multi-GPU Communication Library)。

图8 Pserver通信方式的结构
图9 NCCL2通信方式的结构
#在启动训练前,需要配置如下参数
#从环境变量获取设备的ID,并指定给CUDAPlace
device_id = fluid.dygraph.parallel.Env().dev_id
place = fluid.CUDAPlace(device_id)
#对定义的网络做预处理,设置为并行模式
strategy = fluid.dygraph.parallel.prepare_context() 
model = MNIST()
model = fluid.dygraph.parallel.DataParallel(model, strategy)
#定义多GPU训练的reader,不同ID的GPU加载不同的数据集
valid_loader = paddle.batch(paddle.dataset.mnist.test(), batch_size=16, drop_last=true)
valid_loader = fluid.contrib.reader.distributed_batch_reader(valid_loader)
#收集每批次训练数据的loss,并聚合参数的梯度
avg_loss = model.scale_loss(avg_loss)  ## 新增
avg_loss.backward()
mnist.apply_collective_grads()         ## 新增

4.3 调试与优化

1. 计算分类准确率,观测模型效果

损失函数一般不可直接作为衡量模型训练结果的标准,一般可用准确率来衡量模型训练效果,但准确率一般是离散的,不适合用来做损失函数。

2. 检查模型训练过程,识别潜在问题

当模型的损失或评估指标出现异常,通常需要输出某层的输入和输出来定位问题,分析每一层的内容来获取错误的原因。也可以输出一定批次的训练结果和可视化图像(损失函数)来监控模型的训练效果。

  # ************加入对每一层输入和输出的尺寸和数据内容的打印,根据check参数决策是否打印每层的参数和输出尺寸****************

     def forward(self, inputs, label=None, check_shape=False, check_content=False):
         # 给不同层的输出不同命名,方便调试
         outputs1 = self.conv1(inputs)
         outputs2 = self.pool1(outputs1)
         outputs3 = self.conv2(outputs2)
         outputs4 = self.pool2(outputs3)
         _outputs4 = fluid.layers.reshape(outputs4, [outputs4.shape[0], -1])
         outputs5 = self.fc(_outputs4)
         
         # 选择是否打印神经网络每层的参数尺寸和输出尺寸,验证网络结构是否设置正确
         if check_shape:
             # 打印每层网络设置的超参数-卷积核尺寸,卷积步长,卷积padding,池化核尺寸
             print("\n########## print network layer's superparams ##############")
             print("conv1-- kernel_size:{}, padding:{}, stride:{}".format(self.conv1.weight.shape, self.conv1._padding, self.conv1._stride))
             print("conv2-- kernel_size:{}, padding:{}, stride:{}".format(self.conv2.weight.shape, self.conv2._padding, self.conv2._stride))
             print("pool1-- pool_type:{}, pool_size:{}, pool_stride:{}".format(self.pool1._pool_type, self.pool1._pool_size, self.pool1._pool_stride))
             print("pool2-- pool_type:{}, poo2_size:{}, pool_stride:{}".format(self.pool2._pool_type, self.pool2._pool_size, self.pool2._pool_stride))
             print("fc-- weight_size:{}, bias_size_{}, activation:{}".format(self.fc.weight.shape, self.fc.bias.shape, self.fc._act))
             
             # 打印每层的输出尺寸
             print("\n########## print shape of features of every layer ###############")
             print("inputs_shape: {}".format(inputs.shape))
             print("outputs1_shape: {}".format(outputs1.shape))
             print("outputs2_shape: {}".format(outputs2.shape))
             print("outputs3_shape: {}".format(outputs3.shape))
             print("outputs4_shape: {}".format(outputs4.shape))
             print("outputs5_shape: {}".format(outputs5.shape))
             
         # 选择是否打印训练过程中的参数和输出内容,可用于训练过程中的调试
         if check_content:
            # 打印卷积层的参数-卷积核权重,权重参数较多,此处只打印部分参数
             print("\n########## print convolution layer's kernel ###############")
             print("conv1 params -- kernel weights:", self.conv1.weight[0][0])
             print("conv2 params -- kernel weights:", self.conv2.weight[0][0])

             # 创建随机数,随机打印某一个通道的输出值
             idx1 = np.random.randint(0, outputs1.shape[1])
             idx2 = np.random.randint(0, outputs3.shape[1])
             # 打印卷积-池化后的结果,仅打印batch中第一个图像对应的特征
             print("\nThe {}th channel of conv1 layer: ".format(idx1), outputs1[0][idx1])
             print("The {}th channel of conv2 layer: ".format(idx2), outputs3[0][idx2])
             print("The output of last layer:", outputs5[0], '\n')


#****************在向前计算的时候,计算精度*****************
#使用飞桨中的:fluid.layers.accuracy可以直接计算准确率
# 如果label不是None,则计算分类精度并返回
         if label is not None:
             acc = fluid.layers.accuracy(input=outputs5, label=label)
             return outputs5, acc
         else:
             return outputs5


#****前向计算的过程,同时拿到模型输出值和分类准确率****
            if batch_id == 0 and epoch_id==0:
                # 打印模型参数和每层输出的尺寸
                predict, acc = model(image, label, check_shape=True, check_content=False)
            elif batch_id==401:
                # 打印模型参数和每层输出的值
                predict, acc = model(image, label, check_shape=False, check_content=True)
            else:
                predict, acc = model(image, label)

可视化分析

训练模型时,经常需要观察模型的评价指标,分析模型的优化过程,以确保训练是有效的。可选用这两种工具:Matplotlib库和VisualDL。

  • Matplotlib库:Matplotlib库是Python中使用的最多的2D图形绘图库,它有一套完全仿照MATLAB的函数形式的绘图接口,使用轻量级的PLT库(Matplotlib)作图是非常简单的。
  • VisualDL:如果期望使用更加专业的作图工具,可以尝试VisualDL,飞桨可视化分析工具。VisualDL能够有效地展示飞桨在运行过程中的计算图、各种指标变化趋势和数据信息。
#引入matplotlib库
import matplotlib.pyplot as plt

#*******训练开始前,声明两个列表变量存储对应的批次编号(iters=[])和训练损失(losses=[])*********
#将训练的批次编号作为X轴坐标,该批次的训练损失作为Y轴坐标
iters=[]
losses=[]
for epoch_id in range(EPOCH_NUM):
	
    #**********随着训练的进行,将iter和losses两个列表填满***********
    for batch_id, data in enumerate(train_loader()):
        predict, acc = model(image, label)
        loss = fluid.layers.cross_entropy(predict, label)
        avg_loss = fluid.layers.mean(loss)
        # 累计迭代次数和对应的loss
   	    iters.append(batch_id + epoch_id*len(list(train_loader()))
	    losses.append(avg_loss)

#*********训练结束后,将两份数据以参数形式导入PLT的横纵坐标******
plt.xlabel("iter", fontsize=14),plt.ylabel("loss", fontsize=14)

#*********最后,调用plt.plot()函数即可完成作图***************
plt.plot(iters, losses, color='red', label='train loss')

3.加入校验或测试,更好评价模型效果

理想训练情形是训练集和验证集上均有较高的准确率,当训练集上的准确率低于验证集时,可能是发生了过拟合现象,需要加入正则化项来解决相应问题。

  • 训练集 :用于训练模型的参数,即训练过程中主要完成的工作。
  • 校验集 :用于对模型超参数的选择,比如网络结构的调整、正则化项权重的选择等。
  • 测试集 :用于模拟模型在应用后的真实效果。因为测试集没有参与任何模型优化或参数训练的工作,所以它对模型来说是完全未知的样本。在不以校验数据优化网络结构或模型超参数时,校验数据和测试数据的效果是类似的,均更真实的反映模型效果。
#******读取上一步训练保存的模型参数,并输出其在验证集上的效果***************
with fluid.dygraph.guard():
    print('start evaluation .......')
    #加载模型参数
    model = MNIST()
    model_state_dict, _ = fluid.load_dygraph('mnist')
    model.load_dict(model_state_dict)

    model.eval()
    eval_loader = load_data('eval')

    acc_set = []
    avg_loss_set = []
    for batch_id, data in enumerate(eval_loader()):
        x_data, y_data = data
        img = fluid.dygraph.to_variable(x_data)
        label = fluid.dygraph.to_variable(y_data)
        prediction, acc = model(img, label)
        loss = fluid.layers.cross_entropy(input=prediction, label=label)
        avg_loss = fluid.layers.mean(loss)
        acc_set.append(float(acc.numpy()))
        avg_loss_set.append(float(avg_loss.numpy()))
    
    #计算多个batch的平均损失和准确率
    acc_val_mean = np.array(acc_set).mean()
    avg_loss_val_mean = np.array(avg_loss_set).mean()

    print('loss={}, acc={}'.format(avg_loss_val_mean, acc_val_mean))

过拟合与正则化项

造成过拟合的原因:一、模型过于敏感,使用强大模型(表示空间大)的同时训练数据太少,导致在训练数据上表现良好的候选假设太多,锁定了一个“虚假正确”的假设。二、训练数据噪声大,导致模型学习到了噪声而非真实的规律。对于情况1,我们使用数据清洗和修正来解决。 对于情况2,我们或者限制模型表示能力,或者收集更多的训练数据。一般是限制模型的表示能力。

图 10  过拟合现象,训练误差不断降低,但测试误差先降后增;回归与分类模型的欠拟合,理想和过拟合状态的表现

正则化项是为了在无法增大数据量的情况下防止过拟合而做的,可以降低模型的复杂度,主要是通过限制参数数量和可能取值(尽量小)来实现的。

具体来说,在模型的优化目标(损失)中人为加入对参数规模的惩罚项。当参数越多或取值越大时,该惩罚项就越大。通过调整惩罚项的权重系数,可以使模型在“尽量减少训练损失”和“保持模型的泛化能力”之间取得平衡。泛化能力表示模型在没有见过的样本上依然有效。正则化项的存在,增加了模型在训练集上的损失。

#为所有的参数加上统一的正则化项

#各种优化算法均可以加入正则化项,避免过拟合,参数regularization_coeff调节正则化项的权重
#权重越大时,对模型复杂度的惩罚越高
#optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01, regularization=fluid.regularizer.L2Decay(regularization_coeff=0.1),parameter_list=model.parameters()))
optimizer = fluid.optimizer.AdamOptimizer(learning_rate=0.01, regularization=fluid.regularizer.L2Decay(regularization_coeff=0.1),parameter_list=model.parameters())

5. 模型加载及恢复训练

进行恢复训练的程序不仅要保存模型参数,还要保存优化器参数。这是因为某些优化器含有一些随着训练过程变换的参数,例如Adam, AdaGrad等优化器采用可变学习率的策略,随着训练进行会逐渐减少学习率。这些优化器的参数对于恢复训练至关重要。

# 定义学习率,并加载优化器参数到模型中
    total_steps = (int(60000//BATCH_SIZE) + 1) * EPOCH_NUM
    #学习率以多项式曲线从0.01衰减到0.001(polynomial decay)
    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):
    #训练过程
    # 保存模型参数和优化器的参数
        fluid.save_dygraph(model.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))
        fluid.save_dygraph(optimizer.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))

恢复训练有如下两个要点:

  • 保存模型时同时保存模型参数和优化器参数。
  • 恢复参数时同时恢复模型参数和优化器参数。
  1. 使用model.state_dict()获取模型参数。
  2. 使用optimizer.state_dict()获取优化器和学习率相关的参数。
  3. 调用fluid.save_dygraph()将参数保存到本地。
    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()

     

  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值