基于PaddleX的简明猫图像分类全流程实现
基于飞桨工具组件完成猫十二分类问题赛题,轻松实现图像分类全流程;领取AI学习地图100算力+100积分。
·
- 飞桨工具组件 - PaddleX
- 飞桨全流程开发工具,集飞桨核心框架、模型库、工具及组件等深度学习开发所需全部能力于一身,打通深度学习开发全流程。
- PaddleX同时提供简明易懂的Python API,及一键下载安装的图形化开发客户端。用户可根据实际生产需求选择相应的开发方式,获得飞桨全流程开发的最佳体验。
- 比赛链接:飞桨领航团开学季新人赛 - 猫十二分类问题
- AI 学习地图:答题分数达到基线值(70)赢大礼(100 算力和 100 积分)- 试题一:猫十二分类问题
数据说明
本场比赛要求参赛选手对十二种猫进行分类,属于CV方向经典的图像分类任务。图像分类任务作为其他图像任务的基石,可以让大家更快上手计算机视觉。比赛数据集包含12种猫的图片,并划分为训练集与测试集。
- 训练集: 提供高清彩色图片以及图片所属的分类,共有2160张猫的图片,含标注文件。
- 测试集: 仅提供彩色图片,共有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()
name | cls | |
---|---|---|
0 | 8GOkTtqw7E6IHZx4olYnhzvXLCiRsUfM.jpg | 00 |
1 | hwQDH3VBabeFXISfjlWEmYicoyr6qK1p.jpg | 00 |
2 | RDgZKvM6sp3Tx9dlqiLNEVJjmcfQ0zI4.jpg | 00 |
3 | ArBRzHyphTxFS2be9XLaU58m34PudlEf.jpg | 00 |
4 | kmW7GTX6uyM2A53NBZxibYRpQnIVatCH.jpg | 00 |
- 模型输入图片格式应当为 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 - 数据划分:85%(1836)训练+15%(322)验证。
!paddlex --split_dataset --format ImageNet\
--dataset_dir data/data10954/ImageNetDataset\
--val_value 0.15\
--test_value 0
4 数据变换与读取
- PaddleX - 数据变换:数据变换方法对训练的耗时和最终效果也有重要的影响。
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])
])
- PaddleX - 数据读取器:构建模型训练时候的训练、验证读取器。
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 配置与训练
- PaddleX - 图像分类模型:这里选择 ResNet101_vd_ssld,其在 ImageNet 数据集上 TOP1 准确率达到 0.8373。
model = pdx.cls.ResNet101_vd_ssld(num_classes=len(train_dataset.labels))
5.1 学习率策略
学习率策略和参数采用经典的 IMAGENET 训练方式:
- LinearWarmup:学习率预热 5 个 Epoch(
warmup_steps=step_each_epoch * 5
); - PiecewiseDecay:阶段性学习率衰减,每 30 个 Epoch 学习率下降至原来的 0.1 倍,共 120 个 Epoch(
learning_rate * (0.1**i)
); - 选择带动量的 Momentum 优化器,权重 L2 正则化系数 0.0005;
- 批大小和初始学习率 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()
- 生成比赛提交文件,在左侧栏
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。
更多推荐
已为社区贡献1438条内容
所有评论(0)