目标检测:单发多框检测(SSD)——(mxnet学习笔记)

前期准备

1、锚框的理解

目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边缘从而更准确地预测目标的真实边界框(ground-truth bounding box)。以下代码均来自开源mxnet学习模型,本文详细注释作为笔记。

%matplotlib inline
import d2lzh as d2l
from mxnet import contrib, gluon, image, nd
import numpy as np
np.set_printoptions(2)
  1. %matplotlib inline:这是一个魔术命令,用于Jupyter Notebook环境中,使得matplotlib生成的图像可以直接显示在代码单元格下面,而不是新开一个窗口。

  2. import d2lzh as d2l:导入名为d2lzh的模块,并将其重命名为d2l

  3. from mxnet import contrib, gluon, image, nd:从mxnet库中导入contribgluonimagend四个模块。mxnet是一个开源的深度学习框架,contrib包含一些额外的功能,gluon是用于构建神经网络的高级API,image提供了图像处理的功能,ndndarray的缩写,用于高效的多维数组操作。

  4. import numpy as np:导入numpy库,并将其重命名为npnumpy是Python中用于科学计算的基础包,提供了大量的数学函数库,对数组的支持让处理大型矩阵变得方便。

  5. np.set_printoptions(2):设置numpy的打印选项,使得打印出的数组每个元素保留两位小数。这对于控制输出的格式非常有用,尤其是在调试或者展示结果时。

img = image.imread('G:\d2lzh/img/catdog.jpg').asnumpy()
h, w = img.shape[0:2]

print(h, w)
X = nd.random.uniform(shape=(1, 3, h, w))  # 构造输入数据
Y = contrib.nd.MultiBoxPrior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape

boxes = Y.reshape((h, w, 5, 4))
boxes[250, 250, 0, :]
  1. img = image.imread('../img/catdog.jpg').asnumpy():使用MXNet的image模块中的imread函数读取位于../img/catdog.jpg路径的图像文件,并将其转换为NumPy数组。这个数组将包含图像的像素值。

  2. h, w = img.shape[0:2]:从图像数组img中获取高度(h)和宽度(w)。img.shape返回一个包含数组维度的元组,[0:2]表示取前两个维度,即高度和宽度。

  3. print(h, w):打印出图像的高度和宽度。

  4. X = nd.random.uniform(shape=(1, 3, h, w)):使用MXNet的nd模块生成一个随机数数组X,形状为(1, 3, h, w)。这里1表示批量大小(batch size)为1,3表示颜色通道数(RGB),hw分别是图像的高度和宽度。这个数组模拟了一个批量中的一个图像的输入数据。

  5. Y = contrib.nd.MultiBoxPrior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5]):使用MXNet的contrib.nd模块中的MultiBoxPrior函数生成多尺度锚框(MultiBox Prior)。这个函数接受输入数据X和锚框的尺寸(sizes)以及比例(ratios)。sizes参数定义了锚框的面积相对于基础锚框的面积的比例,而ratios参数定义了锚框的宽高比。这里定义了三组不同的尺寸和比例,用于生成不同大小和形状的锚框。

  6. Y.shape:获取生成的多尺度锚框Y的形状。这个形状将告诉我们生成的锚框的数量和维度。由于Y是一个四维数组,其形状将包含批量大小、锚框数量、锚框的维度(通常是4,表示x1, y1, x2, y2即图像中边框里左上角右下角的xy坐标,是除h、w后的结果)。

  7. boxes将锚框变量y的形状变为(图像高,图像宽,以相同像素为中心的锚框个数,4)

# 用于在图像上显示边界框(bounding boxes)的函数
def show_bboxes(axes, bboxes, labels=None, colors=None):
    def _make_list(obj, default_values=None):
        if obj is None:
            obj = default_values
        elif not isinstance(obj, (list, tuple)):
            obj = [obj]
        return obj

    labels = _make_list(labels)
    colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
    for i, bbox in enumerate(bboxes):
        color = colors[i % len(colors)]
        rect = d2l.bbox_to_rect(bbox.asnumpy(), color)
        axes.add_patch(rect)
        if labels and len(labels) > i:
            text_color = 'k' if color == 'w' else 'w'
            axes.text(rect.xy[0], rect.xy[1], labels[i],
                      va='center', ha='center', fontsize=9, color=text_color,
                      bbox=dict(facecolor=color, lw=0))

