自定义数据读取器
(第二个重点)
在读取真实框信息时,需要设置用于训练的标签信息,导入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也是如此 - 第五、
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的计算在下面
- iou面积比较
- 不包含,直接
计算损失,其中,是0,就是我们看到的
noobj_confi_loss = noobj_confi_loss + 0.5 * paddle.sum(pred[i,0,m,n]**2+pred[i,5,m,n]**2)
.
- 包含,偏移量还原相对坐标比较麻烦放在了下边,接下来比较两个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)
这个部分是将偏移量转化成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))
obj_confi_loss的计算
obj_confi_loss = obj_confi_loss + (pred[i,0,m,n] - iou1)**2
我们可以看到后一项是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我们可以看到后一项是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)
这个的计算比较直观
到这里,损失函数全部解析完成
训练过程
训练过程比较简单,代码格式比较固定,不做过多解释
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")
训练过程效果如图所示:
后处理(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
效果如图所示
我自己将训练的轮次提升到300轮(时间耗时差不多16个小时),训练的效果如下:
但是效果依旧不好: