• 飞桨工具组件 - PaddleX
  • 飞桨全流程开发工具,集飞桨核心框架、模型库、工具及组件等深度学习开发所需全部能力于一身,打通深度学习开发全流程。
  • PaddleX同时提供简明易懂的Python API,及一键下载安装的图形化开发客户端。用户可根据实际生产需求选择相应的开发方式,获得飞桨全流程开发的最佳体验。

数据说明

本场比赛要求参赛选手对十二种猫进行分类,属于CV方向经典的图像分类任务。图像分类任务作为其他图像任务的基石,可以让大家更快上手计算机视觉。比赛数据集包含12种猫的图片,并划分为训练集与测试集。

  1. 训练集: 提供高清彩色图片以及图片所属的分类,共有2160张猫的图片,含标注文件。
  2. 测试集: 仅提供彩色图片,共有240张猫的图片,不含标注文件。

1 导入依赖

!pip install paddlex
import warnings
warnings.filterwarnings('ignore')

import os
os.environ['CUDA_VISIBLE_DEVICES'] = '0'

import paddle
import paddlex as pdx
from paddlex import transforms as T

import numpy as np
import pandas as pd
import shutil
import glob

import cv2
import imghdr
from PIL import Image

2 数据清洗

  • 生成 ImageNet 格式文件夹,目标数据格式如下:
Dataset/ # 图像分类数据集根目录
|--class A/  # 当前文件夹所有图片属于 A 类别
|  |--a_1.jpg
|  |--a_2.jpg
|  |--...
|  |--...
|
|--...
|
|--class Z/ # 当前文件夹所有图片属于 Z 类别
|  |--z1.jpg
|  |--z2.jpg
|  |--...
|  |--...
  • 因为我们的训练集中有一个 train_list.txt 存放对应的类别信息,所以我们先将所有图片放如对应的类别文件夹中,之后再利用PaddleX自动数据划分即可。

2.1 解压数据集

pdx.utils.decompress('data/data10954/cat_12_train.zip')
pdx.utils.decompress('data/data10954/cat_12_test.zip')

生成 12 个类别文件夹。

for i in range(12):
    cls_path = os.path.join('data/data10954/ImageNetDataset/', '%02d' % int(i))
    if not os.path.exists(cls_path):
        os.makedirs(cls_path)
  • 这里为什么要用 00/01/... 作为类别呢?因为PaddleX划分是根据 字符串 排序的,所以划分之后的 2/3/..的数值编号是排在 10/11 之后的。
  • 我们希望模型输出的类别数字和我们文件夹(即比赛提交的数字)是统一的,所以设置成 XX 的格式。