这个函数的目的是将边界框绘制在图像上,并可选地添加标签。它通过循环遍历每个边界框,将其转换为矩形对象,并将其添加到图像的轴对象上。如果提供了标签和颜色,它还会在每个边界框旁边添加文本。

  1. def show_bboxes(axes, bboxes, labels=None, colors=None)::定义一个函数show_bboxes,它接受四个参数:axes(图像的轴对象),bboxes(边界框的坐标),labels(可选的边界框标签),colors(可选的边界框颜色)。

  2. def _make_list(obj, default_values=None)::定义一个内部函数_make_list,它将任何对象转换为列表。如果对象是None,则使用default_values作为默认值;如果对象不是列表或元组,则将其转换为包含该对象的列表。

  3. if obj is None::检查对象是否为None

  4. obj = default_values:如果是None,则将对象设置为默认值。

  5. elif not isinstance(obj, (list, tuple))::如果对象不是列表或元组。

  6. obj = [obj]:将对象转换为包含该对象的列表。

  7. return obj:返回转换后的列表。

  8. labels = _make_list(labels):使用_make_list函数确保labels是列表。

  9. colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c']):使用_make_list函数确保colors是列表,并提供一个默认颜色列表(蓝色、绿色、红色、洋红色、青色)。

  10. for i, bbox in enumerate(bboxes)::遍历边界框列表bboxes,并使用enumerate函数获取每个边界框的索引i和值bbox

  11. color = colors[i % len(colors)]:从颜色列表中循环选择颜色。

  12. rect = d2l.bbox_to_rect(bbox.asnumpy(), color):使用d2l包中的bbox_to_rect函数将边界框坐标转换为矩形对象,并指定颜色。

  13. axes.add_patch(rect):将矩形对象添加到图像的轴对象上。

  14. if labels and len(labels) > i::如果提供了标签列表,并且标签的数量大于当前的索引。

  15. text_color = 'k' if color == 'w' else 'w':如果矩形颜色是白色,则文本颜色设置为黑色,否则设置为白色。

  16. axes.text(rect.xy[0], rect.xy[1], labels[i],:在矩形的左上角位置添加文本,显示边界框的标签。

  17. va='center', ha='center', fontsize=9, color=text_color,:设置文本的垂直和水平对齐方式、字体大小和颜色。

  18. bbox=dict(facecolor=color, lw=0)):设置文本背景框的样式,包括颜色和线宽。

注意:_make_list 函数的设计允许你选择性地提供 default_values 参数:

如果你不提供 default_values 参数,那么当 objNone 时,函数将不会设置任何默认值,obj 将保持为 None。如果你提供了 default_values 参数,那么当 objNone 时,obj 将被设置为你提供的 default_values

d2l.set_figsize()
bbox_scale = nd.array((w, h, w, h))
fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
            ['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2',
             's=0.75, r=0.5'])

实际效果:

  1. bbox_scale = nd.array((w, h, w, h)):创建一个MXNet的ndarray对象,包含宽度w和高度h的值。这个数组将用于缩放边界框的坐标,使其适应图像的实际尺寸。

  2. fig = d2l.plt.imshow(img):使用d2l库中的imshow函数来显示图像img,并返回一个图像显示对象fig。这个对象包含了图像的显示信息,包括图像数据和轴对象。

  3. show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,:调用show_bboxes函数来在图像上显示边界框。这里fig.axes提供了图像的轴对象,boxes[250, 250, :, :]选择了边界框数组中的特定区域(可能是一个特定的特征图位置),并且每个边界框的坐标都乘以bbox_scale来进行缩放。

  4. ['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2', 's=0.75, r=0.5']:这是一个字符串列表,包含了每个边界框的标签。这些标签描述了每个边界框的尺寸(s)和比例(r)。

2、交并比的理解

如果该目标的真实边界框已知,这里的“较好”该如何量化呢?一种直观的方法是衡量锚框和真实边界框之间的相似度。Jaccard系数(Jaccard index)可以衡量两个集合的相似度。使用交并比来衡量锚框与真实边界框以及锚框与锚框之间的相似度。

