分享

动手学习深度学习pytorch版——从零开始实现YOLOv1

 行走在理想边缘 2023-02-15 发布于四川

  以下是YOLOv1网络的实践操作部分,具体理论可以参考我的博客“ 经典论文解析——YOLOv1——目标检测”。文中代码均使用了GPU,如果要用CPU尝试,可以将代码中的.cuda()都去掉,但是不建议这么做,因为这个网络还挺大的,CPU很可能跑不动。
完整代码文件有空我会放在github上,想参考的读者目前就只能复制粘贴了(但是文中提供的代码也是完整的了)。如有错误,请评论指出,共同进步,谢谢。

2021年3月27日更新:

  • 介于很多人要求,将代码修正后放在github上,链接点这里。其中主要代码使用了我自己的pytorch项目模板,如果有需要可以留言我再传上去。主体部分代码和文章里一样,做了几个小修改:1). 计算loss时的m,n反了,进行修正;2). labels2bbox用矩阵运算完成,速度更快一点;
  • 我自己又跑了一遍,没发现nan的问题,初始配置可以参见代码,我的显存不够,所以优化器只能用SGD,Adam我跑不起来。
  • 注意,github里的代码和文章里的代码在bbox的格式上有点区别,github里bbox[0:4]是坐标,bbox[4]是置信度,bbox[5]是类别。所以相应代码略有不同。直接使用github里的代码就可以了,文章里的代码可用作为参考。

1. 数据处理

1.1 数据集介绍

  搭建网络前首先要将原始数据集的数据格式转变为我们方便使用的格式。这里使用的是VOC2012数据集,下载下来有如下五个文件夹:
在这里插入图片描述
  Annotations文件夹是存放图片对应的xml文件,比如“2007_000027.xml"存放的是图片2007_000027.jpg对应的信息,用记事本打开可以看到,这是xml格式的数据,里面除了图片的基本信息以外,还有一项< object >类,里面分别存放了类别的名称(< name >),识别的难易程度(< difficult >),以及bounding box的坐标信息< bndbox >,这里存放的box信息是以两点式存放,也就是左上角点和右下角点。当然,VOC数据集不只是用于目标检测任务,所以还存放了一些其他信息,比如人体的具体部分(< part >)等,这些就不用关注了。
在这里插入图片描述
  ImageSets文件夹里存放了官方为我们划分好的训练集和验证集的txt文件。我们主要使用“ImageSets/Main/"文件夹下的train.txt和val.txt文件,train.txt文件存放了官方划分的训练集的图片名称,val.txt文件存放了验证集图片的名称。
  还有一个需要关注的文件夹就是JEPGImages,里面存放了对应图片名称的原始图片。剩下的两个文件夹我们就不需要特别关注了。

1.2 数据提取和转换

  了解数据集后,很明显,我们需要将图片对应的xml文件中bounding box的信息提取出来,并转换为我们需要的 ( c l s , x c , y c , w , h ) (cls,x_c,y_c,w,h) (cls,xc,yc,w,h)格式,其中cls是根据物体类别的序号决定的,物体类别排序储存在全局变量CLASSES中。

CLASSES = ['person', 'bird', 'cat', 'cow', 'dog', 'horse', 'sheep',
           'aeroplane', 'bicycle', 'boat', 'bus', 'car', 'motorbike', 'train',
           'bottle', 'chair', 'dining table', 'potted plant', 'sofa', 'tvmonitor']

  使用下面三个子函数,调用make_label_txt()函数,就可以在当前项目文件夹的labesl文件夹下创造出与图片对应的txt文件,比如图片2007_000027.jpg,就有对应的2007_000027.txt文件,里面储存着图片2007_000027.jpg的所有bbox信息,每行一个。注意,要从.xml中提取信息,需要有xml库。

import xml.etree.ElementTree as ET
import os
import cv2

def convert(size, box):
    """将bbox的左上角点、右下角点坐标的格式,转换为bbox中心点+bbox的w,h的格式
    并进行归一化"""
    dw = 1. / size[0]
    dh = 1. / size[1]
    x = (box[0] + box[1]) / 2.0
    y = (box[2] + box[3]) / 2.0
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh
    return (x, y, w, h)


