【项目07】基于AlexNet的图像分类(Fluid)(蝴蝶分类)

>作者:欧新宇(Xinyu OU)
>开发平台:Paddle 2.1 (基于Paddle1.8 Fluid库)
运行环境:Intel Core i7-7700K CPU 4.2GHz, nVidia GeForce GTX 1080 Ti
特别注意:鉴于paddle.fluid将在后期被飞桨弃用,本项目于此次更新后停止更新。
本教案所涉及的数据集仅用于教学和交流使用,请勿用作商用。

最后更新:2021年8月15日


【实验目的】

  1. 学会基于Paddle 1.8版实现卷积神经网络
  2. 学会自己设计AlexNet的类结构,并基于AlexNet模型进行训练、验证和推理
  3. 学会对模型进行整体准确率测评和单样本预测
  4. 熟练函数化编程方法

【实验要求】

  1. 按照给定的网络体系结构图设计卷积神经网络
  2. 使用训练集训练模型,并在训练过程中输出验证集精度
  3. 使用训练好的模型在测试集上输出测试精度
  4. 对给定的测试样本进行预测,输出每一个样本的预测结果该类别的概率
  5. 尽力而为地在测试集上获得最优精度**(除网络模型不能更改,其他参数均可修改)**

【项目逻辑结构图】

建设中...

【实验一】 数据集准备

实验摘要: 对于模型训练的任务,需要数据预处理,将数据整理成为适合给模型训练使用的格式。蝴蝶识别数据集是一个包含有7个不同种类619个样本的数据集,不同种类的样本按照蝴蝶的类别各自放到了相应的文件夹。不同的样本具有不同的尺度,但总体都是规整的彩色图像。

实验目的:

  1. 学会观察数据集的文件结构,考虑是否需要进行数据清理,包括删除无法读取的样本、处理冗长不合规范的文件命名等
  2. 能够按照训练集、验证集、训练验证集、测试集四种子集对数据集进行划分,并生成数据列表
  3. 能够根据数据划分结果和样本的类别,生成包含数据集摘要信息下数据集信息文件 dataset_info.json
  4. 能简单展示和预览数据的基本信息,包括数据量,规模,数据类型和位深度等

1.0 处理数据集中样本命名的非法字符

原始的数据集的名字有可能会存在特殊的命名符号,从而导致在某些情况下无法正确识别。因此,可以通过批量改名的重命名方式来解决该问题。通过观察,本数据集相对规范,不需要进行数据清洗。

1.1 生产图像列表及类别标签

##################################################################################
# 数据集预处理
# 作者: Xinyu Ou (http://ouxinyu.cn)
# 数据集名称:蝴蝶识别数据集
# 数据集简介: Butterfly蝴蝶数据集包含7个不同种类的蝴蝶。
# 本程序功能:
# 1. 将数据集按照7:1:2的比例划分为训练验证集、训练集、验证集、测试集
# 2. 代码将生成4个文件:训练验证集trainval.txt, 训练集列表train.txt, 验证集列表val.txt, 测试集列表test.txt, 数据集信息dataset_info.json
# 3. 代码输出信息:图像列表已生成, 其中训练验证集样本490,训练集样本423个, 验证集样本67个, 测试集样本129个, 共计619个。
# 4. 生成数据集标签词典时,需要根据标签-文件夹列表匹配标签列表
###################################################################################

import os
import json
import codecs

num_trainval = 0
num_train = 0
num_val = 0
num_test = 0
class_dim = 0
dataset_info = {
    'dataset_name': '',
    'num_trainval': -1,
    'num_train': -1,
    'num_val': -1,
    'num_test': -1,
    'class_dim': -1,
    'label_dict': {}
}
# 本地运行时,需要修改数据集的名称和绝对路径,注意和文件夹名称一致
dataset_name = 'Butterfly'
dataset_path = 'D:\\Workspace\\ExpDatasets\\'
dataset_root_path = os.path.join(dataset_path, dataset_name)
excluded_folder = ['.DS_Store', '.ipynb_checkpoints']      # 被排除的文件夹
    
# 定义生成文件的路径
data_path = os.path.join(dataset_root_path, 'Data')
trainval_list = os.path.join(dataset_root_path, 'trainval.txt')
train_list = os.path.join(dataset_root_path, 'train.txt')
val_list = os.path.join(dataset_root_path, 'val.txt')
test_list = os.path.join(dataset_root_path, 'test.txt')
dataset_info_list = os.path.join(dataset_root_path, 'dataset_info.json')

# 检测数据集列表是否存在,如果存在则先删除。其中测试集列表是一次写入,因此可以通过'w'参数进行覆盖写入,而不用进行手动删除。
if os.path.exists(trainval_list):
    os.remove(trainval_list)
if os.path.exists(train_list):
    os.remove(train_list)
if os.path.exists(val_list):
    os.remove(val_list)
if os.path.exists(test_list):
    os.remove(test_list)

# 按照比例进行数据分割
class_name_list = os.listdir(data_path)
with codecs.open(trainval_list, 'a', 'utf-8') as f_trainval:
    with codecs.open(train_list, 'a', 'utf-8') as f_train:
        with codecs.open(val_list, 'a', 'utf-8') as f_val:
            with codecs.open(test_list, 'a', 'utf-8') as f_test:
                for class_name in class_name_list:
                    if class_name not in excluded_folder:
                        dataset_info['label_dict'][str(class_dim)] = class_name        # 按照文件夹名称和label_match进行标签匹配
                        images = os.listdir(os.path.join(data_path, class_name))
                        count = 0
                        for image in images:
                            if count % 10 == 0:  # 抽取大约10%的样本作为验证数据
                                f_val.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
                                f_trainval.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
                                num_val += 1
                                num_trainval += 1
                            elif count % 10 == 1 or count % 10 == 2:  # 抽取大约20%的样本作为测试数据
                                f_test.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
                                num_test += 1
                            else:
                                f_train.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
                                f_trainval.write("{0}\t{1}\n".format(os.path.join(data_path, class_name, image), class_dim))
                                num_train += 1
                                num_trainval += 1
                            count += 1
                    class_dim += 1        