3、标注训练集的锚框

我们将每个锚框视为一个训练样本。为了训练目标检测模型,我们需要为每个锚框标注两类标签:一是锚框所含目标的类别,简称类别;二是真实边界框相对锚框的偏移量,简称偏移量(offset)。在目标检测时,我们首先生成多个锚框,然后为每个锚框预测类别以及偏移量,接着根据预测的偏移量调整锚框位置从而得到预测边界框,最后筛选需要输出的预测边界框。

在目标检测的训练集中,每个图像已标注了真实边界框的位置以及所含目标的类别。在生成锚框之后,我们主要依据与锚框相似的真实边界框的位置和类别信息为锚框标注。

开源原文如下:

注:如果一个锚框没有被分配真实边界框,我们只需将该锚框的类别设为背景。类别为背景的锚框通常被称为负类锚框,其余则被称为正类锚框。交并比小于阈值,那么类别同样会被标注为背景。

ground_truth = nd.array([[0, 0.1, 0.08, 0.52, 0.92],
                         [1, 0.55, 0.2, 0.9, 0.88]])
anchors = nd.array([[0, 0.1, 0.2, 0.3], [0.15, 0.2, 0.4, 0.4],
                    [0.63, 0.05, 0.88, 0.98], [0.66, 0.45, 0.8, 0.8],
                    [0.57, 0.3, 0.92, 0.9]])

fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['dog', 'cat'], 'k')
show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']);
  1. ground_truth = nd.array([[0, 0.1, 0.08, 0.52, 0.92], [1, 0.55, 0.2, 0.9, 0.88]]):创建一个MXNet的ndarray对象,包含两个真实边界框的坐标。每个边界框的坐标格式为[class, x_min, y_min, x_max, y_max],其中class是类别标签,(x_min, y_min)(x_max, y_max)分别是边界框的左上角和右下角坐标。

  2. anchors = nd.array([[0, 0.1, 0.2, 0.3], [0.15, 0.2, 0.4, 0.4], [0.63, 0.05, 0.88, 0.98], [0.66, 0.45, 0.8, 0.8], [0.57, 0.3, 0.92, 0.9]]):创建一个MXNet的ndarray对象,包含五个锚框的坐标。每个锚框的坐标格式与真实边界框相同。

  3. fig = d2l.plt.imshow(img):使用d2l库中的imshow函数来显示图像img,并返回一个图像显示对象fig。这个对象包含了图像的显示信息,包括图像数据和轴对象。

  4. show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['dog', 'cat'], 'k'):调用show_bboxes函数来在图像上显示真实边界框。这里fig.axes提供了图像的轴对象,ground_truth[:, 1:]选择了真实边界框的坐标(排除了类别标签),并乘以bbox_scale来进行缩放。标签列表['dog', 'cat']为每个边界框提供了文本标签,'k'指定了文本和边界框的颜色为黑色。

  5. show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']):再次调用show_bboxes函数来在图像上显示锚框。这里anchors * bbox_scale将锚框的坐标乘以bbox_scale进行缩放。标签列表['0', '1', '2', '3', '4']为每个锚框提供了编号标签。

ps:轴对象是什么?

在图形用户界面(GUI)编程和数据可视化中,轴对象(Axis Object)是一个非常重要的概念。它通常指的是图表中的一个区域,这个区域用于显示数据的图形表示。轴对象通常包含以下几个关键部分:

  1. 坐标轴:包括水平轴(x轴)和垂直轴(y轴),它们定义了数据点在图表中的位置。坐标轴上可以有刻度(ticks)和标签(labels),用于指示具体的数值。

  2. 数据区域:这是图表中用于显示数据的部分,比如折线图的线条、散点图的点、柱状图的柱子等。

  3. 标题和标签:轴对象可以包含标题,以及为坐标轴指定的标签,这些用于描述图表的内容和坐标轴代表的数据。

  4. 图例:如果图表中有多个数据系列,轴对象可能会包含图例,以区分不同的数据系列。

  5. 边界和间距:轴对象的边界定义了数据区域的可视范围,以及与图表边缘的间距。

例如,以下是一个简单的matplotlib代码,它创建了一个轴对象并显示了一个简单的折线图:

import matplotlib.pyplot as plt

# 创建一个图形和一个轴对象
fig, ax = plt.subplots()

# 在轴对象上绘制数据
ax.plot([1, 2, 3], [4, 5, 6])

# 设置轴对象的标题和标签
ax.set_title("Simple Plot")
ax.set_xlabel("X Axis Label")
ax.set_ylabel("Y Axis Label")

# 显示图形
plt.show()

言归正传,之前所述代码效果如图:


labels = contrib.nd.MultiBoxTarget(anchors.expand_dims(axis=0),
                                   ground_truth.expand_dims(axis=0),
                                   nd.zeros((1, 3, 5)))

该函数将背景类别设为0,并令从0开始的目标类别的整数索引自加1(1为狗,2为猫)。我们通过expand_dims函数为锚框和真实边界框添加样本维,并构造形状为(批量大小, 包括背景的类别个数, 锚框数)的任意预测结果。第三项表示为锚框标注的类别。返回值的第二项为掩码(mask)变量,形状为(批量大小, 锚框个数的四倍)。掩码变量中的元素与每个锚框的4个偏移量一一对应。 由于我们不关心对背景的检测,有关负类的偏移量不应影响目标函数。通过按元素乘法,掩码变量中的0可以在计算目标函数之前过滤掉负类的偏移量。返回的第一项是为每个锚框标注的四个偏移量,其中负类锚框的偏移量标注为0。

labels[2]:[[0. 1. 2. 0. 2.]]
labels[1]:[[0. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 0. 0. 0. 0. 1. 1. 1. 1.]]
labels[0]:
[[ 0.00e+00  0.00e+00  0.00e+00  0.00e+00  1.40e+00  1.00e+01  2.59e+00
   7.18e+00 -1.20e+00  2.69e-01  1.68e+00 -1.57e+00  0.00e+00  0.00e+00
   0.00e+00  0.00e+00 -5.71e-01 -1.00e+00 -8.94e-07  6.26e-01]]

4、多尺度目标检测

第一部分输入图像的每个像素为中心生成多个锚框。这些锚框是对输入图像不同区域的采样。然而,如果以图像每个像素为中心都生成锚框,很容易生成过多锚框而造成计算量过大。在输入图像中均匀采样一小部分像素,并以采样的像素为中心生成锚框可减少锚框个数。

d2l.set_figsize()

def display_anchors(fmap_w, fmap_h, s):
    fmap = nd.zeros((1, 10, fmap_h, fmap_w))  # 前两维的取值不影响输出结果
    anchors = contrib.nd.MultiBoxPrior(fmap, sizes=s, ratios=[1, 2, 0.5])
    bbox_scale = nd.array((w, h, w, h))
    d2l.show_bboxes(d2l.plt.imshow(img.asnumpy()).axes,
                    anchors[0] * bbox_scale)

这段代码定义了一个名为display_anchors的函数,用于在特征图上生成并显示锚框。下面是对每一行代码的注释:

  1. d2l.set_figsize()

    • 调用d2l库中的set_figsize函数来设置图像显示的尺寸。这个函数可能会设置全局的图像显示大小,以便后续的图像显示函数使用。
  2. def display_anchors(fmap_w, fmap_h, s):

    • 定义一个名为display_anchors的函数,它接受三个参数:fmap_w(特征图的宽度),fmap_h(特征图的高度),s(锚框的大小列表)。
  3. fmap = nd.zeros((1, 10, fmap_h, fmap_w))

    • 创建一个形状为(1, 10, fmap_h, fmap_w)的MXNet ndarray,填充值为0。这个数组模拟了一个批量大小为1,有10个通道的特征图,其中fmap_hfmap_w分别是特征图的高度和宽度。前两维的取值不影响锚框生成的结果。
  4. anchors = contrib.nd.MultiBoxPrior(fmap, sizes=s, ratios=[1, 2, 0.5])

    • 使用contrib.nd模块中的MultiBoxPrior函数在特征图上生成锚框。sizes=s指定了锚框的大小,ratios=[1, 2, 0.5]指定了锚框的宽高比。函数的输出anchors包含了所有生成的锚框的坐标。
  5. bbox_scale = nd.array((w, h, w, h))

    • 创建一个MXNet ndarray,包含图像的宽度w和高度h的值。这个数组将用于将锚框的归一化坐标转换为图像的实际像素坐标。
  6. d2l.show_bboxes(d2l.plt.imshow(img.asnumpy()).axes, anchors[0] * bbox_scale)

    • 首先,d2l.plt.imshow(img.asnumpy())显示图像img,并返回一个图像显示对象,其中img.asnumpy()将图像数据转换为NumPy数组格式。
    • axes是图像显示对象的轴对象,它包含了图像显示的区域。
    • anchors[0]选择了生成的锚框数组中的第一个元素(因为批量大小为1),* bbox_scale将锚框的归一化坐标转换为图像的实际像素坐标。
    • d2l.show_bboxes函数在图像上显示这些锚框。