2.2 异常格式清洗

  • 生成文件名和类别的一一对应关系,之后将根据类别cls将图片放入目标文件夹:data/data10954/ImageNetDataset/*/*.jpg
train_df = pd.read_csv('data/data10954/train_list.txt', header=None, sep='\t')
train_df.columns = ['name', 'cls']
train_df['name'] = train_df['name'].apply(lambda x: str(x).strip().split('/')[-1])
train_df['cls'] = train_df['cls'].apply(lambda x: '%02d' % int(str(x).strip()))
train_df.head()
namecls
08GOkTtqw7E6IHZx4olYnhzvXLCiRsUfM.jpg00
1hwQDH3VBabeFXISfjlWEmYicoyr6qK1p.jpg00
2RDgZKvM6sp3Tx9dlqiLNEVJjmcfQ0zI4.jpg00
3ArBRzHyphTxFS2be9XLaU58m34PudlEf.jpg00
4kmW7GTX6uyM2A53NBZxibYRpQnIVatCH.jpg00
  • 模型输入图片格式应当为 RGB 三通道,假如 imghdr.what 无法识别图片格式则将其删除。
for i in range(len(train_df)):
    img_path = os.path.join('data/data10954/cat_12_train', train_df.at[i, 'name'])

    if os.path.exists(img_path) and imghdr.what(img_path):
        img = Image.open(img_path)
        if img.mode != 'RGB':
            img = img.convert('RGB')
            img.save(img_path)
    else:
        os.remove(img_path)
        print('delete:', img_path)
delete: data/data10954/cat_12_train/ieOvwupZbC4Xckj73znWxo0ARMKD5FrP.jpg
delete: data/data10954/cat_12_train/ovY2atRg8fsZ4jTbKC0UJIOd7mlPEy9u.jpg
  • 从源路径 src_path 移动至目标路径 dst_path。
  • 注意:因为在上一步中仅删除了异常图片,而 DataFrame 没有更新,所以使用 try-except 忽略 DataFrame 中记录仍存在但图片不存在的情况。
for i in range(len(train_df)):
    src_path = os.path.join(
        'data/data10954/cat_12_train',
        train_df.at[i, 'name'])

    dst_path = os.path.join(
        os.path.join(
            'data/data10954/ImageNetDataset/',
            train_df.at[i, 'cls']),
        train_df.at[i, 'name'])

    try:
        shutil.move(src_path, dst_path)
    except Exception as e:
        print(e)
[Errno 2] No such file or directory: 'data/data10954/cat_12_train/ieOvwupZbC4Xckj73znWxo0ARMKD5FrP.jpg'
[Errno 2] No such file or directory: 'data/data10954/cat_12_train/ovY2atRg8fsZ4jTbKC0UJIOd7mlPEy9u.jpg'

3 数据划分

!paddlex --split_dataset --format ImageNet\
    --dataset_dir data/data10954/ImageNetDataset\
    --val_value 0.15\
    --test_value 0

4 数据变换与读取

train_transforms = T.Compose([
    T.MixupImage(mixup_epoch=115),
    T.ResizeByShort(short_size=256),
    T.RandomCrop(crop_size=224, aspect_ratio=[0.75, 1.25], scaling=[0.3, 1.0]),
    T.RandomHorizontalFlip(0.5),
    T.RandomDistort(
        brightness_range=0.4, brightness_prob=0.5,
        contrast_range=0.4, contrast_prob=0.5,
        saturation_range=0.4, saturation_prob=0.5,
        hue_range=18, hue_prob=0.5),
    T.RandomBlur(0.05),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

eval_transforms = T.Compose([
    T.ResizeByShort(short_size=256),
    T.CenterCrop(crop_size=224),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
train_dataset = pdx.datasets.ImageNet(
    data_dir='data/data10954/ImageNetDataset',
    file_list='data/data10954/ImageNetDataset/train_list.txt',
    label_list='data/data10954/ImageNetDataset/labels.txt',
    transforms=train_transforms,
    shuffle=True)

eval_dataset = pdx.datasets.ImageNet(
    data_dir='data/data10954/ImageNetDataset',
    file_list='data/data10954/ImageNetDataset/val_list.txt',
    label_list='data/data10954/ImageNetDataset/labels.txt',
    transforms=eval_transforms)

5 配置与训练

model = pdx.cls.ResNet101_vd_ssld(num_classes=len(train_dataset.labels))

5.1 学习率策略

学习率策略和参数采用经典的 IMAGENET 训练方式:

  1. LinearWarmup:学习率预热 5 个 Epoch(warmup_steps=step_each_epoch * 5);
  2. PiecewiseDecay:阶段性学习率衰减,每 30 个 Epoch 学习率下降至原来的 0.1 倍,共 120 个 Epoch(learning_rate * (0.1**i));
  3. 选择带动量的 Momentum 优化器,权重 L2 正则化系数 0.0005;
  4. 批大小和初始学习率 256-0.1 线性缩放至 64-0.025,考虑到数据集规模较小和使用了预训练权重,又缩小至 64-0.0125。
  • 可以在notebook中输入 ?paddle.optimizer.lr.PiecewiseDecay 了解该程序块的使用方法,例如 ?paddle.optimizer.lr.CosineAnnealingDecay 余弦衰减。
num_epochs = 120
learning_rate = 0.0125
lr_decay_epochs = [30, 60, 90]
train_batch_size = 64
step_each_epoch = train_dataset.num_samples // train_batch_size

boundaries = [b * step_each_epoch for b in lr_decay_epochs]
values = [learning_rate * (0.1**i) for i in range(len(lr_decay_epochs) + 1)]
lr = paddle.optimizer.lr.PiecewiseDecay(
    boundaries=boundaries,
    values=values)

lr = paddle.optimizer.lr.LinearWarmup(
    learning_rate=lr,
    warmup_steps=step_each_epoch * 5,
    start_lr=0.0,
    end_lr=learning_rate)

optimizer = paddle.optimizer.Momentum(
    learning_rate=lr,
    momentum=0.9,
    weight_decay=paddle.regularizer.L2Decay(0.0005),
    parameters=model.net.parameters())

5.2 开始训练

  • 可以在终端输入 watch -n 0 nvidia-smi 查看内存容量情况,根据需求调整图片变换的大小和批大小等参数。
  • 该版本容量情况:9354MiB / 16384MiB。
  • 因为验证数据划分比例小,并且有 Mixup 数据增强,使得早期验证集上的高指标模型鲁棒性可能不如训练末期的。
model.train(
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,

    num_epochs=num_epochs,
    train_batch_size=train_batch_size,
    optimizer=optimizer,

    save_interval_epochs=1,
    log_interval_steps=step_each_epoch * 5,

    pretrain_weights='IMAGENET',
    save_dir='output/ResNet101_vd_ssld',
    use_vdl=True)

6 评估与预测

6.1 模型评估

model = pdx.load_model('output/ResNet101_vd_ssld/best_model')

不同的数据评估变换也会导致不同的结果,例如下方的原来变换 eval_transforms_origin 和修改之后的变换 eval_transforms_modify,指标相差一点。

eval_transforms_origin = T.Compose([
    T.ResizeByShort(short_size=256),
    T.CenterCrop(crop_size=224),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

eval_transforms_modify = T.Compose([
    T.Resize(target_size=224),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
eval_dataset = pdx.datasets.ImageNet(
    data_dir='data/data10954/ImageNetDataset',
    file_list='data/data10954/ImageNetDataset/val_list.txt',
    label_list='data/data10954/ImageNetDataset/labels.txt',
    transforms=eval_transforms_origin)

model.evaluate(eval_dataset=eval_dataset, batch_size=64)
OrderedDict([('acc1', 0.9713542), ('acc5', 1.0)])
eval_dataset = pdx.datasets.ImageNet(
    data_dir='data/data10954/ImageNetDataset',
    file_list='data/data10954/ImageNetDataset/val_list.txt',
    label_list='data/data10954/ImageNetDataset/labels.txt',
    transforms=eval_transforms_modify)

model.evaluate(eval_dataset=eval_dataset, batch_size=64)
OrderedDict([('acc1', 0.9583333), ('acc5', 1.0)])
  • 需要注意的是,如果在模型训练过程中没有指定 eval_dataset,那么训练过程中保存的模型将不会内置评估时的变换方法,此时需要在 model.predict() 中需要指定 model.predit(image, transforms=eval_transforms)
# 我们训练时候设定了评估变换,所以保存下来的模型就自带了
model.get_model_info()['Transforms']
[{'ResizeByShort': {'short_size': 256, 'max_size': -1, 'interp': 'LINEAR'}},
 {'CenterCrop': {'crop_size': 224}},
 {'Normalize': {'mean': [0.485, 0.456, 0.406],
   'std': [0.229, 0.224, 0.225],
   'min_val': [0, 0, 0],
   'max_val': [255.0, 255.0, 255.0],
   'is_scale': True}}]

6.2 模型预测

抽取部分验证集中的图像做可视化。

df_val = pd.read_csv('data/data10954/ImageNetDataset/val_list.txt', header=None, sep='\s+')
df_val.columns = ['path', 'cls']
df_val = df_val.sample(n=12, replace=False)
df_val.index = range(len(df_val))
import matplotlib.pyplot as plt
%matplotlib inline

plt.figure(figsize=(8, 10))
for i in range(12):
    plt.subplot(4, 3, i+1)
    plt.axis('off')
    image = cv2.imread(os.path.join('data/data10954/ImageNetDataset', df_val.at[i, 'path']))
    result = model.predict(image)[0]
    plt.title("%d (True) / %d (Predict) - %.4f" % (df_val.at[i, 'cls'], result['category_id'], result['score']))
    plt.imshow(image[:, :, [2, 1, 0]])

plt.tight_layout()
plt.show()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hPWBqasz-1639095916270)(output_47_0.png)]

  • 生成比赛提交文件,在左侧栏 work/result.csv 下点击下载。
  • 为规避测试集中的异常图片读取,此处也用 PIL.Image.open.convert('RGB') 进行图片的模式转换,之后转成 numpy.ndarray(注意RGB通道转为BGR)。
test_list = sorted(glob.glob('data/data10954/cat_12_test/*.jpg'))
test_df = pd.DataFrame()

for i in range(len(test_list)):
    img = Image.open(test_list[i]).convert('RGB')
    img = np.asarray(img, dtype='float32')
    img = img[:, :, [2, 1, 0]]

    result = model.predict(img)

    test_df.at[i, 'name'] = str(test_list[i]).split('/')[-1]
    test_df.at[i, 'cls'] = int(result[0]['category_id'])

test_df[['name']] = test_df[['name']].astype(str)
test_df[['cls']] = test_df[['cls']].astype(int)

/')[-1]
    test_df.at[i, 'cls'] = int(result[0]['category_id'])

test_df[['name']] = test_df[['name']].astype(str)
test_df[['cls']] = test_df[['cls']].astype(int)

test_df.to_csv('work/result.csv', index=False, header=False)
  • 将该结果文件拿到 试题一:猫十二分类问题 中,生成版本进行 .csv 提交,得到算力和积分(该版本中保存了一份 0.954 的文件)。

7 项目总结

  • 以上是关于 PaddleX 应用于猫图像十二分类赛题的全流程。
  • 需要注意的是,程序代码复杂程度和提交分数不一定呈正相关,具体步骤和参数上可自行修改。
  • 关于模型可解释性等信息可参考 PaddleX 1.3.11 版本文档
  • 更多图像分类技巧可参考 PaddleClas - Ticks
Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