# 将数据集信息保存到json文件中供训练时使用
dataset_info['dataset_name'] = dataset_name
dataset_info['num_trainval'] = num_trainval
dataset_info['num_train'] = num_train
dataset_info['num_val'] = num_val
dataset_info['num_test'] = num_test
dataset_info['class_dim'] = class_dim

with codecs.open(dataset_info_list, 'w', encoding='utf-8') as f_dataset_info:
    json.dump(dataset_info, f_dataset_info, ensure_ascii=False, indent=4, separators=(',', ':')) # 格式化字典格式的参数列表

print("图像列表已生成, 其中训练验证集样本{},训练集样本{}个, 验证集样本{}个, 测试集样本{}个, 共计{}个。".format(num_trainval, num_train, num_val, num_test, num_train+num_val+num_test))

图像列表已生成, 其中训练验证集样本490,训练集样本423个, 验证集样本67个, 测试集样本129个, 共计619个。

###### 展示数据集列表信息 ###################3
from pprint import pprint
with open(dataset_info_list, 'r') as f_info:
    dataset_info = json.load(f_info)
pprint(dataset_info)
    {'class_dim': 7,
     'dataset_name': 'Butterfly',
     'label_dict': {'0': 'admiral',
                    '1': 'black_swallowtail',
                    '2': 'machaon',
                    '3': 'monarch_closed',
                    '4': 'monarch_open',
                    '5': 'peacock',
                    '6': 'zebra'},
     'num_test': 129,
     'num_train': 423,
     'num_trainval': 490,
     'num_val': 67}

【实验二】 全局参数设置及数据基本处理

实验摘要: 蝴蝶种类识别是一个多分类问题,我们通过卷积神经网络来完成。这部分通过PaddlePaddle手动构造一个Alexnet卷积神经的网络来实现识别功能。本实验主要实现训练前的一些准备工作,包括:全局参数定义,数据集载入,数据预处理,可视化函数定义。

实验目的:

  1. 学会使用配置文件定义全局参数
  2. 学会设置和载入数据集
  3. 学会对输入样本进行基本的预处理
  4. 学会定义可视化函数,可视化训练过程

2.1 导入依赖及全局参数配置

# 1. 导入依赖库
import os
import cv2
import numpy as np
import time                        # 载入time时间库,用于计算训练时间
from random import randint         # 导入随机数生成函数
import paddle as paddle            # 载入PaddlePaddle基本库
import paddle.fluid as fluid       # 载入基于fluid框架的paddle
from paddle.fluid.dygraph import Linear, Conv2D, Pool2D
from PIL import Image              # 载入python的第三方图像处理库
import matplotlib.pyplot as plt    # 载入matplotlib绘图库
from multiprocessing import cpu_count
# plt.rcParams['span.family'] = 'sans-serif'  
# plt.rcParams['span.sans-serif'] = 'SimHei,Times New Roman'# 中文设置成宋体,除此之外的字体设置成New Roman 
# np.set_printoptions(precision=5, suppress=True) # 设置numpy的精度,用于打印输出

# 2. 全局参数配置
# 2.1 定义数据集列表文件及模型路径
dataset_name      = 'Butterfly'
dataset_path      = 'D:\\Workspace\\ExpDatasets\\'
dataset_root_path = os.path.join(dataset_path, dataset_name)

trainval_list = os.path.join(dataset_root_path, 'trainval.txt')
train_list = os.path.join(dataset_root_path, 'train.txt')
val_list = os.path.join(dataset_root_path, 'val.txt')
test_list = os.path.join(dataset_root_path, 'test.txt')

architecture      = 'Alexnet'
result_root_path  = 'D:\\Workspace\\ExpResults\\'
final_models_path = os.path.join(result_root_path, 'Project09Alexnet', dataset_name + '_' + architecture)

# 2.3 训练参数定义

# 定义使用CPU还是GPU,使用CPU时use_cuda = False,使用GPU时use_cuda = True
use_cuda          = True  # True, False 如果设备有GPU,怎么我们可以启用GPU进行快速训练
PLACE             = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()


# 2.2 图像基本信息
img_size = 227
img_channel = 3

# 2.3 训练参数定义
total_epoch        = 50        # 总迭代次数, 代码调试好后考虑Epochs_num = 50
log_interval       = 10
eval_interval      = 1        # 设置在训练过程中,每隔一定的周期进行一次测试
learning_rate      = 0.001    # 学习率
momentum           = 0.9      # 动量
BUF_SIZE           = 512      # 设置存储数据的缓存大小
BATCH_SIZE         = 128      # 设置每个批次的数据大小,同时对训练提供器和测试提供器有效

2.2 数据集定义及数据预处理

# 定义数据集映射函数获取数据的图像矩阵和label
def data_mapper(sample):
    img, label = sample
    img = cv2.imread(img, 1)
    img = cv2.resize(img, (img_size, img_size))       # 将图像尺度resize为指定尺寸
    img = np.array(img).astype('float32')   # 将图像数据类型转化为float32
    img = img.transpose((2, 0, 1))          # 调整数据形状paddle默认格式(通道,高度,宽度)
    img = img/255.0                         # 将像素值归一化到[0,1]之间
    
    return img, label

