针对YOLOv1的paddle代码解读(二)

1,519 阅读23分钟

自定义数据读取器(第二个重点)

在读取真实框信息时,需要设置用于训练的标签信息,导入from utils import convert_bbox2labels ,设置标签。

设置标签就是确定真实框的中心点落在了哪一个网格中,并将此网格设置为负责当前真实框的预测,把置信度和对应类别概率均设置为1

from paddle.io import Dataset, BatchSampler, DataLoader
import paddle.vision.transforms as T
from utils import convert_bbox2labels # 导入设置标签的函数

class MY_VOC(Dataset):
    def __init__(self,name_path=None, img_Dir='pascalvoc/VOCdevkit/VOC2007/JPEGImages', labels_Dir='pascalvoc/VOCdevkit/VOC2007/labels', is_train=True,is_aug=True):
        """
        name_path: 包含训练集或者验证集或者测试集图像名称的txt文件
        is_train: 调用的是训练集(True),还是验证集(False)
        is_aug:  是否进行数据增广
        """
        self.filenames = []  # 储存数据集的文件名称
        self.is_train = is_train
        if not name_path:
            raise ValueError("name_path有误!")
        with open(name_path, 'r') as f: # 读取包含图像名称的txt文件
            self.filenames = [x.strip() for x in f]
        
        self.imgpath = img_Dir+ '/'  # 原始图像所在的文件夹
        self.labelpath =labels_Dir + "/"  # 图像对应的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))

        # 图像增强部分,这里不做过多处理
        if self.is_aug:
            if self.is_train:
                if random.random()>0.6:
                    aug = T.Compose([T.ColorJitter(0.45,0.07,0.2,0.05), # 调整图像的亮度,对比度,饱和度和色调。
                                     T.ToTensor()])
                else:
                    aug = T.Compose([ T.ToTensor()])
            else:
                aug = T.Compose([T.ToTensor()])
            img = aug(img)  # 将形状为 (H x W x C)的输入数据 PIL.Image 或 numpy.ndarray 转换为 (C x H x W)。 
                            # 如果输入的 numpy.ndarray 数据类型是 'uint8',那个会将输入数据从(0-255)的范围缩放到 (0-1)的范围。
        else:
            aug = T.Compose([ T.ToTensor()])
            img = aug(img)  # 将形状为 (H x W x C)的输入数据 PIL.Image 或 numpy.ndarray 转换为 (C x H x W)。
                            # 如果输入的 numpy.ndarray 数据类型是 'uint8',那个会将输入数据从(0-255)的范围缩放到 (0-1)的范围。

        # 读取图像对应的bbox信息,按1维的方式储存,每5个元素表示一个bbox的(cls,x,y,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]  # 读取到的信息格式为[cls,x,y,w,h, cls,x,y,w,h, cls,x,y,w,h] 每5个元素为一组标签信息
        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):  # len(bbox)//5 表示总共真实框的数目
            if padw != 0:
                bbox[i * 5 + 1] = (bbox[i * 5 + 1] * w + padw) / h # 除以h,是因为在原始图像中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 # 除以w,是因为在原始图像中w为最长边
                bbox[i * 5 + 4] = (bbox[i * 5 + 4] * h) / w

        labels = convert_bbox2labels(bbox,len(CLASSES))  # 将所有bbox的(cls,x,y,w,h)数据转换为训练时方便计算Loss的数据形式(7,7,5*B+cls_num)

        labels = T.ToTensor()(labels)  # 将形状为 (H x W x C)的输入数据 PIL.Image 或 numpy.ndarray ,转换为 (C x H x W)。 
        return img,labels

数据集读取的三个基本步骤:__init____getitem____len__,其中__init__学习构造方式、__getitem__重点分析。

__init__中值得我们学习的部分
self.filenames = []  # 储存数据集的文件名称,列表存储整个数据集
with open(name_path, 'r') as f: # 读取包含图像名称的txt文件,将所有文件名称都放到准备好的集合中
    self.filenames = [x.strip() for x in f]

self.imgpath = img_Dir+ '/'  # 原始图像所在的文件夹
self.labelpath =labels_Dir + "/"  # 图像对应的label文件(.txt文件)的文件夹
__getitem__(重点)
  • Padding
    • 首先是padding的解读,其实就是我们想要将不同的矩形按照最长的边,将整体填充成一个正方形,在将正方形resize成448 * 448大小的图片,填充部分的像素都是0
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))
  • 图像增强的部分
    • 首先区分是否有图像增强参数
      • 有,看是不是训练集
        • 是,生成一个随机数,看是否大于阈值
          • 大于,进行图像增强、转化tensor
          • 不大于,直接转化tensor
        • 没有,直接转化tensor
      • 没有直接转化tensor
if self.is_aug:
    if self.is_train:
        if random.random()>0.6:
            aug = T.Compose([T.ColorJitter(0.45,0.07,0.2,0.05), # 调整图像的亮度,对比度,饱和度和色调。
                             T.ToTensor()])
        else:
            aug = T.Compose([ T.ToTensor()])
    else:
        aug = T.Compose([T.ToTensor()])
    img = aug(img)  # 将形状为 (H x W x C)的输入数据 PIL.Image 或 numpy.ndarray 转换为 (C x H x W)。 
                    # 如果输入的 numpy.ndarray 数据类型是 'uint8',那个会将输入数据从(0-255)的范围缩放到 (0-1)的范围。
else:
    aug = T.Compose([ T.ToTensor()])
    img = aug(img)  # 将形状为 (H x W x C)的输入数据 PIL.Image 或 numpy.ndarray 转换为 (C x H x W)。
                    # 如果输入的 numpy.ndarray 数据类型是 'uint8',那个会将输入数据从(0-255)的范围缩放到 (0-1)的范围。
  • 生成label(重中之重) 1.先看第一个部分我们将labels中数据txt数据,将其构造成[cls,x,y,w,h, cls,x,y,w,h, cls,x,y,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]  # 读取到的信息格式为[cls,x,y,w,h, cls,x,y,w,h, cls,x,y,w,h] 每5个元素为一组标签信息
if len(bbox)%5!=0:
    raise ValueError("File:"+self.labelpath+self.filenames[item]+".txt"+"——bbox Extraction Error!")

2.修正相对坐标

接下来,我们需要对我们之前生成的x,y,w,h进行修正,因为我们之前的相对坐标的计算是根据图片的真实的宽高进行的,前面我们使用padding进行填充,导致了位置的偏移,我们假设之前我们的点在中心点,现在我们对宽进行填充,就会导致我们的点相对中心向左偏移,所以进行padding后,我们需要对相对坐标进行修正,当我们填充的是宽的时候,我们的x,w需要进行相对应的变化;当我们填充的是高的时候,我们的y,h需要进行相对应的变化;

