深度学习24物体检测算法R-CNN、SSD、YOLO

1. RCNN

  • 首先从输入图像中选取若干个提议区域锚框 是选取方式的一种),并标注它们的类别边界框(如偏移量)。然后用卷积神经网络来对每个提议区域(锚框)进行前向传播以抽取特征。最后用每个提议区域的特征来预测类别和边界框。

R-CNN 模型的四个步骤:

  1. 对输入图像使用选择性搜索 来选取多个高质量的提议区域。这些提议区域通常是在多个尺度 下选取的,并具有不同的形状和大小 ;每个提议区域都将被标注类别和真实边框
  2. 选择一个预训练的卷积神经网络,并将其在输出层之前截断。将每个提议区域变形为网络需要的输入尺寸,并通过前向传播输出抽取的提议区域特征
  3. 将每个提议区域的特征连同其标注的类别作为一个样本。训练多个支持向量机 对目标分类,其中每个支持向量机用来判断样本是否属于某一个类别
  4. 将每个提议区域的特征连同其标注的边界框作为一个样本,训练线性回归模型 来预测真实边界框

每次选择的锚框的大小是不同的,在这种情况下,怎样使这些大小不一的锚框变成一个batch?

使用RoI pooling(兴趣区域池化层)

RoI pooling(兴趣区域池化层)

  • R-CNN 中比较关键的层,作用是将大小不一的锚框变成统一的形状
  • 给定一个锚框,先将其均匀地分割成 n * m 块,然后输出每块里的最大值,这样的话,不管锚框有多大,只要给定了 n 和 m 的值,总是输出 nm 个值,这样的话,不同大小的锚框就都可以变成同样的大小,然后作为一个小批量,之后的处理就比较方便了
  • 上图中对 3 * 3 的黑色方框中的区域进行 2 * 2 的兴趣区域池化,由于 3 * 3 的区域不能均匀地进行切割成 4 块,所以会进行取整(最终将其分割成为 2 * 2、1 * 2、2 * 1、1 * 1 四块),在做池化操作的时候分别对四块中每一块取最大值,然后分别填入 2 * 2 的矩阵中相应的位置