def convert_annotation(image_id):
    """把图像image_id的xml文件转换为目标检测的label文件(txt)
    其中包含物体的类别,bbox的左上角点坐标以及bbox的宽、高
    并将四个物理量归一化"""
    in_file = open(DATASET_PATH + 'Annotations/%s' % (image_id))
    image_id = image_id.split('.')[0]
    out_file = open('./labels/%s.txt' % (image_id), 'w')
    tree = ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size')
    w = int(size.find('width').text)
    h = int(size.find('height').text)

    for obj in root.iter('object'):
        difficult = obj.find('difficult').text
        cls = obj.find('name').text
        if cls not in CLASSES or int(difficult) == 1:
            continue
        cls_id = CLASSES.index(cls)
        xmlbox = obj.find('bndbox')
        points = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
             float(xmlbox.find('ymax').text))
        bb = convert((w, h), points)
        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')


def make_label_txt():
    """在labels文件夹下创建image_id.txt,对应每个image_id.xml提取出的bbox信息"""
    filenames = os.listdir(DATASET_PATH + 'Annotations')
    for file in filenames:
        convert_annotation(file)

  生成好图片对应的.txt文件后,要注意进行检查,自己写项目的时候要养成习惯,做完一部分工作后,最好写代码验证一下是否能正常完成自己想要的任务。比如我们将xml文件的bounding box提取出来,就需要检验一下提出的数据经过转换后是否保持原样,最简单的检验方式就是在原图中,利用.txt里的数据绘出bbox并输出对应框的类别信息,看看是否合理。可以用以下代码检验。以图片2008_000007.jpg为例,可以看到如下图结果,确认信息提取无误。
在这里插入图片描述

def show_labels_img(imgname):
	"""imgname是输入图像的名称,无下标"""
    img = cv2.imread(DATASET_PATH + "JPEGImages/" + imgname + ".jpg")
    h, w = img.shape[:2]
    print(w,h)
    label = []
    with open("./labels/"+imgname+".txt",'r') as flabel:
        for label in flabel:
            label = label.split(' ')
            label = [float(x.strip()) for x in label]
            print(CLASSES[int(label[0])])
            pt1 = (int(label[1] * w - label[3] * w / 2), int(label[2] * h - label[4] * h / 2))
            pt2 = (int(label[1] * w + label[3] * w / 2), int(label[2] * h + label[4] * h / 2))
            cv2.putText(img,CLASSES[int(label[0])],pt1,cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,0,255))
            cv2.rectangle(img,pt1,pt2,(0,0,255,2))

    cv2.imshow("img",img)
    cv2.waitKey(0)

1.3 pytorch的Dataset类构造

  准备好数据后,就要按老套路开始准备训练了。首先要准备训练使用的Dataset类。具体代码如下:

class VOC2012(Dataset):
    def __init__(self,is_train=True,is_aug=True):
        """
        :param is_train: 调用的是训练集(True),还是验证集(False)
        :param is_aug:  是否进行数据增广
        """
        self.filenames = []  # 储存数据集的文件名称
        if is_train:
            with open(DATASET_PATH + "ImageSets/Main/train.txt", 'r') as f: # 调用包含训练集图像名称的txt文件
                self.filenames = [x.strip() for x in f]
        else:
            with open(DATASET_PATH + "ImageSets/Main/val.txt", 'r') as f:
                self.filenames = [x.strip() for x in f]
        self.imgpath = DATASET_PATH + "JPEGImages/"  # 原始图像所在的路径
        self.labelpath = "./labels/"  # 图像对应的label文件(.txt文件)的路径
        self.is_aug = is_aug

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

    def __getitem__(self, item):
        img = cv2.imread(self.imgpath+self.filenames[item]+".jpg")  # 读取原始图像
        h,w = img.shape[0:2]
        input_size = 448  # 输入YOLOv1网络的图像尺寸为448x448
        # 因为数据集内原始图像的尺寸是不定的,所以需要进行适当的padding,将原始图像padding成宽高一致的正方形
        # 然后再将Padding后的正方形图像缩放成448x448
        padw, padh = 0, 0  # 要记录宽高方向的padding具体数值,因为padding之后需要调整bbox的位置信息
        if h>w:
            padw = (h - w) // 2
            img = np.pad(img,((0,0),(padw,padw),(0,0)),'constant',constant_values=0)
        elif w>h:
            padh = (w - h) // 2
            img = np.pad(img,((padh,padh),(0,0),(0,0)), 'constant', constant_values=0)
        img = cv2.resize(img,(input_size,input_size))
        # 图像增广部分,这里不做过多处理,因为改变bbox信息还蛮麻烦的
        if self.is_aug:
            aug = transforms.Compose([
                transforms.ToTensor()
            ])
            img = aug(img)

        # 读取图像对应的bbox信息,按1维的方式储存,每5个元素表示一个bbox的(cls,xc,yc,w,h)
        with open(self.labelpath+self.filenames[item]+".txt") as f:
            bbox = f.read().split('\n')
        bbox = [x.split() for x in bbox]
        bbox = [float(x) for y in bbox for x in y]
        if len(bbox)%5!=0:
            raise ValueError("File:"+self.labelpath+self.filenames[item]+".txt"+"——bbox Extraction Error!")

        # 根据padding、图像增广等操作,将原始的bbox数据转换为修改后图像的bbox数据
        for i in range(len(bbox)//5):
            if padw != 0:
                bbox[i * 5 + 1] = (bbox[i * 5 + 1] * w + padw) / h
                bbox[i * 5 + 3] = (bbox[i * 5 + 3] * w) / h
            elif padh != 0:
                bbox[i * 5 + 2] = (bbox[i * 5 + 2] * h + padh) / w
                bbox[i * 5 + 4] = (bbox[i * 5 + 4] * h) / w
            # 此处可以写代码验证一下,查看padding后修改的bbox数值是否正确,在原图中画出bbox检验

        labels = convert_bbox2labels(bbox)  # 将所有bbox的(cls,x,y,w,h)数据转换为训练时方便计算Loss的数据形式(7,7,5*B+cls_num)
        # 此处可以写代码验证一下,经过convert_bbox2labels函数后得到的labels变量中储存的数据是否正确
        labels = transforms.ToTensor()(labels)
        return img,labels

  其中,convert_bbox2labels()函数是将bbox信息转换为YOLOv1网络所需的标签格式,也就是 7 × 7 × 30 7\times7\times30 7×7×30的数据格式,具体转换方式可以参见我的博客“经典论文解析——YOLOv1——目标检测”。实现代码如下:

def convert_bbox2labels(bbox):
    """将bbox的(cls,x,y,w,h)数据转换为训练时方便计算Loss的数据形式(7,7,5*B+cls_num)
    注意,输入的bbox的信息是(xc,yc,w,h)格式的,转换为labels后,bbox的信息转换为了(px,py,w,h)格式"""
    gridsize = 1.0/7
    labels = np.zeros((7,7,5*NUM_BBOX+len(CLASSES)))  # 注意,此处需要根据不同数据集的类别个数进行修改
    for i in range(len(bbox)//5):
        gridx = int(bbox[i*5+1] // gridsize)  # 当前bbox中心落在第gridx个网格,列
        gridy = int(bbox[i*5+2] // gridsize)  # 当前bbox中心落在第gridy个网格,行
        # (bbox中心坐标 - 网格左上角点的坐标)/网格大小  ==> bbox中心点的相对位置
        gridpx = bbox[i * 5 + 1] / gridsize - gridx
        gridpy = bbox[i * 5 + 2] / gridsize - gridy
        # 将第gridy行,gridx列的网格设置为负责当前ground truth的预测,置信度和对应类别概率均置为1
        labels[gridy, gridx, 0:5] = np.array([gridpx, gridpy, bbox[i * 5 + 3], bbox[i * 5 + 4], 1])
        labels[gridy, gridx, 5:10] = np.array([gridpx, gridpy, bbox[i * 5 + 3], bbox[i * 5 + 4], 1])
        labels[gridy, gridx, 10+int(bbox[i*5])] = 1
    return labels

2. YOLOv1网络实现

2.1 Loss函数设计

  为了方便Loss计算,我们将网络输出的一维1470数据reshape成 7 × 7 × 30 7\times7\times30 7×7×30的数据格式,并且,根据上文Dataset类的实现可知,提取出的label(样本标签)也是 7 × 7 × 30 7\times7\times30 7×7×30。但是有一点要注意,label数据经过pytorch的toTensor()函数转换后,数据会变成 b a t c h s i z e × 30 × 7 × 7 batchsize\times30\times7\times7 batchsize×30×7×7,所以网络的输出也应当对应改成 b a t c h s i z e × 30 × 7 × 7 batchsize\times30\times7\times7 batchsize×30×7×7。损失函数的计算函数如下:

class Loss_yolov1(nn.Module):
    def __init__(self):
        super(Loss_yolov1,self).__init__()

    def 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  # 每个网格的bbox数量
        num_cls = 20  # 类别数量
        noobj_confi_loss = 0.  # 不含目标的网格损失(只有置信度损失)
        coor_loss = 0.  # 含有目标的bbox的坐标损失
        obj_confi_loss = 0.  # 含有目标的bbox的置信度损失
        class_loss = 0.  # 含有目标的网格的类别损失
        n_batch = labels.size()[0]  # batchsize的大小

        # 可以考虑用矩阵运算进行优化,提高速度,为了准确起见,这里还是用循环
        for i in range(n_batch):  # batchsize循环
            for n in range(7):  # x方向网格循环
                for m in range(7):  # y方向网格循环
                    if labels[i,4,m,n]==1:# 如果包含物体
                        # 将数据(px,py,w,h)转换为(x1,y1,x2,y2)
                        # 先将px,py转换为cx,cy,即相对网格的位置转换为标准化后实际的bbox中心位置cx,xy
                        # 然后再利用(cx-w/2,cy-h/2,cx+w/2,cy+h/2)转换为xyxy形式,用于计算iou
                        bbox1_pred_xyxy = ((pred[i,0,m,n]+n)/num_gridx - pred[i,2,m,n]/2,(pred[i,1,m,n]+m)/num_gridy - pred[i,3,m,n]/2,
                                           (pred[i,0,m,n]+n)/num_gridx + pred[i,2,m,n]/2,(pred[i,1,m,n]+m)/num_gridy + pred[i,3,m,n]/2)
                        bbox2_pred_xyxy = ((pred[i,5,m,n]+n)/num_gridx - pred[i,7,m,n]/2,(pred[i,6,m,n]+m)/num_gridy - pred[i,8,m,n]/2,
                                           (pred[i,5,m,n]+n)/num_gridx + pred[i,7,m,n]/2,(pred[i,6,m,n]+m)/num_gridy + pred[i,8,m,n]/2)
                        bbox_gt_xyxy = ((labels[i,0,m,n]+n)/num_gridx - labels[i,2,m,n]/2,(labels[i,1,m,n]+m)/num_gridy - labels[i,3,m,n]/2,
                                        (labels[i,0,m,n]+n)/num_gridx + labels[i,2,m,n]/2,(labels[i,1,m,n]+m)/num_gridy + labels[i,3,m,n]/2)
                        iou1 = calculate_iou(bbox1_pred_xyxy,bbox_gt_xyxy)
                        iou2 = calculate_iou(bbox2_pred_xyxy,bbox_gt_xyxy)
                        # 选择iou大的bbox作为负责物体
                        if iou1 >= iou2:
                            coor_loss = coor_loss + 5 * (torch.sum((pred[i,0:2,m,n] - labels[i,0:2,m,n])**2)                                         + torch.sum((pred[i,2:4,m,n].sqrt()-labels[i,2:4,m,n].sqrt())**2))
                            obj_confi_loss = obj_confi_loss + (pred[i,4,m,n] - iou1)**2
                            # iou比较小的bbox不负责预测物体,因此confidence loss算在noobj中,注意,对于标签的置信度应该是iou2
                            noobj_confi_loss = noobj_confi_loss + 0.5 * ((pred[i,9,m,n]-iou2)**2)
                        else:
                            coor_loss = coor_loss + 5 * (torch.sum((pred[i,5:7,m,n] - labels[i,5:7,m,n])**2)                                         + torch.sum((pred[i,7:9,m,n].sqrt()-labels[i,7:9,m,n].sqrt())**2))
                            obj_confi_loss = obj_confi_loss + (pred[i,9,m,n] - iou2)**2
                            # iou比较小的bbox不负责预测物体,因此confidence loss算在noobj中,注意,对于标签的置信度应该是iou1
                            noobj_confi_loss = noobj_confi_loss + 0.5 * ((pred[i, 4, m, n]-iou1) ** 2)
                        class_loss = class_loss + torch.sum((pred[i,10:,m,n] - labels[i,10:,m,n])**2)
                    else:  # 如果不包含物体
                        noobj_confi_loss = noobj_confi_loss + 0.5 * torch.sum(pred[i,[4,9],m,n]**2)

        loss = coor_loss + obj_confi_loss + noobj_confi_loss + class_loss
        # 此处可以写代码验证一下loss的大致计算是否正确,这个要验证起来比较麻烦,比较简洁的办法是,将输入的pred置为全1矩阵,再进行误差检查,会直观很多。
        return loss/n_batch

  计算iou的辅助函数如下,注意输入的bbox信息格式应该是两点式,即左上角点xy坐标和右下角点xy坐标(x1,y1,x2,y2)。

def calculate_iou(bbox1,bbox2):
    """计算bbox1=(x1,y1,x2,y2)和bbox2=(x3,y3,x4,y4)两个bbox的iou"""
    intersect_bbox = [0., 0., 0., 0.]  # bbox1和bbox2的交集
    if bbox1[2]<bbox2[0] or bbox1[0]>bbox2[2] or bbox1[3]<bbox2[1] or bbox1[1]>bbox2[3]:
        pass
    else:
        intersect_bbox[0] = max(bbox1[0],bbox2[0])
        intersect_bbox[1] = max(bbox1[1],bbox2[1])
        intersect_bbox[2] = min(bbox1[2],bbox2[2])
        intersect_bbox[3] = min(bbox1[3],bbox2[3])

    area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])  # bbox1面积
    area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])  # bbox2面积
    area_intersect = (intersect_bbox[2] - intersect_bbox[0]) * (intersect_bbox[3] - intersect_bbox[1])  # 交集面积
    # print(bbox1,bbox2)
    # print(intersect_bbox)
    # input()

    if area_intersect>0:
        return area_intersect / (area1 + area2 - area_intersect)  # 计算iou
    else:
        return 0