# 定义数据集reader,用于从列表文件中批量获取图像
def data_reader(data_list_path):
    #定义读取函数,从列表文件中读取
    def reader():
        with open(data_list_path, 'r') as f:
            lines = f.readlines()
            for line in lines:
                img_path, label = line.split('\t')
                yield img_path, int(label)
    #使用多线程方式,通过用户自定义的映射器mapper来映射reader返回的样本(到输出队列) 
    return paddle.reader.xmap_readers(data_mapper, reader, cpu_count(), 512) 

2.3 设置数据提供器

# 用于训练/测试的数据提供器,每次从缓存中随机读取批次大小的数据
trainval_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(trainval_list), buf_size=BUF_SIZE), batch_size=BATCH_SIZE, drop_last=False)
train_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(train_list), buf_size=BUF_SIZE), batch_size=BATCH_SIZE, drop_last=False)
val_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(val_list), buf_size=BUF_SIZE), batch_size=BATCH_SIZE, drop_last=False)
test_reader = paddle.batch(paddle.reader.shuffle(reader=data_reader(test_list), buf_size=BUF_SIZE), batch_size=BATCH_SIZE, drop_last=False)
# 测试:输出第0个batch的数据形态
for batch_id, data in enumerate(train_reader()):
    print(data[0][0].shape)
    break

(3, 227, 227)

2.4 定义过程可视化函数

定义训练过程中用到的可视化方法, 包括训练损失, 训练集批准确率, 测试集准确率. 根据具体的需求,可以在训练后展示这些数据和迭代次数的关系. 值得注意的是, 训练过程中可以每个epoch绘制一个数据点,也可以每个batch绘制一个数据点,也可以每个n个batch或n个epoch绘制一个数据点.

# 绘制训练batch精度和平均loss
def draw_process(title, loss_label, accuracy_label, iters, losses, accuracies):

    # 第一组坐标轴 Loss
    _, ax1 = plt.subplots()   # plt.subplots(figsize=(14,6))
    ax1.plot(iters, losses, color='red', label=loss_label)
    ax1.set_xlabel('Iters', spansize=20)
    ax1.set_ylabel(loss_label, spansize=20)
    max_loss = max(losses)
    ax1.set_ylim(0, max_loss*1.2)

    # 第二组坐标轴 accuracy
    ax2 = ax1.twinx()
    ax2.plot(iters, accuracies, color='blue', label=accuracy_label)
    ax2.set_ylabel(accuracy_label, spansize=20)
    max_acc = max(accuracies)
    ax2.set_ylim(0, max_acc*1.2)

    plt.title(title, spansize=24)
    # 图例
    handles1, labels1 = ax1.get_legend_handles_labels()
    handles2, labels2 = ax2.get_legend_handles_labels()
    plt.legend(handles1+handles2, labels1+labels2, loc='best')
    plt.grid()

【实验三】 模型训练与评估

实验摘要: 蝴蝶种类识别是一个多分类问题,我们通过卷积神经网络来完成。这部分通过PaddlePaddle手动构造一个Alexnet卷积神经的网络来实现识别功能,最后一层采用Softmax激活函数完成分类任务。

实验目的:

  1. 掌握卷积神经网络的构建和基本原理
  2. 深刻理解训练集、验证集、训练验证集及测试集在模型训练中的作用
  3. 学会按照网络拓扑结构图定义神经网络类 (Paddle 1.8)
  4. 学会在线测试和离线测试两种测试方法

3.1 配置网络

3.1.1 网络拓扑结构图

Lec04006AlexNet

需要注意的是,在Alexnet中实际输入的尺度会被Crop为3×227×227

3.1.2 网络参数配置表

Layer Input Kernels_num Kernels_size Stride Padding PoolingType Output Parameters
Input 3×227×227
Conv1 3×227×227 96 3×11×11 4 0 96×55×55 (3×11×11+1)×96=34944
Pool1 96×55×55 96 96×3×3 2 0 max 96×27×27 0
Conv2 96×27×27 256 96×5×5 1 2 256×27×27 (96×5×5+1)×256=614656
Pool2 256×27×27 256 256×3×3 2 0 max 256×13×13 0
Conv3 256×13×13 384 256×3×3 1 1 384×13×13 (256×3×3+1)×384=885120
Conv4 384×13×13 384 384×3×3 1 1 384×13×13 (384×3×3+1)×384=1327488
Conv5 384×13×13 256 384×3×3 1 1 256×13×13 (384×3×3+1)×256=884992
Pool5 256×13×13 256 256×3×3 2 0 max 256×6×6 0
FC6 (256×6×6)×1 4096×1 (9216+1)×4096=37752832
FC7 4096×1 4096×1 (4096+1)×4096=16781312
FC8 4096×1 1000×1 (4096+1)×1000=4097000
Output 1000×1
Total = 62378344

其中卷积层参数:3747200,占总参数的6%。

3.1.3 定义神经网络类

从Alexnet开始,包括VGG,GoogLeNet,Resnet等模型都是层次较深的模型,如果按照逐层的方式进行设计,代码会变得非常繁琐。因此,我们可以考虑将相同结构的模型进行汇总和合成,例如Alexnet中,卷积层+激活+池化层就是一个完整的结构体。

from paddle.fluid.dygraph import Linear, Conv2D, Pool2D
from paddle.fluid.layers import dropout
import paddle.fluid as fluid
import numpy as np

