
1. RCNN首先从输入图像中选取若干个提议区域锚框是选取方式的一种并标注它们的类别和边界框如偏移量。然后用卷积神经网络来对每个提议区域锚框进行前向传播以抽取特征。最后用每个提议区域的特征来预测类别和边界框。R-CNN 模型的四个步骤对输入图像使用选择性搜索来选取多个高质量的提议区域。这些提议区域通常是在多个尺度下选取的并具有不同的形状和大小每个提议区域都将被标注类别和真实边框选择一个预训练的卷积神经网络并将其在输出层之前截断。将每个提议区域变形为网络需要的输入尺寸并通过前向传播输出抽取的提议区域特征将每个提议区域的特征连同其标注的类别作为一个样本。训练多个支持向量机对目标分类其中每个支持向量机用来判断样本是否属于某一个类别将每个提议区域的特征连同其标注的边界框作为一个样本训练线性回归模型来预测真实边界框每次选择的锚框的大小是不同的在这种情况下怎样使这些大小不一的锚框变成一个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-CNNR-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 networkRPN模型的其余部分保持不变从而减少区域的生成数量并保证目标检测的精度Faster R-CNN 的改进使用RPN 神经网络来替代 selective searchRoI 的输入是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. YOLOYOLOyolo 也是一个 single-stage 的算法只有一个单神经网络来做预测yolo 也需要锚框这点和 SSD 相同但是 SSD 是对每个像素点生成多个锚框所以在绝大部分情况下两个相邻像素的所生成的锚框的重叠率是相当高的这样就会导致很大的重复计算量。yolo 的想法是尽量让锚框不重叠首先将图片均匀地分成 S * S 块每一块就是一个锚框每一个锚框预测 B 个边缘框考虑到一个锚框中可能包含多个物体所以最终就会产生 S ^ 2 * B 个样本因此速度会远远快于 SSDyolo 在后续的版本V2,V3,V4...中有持续的改进但是核心思想没有变真实的边缘框不会随机的出现真实的边缘框的比例、大小在每个数据集上的出现是有一定的规律的在知道有一定的规律的时候就可以使用聚类算法将这个规律找出来给定一个数据集先分析数据集中的统计信息然后找出边缘框出现的规律这样之后在生成锚框的时候就会有先验知识从而进一步做出优化上图中表示 yolo v3 的直线底端表示论文中的原始精度顶端表示通过改进之后所能达到的最大精度非锚框算法center net基于非锚框的目标检测center net 的优点在于简单center net 会对每个像素做预测用 FCN 对每个像素做预测类似于图像分割中用 FCN 对每个像素标号预测该像素点是不是真实边缘框的中心点将目标检测的边缘框换算成基于每个像素的标号然后对每个像素做预测就免去了一些锚框相关的操作4. 多尺度目标检测实现%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) # 最后一个元素为通道数通道数为3561 728 (561, 728, 3)# 在特征图(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,sizess,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# 探测小目标 # 调用display_anchors函数输入参数是特征图的宽度fmap_w4、高度fmap_h4以及锚框的尺寸s[0.15]。 # 这行代码将在特征图上生成并显示锚框特征图的尺寸是4x4锚框的尺寸是0.15相对于特征图的尺寸。 # 对于每个特征图的像素都会生成一个以该像素为中心尺寸为0.15的锚框。 display_anchors(fmap_w4,fmap_h4,s[0.15])# 将特征图的高度和宽度减小一半然后使用较大的锚框来检测较大的目标 display_anchors(fmap_w2,fmap_h2,s[0.4])# 将特征图的高度和宽度较小一半然后将锚框的尺度增加到0.8 display_anchors(fmap_w1,fmap_h1,s[0.8])5. 单发多框检测(SSD)%matplotlib inline import torch import torchvision from torch import nn from torch.nn import functional as F from d2l import torch as d2l# 类别预测层 # 定义一个函数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_size3, padding1)# 边界框预测层 # 定义一个函数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_size3, padding1)# 连接多尺度的预测 # 定义一个函数forward输入参数是输入数据x和一个网络层block。 def forward(x, block): # 返回通过网络层block处理输入数据x后的结果。 return block(x)# 使用全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是因为每个锚框需要预测101个类别的概率共有5个锚框所以通道数是5*(101)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])# 定义一个函数flatten_pred输入参数是预测结果pred。 def flatten_pred(pred): # pred.permute(0,2,3,1)通道数放在最后 # start_dim1把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_dim1)# 定义一个函数concat_preds输入参数是多个预测结果的列表preds。 def concat_preds(preds): #[print(flatten_pred(p).shape) for p in preds] # 返回连接后的预测结果。首先使用列表推导式和flatten_pred函数拉平每个预测结果然后使用torch.cat方法在dim1的维度上连接所有的预测结果。 return torch.cat([flatten_pred(p) for p in preds], dim1) # 打印连接后的预测结果的形状。这里输入的是两个预测结果Y1和Y2输出的形状是(2, 35300)表示有2张图片每张图片有25300个预测结果。 print(concat_preds([Y1, Y2]).shape)torch.Size([2, 25300])# 高和宽减半块 # 定义一个函数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_size3,padding1)) # 添加一个批量归一化层输入通道数是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])# 基本网络块 # 定义一个函数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[i1]。 blk.append(down_sample_blk(num_filters[i],num_filters[i1])) # 返回一个顺序容器其中包含了所有的网络层。 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])# 完整的单发多框检测模型由五个模块组成 # 定义一个函数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# 为每个块定义前向计算 # 定义一个函数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,sizessize,ratiosratio) # 使用类别预测器cls_predictor预测每个锚框的类别。 cls_preds cls_predictor(Y) # 使用边界框预测器bbox_predictor预测每个锚框的边界框。 bbox_preds bbox_predictor(Y) # 返回 卷积层输出、卷积层输出上生成的锚框、每一个锚框的类别的预测每一个锚框到真实边缘框的预测 # 返回输出数据Y、生成的锚框、类别预测结果和边界框预测结果。 return (Y,anchors,cls_preds,bbox_preds)# 超参数 # 小的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# 定义完整的模型 # 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,fblk_{i},get_blk(i)) # 创建类别预测器并保存到 self 的属性中。 setattr(self,fcls_{i},cls_predictor(idx_to_in_channels[i],num_anchors,num_classes)) # 创建边界框预测器并保存到 self 的属性中。 setattr(self,fbbox_{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, fblk_{i}), sizes[i], ratios[i], getattr(self, fcls_{i}), getattr(self, fbbox_{i})) # 进行前向计算并获取输出数据、锚框、类别预测和边界框预测。 # 将所有模块的锚框沿着第一个维度索引从 0 开始拼接起来。 anchors torch.cat(anchors, dim1) # 把所有的anchors并在一起 # 将所有模块的类别预测结果拼接起来。 cls_preds concat_preds(cls_preds) # 将类别预测结果重塑为三维张量第一维度是批量大小第二维度是 -1 表示自动计算第三维度是类别数量1。 cls_preds cls_preds.reshape(cls_preds.shape[0],-1,self.num_classes1) # 将所有模块的边界框预测结果拼接起来。 bbox_preds concat_preds(bbox_preds) # 返回锚框、类别预测结果和边界框预测结果。 return anchors, cls_preds, bbox_preds# 创建一个模型实例然后使用它执行前向计算 # 创建一个 TinySSD 类的实例类别数量为 1。 net TinySSD(num_classes1) # 创建一个全零张量形状为323256256代表有 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*421776。 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])# 读取香蕉检测数据集 # 设置批量大小为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# 初始化其参数并定义优化算法 # 选择一个可用的设备进行计算优先选择 GPU并创建一个 TinySSD 类的实例类别数量为1。 device, net d2l.try_gpu(), TinySSD(num_classes1) # 使用随机梯度下降SGD优化器学习率设置为 0.2权重衰减为 5e-4。optimizer 的参数是模型的所有参数。 trainer torch.optim.SGD(net.parameters(),lr0.2,weight_decay5e-4)# 定义损失函数和评价函数 # 交叉熵损失用于类别预测。reduction 参数设置为 none 表示保留每个元素的损失。 cls_loss nn.CrossEntropyLoss(reductionnone) # L1 损失用于边界框预测。reduction 参数设置为 none 表示保留每个元素的损失。 bbox_loss nn.L1Loss(reductionnone) # 定义损失计算函数 # 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(dim1) # 计算边界框损失只考虑非背景的预测。 # mask使得锚框为背景框时为0就不预测偏移否则为1才预测锚框的偏移。 bbox bbox_loss(bbox_preds * bbox_masks, bbox_labels * bbox_masks).mean(dim1) # 返回总损失。 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())# 训练模型 # 设置训练周期数和计时器 num_epochs, timer 20, d2l.Timer() # 创建一个动画对象用于动态显示训练过程中的类别错误和边界框的平均绝对误差 animator d2l.Animator(xlabelepoch,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(fclass 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# 预测目标 # 读取待预测的图片并将其转换为浮点型张量 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,dim2).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)# 筛选所有置信度不低于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), devicerow.device)] # 显示边界框 d2l.show_bboxes(fig.axes,bbox,%.2f % score,w) # 显示置信度不低于0.9的边界框 display(img,output.cpu(),threshold0.9)b\r\n\r\n\r\n# 定义平滑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,scalars) # 绘制损失函数曲线 d2l.plt.plot(x,y,l,labelsigma%.1f % s) # 显示图例 d2l.plt.legend()matplotlib.legend.Legend at 0x1c52f36c908# 定义焦点损失函数 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, labelgamma%.1f % gamma) # 显示图例 d2l.plt.legend()matplotlib.legend.Legend at 0x1c52f3145c0