【PaddlePaddle】手写数字识别模型

目录

1. 加载paddle, numpy等相关类库

2. 数据处理

3. 模型设计

1. 优化算法

2. 分布式训练 

3. 检查模型训练过程

4. 模型加载和恢复训练

5. 可视化分析

4. 预测数据

1. 加载paddle, numpy等相关类库

import paddle
import paddle.fluid as fluid
from paddle.fluid.dygraph.nn import Linear, Conv2D, Pool2D
import numpy as np
import os
import gzip
import json
import random
import matplotlib.pyplot as plt
from PIL import Image

2. 数据处理

  1. 加载数据集

  2. 划分训练集、验证集和测试集

  3. 分批次读取训练数据

  4. 打乱数据

  5. 数据校验

def load_data(mode='train'):
    # 加载数据集
    datafile = './work/mnist.json.gz'
    print('loading mnist dataset from {} ......'.format(datafile))
    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))
    
    # 校验数据,图像数量和标签数量是否相等
    imgs_length = len(imgs)
    assert len(imgs) == len(labels), \
          "length of train_imgs({}) should be the same as train_labels({})".format(len(imgs), len(label))
    
    
    # 定义数据集每个数据的序号,根据序号读取数据
    index_list = list(range(imgs_length))
    BATCHSIZE = 100
    IMG_ROWS = 28
    IMG_COLS = 28
    
    # 定义数据生成器
    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

3. 模型设计

1. 优化算法

目前四种比较成熟的优化算法:SGD、Momentum、AdaGrad和Adam

 

  • SGD: 随机梯度下降算法,每次训练少量数据,抽样偏差导致参数收敛过程中震荡。

  • Momentum: 引入物理“动量”的概念,累积速度,减少震荡,使参数更新的方向更稳定。

每个批次的数据含有抽样误差,导致梯度更新的方向波动较大。如果我们引入物理动量的概念,给梯度下降的过程加入一定的“惯性”累积,就可以减少更新路径上的震荡,即每次更新的梯度由“历史多次梯度的累积方向”和“当次梯度”加权相加得到。历史多次梯度的累积方向往往是从全局视角更正确的方向,这与“惯性”的物理概念很像,也是为何其起名为“Momentum”的原因。

  • AdaGrad: 根据不同参数距离最优解的远近,动态调整学习率。学习率逐渐下降,依据各参数变化大小调整学习率。

通过调整学习率的实验可以发现:当某个参数的现值距离最优解较远时(表现为梯度的绝对值较大),我们期望参数更新的步长大一些,以便更快收敛到最优解。当某个参数的现值距离最优解较近时(表现为梯度的绝对值较小),我们期望参数的更新步长小一些,以便更精细的逼近最优解。参数更新的步长应该随着优化过程逐渐减少,减少的程度与当前梯度的大小有关。根据这个思想编写的优化算法称为“AdaGrad”,Ada是Adaptive的缩写,表示“适应环境而变化”的意思。RMSProp是在AdaGrad基础上的改进,AdaGrad会累加之前所有的梯度平方,而RMSprop仅仅是计算对应的梯度平均值,因而可以解决AdaGrad学习率急剧下降的问题。

  • Adam: 由于动量和自适应学习率两个优化思路是正交的,因此可以将两个思路结合起来,这就是当前广泛应用的算法。
# 定义网络结构,同上一节所使用的网络结构
class MNIST(fluid.dygraph.Layer):
    def __init__(self):
        super(MNIST, self).__init__()
        self.conv1 = Conv2D(num_channels=1, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
        self.pool1 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
        self.conv2 = Conv2D(num_channels=20, num_filters=20, filter_size=5, stride=1, padding=2, act='relu')
        self.pool2 = Pool2D(pool_size=2, pool_stride=2, pool_type='max')
        self.fc = Linear(input_dim=980, output_dim=10, act='softmax')

    def forward(self, inputs):
        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)
        return outputs5

# 训练配置,并启动训练过程
# 在使用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()
    train_loader = load_data('train')

    # 创建异步数据读取器
    # place = fluid.CPUPlace()
    # data_loader = fluid.io.DataLoader.from_generator(capacity=5, return_list=True)
    # data_loader.set_batch_generator(train_loader, places=place)
    
    # 设置学习率和L2正则化系数,防止模型过拟合
    optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001, regularization=fluid.regularizer.L2Decay(regularization_coeff=0.1), 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())
    
    # 读取数据,定义数据生成器后,该部分代码更加简洁
    EPOCH_NUM = 10
    Batch = 0
    Batchs = []
    all_train_loss = []
    all_train_accs = []
    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.square_error_cost(predict, label)
            loss = fluid.layers.cross_entropy(predict, label)
            avg_loss = fluid.layers.mean(loss)
            if batch_id % 200 == 0:
                print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.numpy(), avg_acc.numpy()))
            Batchs.append(Batch)
            all_train_loss.append(avg_loss.numpy())
            all_train_accs.append(avg_acc.numpy())
            Batch = Batch + 200

            #后向传播
            avg_loss.backward()
            optimizer.minimize(avg_loss)
            model.clear_gradients()

    #保存模型
    fluid.save_dygraph(model.state_dict(), 'mnist')
    # 保存模型参数和优化器的参数
    fluid.save_dygraph(model.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))
    fluid.save_dygraph(optimizer.state_dict(), './checkpoint/mnist_epoch{}'.format(epoch_id))