ps:MultiBoxPrior是在每个像素点上生成锚框的,那也就是在特征图上生成锚框,其实这不过是生成归一化比例下的锚框分布罢了,实际上乘bbox_scale = nd.array((w, h, w, h))后就会成为w*h大小的锚框,分布情况不变,我们anchors = contrib.nd.MultiBoxPrior(fmap, sizes=s, ratios=[1, 2, 0.5])只是为了获取一个分布情况。展示结果如下:

display_anchors(fmap_w=2, fmap_h=2, s=[0.4])

在某个尺度下,假设我们依据ci张形状为h×w的特征图生成h×w组不同中心的锚框,且每组的锚框个数为a。例如,在刚才实验的第一个尺度下,我们依据10(通道数)张形状为4×4的特征图生成了16组不同中心的锚框,且每组含3个锚框。 接下来,依据真实边界框的类别和位置,每个锚框将被标注类别和偏移量。在当前的尺度下,目标检测模型需要根据输入图像预测h×w组不同中心的锚框的类别和偏移量。

什么是感受野?

感受野(Receptive Field)是神经网络中,特别是卷积神经网络(Convolutional Neural Networks, CNNs)中的一个概念,它指的是网络中一个神经元对于输入图像的哪些区域是敏感的。换句话说,它描述了网络中一个特定层的神经元在原始输入数据上“看到”的区域大小和位置。

言归正传,在卷积神经网络中,特征图上相同位置的多个单元(ci个单元)共享相同的感受野,这意味着它们都对输入图像中的同一区域敏感。这些单元可以用来预测输入图像中与它们感受野位置相近的锚框(a个)的类别和位置偏移量。简而言之,我们利用输入图像中特定区域的信息来预测该区域附近的锚框的属性。

当特征图来自网络的不同层时,它们在输入图像上的感受野大小不同,因此可以用来检测不同尺寸的目标。通常,接近网络输出层的特征图具有较大的感受野,适合检测输入图像中较大的目标。通过设计网络结构,我们可以使得不同层的特征图分别负责检测不同尺寸的目标。

单发多框检测模型:SSD

1、部分需要用到的function

def cls_predictor(num_anchors, num_classes):  # 定义一个名为cls_predictor的函数,它接受两个参数:num_anchors(锚点数量)和num_classes(类别数量)。
    return nn.Conv2D(num_anchors * (num_classes + 1), kernel_size=3,  # 返回一个2D卷积层对象。
                     padding=1)  # 这个卷积层的参数包括:输出通道数为num_anchors乘以(num_classes加1),卷积核大小为3x3,边缘填充为1。
def bbox_predictor(num_anchors):  # 定义一个名为bbox_predictor的函数,它接受一个参数:num_anchors(锚点数量)。
    return nn.Conv2D(num_anchors * 4, kernel_size=3,  # 返回一个2D卷积层对象。
                     padding=1)  # 这个卷积层的参数包括:输出通道数为num_anchors乘以4,卷积核大小为3x3,边缘填充为1。
def forward(x, block):  # 定义一个名为forward的函数,它接受两个参数:x(输入数据)和block(神经网络块)。
    block.initialize()  # 初始化神经网络块,这通常涉及到权重的初始化。
    return block(x)  # 将输入数据x传递给神经网络块,并返回输出结果。