# 修改标签数据
# 根据padding、图像增广等操作,将原始的bbox数据转换为修改后图像的bbox数据
for i in range(len(bbox)//5):  # len(bbox)//5 表示总共真实框的数目
    if padw != 0:
        bbox[i * 5 + 1] = (bbox[i * 5 + 1] * w + padw) / h # 除以h,是因为在原始图像中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 # 除以w,是因为在原始图像中w为最长边
        bbox[i * 5 + 4] = (bbox[i * 5 + 4] * h) / w

3.通过convert_bbox2labels完成label最终构造

labels = convert_bbox2labels(bbox,len(CLASSES))  # 将所有bbox的(cls,x,y,w,h)数据转换为训练时方便计算Loss的数据形式(7,7,5*B+cls_num)

labels = T.ToTensor()(labels)  # 将形状为 (H x W x C)的输入数据 PIL.Image 或 numpy.ndarray ,转换为 (C x H x W)。 
return img,labels

实际上重点就是convert_bbox2labels我们对其源码进行讲解

def convert_bbox2labels(bbox,cls_num):
    """将bbox的(cls,x,y,w,h)数据转换为训练时方便计算Loss的数据形式(7,7,5*B+cls_num)
    cls_num: 类别数
    bbox:包含真实框信息(x,y,w,h),其中x,y表示中心点坐标信息,w,h表示宽高 ,并且数据都已经归一化
    转换为labels后,bbox的信息转换为了(px,py,w,h)格式,其中px,py表示左上角点坐标"""

    gridsize = 1.0/7  # YOLOv1将图片分成7x7的网格
    labels = np.zeros((7,7,2*5+cls_num))  #2表示有两个bbox ,labels此时为numpy的格式,通道数C在最后面
    for i in range(len(bbox)//5):
        gridx = int(bbox[i*5+1] // gridsize)  # 当前bbox中心落在第gridx个网格,列 ,其实计算相当于 x * 7 后取整
        gridy = int(bbox[i*5+2] // gridsize)  # 当前bbox中心落在第gridy个网格,行
        # (gridx,gridy)表示的是真实框的中心点落在了7x7的网格中的一个网格的位置

        # (bbox中心坐标 - 网格坐标)  ==> bbox中心点的相对于网格的偏移量   
        gridpx = bbox[i * 5 + 1] / gridsize - gridx  # 获取中心坐标相对于网格的偏移量
        gridpy = bbox[i * 5 + 2] / gridsize - gridy

        # 设置label
        # 将第gridy行,gridx列的网格,设置为负责当前ground truth的预测,把置信度和对应类别概率均置为1
        # 这里按照 有无物体的置信度+中心点x,y相对于网格的偏移量+ 宽,高 的顺序设置label
        labels[gridy, gridx, 0:5] = np.array([ 1, gridpx, gridpy, bbox[i * 5 + 3], bbox[i * 5 + 4]])  
        labels[gridy, gridx, 5:10] = np.array([1, gridpx, gridpy, bbox[i * 5 + 3], bbox[i * 5 + 4]])  
        labels[gridy, gridx, 10+int(bbox[i*5])] = 1 # 设置对应的类别概率为1

    return labels    # labels此时为numpy的格式,通道数C在最后面
  • 步骤如下:
    • 第一、我们通过labels=np.zeros((7,7,2*5+cls_num)),构造一个容器对真实的标签进行存储
    • 第二、我们传入的数据格式是[cls,x,y,w,h, cls,x,y,w,h, cls,x,y,w,h....],每五个一组cls,x,y,w,h进行遍历,
    • 第三、gridx = int(bbox[i*5+1] // gridsize) 因为gridsize是1/7,所以是相对位置 * 7,找出来他在7 * 7 网格中的位置,但是int操作之后就是能看到,x落在第几列,同理y也是如此,(gridx,gridy)得到的是方格左上角的坐标
    • 第四、gridpx = bbox[i * 5 + 1] / gridsize - gridx,我们使用相对左边 * 7得到在7 * 7 网格的真实位置,在减去左上角的坐标,得到的是点在这个方格中,距离左上角的点的相对位置,也就是中心坐标相对于网格的偏移量,同理y也是如此
      • image.png
    • 第五、labels[gridy, gridx, 0:5] = np.array([ 1, gridpx, gridpy, bbox[i * 5 + 3], bbox[i * 5 + 4]]),因为上面的那个点落到了那个放个中,那个放个就负责预测这个物体,所以labels[gridy, gridx, 0:5]我们可以看到首先是gridy,gridx找到这个格子,然后将这个格子的0:5的位置放上置信度1,偏移量x,偏移量y,padding后的相对宽度,padding后的相对高度
    • 第六、labels[gridy, gridx, 5:10] = np.array([1, gridpx, gridpy, bbox[i * 5 + 3], bbox[i * 5 + 4]])是因为一个格子有两个bounding box,注意在labels中0~5 和 5 ~ 10中的数据是一样的
    • 第七、labels[gridy, gridx, 10+int(bbox[i*5])] = 1 再将对应的20个class中对应类的值设置成1,这里也可以知道这个部分是两个框共享class信息。
    • 第八、完成上面循环最终label才算构建完成,和cv读取的image信息一起返回回去

到这里,自定义数据读取器全部解析完成

损失函数(重中之重)

已经知道一张448x448大小的图片经过YOLO网络后,会得到30x7x7的特征图,每个特征点会对应7x7的网格中的一个网格。一个特征点对应一个长为30的向量。得到特征图后就可以根据真实框求损失函数了。

首先看一下原文中的一段描述

标黄的句子的意思先将输入图像划分为一个S×S网格(7×7)。如果一个对象的中心落在一个网格单元中,该网格单元负责检测该对象。

‘如果一个对象的中心落在一个网格单元中,该网格单元负责检测该对象’这句话用下图说明:

图中x与y分别表示的是偏移量,数值在0 ~ 1之间,比如上图中(2,5)是特征点的整数部分,加上偏移量后,最终预测的中心点坐标就是(2+x,5+y)。 图中的w,h也是相对于图片大小的相对值,数值在0 ~ 1之间

图中显示(2,5)处得到的向量中有2个bounding boxes,那么那哪个bounding boxes会参与损失计算呢,论文中是这样写的:

意思是说在预测过程中,每个网格会有对应的两个bounding boxes,这两个bounding boxes中,哪个bounding boxes与真实框有更高的IOU值,哪个bounding boxes就负责预测,也就是此时的bounding boxes可以参与损失计算。在YOLOv1中,是这样处理这个问题的,并没有固定一个bounding boxes负责预测真实框

图中第一段标黄的部分,意思是在训练时,希望负责进行预测的那个bounding boxes中的置信等于预测过程中的那个最大的IOU值

YOLOv1网络的损失函数都是均方差

对损失函数中关于w与h的开方,做出如下解释:

from utils import calculate_iou  # 导入计算IOU的函数

def getloss(pred, labels):
    """
    pred: (batchsize,30,7,7)的网络输出数据 通道数30包含信息为:(表示有无物体的置信度,x,y,w,h,表示有无物体的置信度,x,y,w,h,20个类别概率)
    labels: (batchsize,30,7,7)的样本标签数据 通道数30包含信息为:(表示有无物体的置信度,x,y,w,h,表示有无物体的置信度,x,y,w,h,20个类别概率)
    pred 与 labels均为 tensor
    返回当前批次样本的平均损失
    """
    num_gridx, num_gridy = labels.shape[-2:]  # 划分的网格数量 7 x 7
    # 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.shape[0]  # batchsize的大小

    for i in range(n_batch):  # batchsize循环
        for n in range(7):  # x方向网格循环
            for m in range(7):  # y方向网格循环
                if labels[i,0,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,1,m,n]+n)/num_gridx - pred[i,3,m,n]/2,(pred[i,2,m,n]+m)/num_gridy - pred[i,4,m,n]/2,
                                        (pred[i,1,m,n]+n)/num_gridx + pred[i,3,m,n]/2,(pred[i,2,m,n]+m)/num_gridy + pred[i,4,m,n]/2)
                    bbox2_pred_xyxy = ((pred[i,6,m,n]+n)/num_gridx - pred[i,8,m,n]/2,(pred[i,7,m,n]+m)/num_gridy - pred[i,9,m,n]/2,
                                        (pred[i,6,m,n]+n)/num_gridx + pred[i,8,m,n]/2,(pred[i,7,m,n]+m)/num_gridy + pred[i,9,m,n]/2)
                    bbox_gt_xyxy = ((labels[i,1,m,n]+n)/num_gridx - labels[i,3,m,n]/2,(labels[i,2,m,n]+m)/num_gridy - labels[i,4,m,n]/2,
                                    (labels[i,1,m,n]+n)/num_gridx + labels[i,3,m,n]/2,(labels[i,2,m,n]+m)/num_gridy + labels[i,4,m,n]/2)
                    iou1 = calculate_iou(bbox1_pred_xyxy,bbox_gt_xyxy) # 计算IOU值
                    iou2 = calculate_iou(bbox2_pred_xyxy,bbox_gt_xyxy) # 计算IOU值

                    # 选择iou大的pred_bbox负责预测
                    if iou1 >= iou2:
                        coor_loss = coor_loss + 5 * (paddle.sum((pred[i,1:3,m,n] - labels[i,1:3,m,n])**2)+ paddle.sum((pred[i,3:5,m,n].sqrt()-labels[i,3:5,m,n].sqrt())**2))
                        obj_confi_loss = obj_confi_loss + (pred[i,0,m,n] - iou1)**2
                        # iou比较小的bbox不负责预测物体,因此置信度损失算在noobj中,此时,对于标签的置信度应该是iou2
                        noobj_confi_loss = noobj_confi_loss + 0.5 * ((pred[i,5,m,n]-iou2)**2)
                    else:
                        coor_loss = coor_loss + 5 * (paddle.sum((pred[i,6:8,m,n] - labels[i,6:8,m,n])**2) \
                                    + paddle.sum((pred[i,8:10,m,n].sqrt()-labels[i,8:10,m,n].sqrt())**2))
                        obj_confi_loss = obj_confi_loss + (pred[i,5,m,n] - iou2)**2
                        # iou比较小的bbox不负责预测物体,因此置信度损失算在noobj中,此时,对于标签的置信度应该是iou1
                        noobj_confi_loss = noobj_confi_loss + 0.5 * ((pred[i, 0, m, n]-iou1) ** 2)
                    # 计算类别损失
                    class_loss = class_loss + paddle.sum((pred[i,10:,m,n] - labels[i,10:,m,n])**2)
                else:  # 如果不包含物体,就只计算置信度损失
                    noobj_confi_loss = noobj_confi_loss + 0.5 * paddle.sum(pred[i,0,m,n]**2+pred[i,5,m,n]**2)

    loss = coor_loss + obj_confi_loss + noobj_confi_loss + class_loss
    return loss/n_batch

准备工作,对各种损失进行定义

noobj_confi_loss = 0.  # 不含目标的网格损失(只有置信度损失)
coor_loss = 0.  # 含有目标的bbox的坐标损失
obj_confi_loss = 0.  # 含有目标的bbox的置信度损失
class_loss = 0.  # 含有目标的网格的类别损失
n_batch = labels.shape[0]  # batchsize的大小

重点部分在这个三层循环,第一层循环是batchsize,第二层循环是格子x,第三层循环是格子y,重点分析循环内部代码

  • if labels[i,0,m,n]== 1方格中包含物体
    • 包含,偏移量还原相对坐标比较麻烦放在了下边,接下来比较两个iou的值
      • iou面积比较
        • iou1大,coor_loss、obj_confi_loss、noobj_confi_loss的计算在下面
        • 否则反过来
      • class_loss的计算在下面
    • 不包含,直接image.png计算损失,其中Ci^\hat{C_i},是0,就是我们看到的noobj_confi_loss = noobj_confi_loss + 0.5 * paddle.sum(pred[i,0,m,n]**2+pred[i,5,m,n]**2).

偏移量还原相对坐标
bbox1_pred_xyxy = ((pred[i,1,m,n]+n)/num_gridx - pred[i,3,m,n]/2,(pred[i,2,m,n]+m)/num_gridy - pred[i,4,m,n]/2,(pred[i,1,m,n]+n)/num_gridx + pred[i,3,m,n]/2,(pred[i,2,m,n]+m)/num_gridy + pred[i,4,m,n]/2) 这个部分是将偏移量转化成xy的部分,(pred[i,1,m,n]+n)偏移量加上左上角的值,是实际的坐标,(pred[i,1,m,n]+n)/num_gridx是相对坐标,(pred[i,1,m,n]+n)/num_gridx - pred[i,3,m,n]/2,相对坐标和宽的一半相减就是左上角xmin的坐标,同理ymin,xmax,ymax的坐标计算方法相同,两个框都要进行计算
bbox_gt_xyxy = ((labels[i,1,m,n]+n)/num_gridx - labels[i,3,m,n]/2,(labels[i,2,m,n]+m)/num_gridy - labels[i,4,m,n]/2,(labels[i,1,m,n]+n)/num_gridx + labels[i,3,m,n]/2,(labels[i,2,m,n]+m)/num_gridy + labels[i,4,m,n]/2)的计算方式和bbox1_pred_xyxy是一致的,我们在这里只需要制作一个bbox_gt_xyxy即可,是因为我们在上面构造的时候0 ~ 5和5 ~ 10中的数据都是一样的,所以计算一个即可 直接调用calculate_iou算法即可计算出来iou的值

coor_loss的计算
coor_loss = coor_loss + 5 * (paddle.sum((pred[i,1:3,m,n] - labels[i,1:3,m,n])**2)+ paddle.sum((pred[i,3:5,m,n].sqrt()-labels[i,3:5,m,n].sqrt())**2)) image.png

obj_confi_loss的计算
obj_confi_loss = obj_confi_loss + (pred[i,0,m,n] - iou1)**2image.png 我们可以看到后一项是iou1,p(object)为1,所以原本是confidence = p(object) * iou1,就直接变成了confdience = iou1

noobj_confi_loss的计算
noobj_confi_loss = noobj_confi_loss + 0.5 * ((pred[i,5,m,n]-iou2)**2) iou比较小的bbox不负责预测物体,因此置信度损失算在noobj中,此时,对于标签的置信度应该是iou2 image.png 我们可以看到后一项是iou2,p(object)为1,所以原本是confidence = p(object) * iou2,就直接变成了confdience = iou2

class_loss的计算
class_loss = class_loss + paddle.sum((pred[i,10:,m,n] - labels[i,10:,m,n])**2) image.png这个的计算比较直观

到这里,损失函数全部解析完成

训练过程

训练过程比较简单,代码格式比较固定,不做过多解释

if __name__ == '__main__':
    epoch = 135
    batchsize = 128
    lr = 0.0001
    lowest_loss = paddle.to_tensor([100.0])

    train_dataset = MY_VOC(name_path='pascalvoc/VOCdevkit/VOC2007/ImageSets/Main/train.txt')
    train_dataloader = DataLoader(train_dataset,batch_size=batchsize,shuffle =True,drop_last=True)

    # val_dataset = MY_VOC(name_path='pascalvoc/VOCdevkit/VOC2007/ImageSets/Main/val.txt',is_train=False)
    # val_dataloader = DataLoader(val_dataset, batch_size=1, shuffle=True,drop_last=True)

    model = YOLOv1()
    optimizer = paddle.optimizer.Momentum(learning_rate=lr, momentum=0.9,parameters=model.parameters(),weight_decay=0.0005)

    writer = LogWriter(logdir="./log/train") # 初始化一个记录器
    for e in range(epoch):
        #训练
        model.train()
        loss1 = paddle.to_tensor([0.0]) # 记录训练时的损失
        loss2 = paddle.to_tensor([0.0]) # 记录验证时的损失
        avg_loss = 0.0
        for i,(inputs,labels) in enumerate(train_dataloader):
            pred = model(inputs)
            loss = getloss(pred, labels)
            optimizer.clear_grad()
            loss.backward()
            optimizer.step()
            loss1 = loss1 + loss
            if i % 10 == 0:  # 每10步输出一次
                timestring = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(time.time()))
                print('{}[TRAIN]Epoch {}/{}| iter {}/{}| Loss: {}'.format(timestring, e,epoch,i,len(train_dataloader),loss.numpy()))
                # print("Epoch %d/%d| Step %d/%d| Loss: %.2f"%(e,epoch,i,len(train_dataloader),loss))
        avg_loss = loss1/len(train_dataloader) # 计算一个epoch的平均损失
        writer.add_scalar(tag="train_loss", step=e, value=avg_loss)

        if (e+1)%10==0:
            paddle.save(model.state_dict(),"work/models/last.pdparams")
        if (e+1)>10:
            if lowest_loss>avg_loss:
                lowest_loss = avg_loss
                paddle.save(model.state_dict(),"work/models/best.pdparams") # 保存最好的模型
                print("best model is saved  at Epoch {}/{}, lowest_loss is {}".format(e,epoch,lowest_loss.numpy()))
    paddle.save(model.state_dict(),"work/models/last.pdparams")

训练过程效果如图所示: image.png

image.png

image.png

后处理(NMS去除重叠框)

在测试阶段,类别概率乘以置信度得到class-specific confidence scores,得到的这个分数将会用于后续的 NMS 处理。

def pred2bbox(pred):
    """
    将网络输出的7*7*30的数据转换为bbox的(98,25)的格式,然后再将NMS处理后的结果返回
    pred: 数据类型为numpy ,包含的信息为 (表示有无物体的置信度,x,y,w,h,表示有无物体的置信度,x,y,w,h,20个类别概率)
          输入的数据中,bbox坐标的格式是(px,py,w,h)(中心点坐标+宽高),需要转换为(x1,y1,x2,y2)的格式再输入NMS 
    return: 返回NMS处理后的结果
    """
    if pred.shape[0:2]!=(7, 7):
        raise ValueError("Error: Wrong labels size:",pred.shape)

    bbox = np.zeros((98,25))   # 98 = 7*7*2 ,2表示有两个bbox  , 
                               #25 = 4+1+20  ,25表示一个bbox中有25个元素,包含信息为(表示有无物体的置信度,x,y,w,h,,20个类别概率)

    # 先把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         
            bbox[2*(i*7+j),0] = pred[i,j,0] # 获取置信度           
            bbox[2*(i*7+j),1] = (pred[i, j, 1] + j) / 7 - pred[i, j, 3] / 2  # 中心点坐标
            bbox[2*(i*7+j),2] = (pred[i, j, 2] + i) / 7 - pred[i, j, 4] / 2  # 中心点坐标
            bbox[2*(i*7+j),3] = (pred[i, j, 1] + j) / 7 + pred[i, j, 3] / 2  # 宽
            bbox[2*(i*7+j),4] = (pred[i, j, 2] + i) / 7 + pred[i, j, 4] / 2  # 高
            bbox[2*(i*7+j),5:] = pred[i,j,10:]  # 类别概率

            # 再处理第二个bbox
            bbox[2*(i*7+j)+1,0] = pred[i, j, 5]
            bbox[2*(i*7+j)+1,1] = (pred[i, j, 6] + j) / 7 - pred[i, j, 8] / 2
            bbox[2*(i*7+j)+1,2] = (pred[i, j, 7] + i) / 7 - pred[i, j, 9] / 2
            bbox[2*(i*7+j)+1,3] = (pred[i, j, 6] + j) / 7 + pred[i, j, 8] / 2
            bbox[2*(i*7+j)+1,4] = (pred[i, j, 7] + i) / 7 + pred[i, j, 9] / 2
            bbox[2*(i*7+j)+1,5:] = pred[i,j,10:]
    return NMS(bbox)  # 对所有98个bbox执行NMS算法,清理置信度较低以及iou重合度过高的bbox

这个将网络输出的7 * 7 * 30的数据转换为bbox的(98,25)的格式,然后再将NMS处理后的结果返回(我们的网络预测出来的东西是px,py,w,h,px是偏移量)

  • 第一、bbox = np.zeros((98,25))准备一个98 * 25的数据容器,用来装7 * 7 * 2 * 25 (c,x,y,w,h,class_20)的数据
  • 第二、我们针对7 * 7 进行循环,获取第一个框bbox的置信度、将中心点坐标x,中心点坐标y,宽w,高h 转化成bbox为(x1,y1,x2,y2),类别概率20,然后在获得第二个框的上述25个数据,最终循环结束bbox就是98 * 25 的数据。
  • 第三、输入到NMS中去掉相互重叠的框

NMS 非极大值抑制

def NMS(bbox, conf_thresh=0.1, iou_thresh=0.5):
    """bbox数据格式是(n,25),第1个是置信度,第2——5个是(x1,y1,x2,y2)的坐标信息,后20个是类别概率
    conf_thresh: cls-specific confidence score的阈值
    iou_thresh: NMS算法中iou的阈值
    """
    n = bbox.shape[0]
    bbox_prob = bbox[:,5:]  # 获取类别预测的条件概率
    bbox_confi = np.expand_dims(bbox[:, 0], 1)  # 获取预测置信度
    bbox_cls_spec_conf = bbox_confi*bbox_prob  # 置信度*类别预测的条件概率=cls-specific confidence score 整合了是否有物体及是什么物体的两种信息
    bbox_cls_spec_conf[bbox_cls_spec_conf<=conf_thresh] = 0  # 将低于阈值的bbox忽略  shape = (98,20)
    for c in range(20):
        rank = np.argsort(bbox_cls_spec_conf[:,c], axis=-1)
        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],1:5],bbox[rank[j],1:5])
                        if iou > iou_thresh:  # 根据iou进行非极大值抑制抑制
                            bbox_cls_spec_conf[rank[j],c] = 0
    bbox = bbox[np.max(bbox_cls_spec_conf,axis=-1)>0]  # 将20个类别中最大的cls-specific confidence score为0的bbox都排除
    bbox_cls_spec_conf = bbox_cls_spec_conf[np.max(bbox_cls_spec_conf,axis=-1)>0]
    res = np.ones((bbox.shape[0],6))
    res[:,1:5] = bbox[:,0:4]  # 储存最后的bbox坐标信息
    res[:,0] = np.argmax(bbox[:,5:],axis=1)  # 储存bbox对应的类别信息
    res[:,5] = np.max(bbox_cls_spec_conf,axis=1)  # 储存bbox对应的class-specific confidence scores
    return res
  • 首先我们现将需要的数据进行整合
n = bbox.shape[0] # 98
bbox_prob = bbox[:,5:]  # 获取类别预测的条件概率
bbox_confi = np.expand_dims(bbox[:, 0], 1)  # 获取预测置信度
bbox_cls_spec_conf = bbox_confi*bbox_prob  # 置信度*类别预测的条件概率=cls-specific confidence score 整合了是否有物体及是什么物体的两种信息
bbox_cls_spec_conf[bbox_cls_spec_conf<=conf_thresh] = 0  # 将低于阈值的bbox忽略  shape = (98,20)

bbox_cls_spec_conf = bbox_confi*bbox_prob特别注意一下这个部分 P(class|object) * P(object) 得到的是这个框中是否有物体,并且物体是i类的可信程度 bbox_cls_spec_conf[bbox_cls_spec_conf<=conf_thresh] = 0 mask的写法值得借鉴

  • 对20个类别进行循环,按照置信度进行排序,然后我们对98个数据进行循环,取出来第一个,和后面的不为零的数据进行计算IOU,假设大于设定阈值,就把后面的设置成0,再向后搜索直到搜索到0为止
for c in range(20):
    rank = np.argsort(bbox_cls_spec_conf[:,c], axis=-1)
    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],1:5],bbox[rank[j],1:5])
                    if iou > iou_thresh:  # 根据iou进行非极大值抑制抑制
                        bbox_cls_spec_conf[rank[j],c] = 0
  • 将20个类别中最大的cls-specific confidence score为0的bbox都排除,重新构建一个新的矩阵,res = np.ones((bbox.shape[0],6))矩阵大小是剩余的框数量 * 6,这6维存储的信息分别是类别信息、x1,y1,x2,y2,和最后的置信度信息