# 定义卷积神经网络Alexnet
class Alexnet(fluid.dygraph.Layer):
    
    name_scope = 'Alexnet'
    def __init__(self, num_classes=1000):
        super(Alexnet, self).__init__()
        self.conv1 = Conv2D(num_channels=3, num_filters=96, filter_size=11, stride=4, padding=0, act='relu')
        self.pool1 = Pool2D(pool_size=3, pool_stride=2, pool_type='max')
        self.conv2 = Conv2D(num_channels=96, num_filters=256, filter_size=5, stride=1, padding=2, act='relu')
        self.pool2 = Pool2D(pool_size=3, pool_stride=2, pool_type='max')
        self.conv3 = Conv2D(num_channels=256, num_filters=384, filter_size=3, stride=1, padding=1, act='relu')
        self.conv4 = Conv2D(num_channels=384, num_filters=384, filter_size=3, stride=1, padding=1, act='relu')
        self.conv5 = Conv2D(num_channels=384, num_filters=256, filter_size=3, stride=1, padding=1, act='relu')
        self.pool5 = Pool2D(pool_size=3, pool_stride=2, pool_type='max')
        self.fc6 = Linear(input_dim=256*6*6, output_dim=4096, act='relu')
        self.fc7 = Linear(input_dim=4096, output_dim=4096, act='relu')
        self.fc8 = Linear(input_dim=4096, output_dim=num_classes)

    def forward(self, input):
        x = self.conv1(input)
        x = self.pool1(x)
        x = self.conv2(x)
        x = self.pool2(x)
        x = self.conv3(x)
        x = self.conv4(x)
        x = self.conv5(x)
        x = self.pool5(x)
        x = fluid.layers.reshape(x, shape=[x.shape[0], -1])
        x = self.fc6(x)
        x = dropout(x, 0.5)
        x = self.fc7(x)
        x = dropout(x, 0.5)
        y = self.fc8(x)
        return y
    
#### 网络测试
if __name__ == '__main__':
    model = Alexnet()
    paddle.summary(model, (1,3,227,227))
    ---------------------------------------------------------------------------
     Layer (type)       Input Shape          Output Shape         Param #    
    ===========================================================================
       Conv2D-11     [[1, 3, 227, 227]]    [1, 96, 55, 55]        34,944     
       Pool2D-7      [[1, 96, 55, 55]]     [1, 96, 27, 27]           0       
       Conv2D-12     [[1, 96, 27, 27]]     [1, 256, 27, 27]       614,656    
       Pool2D-8      [[1, 256, 27, 27]]    [1, 256, 13, 13]          0       
       Conv2D-13     [[1, 256, 13, 13]]    [1, 384, 13, 13]       885,120    
       Conv2D-14     [[1, 384, 13, 13]]    [1, 384, 13, 13]      1,327,488   
       Conv2D-15     [[1, 384, 13, 13]]    [1, 256, 13, 13]       884,992    
       Pool2D-9      [[1, 256, 13, 13]]     [1, 256, 6, 6]           0       
       Linear-7         [[1, 9216]]           [1, 4096]         37,752,832   
       Linear-8         [[1, 4096]]           [1, 4096]         16,781,312   
       Linear-9         [[1, 4096]]           [1, 1000]          4,097,000   
    ===========================================================================
    Total params: 62,378,344
    Trainable params: 62,378,344
    Non-trainable params: 0
    ---------------------------------------------------------------------------
    Input size (MB): 0.59
    Forward/backward pass size (MB): 5.96
    Params size (MB): 237.95
    Estimated Total Size (MB): 244.51
    ---------------------------------------------------------------------------

3.2 定义测试函数

测试部分的具体流程包括:

  1. 设置模型运行模式为验证模式model.eval()
  2. 基于周期epoch-批次batch的结构进行两层循环训练,具体包括:
    1). 定义输入层(image,label),图像输入维度 [batch, channel, Width, Height] (-1,imgChannel,imgSize,imgSize),标签输入维度 [batch, 1] (-1,1)
    2). 定义输出层,包括前向传播的输出predict=model(image)及精度accuracy。如果需要,还可以输出针对测试集的损失loss。
    值得注意的,在计算测试集精度的时候,需要对每个批次的精度/损失求取平均值。

在定义test()函数的时候,我们需要为其指定两个参数:model是测试的模型,data_reader是迭代的数据读取器,取值为val_reader(), test_reader(),分别对验证集和测试集。此处验证集和测试集数据的测试过程是相同的,只是所使用的数据不同。

def test(model, data_reader):
    accs = []
    losses = []
    model.eval() #评估模式
    for batch_id,data in enumerate(data_reader):#测试集
        images = np.array([x[0] for x in data], dtype='float32').reshape(-1, img_channel, img_size, img_size)            
        labels = np.array([x[1] for x in data], dtype='int64').reshape(-1,1)

        image = fluid.dygraph.to_variable(images)
        label = fluid.dygraph.to_variable(labels)

        digits = model(image)
        predict = fluid.layers.softmax(digits)
                
        loss = fluid.layers.cross_entropy(predict, label) # 获取批loss值
        loss = fluid.layers.mean(loss) # 求单个样本的loss
        acc = fluid.layers.accuracy(predict,label)
        losses.append(loss.numpy())
        accs.append(acc.numpy())
        
    avg_loss = np.mean(losses)    
    avg_acc = np.mean(accs)
    
    return avg_loss, avg_acc

3.3 定义训练函数

在动态图模式下,所有的训练测试代码都需要基于动态图守护进程fluid.dygraph.guard(PLACE)

训练部分的具体流程包括:

  1. 模型实例化,并设置为训练模式model.train()
  2. 定义优化器optimizer
  3. 基于周期epoch-批次batch的结构进行两层循环训练,具体包括:
    1). 定义输入层(image,label),图像输入维度 [batch, channel, Width, Height] (-1,imgChannel,imgSize,imgSize),标签输入维度 [batch, 1] (-1,1)
    2). 定义输出层,包括前向传播的输出predict=model(image),损失loss及平均损失,精度accuracy。
    3). 执行反向传播,并将损失最小化,清除梯度

