yolov3 原理代码复现2

代码复现2之Loss函数的实现

yolov3代码复现3之图像预测处理
我们在第一篇文章中,我们可以将输入的数据以[b, 255, 13, 13]的形式的出现,总共有三个尺度的数据输出,输出了这三个尺度的数据,那么我们接下来要怎么处理呢?

首先,我们要对数据进行维度转变,将[b, 255, 13, 13]转变为[b, 13, 13, 3, 85],那么为了达到这个目的,我们可以使用pytorch中自带的permute这个函数。

    prediction = prediction.view(prediction.size(0), 3, prediction.size(1)/3, prediction.size(2), prediction.size(3)).permute(0, 1, 3, 4, 2).contiguous()
    # contiguous:view只能用在contiguous的variable上。如果在view之前用了transpose, permute等,需要用contiguous()来返回一个contiguous copy。
    # [b, 255, 13, 13]->[b, 3, 13, 13,85]

这些代码里面的prediction指的就是输出的数据,比如[b, 255, 13, 13],注意里面的注释,每次transpose或者permute之后,都加上contiguous,一种解释是有些tensor并不是占用一整块内存,而是由不同的数据块组成,而tensor的view()操作依赖于内存是整块的,这时只需要执行contiguous()这个函数,把tensor变成在内存中连续分布的形式。

好了,我们接下来来想一想[b,3,13,13, 85]的含义,3指的是三个anchor,每个尺度下的anchor不一样,比如,13x13尺度下的anchor是(116, 90), (156, 198), (373, 326),26x26的anchor是(30, 61), (62, 45), (59, 119),52x52的三个anchor是 (10, 13), (16, 30), (33, 23),大家也许发现了,最后输出的尺度越小,对应的anchor也就越大,我看了那些博主的意思,总结一下,其实可以理解为用眼睛看东西,输出尺寸就代表我们离物体的远近,输出尺寸越小,我们离物体就越近,这时候,我们眼睛需要扫描的范围更大才能确定这个物体的真实模样,输出尺寸越大,代表我们离物体越远,这时候,就不要扫描大范围来确定实物的真实模样了。至于anchor的数值是怎么定的,这是论文里面给的,可能是实验得到的,也可能是其他方式得到的,这个不需要深究。

接下来咱们看看85的含义,上篇文章,我们说了这85对应的是x,y,w,h,置信度和80个类别,后面我想了想,为什么会这么定义,其实这很可能是我们或者论文作者自己定义的,这85个值,都是经过一系列函数得到的最终值,我们需要根据这些数值来确定85个数据中每个数据对应的最后的函数,所以,你只需要在这85个数据中定义5个为x,y,w,h,置信度,其余定义为类别都可以,为了方便,我们就按照默认的定义。
好了,懂得这个之后,我们开始考虑loss函数怎么求,loss函数,只在yolov1的论文中有明确的公式,至于yolov3,每个版本都有些不一样,其实,我个人觉得,这些loss函数,只要不相差太多就行,本来就是根据这个loss函数求解最佳的解。所以,我们的loss函数中,包含有以下几部分:
x,y的loss,wh的loss,置信度的loss,类别的loss。

要求解xy,wh的loss,我们需要了解一个概念,边框回归,对这个概念没有了解的同学,点击它去学习一下。
了解到边框回归之后,我们就可以开始考虑了x,y,w,h的真实含义了,我最开始就是把这几个数值理解为字面上的意思了,导致我纠结了好几天,看其他博主的代码总感觉这不对那不对,最后我搞清楚了,这几个数值的真实含义不是字面上的意思,我们先看看下面几张图,这几张图是上面那链接里面的截图。
边框回归公式1经过我的思前虑后,我发现,神经网络输出的x,y,w,h应该分别对应上面这张图的在这里插入图片描述,只有这样,才能将这这几个参数的真实含义对应起来,因为这些参数的具体数值就是经过一系列函数得到的。

理解了这些含义之后,我们就可以开始为xy,wh的loss做准备了,预测的xywh已经知道了,但是target目标的xywh我们依旧不知道,这时候,就需要我们想办法来得到了。

我们可以查看coco数据集里面的标签文件,数据是类似以下这种,16 0.328250 0.769577 0.463156 0.242207 ,第一个数值代表的是类别,后面的数据就代表了xywh了,只不过这些值是经过均值化了的,所谓均值化,我的理解就是单位化,比如原照片尺寸为416x416,中心点的x坐标为120,那么我们最后存储的数值为120/416,这就是上面数据的由来。知道这个之后,我们要注意,一张照片里面,多个边框是常见的事情,也就是一张照片有多个标签,对于这些边框,我们要一个个的进行处理。