bbox = bbox[np.max(bbox_cls_spec_conf,axis=-1)>0]  # 将20个类别中最大的cls-specific confidence score为0的bbox都排除
bbox_cls_spec_conf = bbox_cls_spec_conf[np.max(bbox_cls_spec_conf,axis=-1)>0]
res = np.ones((bbox.shape[0],6))
res[:,1:5] = bbox[:,0:4]  # 储存最后的bbox坐标信息
res[:,0] = np.argmax(bbox[:,5:],axis=1)  # 储存bbox对应的类别信息
res[:,5] = np.max(bbox_cls_spec_conf,axis=1)  # 储存bbox对应的class-specific confidence 

验证

  • 在图片上绘制框
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
    img: 图像
    bbox: 是(n,6)的尺寸,其中第0列代表bbox的分类序号,1~4为bbox坐标信息(xyxy)(均归一化了),5是bbox的专属类别置信度
    """
    h,w = img.shape[0:2]
    n = bbox.shape[0]
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    for i in range(n):
        p1 = (int(w*bbox[i,1]), int(h*bbox[i,2]))   # 还原真实的坐标
        p2 = (int(w*bbox[i,3]), int(h*bbox[i,4]))
        cls_name = CLASSES[int(bbox[i,0])]
        confidence = bbox[i,5]
        cv2.rectangle(img,p1,p2,COLOR[int(bbox[i,0])],3)
        cv2.putText(img,cls_name,p1,cv2.FONT_HERSHEY_SIMPLEX,1.5,COLOR[int(bbox[i,0])],2)
    plt.imshow(img)
  • 图片验证展示(这个是作者的结果展示)
for i,(inputs,labels) in enumerate(train_dataloader):
    pred = model(inputs)  # pred的尺寸是(1,30,7,7)
    pred = pred.squeeze(axis=0)  # 压缩为(30,7,7)
    pred = paddle.transpose(pred, perm=(1,2,0))  # 转换为(7,7,30)
    bbox = pred2bbox(pred.numpy())
    inputs = inputs.squeeze(axis=0)  # 输入图像的尺寸是(1,3,448,448),压缩为(3,448,448)
    inputs = paddle.transpose(inputs, perm=(1,2,0))  # 转换为(448,448,3)
    img = inputs.numpy()
    img = 255*img  
    img = img.astype(np.uint8)
    draw_bbox(img,bbox)  # 将网络预测结果进行可视化,将bbox画在原图中
    print(bbox)
    break

效果如图所示

image.png

我自己将训练的轮次提升到300轮(时间耗时差不多16个小时),训练的效果如下:

image.png

image.png

但是效果依旧不好:

image.png

image.png