大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
个人主页-Sonhhxg_柒的博客_CSDN博客
欢迎各位→点赞 + 收藏⭐️ + 留言
系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟
文章目录
现代物体检测算法的组成部分
锚箱(Anchor boxes)
区域提案网络
分类和回归
在自定义数据集上训练 Faster R-CNN
YOLO的工作细节
在自定义数据集上训练 YOLO
安装Darknet
设置数据集格式
配置架构
训练和测试模型
SSD的工作细节
SSD 代码中的组件
固态硬盘300
多框损失
在自定义数据集上训练 SSD
概括
测试你的理解
在上一章中,我们了解了 R-CNN 和 Fast R-CNN 技术,它们利用区域提议来生成图像中对象位置的预测以及与图像中对象对应的类。此外,我们了解了推理速度的瓶颈,这是因为有两种不同的模型——一个用于区域建议生成,另一个用于对象检测。在本章中,我们将学习不同的现代技术,例如 Faster R-CNN、YOLO 和Single-Shot Detector ( SSD),通过使用单个模型在单次镜头中对对象类别和边界框进行预测,从而克服了缓慢的推理时间。我们将从了解锚框开始,然后继续了解每种技术的工作原理以及如何实现它们以检测图像中的对象。
我们将在本章中介绍以下主题:
R-CNN 和 Fast R-CNN 技术的缺点是它们有两个不相交的网络——一个用于识别可能包含对象的区域,另一个用于对识别对象的边界框进行校正。此外,这两种模型都需要与区域提议一样多的前向传播。现代目标检测算法主要集中于训练单个神经网络,并且能够在一次前向传播中检测所有目标。在接下来的部分中,我们将了解典型现代对象检测算法的各个组成部分:
到目前为止,我们已经收到了来自该selectivesearch方法的区域建议。锚框是选择性搜索的便捷替代品——我们将在本节中了解它们如何替代selectivesearch基于区域的建议。
通常,大多数对象具有相似的形状——例如,在大多数情况下,与人的图像相对应的边界框的高度大于宽度,而与卡车图像相对应的边界框将宽度大于高度。因此,即使在训练模型之前(通过检查与各种类别的对象相对应的边界框的基本事实),我们也会对图像中存在的对象的高度和宽度有一个不错的了解。
此外,在某些图像中,感兴趣的对象可能会被缩放——导致高度和宽度比平均值小得多或大得多——同时仍保持纵横比(即)。
一旦我们对图像中存在的对象的纵横比和高度和宽度(可以从数据集中的真实值中获得)有了一个不错的了解,我们就可以定义具有代表大多数对象的高度和宽度的锚框。我们数据集中的边界框。
通常,这是通过在图像中存在的对象的地面实况边界框之上采用 K-means 聚类来获得的。
现在我们了解了如何获得锚框的高度和宽度,我们将了解如何在此过程中利用它们:
1.将每个锚框从左上角滑到右下角的图像上。
2.与对象的并集(IoU )有高交集的锚框将有一个标签,指出它包含一个对象,其他的将被标记为 0:
一旦我们获得了此处定义的基本事实,我们就可以构建一个模型,该模型可以预测对象的位置以及与锚框相对应的偏移量,以将其与基本事实相匹配。现在让我们了解下图中锚框是如何表示的:
在上图中,我们有两个锚框,一个高度大于宽度,另一个宽度大于高度,以对应图像中的对象(类)——人和汽车。
我们在图像上滑动两个锚框,并注意锚框与 ground truth 的 IoU 最高的位置,并表示该特定位置包含对象,而其余位置不包含对象。
除了前面的两个锚框之外,我们还将创建具有不同比例的锚框,以便我们适应可以在图像中呈现对象的不同比例。不同比例的锚框看起来如何的示例如下:
请注意,所有锚框都具有相同的中心,但纵横比或比例不同。
现在我们了解了锚框,在下一节中,我们将了解 RPN,它利用锚框来预测可能包含对象的区域。
想象一下我们有一个 224 x 224 x 3 图像的场景。此外,假设此示例中的锚盒的形状为 8 x 8。如果我们的步幅为 8 像素,我们将每行获取 224/8 = 28 个图片裁剪——本质上是 28*28 = 576 个图片裁剪。然后,我们获取这些作物中的每一个,并通过一个区域建议网络模型 (RPN),该模型指示作物是否包含图像。本质上,RPN表明作物包含对象的可能性。
让我们比较一下selectivesearchRPN 的输出和输出。
selectivesearch基于像素值之上的一组计算给出一个候选区域. 然而,RPN 根据锚框和锚框在图像上滑动的步幅生成候选区域。一旦我们使用这两种方法中的任何一种获得候选区域,我们就会确定最有可能包含对象的候选区域。
虽然基于区域建议的生成selectivesearch是在神经网络之外完成的,但我们可以构建一个RPN,它是对象检测网络的一部分。使用 RPN,我们现在不需要执行不必要的计算来计算网络之外的区域建议。这样,我们就有了一个单一的模型来识别区域,识别图像中的对象类别,并识别它们对应的边界框位置。
接下来,我们将学习 RPN 如何识别候选区域(滑动锚框后获得的裁剪)是否包含对象。在我们的训练数据中,我们将让基本事实对应于对象。我们现在获取每个候选区域,并与图像中对象的地面实况边界框进行比较,以确定候选区域与地面实况边界框之间的 IoU 是否大于某个阈值。如果 IoU 大于某个阈值(例如 0.5),则候选区域包含一个对象,如果 IoU 小于某个阈值(例如 0.1),则候选区域不包含对象,并且所有候选区域都有训练时忽略两个阈值 (0.1 - 0.5) 之间的 IoU。
一旦我们训练了一个模型来预测候选区域是否包含一个对象,我们就会执行非最大抑制,因为多个重叠区域可以包含一个对象。
总而言之,RPN 训练模型以使其能够通过执行以下步骤来识别极有可能包含对象的区域建议:
到目前为止,我们已经了解了以下步骤来识别对象并对边界框执行偏移:
这些步骤的两个问题如下:
我们在本节中解决这两个问题,我们将先前获得的形状一致的特征图传递给网络。我们希望网络能够预测区域内包含的对象的类别以及与该区域相对应的偏移量,以确保边界框尽可能紧密地围绕图像中的对象。
让我们通过下图来理解这一点:
在上图中,我们将 RoI 池的输出作为输入(7 x 7 x 512 形状),将其展平,并在预测两个不同方面之前连接到密集层:
因此,如果数据中有 20 个类,则神经网络的输出总共包含 25 个输出——21 个类(包括背景类)以及要应用于高度、宽度和两个中心坐标的 4 个偏移量边界框。
现在我们已经了解了对象检测管道的不同组件,让我们用下图来总结一下:
有了 Faster R-CNN 的每个组件的工作细节,在下一节中,我们将使用 Faster R-CNN 算法编写目标检测代码。
在下面的代码中,我们将训练 Faster R-CNN 算法来检测图像中存在的对象周围的边界框。为此,我们将进行与上一章相同的卡车与公共汽车检测练习:
1.下载数据集:
import os
if not os.path.exists('images'):
!pip install -qU torch_snippets
from google.colab import files
files.upload() # upload kaggle.json
!mkdir -p ~/.kaggle
!mv kaggle.json ~/.kaggle/
!ls ~/.kaggle
!chmod 600 /root/.kaggle/kaggle.json
!kaggle datasets download \
-d sixhky/open-images-bus-trucks/
!unzip -qq open-images-bus-trucks.zip
!rm open-images-bus-trucks.zip
2.读取包含有关图像及其边界框和类的信息元数据的 DataFrame:
from torch_snippets import *
from PIL import Image
IMAGE_ROOT = 'images/images'
DF_RAW = df = pd.read_csv('df.csv')
3.定义对应于标签和目标的索引:
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)
4.定义预处理图像的函数 - preprocess_image:
def preprocess_image(img):
img = torch.tensor(img).permute(2,0,1)
return img.to(device).float()
5.定义数据集类 - OpenDataset:
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.glob(self.image_dir+'/*')
self.df = df
self.image_infos = df.ImageID.unique()
def __getitem__(self, ix):
# 加载图像和遮罩
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).
# 张量字典
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
def collate_fn(self, batch):
return tuple(zip(*batch))
def __len__(self):
return len(self.image_infos)
6.创建训练和验证数据加载器和数据集:
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)]
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)
7.定义模型:
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
该模型包含以下关键子模块:
我们注意到以下几点:
8.定义函数来训练批量数据并计算验证数据的损失值:
# 定义训练和验证函数
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( ) loss
= model(input, targets)
loss = sum(loss for loss in loss.values())
loss.backward ()
optimizer.step()
return loss, loss
@torch.no_grad()
def validate_batch(inputs, model):
model.train()
#获取loss,模型只需要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
9.在越来越多的时期训练模型:
model = get_model().to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=0.005, \
momentum=0.9,weight_decay=0.0005)
n_epochs = 5
log = Report(n_epochs)
for epoch in range(n_epochs):
_n = len(train_loader)
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)
log.record(pos, trn_loss=loss.item(), \
trn_loc_loss=loc_loss.item(), \
trn_regr_loss=regr_loss.item(), \
trn_objectness_loss=loss_objectness.item(), \
trn_rpn_box_reg_loss=loss_rpn_box_reg.item(), \
end='\r')
_n = len(test_loader)
for ix,inputs in enumerate(test_loader):
loss, losses = validate_batch(inputs, model)
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)
log.record(pos, val_loss=loss.item(), \
val_loc_loss=loc_loss.item(), \
val_regr_loss=regr_loss.item(), \
val_objectness_loss=loss_objectness.item(), \
val_rpn_box_reg_loss=loss_rpn_box_reg.item(), \
end='\r')
if (epoch+1)%(n_epochs//5)==0: log.report_avgs(epoch+1)
10.绘制不同损失值在不断增加的时期内的变化:
11.预测新图像:
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()
model.eval()
for ix, (images, targets) in enumerate(test_loader):
if ix==3: 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(images[ix].cpu(). permute(1,2,0), bbs=bbs, \
texts=labels, sz=5)
上述代码提供以下输出:
在本节中,我们使用fasterrcnn_resnet50_fpnPyTorchmodels包中提供的模型类训练了一个 Faster R-CNN 模型。在下一节中,我们将了解 YOLO,这是一种现代对象检测算法,它可以在一次拍摄中执行对象类别检测和区域校正,而无需单独的RPN。
You Only Look Once ( YOLO ) 及其变体是著名的对象检测算法之一。在本节中,我们将深入了解 YOLO 的工作原理以及 YOLO 克服的基于 R-CNN 的对象检测框架的潜在局限性。
首先,让我们了解基于 R-CNN 的检测算法可能存在的局限性。在 Faster R-CNN 中,我们使用锚框在图像上滑动并识别可能包含对象的区域,然后进行边界框校正。然而,在全连接层中,只有检测到的区域的 RoI 池化输出作为输入传递,在区域没有完全包围对象的情况下(对象超出区域提议的边界框的边界),网络必须猜测对象的真实边界,因为它没有看到完整的图像(但只看到了区域提议)。
YOLO 在这种情况下会派上用场,因为它会查看整个图像,同时预测与图像对应的边界框。
此外,Faster R-CNN 仍然很慢,因为我们有两个网络:RPN 和预测对象周围的类别和边界框的最终网络。
在这里,我们将了解 YOLO 如何克服 Faster R-CNN 的局限性,既可以一次查看整个图像,也可以使用单个网络进行预测。我们将通过以下示例了解如何为 YOLO 准备数据:
1.创建一个基本事实来训练给定图像的模型:
这里,pc(objectness score)是单元格包含对象的概率。
让我们了解如何计算bx、by、bw和bh。
首先,我们将网格单元(让我们考虑b1网格单元)视为我们的宇宙,并将其归一化为 0 到 1 之间的尺度,如下所示:
bx和by是地面实况边界框的中点相对于图像(网格单元)的位置,如前所述。在我们的例子中,bx = 0.5,因为地面实况的中点距离原点 0.5 个单位。同样,by = 0.5:
到目前为止,我们已经计算了从网格单元中心到图像中对象对应的地面实况中心的偏移量。现在,让我们了解如何计算bw和bh。
bw是边界框的宽度与网格单元格宽度的比率。
bh是边界框高度与网格单元高度的比值。
接下来,我们将预测与网格单元对应的类。如果我们有三个类别(c1 – 卡车、c2 – 汽车、c3 – 公共汽车),我们将预测单元格在这三个类别中的任何一个中包含对象的概率。注意这里我们不需要背景类,因为pc对应的是网格单元格是否包含对象。
现在我们了解了如何表示每个单元格的输出层,让我们了解如何构建 3 x 3 网格单元格的输出。
单元格a3的输出如前面的屏幕截图所示。由于网格单元不包含对象,因此第一个输出(pc – objectness score)为 0,其余值无关紧要,因为该单元不包含对象的任何地面实况边界框的中心。
前面的输出是这样的,因为网格单元包含一个具有bx、by、bw和bh值的对象,这些值是通过与我们之前经历的相同方式获得的(在最后的项目符号中),最后是类car导致 c2 为 1,而 c1 和 c3 为 0。
请注意,对于每个单元格,我们能够获取 8 个输出。因此,对于 3 x 3 的单元格网格,我们获取 3 x 3 x 8 的输出。
2.定义一个模型,其中输入为图像,输出为 3 x 3 x 8,ground truth 与上一步中定义的一样:
3.通过考虑锚框来定义基本事实。
到目前为止,我们一直在构建一个场景,即期望网格单元内只有一个对象。然而,在现实中,可能存在同一个网格单元内有多个对象的情况。这将导致创建不正确的基本事实。让我们通过以下示例图像来了解这种现象:
在前面的示例中,汽车和人的地面实况边界框的中点落在同一个单元格 - 单元格b1中。
避免这种情况的一种方法是使用具有更多行和列的网格——例如,19 x 19 网格。但是,仍然可能存在增加网格单元数量无济于事的情况。在这种情况下,锚盒就派上用场了。假设我们有两个锚框——一个高度大于宽度(对应于人),另一个宽度大于高度(对应于汽车):
通常,锚框将网格单元中心作为它们的中心。在我们有两个锚框的场景中,每个单元格的输出表示为两个锚框预期输出的串联:
这里,bx、by、bw和bh表示与锚框的偏移量(在此场景中,这是在图像中看到的宇宙,而不是网格单元)。
从前面的屏幕截图中,我们看到我们有一个 3 x 3 x 16 的输出,因为我们有两个锚点。预期输出的形状为N x N x ( num_classes + 1 ) x ( num_anchor_boxes),其中N x N是网格中的单元num_classes数,是数据集中的类数,并且num_anchor_boxes是锚框的数量。
4.现在我们定义损失函数来训练模型。
在计算与模型相关的损失时,我们需要确保在 objectness score 小于某个阈值(这对应于不包含对象的单元格)时不计算回归损失和分类损失。
接下来,如果单元格包含一个对象,我们需要确保跨不同类的分类尽可能准确。
最后,如果单元格包含对象,则边界框偏移量应尽可能接近预期。但是,由于宽度和高度的偏移量与中心的偏移量相比可能要高得多(因为中心的偏移量范围在 0 和 1 之间,而宽度和高度的偏移量则不需要),我们给予较低的权重通过获取平方根值来偏移宽度和高度。
计算定位和分类的损失如下:
在这里,我们观察到以下情况:
整体损失是分类和回归损失值的总和。
有了这个,我们现在可以训练一个模型来预测物体周围的边界框。但是,为了更深入地了解 YOLO 及其变体,我们鼓励您阅读原始论文。现在我们了解了 YOLO 如何一次性预测边界框和对象类别,我们将在下一节中对其进行编码。
建立在他人工作之上对于成为深度学习的成功实践者非常重要。对于这个实现,我们将使用官方的 YOLO-v4 实现来识别图像中公共汽车和卡车的位置。我们将克隆作者自己的 YOLO 实现的存储库,并在下面的代码中根据我们的需要对其进行自定义。
首先,从 GitHub 拉取darknet存储库并在环境中编译它。该模型是用一种名为 Darknet 的单独语言编写的,它与 PyTorch 不同。我们将使用以下代码执行此操作:
1.拉取 Git 存储库:
!git clone https://github.com/AlexeyAB/darknet
%cd darknet
2.重新配置Makefile文件:
!sed -i 's/OPENCV=0/OPENCV=1/' Makefile
# In case you dont have a GPU, make sure to comment out the
# below 3 lines
!sed -i 's/GPU=0/GPU=1/' Makefile
!sed -i 's/CUDNN=0/CUDNN=1/' Makefile
!sed -i 's/CUDNN_HALF=0/CUDNN_HALF=1/' Makefile
Makefile是在环境中安装所需的配置文件darknet(将此过程视为类似于您在 Windows 上安装软件时所做的选择)。我们强制darknet安装以下标志:OPENCV、GPU、CUDNN和CUDNN_HALF. 这些都是使训练更快的重要优化。
此外,在前面的代码中,有一个奇怪的函数叫做sed,它代表流编辑器。它是一个强大的 Linux 命令,可以直接从命令提示符修改文本文件中的信息。具体来说,这里我们使用它的搜索和替换功能来替换OPENCV=0,OPENCV=1等等。这里要理解的语法是sed 's/
3.编译darknet源代码:
!make
4.安装torch_snippets包:
!pip install -q torch_snippets
5.下载并提取数据集,并删除 ZIP 文件以节省空间:
!wget --quiet \
https://www.dropbox.com/s/agmzwk95v96ihic/open-images-bus-trucks.tar.xz
!tar -xf open-images-bus-trucks.tar.xz
!rm open-images-bus-trucks.tar.xz
6.获取预训练的权重以进行样本预测:
!wget --quiet\ https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights
7.运行以下命令测试是否安装成功:
!./darknet detector test cfg/coco.data cfg/yolov4.cfg\ yolov4.weights
data/person.jpg
这将预测使用由 预训练权重data/person.jpg 构建的网络- 。此外,它从 中获取类,这是预训练权重的训练对象。cfg/yolov4.cfg yolov4.weights cfg/coco.data
上述代码对样本图像 ( data/person.jpg) 的预测结果如下:
现在我们已经了解了如何安装darknet,在下一节中,我们将学习如何为我们的自定义数据集创建基本事实以利用darknet.
YOLO 使用固定格式进行训练。一旦我们以所需格式存储图像和标签,我们就可以使用单个命令在数据集上进行训练。那么,让我们了解一下 YOLO 训练所需的文件和文件夹结构。
有三个重要步骤:
1.通过运行以下行创建一个包含类名称的文本文件,data/obj.names每行一个类(%%writefile这是一个神奇的命令,它创建一个文本文件,data/obj.names其中包含笔记本单元格中存在的任何内容):
%%writefile data/obj.names
bus
truck
2.data/obj.data在描述数据集中的参数和包含训练和测试图像路径的文本文件的位置以及包含对象名称的文件的位置和要保存训练模型的文件夹处创建一个文本文件:
%%writefile data/obj.data
classes = 2
train = data/train.txt
valid = data/val.txt
names = data/obj.names
backup = backup/
3.将所有图像和地面实况文本文件移动到该data/obj文件夹。我们将把bus-trucks数据集中的图像连同标签一起复制到这个文件夹中:
!mkdir -p data/obj
!cp -r open-images-bus-trucks/images/* data/obj/
!cp -r open-images-bus-trucks/yolo_labels/all/\
{train,val}.txt data/
!cp -r open-images-bus-trucks/yolo_labels/all/\
labels/*.txt data/obj/
请注意,所有训练和验证图像都在同一个data/obj文件夹中。我们还将一堆文本文件移动到同一个文件夹。每个包含图像基本事实的文件都与图像共享相同的名称。例如,文件夹可能包含1001.jpgand 1001.txt,这意味着文本文件包含该图像的标签和边界框。如果data/train.txt包含1001.jpg作为其中一条线,则它是训练图像。如果它存在于 中val.txt,则它是一个验证图像。
文本文件本身应包含如下信息:cls, xc, yc, w, h,,其中是边界框中对象的类索引,在该处cls表示宽度和高度矩形的质心。、、和中的每一个都是图像宽度和高度的一部分。将每个对象存储在单独的行上。(xc, yc)whxcycwh
例如,如果宽度为 800 和高度为 600 的图像分别在中心 (500,300) 和 (100,400) 包含一辆卡车和一辆公共汽车,并且宽度和高度分别为 (200,100) 和 (300,50),那么文本文件将如下所示:
1 0.62 0.50 0.25 0.12
0 0.12 0.67 0.38 0.08
现在我们已经创建了数据,让我们在下一节中配置网络架构。
YOLO 附带一长串架构。有些很大,有些很小,用于在大型或小型数据集上进行训练。配置可以有不同的主干。标准数据集有预训练的配置。每个配置都是我们克隆的同一个 GitHub 存储库的文件夹中的一个.cfg文件。cfgs它们中的每一个都包含作为文本文件的网络架构(与我们使用nn.Module类构建它的方式相反)以及一些超参数,例如批量大小和学习率。我们将采用最小的可用架构并为我们的数据集配置它:
# 创建现有配置的副本并就地修改
!cp cfg/yolov4-tiny-custom.cfg cfg/\
yolov4-tiny-bus-trucks.cfg
# max_batches 为 4000(因为数据集足够小)
!sed - i 's/max_batches = 500200/max_batches=4000/' \
cfg/yolov4-tiny-bus-trucks.cfg
# 每批次的子批次数
!sed -i 's/subdivisions=1/subdivisions=16/' \
cfg/yolov4-tiny-bus-trucks.cfg
# 学习率衰减后的批次数
!sed -i 's/steps=400000,450000/steps=3200,3600/' \
cfg/yolov4-tiny-bus- Trucks.cfg
# 类数是 2 而不是 80
#(这是 COCO 类的数量)
!sed -i 's/classes=80/classes=2/g' \
cfg/yolov4-tiny-bus-trucks.cfg
# 在分类和回归头中,
# 改变输出卷积过滤器的数量
# 从 255 -> 21 和 57 -> 33,因为我们有更少的类
# 我们不需要许多过滤器
!sed -i 's/filters=255/filters=21/g' \
cfg/yolov4-tiny-bus-trucks.cfg
!sed -i 's/filters=57/filters=33/g' \
cfg /yolov4-tiny-bus-trucks.cfg
通过这种方式,我们重新调整yolov4-tiny了目标,以便在我们的数据集上进行训练。剩下的唯一步骤是加载预训练的权重并训练模型,我们将在下一节中进行。
我们将从以下 GitHub 位置获取权重并将它们存储在build/darknet/x64:
!wget --quiet \ https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v4_pre/yolov4-tiny.conv.29
!cp yolov4-tiny.conv.29 build/darknet/x64/
最后,我们将使用以下代码来训练模型:
!./darknet detector train data/obj.data \
cfg/yolov4-tiny-bus-trucks.cfg yolov4-tiny.conv.29 \
-dont_show -mapLastAt
该-dont_show标志跳过显示中间预测图像,-mapLastAt并将定期在验证数据上打印平均精度。整个培训可能需要 1 或 2 个小时。权重会定期存储在备份文件夹中,并且可以在训练后用于预测,例如以下代码,该代码对新图像进行预测:
!pip install torch_snippets
from torch_snippets import Glob, stem, show, read
# upload your own images to a folder
image_paths = Glob('images-of-trucks-and-busses')
for f in image_paths:
!./darknet detector test \
data/obj.data cfg/yolov4-tiny-bus-trucks.cfg\
backup/yolov4-tiny-bus-trucks_4000.weights {f}
!mv predictions.jpg {stem(f)}_pred.jpg
for i in Glob('*_pred.jpg'):
show(read(i, 1), sz=20)
前面的代码导致:
现在我们已经了解了如何利用 YOLO 在我们的自定义数据集上执行对象检测,在下一节中,我们将了解如何利用 SSD 执行对象检测。
到目前为止,我们已经看到了一个场景,我们在逐渐卷积和汇集前一层的输出后进行预测。但是,我们知道不同的层对原始图像有不同的感受野。例如,与具有较大感受野的最终层相比,初始层具有较小的感受野。在这里,我们将了解 SSD 如何利用这种现象来预测图像的边界框。
SSD 如何帮助克服检测不同尺度物体的问题背后的工作原理如下:
现在我们了解了 SSD 与 YOLO 的主要区别(即 SSD 中的 default box 替换了 YOLO 中的 anchor box 并且多层连接到 SSD 中的最后一层,而不是 YOLO 中的渐进卷积池),让我们了解一下以下:
SSD的网络架构如下:
正如您在上图中看到的,我们正在拍摄一张 300 x 300 x 3 大小的图像,并将其通过预训练的 VGG-16 网络以获得conv5_3层的输出。conv5_3此外,我们通过在输出中添加更多卷积来扩展网络。
接下来,我们获得每个单元格和每个默认框的边界框偏移和类别预测(下一节将详细介绍默认框;现在,让我们假设这类似于锚框)。来自conv5_3输出的预测总数为 38 x 38 x 4,其中 38 x 38 是层的输出形状,conv5_34 是在层上运行的默认框的数量conv5_3。同理,全网的参数总数如下:
层 | 参数数量 |
conv5_3 | 38 X 38 X 4 = 5,776 |
FC6 | 19 X 19 X 6 = 2,166 |
conv8_2 | 10 X 10 X 6 = 600 |
conv9_2 | 5 X 5 X 6 = 150 |
conv10_2 | 3 X 3 X 4 = 36 |
conv11_2 | 1 X 1 X 4 = 4 |
总参数 | 8732 |
请注意,与原始论文中描述的架构中的其他层相比,某些层具有更多数量的框(6 个而不是 4 个)。
现在,让我们了解一下默认框的不同比例和纵横比。我们将从比例开始,然后进行纵横比。
让我们想象一个场景,对象的最小比例是图像高度的 20% 和宽度的 20%,对象的最大比例是高度的 90% 和宽度的 90%。在这种情况下,我们逐渐增加跨层的规模(随着我们向后面的层进行,图像大小会大大缩小),如下所示:
启用图像逐渐缩放的公式如下:
现在我们了解了如何计算跨层的比例,现在我们将了解如何提出不同纵横比的框。
可能的纵横比如下:
不同层的框的中心如下:
这里i和j一起代表层l中的一个单元。
不同纵横比对应的宽高计算如下:
请注意,我们正在考虑某些层中的四个框和另一层中的六个框。现在,如果我们想要四个框,我们删除 {3,1/3} 纵横比,否则我们考虑所有六个可能的框(五个具有相同比例的框和一个具有不同比例的框)。那么,让我们学习如何获得第六个盒子:
现在我们有了所有可能的框,让我们了解如何准备训练数据集。
IoU 大于阈值(例如 0.5)的默认框被视为正匹配,其余为负匹配。
在 SSD 的输出中,我们预测框属于一个类(其中第 0个类表示背景)的概率,以及 ground truth 相对于默认框的偏移量。
最后,我们通过优化以下损失值来训练模型:
在前面的等式中,pos表示与基本事实高度重叠的少数默认框,而neg表示预测类但实际上不包含对象的错误分类框。最后,我们确保pos:neg比率最多为 1:3,就好像我们不执行此采样一样,我们将拥有背景类框的优势。
这里t是预测的偏移量,d是实际的偏移量。
现在我们了解了如何训练 SSD,让我们在下一节中将它用于我们的公共汽车与卡车对象检测练习。
本节的核心实用程序功能位于 GitHub 存储库中:https ://github.com/sizhky/ssd-utils/ 。在开始培训过程之前,让我们一一了解它们。
GitHub 存储库中有三个文件。让我们在训练之前深入了解它们并了解它们。请注意,此部分不是训练过程的一部分,而是用于了解训练期间使用的导入。
我们正在从SSD300MultiBoxLossGitHub 存储库中的文件中导入和类。model.py让我们了解一下它们。
当您查看SSD300函数定义时,很明显该模型包含三个子模块:
class SSD300(nn.Module):
...
def __init__(self, n_classes, device):
...
self.base = VGGBase()
self.aux_convs = AuxiliaryConvolutions()
self.pred_convs = PredictionConvolutions(n_classes)
...
我们将输入发送到first,它返回两个维度为和的特征向量。第二个输出将作为 的输入,它返回更多维度为 、、和的特征图。最后,这四个特征图的第一个输出被发送到,它返回我们之前讨论的 8,732 个锚框。 VGGBase(N, 512, 38, 38)(N, 1024, 19, 19)AuxiliaryConvolutions(N, 512, 10, 10)(N, 256, 5, 5)(N, 256, 3, 3)(N, 256, 1, 1)VGGBasePredictionConvolutions
SSD300该类的另一个关键方面是create_prior_boxes方法。对于每个特征图,都有三个与之相关的项目:网格的大小、网格单元的缩小比例(这是此特征图的基本锚框)以及单元中所有锚的纵横比. 使用这三种配置,代码使用三重循环并为所有 8,732 个锚框for创建一个列表。(cx, cy, w, h)
最后,该detect_objects方法获取分类和回归值的张量(预测锚框的)并将它们转换为实际的边界框坐标。
作为人类,我们只担心少数几个边界框。但是对于 SSD 的工作方式,我们需要比较来自几个特征图的 8,732 个边界框,并预测一个锚框是否包含有价值的信息。我们将此损失计算任务分配给。 MultiBoxLoss
前向方法的输入是来自模型的锚框预测和地面实况边界框。
首先,我们通过将模型中的每个锚点与边界框进行比较,将地面实况框转换为 8,732 个锚框的列表。如果 IoU 足够高,则该特定锚框将具有非零回归坐标,并将对象关联为分类的基本事实。自然,大多数计算出的锚框都会有它们的关联类,background因为它们与实际边界框的 IoU 很小,或者在很多情况下为零。
一旦将基本事实转换为这 8,732 个锚框回归和分类张量,就很容易将它们与模型的预测进行比较,因为现在形状相同。
我们MSE-Loss对回归张量和CrossEntropy-Loss定位张量执行并将它们相加以作为最终损失返回。
在下面的代码中,我们将训练 SSD 算法来检测图像中存在的对象周围的边界框。我们将使用我们一直在研究的卡车与公共汽车对象检测任务:
1.下载图像数据集并克隆托管模型代码和其他用于处理数据的实用程序的 Git 存储库:
import os
if not os.path.exists('open-images-bus-trucks'):
!pip install -q torch_snippets
!wget --quiet https://www.dropbox.com/s/agmzwk95v96ihic/\
open-images-bus-trucks.tar.xz
!tar -xf open-images-bus-trucks.tar.xz
!rm open-images-bus-trucks.tar.xz
!git clone https://github.com/sizhky/ssd-utils/
%cd ssd-utils
2.预处理数据,就像我们在自定义数据集部分训练Faster R-CNN 中所做的那样:
from torch_snippets import *
DATA_ROOT = '../open-images-bus-trucks/'
IMAGE_ROOT = f'{DATA_ROOT}/images'
DF_RAW = pd.read_csv(f'{DATA_ROOT}/df.csv')
df = DF_RAW.copy()
df = df[df['ImageID'].isin(df['ImageID'].unique().tolist())]
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)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
3.准备一个数据集类,就像我们在自定义数据集部分训练 Faster R-CNN 中所做的那样:
import collections, os, torch
from PIL import Image
from torchvision import transforms
normalize = transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
denormalize = transforms.Normalize(
mean=[-0.485/0.229,-0.456/0.224,-0.406/0.255],
std=[1/0.229, 1/0.224, 1/0.255]
)
def preprocess_image(img):
img = torch.tensor(img).permute(2,0,1)
img = normalize(img)
return img.to(device).float()
class OpenDataset(torch.utils.data.Dataset):
w, h = 300, 300
def __init__(self, df, image_dir=IMAGE_ROOT):
self.image_dir = image_dir
self.files = glob.glob(self.image_dir+'/*')
self.df = df
self.image_infos = df.ImageID.unique()
logger.info(f'{len(self)} items loaded')
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
return img, boxes, labels
def collate_fn(self, batch):
images, boxes, labels = [], [], []
for item in batch:
img, image_boxes, image_labels = item
img = preprocess_image(img)[None]
images.append(img)
boxes.append(torch.tensor( \
image_boxes).float().to(device)/300.)
labels.append(torch.tensor([label2target[c] \
for c in image_labels]).long().to(device))
images = torch.cat(images).to(device)
return images, boxes, labels
def __len__(self):
return len(self.image_infos)
4.准备训练和测试数据集以及数据加载器:
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)]
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)
5.定义函数来训练一批数据并计算验证数据的准确度和损失值:
def train_batch(inputs, model, criterion, optimizer):
model.train()
N = len(train_loader)
images, boxes, labels = inputs
_regr, _clss = model(images)
loss = criterion(_regr, _clss, boxes, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss
@torch.no_grad()
def validate_batch(inputs, model, criterion):
model.eval()
images, boxes, labels = inputs
_regr, _clss = model(images)
loss = criterion(_regr, _clss, boxes, labels)
return loss
6.导入模型:
from model import SSD300, MultiBoxLoss
from detect import *
7.初始化模型、优化器和损失函数:
n_epochs = 5
model = SSD300(num_classes, device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, \
weight_decay=1e-5)
criterion = MultiBoxLoss(priors_cxcy=model.priors_cxcy, \
device=device)
log = Report(n_epochs=n_epochs)
logs_to_print = 5
8.在越来越多的时期训练模型:
for epoch in range(n_epochs):
_n = len(train_loader)
for ix, inputs in enumerate(train_loader):
loss = train_batch(inputs, model, criterion, \
optimizer)
pos = (epoch + (ix+1)/_n)
log.record(pos, trn_loss=loss.item(), end='\r')
_n = len(test_loader)
for ix,inputs in enumerate(test_loader):
loss = validate_batch(inputs, model, criterion)
pos = (epoch + (ix+1)/_n)
log.record(pos, val_loss=loss.item(), end='\r')
训练和测试损失值在 epoch 上的变化如下:
9.获取对新图像的预测:
image_paths = Glob(f'{DATA_ROOT}/images/*')
image_id = choose(test_ds.image_infos)
img_path = find(image_id, test_ds.files)
original_image = Image.open(img_path, mode='r')
original_image = original_image .convert('RGB')
bbs, labels, scores = detect(original_image, model, \
min_score=0.9, max_overlap=0.5,\
top_k=200, device=device)
labels = [target2label[c.item()] for c in labels]
label_with_conf = [f'{l} @ {s:.2f}' \
for l,s in zip(labels,scores)]
print(bbs, label_with_conf)
show(original_image, bbs=bbs, \
texts=label_with_conf, text_sz=10)
前面的代码按如下方式获取输出样本(每次执行迭代一个图像):
由此可见,我们可以合理准确地检测出图像中的物体。
在本章中,我们了解了现代目标检测算法的工作细节:Faster R-CNN、YOLO 和 SSD。我们了解了他们如何克服拥有两个独立模型的限制——一个用于获取区域建议,另一个用于获取区域建议上的类和边界框偏移量。darknet此外,我们从头开始使用 PyTorch、YOLO 和 SSD 实现了 Faster R-CNN 。
在下一章中,我们将学习图像分割,它通过识别与对象对应的像素来超越对象定位。
此外,在第 15 章,结合计算机视觉和 NLP 技术中,我们将学习 DETR,一种基于变换器的目标检测算法,在第 10 章,目标检测和分割的应用中,我们将学习 Detectron2 框架,它有助于不仅可以检测物体,还可以在一次拍摄中对其进行分割。