那么,怎么处理呢?首先,我们要知道,每个边框在每个输出尺寸下有三个anchor来预测,我们要找到最合适的anchor来对这个边框进行预测,那么,怎么来判断最合适的anchor呢?我们通过iou这个概念来确定,什么是Iou?简单来说,就是两个边框的重合度。而用iou来判断边框,我们又面临着边框的中心坐标没有给定的情况,这时候,我们可以将两个边框的左上角进行对齐,然后再求解iou,我们称这个为bbox_wh_iou,代码情况如下:

def bbox_wh_iou(wh1, wh2):
    wh2 = wh2.unsqueeze(0)
    w1, h1 = wh1[:,0], wh1[:,1]
    w2, h2 = wh2[:,0], wh2[:,1]
    inter_area = torch.min(w1, w2) * torch.min(h1, h2) # 之所以这样处理,就是将两个边框移到左上角后,重合的部分等于这两个数值相乘
    union_area = (w1 * h1 + 1e-16) + w2 * h2 - inter_area
    # print('->', inter_area / union_area)
    return inter_area / union_area

在实际的模型训练中,我们一次性的会将多个数据放到gpu或者cpu上,经常性的是一次性放64个数据,那么,我们在处理这些标签之前,我们就要加上这种标签对应的图片编号,最后标签的形势就当成一个list,targets = [image, class, x, y, w, h],基本的代码如下:

'''this code was desinged by nike hu'''
anchors = [(116, 90), (156, 198), (373, 326), (30, 61), (62, 45), (59, 119), (10, 13), (16, 30), (33, 23)]
def build_target(target, iou_binary=0.4):
    nt = len(target)
    # 这里统计传入的target的行数,targets = [image, class, x, y, w, h], 这里的image是一个数字,代表是当前batch的第几个图片,x,y,w,h都进行了归一化,除以了宽或者高
    txys, twhs, tcls,tindexs = [], [], [], []
    target = torch.Tensor(target) # 转化为torch类型
    for i in range(nt): # 对每一个边框进行检测哪个anchor最适合
        txy, twh, tcl, tindex = [], [], [], []
        for j in range(3): # 有三种尺度下的anchor
            image_anchors = torch.Tensor(anchors[3*j: 3*j+3]) # 分别对应不同尺度的三个anchor,->[3, 2]
            # print('最初->', image_anchors)
            target_change = torch.zeros_like(target[i])
            target_change[0:2] = target[i, 0:2] # 需要一个新的变量来更新target,这里维度减少,->[n]
            if j == 0: # 不同尺度下,放缩尺度不一样
                image_anchors /= 32 # 13X13,图片放缩32倍
                target_change[2:] = target[i, 2:] * 13 # 由于target目标是以单位为参照对象,这里我们要放大
            if j == 1:
                image_anchors /= 16
                target_change[2:] = target[i, 2:] * 26
            if j == 2:
                image_anchors /= 8
                target_change[2:] = target[i, 2:] * 52
            # print('image_anchor->', image_anchors, 'target->', target_change)
            wh_iou = bbox_wh_iou(image_anchors, target_change[4:]) # 统计原图中边框和anchor的iou
            # print(wh_iou)
            wh_iou_id = wh_iou.max(dim=0)[1] # 还是使用最大值来删选
            # print('下标->', wh_iou_id)
            anchor_id = torch.arange(3) # anchor的下标
            # wh_iou_id = (wh_iou > iou_binary) # 大于iou阈值的部分,返回[trur, flase....这种
            txy.append(target_change[2:4] - target_change[2:4].long())
            gj, gi = target_change[2:4].long() # 这是中心点对应网格的坐标
            a = anchor_id[wh_iou_id].long() # 这是每一层对应的三个anchor中选取最合适的那个
            tindex.append((target_change[0].long(), a, gj, gi)) # 分别对应图片编号,anchor下标, 中心网格的y,x
            gwh = target_change[4:] # 这是标签的宽和高
            twh.append(torch.log(gwh / image_anchors[a])) # 这是存储放缩的倍数
            tcl.append(target_change[1].long())
            # print('TXY->', txy)
            # print('tindex->', tindex)
            # print('twh->', twh)
            # print('tcl->', tcl)
        # print('..................................')
        tcls.append(tcl)
        tindexs.append(tindex)
        twhs.append(twh)
        txys.append(txy)
    return txys, twhs, tcls,tindexs

对于上面这些代码,旁边的注释可能会出现词不达意的情况,因为很多时候这些注释都是我敲代码的过程中用我能懂得语言写上的,对以上代码再解释一下吧,由于我们一张图片中可能存在多个标签,所以这些代码就是针对多个标签来处理的,最后的输出是一个list, list里面有多个list,输出结果如下:

xy-> [[tensor([0.2673, 0.0045]), tensor([0.5345, 0.0090]), tensor([0.0690, 0.0180])], [tensor([0.6748, 0.8784]), tensor([0.3495, 0.7567]), tensor([0.6991, 0.5134])], [tensor([0.1904, 0.7650]), tensor([0.3809, 0.5299]), tensor([0.7617, 0.0599])]]