def flatten_pred(pred):  # 定义一个名为flatten_pred的函数,它接受一个参数:pred(预测结果)。
    return pred.transpose((0, 2, 3, 1)).flatten()  # 对预测结果进行转置和展平处理。
    # pred.transpose((0, 2, 3, 1)):将预测结果的维度进行转置,将通道维度移到最后。
    # flatten():将转置后的多维数组展平成一维数组。

def concat_preds(preds):  # 定义一个名为concat_preds的函数,它接受一个参数:preds(预测结果列表)。
    return nd.concat(*[flatten_pred(p) for p in preds], dim=1)  # 将列表中的每个预测结果展平并沿着第二维度(dim=1)进行拼接。
    # [flatten_pred(p) for p in preds]:对preds列表中的每个预测结果调用flatten_pred函数进行展平。
    # nd.concat(..., dim=1):使用mxnet的nd.concat函数将展平后的预测结果沿着dim=1的维度进行拼接。

效果如下:

2、一些用到的块

长宽减半块如下:

def down_sample_blk(num_channels):
    blk = nn.Sequential()
    for _ in range(2):
        blk.add(nn.Conv2D(num_channels, kernel_size=3, padding=1),
                nn.BatchNorm(in_channels=num_channels),
                nn.Activation('relu'))
    blk.add(nn.MaxPool2D(2))
    return blk
  • def down_sample_blk(num_channels): 定义了一个名为down_sample_blk的函数,它接受一个参数num_channels,这个参数表示卷积层的输出通道数。

  • blk = nn.Sequential() 创建了一个Sequential容器,这是一个用于按顺序添加神经网络层的工具,它会自动将添加的层串联起来形成一个网络。

  • for _ in range(2): 这是一个循环,它会执行两次。循环变量_是一个占位符,表示我们不使用这个变量的值。

  • blk.add(nn.Conv2D(num_channels, kernel_size=3, padding=1), 这行代码向Sequential容器中添加了一个2D卷积层。num_channels是输出通道数,kernel_size=3表示卷积核的大小为3x3,padding=1表示在输入数据周围填充1个单位的零,以保持输出的空间维度不变。

  • nn.BatchNorm(in_channels=num_channels), 这行代码向Sequential容器中添加了一个批量归一化层,in_channels=num_channels表示输入通道数与num_channels相同。

  • nn.Activation('relu')) 这行代码向Sequential容器中添加了一个ReLU激活函数,它将对前一层的输出进行非线性变换。

  • blk.add(nn.MaxPool2D(2)) 这行代码向Sequential容器中添加了一个最大池化层,2表示池化窗口的大小为2x2,步长默认也为2,这将导致输出的空间维度减半。

  • return blk 返回创建好的神经网络块,这个块可以被用作更大的神经网络中的一个组件,用于实现特征的下采样。

类似的初始基础块如下:

def base_net():
    blk = nn.Sequential()
    for num_filters in [16, 32, 64]:
        blk.add(down_sample_blk(num_filters))
    return blk

组合得到完整块如下:

def get_blk(i):
    if i == 0:
        blk = base_net()
    elif i == 4:
        blk = nn.GlobalMaxPool2D()
    else:
        blk = down_sample_blk(128)
    return blk

3、向前计算、参数设置、完整模型(TinySSD)

def blk_forward(X, blk, size, ratio, cls_predictor, bbox_predictor):
    Y = blk(X)
    anchors = contrib.ndarray.MultiBoxPrior(Y, sizes=size, ratios=ratio)
    cls_preds = cls_predictor(Y)
    bbox_preds = bbox_predictor(Y)
    return (Y, anchors, cls_preds, bbox_preds)

sizes = [[0.2, 0.272], [0.37, 0.447], [0.54, 0.619], [0.71, 0.79],
         [0.88, 0.961]]