2. 分布式训练 

在工业实践中,很多较复杂的任务需要使用更强大的模型。强大模型加上海量的训练数据,经常导致模型训练耗时严重。

在机器资源充沛的情况下,建议采用分布式训练,大部分模型的训练时间可压缩到小时级别。

分布式训练有两种实现模式:模型并行数据并行

(1)模型并行

模型并行是将一个网络模型拆分为多份,拆分后的模型分到多个设备上(GPU)训练,每个设备的训练数据是相同的。模型并行的实现模式可以节省内存,但是应用较为受限。

模型并行的方式一般适用于如下两个场景:

  1. 模型架构过大: 完整的模型无法放入单个GPU。如2012年ImageNet大赛的冠军模型AlexNet是模型并行的典型案例,由于当时GPU内存较小,单个GPU不足以承担AlexNet,因此研究者将AlexNet拆分为两部分放到两个GPU上并行训练。

  2. 网络模型的结构设计相对独立: 当网络模型的设计结构可以并行化时,采用模型并行的方式。如在计算机视觉目标检测任务中,一些模型(如YOLO9000)的边界框回归和类别预测是独立的,可以将独立的部分放到不同的设备节点上完成分布式训练。

(2)数据并行

数据并行与模型并行不同,数据并行每次读取多份数据,读取到的数据输入给多个设备(GPU)上的模型,每个设备上的模型是完全相同的。值得注意的是,每个设备的输入数据不同,因此每个设备的模型计算出的梯度是不同的。如果每个设备的梯度只更新当前设备的模型,就会导致下次训练时,每个模型的参数都不相同。因此我们还需要一个梯度同步机制,保证每个设备的梯度是完全相同的。

梯度同步有两种方式:PRC通信方式NCCL2通信方式(Nvidia Collective multi-GPU Communication Library)

  1. PRC通信方式:通常用于CPU分布式训练,它有两个节点:参数服务器Parameter server和训练节点Trainer。parameter server收集来自每个设备的梯度更新信息,并计算出一个全局的梯度更新。Trainer用于训练,每个Trainer上的程序相同,但数据不同。当Parameter server收到来自Trainer的梯度更新请求时,统一更新模型的梯度。
  2. NCCL2通信方式:进行分布式训练,不需要启动Parameter server进程,每个Trainer进程保存一份完整的模型参数,在完成梯度计算之后通过Trainer之间的相互通信,Reduce梯度数据到所有节点的所有设备,然后每个节点再各自完成参数更新。
                                         PRC通信方式                                        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()         # 新增

 修改后的训练过程代码如下

def train_multi_gpu():
    ##修改1-从环境变量获取使用GPU的序号
    place = fluid.CUDAPlace(fluid.dygraph.parallel.Env().dev_id)

    with fluid.dygraph.guard(place):
        ##修改2-对原模型做并行化预处理
        strategy = fluid.dygraph.parallel.prepare_context()
        model = MNIST()
        model = fluid.dygraph.parallel.DataParallel(model, strategy)

        model.train()
        train_loader = load_data('train')

        ##修改3-多GPU数据读取,必须确保每个进程读取的数据是不同的
        train_loader = fluid.contrib.reader.distributed_batch_reader(train_loader)
        
        # ...  同修改前的代码一致
        
                # 修改4-多GPU训练需要对Loss做出调整,并聚合不同设备上的参数梯度
                avg_loss = model.scale_loss(avg_loss)
                avg_loss.backward()
                model.apply_collective_grads()

        # ...  同修改前的代码一致

终端运行 train_multi_gpu.py

$ python -m paddle.distributed.launch --selected_gpus=0,1,2,3 --log_dir ./mylog train_multi_gpu.py
  • paddle.distributed.launch:启动分布式运行。
  • selected_gpus:设置使用的GPU的序号(需要是多GPU卡的机器,通过命令watch nvidia-smi查看GPU的序号)。
  • log_dir:存放训练的log,若不设置,每个GPU上的训练信息都会打印到屏幕。
  • train_multi_gpu.py:多GPU训练的程序,包含修改过的train_multi_gpu()函数。

训练完成后,在指定的./mylog文件夹下会产生四个日志文件,其中worklog.0的内容如下:

