首先我们要下载mmclassification-master项目,然后在python中打开:
一定要以刚下载好的这个文件夹作为根目录打开,否则会出现找不到库的报错。这个项目里有很多分类模型,都包含在configs文件夹中,由于今天还是以完成花朵分类为任务,我们依然选择resnet的网络:
点开之后可以看到这个网络的具体类别,比如18层网络,50层网络等。此外,还有能适应多机多卡、不同batch的.py文件:
以resnet18_8xb16_cifar10.py为例,这就是可以适应8卡,每张卡的batch均为16,使用的数据集为cifar10的.py文件。此外,有些模型还标注了学习率衰减策略,数据增强策略等。当然如果我们的电脑是单卡也没关系,我们适当选择一个比较匹配的文件即可,所以我们选择resnet18_8xb32_in1k.py文件。首先看看里面的内容是什么:
_base_ = [
'../_base_/models/resnet18.py', '../_base_/datasets/imagenet_bs32.py',
'../_base_/schedules/imagenet_bs256.py', '../_base_/default_runtime.py'
]
这个文件为我们指出了四个模块,分别为网络结构的定义,数据集定义,学习策略,常规的训练方式。不懂没关系,这四个部分就是四个路径,我们稍后需要按图索骥,挨个调整。
下面我们需要逐个进入四个模块,我们首先进入这个resnet18文件看看:
model = dict(
# type对应着mmcls/classifiers/image中的类名,不能写错
# 所有的resnetxx文件都有这个type,type的值不能随意改动,必须和对应的定义严格匹配
type='ImageClassifier',
backbone=dict( # backbone是提取特征的依据,可选的模型包括在mmcls/models/backbones里
type='ResNet',
depth=18, # 层数
num_stages=4, # 可以理解成输出的特征数,stage越靠后,输出的特征数量越少
out_indices=(3, ), # 输出,这里设置成了只输出最后一个stage内容
style='pytorch'), # 默认为pytorch,3*3卷积核,straight=2
neck=dict(type='GlobalAveragePooling'), # 对特征进行整合策略,这个参数是可以改的
# 对于简单的模型而言,做一个全局的平均池化(GlobalAveragePooling)即可
head=dict( # 卷机虽然不限制输入大小,但全连接层需要限制
type='LinearClsHead', # 全连接
num_classes=102, # 分类数量,需要具体修改
in_channels=512, # 根据neck特征图数量定
loss=dict(type='CrossEntropyLoss', loss_weight=1.0), # 既要输出结果,也要计算损失
# 这里的type可以改成mmcls/models/losses中提供的内容(如FocalLoss)
topk=(1, 5),
))
需要强调的点我都已经放到注释里了。接下来就可以继续看imagenet_bs32.py文件了。这里主要讲的是读取数据以及对数据的一些处理,如随机剪裁、图像增强等,以及对训练集,验证集和测试集的部分设置:
dataset_type = 'ImageNet' # 数据的格式,非固定,可按照自己的格式去做
# ImageNet来自于mmcls/datasets/imagenet.py
img_norm_cfg = dict(
mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True) # 标准化
train_pipeline = [ # 训练
dict(type='LoadImageFromFile'),
# 图像增强
dict(type='RandomResizedCrop', size=224), # 随机裁剪
dict(type='RandomFlip', flip_prob=0.5, direction='horizontal'), # 概率旋转
dict(type='Normalize', **img_norm_cfg), # 归一化
dict(type='ImageToTensor', keys=['img']), # 图像数据转换格式
dict(type='ToTensor', keys=['gt_label']), # 标签数据转换格式
dict(type='Collect', keys=['img', 'gt_label']) # 定义返回图像和标签
]
test_pipeline = [ # 测试
# 测试不需要数据增强
dict(type='LoadImageFromFile'),
dict(type='Resize', size=(256, -1)), # 调整大小,避免剪裁时丢失太多信息。训练集也可以加入该操作
dict(type='CenterCrop', crop_size=224), # 剪裁
dict(type='Normalize', **img_norm_cfg), # 归一化
dict(type='ImageToTensor', keys=['img']), # 图像数据转换格式
dict(type='Collect', keys=['img']) # 测试集没有标签,需要返回分类标签
]
data = dict(
samples_per_gpu=32, # 每张卡的batch(我的电脑是单卡的),显存不够可调小。如果还不行的话可以剪裁成更小的输入图像
workers_per_gpu=2,
train=dict(
type=dataset_type, # 可以自行修改
data_prefix='data/imagenet/train', # 读取数据路径
pipeline=train_pipeline),
val=dict(
type=dataset_type,
# 两种不同方式读标签
data_prefix='data/imagenet/val', # 以文件夹名为标签
ann_file='data/imagenet/meta/val.txt', # 以文档为标签
pipeline=test_pipeline),
test=dict(
# replace `data/val` with `data/test` for standard test
type=dataset_type,
data_prefix='data/imagenet/val',
ann_file='data/imagenet/meta/val.txt',
pipeline=test_pipeline))
# 经过interval个epoch才进行一次验证
# metric='accuracy'为设置评估标准
evaluation = dict(interval=1, metric='accuracy')
老样子,关键点已经写在注释里了。
继续打开imagenet_bs256.py:
# optimizer
# 指定优化器 学习率 动量 衰减
optimizer = dict(type='Adam', lr=0.1, momentum=0.9, weight_decay=0.0001)
optimizer_config = dict(grad_clip=None)
# learning policy
# 在迭代进行30次、60次和90次时衰减
lr_config = dict(policy='step', step=[30, 60, 90])
# 第一个参数最好使用默认值
runner = dict(type='EpochBasedRunner', max_epochs=100) # 最多迭代100个epoch,可以进行修改
最后是default_runtime.py文件:
# checkpoint saving
checkpoint_config = dict(interval=20) # 间隔多少epoch保存一次,可以设置大一点
# yapf:disable
log_config = dict(
interval=100,
hooks=[
dict(type='TextLoggerHook'),
])
dist_params = dict(backend='nccl')
log_level = 'INFO'
load_from = None # 加载模型位置
resume_from = None # 从上一个保存的epoch开始继续做训练
workflow = [('train', 1)]
这样我们就调好了四个文件了,但是现在代码还不能跑起来,我们还需要继续做调试。
首先我们复制一下resnet18_8xb32_in1k.py完整的路径,然后打开tools文件夹,找到train文件并打开,然后在右上角依次点击:
然后将复制的路径粘贴到这里:
需要注意我们要把复制来的“\”全部改成“\”。改完之后我们就可以运行了,会有报错(就算没有报错也可以按照下面的步骤继续),但是这次运行可以生成我们需要的配置文件:
因为我们调整的内容中,生成的新文件会保存在tools\work_dirs\resnet18_8xb32_in1k路径下,点开文件夹会有一个最新生成的.py文件:
我们把它重命名并复制到resnet当中:
这个文件参考了我们上述所有的调整,并将它们整合在了一起。紧接着我们把参数再调整一下:
不知道大家是否记得我们之前做花朵分类时是有两种读取文件和标签的方式的,一种是用不同文件夹储存不同分类的花朵,文件夹名就是对应分类;另一种是将所有图片都放在一个文件夹里,然后单做一个分类文本,写好图片名称和对应的分类。那么我们还是依次介绍这两种方式读取方法。
首先还是按文件夹名分类的方式,我们有训练集合验证集两个文件,两个都是102文件夹存储图像:
记好这两个路径,然后我们来修改刚刚生成好的today_resnet18_8xb32_in1k.py文件,需要改的不多,大家仔细看注释:
model = dict(
type='ImageClassifier',
backbone=dict(
type='ResNet',
depth=18,
num_stages=4,
out_indices=(3, ),
style='pytorch'),
neck=dict(type='GlobalAveragePooling'),
head=dict(
type='LinearClsHead', # 全连接层
num_classes=102,
in_channels=512,
loss=dict(type='CrossEntropyLoss', loss_weight=1.0),
topk=(1, 5)))
dataset_type = 'ImageNet'
img_norm_cfg = dict(
mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)
train_pipeline = [
dict(type='LoadImageFromFile'),
dict(type='RandomResizedCrop', size=224), # size可以适当调整
dict(type='RandomFlip', flip_prob=0.5, direction='horizontal'),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='ImageToTensor', keys=['img']),
dict(type='ToTensor', keys=['gt_label']),
dict(type='Collect', keys=['img', 'gt_label'])
]
test_pipeline = [
dict(type='LoadImageFromFile'),
dict(type='Resize', size=(256, -1)),
dict(type='CenterCrop', crop_size=224),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='ImageToTensor', keys=['img']),
dict(type='Collect', keys=['img'])
]
data = dict(
samples_per_gpu=32,
workers_per_gpu=2,
train=dict(
type='ImageNet',
data_prefix='../mmcls/data/flower_data/train', # 训练内容的所在路径
pipeline=[
dict(type='LoadImageFromFile'), # 加载数据方法,不需要改
dict(type='RandomResizedCrop', size=224),
dict(type='RandomFlip', flip_prob=0.5, direction='horizontal'),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='ImageToTensor', keys=['img']),
dict(type='ToTensor', keys=['gt_label']),
dict(type='Collect', keys=['img', 'gt_label'])
]),
val=dict(
type='ImageNet',
data_prefix='../mmcls/data/flower_data/valid',
# ann_file就是以文件夹名作为分类标签
# ann_file='data/imagenet/meta/val.txt', # 传入标注文件
pipeline=[
dict(type='LoadImageFromFile'),
dict(type='Resize', size=(256, -1)),
dict(type='CenterCrop', crop_size=224),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='ImageToTensor', keys=['img']),
dict(type='Collect', keys=['img'])
]),
test=dict( # 测试集暂时设置成和验证集一样
type='ImageNet',
data_prefix='../mmcls/data/flower_data/valid',
# ann_file='data/imagenet/meta/val.txt',
pipeline=[
dict(type='LoadImageFromFile'),
dict(type='Resize', size=(256, -1)),
dict(type='CenterCrop', crop_size=224),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='ImageToTensor', keys=['img']),
dict(type='Collect', keys=['img'])
]))
evaluation = dict(interval=1, metric='accuracy') # 每interval个epoch做一次评估
optimizer = dict(type='SGD', lr=0.1, momentum=0.9, weight_decay=0.0001)
optimizer_config = dict(grad_clip=None)
lr_config = dict(policy='step', step=[30, 60, 90])
runner = dict(type='EpochBasedRunner', max_epochs=100)
checkpoint_config = dict(interval=50) # 每50个epoch保存一次
log_config = dict(interval=100, hooks=[dict(type='TextLoggerHook')])
dist_params = dict(backend='nccl')
log_level = 'INFO'
load_from = None
resume_from = None
workflow = [('train', 1)]
work_dir = './work_dirs\\resnet18_8xb32_in1k' # 工作路径,可以自行拟定
# 默认为tool/work_dirs/esnet18_8xb32_in1k
gpu_ids = [0]
改到这里,就可以去跑train.py文件了,结果我们会在最后进行展示。
接下来我们介绍用文本记录分类的方法,只需要把存储图像的文件夹和文本文件提供好即可:
当然,我们的today_resnet18_8xb32_in1k.py文件也需要修改,修改的方法我已经在上文中的注释中提到过,这里就不在赘述了。当然,改完这个文件还是不能运行的,我们需要自己写一个数据处理的文件,放在mmcls\datasets里面,我把它写作my_filelist.py。这个文件可以参考同级目录下的imagenet.py文件。尽量不要改太多的内容,以免跑不起来:
import numpy as np
from .builder import DATASETS
from .base_dataset import BaseDataset
@DATASETS.register_module()
class MyFilelist(BaseDataset): # 类名可以自己调整
CLASS=[] # 记录分类名称
for i in range(102):
CLASS.append('第' + str(i) + '个类别')
def load_annotations(self): # 方法名不要乱动
assert isinstance(self.ann_file, str) # assert遇到False会自定义报错,如assert False,'出错了!'
data_infos = [] # 新建列表,保证每一个标签要传进去
with open(self.ann_file) as f: # 打开标注文件
samples = [x.strip().split(' ') for x in f.readlines()] # 打开标注文件,以空格为分割符,生成一个二维列表
for filename, gt_label in samples: # filename代表文件名,gt_label代表标签名
info = {'img_prefix': self.data_prefix} # 设置好文件路径的前缀
info['img_info'] = {'filename': filename} # 图片的名字信息
info['gt_label'] = np.array(gt_label, dtype=np.int64) # 图片标签信息
data_infos.append(info)
return data_infos
写好之后,我们转到同目录的___ init __.py文件中,引用我们自己写的my_filelist:
# Copyright (c) OpenMMLab. All rights reserved.
from .base_dataset import BaseDataset
from .builder import (DATASETS, PIPELINES, SAMPLERS, build_dataloader,
build_dataset, build_sampler)
from .cifar import CIFAR10, CIFAR100
from .dataset_wrappers import (ClassBalancedDataset, ConcatDataset,
KFoldDataset, RepeatDataset)
from .imagenet import ImageNet
from .imagenet21k import ImageNet21k
from .mnist import MNIST, FashionMNIST
from .multi_label import MultiLabelDataset
from .samplers import DistributedSampler, RepeatAugSampler
from .voc import VOC
from .my_filelist import MyFilelist # 导进自己起的文件
__all__ = [
'BaseDataset', 'ImageNet', 'CIFAR10', 'CIFAR100', 'MNIST', 'FashionMNIST',
'VOC', 'MultiLabelDataset', 'build_dataloader', 'build_dataset',
'DistributedSampler', 'ConcatDataset', 'RepeatDataset',
'ClassBalancedDataset', 'DATASETS', 'PIPELINES', 'ImageNet21k', 'SAMPLERS',
'build_sampler', 'RepeatAugSampler', 'KFoldDataset', 'MyFilelist' # 在最后加上自己新写的文件名
]
紧接着还要回到today_resnet18_8xb32_in1k.py文件中,修改好路径,然后将第53行的内容改成
type='MyFilelist'
到这里调试也就结束啦,以文本的方式读取的代码也能跑起来了。
因为上述两种方法跑起来的终端显示是一样的,我们一起来展示。运行train.py:
这里面就记录有关键的准确率信息。至于训练好的模型如何使用,就先给大家卖个关子吧,今天的内容已经很多了~