本博客讲解如何利用Faster-RCNN去训练Pascol VOC数据集或者训练自定义数据集。
Faster-RCNN原理,参考博文:RCNN、Fast-RCNN、Faster-RCNN理论合集
通过import detection.faster_rcnn
, 然后按Ctr
+ 鼠标左键,就可以参考faster_rcnn所实现的一些源码了
import torchvision.models.detection.faster_rcnn
但这里其实只是代码的一部分,和训练相关的代码这里并没有
,根据官方的提示。可以在pytorch github中找到
可以看到训练相关的源代码了,如果自己想看faster-cnn源代码可以结合这两部分。
这里有使用到coco的评价准则,所以需要安装pycocotools
,系统的话建议使用Linux,训练最好也是使用GPU训练
├── backbone: 特征提取网络(分类网络),可以根据自己的要求选择
├── network_files: Faster R-CNN网络(包括Fast R-CNN以及RPN等模块)
├── train_utils: 训练验证相关模块(包括cocotools)
├── my_dataset.py: 自定义dataset用于读取VOC数据集
├── train_mobilenet.py: 以MobileNetV2做为backbone进行训练
├── train_resnet50_fpn.py: 以resnet50+FPN做为backbone进行训练
├── train_multi_GPU.py: 针对使用多GPU的用户使用
├── predict.py: 简易的预测脚本,使用训练好的权重进行预测测试
├── validation.py: 利用训练好的权重验证/测试数据的COCO指标,并生成record_mAP.txt文件
└── pascal_voc_classes.json: pascal_voc标签文件
backbone
就是一系列分类网络,可以根据自己的需求进行选择,我这边根据官方的样例使用了两个backbone:mobienetv2 和resnet50+fpn(特征金字塔结构)
;train_utils
文件中。my_dataset.py
:自定义dataset去读取VOC数据集,官方也提供了直接读取VOC数据集的工具,我这里主要是强调如何自己读取dataset,了解自定义数据集的原理之后就可以按照自己的需求创建自定义数据集,编写相应的脚本去读取就可以了。train_mobilenet.py
: 以MobileNetV2做为backbone进行训练,该backbone预测特征层只有一层,和Faster-RCNN论文描述的方法基本上保持一致train_resnet50_fpn.py
: 以resnet50+FPN做为backbone进行训练,个人比较推荐用这个脚本,因为该脚本的训练效果是很好的,如果你要在实际应用去使用的话,尽可能去使用该脚本。对于 train_mobilenet.py
: 以MobileNetV2做为backbone进行训练,准确率相比要低很多,不建议在实际项目中去使用,主要原因一方面是训练数据不够多,另外该官方没有提供完整的预训练模型权重,但官方给出了完整的resnet50+FPN
的预训练权重。基于resnet50+FPN
预训练权重进行迁移学习,就能很快迭代到属于自己的模型了。train_multi_GPU.py
: 针对使用多GPU的用户使用,它的训练方法和单GPU训练方法是不一样的,单GPU可以直接在IDE环境中run
脚本文件进行训练。但train_multi_GPU.py
需要在终端命令行窗口输入指令进行训练。validation.py
: 利用训练好的权重验证/测试数据的COCO指标,并生成record_mAP.txt文件pascal_voc_classes.json:
pascal_voc标签文件,标签文件如下:{
"aeroplane": 1,
"bicycle": 2,
"bird": 3,
"boat": 4,
"bottle": 5,
"bus": 6,
"car": 7,
"cat": 8,
"chair": 9,
"cow": 10,
"diningtable": 11,
"dog": 12,
"horse": 13,
"motorbike": 14,
"person": 15,
"pottedplant": 16,
"sheep": 17,
"sofa": 18,
"train": 19,
"tvmonitor": 20
}
标签为啥没有从0开始?
因为在我们目标检测当中,一般0是专门留给我们的背景的。虽然我们说pascal_voc只有20个类别,其实实际训练过程中给出了21个类别,专门为背景设了一个类别。
预训练权重下载地址(下载后放入backbone文件夹中)
给的是完整的模型权重,不仅包括
back_bone部分,还包括RPN和Fast-RCNN后半部分的权重。所以针对
ResNet50+FPN给出的权重,可以针对自己的数据集进行较好的迁移学习,并且很快能得到一个比较理想的结果
fasterrcnn_resnet50_fpn_coco.pth
文件, 不是fasterrcnn_resnet50_fpn_coco-258fb6c6.pth
本例程使用的是PASCAL VOC2012
数据集
backbone
文件夹中,代码中是从backbone文件中寻找预训练权重的。python -m torch.distributed.launch --nproc_per_node=8 --use_env train_multi_GPU.py
指令,nproc_per_node参数为使用GPU数量
CUDA_VISIBLE_DEVICES=0,3
(例如我只要使用设备中的第1块和第4块GPU设备)CUDA_VISIBLE_DEVICES=0,3 python -m torch.distributed.launch --nproc_per_node=2 --use_env train_multi_GPU.py
其中torch.distributed.launch
是一个多进程的工具,可以开启多个进程执行python脚本,同时调用多个GPU进行加速训练。nproc_per_node
指定GPU的个数,train_multi_GPU.py
就是项目中训练多GPU的代码
--data-path
(VOC_root)设置为自己存放VOCdevkit文件夹所在的根目录FPN
结构的Faster RCNN很吃显存
,如果GPU的显存不够(如果batch_size小于8的话)建议在create_model函数中使用默认的norm_layer
, 即不传递norm_layer变量,默认去使用FrozenBatchNorm2d(即不会去更新参数的bn层),使用中发现效果也很好。results.txt
是每个epoch在验证集上的COCO
指标,前12个值是COCO
指标,后面两个值是训练平均损失以及学习率train_weights
设置为你自己生成的权重路径。validation
文件时,注意确保你的验证集或者测试集中必须包含每个类别的目标,并且使用时只需要修改--num-classes
、--data-path
和--weights-path
即可,其他代码尽量不要改动这里以train_mobienet.py
代码进行讲解
gpu
还是cpu
device = torch.device(parser_data.device if torch.cuda.is_available() else "cpu")
print(device)
data_transform = {
"train": transforms.Compose([transforms.ToTensor(),
transforms.RandomHorizontalFlip(0.5)]),
"val": transforms.Compose([transforms.ToTensor()])
}
ToTensor()
:包含了两个功能:1. 图片归一化到(0,1) 2.将nd.array数据转换为tensor
目标检测图像预处理和分类网络的预处理是不一样的,比如transforms.RandomHorizontalFlip(0.5)
,因为在我们目标检测中,我们对图片水平方向进行随机翻转的话,我们所标注的GT Box
坐标也同样需要进行水平翻转。
VOC_root = parser_data.data_path
# load train data set
train_data_set = VOC2012DataSet(VOC_root, data_transform["train"], True)
并通过dataloader载入数据集:
# 注意这里的collate_fn是自定义的,因为读取的数据包括image和targets,不能直接使用默认的方法合成batch
train_data_loader = torch.utils.data.DataLoader(train_data_set,
batch_size=4,
shuffle=True,
num_workers=0,
collate_fn=utils.collate_fn)
其中collate_fn
代码
def collate_fn(batch):
return tuple(zip(*batch))
batch_size
的大小需要根据你电脑的GPU显存去设计,如果提示显存不够就需要减小batch_size
的大小。
同理验证集的数据集
# load validation data set
val_data_set = VOC2012DataSet(VOC_root, data_transform["val"], False)
val_data_set_loader = torch.utils.data.DataLoader(val_data_set,
batch_size=2,
shuffle=False,
num_workers=0,
collate_fn=utils.collate_fn)
# create model num_classes equal background + 20 classes
model = create_model(num_classes=21)
print(model)
model.to(device)
以backbone为mobienet为例,训练第一阶段载入mobienet v2
的backbone
模型初始化backbone这块,然后冻结backbone
去训练RPN以及Fast-RCNN的后半部分。通过固定backbone权重去微调RPN和Fast-RCNN的后半部分。
这里通过5个epoch
对网络进行微调,并保存模型
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# first frozen backbone and train 5 epochs #
# 首先冻结前置特征提取网络权重(backbone),训练rpn以及最终预测网络部分 #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
for param in model.backbone.parameters():
param.requires_grad = False
# define optimizer
params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params, lr=0.005,
momentum=0.9, weight_decay=0.0005)
num_epochs = 5
for epoch in range(num_epochs):
# train for one epoch, printing every 10 iterations
utils.train_one_epoch(model, optimizer, train_data_loader,
device, epoch, print_freq=50)
# evaluate on the test dataset
utils.evaluate(model, val_data_set_loader, device=device)
torch.save(model.state_dict(), "./save_weights/pretrain.pth")
阶段2:
解冻前置特征提取网络权重(backbone),接着训练整个网络权重。
这里代码借鉴pytorch 上实现的resnet50+fpn代码来实现的。
# 冻结backbone部分底层权重
for name, parameter in model.backbone.named_parameters():
split_name = name.split(".")[0]
if split_name in ["0", "1", "2", "3"]:
parameter.requires_grad = False
else:
parameter.requires_grad = True
冻结了backbone的部分底层权重,比如前一两层都是相对通用的特征。而且我们的pascal_voc的训练数据不是很大,也就5000多张图片,训练整个网络其实是远远不够的。最终的结果来看,它比训练整个模型的效果要好一点。
对require_grad=True
对需要训练的参数进行训练,学习率策略lr_scheduler
,设置为每隔5步,将学习率降低为原来的0.33倍。
# define optimizer
params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params, lr=0.005,
momentum=0.9, weight_decay=0.0005)
# learning rate scheduler
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer,
step_size=5,
gamma=0.33)
这里迭代了20
个epoch
,每迭代一个epoch
,将学习率的调整策略lr_scheduler
,执行一个step
方法,这样就记录了已经执行了一步了,每隔5步,它将会降低一次学习率。
num_epochs = 20
for epoch in range(num_epochs):
# train for one epoch, printing every 50 iterations
utils.train_one_epoch(model, optimizer, train_data_loader,
device, epoch, print_freq=50, warmup=True)
# update the learning rate
lr_scheduler.step()
保存权重这块,我这里是从第10个epoch
开始保存的,在我训练过程中,我发现基本上在第10个epoch就开始收敛了。
注意这里保存的权重,并不光指模型的权重,之前在训练分类网络的时候,基本上就将model.state_dict()
权重进行保存。这里除了保存模型权重之外,还保存了有关优化器optimizer的状态以及学习策略lr_sheduler以及epoch数,这样我们再后面如果想接着你上次训练的话,可以载入这些参数,接着上次的方法继续训练了。
for epoch in range(num_epochs):
# train for one epoch, printing every 50 iterations
utils.train_one_epoch(model, optimizer, train_data_loader,
device, epoch, print_freq=50, warmup=True)
# update the learning rate
lr_scheduler.step()
# evaluate on the test dataset
utils.evaluate(model, val_data_set_loader, device=device)
# save weights
if epoch > 10:
save_files = {
'model': model.state_dict(),
'optimizer': optimizer.state_dict(),
'lr_scheduler': lr_scheduler.state_dict(),
'epoch': epoch}
torch.save(save_files, "./save_weights/mobile-model-{}.pth".format(epoch))