在训练过程中,可以将周期,批次,损失及精度等信息打印到屏幕。

值得注意的是,在每一轮的训练中,每100个batch之后会输出一次平均训练误差和准确率。每一轮训练之后,使用测试集进行一次测试,在每轮测试中,均打输出一次平均测试误差和准确率。

注意

注意在下列的代码中,我们每个epoch都执行一次模型保存,这种方式一般应用在复杂的模型和大型数据集上。这种经常性的模型保存,有利于我们执行EarlyStopping策略,当我们发现运行曲线不再继续收敛时,就可以结束训练,并选择之前保存的最好的一个模型作为最终的模型。FinalModel**

def train(model):
    # 启动训练和在线测试            
    start = time.perf_counter()
    print('启动训练...')    

    optimizer = fluid.optimizer.Momentum(learning_rate=learning_rate, momentum=momentum, parameter_list=model.parameters())#优化器选用SGD随机梯度下降,学习率为0.001.
#     optimizer = fluid.optimizer.AdamOptimizer(learning_rate=learning_rate, parameter_list=model.parameters())#优化器选用SGD随机梯度下降,学习率为0.001.
#     optimizer = fluid.optimizer.SGDOptimizer(learning_rate=learning_rate, parameter_list=model.parameters())#优化器选用SGD随机梯度下降,学习率为0.001.

    num_batch = 0
    best_result = 0
    best_result_id = 0
    elapsed = 0
    for epoch in range(1, total_epoch+1):
        
        model.train() #训练模式
        for batch_id, data in enumerate(train_reader()):
            num_batch += 1
            
            # 定义输入层数据的形状和类型
            images = np.array([x[0] for x in data], dtype='float32').reshape(-1, img_channel, img_size, img_size)            
            labels = np.array([x[1] for x in data], dtype='int64').reshape(-1,1)

            image = fluid.dygraph.to_variable(images)
            label = fluid.dygraph.to_variable(labels)
            
            # 定义输出层 loss+accuracy
            # 预测结果 = softmax(预测概率),模型的输出是预测概率
            # 损失loss = cross_entropy(预测结果 与 label 之间的距离)
            # 精度acc = accuracy(预测结果 与 label之间的距离)            
            digits = model(image) # 前向传播输出的分值,未进行归一化
            predict = fluid.layers.softmax(digits) # 预测结果,归一化概率 = softmax(输出概率)
#             print(digits)
#             print(predict)

            loss = fluid.layers.cross_entropy(predict, label)
            avg_loss = fluid.layers.mean(loss)                  # 获取一批的平均loss值
            acc = fluid.layers.accuracy(predict, label)         # 计算一批的精度
            
            # 执行反向传播算法    
            avg_loss.backward()   # 使用backward() 方法可以执行反向网络
            optimizer.minimize(avg_loss)
            model.clear_gradients() # 将参数梯度清零以保证下一轮训练的正确性
            
            # 每隔log_interval个batch打印一次训练损失, 也可根据TOTAL_EPOCH设定按照周期epoch进行输出
            if num_batch % log_interval == 0: # 每log_interval个batch打印一次信息
                elapsed_step = time.perf_counter() - elapsed - start
                elapsed = time.perf_counter() - start
                print("Epoch:{}/{}, batch:{}, train_loss:{}, train_accuracy:{} ({:.2f}s)".format(epoch,total_epoch,num_batch,avg_loss.numpy(),acc.numpy(),elapsed_step))
                
                # 记录训练过程,用于可视化训练过程中的loss和accuracy
                all_train_iters.append(num_batch)
                all_train_losses.append(avg_loss.numpy()[0])
                all_train_accs.append(acc.numpy()[0])
            
        # 每隔一定周期进行一次测试    
        if epoch % eval_interval == 0 or epoch == total_epoch:        
            #模型校验
            avg_loss, avg_acc = test(model, val_reader())               
            print('[validation] Epoch:{}/{}, test_loss:[{:.5f}], test_accuracy:[{:.5f}]'.format(epoch,total_epoch, avg_loss, avg_acc))
            
            # 将性能最好的模型保存为final模型
            if avg_acc > best_result:
                best_result = avg_acc
                best_result_id = epoch
                
                # 保存最优模型
                fluid.save_dygraph(model.state_dict(), os.path.join(final_models_path, 'best_model'))
            print('当前性能最好的模型 epoch_{} 的精度: {:.5f}, 已将其赋值为:best_model'.format(best_result_id, best_result))

            # 记录测试过程,用于可视化训练过程中的loss和accuracy
            all_test_iters.append(epoch)
            all_test_losses.append(avg_loss)
            all_test_accs.append(avg_acc)           

            
    # 输出训练过程图
    # 保存最终模型
    fluid.save_dygraph(model.state_dict(), os.path.join(final_models_path, 'model_final'))
    print('训练完成,最终性能accuracy={:.5f}(epoch={}), 总耗时{:.2f}s, 已将其保存为:best_model'.format(best_result, best_result_id, time.perf_counter() - start))
    draw_process("Training Process", 'Train Loss', 'Train Accuracy', all_train_iters, all_train_losses, all_train_accs)
    draw_process("Validation Results", 'Validation Loss', 'Validation Accuracy', all_test_iters, all_test_losses, all_test_accs)
            

值得注意的是,因为训练数据样本较少(210),而Batch_Size=128,因此每个周期的batch数只有不足2个批次。因此无法按照批次来进行print训练过程,只能按照epoch进行print。而基于epoch的print,需要将其缩进往前移一个层次。