2.2 网络结构实现

  这一部分需要说明一下,由于原论文是采用自己设计的20层卷积层先在ImageNet上训练了一周,完成特征提取部分的训练。我们作为学习者而非发明者来说,花一周时间训练实在是太长了。因此,在这里我打算对原论文的结构做一点改变。YOLOv1的前20层是用于特征提取的,也就是随便替换为一个分类网络(除去最后的全连接层)其实都行。因此,我打算使用ResNet34的网络作为特征提取部分。这样做的好处是,pytorch的torchvision中提供了ResNet34的预训练模型,训练集也是ImageNet,等于说有先成训练好的模型可以直接使用,从而免去了特征提取部分的训练时间。然后,除去ResNet34的最后两层,再连接上YOLOv1的最后4个卷积层和两个全连接层,作为我们训练的网络结构。
  此外,还进行了一些小调整,比如最后增加了一个Sigmoid层,以及在卷积层后增加了BN层等等。具体代码如下:

import torchvision.models as tvmodel
import torch.nn as nn
import torch

class YOLOv1_resnet(nn.Module):
    def __init__(self):
        super(YOLOv1_resnet,self).__init__()
        resnet = tvmodel.resnet34(pretrained=True)  # 调用torchvision里的resnet34预训练模型
        resnet_out_channel = resnet.fc.in_features  # 记录resnet全连接层之前的网络输出通道数,方便连入后续卷积网络中
        self.resnet = nn.Sequential(*list(resnet.children())[:-2])  # 去除resnet的最后两层
        # 以下是YOLOv1的最后四个卷积层
        self.Conv_layers = nn.Sequential(
            nn.Conv2d(resnet_out_channel,1024,3,padding=1),
            nn.BatchNorm2d(1024),  # 为了加快训练,这里增加了BN层,原论文里YOLOv1是没有的
            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(),
            nn.Conv2d(1024, 1024, 3, padding=1),
            nn.BatchNorm2d(1024),
            nn.LeakyReLU(),
        )
        # 以下是YOLOv1的最后2个全连接层
        self.Conn_layers = nn.Sequential(
            nn.Linear(7*7*1024,4096),
            nn.LeakyReLU(),
            nn.Linear(4096,7*7*30),
            nn.Sigmoid()  # 增加sigmoid函数是为了将输出全部映射到(0,1)之间,因为如果出现负数或太大的数,后续计算loss会很麻烦
        )

    def forward(self, input):
        input = self.resnet(input)
        input = self.Conv_layers(input)
        input = input.view(input.size()[0],-1)
        input = self.Conn_layers(input)
        return input.reshape(-1, (5*NUM_BBOX+len(CLASSES)), 7, 7)  # 记住最后要reshape一下输出数据

  这里分享一下调试网络结构代码的小技巧。可以直接用如下代码测试,如果程序报错,则在forward每一步之间print(input.size()),看一看每一个模块输出的尺寸是否是满足要求的,如果要调试nn.Sequential里的网络结构,可以直接将不需要的部分注释,然后一步步的反注释,从而验证每一层输出的数据的尺寸是否符合预期。通常,程序报错是因为图像尺寸出现了问题而不是通道数的问题,也就是某一些卷积层可能需要加padding保持图像尺寸,而漏加了padding。

