016、Mosaic加MixUp 数据增强源码拆解:拼接逻辑、概率控制与标注框变换
016、Mosaic加MixUp 数据增强源码拆解拼接逻辑、概率控制与标注框变换去年有个项目训练YOLOv5检测小目标mAP死活卡在0.65上不去。我翻来覆去调学习率、改anchor折腾了两天最后发现是数据增强的Mosaic概率被我手贱改成了0.3——默认是1.0。改回去之后mAP直接跳到0.72。这事让我意识到很多人对Mosaic和MixUp的理解停留在“把四张图拼一起”的层面但源码里那些概率控制、标注框裁剪的逻辑才是真正影响训练效果的关键。今天直接拆YOLOv5的load_mosaic和mixup实现版本是v6.0代码路径在utils/datasets.py里。我会把每个if判断、每个坐标变换都讲清楚包括我踩过的坑。Mosaic拼接不是简单拼图是带随机裁剪的暴力数据增强先看核心函数load_mosaic。它的输入是一张图片的索引输出是一张拼接好的马赛克图。很多人以为Mosaic就是把四张图等分拼成2x2网格但源码里根本不是这样——它用了随机中心点。defload_mosaic(self,index):# labels4是拼接后的标注segments4是分割掩码如果有labels4,segments4[],[]sself.img_size# 目标尺寸比如640# 随机生成马赛克中心点坐标范围是[s//2, s*1.5 - s//2]# 这里踩过坑中心点不是固定在图中心而是随机偏移yc,xc(int(random.uniform(-x,2*sx))forxin...)# 实际代码更复杂注意这个中心点生成逻辑。YOLOv5源码里中心点yc, xc是从[-s//2, s*1.5]范围内随机取的。这意味着拼接后的四张图它们的中心并不在最终图像的几何中心而是随机偏移。这样做的好处是每张图被裁剪的比例不同模型被迫学习不同尺度的特征对小目标尤其友好。接下来是加载四张图片。索引index是当前图片另外三张从数据集中随机取但有个细节如果数据集是self.indices已经shuffle过的取相邻索引的图片这样能保证每个epoch里拼接组合不同。# 加载四张图索引分别是index, index1, index2, index3# 但实际代码里用了取模防止越界indices[index][random.randint(0,len(self.labels)-1)for_inrange(3)]然后对每张图做处理。关键步骤是计算每张图在最终马赛克中的放置位置。以第一张图左上角为例# 第一张图放在左上角它的右下角坐标是(xc, yc)# 所以它的左上角坐标是(xc - w, yc - h)其中w,h是原图尺寸# 但这里有个坑如果原图尺寸大于xc或yc左上角坐标会变成负数# 所以后面要用np.clip裁剪x1a,y1a,x2a,y2amax(xc-w,0),max(yc-h,0),xc,yc这个坐标计算逻辑我当初看的时候绕了半天。简单说每张图在最终马赛克中的位置是由中心点(xc, yc)决定的。第一张图索引0的右下角对齐中心点第二张图索引1的左下角对齐中心点第三张图索引2的右上角对齐中心点第四张图索引3的左上角对齐中心点。这样四张图正好拼成一个完整的矩形。但问题来了如果原图尺寸很大比如1920x1080而马赛克目标尺寸是640x640那么原图放在马赛克中时大部分区域会被裁剪掉。这就是Mosaic的暴力之处——它强制模型从局部特征中学习。标注框变换坐标映射与裁剪图片拼好了标注框怎么办不能直接把原图的标注框拿过来用因为每张图在马赛克中的位置变了而且可能被裁剪。# 对每张图的标注框做平移和裁剪# 假设原图标注框坐标是(x1, y1, x2, y2)归一化到[0,1]# 先反归一化到原图尺寸boxeslabels[:,1:5].copy()boxes[:,0]boxes[:,0]*w# x1boxes[:,2]boxes[:,2]*w# x2boxes[:,1]boxes[:,1]*h# y1boxes[:,3]boxes[:,3]*h# y2然后做平移变换。比如第一张图它的左上角在马赛克中的坐标是(x1a, y1a)所以标注框的坐标要加上这个偏移量boxes[:,[0,2]]x1a# x坐标偏移boxes[:,[1,3]]y1a# y坐标偏移但别忘了原图可能有一部分被裁剪掉了因为马赛克边界。所以还要对标注框做裁剪# 裁剪到马赛克图像范围内boxes[:,0]np.clip(boxes[:,0],x1a,x2a)boxes[:,1]np.clip(boxes[:,1],y1a,y2a)boxes[:,2]np.clip(boxes[:,2],x1a,x2a)boxes[:,3]np.clip(boxes[:,3],y1a,y2a)这里有个容易忽略的点裁剪后标注框的面积可能变得很小甚至变成无效框比如宽或高为0。YOLOv5的处理方式是计算裁剪后的宽高如果小于某个阈值比如2像素就丢弃这个标注。# 别这样写直接判断宽高0# 应该用面积阈值因为1像素宽的框对训练没意义wboxes[:,2]-boxes[:,0]hboxes[:,3]-boxes[:,1]valid(w2)(h2)labelslabels[valid]boxesboxes[valid]概率控制不是每次都用MosaicYOLOv5的Mosaic不是100%触发的。在训练的前10个epochMosaic概率是1.0之后逐渐降低。这个设计是为了让模型在后期适应更真实的单图分布。# 在train.py中每个batch前判断是否使用Mosaicifself.mosaicandrandom.random()self.mosaic_prob:# 使用Mosaicimages,labelsself.load_mosaic(index)else:# 使用普通单图加载images,labelsself.load_image(index)mosaic_prob的默认值是1.0但可以通过配置文件调整。我建议在训练小目标数据集时保持1.0因为Mosaic对小目标的提升非常明显。但如果你的数据集里目标尺寸分布很均匀可以适当降低到0.5-0.8避免模型过度依赖拼接特征。MixUp两张图的叠加标注框合并MixUp是YOLOv5 v5.0之后加入的增强它和Mosaic是串联的——先做Mosaic再做MixUp。源码里是这样defload_mixup(self,index):# 先加载另一张图可以是Mosaic后的也可以是单图ifself.mosaicandrandom.random()self.mosaic_prob:img2,labels2self.load_mosaic(random.randint(0,len(self.labels)-1))else:img2,labels2self.load_image(random.randint(0,len(self.labels)-1))# 混合比例从Beta分布采样rnp.random.beta(32.0,32.0)# 这个参数可以调默认32.0# 混合图像img(img*rimg2*(1-r)).astype(np.uint8)# 合并标注框labelsnp.concatenate((labels,labels2),axis0)注意这个r的取值。Beta(32, 32)的分布非常集中大部分值在0.4-0.6之间这意味着两张图的混合比例接近1:1。如果你想让混合更极端比如一张图占90%可以把参数调小比如Beta(2, 2)。MixUp的标注框合并很简单就是直接把两张图的标注框拼在一起。但有个问题如果两张图里有相同类别的目标模型可能会混淆。实际训练中MixUp对分类任务的提升更明显对检测任务的效果取决于数据集。我个人的经验是如果数据集类别不平衡MixUp能缓解如果类别分布均匀MixUp的收益不大甚至可能降低mAP。标注框变换的细节归一化与反归一化整个数据增强流程中标注框的坐标变换是最容易出bug的地方。YOLOv5的标注框存储格式是(class_id, x_center, y_center, width, height)全部归一化到[0,1]。但在Mosaic和MixUp中需要先反归一化到像素坐标做完变换后再归一化回去。# 反归一化boxes[:,0]boxes[:,0]*w# x_center - 像素坐标boxes[:,1]boxes[:,1]*h# y_centerboxes[:,2]boxes[:,2]*w# widthboxes[:,3]boxes[:,3]*h# height# 转换为(x1, y1, x2, y2)格式方便裁剪x1boxes[:,0]-boxes[:,2]/2y1boxes[:,1]-boxes[:,3]/2x2boxes[:,0]boxes[:,2]/2y2boxes[:,1]boxes[:,3]/2# 裁剪后再转回(x_center, y_center, width, height)boxes[:,0](x1x2)/2boxes[:,1](y1y2)/2boxes[:,2]x2-x1 boxes[:,3]y2-y1# 归一化到马赛克图像尺寸boxes[:,[0,2]]/s# s是马赛克尺寸boxes[:,[1,3]]/s这里有个容易犯的错误裁剪后标注框的中心点可能不在原图中心但归一化时用的是马赛克图像的尺寸s而不是原图尺寸。所以如果裁剪后的标注框靠近马赛克边缘它的归一化坐标会偏小这是正确的。个人经验什么时候该调什么时候不该调Mosaic概率不要轻易改。默认1.0是最优的除非你的数据集全是小目标且背景单一可以降到0.8。我见过有人改成0.3结果mAP掉了5个点。MixUp的Beta参数。默认32.0是经验值但如果你发现训练初期loss下降太慢可以改成8.0或16.0让混合比例更随机。不过别低于2.0否则混合太极端模型学不到稳定特征。标注框裁剪阈值。源码里是2像素但如果你训练的是超大分辨率图像比如4K可以提高到5-10像素。反之如果是小目标数据集保持2像素别动。Mosaic和MixUp的串联顺序。YOLOv5是先Mosaic再MixUp这个顺序不要改。如果先MixUp再Mosaic两张混合后的图再拼接标注框会乱成一团。调试技巧。如果你怀疑数据增强有问题可以在训练脚本里加一行cv2.imwrite(debug.jpg, img)把增强后的图像保存下来肉眼检查标注框是否对齐。我每次改数据增强代码都会这么做能省下大量排查时间。最后说一句数据增强不是越多越好。Mosaic和MixUp的组合已经很强了再加其他增强比如CutOut、RandAugment反而可能过拟合。我一般只保留Mosaic、MixUp、HSV抖动和随机缩放其他都关掉。