3.5 执行训练主函数

if __name__ == '__main__':
    # 初始化绘图列表
    all_train_iters = []
    all_train_losses = []
    all_train_accs = []
    all_test_losses = []
    all_test_iters = []
    all_test_accs = []
    
    with fluid.dygraph.guard(PLACE): 
        model = Alexnet() #模型实例化
        
        # 启动训练过程
        train(model) 
        
# 训练完成,最终性能accuracy=0.97864(epoch=10), 总耗时52.69s, 已将其保存为:best_model
# 训练完成,最终性能accuracy=0.98348(epoch=19), 总耗时105.66s, 已将其保存为:best_model
    启动训练...
    [validation] Epoch:1/50, test_loss:[6.54548], test_accuracy:[0.14925]
    当前性能最好的模型 epoch_1 的精度: 0.14925, 已将其赋值为:best_model
    [validation] Epoch:2/50, test_loss:[5.50086], test_accuracy:[0.20896]
    当前性能最好的模型 epoch_2 的精度: 0.20896, 已将其赋值为:best_model
    Epoch:3/50, batch:10, train_loss:[5.1387396], train_accuracy:[0.2265625] (11.00s)
    [validation] Epoch:3/50, test_loss:[2.93469], test_accuracy:[0.20896]
    当前性能最好的模型 epoch_2 的精度: 0.20896, 已将其赋值为:best_model
    [validation] Epoch:4/50, test_loss:[2.40430], test_accuracy:[0.17910]
    当前性能最好的模型 epoch_2 的精度: 0.20896, 已将其赋值为:best_model
    Epoch:5/50, batch:20, train_loss:[2.2839901], train_accuracy:[0.23076923] (1.52s)
    [validation] Epoch:5/50, test_loss:[2.38220], test_accuracy:[0.20896]
    当前性能最好的模型 epoch_2 的精度: 0.20896, 已将其赋值为:best_model
    [validation] Epoch:6/50, test_loss:[1.97962], test_accuracy:[0.20896]
    当前性能最好的模型 epoch_2 的精度: 0.20896, 已将其赋值为:best_model
    [validation] Epoch:7/50, test_loss:[2.19476], test_accuracy:[0.17910]
    当前性能最好的模型 epoch_2 的精度: 0.20896, 已将其赋值为:best_model
    Epoch:8/50, batch:30, train_loss:[2.2994266], train_accuracy:[0.125] (1.94s)
    [validation] Epoch:8/50, test_loss:[1.98743], test_accuracy:[0.20896]
    当前性能最好的模型 epoch_2 的精度: 0.20896, 已将其赋值为:best_model
    [validation] Epoch:9/50, test_loss:[1.87598], test_accuracy:[0.23881]
    当前性能最好的模型 epoch_9 的精度: 0.23881, 已将其赋值为:best_model
    Epoch:10/50, batch:40, train_loss:[2.2276525], train_accuracy:[0.17948718] (6.75s)
    [validation] Epoch:10/50, test_loss:[1.79811], test_accuracy:[0.25373]
    当前性能最好的模型 epoch_10 的精度: 0.25373, 已将其赋值为:best_model
    [validation] Epoch:11/50, test_loss:[1.79225], test_accuracy:[0.26866]
    当前性能最好的模型 epoch_11 的精度: 0.26866, 已将其赋值为:best_model
    [validation] Epoch:12/50, test_loss:[1.81987], test_accuracy:[0.20896]
    当前性能最好的模型 epoch_11 的精度: 0.26866, 已将其赋值为:best_model
    Epoch:13/50, batch:50, train_loss:[2.0737376], train_accuracy:[0.21875] (10.71s)
    [validation] Epoch:13/50, test_loss:[1.80168], test_accuracy:[0.29851]
    当前性能最好的模型 epoch_13 的精度: 0.29851, 已将其赋值为:best_model
    [validation] Epoch:14/50, test_loss:[1.78403], test_accuracy:[0.29851]
    当前性能最好的模型 epoch_13 的精度: 0.29851, 已将其赋值为:best_model
    Epoch:15/50, batch:60, train_loss:[1.9123802], train_accuracy:[0.2820513] (6.87s)
    [validation] Epoch:15/50, test_loss:[1.73530], test_accuracy:[0.37313]
    当前性能最好的模型 epoch_15 的精度: 0.37313, 已将其赋值为:best_model
    [validation] Epoch:16/50, test_loss:[1.72165], test_accuracy:[0.35821]
    当前性能最好的模型 epoch_15 的精度: 0.37313, 已将其赋值为:best_model
    [validation] Epoch:17/50, test_loss:[1.70052], test_accuracy:[0.34328]
    当前性能最好的模型 epoch_15 的精度: 0.37313, 已将其赋值为:best_model
    Epoch:18/50, batch:70, train_loss:[1.7967557], train_accuracy:[0.265625] (5.62s)
    [validation] Epoch:18/50, test_loss:[1.66329], test_accuracy:[0.40299]
    当前性能最好的模型 epoch_18 的精度: 0.40299, 已将其赋值为:best_model
    [validation] Epoch:19/50, test_loss:[1.64117], test_accuracy:[0.43284]
    当前性能最好的模型 epoch_19 的精度: 0.43284, 已将其赋值为:best_model
    Epoch:20/50, batch:80, train_loss:[1.4582388], train_accuracy:[0.41025642] (11.79s)
    [validation] Epoch:20/50, test_loss:[1.62740], test_accuracy:[0.37313]
    当前性能最好的模型 epoch_19 的精度: 0.43284, 已将其赋值为:best_model
    [validation] Epoch:21/50, test_loss:[1.60874], test_accuracy:[0.25373]
    当前性能最好的模型 epoch_19 的精度: 0.43284, 已将其赋值为:best_model
    [validation] Epoch:22/50, test_loss:[1.59241], test_accuracy:[0.41791]
    当前性能最好的模型 epoch_19 的精度: 0.43284, 已将其赋值为:best_model
    Epoch:23/50, batch:90, train_loss:[1.7105892], train_accuracy:[0.359375] (1.78s)
    [validation] Epoch:23/50, test_loss:[1.63031], test_accuracy:[0.34328]
    当前性能最好的模型 epoch_19 的精度: 0.43284, 已将其赋值为:best_model
    [validation] Epoch:24/50, test_loss:[1.58896], test_accuracy:[0.44776]
    当前性能最好的模型 epoch_24 的精度: 0.44776, 已将其赋值为:best_model
    Epoch:25/50, batch:100, train_loss:[1.8568017], train_accuracy:[0.3846154] (5.18s)
    [validation] Epoch:25/50, test_loss:[1.56869], test_accuracy:[0.37313]
    当前性能最好的模型 epoch_24 的精度: 0.44776, 已将其赋值为:best_model
    [validation] Epoch:26/50, test_loss:[1.52380], test_accuracy:[0.50746]
    当前性能最好的模型 epoch_26 的精度: 0.50746, 已将其赋值为:best_model
    [validation] Epoch:27/50, test_loss:[1.50590], test_accuracy:[0.41791]
    当前性能最好的模型 epoch_26 的精度: 0.50746, 已将其赋值为:best_model
    Epoch:28/50, batch:110, train_loss:[1.7291274], train_accuracy:[0.328125] (7.16s)
    [validation] Epoch:28/50, test_loss:[1.48486], test_accuracy:[0.41791]
    当前性能最好的模型 epoch_26 的精度: 0.50746, 已将其赋值为:best_model
    [validation] Epoch:29/50, test_loss:[1.48682], test_accuracy:[0.34328]
    当前性能最好的模型 epoch_26 的精度: 0.50746, 已将其赋值为:best_model
    Epoch:30/50, batch:120, train_loss:[1.6209711], train_accuracy:[0.43589744] (1.50s)
    [validation] Epoch:30/50, test_loss:[1.46470], test_accuracy:[0.41791]
    当前性能最好的模型 epoch_26 的精度: 0.50746, 已将其赋值为:best_model
    [validation] Epoch:31/50, test_loss:[1.44141], test_accuracy:[0.52239]
    当前性能最好的模型 epoch_31 的精度: 0.52239, 已将其赋值为:best_model
    [validation] Epoch:32/50, test_loss:[1.44740], test_accuracy:[0.40299]
    当前性能最好的模型 epoch_31 的精度: 0.52239, 已将其赋值为:best_model
    Epoch:33/50, batch:130, train_loss:[1.6043384], train_accuracy:[0.375] (7.07s)
    [validation] Epoch:33/50, test_loss:[1.38747], test_accuracy:[0.47761]
    当前性能最好的模型 epoch_31 的精度: 0.52239, 已将其赋值为:best_model
    [validation] Epoch:34/50, test_loss:[1.36523], test_accuracy:[0.50746]
    当前性能最好的模型 epoch_31 的精度: 0.52239, 已将其赋值为:best_model
    Epoch:35/50, batch:140, train_loss:[1.4893553], train_accuracy:[0.33333334] (1.54s)
    [validation] Epoch:35/50, test_loss:[1.33817], test_accuracy:[0.41791]
    当前性能最好的模型 epoch_31 的精度: 0.52239, 已将其赋值为:best_model
    [validation] Epoch:36/50, test_loss:[1.30131], test_accuracy:[0.44776]
    当前性能最好的模型 epoch_31 的精度: 0.52239, 已将其赋值为:best_model
    [validation] Epoch:37/50, test_loss:[1.29297], test_accuracy:[0.46269]
    当前性能最好的模型 epoch_31 的精度: 0.52239, 已将其赋值为:best_model
    Epoch:38/50, batch:150, train_loss:[1.4481435], train_accuracy:[0.4375] (1.83s)
    [validation] Epoch:38/50, test_loss:[1.27471], test_accuracy:[0.46269]
    当前性能最好的模型 epoch_31 的精度: 0.52239, 已将其赋值为:best_model
    [validation] Epoch:39/50, test_loss:[1.24825], test_accuracy:[0.49254]
    当前性能最好的模型 epoch_31 的精度: 0.52239, 已将其赋值为:best_model
    Epoch:40/50, batch:160, train_loss:[1.3468556], train_accuracy:[0.43589744] (1.49s)
    [validation] Epoch:40/50, test_loss:[1.23397], test_accuracy:[0.56716]
    当前性能最好的模型 epoch_40 的精度: 0.56716, 已将其赋值为:best_model
    [validation] Epoch:41/50, test_loss:[1.21647], test_accuracy:[0.50746]
    当前性能最好的模型 epoch_40 的精度: 0.56716, 已将其赋值为:best_model
    [validation] Epoch:42/50, test_loss:[1.18106], test_accuracy:[0.49254]
    当前性能最好的模型 epoch_40 的精度: 0.56716, 已将其赋值为:best_model
    Epoch:43/50, batch:170, train_loss:[1.3286947], train_accuracy:[0.484375] (5.61s)
    [validation] Epoch:43/50, test_loss:[1.16236], test_accuracy:[0.53731]
    当前性能最好的模型 epoch_40 的精度: 0.56716, 已将其赋值为:best_model
    [validation] Epoch:44/50, test_loss:[1.12857], test_accuracy:[0.50746]
    当前性能最好的模型 epoch_40 的精度: 0.56716, 已将其赋值为:best_model
    Epoch:45/50, batch:180, train_loss:[1.3960109], train_accuracy:[0.53846157] (1.52s)
    [validation] Epoch:45/50, test_loss:[1.10728], test_accuracy:[0.49254]
    当前性能最好的模型 epoch_40 的精度: 0.56716, 已将其赋值为:best_model
    [validation] Epoch:46/50, test_loss:[1.08434], test_accuracy:[0.53731]
    当前性能最好的模型 epoch_40 的精度: 0.56716, 已将其赋值为:best_model
    [validation] Epoch:47/50, test_loss:[1.09052], test_accuracy:[0.62687]
    当前性能最好的模型 epoch_47 的精度: 0.62687, 已将其赋值为:best_model
    Epoch:48/50, batch:190, train_loss:[1.099474], train_accuracy:[0.59375] (7.21s)
    [validation] Epoch:48/50, test_loss:[1.06776], test_accuracy:[0.52239]
    当前性能最好的模型 epoch_47 的精度: 0.62687, 已将其赋值为:best_model
    [validation] Epoch:49/50, test_loss:[1.06788], test_accuracy:[0.62687]
    当前性能最好的模型 epoch_47 的精度: 0.62687, 已将其赋值为:best_model
    Epoch:50/50, batch:200, train_loss:[1.2934189], train_accuracy:[0.4871795] (1.51s)
    [validation] Epoch:50/50, test_loss:[1.04287], test_accuracy:[0.56716]
    当前性能最好的模型 epoch_47 的精度: 0.62687, 已将其赋值为:best_model
    训练完成,最终性能accuracy=0.62687(epoch=47), 总耗时103.69s, 已将其保存为:best_model

