那个合作网站做360推广比较好,地域性旅游网站建设系统结构,企业门户网站什么意思,辽宁定制网站建设推广论文网址#xff1a;https://arxiv.org/pdf/1506.02640
训练集博客链接#xff1a;目标检测实战篇1——数据集介绍(PASCAL VOC#xff0c;MS COCO)-CSDN博客
代码文件#xff1a;在我资源里#xff0c;但是好像还在审核#xff0c;大家可以先可以#xff0c;如果没有的…论文网址https://arxiv.org/pdf/1506.02640
训练集博客链接目标检测实战篇1——数据集介绍(PASCAL VOCMS COCO)-CSDN博客
代码文件在我资源里但是好像还在审核大家可以先可以如果没有的话就评论我发给你。
0、作者
Joseph Redmon是发表这篇论文的作者最著名的四篇论文为YOLOv1YOLO9000YOLOV2、YOLOv3和Xron-Net与轻量化相关作者毕业于华盛顿大学。很厉害的一位人。大家可以去了解一下。
一、前言和预备知识
之前我所发布的都是分类问题解决的概念就是输入一张图片然后输出这个图片中的内容是什么类别这种方法在很多问题上以及能够解决了但是要清楚这个物体在图片中那个位置这还是很难解决的所以就引入了目标检测的概念目标检测就是做到不仅判断物体的类别还可以得出物体所在位置而目前最热门的目标检测的模型也就是YOLO系列了。
YOLO系列的模型是为了解决当时目标检测模型的帧率太低而提出来的模型。英文全称是You only look once。
深度学习目标检测算法分类
1two-stage 两个阶段的检测模型举例 Faster-RCNN Mask-Rcnn系列 (2) one-stage 一个阶段的检测YOLO系列
这两个主要区别可以简单理解为两个阶段有一个选择预选框和物体分类的一个过程而单阶段的将检测问题转换为一个回归问题。
YOLO是目前比较流行的目标检测算法速度快并且结构简单。
YOLOV1是以Joseph Redmon为首的大佬们于2015年提出的一种新的目标检测算法。它与之前的目标检测算法如R-CNN等不同之处在于R-CNN等目标检测算法是两阶段算法 步骤为先在图片上生成候选框然后利用分类器对这些候选框进行逐一的判断而YOLOv1是一阶段算法是端到端的算法它把目标检测问题看作回归问题将图片输入单一的神经网络然后就输出得到了图片的物体边界框boundingbox以及分类概率等信息。
总结YOLOv1直接从输入的图像仅仅经过一个神经网络直接得到一些bounding box位置坐标以及每个bounding box对所有类别的一个概率情况因为整个的检测过程仅仅有一个网络所以可以直接进行端到端的优化而无需像Faster R-CNN的分阶段的优化。
end-to-end(端到端)指的是一个过程输入原始数据输出最后结果。之前的网络Fast RCNN等这种网络分为两个阶段一个是预选框的生成和目标分类与边界框回归具体内容大家可以自行理解。
YOLO的核心思想就是把目标检测转变为一个回归问题利用整张图作为网络的输入仅仅经过一个神经网络得到bounding box边界框 的位置及其所属的类别。
二、网络结构 YOLOv1的网络结构简单清晰是一个最传统的one-stage的卷积神经网络。
网络输入448*448*3的彩色图片。
中间层由若干卷积层和最大池化层组成用于提取图片的抽象特征。
全连接层由两个全连接层组成用来预测目标的位置和类别概率值。
网络输出7*7*30的预测结果。
三、网络细节
YOLOv1采用的是“分而治之”的策略将一张图片平均分成7×7个网格每个网格分别负责预测中心点落在该网格内的目标。回忆一下在Faster R-CNN中是通过一个RPN来获得目标的感兴趣区域这种方法精度高但是需要额外再训练一个RPN网络这无疑增加了训练的负担。在YOLOv1中通过划分得到了7×7个网格这49个网格就相当于是目标的感兴趣区域。通过这种方式我们就不需要再额外设计一个RPN网络这正是YOLOv1作为单阶段网络的简单快捷之处 四、预测阶段讲解前向推断
就是模型训练完后向训练好的模型输入图片然后得到最后结果的一个过程。
YOLOv1训练完后是一个深度卷积神经网络网络结构如下图所示 将网络当成一个黑箱子输入的是一个448*448*3的RGB图像输出的是一个7*7*30的向量。
网络结构很简单大概就是卷积、池化、全连接层一目了然。
而最后输出的向量包含了类别、框、置信度等结果而我们只需要解释这个向量就得到最后的结果了。
下面对最后输出的向量进行一个分析当我们输入一张图片的时候最后会生成7*7*30的向量如何去理解呢可以将这个向量看做成7*7个1*30的向量也就是49个1*30的向量这里的每个1*30的向量对应于前面的一个grid cell将向量拆分为两个预测框的坐标以及预测框包含物体的置信度和20个类别的条件概率。其中每个gridcell只会预测一个物体也因此暴露出YOLOv1在小目标上检测的缺陷。然后预测中又引入了NMS向量对上述的98个框进行筛选最后才可以得出我们的结果大家读到这里可能还是有一些不能理解的所以请大家观看代码代码都有注释。
import numpy as np
import torch
from PIL import ImageFont, ImageDraw
from cv2 import cv2
from matplotlib import pyplot as plt
# import cv2
from torchvision.transforms import ToTensorfrom draw_rectangle import draw
from new_resnet import resnet50# voc数据集的类别信息这里转换成字典形式 键是真名 后面是值
classes {aeroplane: 0, bicycle: 1, bird: 2, boat: 3, bottle: 4, bus: 5, car: 6, cat: 7, chair: 8, cow: 9, diningtable: 10, dog: 11, horse: 12, motorbike: 13, person: 14, pottedplant: 15, sheep: 16, sofa: 17, train: 18, tvmonitor: 19}# 测试图片的路径
img_root rD:\\program\\Object_detection\\YOLOV1-pytorch-main\\test.jpg
# 网络模型
model resnet50()
# 加载权重就是在train.py中训练生成的权重文件yolo.pth
model.load_state_dict(torch.load(rD:\\program\\Object_detection\\YOLOV1-pytorch-main\\yolo.pth))
# 测试模式
model.eval()# 设置置信度 超参数设置
confident 0.2
# 设置iou阈值
iou_con 0.4# 类别信息这里写的都是voc数据集的如果是自己的数据集需要更改
VOC_CLASSES (aeroplane, bicycle, bird, boat,bottle, bus, car, cat, chair,cow, diningtable, dog, horse,motorbike, person, pottedplant,sheep, sofa, train, tvmonitor)
# 类别总数20
CLASS_NUM len(VOC_CLASSES) # 获得元组长度
注意预测和训练的时候是不同的训练的时候是有标签参考的在测试的时候会直接输出两个预测框
保留置信度比较大的再通过NMS处理得到最终的预测结果不要跟训练阶段搞混了
# target 7*7*30 值域为0-1
class Pred():# 参数初始化def __init__(self, model, img_root): # 传入预测模型以及需要预测图片的路径self.model modelself.img_root img_rootdef result(self):# 读取测试的图像img cv2.imread(self.img_root)# 获取高宽信息h, w, _ img.shape # 行数 列数# 调整图像大小image cv2.resize(img, (448, 448)) # cv2.resize(img,(448,448)) 第一个参数是需要调整的图像第二个是需要调整到的大小# CV2读取的图像是BGR这里转回RGB模式img cv2.cvtColor(image, cv2.COLOR_BGR2RGB)# 图像均值mean (123, 117, 104) # RGB 训练数据集上得到的图象均值# 减去均值进行标准化操作 OPENCV读入的图片默认是numpy数组的类型img img - np.array(mean, dtypenp.float32)# 创建数据增强函数transform ToTensor()# 图像转为tensor因为cv2读取的图像是numpy格式img transform(img)# 输入要求是BCHW加一个batch维度 输入一般要求都是四个维度不管什么代码中也就是增加一个batchsize 的维度img img.unsqueeze(0)# 图像输入模型返回值为1*7*7*30的张量Result self.model(img)# 获取目标的边框信息bbox self.Decode(Result)# 非极大值抑制处理bboxes self.NMS(bbox) # n*6 bbox坐标是基于7*7网格需要将其转换成448# draw(image, bboxes, classes)if len(bboxes) 0:print(未识别到任何物体)print(尝试减小 confident 以及 iou_con)print(也可能是由于训练不充分可在训练时将epoch增大) for i in range(0, len(bboxes)): # bbox坐标将其转换为原图像的分辨率bboxes[i][0] bboxes[i][0] * 64bboxes[i][1] bboxes[i][1] * 64bboxes[i][2] bboxes[i][2] * 64bboxes[i][3] bboxes[i][3] * 64x1 bboxes[i][0].item() # 后面加item()是因为画框时输入的数据不可一味tensor类型x2 bboxes[i][1].item()y1 bboxes[i][2].item()y2 bboxes[i][3].item()class_name bboxes[i][5].item()print(x1, x2, y1, y2, VOC_CLASSES[int(class_name)])cv2.rectangle(image, (int(x1), int(y1)), (int(x2), int(y2)), (144, 144, 255)) # 画框plt.imsave(test_001.jpg, image)# cv2.imwrite(img, image)# cv2.imshow(img, image)# cv2.waitKey(0)# 接受的result的形状为1*7*7*30def Decode(self, result): # 接受模型输出结果# 去掉batch维度 从1*7*7*30的张量变成7*7*30result result.squeeze() # 函数作用将张量中所有维度为1的都给去掉# 提取置信度信息并在取出的置信度信息向量的最后一个维度增加一维跟后面匹配result[:, :, 4]的形状为7*7,最后变为7*7*1grid_ceil1 result[:, :, 4].unsqueeze(2)# 同上grid_ceil2 result[:, :, 9].unsqueeze(2)# 两个置信度信息按照维度2拼接grid_ceil_con torch.cat((grid_ceil1, grid_ceil2), 2)# 按照第二个维度进行最大值求取一个grid ceil两个bbox两个confidence也就是找置信度比较大的那个形状都是7*7grid_ceil_con, grid_ceil_index grid_ceil_con.max(2)# 找出一个gird cell中预测类别最大的物体的索引和预测条件类别概率class_p, class_index result[:, :, 10:].max(2)# 计算出物体的真实概率类别最大的物体乘上置信度比较大的那个框得到最终的真实物体类别概率class_confidence class_p * grid_ceil_con# 定义一个张量记录位置信息bbox_info torch.zeros(7, 7, 6)for i in range(0, 7):for j in range(0, 7):# 获取置信度比较大的索引位置bbox_index grid_ceil_index[i, j]# 第一个面存储的是# 把置信度比较大的那个框的位置信息保存到bbox_info中另一个直接抛弃bbox_info[i, j, :5] result[i, j, (bbox_index * 5):(bbox_index1) * 5]# 真实目标概率bbox_info[:, :, 4] class_confidence# 类别信息bbox_info[:, :, 5] class_index# 返回预测的结果,7*7*6 6 bbox4个信息类别概率类别代号return bbox_info# 非极大值抑制处理按照类别处理bbox为Decode获取的预测框的位置信息和类别概率和类别信息def NMS(self, bbox, iou_coniou_con):for i in range(0, 7):for j in range(0, 7):# xc bbox[i, j, 0]# yc bbox[i, j, 1]# w bbox[i, j, 2] * 7# h bbox[i, j, 3] * 7# Xc i xc# Yc j yc# xmin Xc - w/2# xmax Xc w/2# ymin Yc - h/2# ymax Yc h/2# 注意目前bbox的四个坐标是以grid ceil的左上角为坐标原点而且单位不一致中心点偏移的坐标是归一化为(0-7),宽高是(0-7),单位不一致全部归一化为(0-7)# 计算预测框的左上角右下角相对于7*7网格的位置xmin j bbox[i, j, 0] - bbox[i, j, 2] * 7 / 2 # xminxmax j bbox[i, j, 0] bbox[i, j, 2] * 7 / 2 # xmaxymin i bbox[i, j, 1] - bbox[i, j, 3] * 7 / 2 # yminymax i bbox[i, j, 1] bbox[i, j, 3] * 7 / 2 # ymaxbbox[i, j, 0] xminbbox[i, j, 1] xmaxbbox[i, j, 2] yminbbox[i, j, 3] ymax# 调整形状bbox本来就是(49*6)这里感觉没必要bbox bbox.view(-1, 6)# 存放最终需要保留的预测框bboxes []# 取出每个gird cell中的类别信息返回一个列表ori_class_index bbox[:, 5]# 按照类别进行排序从高到低返回的是排序后的类别列表和对应的索引位置,如下类别排序tensor([ 1., 1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2., 2.,3., 3., 3., 4., 4., 4., 4., 5., 5., 5., 6., 6., 6., 6.,6., 6., 6., 6., 7., 8., 8., 8., 8., 8., 14., 14., 14., 14.,14., 14., 14., 15., 15., 16., 17.], grad_fnSortBackward0)位置索引tensor([48, 47, 46, 45, 44, 43, 42, 7, 8, 22, 11, 16, 14, 15, 24, 20, 1, 2,6, 0, 13, 23, 25, 27, 32, 39, 38, 35, 33, 31, 30, 28, 3, 26, 10, 19,9, 12, 29, 41, 40, 21, 37, 36, 34, 18, 17, 5, 4])class_index, class_order ori_class_index.sort(dim0, descendingFalse)# class_index是一个tensor这里把他转为列表形式class_index class_index.tolist() # .tolist()转成一个列表# 根据排序后的索引更改bbox排列顺序bbox bbox[class_order, :]a 0for i in range(0, CLASS_NUM):# 统计目标数量即某个类别出现在grid cell中的次数num class_index.count(i)# 预测框中没有这个类别就直接跳过if num 0:continue# 提取同一类别的所有信息x bbox[a:anum, :]# 提取真实类别概率信息score x[:, 4]# 提取出来的某一类别按照真实类别概率信息高度排序递减score_index, score_order score.sort(dim0, descendingTrue)# 根据排序后的结果更改真实类别的概率排布y x[score_order, :]# 先看排在第一位的物体的概率是否大有给定的阈值不满足就不看这个类别了丢弃全部的预测框if y[0, 4] confident:for k in range(0, num):# 真实类别概率排序后的y_score y[:, 4]# 对真实类别概率重新排序保证排列顺序依照递减其实跟上面一样的多此一举_, y_score_order y_score.sort(dim0, descendingTrue)y y[y_score_order, :]# 判断概率是否大于0if y[k, 4] 0:# 计算预测框的面积area0 (y[k, 1] - y[k, 0]) * (y[k, 3] - y[k, 2])for j in range(k1, num):# 计算剩余的预测框的面积area1 (y[j, 1] - y[j, 0]) * (y[j, 3] - y[j, 2])x1 max(y[k, 0], y[j, 0])x2 min(y[k, 1], y[j, 1])y1 max(y[k, 2], y[j, 2])y2 min(y[k, 3], y[j, 3])w x2 - x1h y2 - y1if w 0 or h 0:w 0h 0inter w * h# 计算与真实目标概率最大的那个框的iouiou inter / (area0 area1 - inter)# iou大于一定值则认为两个bbox识别了同一物体删除置信度较小的bbox# 同时物体类别概率小于一定值也认为不包含物体if iou iou_con or y[j, 4] confident:y[j, 4] 0for mask in range(0, num):if y[mask, 4] 0:bboxes.append(y[mask])# 进入下个类别a num a# 返回最终预测的框return bboxesif __name__ __main__:Pred Pred(model, img_root)Pred.result()
上述就是预测的代码通过阅读代码的方式能够清晰地理解到位YOLOv1模型的预测过程生成一个7*7*30的向量对应于98个框然后通过系列算法如NMS进行筛选出我们最后结果。
五、训练阶段 训练阶段我首先主要解释一下几个问题首先是YOLOv1是端到端的一个网络也就是输入的是原图输出的是一个向量而训练的过程包括两个过程一个是前向传播另一个就是反向传播通过不断迭代使梯度下降进而使损失函数最小化这个过程。训练的流程大致为输入batchsize张图片得出batchsize*7*7*30的一个张量将该张量与标签batchsize*7*7*30的向量计算出损失为多少然后根据这个损失对网络的参数进行梯度计算不断迭代不断优化参数这样的一个过程。我觉得这样理解好理解的多。
这个代码大致流程可以分为以下部分
1、超参数初始化
2、数据初始化构造数据迭代器因为原本一张图片对应的标签并不是7*7*30的一个向量因此要对数据进行预处理把每一个图片的标签变成7*7*30的一个格式。
3、网络初始化我这里给的代码使ResNet50的网络这个网络也叫做backbone大家以后会经常看到的网络结构差不多然后输入是1*7*7*30输出就是1*7*7*30的这样一个结构
4、上述都可以看成初始化过程然后就开始训练也就是前向传播反向传播参数优化这样迭代的一个过程最后保存参数。
这块的代码如下图所示都带有注释的。
from yoloData import yoloDataset # 数据集处理
from yoloLoss import yoloLoss # YOLOv1的损失函数
from new_resnet import resnet50 # backbone采用的是resnet
from torchvision import models
import torchvision.transforms as transforms
from torch.utils.data import DataLoader # 这四个torch
import torch
voc——————.txt 第一个为图片的路径 后边的五个数字依次是 位置坐标和该框所属于的类别。
device cuda # 设备名称
file_root rD:\cv\第三章\YOLO\yolov1-main\VOCtrainval_11-May-2012\VOCdevkit\VOC2012\JPEGImages\\ # 最后两个\\十分重要
batch_size 4 # batch_size
learning_rate 0.001 # 学习率
num_epochs 100# 自定义训练数据集
train_dataset yoloDataset(img_rootfile_root, list_filevoctrain.txt, trainTrue, transform[transforms.ToTensor()]) # 对txt标签数据处理
# 加载自定义的训练数据集
train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue, num_workers0)
# 自定义测试数据集
test_dataset yoloDataset(img_rootfile_root, list_filevoctest.txt, trainFalse, transform[transforms.ToTensor()])
# 加载自定义的测试数据集
test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleTrue, num_workers0)
print(the dataset has %d images % (len(train_dataset)))
下面这段代码主要适用于迁移学习训练可以将预训练的ResNet-50模型的参数赋值给新的网络以加快训练速度和提高准确性。# 创建改编的ResNet50
net resnet50()
# 放到GPU上
net net.cuda()
# 直接使用pytorch加载resnet50,带预训练权重
resnet models.resnet50(pretrainedTrue) # torchvison库中的网络
# 获取resnet的训练参数
new_state_dict resnet.state_dict()
# 获取刚创建的改编的resnet50()的参数
op net.state_dict()# 无论名称是否相同都可以使用
for new_state_dict_num, new_state_dict_value in enumerate(new_state_dict.values()):# op.keys()表示获取模型参数字典中的所有键值for op_num, op_key in enumerate(op.keys()):# 320个key中不需要最后的全连接层的两个参数if op_num new_state_dict_num and op_num 317:op[op_key] new_state_dict_value
# 将预训练好的参数放入改编的ResNet的网络中加快训练速度
net.load_state_dict(op)print(cuda, torch.cuda.current_device(), torch.cuda.device_count()) # 确认一下cuda的设备# 创建损失函数
criterion yoloLoss(7, 2, 5, 0.5)
# 放到GPU上
criterion criterion.to(device)
# 训练前需要加入的语句一般有Dropout()的时候要加
net.train()# 里面存字典
params []
# net.named_parameters()是一个PyTorch函数它返回一个包含模型中所有需要学习的参数即权重和偏置项及其名称的迭代器。
params_dict dict(net.named_parameters())
for key, value in params_dict.items():# 把字典放到列表中这个“”可以理解为appendparams [{params: [value], lr:learning_rate}]# 定义优化器 “随机梯度下降”
optimizer torch.optim.SGD(# 上面已经将模型参数打包成字典了这里不需要用net.parameters()了params,# 学习率lrlearning_rate,# 动量momentum0.9,# 正则化weight_decay5e-4)# Windows环境下使用多进程时需要调用的函数我训练的时候没用。在Windows下使用多进程需要先将Python脚本打包成exe文件而freeze_support()的作用就是冻结可执行文件的代码确保在Windows下正常运行多进程。
# torch.multiprocessing.freeze_support() # 多进程相关 猜测是使用多显卡训练需要
这里解释下自己定义参数列表和直接使用net.parameter()的区别在大多数情况下直接使用net.parameters()和将模型参数放到字典中是没有区别的因为net.parameters()本身就是一个包含模型所有参数的列表。但是如果我们想要对不同的参数设置不同的超参数那么将模型参数放到字典中会更加方便。使用net.parameters()的话我们需要手动区分不同的参数再分别进行超参数的设置。而将模型参数放到字典中后我们可以直接对每个参数设置对应的超参数更加简洁和灵活。举个例子如果我们想要对卷积层和全连接层设置不同的学习率使用net.parameters()的话我们需要手动区分哪些参数属于卷积层哪些参数属于全连接层然后分别对这两部分参数设置不同的学习率。而将模型参数放到字典中后我们可以直接对卷积层和全连接层的参数分别设置不同的学习率更加方便和清晰。# 开始训练
for epoch in range(num_epochs):# 这个地方形成习惯因为网络可能会用到Dropout和batchnormnet.train()# 调整学习率if epoch 60:learning_rate 0.0001if epoch 80:learning_rate 0.00001# optimizer.param_groups 返回一个包含优化器参数分组信息的列表每个分组是一个字典主要包含以下键值# params当前参数分组中需要更新的参数列表如网络的权重偏置等。# lr当前参数分组的学习率。就是我们要提取更新的# momentum当前参数分组的动量参数。# weight_decay当前参数分组的权重衰减参数。for param_group in optimizer.param_groups:param_group[lr] learning_rate # 更改全部的学习率print(\n\nStarting epoch %d / %d % (epoch 1, num_epochs))print(Learning Rate for this epoch: {}.format(learning_rate))# 计算损失total_loss 0.# 开始迭代训练for i, (images, target) in enumerate(train_loader):images, target images.cuda(), target.cuda()pred net(images)# 创建损失函数loss criterion(pred, target)total_loss loss.item()# 梯度清零optimizer.zero_grad()# 反向传播loss.backward()# 参数优化optimizer.step()if (i 1) % 5 0:print(Epoch [%d/%d], Iter [%d/%d] Loss: %.4f, average_loss: %.4f % (epoch 1, num_epochs, i 1, len(train_loader), loss.item(), total_loss / (i 1)))# 开始测试validation_loss 0.0net.eval()for i, (images, target) in enumerate(test_loader):images, target images.cuda(), target.cuda()# 输入图像pred net(images)# 计算损失loss criterion(pred, target)# 累加损失validation_loss loss.item()# 计算平均lossvalidation_loss / len(test_loader)best_test_loss validation_lossprint(get best test loss %.5f % best_test_loss)# 保存模型参数torch.save(net.state_dict(), yolo.pth)
六、损失函数 可以大致看成四个部分
第一部分表示负责检测物体的bounding box中心点定位误差要和ground truth尽可能拟合x xx带上标的是标注值不带上标的是预测值。
第二表示表示负责检测物体的bounding box的宽高定位误差加根号是为了使得对小框的误差更敏感。
第三部分 第四部分是负责检测物体的grid的分类误差。
损失函数的代码带注释
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import warningswarnings.filterwarnings(ignore) # 忽略警告消息
CLASS_NUM 20 # 使用自己的数据集时需要更改class yoloLoss(nn.Module):def __init__(self, S, B, l_coord, l_noobj):# 一般而言 l_coord 5 l_noobj 0.5super(yoloLoss, self).__init__()# 网格数self.S S # S 7# bounding box数量self.B B # B 2# 权重系数self.l_coord l_coord# 权重系数self.l_noobj l_noobjdef compute_iou(self, box1, box2): # box1(2,4) box2(1,4)N box1.size(0) # 2M box2.size(0) # 1lt torch.max( # 返回张量所有元素的最大值# [N,2] - [N,1,2] - [N,M,2]box1[:, :2].unsqueeze(1).expand(N, M, 2),# [M,2] - [1,M,2] - [N,M,2]box2[:, :2].unsqueeze(0).expand(N, M, 2),)rb torch.min(# [N,2] - [N,1,2] - [N,M,2]box1[:, 2:].unsqueeze(1).expand(N, M, 2),# [M,2] - [1,M,2] - [N,M,2]box2[:, 2:].unsqueeze(0).expand(N, M, 2),)wh rb - lt # [N,M,2]wh[wh 0] 0 # clip at 0inter wh[:, :, 0] * wh[:, :, 1] # [N,M] 重复面积area1 (box1[:, 2] - box1[:, 0]) * (box1[:, 3] - box1[:, 1]) # [N,]area2 (box2[:, 2] - box2[:, 0]) * (box2[:, 3] - box2[:, 1]) # [M,]area1 area1.unsqueeze(1).expand_as(inter) # [N,] - [N,1] - [N,M]area2 area2.unsqueeze(0).expand_as(inter) # [M,] - [1,M] - [N,M]iou inter / (area1 area2 - inter)# iou的形状是(2,1)里面存放的是两个框的iou的值return iou # [2,1]def forward(self, pred_tensor, target_tensor):pred_tensor: (tensor) size(batchsize,7,7,30)target_tensor: (tensor) size(batchsize,7,7,30)就是在yoloData中制作的标签# batchsize大小N pred_tensor.size()[0]# 判断目标在哪个网格输出B*7*7的矩阵有目标的地方为True其他地方为false,这里就用了第4位第9位和第4位一样就没判断coo_mask target_tensor[:, :, :, 4] 0# 判断目标不在那个网格输出B*7*7的矩阵没有目标的地方为True其他地方为falsenoo_mask target_tensor[:, :, :, 4] 0# 将 coo_mask tensor 在最后一个维度上增加一维并将其扩展为与 target_tensor tensor 相同的形状得到含物体的坐标等信息大小为batchsize*7*7*30coo_mask coo_mask.unsqueeze(-1).expand_as(target_tensor)# 将 noo_mask 在最后一个维度上增加一维并将其扩展为与 target_tensor tensor 相同的形状得到不含物体的坐标等信息大小为batchsize*7*7*30noo_mask noo_mask.unsqueeze(-1).expand_as(target_tensor)# 根据label的信息从预测的张量取出对应位置的网格的30个信息按照出现序号拼接成以一维张量这里只取包含目标的coo_pred pred_tensor[coo_mask].view(-1, int(CLASS_NUM 10))# 所有的box的位置坐标和置信度放到box_pred中塑造成X行5列-1表示自动计算一个box包含5个值box_pred coo_pred[:, :10].contiguous().view(-1, 5)# 类别信息class_pred coo_pred[:, 10:] # [n_coord, 20]# pred_tensor[coo_mask]把pred_tensor中有物体的那个网格对应的30个向量拿出来这里对应的是label向量只计算有目标的coo_target target_tensor[coo_mask].view(-1, int(CLASS_NUM 10))box_target coo_target[:, :10].contiguous().view(-1, 5)class_target coo_target[:, 10:]# 不包含物体grid ceil的置信度损失,这里是label的输出的向量。noo_pred pred_tensor[noo_mask].view(-1, int(CLASS_NUM 10))noo_target target_tensor[noo_mask].view(-1, int(CLASS_NUM 10))# 创建一个跟noo_pred相同形状的张量形状为x,30里面都是全0或全1再使用bool将里面的0或1转为true和falsenoo_pred_mask torch.cuda.ByteTensor(noo_pred.size()).bool()# 把创建的noo_pred_mask全部改成false因为这里对应的是没有目标的张量noo_pred_mask.zero_()# 把不包含目标的张量的置信度位置置为1noo_pred_mask[:, 4] 1noo_pred_mask[:, 9] 1# 跟上面的pred_tensor[coo_mask]一个意思把不包含目标的置信度提取出来拼接成一维张量noo_pred_c noo_pred[noo_pred_mask]# 同noo_pred_cnoo_target_c noo_target[noo_pred_mask]# 计算loss让预测的值越小越好因为不包含目标置信度越为0越好nooobj_loss F.mse_loss(noo_pred_c, noo_target_c, size_averageFalse) # 均方误差# 注意上面计算的不包含目标的损失只计算了置信度其他的都没管计算包含目标的损失位置损失类别损失# 先创建两个张量用于后面匹配预测的两个编辑框一个负责预测一个不负责预测# 创建一跟box_target相同的张量,这里用来匹配后面负责预测的框coo_response_mask torch.cuda.ByteTensor(box_target.size()).bool()# 全部置为Falsecoo_response_mask.zero_() # 全部元素置False# 创建一跟box_target相同的张量这里用来匹配不负责预测的框no_coo_response_mask torch.cuda.ByteTensor(box_target.size()).bool()# 全部置为Falseno_coo_response_mask.zero_()# 创建一个全0张量匹配后面的预测框的ioubox_target_iou torch.zeros(box_target.size()).cuda()# 遍历每一个标注框每次遍历两个是因为一个标注框对应有两个预测框要跟他匹配box1 预测框 box2 ground truth# box_target.size()[0]有多少bbox并且一次取两个bbox因为两个bbox才是一个完整的预测框for i in range(0, box_target.size()[0], 2): ## 第i个grid ceil对应的两个bboxbox1 box_pred[i:i 2]# 创建一个和box1大小(2,5)相同的浮点型张量用来存储坐标这里代码使用的torch版本可能比较老其实Variable可以省略的box1_xyxy Variable(torch.FloatTensor(box1.size()))# box1_xyxy[:, :2]为预测框中心点坐标相对于所在grid cell左上角的偏移前面在数据处理的时候讲过label里面的值是归一化为(0-7)的# 因此这里得反归一化成跟宽高一样比例归一化到(0-1),减去宽高的一半得到预测框的左上角的坐标相对于他所在的grid cell的左上角的偏移量box1_xyxy[:, :2] box1[:, :2] / float(self.S) - 0.5 * box1[:, 2:4]# 计算右下角坐标相对于预测框所在的grid cell的左上角的偏移量box1_xyxy[:, 2:4] box1[:, :2] / float(self.S) 0.5 * box1[:, 2:4]# target中的两个框的目标信息是一模一样的这里取其中一个就行了box2 box_target[i].view(-1, 5)box2_xyxy Variable(torch.FloatTensor(box2.size()))box2_xyxy[:, :2] box2[:, :2] / float(self.S) - 0.5 * box2[:, 2:4]box2_xyxy[:, 2:4] box2[:, :2] / float(self.S) 0.5 * box2[:, 2:4]# 计算两个预测框与标注框的IoU值返回计算结果是个列表iou self.compute_iou(box1_xyxy[:, :4], box2_xyxy[:, :4])# 通过max()函数获取与gt最大的框的iou和索引号(第几个框)max_iou, max_index iou.max(0)# 将max_index放到GPU上max_index max_index.data.cuda()# 保留IoU比较大的那个框coo_response_mask[i max_index] 1 # IOU最大的bbox# 舍去的bbox,两个框单独标记为1分开存放方便下面计算no_coo_response_mask[i 1 - max_index] 1# 将预测框比较大的IoU的那个框保存在box_target_iou中# 其中i max_index表示当前预测框对应的位置torch.LongTensor([4]).cuda()表示在box_target_iou中存储最大IoU的值的位置。box_target_iou[i max_index, torch.LongTensor([4]).cuda()] max_iou.data.cuda()# 放到GPU上box_target_iou Variable(box_target_iou).cuda()# 负责预测物体的预测框的位置信息含物体的grid ceil的两个bbox与ground truth的IOU较大的一方box_pred_response box_pred[coo_response_mask].view(-1, 5)# 标注信息拿出来一个计算就行了因为两个信息一模一样box_target_response_iou box_target_iou[coo_response_mask].view(-1, 5)# IOU较小的一方no_box_pred_response box_pred[no_coo_response_mask].view(-1, 5)no_box_target_response_iou box_target_iou[no_coo_response_mask].view(-1, 5)# 不负责预测物体的置信度置为0本来就是0这里有点多此一举no_box_target_response_iou[:, 4] 0# 负责预测物体的标注框对应的label的信息box_target_response box_target[coo_response_mask].view(-1, 5)# 包含物的体grid ceil中IOU较大的bbox置信度损失contain_loss F.mse_loss(box_pred_response[:, 4], box_target_response_iou[:, 4], size_averageFalse)# 不包含物体的grid ceil中舍去的bbox的置信度损失no_contain_loss F.mse_loss(no_box_pred_response[:, 4], no_box_target_response_iou[:, 4], size_averageFalse)# 负责预测物体的预测框的位置损失loc_loss F.mse_loss(box_pred_response[:, :2], box_target_response[:, :2], size_averageFalse) F.mse_loss(torch.sqrt(box_pred_response[:, 2:4]), torch.sqrt(box_target_response[:, 2:4]), size_averageFalse)# 负责预测物体的所在grid cell的类别损失class_loss F.mse_loss(class_pred, class_target, size_averageFalse)# 计算总损失这里有个权重return (self.l_coord * loc_loss contain_loss self.l_noobj * (nooobj_loss no_contain_loss) class_loss) / N有很多地方不是写的很好后续会不断更改完善大家有问题可以一起商讨。有不足希望大家批评指正谢谢给我观众老爷。