016、Mosaic加MixUp 数据增强源码拆解:拼接逻辑、概率控制与标注框变换
2026/6/4 19:43:42 网站建设 项目流程

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=[],[]s=self.img_size# 目标尺寸,比如640# 随机生成马赛克中心点坐标,范围是[s//2, s*1.5 - s//2]# 这里踩过坑:中心点不是固定在图中心,而是随机偏移yc,xc=(int(random.uniform(-x,2*s+x))forxin...)# 实际代码更复杂

注意这个中心点生成逻辑。YOLOv5源码里,中心点yc, xc是从[-s//2, s*1.5]范围内随机取的。这意味着拼接后的四张图,它们的中心并不在最终图像的几何中心,而是随机偏移。这样做的好处是:每张图被裁剪的比例不同,模型被迫学习不同尺度的特征,对小目标尤其友好。

接下来是加载四张图片。索引index是当前图片,另外三张从数据集中随机取,但有个细节:如果数据集是self.indices(已经shuffle过的),取相邻索引的图片,这样能保证每个epoch里拼接组合不同。

# 加载四张图,索引分别是index, index+1, index+2, index+3# 但实际代码里用了取模,防止越界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,y2a=max(xc-w,0),max(yc-h,0),xc,yc

这个坐标计算逻辑,我当初看的时候绕了半天。简单说:每张图在最终马赛克中的位置,是由中心点(xc, yc)决定的。第一张图(索引0)的右下角对齐中心点,第二张图(索引1)的左下角对齐中心点,第三张图(索引2)的右上角对齐中心点,第四张图(索引3)的左上角对齐中心点。这样四张图正好拼成一个完整的矩形。

但问题来了:如果原图尺寸很大,比如1920x1080,而马赛克目标尺寸是640x640,那么原图放在马赛克中时,大部分区域会被裁剪掉。这就是Mosaic的暴力之处——它强制模型从局部特征中学习。

标注框变换:坐标映射与裁剪

图片拼好了,标注框怎么办?不能直接把原图的标注框拿过来用,因为每张图在马赛克中的位置变了,而且可能被裁剪。

# 对每张图的标注框做平移和裁剪# 假设原图标注框坐标是(x1, y1, x2, y2),归一化到[0,1]# 先反归一化到原图尺寸boxes=labels[:,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像素宽的框对训练没意义w=boxes[:,2]-boxes[:,0]h=boxes[:,3]-boxes[:,1]valid=(w>2)&(h>2)labels=labels[valid]boxes=boxes[valid]

概率控制:不是每次都用Mosaic

YOLOv5的Mosaic不是100%触发的。在训练的前10个epoch,Mosaic概率是1.0,之后逐渐降低。这个设计是为了让模型在后期适应更真实的单图分布。

# 在train.py中,每个batch前判断是否使用Mosaicifself.mosaicandrandom.random()<self.mosaic_prob:# 使用Mosaicimages,labels=self.load_mosaic(index)else:# 使用普通单图加载images,labels=self.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,labels2=self.load_mosaic(random.randint(0,len(self.labels)-1))else:img2,labels2=self.load_image(random.randint(0,len(self.labels)-1))# 混合比例,从Beta分布采样r=np.random.beta(32.0,32.0)# 这个参数可以调,默认32.0# 混合图像img=(img*r+img2*(1-r)).astype(np.uint8)# 合并标注框labels=np.concatenate((labels,labels2),axis=0)

注意这个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)格式,方便裁剪x1=boxes[:,0]-boxes[:,2]/2y1=boxes[:,1]-boxes[:,3]/2x2=boxes[:,0]+boxes[:,2]/2y2=boxes[:,1]+boxes[:,3]/2# 裁剪后,再转回(x_center, y_center, width, height)boxes[:,0]=(x1+x2)/2boxes[:,1]=(y1+y2)/2boxes[:,2]=x2-x1 boxes[:,3]=y2-y1# 归一化到马赛克图像尺寸boxes[:,[0,2]]/=s# s是马赛克尺寸boxes[:,[1,3]]/=s

这里有个容易犯的错误:裁剪后,标注框的中心点可能不在原图中心,但归一化时用的是马赛克图像的尺寸s,而不是原图尺寸。所以如果裁剪后的标注框靠近马赛克边缘,它的归一化坐标会偏小,这是正确的。

个人经验:什么时候该调,什么时候不该调

  1. Mosaic概率不要轻易改。默认1.0是最优的,除非你的数据集全是小目标且背景单一,可以降到0.8。我见过有人改成0.3,结果mAP掉了5个点。

  2. MixUp的Beta参数。默认32.0是经验值,但如果你发现训练初期loss下降太慢,可以改成8.0或16.0,让混合比例更随机。不过别低于2.0,否则混合太极端,模型学不到稳定特征。

  3. 标注框裁剪阈值。源码里是2像素,但如果你训练的是超大分辨率图像(比如4K),可以提高到5-10像素。反之,如果是小目标数据集,保持2像素,别动。

  4. Mosaic和MixUp的串联顺序。YOLOv5是先Mosaic再MixUp,这个顺序不要改。如果先MixUp再Mosaic,两张混合后的图再拼接,标注框会乱成一团。

  5. 调试技巧。如果你怀疑数据增强有问题,可以在训练脚本里加一行cv2.imwrite('debug.jpg', img),把增强后的图像保存下来,肉眼检查标注框是否对齐。我每次改数据增强代码都会这么做,能省下大量排查时间。

最后说一句:数据增强不是越多越好。Mosaic和MixUp的组合已经很强了,再加其他增强(比如CutOut、RandAugment)反而可能过拟合。我一般只保留Mosaic、MixUp、HSV抖动和随机缩放,其他都关掉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询