output_31_1

output_31_2

3.6 离线测试

离线测试同样要基于动态守护框架fluid.dygraph.guard()。测试过程与训练过程中的在线测试流程基本一致,只需要提前实现载入已保存的模型即可,载入模型使用fluid.load_dygraph()方法。

with fluid.dygraph.guard(PLACE):
    model_dict, _ = fluid.load_dygraph(os.path.join(final_models_path, 'best_model'))
    model = Alexnet() #模型实例化
    model.load_dict(model_dict) #加载模型参数    
 
    #启动训练过程
    _, avg_acc = test(model, test_reader())            
    print('测试集精度为:{:.5f}'.format(avg_acc))    

测试集精度为:0.25391

【结果分析】
需要注意的是此处的精度与训练过程中输出的测试精度是不相同的,因为训练过程中使用的是验证集, 而这里的离线测试使用的是测试集.

【实验四】 模型推理和预测(应用)

实验摘要: 对训练过的模型,我们通过测试集进行模型效果评估,并可以在实际场景中进行预测,查看模型的效果。

实验目的:

  1. 学会使用部署和推理模型进行测试
  2. 学会对测试样本进行基本数据预处理
  3. 对于测试样本能够实现单样本推理

4.1 导入依赖库及全局参数配置

# 导入依赖库
import os
import cv2
import json
import numpy as np
import paddle                      # 载入PaddlePaddle基本库
import paddle.fluid as fluid       # 载入基于fluid框架的paddle
from paddle.fluid.dygraph import Linear, Conv2D, Pool2D
import matplotlib.pyplot as plt    # 载入python的第三方图像处理库

