PyTorch深度学习实战(21)——从零开始实现Faster R-CNN目标检测
- 0. 前言
- 1. Fast R-CNN 目标检测模型组成
- 1.1 锚框
- 1.2 区域提议网络
- 1.3 分类和回归
- 2. 实现 R-CNN 目标检测
- 2.1 数据处理
- 2.2 模型构建
- 2.3 模型训练与测试
- 小结
- 系列链接
0. 前言
Faster R-CNN
是对 R-CNN
系列算法的进一步改进,与 R-CNN 和 Fast R-CNN 不同,Faster R-CNN
提出了一种全新的目标检测框架,将候选框生成和目标分类合并到一个网络中,实现了端到端的训练,可以同时优化候选框生成和目标分类任务,提高了检测的准确性和效率。同时,利用共享的卷积特征可以加速特征提取的计算,进一步提高了检测速度。在本节中,将介绍 Faster R-CNN
的工作原理,然后在自定义数据集上训练 Faster R-CNN
目标检测模型。
1. Fast R-CNN 目标检测模型组成
R-CNN
和 Fast R-CNN
模型的主要缺陷是它们使用两个独立的网络,一个用于识别可能包含对象的区域,另一个用于对预测的识别对象边界框进行校正。此外,这两种模型都需要多次前向传播才能完成所有区域提议的检测。而新兴的目标检测算法主要集中于训练单个神经网络,并能够在一次前向传播中检测所有目标。接下来,我们将介绍单阶段对象检测算法的各个组成部分,包括锚框、区域提议网络 (Region Proposal Network
, RPN
) 和 RoI
(Region of Interest
) 池化。
1.1 锚框
锚框 (Anchor Box
),也称先验框 (Prior Box
),在目标检测中,锚框是一些预设的固定大小和宽高比的矩形框,这些矩形框被放置于输入图像中的所有可能位置。目标检测算法通过在输入图像的每个位置上运用锚框,来判断该位置是否包含目标,并对矩形框进行调整,从而尽可能准确地预测出真实的目标位置。
我们已经学习了如何基于选择性搜索方法获取区域建议,相比之下,锚框可以更快速地提供目标检测结果。在使用锚框进行目标检测时,我们首先生成一系列预定义的锚框,对于每个锚框,需要对其进行分类(即确定其是否包含目标)和回归(即调整它们的位置),以得到预测边界框,通过使用这些预测边界框,可以获得目标检测结果。区域提议通常包括几个阶段,如生成候选区域、提取特征并进行分类以及回归。而使用锚框,则可以直接在特征图上进行分类和回归,从而提高了检测效率和准确性。
一般来说,大多数物体的形状是类似的,例如,在大多数情况下,人的图像对应的边界框高度大于宽度,而卡车的图像对应的边界框宽度大于高度。因此,只需检查数据集中各种目标对象对应的标注边界框,我们就可以相当准确地了解图像中物体的高度和宽度。此外,图像中感兴趣的对象可能会被缩放,导致高度和宽度大幅降低,但相应的宽高比保持不变(即高度/宽度)。
当我们根据数据集的标注信息了解了图像中目标对象的宽高比、高度和宽度后,我们就可以定义能够代表大多数对象边界框的锚框。通常,可以对图像中目标对象的实际边界框应用 K-means
聚类获得代表性锚框。
获得锚框后,将其应用于目标检测模型中:
- 将每个锚框从左上角滑到右下角覆盖整张图像
- 与目标对象交并比 (
Intersection over Union
,IoU
) 较高的锚框被标记为包含目标的锚框,其他的则被标记为0
我们可以修改 IoU
的阈值,如果 IoU
大于给定阈值,则对象类别为 1
;如果小于另一指定阈值,则对象类别为 0
,否则将其保留为未知类别。
获取了真实边界框之后,构建模型来预测物体的位置以及与锚框相对应的偏移量,以使其与真实框匹配。下图展示了锚框的表示方式:
在上图中,包含两个锚框,一个高度大于宽度,另一个宽度大于高度,以对应图像中的目标对象。在图像上滑动这两个锚框,并记录与真实框的 IoU
最高的位置,以指定该特定位置包含目标对象,而其余位置则不包含目标对象。除了以上两个锚框之外,我们还会创建多个具有不同比例的锚框,以便适应图像中不同比例的目标对象:
在上图中,可以看到所有锚框的中心位置相同,但比例不同。在下一小节中,我们将介绍区域提议网络 (Region Proposal Network
, RPN
) ,利用锚框来预测可能包含对象的区域。
1.2 区域提议网络
假设图像尺寸为 224 x 224 x 3
,且锚框形状为 8 x 8
,如果步幅为 8
,那么每一行都需要取出 224/8 = 28
个图像切片,也就是说,一张图片会产生 28*28 = 576
个切片。然后,将每个切片输入到区域提议网络 (Region Proposal Network
, RPN
) 模型中,以确定该切片是否包含目标对象。简而言之,RPN
可以输出每个切片包含目标对象的概率。接下来,我们比较选择性搜索的输出和 RPN
的输出。
选择性搜索是一种基于图像分割的方法,它通过将图像分割成多个区域,然后合并这些区域来产生候选框,最终输出的是多个候选框,用于表示可能包含待检测目标对象的区域。区域提议网络 (Region Proposal Network
, RPN
) 是一种深度学习算法,它将整张图像作为输入,并输出若干个“建议区域” (proposed region
)。这些建议区域是基于锚框 (anchor box
) 概念计算得到的,每个建议区域都是一个带有坐标和大小信息的矩形框,用于表示可能包含待检测目标对象的区域。前者是传统的基于图像分割的算法,而后者则是深度学习算法,更加高效准确。
基于选择性搜索的区域提议生成是在神经网络之外完成的,而 RPN
则是目标检测网络的一部分。使用 RPN
可以不必在网络外部执行区域提议计算,这样,我们仅需要使用一个单一的模型就可以确定候选区域、确定图像中物体的类别以及相应的边界框位置。
接下来,我们将学习 RPN
如何确定候选区域(在滑动锚框后获得的切片)是否包含目标对象。在训练数据中,有与物体对应的真实标注信息,获取每个候选区域并与图像中物体的真实边界框进行比较,以确定该候选区域与真实边界框之间的 IoU
是否大于给定阈值。如果 IoU
大于给定阈值(例如 0.5
),则该候选区域包含目标对象;如果 IoU
小于给定阈值(例如 0.1
),则该候选区域不包含目标对象,并且在训练时忽略所有 IoU
在这两个阈值之间 (0.1 - 0.5
) 的候选区域。
使用训练模型预测区域候选是否包含目标对象后,执行非极大值抑制 (non-maximum suppression
, NMS
),因为多个重叠的区域可能包含同一个目标对象。
综上,RPN
通过执行以下步骤训练模型,使其能够识别具有较高置信度包含目标对象的候选区域:
- 在图像上滑动不同纵横比和大小的锚框以获取图像切片
- 计算图像中目标对象的真实边界框与图像切片间的
IoU
- 准备训练数据集,
IoU
大于指定阈值的图像切片包含目标对象,而IoU
小于指定阈值的图像切片不包含目标对象 - 训练模型识别包含目标对象的区域
- 执行非极大值抑制,识别出包含对象概率最高的候选区域,并剔除与其重叠度较高的其他候选区域
1.3 分类和回归
使用以下步骤预测对象类别及其边界框偏移量:
- 识别包含目标对象的区域
- 使用
RoI
(Region of Interest
) 池化确保所有候选区域的特征图尺寸相同
上述步骤包含如下两个问题:
- 区域区域与目标对象边界框并未完全对应
- 可以确定区域是否包含目标对象,但不确定该区域中的目标对象类别
在本节中,我们将解决以上两个问题。将得到的形状一致的特征图输入到神经网络中,并且希望网络能够预测区域内包含的目标对象类别以及与该区域相对应的偏移量,以确保预测边界框尽可能接近目标对象真实边界框:
从上图中可以看到,将 RoI
池化的输出(形状为 7 x 7 x 512
) 作为输入,并在展平后将其连接到全连接层,得到以下两个预测结果:
- 区域内的对象类别
- 区域内预测边界框的偏移量
因此,如果数据中包含 20
个类别,则神经网络将包含 25
个输出——21
个类别概率(包括背景类别)以及边界框高度、宽度和两个中心坐标的偏移量。
我们可以使用下图来总结,Faster R-CNN
目标检测模型的架构与原理:
2. 实现 R-CNN 目标检测
在本节中,使用 PyTorch
构建 Faster R-CNN
模型检测图像中目标对象的类别及其边界框,我们将继续使用与 R-CNN 一节中相同的数据集构建 Faster R-CNN
目标检测模型。
2.1 数据处理
(1) 读取包含图像及其边界框和类别信息元数据的 DataFrame
:
import selectivesearch
from torchvision import transforms, models, datasets
from torchvision.ops import nms
import os
import torch
import numpy as np
from torch.utils.data import DataLoader, Dataset
from glob import glob
from random import randint
import cv2
from pathlib import Path
import torch.nn as nn
from torch import optim
from matplotlib import pyplot as plt
import pandas as pd
import matplotlib.patches as mpatches
from PIL import Image
device = 'cuda' if torch.cuda.is_available() else 'cpu'
IMAGE_ROOT = 'open-images-bus-trucks/images/images'
DF_RAW = df = pd.read_csv('open-images-bus-trucks/df.csv')
print(DF_RAW.head())
(2) 定义标签对应的索引:
label2target = {l:t+1 for t,l in enumerate(DF_RAW['LabelName'].unique())}
label2target['background'] = 0
target2label = {t:l for l,t in label2target.items()}
background_class = label2target['background']
num_classes = len(label2target)
(3) 定义图像预处理函数 preprocess_image()
与图像查找函数 find()
:
def preprocess_image(img):
img = torch.tensor(img).permute(2,0,1)
return img.to(device).float()
def find(item, original_list):
results = []
for o_i in original_list:
if item in o_i:
results.append(o_i)
if len(results) == 1:
return results[0]
else:
return results
(4) 定义数据集类 OpenDataset
。
定义 __init__
方法,将图像文件夹和包含图像元数据的 DataFrame
作为输入:
class OpenDataset(torch.utils.data.Dataset):
w, h = 224, 224
def __init__(self, df, image_dir=IMAGE_ROOT):
self.image_dir = image_dir
self.files = glob(self.image_dir+'/*')
self.df = df
self.image_infos = df.ImageID.unique()
定义 __getitem__
方法,返回预处理后的图像和目标值:
def __getitem__(self, ix):
# load images and masks
image_id = self.image_infos[ix]
img_path = find(image_id, self.files)
img = Image.open(img_path).convert("RGB")
img = np.array(img.resize((self.w, self.h), resample=Image.BILINEAR))/255.
data = df[df['ImageID'] == image_id]
labels = data['LabelName'].values.tolist()
data = data[['XMin','YMin','XMax','YMax']].values
data[:,[0,2]] *= self.w
data[:,[1,3]] *= self.h
boxes = data.astype(np.uint32).tolist() # convert to absolute coordinates
# torch FRCNN expects ground truths as a dictionary of tensors
target = {}
target["boxes"] = torch.Tensor(boxes).float()
target["labels"] = torch.Tensor([label2target[i] for i in labels]).long()
img = preprocess_image(img)
return img, target
以上 __getitem__
方法将输出作为张量字典而非张量列表返回,这是因为我们期望输出包含边界框的绝对坐标和标签信息。
定义 collate_fn
方法(处理字典列表)和 __len__
方法:
def collate_fn(self, batch):
return tuple(zip(*batch))
def __len__(self):
return len(self.image_infos)
(5) 创建训练和验证数据加载器和数据集:
from sklearn.model_selection import train_test_split
trn_ids, val_ids = train_test_split(df.ImageID.unique(), test_size=0.1, random_state=99)
trn_df, val_df = df[df['ImageID'].isin(trn_ids)], df[df['ImageID'].isin(val_ids)]
len(trn_df), len(val_df)
train_ds = OpenDataset(trn_df)
test_ds = OpenDataset(val_df)
train_loader = DataLoader(train_ds, batch_size=4, collate_fn=train_ds.collate_fn, drop_last=True)
test_loader = DataLoader(test_ds, batch_size=4, collate_fn=test_ds.collate_fn, drop_last=True)
2.2 模型构建
(1) 定义模型:
import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
device = 'cuda' if torch.cuda.is_available() else 'cpu'
def get_model():
model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=True)
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
return model
该模型包含以下关键子模块:
GeneralizedRCNNTransform
:在调整图像大小后执行归一化转换BackboneWithFPN
:将输入转换为特征图RegionProposalNetwork
:为图像特征图生成锚框,并针对分类和回归任务获取区域特征图RoIHeads
采用区域特征图,使用RoI
池化对其进行处理,生成固定大小的区域特征图,并返回每个区域提议的分类概率和偏移量
(2) 定义函数在批数据上训练网络,并计算验证数据集的损失值:
def train_batch(inputs, model, optimizer):
model.train()
input, targets = inputs
input = list(image.to(device) for image in input)
targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
optimizer.zero_grad()
losses = model(input, targets)
loss = sum(loss for loss in losses.values())
loss.backward()
optimizer.step()
return loss, losses
@torch.no_grad()
def validate_batch(inputs, model, optimizer):
model.train()
input, targets = inputs
input = list(image.to(device) for image in input)
targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
optimizer.zero_grad()
losses = model(input, targets)
loss = sum(loss for loss in losses.values())
return loss, losses
2.3 模型训练与测试
(1) 训练模型。
初始化模型:
model = get_model().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.005,
momentum=0.9, weight_decay=0.0005)
n_epochs = 10
train_loss_epochs = []
train_loc_loss_epochs = []
train_regr_loss_epochs = []
train_objectness_loss_epochs = []
train_rpn_box_reg_loss_epochs = []
val_loss_epochs = []
val_loc_loss_epochs = []
val_regr_loss_epochs = []
val_objectness_loss_epochs = []
val_rpn_box_reg_loss_epochs = []
训练模型并计算训练和测试数据集的损失值:
for epoch in range(n_epochs):
_n = len(train_loader)
trn_loss = []
trn_loc_loss = []
trn_regr_loss = []
trn_objectness_loss = []
trn_rpn_box_reg_loss = []
val_loss = []
val_loc_loss = []
val_regr_loss = []
val_objectness_loss = []
val_rpn_box_reg_loss = []
for ix, inputs in enumerate(train_loader):
loss, losses = train_batch(inputs, model, optimizer)
loc_loss, regr_loss, loss_objectness, loss_rpn_box_reg = \
[losses[k] for k in ['loss_classifier','loss_box_reg','loss_objectness','loss_rpn_box_reg']]
pos = (epoch + (ix+1)/_n)
trn_loss.append(loss.item())
trn_loc_loss.append(loc_loss.item())
trn_regr_loss.append(regr_loss.item())
trn_objectness_loss.append(loss_objectness.item())
trn_rpn_box_reg_loss.append(loss_rpn_box_reg.item())
train_loss_epochs.append(np.average(trn_loss))
train_loc_loss_epochs.append(np.average(trn_loc_loss))
train_regr_loss_epochs.append(np.average(trn_regr_loss))
train_objectness_loss_epochs.append(np.average(trn_objectness_loss))
train_rpn_box_reg_loss_epochs.append(np.average(trn_rpn_box_reg_loss))
_n = len(test_loader)
for ix,inputs in enumerate(test_loader):
loss, losses = validate_batch(inputs, model, optimizer)
loc_loss, regr_loss, loss_objectness, loss_rpn_box_reg = \
[losses[k] for k in ['loss_classifier','loss_box_reg','loss_objectness','loss_rpn_box_reg']]
pos = (epoch + (ix+1)/_n)
val_loss.append(loss.item())
val_loc_loss.append(loc_loss.item())
val_regr_loss.append(regr_loss.item())
val_objectness_loss.append(loss_objectness.item())
val_rpn_box_reg_loss.append(loss_rpn_box_reg.item())
val_loss_epochs.append(np.average(val_loss))
val_loc_loss_epochs.append(np.average(val_loc_loss))
val_regr_loss_epochs.append(np.average(val_regr_loss))
val_objectness_loss_epochs.append(np.average(val_objectness_loss))
val_rpn_box_reg_loss_epochs.append(np.average(val_rpn_box_reg_loss))
(2) 绘制损失值随训练的变化情况:
epochs = np.arange(n_epochs)+1
plt.plot(epochs, train_loss_epochs, 'bo', label='Training loss')
plt.plot(epochs, val_loss_epochs, 'r', label='Test loss')
plt.title('Training and Test loss over increasing epochs')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.grid('off')
plt.show()
(3) 使用训练后的 Faster R-CNN
模型预测测试图像。
Faster R-CNN
目标检测模型的输出包含目标对象对应的边界框、类别标签和置信度分数。定义 decode_output 函数
,接受模型的输出并得到由应用非极大值抑制后的边界框、置信度分数和类别组成的列表:
from torchvision.ops import nms
def decode_output(output):
'convert tensors to numpy arrays'
bbs = output['boxes'].cpu().detach().numpy().astype(np.uint16)
labels = np.array([target2label[i] for i in output['labels'].cpu().detach().numpy()])
confs = output['scores'].cpu().detach().numpy()
ixs = nms(torch.tensor(bbs.astype(np.float32)), torch.tensor(confs), 0.05)
bbs, confs, labels = [tensor[ixs] for tensor in [bbs, confs, labels]]
if len(ixs) == 1:
bbs, confs, labels = [np.array([tensor]) for tensor in [bbs, confs, labels]]
return bbs.tolist(), confs.tolist(), labels.tolist()
# print(clss)
def show_bbs(im, bbs, clss):
fig, ax = plt.subplots(ncols=2, nrows=1, figsize=(6, 6))
ax[0].imshow(im)
ax[0].grid(False)
ax[0].set_title('Original image')
if len(bbs) == 0:
ax[1].imshow(im)
ax[1].set_title('No objects')
plt.show()
return
ax[1].imshow(im)
for ix, (xmin, ymin, xmax, ymax) in enumerate(bbs):
rect = mpatches.Rectangle(
(xmin, ymin), xmax-xmin, ymax-ymin,
fill=False,
edgecolor='red',
linewidth=1)
ax[1].add_patch(rect)
centerx = xmin # + new_w/2
centery = ymin + 20# + new_h - 10
plt.text(centerx, centery, clss[ix],fontsize = 20,color='red')
ax[1].grid(False)
ax[1].set_title('Predicted bounding box and class')
plt.show()
获取测试图像中目标对象的边界框和类别:
model.eval()
for ix, (images, targets) in enumerate(test_loader):
if ix==20:
break
images = [im for im in images]
outputs = model(images)
for ix, output in enumerate(outputs):
bbs, confs, labels = decode_output(output)
info = [f'{l}@{c:.2f}' for l,c in zip(labels, confs)]
show_bbs(images[ix].cpu().permute(1,2,0), bbs=bbs, clss=labels)
小结
Faster R-CNN 的核心组件是区域提议网络 (Region Proposal Network
, RPN
) 和共享卷积特征。首先,通过卷积神经网络,将输入图像提取为特征图,然后,RPN
在特征图上滑动窗口,并为每个窗口位置生成多个候选框,RPN
利用锚框 (anchor boxes
) 与真实目标框的匹配程度,为每个候选框分配得分,并预测边界框的偏移量。在候选框生成后,Faster R-CNN
通过 RoI
池化层从共享的特征图上提取固定尺寸的特征表示,这些特征表示被输入到分类器和边界框回归器中,进行目标分类和边界框位置的精修。Faster R-CNN
的优势在于,可以同时优化候选框生成和目标分类任务,提高了检测的准确性和效率。在本节中,我们使用 PyTorch
中提供的 fasterrcnn_resnet50_fpn
模型类训练了 Faster R-CNN
模型。
系列链接
PyTorch深度学习实战(1)——神经网络与模型训练过程详解
PyTorch深度学习实战(2)——PyTorch基础
PyTorch深度学习实战(3)——使用PyTorch构建神经网络
PyTorch深度学习实战(4)——常用激活函数和损失函数详解
PyTorch深度学习实战(5)——计算机视觉基础
PyTorch深度学习实战(6)——神经网络性能优化技术
PyTorch深度学习实战(7)——批大小对神经网络训练的影响
PyTorch深度学习实战(8)——批归一化
PyTorch深度学习实战(9)——学习率优化
PyTorch深度学习实战(10)——过拟合及其解决方法
PyTorch深度学习实战(11)——卷积神经网络
PyTorch深度学习实战(12)——数据增强
PyTorch深度学习实战(13)——可视化神经网络中间层输出
PyTorch深度学习实战(14)——类激活图
PyTorch深度学习实战(15)——迁移学习
PyTorch深度学习实战(16)——面部关键点检测
PyTorch深度学习实战(17)——多任务学习
PyTorch深度学习实战(18)——目标检测基础
PyTorch深度学习实战(19)——从零开始实现R-CNN目标检测
PyTorch深度学习实战(20)——从零开始实现Fast R-CNN目标检测