兴趣区域汇聚层(RoI Pooing)与一般的汇聚层有什么不同?

  • 在一般的汇聚层中,通过设置汇聚窗口、填充和步幅的大小间接控制输出形状

  • 在兴趣区域汇聚层中,对每个区域的输出形状是可以直接指定的
    小结

  • 尽管 R-CNN 模型通过预训练的卷积神经网络有效地抽取了图像特征,但是速度非常慢 (如果从一张图片中选取了上千个提议区域,就需要上千次的卷积神经网络的前向传播来执行目标检测,计算量非常大

Fast R-CNN



  • R-CNN 每次拿到一张图片都需要抽取特征,如果说一张图片中生成的锚框数量较大,抽取特征的次数也会相应的增加,大大增加了计算量
  • 因此,R-CNN 的主要性能瓶颈在于,对于每个提议区域,卷积神经网络的前向传播是独立的,没有共享计算(这些提议区域通常有重叠,独立的特征提取会导致重复计算)
  • Faste R-CNN 的改进 是:在拿到一张图片之后,首先使用 CNN 对图片进行特征提取(不是对图片中的锚框进行特征提取,而是对整张图片进行特征提取,仅在整张图像上执行卷积神经网络的前向传播),最终会得到一个 7 * 7 或者 14 * 14 的 feature map
  • 抽取完特征之后,再对图片进行锚框的选择(selective search),搜索到原始图片上的锚框之后将其(按照一定的比例)映射到 CNN 的输出上
  • 映射完锚框之后,再使用 RoI pooling 对 CNN 输出的 feature map 上的锚框进行特征抽取,生成固定长度的特征(将 n * m 的矩阵拉伸成为 nm 维的向量),之后再通过一个全连接层(这样就不需要使用SVM一个一个的操作,而是一次性操作了)对每个锚框进行预测:物体的类别和真实的边缘框的偏移
  • 上图中黄色方框的作用就是将原图中生成的锚框变成对应的向量
  • Fast R-CNN 相对于 R-CNN 更快的原因是:Fast R-CNN 中的 CNN 不再对每个锚框抽取特征,而是对整个图片进行特征的提取(这样做的好处是:不同的锚框之间可能会有重叠 的部分,如果对每个锚框都进行特征提取的话,可能会对重叠的区域进行多次重复的特征提取操作),然后再在整张图片的feature中找出原图中锚框对应的特征,最后一起做预测

Fast R-CNN 中的主要计算:

Faster R-CNN

  • 为了精确地检测目标结果,Fast R-CNN 模型通常需要在选择性搜索中生成大量的提议区域

  • 因此,Faster R-CNN 提出将选择性搜索替换为区域提议网络 (region proposal network,RPN),模型的其余部分保持不变,从而减少区域的生成数量,并保证目标检测的精度

  • Faster R-CNN 的改进:使用 RPN 神经网络来替代 selective search

  • RoI 的输入是CNN 输出的 feature map生成的锚框

  • RPN 的输入是 CNN 输出的 feature map,输出是一些比较高质量的锚框(可以理解为一个比较小而且比较粗糙的目标检测算法 : CNN 的输出进入到 RPN 之后再做一次卷积,然后生成一些锚框(可以是 selective search 或者其他方法来生成初始的锚框),再训练一个二分类问题 :预测锚框是否框住了真实的物体 以及锚框到真实的边缘框的偏移 ,最后使用 NMS 进行去重,使得锚框的数量变少)

  • RPN 的作用是生成大量结果很差的锚框,然后进行预测,最终输出比较好的锚框供后面的网络使用(预测出来的比较好的锚框会进入 RoI pooling,后面的操作与 Fast R-CNN 类似)

  • 通常被称为两阶段的目标检测算法:RPN 做小的目标检测(粗糙),整个网络再做一次大的目标检测(精准)

  • Faster R-CNN 目前来说是用的比较多的算法,准确率比较高,但是速度比较慢
    区域提议网络的计算步骤:

  • 区域提议网络作为Faster R-CNN 模型的一部分,是和整个模型一起训练得到的(Faster R-CNN 的目标函数不仅包括目标检测中的类别和边界框预测,还包括区域提议网络中锚框的二元类别和边界框预测)
  • 作为端到端训练的结果,区域提议网络能够学习到如何生成高质量的提议区域,从而在减少了从数据中学习的提议区域的数量的情况下,仍然保持了目标检测的精度

Mask R-CNN

  • 如果在训练集中还标注了每个目标在图像上的像素级位置,Mask R-CNN 能够有效地利用这些相近地标注信息进一步提升目标检测地精度

Mask R-CNN 是基于 Faster R-CNN 修改而来的,改进在于

  • 假设有每个像素的标号的话,就可以对每个像素做预测(FCN
  • 将兴趣区域汇聚层替换成了兴趣区域对齐层 (RoI pooling -> RoI align),使用双线性插值 (bilinear interpolation)保留特征图上的空间信息,进而更适于像素级预测:对于pooling来说,假如有一个3 * 3的区域,需要对它进行2 * 2的RoI pooling操作,那么会进行取整从而切割成为不均匀的四个部分,然后进行 pooling 操作,这样切割成为不均匀的四部分的做法对于目标检测来说没有太大的问题,因为目标检测不是像素级别的,偏移几个像素对结果没有太大的影响。但是对于像素级别的标号来说,会产生极大的误差;RoI align 不管能不能整除,如果不能整除的话,会直接将像素切开,切开后的每一部分是原像素的加权(它的值是原像素的一部分)
  • 兴趣区域对齐层的输出包含了所有与兴趣区域的形状相同的特征图,它们不仅被用于预测每个兴趣区域的类别和边界框 ,还通过额外的全卷积网络预测目标的像素级位置
  • x 轴表示模型的运行速度,越往右表示模型的速度越快,越往左越慢
  • y 轴表示 mAP(可以简单认为是边界框的预测精度),越往上表示精度越高
  • 图中圆圈的大小表示内存的使用
  • Faster RCNN 相对来说精度比较高,但是它在精度提升的同时,样本的处理速度也在变慢(所以只有在对精度要求特别高的场景下会采用 Faster RCNN,但是在工业上使用较少)
  • R-CNN 是最早、也是最有名的一类基于锚框和 CNN 的目标检测算法(R-CNN 可以认为是使用神经网络来做目标检测工作的奠基工作之一),它对图像选取若干提议区域,使用卷积神经网络对每个提议区域执行前向传播以抽取其特征,然后再用这些特征来预测提议区域的类别和边框
  • Fast/Faster R-CNN持续提升性能:Fast R-CNN 只对整个图像做卷积神经网络的前向传播 ,还引入了兴趣区域汇聚层 (RoI pooling),从而为具有不同形状的兴趣区域抽取相同形状的特征;Faster R-CNN 将 Fast R-CNN 中使用的选择性搜索替换为参与训练的区域提议网络 ,这样可以在减少提议区域数量的情况下仍然保持目标检测的精度;Mask R-CNN 在 Faster R-CNN 的基础上引入了一个全卷积网络 ,从而借助目标的像素级位置进一步提升目标检测的精度
  • Faster R-CNN 和 Mask R-CNN 是在追求高精度场景下的常用算法(Mask R-CNN 需要有像素级别的标号,所以相对来讲局限性会大一点,在无人车领域使用的比较多)

2. SSD

单发多框检测(SSD)

  • 对每个像素生成多个以它为中心的多个锚框
  • 输入图像之后,首先进入一个基础网络来抽取特征,抽取完特征之后对每个像素生成大量的锚框(每个锚框就是一个样本,然后预测锚框的类别以及到真实边界框的偏移)
  • SSD 在给定锚框之后直接对锚框进行预测 ,而不需要做两阶段(为什么 Faster RCNN 需要做两次,而 SSD 只需要做一次?SSD 通过做不同分辨率下的预测来提升最终的效果,越到底层的 feature map,就越大,越往上,feature map 越少,因此底层更加有利于小物体的检测,而上层更有利于大物体的检测
  • SSD 不再使用 RPN 网络,而是直接在生成的大量样本(锚框)上做预测,看是否包含目标物体;如果包含目标物体,再预测该样本到真实边缘框的偏移
  • 上图中绿色的点表示 SSD

  • 从图中可以看出,SSD 相对于Faster RCNN 来讲速度快很多,但是精度不是太好

  • SSD 的实现相对来讲比较简单,R-CNN 系列代码的实现非常困难
    总结

  • SSD通过单神经网络来检测模型

  • 以每个像素为中心产生多个锚框

  • 多个段 的输出上进行多尺度的检测(底层检测小物体,上层检测大物体)

3. YOLO

YOLO



  • yolo 也是一个 single-stage 的算法,只有一个单神经网络来做预测
  • yolo 也需要锚框,这点和 SSD 相同,但是 SSD 是对每个像素点生成多个锚框,所以在绝大部分情况下两个相邻像素的所生成的锚框的重叠率是相当高的,这样就会导致很大的重复计算量。
  • yolo 的想法是尽量让锚框不重叠:首先将图片均匀地分成 S * S 块,每一块就是一个锚框,每一个锚框预测 B 个边缘框(考虑到一个锚框中可能包含多个物体),所以最终就会产生 S ^ 2 * B 个样本,因此速度会远远快于 SSD
  • yolo 在后续的版本(V2,V3,V4...)中有持续的改进,但是核心思想没有变,真实的边缘框不会随机的出现,真实的边缘框的比例、大小在每个数据集上的出现是有一定的规律的,在知道有一定的规律的时候就可以使用聚类算法 将这个规律找出来(给定一个数据集,先分析数据集中的统计信息,然后找出边缘框出现的规律,这样之后在生成锚框的时候就会有先验知识,从而进一步做出优化)
  • 上图中表示 yolo v3 的直线底端表示论文中的原始精度 ,顶端表示通过改进之后所能达到的最大精度
    非****锚框 算法center net



  • 基于非锚框的目标检测
  • center net 的优点在于简单
  • center net 会对每个像素 做预测,用 FCN 对每个像素做预测(类似于图像分割中用 FCN 对每个像素标号),预测该像素点是不是真实边缘框的中心点(将目标检测的边缘框换算成基于每个像素的标号,然后对每个像素做预测,就免去了一些锚框相关的操作)

4. 多尺度目标检测实现

python 复制代码
%matplotlib inline
import torch
from d2l import torch as d2l

img = d2l.plt.imread('01_Data/img/catdog.jpg')
h, w = img.shape[:2]
print(h, w) # 打印导入图片的高、宽
print(img.shape) # 最后一个元素为通道数,通道数为3
复制代码
561 728
(561, 728, 3)
python 复制代码
# 在特征图(fmap)上生成锚框(anchors),每个单位(像素)作为锚框的中心
def display_anchors(fmap_w,fmap_h,s):
    # 使用d2l库的set_figsize函数设置绘图的尺寸。
    d2l.set_figsize()
    # 创建一个全零的张量,形状为(1,10,fmap_h,fmap_w)。这里1是批量大小,10是通道数,fmap_h和fmap_w是特征图的高度和宽度。
    fmap = torch.zeros((1,10,fmap_h,fmap_w)) # 批量大小为1,通道数为10
    # 调用d2l库的multibox_prior函数,以特征图的每个像素为中心,生成多个形状和比例不同的锚框,并将结果赋值给anchors。
    anchors = d2l.multibox_prior(fmap,sizes=s,ratios=[1,2,0.5]) # 生成以每个像素为中心的锚框  
    # 创建一个张量,内容为(w,h,w,h),用于将锚框的坐标从特征图的尺度转换到原始图片的尺度。
    bbox_scale = torch.tensor((w,h,w,h))
    # 使用d2l库的show_bboxes函数在原始图片上显示锚框。这里需要将锚框的坐标乘以bbox_scale进行尺度转换。因为批量大小为1,所以使用anchors[0]。
    d2l.show_bboxes(d2l.plt.imshow(img).axes,anchors[0] * bbox_scale) # anchors[0]是因为这里的batchsize为1   
python 复制代码
# 探测小目标
# 调用display_anchors函数,输入参数是特征图的宽度(fmap_w=4)、高度(fmap_h=4)以及锚框的尺寸(s=[0.15])。
# 这行代码将在特征图上生成并显示锚框,特征图的尺寸是4x4,锚框的尺寸是0.15(相对于特征图的尺寸)。
# 对于每个特征图的像素,都会生成一个以该像素为中心,尺寸为0.15的锚框。
display_anchors(fmap_w=4,fmap_h=4,s=[0.15])
python 复制代码
# 将特征图的高度和宽度减小一半,然后使用较大的锚框来检测较大的目标
display_anchors(fmap_w=2,fmap_h=2,s=[0.4])
python 复制代码
# 将特征图的高度和宽度较小一半,然后将锚框的尺度增加到0.8
display_anchors(fmap_w=1,fmap_h=1,s=[0.8])

5. 单发多框检测(SSD)

python 复制代码
%matplotlib inline
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
python 复制代码
# 类别预测层
# 定义一个函数cls_predictor,输入参数分别是输入通道数(num_inputs)、锚框数(num_anchors)以及类别数(num_classes)。
def cls_predictor(num_inputs, num_anchors, num_classes): # 输入通道数、多少个锚框、多少个类
    # 每个锚框的输出通道数都为(num_classes + 1),加1为加的背景类
    # 每一个锚框都要预测他的类别
    # 返回一个卷积层,输入通道数是num_inputs,输出通道数是num_anchors * (num_classes + 1),
    # 卷积核大小是3,填充大小是1。这里输出通道数乘以(num_classes + 1)的原因是,每个锚框需要预测num_classes + 1个类别的概率,
    # 其中+1是因为增加了背景类(即没有检测到物体的情况)。
    return nn.Conv2d(num_inputs, num_anchors * (num_classes + 1),
                    kernel_size=3, padding=1)
python 复制代码
# 边界框预测层
# 定义一个函数bbox_predictor,输入参数分别是输入通道数(num_inputs)和锚框数(num_anchors)。
def bbox_predictor(num_inputs, num_anchors):
    # 返回一个卷积层,输入通道数是num_inputs,输出通道数是num_anchors * 4,
    # 卷积核大小是3,填充大小是1。这里输出通道数乘以4的原因是,每个锚框需要预测4个值(边界框的中心坐标和宽高)来确定边界框的位置。
    return nn.Conv2d(num_inputs, num_anchors * 4, kernel_size=3, padding=1)   
python 复制代码
# 连接多尺度的预测
# 定义一个函数forward,输入参数是输入数据x和一个网络层block。
def forward(x, block):
    # 返回通过网络层block处理输入数据x后的结果。
    return block(x)
python 复制代码
# 使用全0的输入数据和一个类别预测层,进行前向传播。这里输入数据的形状是(2,8,20,20),表示有2张图片,每张图片有8个通道,尺寸是20x20。
# 类别预测层的参数是8,5,10,表示输入通道数是8,锚框数是5,类别数是10。
Y1 = forward(torch.zeros((2,8,20,20)),cls_predictor(8,5,10))
# 使用另一个全0的输入数据和一个类别预测层,进行前向传播。这里输入数据的形状是(2,16,10,10),表示有2张图片,每张图片有16个通道,尺寸是10x10。
# 类别预测层的参数是16,3,10,表示输入通道数是16,锚框数是3,类别数是10。
Y2 = forward(torch.zeros((2,16,10,10)),cls_predictor(16,3,10))
# 打印Y1的形状,这里是(2, 55, 20, 20),表示有2张图片,每张图片有55个通道,尺寸是20x20。这里的55是因为每个锚框需要预测10+1个类别的概率,共有5个锚框,所以通道数是5*(10+1)=55。
print(Y1.shape) # 5 * (10 + 1) = 55, 55为每一个像素输出的所有锚框数对应的类别
print(Y2.shape) # 2为小批量里面有多少图片
复制代码
torch.Size([2, 55, 20, 20])
torch.Size([2, 33, 10, 10])
python 复制代码
# 定义一个函数flatten_pred,输入参数是预测结果pred。
def flatten_pred(pred):
    # pred.permute(0,2,3,1)通道数放在最后
    # start_dim=1把pred.permute(0,2,3,1)后面三个向量拉成一个向量,这样就变成一个2D矩阵
    # 2D矩阵的高为批量大小,宽为每个图片的所有
    # 如果pred.permute(0,2,3,1)不改的话,对于同一像素的类别预测flatten后会相隔较远
    # 返回拉平后的预测结果。首先使用permute方法调整通道数的位置,然后使用flatten方法将除第一维(批量大小)之外的所有维度拉平。
    return torch.flatten(pred.permute(0,2,3,1),start_dim=1) 
python 复制代码
# 定义一个函数concat_preds,输入参数是多个预测结果的列表preds。
def concat_preds(preds):
    #[print(flatten_pred(p).shape) for p in preds]
    # 返回连接后的预测结果。首先使用列表推导式和flatten_pred函数拉平每个预测结果,然后使用torch.cat方法在dim=1的维度上连接所有的预测结果。
    return torch.cat([flatten_pred(p) for p in preds], dim=1)

# 打印连接后的预测结果的形状。这里输入的是两个预测结果Y1和Y2,输出的形状是(2, 35300),表示有2张图片,每张图片有25300个预测结果。
print(concat_preds([Y1, Y2]).shape)
复制代码
torch.Size([2, 25300])
python 复制代码
# 高和宽减半块
# 定义一个函数down_sample_blk,输入参数是输入通道数(in_channels)和输出通道数(out_channels)。
def down_sample_blk(in_channels, out_channels):
    # 创建一个空列表blk,用于存储网络层。
    blk = []
    # 循环2次,每次添加2个卷积层、1个批量归一化层和1个ReLU激活函数。
    for _ in range(2):
        # 添加一个卷积层,输入通道数是in_channels,输出通道数是out_channels,卷积核大小是3,填充大小是1。
        blk.append(nn.Conv2d(in_channels,out_channels,kernel_size=3,padding=1))     
        # 添加一个批量归一化层,输入通道数是out_channels。
        blk.append(nn.BatchNorm2d(out_channels))
        # 添加一个ReLU激活函数。
        blk.append(nn.ReLU())
        # 更新输入通道数为当前的输出通道数。
        in_channels = out_channels
    # 添加一个最大池化层,池化核大小是2,这可以将输入数据的高度和宽度减半。
    blk.append(nn.MaxPool2d(2))
    # 返回一个顺序容器,其中包含了所有的网络层。
    return nn.Sequential(*blk)

# 打印通过下采样模块处理后的输出数据的形状。这里输入的是全0的数据,形状是(2,3,20,20),表示有2张图片,每张图片有3个通道,尺寸是20x20。
# 下采样模块的参数是3和10,表示输入通道数是3,输出通道数是10。输出的形状是(2,10,10,10),表示有2张图片,每张图片有10个通道,尺寸是10x10。
print(forward(torch.zeros((2,3,20,20)),down_sample_blk(3,10)).shape)
复制代码
torch.Size([2, 10, 10, 10])
python 复制代码
# 基本网络块
# 定义一个函数base_net,没有输入参数。
def base_net(): # 从原始图片抽特征,到第一次featuremao产生锚框,中间的那一截叫base_net  
    # 创建一个空列表blk,用于存储网络层。
    blk = []
    # 定义一个列表num_filters,表示每个下采样模块的输入通道数和输出通道数。
    num_filters = [3,16,32,64] # 通道数由3到16,再到32,再到64
    # 循环遍历num_filters列表,每次添加一个下采样模块。
    for i in range(len(num_filters) - 1):
        # 添加一个下采样模块,输入通道数是num_filters[i],输出通道数是num_filters[i+1]。
        blk.append(down_sample_blk(num_filters[i],num_filters[i+1]))
    # 返回一个顺序容器,其中包含了所有的网络层。
    return nn.Sequential(*blk)

# 打印通过基础网络模块处理后的输出数据的形状。这里输入的是全0的数据,形状是(2,3,256,256),表示有2张图片,每张图片有3个通道,尺寸是256x256。
# 输出的形状是(2,64,32,32),表示有2张图片,每张图片有64个通道,尺寸是32x32。
print(forward(torch.zeros((2,3,256,256)),base_net()).shape)
复制代码
torch.Size([2, 64, 32, 32])
python 复制代码
# 完整的单发多框检测模型由五个模块组成
# 定义一个函数get_blk,输入参数是一个整数i。
def get_blk(i):
    # 如果i等于0,则返回基础网络模块。
    if i == 0:
        # 使得通道数变为64的featuremap
        blk = base_net() # 使得通道数变为64的featuremap
    # 如果i等于1,则返回一个下采样模块,输入通道数是64,输出通道数是128。
    elif i == 1:
        blk = down_sample_blk(64,128) # 通道数变为128
    # 如果i等于4,则返回一个自适应最大池化层,输出尺寸是1x1。
    elif i == 4:
        blk = nn.AdaptiveMaxPool2d((1,1)) # 最后一层,把图片压缩到1×1
    # 如果i等于其他值,则返回一个下采样模块,输入通道数是128,输出通道数也是128。
    else:
        blk = down_sample_blk(128, 128)
    # 返回对应的网络模块。
    return blk
python 复制代码
# 为每个块定义前向计算
# 定义一个函数blk_forward,输入参数是输入数据X、网络模块blk、锚框大小size、纵横比ratio、类别预测器cls_predictor和边界框预测器bbox_predictor。
def blk_forward(X,blk,size,ratio,cls_predictor,bbox_predictor):
    # 通过网络模块blk处理输入数据X,得到输出数据Y。
    Y = blk(X)
    # 在输出数据Y上生成锚框,锚框的大小是size,纵横比是ratio。
    anchors = d2l.multibox_prior(Y,sizes=size,ratios=ratio)
    # 使用类别预测器cls_predictor预测每个锚框的类别。
    cls_preds = cls_predictor(Y)
    # 使用边界框预测器bbox_predictor预测每个锚框的边界框。
    bbox_preds = bbox_predictor(Y)
    # 返回 卷积层输出、卷积层输出上生成的锚框、每一个锚框的类别的预测,每一个锚框到真实边缘框的预测   
    # 返回输出数据Y、生成的锚框、类别预测结果和边界框预测结果。
    return (Y,anchors,cls_preds,bbox_preds)
python 复制代码
# 超参数
# 小的size看小的特征,大的size看整个图片特征
# sizes 是一个二维列表,表示各个尺度下的锚框大小。数值越小,看的特征越小,数值越大,看的是整个图片的特征。
sizes = [[0.2,0.272],[0.37,9.447],[0.54,0.619],[0.71,0.79],[0.88,0.961]]
# ratios 是一个二维列表,表示各个尺度下的锚框纵横比。这里每个尺度下的锚框纵横比都是 [1,2,0.5]。
ratios = [[1,2,0.5]] * 5
# num_anchors 是一个整数,表示每个尺度下的锚框数量。这里的计算方法是:每个尺度下的大小数量 + 每个尺度下的纵横比数量 - 1。   
num_anchors = len(sizes[0]) + len(ratios[0]) - 1
python 复制代码
# 定义完整的模型
# TinySSD 是 PyTorch 的一个自定义模型类,继承自 nn.Module 类。
class TinySSD(nn.Module):
    # 构造函数,输入参数是分类的数量 num_classes 和其他命名参数。
    def __init__(self, num_classes, **kwargs):
        # 调用父类 nn.Module 的构造函数。
        super(TinySSD,self).__init__(**kwargs)
        # 保存分类的数量。
        self.num_classes = num_classes
        # 定义一个列表,表示每个模块的输入通道数。
        idx_to_in_channels = [64,128,128,128,128]
        # 对于每个模块:
        for i in range(5):
            # setattr是给对象的属性赋值
            # 创建模块,并保存到 self 的属性中。
            setattr(self,f'blk_{i}',get_blk(i))
            # 创建类别预测器,并保存到 self 的属性中。
            setattr(self,f'cls_{i}',cls_predictor(idx_to_in_channels[i],num_anchors,num_classes))   
            # 创建边界框预测器,并保存到 self 的属性中。
            setattr(self,f'bbox_{i}',bbox_predictor(idx_to_in_channels[i],num_anchors))      
    
    # 定义前向传播函数,输入参数是输入数据 X。
    def forward(self,X):
        # 初始化锚框、类别预测和边界框预测的列表。
        anchors, cls_preds, bbox_preds = [None] * 5, [None] * 5, [None] * 5
        # 对于每个模块:
        for i in range(5):
            X, anchors[i], cls_preds[i], bbox_preds[i] = blk_forward(
                X, getattr(self, f'blk_{i}'), sizes[i], ratios[i],
                getattr(self, f'cls_{i}'), getattr(self, f'bbox_{i}'))
            # 进行前向计算,并获取输出数据、锚框、类别预测和边界框预测。
        # 将所有模块的锚框沿着第一个维度(索引从 0 开始)拼接起来。
        anchors = torch.cat(anchors, dim=1) # 把所有的anchors并在一起
        # 将所有模块的类别预测结果拼接起来。
        cls_preds = concat_preds(cls_preds)
        # 将类别预测结果重塑为三维张量,第一维度是批量大小,第二维度是 -1 表示自动计算,第三维度是类别数量+1。
        cls_preds = cls_preds.reshape(cls_preds.shape[0],-1,self.num_classes+1)   
        # 将所有模块的边界框预测结果拼接起来。
        bbox_preds = concat_preds(bbox_preds)
        # 返回锚框、类别预测结果和边界框预测结果。
        return anchors, cls_preds, bbox_preds
python 复制代码
# 创建一个模型实例,然后使用它执行前向计算
# 创建一个 TinySSD 类的实例,类别数量为 1。
net = TinySSD(num_classes=1)
# 创建一个全零张量,形状为(32,3,256,256),代表有 32 张 3 通道的 256x256 图像。
X = torch.zeros((32,3,256,256))
# 使用模型进行前向计算,输入是创建的全零张量,输出是锚框、类别预测和边界框预测。
anchors, cls_preds, bbox_preds = net(X)
# 打印锚框的形状,每一张图片都有5444个锚框。
print('output anchors:',anchors.shape) 
# 打印类别预测的形状,32为批量大小,2为类别1加上背景为2。
print('output class preds:',cls_preds.shape) 
# 打印边界框预测的形状,每张图片有5444个锚框,每个锚框有4个坐标,所以是5444*4=21776。
print('output bbox preds:',bbox_preds.shape) 
复制代码
output anchors: torch.Size([1, 5444, 4])
output class preds: torch.Size([32, 5444, 2])
output bbox preds: torch.Size([32, 21776])
python 复制代码
# 读取香蕉检测数据集
# 设置批量大小为32。
batch_size = 32
# 使用 d2l 的 load_data_bananas 函数加载香蕉检测数据集,只使用训练数据集(train_iter)。
# "_" 是一个常用的占位符,表示我们不关心的变量(在这里,我们不关心测试数据集)。
train_iter, _ = d2l.load_data_bananas(batch_size)
复制代码
read 1000 training examples
read 100 validation examples
python 复制代码
# 初始化其参数并定义优化算法
# 选择一个可用的设备进行计算(优先选择 GPU),并创建一个 TinySSD 类的实例,类别数量为1。
device, net = d2l.try_gpu(), TinySSD(num_classes=1)
# 使用随机梯度下降(SGD)优化器,学习率设置为 0.2,权重衰减为 5e-4。optimizer 的参数是模型的所有参数。
trainer = torch.optim.SGD(net.parameters(),lr=0.2,weight_decay=5e-4)
python 复制代码
# 定义损失函数和评价函数
# 交叉熵损失用于类别预测。reduction 参数设置为 'none' 表示保留每个元素的损失。
cls_loss = nn.CrossEntropyLoss(reduction='none')
# L1 损失用于边界框预测。reduction 参数设置为 'none' 表示保留每个元素的损失。
bbox_loss = nn.L1Loss(reduction='none')

# 定义损失计算函数
# cls_preds 和 cls_labels 是类别预测和标签。
# bbox_preds 和 bbox_labels 是边界框预测和标签。
# bbox_masks 是用于过滤无关的负类样本的掩码。
# 函数返回总损失,即类别损失和边界框损失的和。
def calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks):
    batch_size, num_classes = cls_preds.shape[0], cls_preds.shape[2]
    # 计算类别损失,然后调整形状,以便每个样本有一个损失值。
    cls = cls_loss(cls_preds.reshape(-1,num_classes),cls_labels.reshape(-1)).reshape(batch_size,-1).mean(dim=1)      
    # 计算边界框损失,只考虑非背景的预测。
    # mask使得锚框为背景框时为0,就不预测偏移,否则为1,才预测锚框的偏移。
    bbox = bbox_loss(bbox_preds * bbox_masks, bbox_labels * bbox_masks).mean(dim=1)   
    # 返回总损失。
    return cls + bbox

# 定义评价函数
# 计算类别预测的准确性。函数返回正确预测的数量。
def cls_eval(cls_preds,cls_labels):
    return float((cls_preds.argmax(dim=-1).type(cls_labels.dtype)==cls_labels).sum()) 

# 计算边界框预测的准确性。函数返回所有边界框预测和标签之间的绝对差的和。
def bbox_eval(bbox_preds,bbox_labels,bbox_masks):
    return float((torch.abs((bbox_labels - bbox_preds) * bbox_masks)).sum())  
python 复制代码
# 训练模型
# 设置训练周期数和计时器
num_epochs, timer = 20, d2l.Timer()
# 创建一个动画对象,用于动态显示训练过程中的类别错误和边界框的平均绝对误差
animator = d2l.Animator(xlabel='epoch',xlim=[1,num_epochs],
                       legend=['class error','bbox mae'])
# 将网络移动到相应的设备(如GPU)
net = net.to(device)
# 开始训练周期
for epoch in range(num_epochs):
    # 创建一个累加器用于存储训练过程中的指标
    metric = d2l.Accumulator(4)
    # 设置网络为训练模式
    net.train()
    # 遍历训练数据
    for features, target in train_iter:
        # 计时开始
        timer.start()
        # 清零梯度
        trainer.zero_grad()
        # 将特征和目标移动到相应的设备
        X, Y = features.to(device), target.to(device)
        # 前向传播,得到锚框、类别预测和边界框预测
        anchors, cls_preds, bbox_preds = net(X)
        # 获取边界框标签、掩码和类别标签
        bbox_labels, bbox_masks, cls_labels = d2l.multibox_target(anchors, Y)   
        # 计算损失
        l = calc_loss(cls_preds, cls_labels, bbox_preds, bbox_labels, bbox_masks)  
        # 反向传播,计算梯度
        l.mean().backward()
        # 更新参数
        trainer.step()
        # 更新指标
        metric.add(cls_eval(cls_preds,cls_labels),cls_labels.numel(),
                  bbox_eval(bbox_preds,bbox_labels,bbox_masks),
                  bbox_labels.numel())
    # 计算类别错误率和边界框平均绝对误差
    cls_err, bbox_mae = 1 - metric[0] / metric[1], metric[2] / metric[3]
    # 更新动画
    animator.add(epoch + 1, (cls_err, bbox_mae))

# 打印最后的类别错误率和边界框平均绝对误差
print(f'class err {cls_err:.2e}, bbox mae {bbox_mae:.2e}')
# 打印训练速度和设备信息
print(f'{len(train_iter.dataset) / timer.stop():.1f} examples/sec on'
     f'{str(device)}')
复制代码
class err 3.25e-03, bbox mae 3.04e-03
3202.7 examples/sec oncuda:0
python 复制代码
# 预测目标
# 读取待预测的图片,并将其转换为浮点型张量
X = torchvision.io.read_image('01_Data/04_banana.jpg').unsqueeze(0).float()
# 从四维张量中移除单维条目,然后调整各维度的顺序,最后转换为长整型张量
img = X.squeeze(0).permute(1,2,0).long()

# 定义预测函数
def predict(X):
    # 将网络设置为评估模式
    net.eval()
    # 前向传播,得到锚框、类别预测和边界框预测
    anchors, cls_preds, bbox_preds = net(X.to(device))
    # 对类别预测进行softmax操作,得到类别概率,然后调整各维度的顺序
    cls_probs = F.softmax(cls_preds,dim=2).permute(0,2,1) # softmax使得变成概率
    # 调用多框检测函数,得到预测结果
    output = d2l.multibox_detection(cls_probs, bbox_preds, anchors) # 里面调用了NMS   
    # 获取置信度不为-1的预测结果的索引
    idx = [i for i, row in enumerate(output[0]) if row[0] != -1]
    # 返回置信度最大的预测结果
    return output[0, idx] # 只要置信度最大的框

# 对图片进行预测
output = predict(X)
python 复制代码
# 筛选所有置信度不低于0.9的边界框,做为最终输出
# 定义显示边界框的函数
def display(img, output, threshold):
    # 设置图像大小
    d2l.set_figsize((5,5))
    # 展示图像
    fig = d2l.plt.imshow(img)
    # 遍历预测结果
    for row in output:
        # 获取置信度
        score = float(row[1])
        # 如果置信度低于阈值,则忽略该预测结果
        if score < threshold:
            continue
        # 获取图像的高度和宽度
        h, w = img.shape[0:2]
        # 获取预测的边界框,并将其转换为实际像素坐标
        bbox = [row[2:6] * torch.tensor((w, h, w, h), device=row.device)]
        # 显示边界框
        d2l.show_bboxes(fig.axes,bbox,'%.2f' % score,'w')

# 显示置信度不低于0.9的边界框
display(img,output.cpu(),threshold=0.9)

b'\r\n\r\n\r\n

python 复制代码
# 定义平滑L1损失函数
def smooth_11(data, scalar):
    out = []
    # 遍历输入数据
    for i in data:
        # 对于绝对值小于1/scalar^2的数据,采用平方损失
        if abs(i) < 1 / (scalar**2):
            out.append(((scalar * i)**2) / 2)
        # 对于绝对值大于1/scalar^2的数据,采用绝对值损失
        else:
            out.append(abs(i) - 0.5 / (scalar**2))
    # 返回平滑L1损失
    return torch.tensor(out)

# 定义不同的sigma值
sigmas = [10, 1, 0.5]
# 定义不同的线型
lines = ['-','--','-.']
# 定义x值范围
x = torch.arange(-2,2,0.1)
# 设置图像大小
d2l.set_figsize()

# 遍历不同的sigma值
for l, s in zip(lines,sigmas):
    # 计算平滑L1损失
    y = smooth_11(x,scalar=s)
    # 绘制损失函数曲线
    d2l.plt.plot(x,y,l,label='sigma=%.1f' % s)

# 显示图例
d2l.plt.legend()
复制代码
<matplotlib.legend.Legend at 0x1c52f36c908>
python 复制代码
# 定义焦点损失函数
def focal_loss(gamma, x):
    # 根据公式计算焦点损失
    return -(1 - x)**gamma * torch.log(x)

# 定义x值范围
x = torch.arange(0.01, 1, 0.01)
# 遍历不同的gamma值
for l, gamma in zip(lines, [0, 1, 5]):
    # 计算焦点损失
    y = d2l.plt.plot(x, focal_loss(gamma, x), l, label='gamma=%.1f' % gamma)   

# 显示图例
d2l.plt.legend()
复制代码
<matplotlib.legend.Legend at 0x1c52f3145c0>
相关推荐
Bigger11 分钟前
别再拿大炮打蚊子了,我给 Codex 加了一个自动驾驶
人工智能·openai·vibecoding
企业数字化服务商12 分钟前
深圳企业大模型私有化部署后,如何与现有OA/CRM系统无缝集成
大数据·人工智能
向哆哆13 分钟前
打击罂粟种植检测数据集分享(适用于YOLO系列深度学习分类检测任务)
深度学习·yolo·目标检测·分类
咖啡星人k14 分钟前
2026 AI Agent 智能体:ReAct 让 AI 边想边做、把大任务拆成小步骤(MonkeyCode 实战)
人工智能·深度学习·机器学习·语言模型·自然语言处理
狂师19 分钟前
整理了一份 DeepSeek Harness 必备插件清单!
人工智能·开源·deepseek
Blockchina20 分钟前
5个开源AI视频项目怎么选?从脚本、分镜到Shorts和剪映自动化
人工智能·开源·音视频
aneasystone本尊20 分钟前
学习大模型推理的前向传播:Transformer 与注意力机制
人工智能
奈斯先生Vector22 分钟前
从“能调用”到“可运营”:AI Agent 进入多模型时代后的架构升级
java·javascript·数据库·人工智能·算法·架构·aigc
会编程的土豆25 分钟前
从零理解 AI Agent:概念、ReAct、Prompt 五要素与面试要点
人工智能·react.js·prompt