ratios = [[1, 2, 0.5]] * 5
num_anchors = len(sizes[0]) + len(ratios[0]) - 1
class TinySSD(nn.Block):
    def __init__(self, num_classes, **kwargs):
        super(TinySSD, self).__init__(**kwargs)
        self.num_classes = num_classes
        for i in range(5):
            # 即赋值语句self.blk_i = get_blk(i)
            setattr(self, 'blk_%d' % i, get_blk(i))
            setattr(self, 'cls_%d' % i, cls_predictor(num_anchors,
                                                      num_classes))
            setattr(self, 'bbox_%d' % i, bbox_predictor(num_anchors))

    def forward(self, X):
        anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5
        for i in range(5):
            # getattr(self, 'blk_%d' % i)即访问self.blk_i
            X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(
                X, getattr(self, 'blk_%d' % i), sizes[i], ratios[i],
                getattr(self, 'cls_%d' % i), getattr(self, 'bbox_%d' % i))
        # reshape函数中的0表示保持批量大小不变
        return (nd.concat(*anchors, dim=1),
                concat_preds(cls_preds).reshape(
                    (0, -1, self.num_classes + 1)), concat_preds(bbox_preds))
  • class TinySSD(nn.Block): 定义了一个名为TinySSD的类,它继承自mxnet.gluon.nn.Block,是一个神经网络的构建块。

  • def __init__(self, num_classes, **kwargs): 类的构造函数,接受num_classes参数(类别的数量)和任意数量的关键字参数。

  • super(TinySSD, self).__init__(**kwargs) 调用父类的构造函数,这是初始化nn.Block类的必要步骤。

  • self.num_classes = num_classes 将传入的类别数保存为类的属性。

  • for i in range(5): 循环5次,为每个尺度的特征图创建相应的网络块和预测器。

  • setattr(self, 'blk_%d' % i, get_blk(i)) 使用setattr动态设置属性,创建名为blk_i的属性,其中i是当前的尺度索引。

  • setattr(self, 'cls_%d' % i, cls_predictor(num_anchors, num_classes)) 创建分类预测器,并设置属性名为cls_i

  • setattr(self, 'bbox_%d' % i, bbox_predictor(num_anchors)) 创建边界框预测器,并设置属性名为bbox_i

  • def forward(self, X): 定义前向传播函数,X是输入的特征图。

  • anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5 初始化三个列表,用于存储每个尺度的锚点、分类预测和边界框预测。

  • for i in range(5): 循环5次,处理每个尺度的特征图。

  • X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(...) 调用blk_forward函数处理每个尺度的特征图,并返回处理后的特征图以及锚点、分类预测和边界框预测。

  • return (nd.concat(*anchors, dim=1), ...) 返回所有尺度的锚点在dim=1维度上拼接的结果,以及所有尺度的分类预测和边界框预测在concat_preds函数处理后的结果。

模型检验:

SSD的训练

1、定义损失函数和评价函数

cls_loss = gloss.SoftmaxCrossEntropyLoss()
bbox_loss = gloss.L1Loss()

def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
    cls = cls_loss(cls_preds, cls_labels)
    bbox = bbox_loss(bbox_preds * bbox_masks, bbox_labels * bbox_masks)
    return cls + bboxdef cls_eval(cls_preds, cls_labels):
    # 由于类别预测结果放在最后一维,argmax需要指定最后一维
    return (cls_preds.argmax(axis=-1) == cls_labels).sum().asscalar()

def bbox_eval(bbox_preds, bbox_labels, bbox_masks):
    return ((bbox_labels - bbox_preds) * bbox_masks).abs().sum().asscalar()
  • cls_loss = gloss.SoftmaxCrossEntropyLoss() 创建一个用于分类任务的Softmax交叉熵损失函数对象。

  • bbox_loss = gloss.L1Loss() 创建一个用于边界框回归任务的L1损失函数对象,L1损失即绝对值损失。

  • def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks): 定义一个名为calc_loss的函数,用于计算总损失,参数包括分类预测、分类标签、边界框预测、边界框标签和边界框掩码。

  • cls = cls_loss(cls_preds, cls_labels) 计算分类损失,使用Softmax交叉熵损失函数。

  • bbox = bbox_loss(bbox_preds * bbox_masks, bbox_labels * bbox_masks) 计算边界框损失,使用L1损失函数,但是只计算那些由bbox_masks标记为有效的边界框的损失。

  • return cls + bbox 返回总损失,即分类损失和边界框损失的和。

  • def cls_eval(cls_preds, cls_labels): 定义一个名为cls_eval的函数,用于评估分类任务的性能。

  • return (cls_preds.argmax(axis=-1) == cls_labels).sum().asscalar() 计算分类预测正确的数量。argmax(axis=-1)获取预测概率最高的类别索引,然后与真实标签cls_labels比较,得到一个布尔数组,表示每个预测是否正确。sum()计算正确的预测总数,asscalar()将结果转换为一个标量。

  • def bbox_eval(bbox_preds, bbox_labels, bbox_masks): 定义一个名为bbox_eval的函数,用于评估边界框回归任务的性能。

  • return ((bbox_labels - bbox_preds) * bbox_masks).abs().sum().asscalar() 计算边界框预测的绝对误差。bbox_labels - bbox_preds计算预测值和真实值之间的差,* bbox_masks只考虑那些由bbox_masks标记为有效的边界框,abs()计算绝对值,sum()求和所有有效的误差,asscalar()将结果转换为一个标量。