if __name__ == '__main__':
    x = torch.randn((1,3,448,448))
    net = YOLOv1_resnet()
    print(net)
    y = net(x)
    print(y.size())

3. 开始训练啦

  训练过程也是老套路了,这里要注意,因为特征提取部分以及用ImageNet训练过了,所以不需要再训练,需要将resnet34的网络参数冻结,不进行训练,也就是将参数的requires_grad属性设置为False。另外,在每10个epoch后,最好储存一下训练结果,防止中途发生意外而终止训练,这样可以保证有过程备份(学名叫checkpoint),下次可以继续从断点开始训练。具体代码如下:

if __name__ == '__main__':
    epoch = 50
    batchsize = 5
    lr = 0.01

    train_data = VOC2012()
    train_dataloader = DataLoader(VOC2012(is_train=True),batch_size=batchsize,shuffle=True)

    model = YOLOv1_resnet().cuda()
    # model.children()里是按模块(Sequential)提取的子模块,而不是具体到每个层,具体可以参见pytorch帮助文档
    # 冻结resnet34特征提取层,特征提取层不参与参数更新
    for layer in model.children():
        layer.requires_grad = False
        break
    criterion = Loss_yolov1()
    optimizer = torch.optim.SGD(model.parameters(),lr=lr,momentum=0.9,weight_decay=0.0005)

    is_vis = False  # 是否进行可视化,如果没有visdom可以将其设置为false
    if is_vis:
        vis = visdom.Visdom()
        viswin1 = vis.line(np.array([0.]),np.array([0.]),opts=dict(title="Loss/Step",xlabel="100*step",ylabel="Loss"))

    for e in range(epoch):
        model.train()
        yl = torch.Tensor([0]).cuda()
        for i,(inputs,labels) in enumerate(train_dataloader):
            inputs = inputs.cuda()
            labels = labels.float().cuda()
            pred = model(inputs)
            loss = criterion(pred, labels)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            print("Epoch %d/%d| Step %d/%d| Loss: %.2f"%(e,epoch,i,len(train_data)//batchsize,loss))
            yl = yl + loss
            if is_vis and (i+1)%100==0:
                vis.line(np.array([yl.cpu().item()/(i+1)]),np.array([i+e*len(train_data)//batchsize]),win=viswin1,update='append')
        if (e+1)%10==0:
            torch.save(model,"./models_pkl/YOLOv1_epoch"+str(e+1)+".pkl")
            # compute_val_map(model)

  我的渣渣电脑GPU是GTX1060的,所以这个训练起来比较慢,batchsize也只能取5,大概要12个小时。loss函数里循环比较多,可以考虑怎么用矩阵计算优化一下,提高效率,但是要注意检查优化后的loss计算结果,否则进行错误的训练就得不偿失了(别问我为什么知道的,:( )。下图是训练过程的loss,最后大概下降到0.1左右。
在这里插入图片描述

4. 网络预测(Inference)

  网络预测的主代码和训练其实很像,麻烦的地方在于,网络预测得到pred数据后,如何将其转换为最终的bbox结果。因为pred会得到98个bbox信息,而其中大部分是没用的,但是由于是inference任务,所以没标签给你参照,所以只能用别的算法来去除无效的bbox。最常见的做法就是使用class-specific confidence scores的阈值和NMS(非极大值抑制)算法来去除没用的bbox。这种方法有两个超参数,一个是设置class-specific confidence scores的阈值,另一个是NMS算法中的IOU阈值。

if __name__ == '__main__':
    val_dataloader = DataLoader(VOC2012(is_train=False), batch_size=1, shuffle=False)
    model = torch.load("./models_pkl/YOLOv1_epoch40.pkl")  # 加载训练好的模型
    for i,(inputs,labels) in enumerate(val_dataloader):
        inputs = inputs.cuda()
        # 以下代码是测试labels2bbox函数的时候再用
        # labels = labels.float().cuda()
        # labels = labels.squeeze(dim=0)
        # labels = labels.permute((1,2,0))
        pred = model(inputs)  # pred的尺寸是(1,30,7,7)
        pred = pred.squeeze(dim=0)  # 压缩为(30,7,7)
        pred = pred.permute((1,2,0))  # 转换为(7,7,30)

        ## 测试labels2bbox时,使用 labels作为labels2bbox2函数的输入
        bbox = labels2bbox(pred)  # 此处可以用labels代替pred,测试一下输出的bbox是否和标签一样,从而检查labels2bbox函数是否正确。当然,还要注意将数据集改成训练集而不是测试集,因为测试集没有labels。
        inputs = inputs.squeeze(dim=0)  # 输入图像的尺寸是(1,3,448,448),压缩为(3,448,448)
        inputs = inputs.permute((1,2,0))  # 转换为(448,448,3)
        img = inputs.cpu().numpy()
        img = 255*img  # 将图像的数值从(0,1)映射到(0,255)并转为非负整形
        img = img.astype(np.uint8)
        draw_bbox(img,bbox.cpu())  # 将网络预测结果进行可视化,将bbox画在原图中,可以很直观的观察结果
        print(bbox.size(),bbox)
        input()

  其中,labels2bbox()的函数如下:

# 注意检查一下输入数据的格式,到底是xywh还是xyxy
def labels2bbox(matrix):
    """
    将网络输出的7*7*30的数据转换为bbox的(98,25)的格式,然后再将NMS处理后的结果返回
    :param matrix: 注意,输入的数据中,bbox坐标的格式是(px,py,w,h),需要转换为(x1,y1,x2,y2)的格式再输入NMS
    :return: 返回NMS处理后的结果
    """
    if matrix.size()[0:2]!=(7,7):
        raise ValueError("Error: Wrong labels size:",matrix.size())
    bbox = torch.zeros((98,25))
    # 先把7*7*30的数据转变为bbox的(98,25)的格式,其中,bbox信息格式从(px,py,w,h)转换为(x1,y1,x2,y2),方便计算iou
    for i in range(7):  # i是网格的行方向(y方向)
        for j in range(7):  # j是网格的列方向(x方向)
            bbox[2*(i*7+j),0:4] = torch.Tensor([(matrix[i, j, 0] + j) / 7 - matrix[i, j, 2] / 2,
                                                (matrix[i, j, 1] + i) / 7 - matrix[i, j, 3] / 2,
                                                (matrix[i, j, 0] + j) / 7 + matrix[i, j, 2] / 2,
                                                (matrix[i, j, 1] + i) / 7 + matrix[i, j, 3] / 2])
            bbox[2 * (i * 7 + j), 4] = matrix[i,j,4]
            bbox[2*(i*7+j),5:] = matrix[i,j,10:]
            bbox[2*(i*7+j)+1,0:4] = torch.Tensor([(matrix[i, j, 5] + j) / 7 - matrix[i, j, 7] / 2,
                                                (matrix[i, j, 6] + i) / 7 - matrix[i, j, 8] / 2,
                                                (matrix[i, j, 5] + j) / 7 + matrix[i, j, 7] / 2,
                                                (matrix[i, j, 6] + i) / 7 + matrix[i, j, 8] / 2])
            bbox[2 * (i * 7 + j)+1, 4] = matrix[i, j, 9]
            bbox[2*(i*7+j)+1,5:] = matrix[i,j,10:]
    return NMS(bbox)  # 对所有98个bbox执行NMS算法,清理cls-specific confidence score较低以及iou重合度过高的bbox


def NMS(bbox, conf_thresh=0.1, iou_thresh=0.3):
    """bbox数据格式是(n,25),前4个是(x1,y1,x2,y2)的坐标信息,第5个是置信度,后20个是类别概率
    :param conf_thresh: cls-specific confidence score的阈值
    :param iou_thresh: NMS算法中iou的阈值
    """
    n = bbox.size()[0]
    bbox_prob = bbox[:,5:].clone()  # 类别预测的条件概率
    bbox_confi = bbox[:, 4].clone().unsqueeze(1).expand_as(bbox_prob)  # 预测置信度
    bbox_cls_spec_conf = bbox_confi*bbox_prob  # 置信度*类别条件概率=cls-specific confidence score整合了是否有物体及是什么物体的两种信息
    bbox_cls_spec_conf[bbox_cls_spec_conf<=conf_thresh] = 0  # 将低于阈值的bbox忽略
    for c in range(20):
        rank = torch.sort(bbox_cls_spec_conf[:,c],descending=True).indices
        for i in range(98):
            if bbox_cls_spec_conf[rank[i],c]!=0:
                for j in range(i+1,98):
                    if bbox_cls_spec_conf[rank[j],c]!=0:
                        iou = calculate_iou(bbox[rank[i],0:4],bbox[rank[j],0:4])
                        if iou > iou_thresh:  # 根据iou进行非极大值抑制抑制
                            bbox_cls_spec_conf[rank[j],c] = 0
    bbox = bbox[torch.max(bbox_cls_spec_conf,dim=1).values>0]  # 将20个类别中最大的cls-specific confidence score为0的bbox都排除
    bbox_cls_spec_conf = bbox_cls_spec_conf[torch.max(bbox_cls_spec_conf,dim=1).values>0]
    res = torch.ones((bbox.size()[0],6))
    res[:,1:5] = bbox[:,0:4]  # 储存最后的bbox坐标信息
    res[:,0] = torch.argmax(bbox[:,5:],dim=1).int()  # 储存bbox对应的类别信息
    res[:,5] = torch.max(bbox_cls_spec_conf,dim=1).values  # 储存bbox对应的class-specific confidence scores
    return res

  最后,将筛选后的bbox进行可视化,绘制在原图上,就可以查看结果了。可视化的辅助代码如下:

COLOR = [(255,0,0),(255,125,0),(255,255,0),(255,0,125),(255,0,250),
         (255,125,125),(255,125,250),(125,125,0),(0,255,125),(255,0,0),
         (0,0,255),(125,0,255),(0,125,255),(0,255,255),(125,125,255),
         (0,255,0),(125,255,125),(255,255,255),(100,100,100),(0,0,0),]  # 用来标识20个类别的bbox颜色,可自行设定
def draw_bbox(img,bbox):
    """
    根据bbox的信息在图像上绘制bounding box
    :param img: 绘制bbox的图像
    :param bbox: 是(n,6)的尺寸,其中第0列代表bbox的分类序号,1~4为bbox坐标信息(xyxy)(均归一化了),5是bbox的专属类别置信度
    """
    h,w = img.shape[0:2]
    n = bbox.size()[0]
    print(bbox)
    for i in range(n):
        p1 = (w*bbox[i,1], h*bbox[i,2])
        p2 = (w*bbox[i,3], h*bbox[i,4])
        cls_name = CLASSES[int(bbox[i,0])]
        confidence = bbox[i,5]
        cv2.rectangle(img,p1,p2,color=COLOR[int(bbox[i,0])])
        cv2.putText(img,cls_name,p1,cv2.FONT_HERSHEY_SIMPLEX,0.5,(0,0,255))
    cv2.imshow("bbox",img)
    cv2.waitKey(0)

  最后,可以根据预测得到的bbox计算mAP,目前我还没有计算过,计算mAP是挺复杂的过程。等有时间再将这一部分补上吧。

5. 结果展示

  稍微展示一下训练50个epoch后的结果,可以看到,其实在验证集上的效果不是那么完美,但是也还可以。毕竟我们训练的时候还没有进行调参,数据增广也没有做,效果应该还有很大的提升空间。

训练集图片验证集图片
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多