YOLO V3学习笔记(基于代码实现的全过程)

最近在学习YOLO V3算法,经过了一段时间的挣扎,目前大概理出了自己的一些思路。

与我前面学习的MTCNN算法相比,YOLO不算复杂,代码量也不如MTCNN,但就是在代码实现的时候有些地方比较绕,需要多思考,多理一下自己的思路。

YOLO的话,核心思想比较容易理解,不同于RCNN系列two-stage的方法,YOLO利用整张图作为网络的输入,将图片划分为N*N的格子,object的中心点落在哪个格子里,这个格子就负责回归边框及所属类别。在YOLO V3里采用了类似FPN的结构,对26*26和52*52的特征图使用了concatenate连接,至于为什么采用路由而非add,我想可能是路由会使张量维度增加,特征抽象能力越强则表达能力越强,并且通过将低阶的位置信息与高阶的语义信息相结合,能提高预测精度。

下面开始从网络---数据集---训练---侦测四个部分慢慢理思路。

网络部分,这是我在百度找的一张DarkNet53的图片,为什么叫DarkNet53呢,这是由于主网络包括了52层的CNN+BN+LeakyRelu层加上一层输出,所以叫DarkNet53。

整个网络采用416*416的输入,不像DarkNet19一样,网络没有采用Maxpooling取而代之的是步长为2的卷积下采样,这样做的目的防止池化粗暴的丢掉特征对整个模型的影响,卷积下采样的话网络有学习参数,信息融合比较好,整个网络共计5个下采样层;整个网络没有全连接层,减少了网络模型的参数;整个网络最主要的部分就是残差结构,加入残差的主要目的是:由于我们输入的是整张图片进行学习,包含了很多的信息,因此就需要更深的网络来提取更抽象的特征,但随着网络的加深,就难以避免整个模型的退化问题,这时引入残差就很好的解决了此问题,通过加入短连接的方式,将输入信息直接绕道与输出信息相加,这样既保护了信息的完整性,整个网络同时又在差异中进行了学习,简化了学习目标和难度,残差结构是神经网络中的一个重要结构,能够解决梯度消失这个万年大难题,但避免不了的一点就是带来了一些计算量,但是相对于解决的问题来讲,这都不是事儿;最后是网络的concatenate连接,在52*52及26*26的特征图上分别进行了一次torch.cat连接,这是参考了SSD的做法,和SSD每一个输出的特征图都进行侦测的做法不同,YOLO V3将底层位置信息和深层语义信息进行融合,不仅解决了YOLO V1和YOLO V2对小目标进行侦测的局限性,还使侦测效果更佳。(这一部分代码较简单且比较长,就不展示了)

下面是网络的数据集制作

首先你要拿到图片里物体关键位置信息,比如object在原图上的中心点坐标、w和h或左上角右下角坐标

首先你要思考,你想让网络学习什么,也就是你想让网络输出什么?毫无疑问,我们需要网络在13*13、26*26、52*52的特征图上输出每个格子里有没有object、如果有,物体的中心落在哪个格子里,我还需要输出偏移量(我需要用这个偏移量对物体的位置进行进一步的修正)、我还需要输出框的w和h、当然我还需要输出这个物体的类别。ok,你想让网络输出的,就我们给网络输入并学习的。

这里受设备影响,我自己制作了部分数据集,包含3个类别,训练了一个过拟合版本。

话不多说,先上代码:

label_file_path = "./data/person_label.txt"
img_base_dir = "data"
trans = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize(constant.DATA_MEAN, constant.DATA_STD)])
"
ANCHORS_GROUP={
    13:[[360,360],[360,180],[180,360]],
    26:[[180,180],[180,90],[90,180]],
    52:[[90,90],[90,45],[45,90]]}

ANCHORS_GROUP_AREA={
    13:[x*y for x,y in ANCHORS_GROUP[13]],
    26:[x*y for x,y in ANCHORS_GROUP[26]],
    52:[x*y for x,y in ANCHORS_GROUP[52]]}
"
def one_hot(cls_num, i):
    b = np.zeros(cls_num)
    b[i] = 1
    return b