grep: warning: GREP_OPTIONS is deprecated; please use an alias or script
dev_id 0
I1104 06:25:04.377323 31961 nccl_context.cc:88] worker: 127.0.0.1:6171 is not ready, will retry after 3 seconds...
I1104 06:25:07.377645 31961 nccl_context.cc:127] init nccl context nranks: 3 local rank: 0 gpu id: 1↩
W1104 06:25:09.097079 31961 device_context.cc:235] Please NOTE: device: 1, CUDA Capability: 61, Driver API Version: 10.1, Runtime API Version: 9.0
W1104 06:25:09.104460 31961 device_context.cc:243] device: 1, cuDNN Version: 7.5.
start data reader (trainers_num: 3, trainer_id: 0)
epoch: 0, batch_id: 10, loss is: [0.47507238]
epoch: 0, batch_id: 20, loss is: [0.25089613]
epoch: 0, batch_id: 30, loss is: [0.13120805]
epoch: 0, batch_id: 40, loss is: [0.12122715]
epoch: 0, batch_id: 50, loss is: [0.07328521]
epoch: 0, batch_id: 60, loss is: [0.11860339]
epoch: 0, batch_id: 70, loss is: [0.08205047]
epoch: 0, batch_id: 80, loss is: [0.08192863]
epoch: 0, batch_id: 90, loss is: [0.0736289]
epoch: 0, batch_id: 100, loss is: [0.08607423]
start data reader (trainers_num: 3, trainer_id: 0)
epoch: 1, batch_id: 10, loss is: [0.07032011]
epoch: 1, batch_id: 20, loss is: [0.09687119]
epoch: 1, batch_id: 30, loss is: [0.0307216]
.....

3. 检查模型训练过程

在网络定义的forward函数中,可以打印每一层输入输出的尺寸,以及每层网络的参数。通过查看这些信息,不仅可以更好地理解训练的执行过程,还可以发现潜在问题,或者启发继续优化的思路。

在下述程序中,使用check_shape变量控制是否打印“尺寸”,验证网络结构是否正确。使用check_content变量控制是否打印“内容值”,验证数据分布是否合理。假如在训练中发现中间层的部分输出持续为0,说明该部分的网络结构设计存在问题,没有充分利用。

def forward(self, inputs, label=None, check_shape=False, check_content=False):
    # ... 同网络定义中代码一致

        # 选择是否打印神经网络每层的参数尺寸和输出尺寸,验证网络结构是否设置正确
        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')
            
        # 如果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)

4. 模型加载和恢复训练

之前我们已经将训练好的模型保存到磁盘文件,这样应用程序可以随时加载模型,完成预测任务。但是在日常训练工作中,我们会遇到一些突发情况,导致训练过程主动或被动的中断,因此我们还要随时保存训练过程中的模型状态,防止中断训练后从初始状态重新训练。加载模型参数代码如下:

params_path = "./checkpoint/mnist_epoch0"

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)

    optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001, parameter_list=model.parameters())
    optimizer.set_dict(opt_dict)

    # ...  同训练过程的代码一致

5. 可视化分析

定义画loss曲线和acc曲线的函数:

def draw_train_acc(Batchs, train_accs):
    title="training accs"
    plt.title(title, fontsize=24)
    plt.xlabel("batch", fontsize=14)
    plt.ylabel("acc", fontsize=14)
    plt.plot(Batchs, train_accs, color='green', label='training accs')
    plt.legend()
    plt.grid()
    plt.show()

def draw_train_loss(Batchs, train_loss):
    title="training loss"
    plt.title(title, fontsize=24)
    plt.xlabel("batch", fontsize=14)
    plt.ylabel("loss", fontsize=14)
    plt.plot(Batchs, train_loss, color='red', label='training loss')
    plt.legend()
    plt.grid()
    plt.show()

 在模型训练过程的最后加入如下代码:

draw_train_acc(Batchs,all_train_accs)
draw_train_loss(Batchs,all_train_loss)

4. 预测数据

# 读取一张本地的样例图片,转变成模型输入的格式
def load_image(img_path):
    # 从img_path中读取图像,并转为灰度图
    im = Image.open(img_path).convert('L')
    im.show()
    im = im.resize((28, 28), Image.ANTIALIAS)
    im = np.array(im).reshape(1, 1, 28, 28).astype(np.float32)
    # 图像归一化
    im = 1.0 - im / 255.
    return im

# 定义预测过程
with fluid.dygraph.guard():
    model = MNIST()
    params_file_path = 'mnist'
    img_path = './work/example_0.jpg'
    # 加载模型参数
    model_dict, _ = fluid.load_dygraph("mnist")
    model.load_dict(model_dict)
    
    model.eval()
    tensor_img = load_image(img_path)
    #模型反馈10个分类标签的对应概率
    results = model(fluid.dygraph.to_variable(tensor_img))
    #取概率最大的标签作为预测输出
    lab = np.argsort(results.numpy())
    print("本次预测的数字是: ", lab[0][-1])

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值