2、训练函数

for epoch in range(20):
    acc_sum, mae_sum, n, m = 0.0, 0.0, 0, 0
    train_iter.reset()  # 从头读取数据
    start = time.time()
    for batch in train_iter:
        X = batch.data[0].as_in_context(ctx)
        Y = batch.label[0].as_in_context(ctx)
        with autograd.record():
            # 生成多尺度的锚框,为每个锚框预测类别和偏移量
            anchors, cls_preds, bbox_preds = net(X)
            # 为每个锚框标注类别和偏移量
            bbox_labels, bbox_masks, cls_labels = contrib.nd.MultiBoxTarget(
                anchors, Y, cls_preds.transpose((0, 2, 1)))
            # 根据类别和偏移量的预测和标注值计算损失函数
            l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels,
                          bbox_masks)
        l.backward()
        trainer.step(batch_size)
        acc_sum += cls_eval(cls_preds, cls_labels)
        n += cls_labels.size
        mae_sum += bbox_eval(bbox_preds, bbox_labels, bbox_masks)
        m += bbox_labels.size

    if (epoch + 1) % 5 == 0:
        print('epoch %2d, class err %.2e, bbox mae %.2e, time %.1f sec' % (
            epoch + 1, 1 - acc_sum / n, mae_sum / m, time.time() - start))
  • for epoch in range(20): 开始一个循环,进行20个训练周期(epoch)。

  • acc_sum, mae_sum, n, m = 0.0, 0.0, 0, 0 初始化四个累加器,分别用于存储分类准确度和边界框平均绝对误差的总和,以及对应的样本数量。

  • train_iter.reset() 重置训练数据迭代器,确保每个epoch都从数据集的开始处读取数据。

  • start = time.time() 记录当前时间,以便计算每个epoch的运行时间。

  • for batch in train_iter: 遍历训练数据集中的每个批次。

  • X = batch.data[0].as_in_context(ctx) 将输入数据移动到指定的计算上下文(如GPU)。

  • Y = batch.label[0].as_in_context(ctx) 将标签数据移动到指定的计算上下文。

  • with autograd.record(): 开始自动梯度记录,这是进行反向传播的前提。

  • anchors, cls_preds, bbox_preds = net(X) 通过神经网络对输入数据X进行前向传播,得到锚框、分类预测和边界框预测。

  • bbox_labels, bbox_masks, cls_labels = contrib.nd.MultiBoxTarget(anchors, Y, cls_preds.transpose((0, 2, 1))) 使用MultiBoxTarget函数为每个锚框分配类别标签和边界框标签,以及一个掩码来指示哪些锚框是有效的。

  • l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks) 计算总损失,包括分类损失和边界框损失。

  • l.backward() 进行反向传播,计算损失相对于模型参数的梯度。

  • trainer.step(batch_size) 使用优化器根据计算出的梯度更新模型参数。

  • acc_sum += cls_eval(cls_preds, cls_labels) 累加分类准确度评估结果。

  • n += cls_labels.size 累加分类标签的总数。

  • mae_sum += bbox_eval(bbox_preds, bbox_labels, bbox_masks) 累加边界框的平均绝对误差评估结果。

  • m += bbox_labels.size 累加边界框标签的总数。

  • if (epoch + 1) % 5 == 0: 每5个epoch输出一次训练信息。

  • print(...) 打印当前epoch、分类错误率、边界框平均绝对误差和epoch的运行时间。

训练好的模型可以使用来预测图像的边框,实现多目标检测

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值