dataset_name      = 'Butterfly'
architecture      = 'Alexnet'
result_root_path  = 'D:\\Workspace\\ExpResults\\'
final_model_path  = os.path.join(result_root_path, 'Project09Alexnet', dataset_name + '_' + architecture)

4.2 定义推理时的预处理函数

在预测之前,通常需要对图像进行预处理。此处的预处理方案和训练模型时所使用的预处理方案必须是一致的。对于彩色图,首先需要将图像resize为模型的输入尺度,其次需要将模型通道进行调整,转化[C,W,H]为[H,W,C],最后将像素色彩值归一化为[0,1].

# 从测试集列表中随机获取一个测试图片
def data_reader(data_list_path):
    with open(test_list, 'r') as f:
        lines = f.readlines()       
        i = randint(1, len(lines))
        line = lines[i]
        img_path, label = line.split('\t')
        label = int(label)

    return img_path, label

#读取预测图像并进行预处理
def load_image(img_path):
    img = cv2.imread(img_path, 3)
    img = cv2.resize(img, (img_size, img_size))   # 将图像尺度resize为指定尺寸
    img = np.array(img).astype('float32')         # 将图像数据类型转化为float32
    img = img.transpose((2, 0, 1))                # 调整数据形状paddle默认格式(通道,高度,宽度)
    img = img/255.0                               # 将像素值归一化到[0,1]之间
    
    return img

4.3 数据推理

img_path, label = data_reader(test_list)
# img_path = 'D:\\Workspace\\ExpDatasets\\Gestures\\Infer\\infer_3.jpg'

#构建预测动态图过程
with fluid.dygraph.guard():
    model = Alexnet()#模型实例化
    model_dict, _ = fluid.load_dygraph(os.path.join(final_model_path, 'best_model'))
    model.load_dict(model_dict)#加载模型参数
    model.eval()#评估模式
    
    # 输出测评结果
    img = load_image(img_path)
    img = img[np.newaxis,:, : ,:]
    img = fluid.dygraph.to_variable(img)
    result = model(img)
    print('手势文件 {} 的标签为: {}, 预测结果为: {}'.format(os.path.basename(img_path), label, np.argmax(result.numpy())))

    # 输出图像文件
    image = cv2.imread(img_path, 1)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    plt.imshow(image)

手势文件 mno012.jpg 的标签为: 4, 预测结果为: 4

output_41_1