class My_Dataset(Dataset):
    def __init__(self):
        with open(label_file_path) as f:
            self.dataset = f.readlines()

    def __len__(self):
        return len(self.dataset)

    def __getitem__(self, index):
        labels = {}
        line = self.dataset[index]
        strs = line.split()
        image_path = os.path.join(img_base_dir, strs[0])
        img = Image.open(image_path)
        img = img.convert("RGB")
        img_data = trans(img)
        # _boxes=np.array(float(x) for x in strs[1:])
        _boxes = np.array(list(map(float, strs[1:])))
        boxes = np.split(_boxes, len(_boxes) // 5)
        for feature_size, anchors in constant.ANCHORS_GROUP.items():  # 获取键和值
            labels[feature_size] = np.zeros((feature_size, feature_size, 3, 5 + constant.CLASS_NUM))
            for box in boxes:
                cls, cx, cy, w, h = box
                cx_offset, cx_index = math.modf(cx * feature_size / constant.IMG_WIDTH)
                cy_offset, cy_index = math.modf(cy * feature_size / constant.IMG_HEIGHT)
                for i, anchor in enumerate(anchors):
                    anchor_area = constant.ANCHORS_GROUP_AREA[feature_size][i]
                    p_w, p_h = w / anchor[0], h / anchor[1]
                    p_area = w * h
                    iou = min(p_area, anchor_area) / max(p_area, anchor_area)
                    labels[feature_size][int(cy_index), int(cx_index), i] = np.array(
                        [iou, cx_offset, cy_offset, np.log(p_w), np.log(p_h), *one_hot(constant.CLASS_NUM, int(cls))])
        return labels[13], labels[26], labels[52], img_data

利用三个循环嵌套,第一个循环,得到每一个feature_map的大小,以13*13为例,同时得到对应的3个anchor box的大小(包括w、h),然后创建一个torch.size([13,13,3,8])形状的零矩阵,接下来就可以往里面填值了,第二个循环,得到object在原图上框的坐标(中心点坐标及w和h)及所属类别,通过等比缩放得到在13*13特征图上具体位置(包括在哪个格子以及偏移量),第三个循环,将w和h进行压缩(这里用log的方式进行压缩,没有采用开根号,若不对数据进行压缩,损失函数会更加倾向于调大的预测框,减小大尺寸与小尺寸之间的差异,可以理解为归一化操作)求IOU值作为置信度,并找到每一个anchor然后对整个零矩阵进行填入值。整个数据集关键的部分就是最后一步,不是很好理解。

下面是网络的训练,网络训练最主要就是损失部分。先来看一下下面这张图:

是也是在别人文章里选取的,也写的比较详细了,只是我这里对于w和h的压缩采用的是log。

最后将这几个损失函数加起来就是总损失,然后对总损失进行梯度下降。


原图链接:https://blog.csdn.net/taifengzikai/article/details/8650075

def loss_fn(output,target,alpha):
    conf_loss_fn=torch.nn.BCEWithLogitsLoss()#置信度
    crood_loss_fn=torch.nn.MSELoss()#坐标
    output=output.permute(0,2,3,1)
    output=output.reshape(output.size(0), output.size(1), output.size(2), 3, -1)
    output=output.cpu()
    mask_obj=target[...,0]>0
    output_obj=output[mask_obj]
    target_obj=target[mask_obj]

    loss_obj_conf=conf_loss_fn(output_obj[:,0],target[:,0])
    loss_obj_crood=crood_loss_fn(output_obj[:,1:5].float(),target_obj[:,1:5].float())
    loss_obj_cls=conf_loss_fn(output_obj[:,5:],target_obj[:,5:])
    loss_obj=loss_obj_cls+loss_obj_crood+loss_obj_conf

    mask_noobj=target[...,0]==0
    output_noobj=output[mask_noobj]
    target_noobj=target[mask_noobj]
    loss_noobj=conf_loss_fn(output_noobj[:,0],target_noobj[:,0])
    loss=alpha*loss_obj+(1-alpha)*loss_noobj
    return loss

if __name__ == '__main__':
    if not os.path.exists("models"):
        os.makedirs("models")
    save_path="models/net_yolo.pth"
    mydataset=dataset.My_Dataset()
    train_loader=DataLoader(mydataset,batch_size=5,shuffle=True)
    device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
    net=MainNet().to(device)
    if os.path.exists(save_path):
        net.load_state_dict(torch.load(save_path))
    else:
        print("No param")
    net.train()
    opt=torch.optim.Adam(net.parameters())
    epoch=0
    while epoch<2000:
        for target_13,target_26,target_52,image_data_ in train_loader:
            image_data=image_data_.to(device)
            output_13,output_26,output_52=net(image_data)
            loss_13=loss_fn(output_13,target_13,0.7)
            loss_26=loss_fn(output_26,target_26,0.7)
            loss_52=loss_fn(output_52,target_52,0.7)
            loss=loss_13+loss_26+loss_52
            opt.zero_grad()
            loss.backward()
            opt.step()
            if epoch%100==0:
                torch.save(net.state_dict(),save_path)
            print("epoch{}".format(epoch), loss.item())
        epoch+=1

这里有两点值得注意:1、只对有中心点的格子做置信度,坐标点,分类损失,格子内没有中心点的只做置信度的损失

2、置信度损失采用torch.nn.BCEWithLogitsLoss(),BCEWith和BCE损失区别是,BCE必须输入Sigmoid激活后的值,而前者将Sigmoid和BCE结合成一个类,比BCE更加稳定。

最后一部分是侦测部分,也是最难的一部分。

经过前面数据集制作部分,已经了解网络学习的是什么,那侦测的话,也就清楚了,实际就是一个反算的过程,只是需要绕一下。还是先上代码:

class Detestor(nn.Module):
    def __init__(self):
        super(Detestor, self).__init__()
        self.save_path="models/net_yolo.pth"
        self.device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.net=MainNet().to(self.device)
        self.net.load_state_dict(torch.load(self.save_path))
        self.net.eval()
    def forward(self,input,thresh,anchors):
        output_13,output_26,output_52=self.net(input)
        idxs_13,vecs_13=self._filter(output_13,thresh)
        boxes_13=self._parse(idxs_13,vecs_13,32,anchors[13])
        idxs_26,vecs_26=self._filter(output_26,thresh)
        boxes_26=self._parse(idxs_26,vecs_26,16,anchors[26])
        idxs_52,vecs_52=self._filter(output_52,thresh)
        boxes_52=self._parse(idxs_52,vecs_52,8,anchors[52])
        return torch.cat([boxes_13,boxes_26,boxes_52],dim=0)

    def _filter(self,output,thresh):
        output=output.permute(0,2,3,1)
        output=output.reshape(output.size(0),output.size(1),output.size(2),3,-1)
        mask=output[...,0]>thresh
        idex=np.nonzero(mask)
        vecs=output[mask]
        return idex,vecs

    def _parse(self,idex,vecs,t,anchors):
        anchors=torch.tensor(anchors)
        a=idex[:,3]
        confidence=vecs[:,0]
        _classify=vecs[:,5:]
        if len(_classify)==0:
            classify=torch.tensor([])
        else:
            classify=torch.argmax(_classify,dim=1).float()
        cy=(idex[:,1].float()+vecs[:,2])*t
        cx=(idex[:,2].float()+vecs[:,1])*t
        w=anchors[a,0]*torch.exp(vecs[:,3])
        h=anchors[a,1]*torch.exp(vecs[:,4])
        x1=cx-w/2
        y1=cy-h/2
        x2=x1+w
        y2=y1+h
        out=torch.stack([confidence,x1,y1,x2,y2,classify],dim=1)
        return out

if __name__ == '__main__':
    transforms=trans.Compose([
        trans.ToTensor(),
        trans.Normalize(constant.DATA_MEAN,constant.DATA_STD)
    ])
    file_images=os.listdir(r"./data/images")
    for i in range(len(file_images)):
        detector = Detestor()
        img1 = Image.open(r"./data/images/{}.jpg".format(i+1))
        img=img1.convert("RGB")
        img=transforms(img)
        img=img.unsqueeze(dim=0)
        out_value=detector(img,0.5,constant.ANCHORS_GROUP)
        boxes=[]
        for j in range(constant.CLASS_NUM):
            classify_mask=(out_value[...,-1]==j)
            _boxes=out_value[classify_mask]
            boxes.append(tool.nms(_boxes))
        for box in boxes:
            try:
                for k, box_ in enumerate(box):
                    img_draw = ImageDraw.Draw(img1)
                    c, x1, y1, x2, y2, cls = box[k, 0:6]
                    img_draw.rectangle((x1, y1, x2, y2), outline="red", width=2)
                    cls_num = {"cat": "0", "dog": "1", "person": "2"}
                    cls_name = {v: k for k, v in cls_num.items()}[f"{int(cls)}"]  # 根据值获取键名
                    font = ImageFont.truetype("simhei.ttf", 20, encoding="utf-8")
                    img_draw.text((x1, y1), f"{cls_name}{'%.2f' % (c)}", (255, 0, 0), font=font)
            except:
                continue
            img1.show()

还是以13*13的特征图为例,理一下流程:

输出形状为[N,24,H,W]---reshape为[N,H,W,3,8](其中的3代表输出的3个建议框,8表示1个置信度+4个偏移量+3个cls概率)---对置信度进行筛选---输出满足条件对象的位置以及对应输出值---找到满足条件的anchor框---进行坐标反算---torch.stack输出框

上面这一部分代码量很少,但是理解确实是有些难度,你得一步一步打出输出形状来帮助理解。

最后在Draw部分有一点值得关注:

for j in range(constant.CLASS_NUM):
    classify_mask=(out_value[...,-1]==j)
    _boxes=out_value[classify_mask]
    boxes.append(tool.nms(_boxes))

意思是找到同类别的对象,然后做NMS,比如说通过循环拿到一张图片里面的所有人的boxes,然后进行NMS。

最后放一张效果图:

以上部分就是我对YOLO V3的学习笔记,若有不当之处还请不吝赐教,共同学习,共同进步。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值