YOLO v1 计算流程–基于pytorch
- 个人理解TOLO v1的计算有如下几个关键部分:
- 1.图像预处理
- YOLO v1要求图像的大小是一致的448 * 448 因此读取图像后需要对图像进行预处理
- 2.图像的前向传播
- 前向传播部分由两部分组成:特征提取和输出构建
- 特征提取可以使用原文章中基于DartNet的特征提取方式,也可以采用其他网络诸如VGG或者ResNet等
- 输出构建时YOLO v1的精华,是YOLO网络的主要思想核心,基于划分好的网格构建bounding box、confidence和class维度,每个网格构建一套
- 3.损失函数的计算:
- 损失函数均基于均方根误差计算
- YOLO v1的损失函数包括三部分:bounding box坐标位置损失、置信度损失(网格中是否包含object中心),class损失(基于one hot编码计算)
图像预处理和划分
- 将图像resize为448×448×3448\times 448 \times 3448×448×3的大小,resize的目的是为了满足全连接层的固定输入维度的要求
- 代码:
- 将448的图像划分为7×77\times 77×7的格点,每个格点边长为64个像素点图解:
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l7qmIDoI-1635915412647)(./1635745220607.png)]
将图像输入到YOLO v1网络前向传播:
- 由于YOLO前边的网络主要作用是特征提取,因此使用resnet的全连接之前的部分代替,通过加载torchvision中的已经训练好的resnet34,并固定参数,后边跟YOLOv1后半部分卷积结构和全连接层变化结构
- 每个网格要预测2个bounding box,每个bounding box除了要预测位置之外,还要附带预测一个confiden值,每个网格还要预测C个类别的分数
- 网格有7×77\times 77×7每个网格2个边界框
- 也就是每个bounding box 预测5个值,其中四个为位置参数(相对值,相对于整个图像大小来说),confidence值,这里有两个,就是2×(4+1)2 \times (4+1)2×(4+1)
- VOC数据集有20个类别,则类别数为20
- 所以一共有参数7×7×(20+2×(4+1))7 \times 7 \times(20+2\times (4+1))7×7×(20+2×(4+1))
*
from torchvision.models import resnet34
import torch.nn as nn
import torchclass YOLOv1_resnet(nn.Module):def __init__(self, num_box, class_num):super(YOLOv1_resnet,self).__init__()self.num_box = num_boxself.class_num = class_numresnet = resnest_model()resnet_out_channel = resnet.fc.in_features self.resnet = nn.Sequential(*list(resnet.children())[:-2]) '''定义YOLO的最后的卷积层'''self.conv_layer = nn.Sequential(nn.Conv2d(resnet_out_channel,1024,3,padding=1),nn.BatchNorm2d(1024),nn.LeakyReLU(),nn.Conv2d(1024,1024,3,stride=2,padding=1),nn.BatchNorm2d(1024),nn.LeakyReLU(),nn.Conv2d(1024, 1024, 3, padding=1),nn.BatchNorm2d(1024),nn.LeakyReLU(),)'''全连接层,三维变量拉平进行处理'''self.dense_layers = nn.Sequential(nn.Linear(7*7*1024,4096),nn.LeakyReLU(),nn.Linear(4096,7*7*30))def forward(self, x):out = self.resnet(x)out = self.conv_layer(out)out = out.view(out.size()[0],-1)out = self.dense_layers(out)return out.reshape(-1, (5*self.num_box+self.class_num), 7, 7)def set_parameter_requires_grad(model):for param in model.parameters():param.requires_grad = False
def resnest_model():model_ft = resnet34(pretrained=True)set_parameter_requires_grad(model_ft)num_ftrs = model_ft.fc.in_features return model_ftif __name__ == '__main__':x = torch.randn((1,3,448,448))net = YOLOv1_resnet(2, 20)print(net)y = net(x)print(y.size())
3)损失函数的计算
- 数据前向传播后,要计算损失才能后向传播,因此需要根据定义计算损失函数,在YOLO v1中损失函数由三个部分组成,均使用平方和计算:
- 使用误差平方和计算
- bounding box损失
- Lloc(l,g)=∑i=0s2∑j=0B1i,jobj[(xi?x^i)2+(yi?y^i)2]+λcoord∑i=0s2∑j=0B1i,jobj[((wi)?w^i)2+((hi)?h^i)2]L_{loc}(l,g)=\sum^{s^2}_{i=0} \sum_{j=0}^B 1^{obj}_{i,j}[(x_i-\hat x_i)^2+(y_i-\hat y_i)^2]+\lambda_{coord}\sum^{s^2}_{i=0} \sum_{j=0}^B 1^{obj}_{i,j}[(\sqrt{(w_i)}-\sqrt{\hat w_i})^2+(\sqrt{(h_i)}-\sqrt{\hat h_i})^2]Lloc?(l,g)=∑i=0s2?∑j=0B?1i,jobj?[(xi??x^i?)2+(yi??y^?i?)2]+λcoord?∑i=0s2?∑j=0B?1i,jobj?[((wi?)
??w^i?
?)2+((hi?)
??h^i?
?)2]
- confidence 损失
- Lconf(o,c)=∑i=0s2∑j=0B1i,jobj[(Ci?C^i)2+λnoobj∑i=0s2∑j=0B1i,jnoobj[(Ci?C^i)2L_{conf}(o,c)=\sum^{s^2}_{i=0} \sum_{j=0}^B 1^{obj}_{i,j}[(C_i-\hat C_i)^2+\lambda_{noobj}\sum^{s^2}_{i=0} \sum_{j=0}^B 1^{noobj}_{i,j}[(C_i-\hat C_i)^2Lconf?(o,c)=∑i=0s2?∑j=0B?1i,jobj?[(Ci??C^i?)2+λnoobj?∑i=0s2?∑j=0B?1i,jnoobj?[(Ci??C^i?)2
- classes 损失
- Lcla(O,C)=∑i=0s21iobj∑c∈classes(pi(c)?pi^(c))L_{cla(O,C)}=\sum^{s^2}_{i=0}1^{obj}_{i}\sum_{c \in classes(p_i(c)-\hat {p_i}(c))}Lcla(O,C)?=∑i=0s2?1iobj?∑c∈classes(pi?(c)?pi?^?(c))?
- 总的损失为所有三种损失的损失相加
- L(o,c,O,C,l,g)=λ1Lconf(o,c)+λ2Lcla(O,C)+λ3Lloc(l,g)L(o,c,O,C,l,g)=\lambda_1L_{conf}(o,c)+\lambda_2L_{cla(O,C)}+\lambda_3L_{loc}(l,g)L(o,c,O,C,l,g)=λ1?Lconf?(o,c)+λ2?Lcla(O,C)?+λ3?Lloc?(l,g)
- 在计算bounding box损失的时候涉及到一个问题:anchors的选取
- anchors生成的过程如下:
- 锚定过程:
- 对于输入图像的每个对象,先找到其中心点,寻找中心点所在的划分好的网格(7*7),则该包含物体中心点的网格中confidence=1,其他48个网格中confidence为0,在YOLO中称为中心点所在的网格对预测该对象负责
- bounding box坐标:
- YOLO中并不会预设anchors坐标或者高宽比,而是锚定中心点之后,在结果中直接生成,组成输出结果的8个特征,不过每个网格cell生成两组boxes坐标,每组坐标有4个,分别为相对中心点坐标(x,y),相对高度和宽度为(w, h),其中相对中心坐标时相对于中心所在的子网格,w,h为相对于整幅图的宽度和高度比例,如下图:
- 由于有两个boxes,因此需要对boxes进行筛选,选取最优的boxes的坐标计算误差,筛选采用IoU 来进行:
- IoU:
- IoU=intersection(A,B)union(A,B)IoU =\frac{intersection(A,B)}{union(A,B)}IoU=union(A,B)intersection(A,B)?
- 物理意义为:IoU为交集部分面积与并集部分面积之比,当两个Box完全重合时IoU=1,不相交时IoU=0
- 本部分代码采用沐神课程中的代码,在我之前的blog中有详细解析,链接https://blog.csdn.net/qq_34992900/article/details/120705041?spm=1001.2014.3001.5501:
def box_iou(boxes1, boxes2):'''args:boxes1: tensor[num_boxes, 4]boxes2: 同上'''box_area = lambda boxes:((boxes[:,2] - boxes[:,0]) * (boxes[:,3] - boxes[:,1]))areas1 = box_area(boxes1)areas2 = box_area(boxes2)inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[: 2:])inters = (inter_lowerrights - inter_upperlefts).clamp(min=0)inter_areas = inters[:, :, 0] * inters[:, :, 1]union_areas = areas1[:, None] + areas2 - inter_areasreturn inter_areas / union_areas
- 定义Loss函数
- 为了方便计算Loss,将网络输出为7×7×307 \times 7 \times 307×7×30的数据格式与label计算,label转化为与网络输出一致的格式7×7×307 \times 7 \times 307×7×30得到;
- 在计算中先计算格点预设两个边框与真实边框的IoU值,选择最大IoU边框,进一步计算边框坐标误差
- 上述步骤如下:
- 根据confidence判断网格中是否包含object的中心
- 若存在中心,则将中心–高宽的形式,转为左上角和右下角坐标的形式
- 通过上述IoU计算大赛分别计算两个bounding box与label的大小
- 选取IoU最大的计算边框坐标误差,IoU小的bounding box则计入到网格没有object的误差,计算分类误差,概率值为IoU
- 计算置信度误差
- 计算分类误差
- 代码如下:
class Loss_yolov1(nn.Module):def __init__(self):super(Loss_yolov1,self).__init__()def box_center_to_conner(self, boxes, n, m, grid_num):"""转换 YOLO v1的bounding box从中间,宽度,高度)转换(左上,右下)"""cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]x1 = (cx + n)/grid_num - w/2y1 = (cy + m)/grid_num - h/2x2 = (cx + n)/grid_num + w/2y2 = (cy + m)/grid_num + w/2boxes = torch.stack((x1, y1, x2, y2), axis=-1)return boxesdef forward(self, pred, labels):""":param pred: (batchsize,30,7,7)的网络输出数据:param labels: (batchsize,30,7,7)的样本标签数据:return: 当前批次样本的平均损失"""num_gridx, num_gridy = labels.size()[-2:] num_b = 2 num_cls = 20 noobj_confi_loss_2 = 0. coor_loss = 0. obj_confi_loss = 0. class_loss = 0. n_batch = labels.size()[0] iloss_1 = 0.for i in range(n_batch): for n in range(7): for m in range(7): if labels[i,4,m,n]==1:bbox1_pred_xyxy = self.box_center_to_conner(pred[i,:4,m,n], n, m, num_gridx)bbox2_pred_xyxy = self.box_center_to_conner(pred[i,4:8,m,n], n, m, num_gridx) bbox_gt_xyxy = self.box_center_to_conner(labels[i,:4,m,n], n, m, num_gridx)iou1 = box_iou(bbox1_pred_xyxy,bbox_gt_xyxy)iou2 = box_iou(bbox2_pred_xyxy,bbox_gt_xyxy)box_pred = pred[i, :4, m, n] if iou1 >= iou2 else pred[i, 4:8, m, n]icoor_loss = (torch.sum((box_pred[0:2,:] - labels[i,0:2,m,n])**2) + torch.sum((pred[i,2:4,m,n].sqrt()-labels[i,2:4,m,n].sqrt())**2))iobj_confi_loss = (pred[i,4,m,n] - iou1)**2inoobj_confi_loss_1 = ((pred[i,9,m,n]-iou2)**2)iclass_loss = torch.sum((pred[i,10:,m,n] - labels[i,10:,m,n])**2)iloss_1 += 5 * icoor_loss + iobj_confi_loss + iclass_loss + 0.5*inoobj_confi_loss_1else: noobj_confi_loss_2 += 0.5 * torch.sum(pred[i,[4,9],m,n]**2)loss = iloss_1 + 0.5 * noobj_confi_loss_2return loss/n_batch
loss_fn = Loss_yolov1()
pred = torch.randn((4,30,7,7))
label = torch.randn((4,30,7,7))
loss = loss_fn(pred, label)
print(loss)
参考:
- 大量参考了该仓代码:https://github.com/lavendelion/YOLOv1-from-scratch
- 部分参考了李沐深度学习课程代码