这个最外面的list包含着每个标签对应的中心点,每个标签的中心点又有三个,因为我们要进行多尺度的预测,13, 26, 52这三个尺度对应着这些中心点,其他最后返回的参数也是这种对应的关系。

好了,在代码中的注释中,我们也知道了各个参数的含义,txy对应预测结果的xy,twh对应预测结果的wh,tcls对应预测结果的类别,tindex对应的是置信度,以及标签对应的中心点所在的小框框的左上角。接下来,我们开始介绍loss函数的计算。

对于loss的计算,我们这里采用的损失函数有均方差函数,用来计算xy和wh的loss,还有多分类损失函数和单分类损失函数,具体的代码如下,注释也在里面:

'''this code was desinged by nike hu'''
def computer_loss(pretection, targert):
    # prediction这里应该是([b, 3, 13, 13, 85],[b, 3, 26, 26, 85],[b,3, 52, 52, 85])
    MSE = nn.MSELoss() # 均方损失函数,loss(xi,yi)=(xi−yi)2,用于计算x,y,w,h的损失,将pred,target逐个元素求差,然后求平方,再求和,再求均值,
    CE = nn.CrossEntropyLoss() # 这个损失函数用于多分类问题虽然说的是交叉熵,是nn.logSoftmax()和nn.NLLLoss()的整合,
    # loss(x,class)=weight[class](−x[class]+log(j∑​exp(x[j])))等价nn.logSoftmax()和nn.NLLLoss()的整合,
    # CrossEntropyLoss用于多类别分类,这用来类别的Loss计算,这种函数,第二个参数只需要一个数值就行
    BCE = nn.BCEWithLogitsLoss() # BCEWithLogitsLoss = Sigmoid+BCELoss,当网络最后一层使用nn.Sigmoid时,就用BCELoss,
    # 当网络最后一层不使用nn.Sigmoid时,就用BCEWithLogitsLoss。(BCELoss)BCEWithLogitsLoss用于单标签二分类或者多标签二分类,
    # 这里用来置信度的loss计算,BCELoss是−1n∑(yn×lnxn+(1−yn)×ln(1−xn))−n1​∑(yn​×lnxn​+(1−yn​)×ln(1−xn​))、
    # ,这个函数,第二个参数和第一个参数的维度个数要一致
    txys, twhs, tcls,tindexs = build_target(targert) # 由于一张图片中很可能有多个标签,所以返回的每一个数值都是一个元祖
    losses = []
    for i in range(len(txys)): # 对每一个标签求loss
        loss_one = torch.zeros(1, 3) # [1, 4],每个值对应的是每一层的Loss
        for j in range(len(pretection)): # 这里相当于是对每一层求取Loss
            index_i = tindexs[i][j] # 第几个标签的第几层,image_id, achor_id, jy,jx
            tconf = torch.zeros_like(pretection[j][...,0]) # 创建一个向量来最后和置信度求解loss,=》[b,3, 13, 13, 1]
            pretection_label = pretection[j][index_i] # 第几层的输出图[b, 3, 13, 13, 85].....=->[85]
            tconf[index_i] = 1 # 对于我们找到的中心点的那个框,我们将这个框对应的置信度设置为1
            loss1 = MSE( torch.sigmoid(pretection_label[:2]), txys[i][j]) # xy的loss
            loss2 = MSE( pretection_label[2:4], twhs[i][j]) # wh的loss
            loss3 = CE( pretection_label[5:], tcls[i][j]) #类别的loss
            loss4 = BCE( pretection_label[4], tconf) # 置信度的Loss
            loss_sum = loss1 + loss2 + loss3 + loss4 # 每一层的Loss总和,[1]
            loss_one[0,j] = loss_sum # 最后应该变为[1,3]每个值对应每一层对某个标签的Loss
        losses.append(loss_one.sum()) # 将每个标签三层的loss总和保存
    losses_sum = sum(loss_sum) # 求解最后的总和,代表一张照片中所有标签的loss总和
    return losses_sum

我个人觉得,这块代码的注释还是很详细了,就不在累述了。这一块代码,我没有选取相关的数据来进行测试,所以可能会有一些错误,大家有兴趣可以自己弄一些数据来测试一下,我不愿弄了,因为我要自己读取几张图片的数据,还要自己造一些标签的文件,感觉有些麻烦,就没做了,基本原理应该是这样的。

终于可以结束这一块的解释了,说实话,这一块是我用的时间最多,最纠结的地方,可能也跟我接触神经网络时间很短,理解不够的原因。希望我能帮大家减少一些坑。打算还写一篇,来介绍怎么处理输出的三个尺度的数据,来将最后预测的边框和类别画在图像上。

2020 4 13

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值