news 2026/10/21 22:22:16

YOLOv8自定义数据增强:继承BaseTransform接入MixUp与Copy-Paste实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv8自定义数据增强:继承BaseTransform接入MixUp与Copy-Paste实战

如果你想让 YOLOv8 的训练 pipeline 加入自定义增强方法,最忌一上来就改训练主循环。我在一次做小样本目标检测的内部实验时,也走过这个弯路:想加一个自定义 MixUp,顺手把训练脚本里的 dataloader 改了又改,结果不是增强没生效,就是训练直接死循环。后来把 YOLOv8 的数据增强调用链梳理清楚,才发现真正的扩展点就两个:继承BaseTransform写新的增强类,以及修改v8_transforms这个组装函数。本文就围绕这两条路展开,把 Copy-Paste、MixUp 这类需要访问“其它样本”的增强方法,完整接到训练流程里去。

我会先讲清楚 YOLOv8 的增强链路在哪儿、labels字典里到底有什么,然后给出可直接照抄的继承代码、注册方法、验证手段,以及训练时的调参经验。这部分对刚接触目标检测源码的读者尤其友好,你不需要把整个框架读一遍,只需要理解一条链:dataset -> v8_transforms -> Compose -> 各个增强类。

1. YOLOv8 数据增强链路到底是怎么串起来的

1.1 增强组装入口:v8_transforms

YOLOv8 在训练时,数据增强不是硬编码在训练循环里的,而是在数据集构建阶段,由v8_transforms(dataset, imgsz, hyp, stretch=False)这个函数生成一个增强对象列表,再交给Compose来按顺序执行。不同小版本里这个函数的实现略有差异,但整体结构大致是下面这样:

# 框架源码中的示意结构,不同版本细节略有不同 def v8_transforms(dataset, imgsz, hyp, stretch=False): pre_transform = Compose([ Mosaic(hyp.mosaic, imgsz, dataset, hyp.mosaic_fraction) ]) return Compose( pre_transform, RandomHSV(hyp.hsv_h, hyp.hsv_s, hyp.hsv_v), RandomFlip(hyp.fliplr, hyp.flipud), RandomAffine(hyp.translate, hyp.scale, hyp.shear, hyp.perspective), MixUp(hyp.mixup, imgsz, dataset, hyp.mixup_fraction) if hyp.mixup else None, CopyPaste(hyp.copy_paste, dataset, hyp.copy_paste_fraction) if hyp.copy_paste else None, # 还有 Albumentations、归一化、ToTensor 等若干操作 )

这段代码里有两个非常关键的设计:

第一,Mosaic 被放在了 pre_transform 里,这意味着它在整条增强链的最前面执行;而MixUp、CopyPaste这类需要“第二张图”的增强,又必须在后面顺序执行。这不是随意安排的,因为它们都有访问dataset的需求,而dataset参数正是从这个函数里传进去的。

第二,Compose只是按列表顺序把上一个增强的输出作为下一个增强的输入,它不对数据格式做任何假设。所以你自己写增强类时,也完全可以把它塞进这个列表的任何位置,只要你能保证输入输出格式是labels字典就行。

1.2 每个增强看到的输入:labels 字典

v8_transforms返回的Compose对象最终会被挂到数据集的transforms属性上。训练时加载器会先读取一张图像和对应标注,组成一个labels字典,然后调用transforms(labels)。每个增强类需要做的就是:处理这个字典,再把它还给下一个增强。

labels字典里最常见的几个字段如下:

字段含义常见格式
img当前图像numpy.ndarray,HWC 顺序,BGR 通道
cls所有目标的类别 id一维numpy.ndarray
bboxes所有目标的边界框二维numpy.ndarray,通常是归一化后的 xyxy
instances目标实例对象(部分版本用这个字段)封装了 bbox、segments、keypoints 的对象
segments分割数据列表,每个元素是一个多边形点集
mask实例分割 mask二维数组,部分版本存在

从编写自定义增强的角度看,你只需要记住img是 HWC 的 BGR 图像,而bboxes绝大多数情况是归一化坐标,也就是所有值在 0 到 1 之间。这一点在我后面写 Copy-Paste 时会反复强调,因为很多人直接在像素坐标上操作,导致标签和像素没对齐,训练结果却没什么明显异常,只是涨点效果被削弱了。

1.3 为什么多图增强必须拿到 dataset 引用

普通的单图增强,比如随机裁剪、颜色抖动,只需要修改labels["img"]和labels["bboxes"]就结束了。但 MixUp 要随机取另一张图片来混合,Copy-Paste 要随机取另一张图片里的目标来粘贴。这两类增强如果不提前持有dataset,就不可能拿到“其它样本”。

这也是为什么在v8_transforms签名里有dataset这个参数。你自定义多图增强时,构造函数的第一个参数一定要放dataset,因为它就是你的“样本来源池”。我在实际实现里,通常会在__init__里把它保存为self.dataset,然后在apply方法里通过随机下标取样本。

2. 继承 BaseTransform 还是直接改源码:什么时候用哪种

2.1 逻辑简单的增强:继承 BaseTransform 就够了

如果你只是想在训练过程里加一个类似 RandomErasing 或 Cutout 的增强,那么继承BaseTransform是最干净的做法。BaseTransform是框架的增强基类,约定很简单:子类实现apply(self, labels),__call__会直接调用它。

我写过一个很基础的 RandomCutout,你可以直接拿去做模板:

import random import numpy as np from ultralytics.data.augment import BaseTransform class RandomCutout(BaseTransform): def __init__(self, p=0.5, max_boxes=4, fill=0): super().__init__() self.p = p self.max_boxes = max_boxes self.fill = fill def apply(self, labels): if random.random() > self.p: return labels img = labels["img"] h, w = img.shape[:2] for _ in range(random.randint(1, self.max_boxes)): cut_w = random.randint(int(w * 0.05), int(w * 0.2)) cut_h = random.randint(int(h * 0.05), int(h * 0.2)) if cut_w <= 0 or cut_h <= 0: continue x0 = random.randint(0, w - cut_w) y0 = random.randint(0, h - cut_h) img[y0:y0 + cut_h, x0:x0 + cut_w] = self.fill return labels

这个类没有任何外部依赖,只需要按约定返回labels。如果不想让 Cutout 方块破坏太多目标,可以自己加一个“只遮挡背景不遮挡前景”的逻辑,就是在随机选位置时计算候选框与已有 bbox 的 IoU,超过阈值就重新采样。这个逻辑用在上面的 Copy-Paste 里也一样。

继承的优点是:你可以把自定义增强放在独立文件里,比如my_augments.py,然后在配置训练环境时手动组装,后期升级框架版本时不会和源码产生太多冲突。缺点是:纯继承没法直接改变v8_transforms的默认顺序,你需要再找一个地方把你的增强塞进列表。

2.2 需要通过索引取其它样本的增强:必须修改组装函数

对于 MixUp 和 Copy-Paste 这类增强,情况就不一样了。它们的核心逻辑往往需要从dataset取出另一张样本,甚至要和现有增强链配合,比如在 Mosaic 之后执行,或在 RandomAffine 之前执行。这时纯粹写在独立类里还不够,你必须找到v8_transforms或等价的组装逻辑,把自己的增强插进 Compose 列表,并关闭框架原生自带的同类增强,否则就会出现“自己混一次、框架又混一次”的情况。

我的做法是优先在本地建一个模块,把自定义类写进去,然后只修改v8_transforms的返回列表。这样升级框架时,只需要重新git diff或者手动 patch 这一小段,就行。直接大段复制源码再改,维护成本太高。

3. 手写 MixUp 并接进 YOLOv8 训练流程

3.1 MixUp 的特殊性:线性混合两套标签

MixUp 的核心思路并不复杂:从数据集里随机取一张图,把两张图按权重线性叠加,同时把两套目标标签直接拼接起来。公式可以写成:

new_img = (1 - lam) * img1 + lam * img2 new_cls = concat(cls1, cls2) new_bboxes = concat(bboxes1, bboxes2)

其中lam通常服从 Beta(α, α) 分布。当α=0.2时,极端权重概率比较低,混合起来整体更接近原图;当α=1.0时,均匀混合,训练难度会明显增大。

这个增强对单阶段检测模型的好处是,能把不同场景的背景纹理和前景目标揉在一起,让模型不那么依赖某一个数据集的背景特征。但代价是目标之间的边界会变模糊,如果训练轮次不够,模型反而学得更吃力。

3.2 适合训练加载链路的 MixUp 实现

下面这段代码是一个可以跑在 YOLOv8 训练链里的自定义 MixUp。为了规避某些版本里dataset[index]会再次触发完整增强链的递归问题,我在取第二张图时优先使用get_image_and_label,这个方法在多数 YOLOv8 版本中都存在,返回的是未经整条增强链处理的“预处理图像和标签”。如果没有,就临时关掉 dataset 上的 transforms 再取。

import random import cv2 import numpy as np from ultralytics.data.augment import BaseTransform class MixUpOnce(BaseTransform): def __init__(self, dataset, p=0.8, alpha=0.2): super().__init__() self.dataset = dataset self.p = p self.alpha = alpha def _load_second(self, index): # 优先使用原始加载接口,避免再次进入完整增强链 if hasattr(self.dataset, "get_image_and_label"): return self.dataset.get_image_and_label(index) # 回退方案:临时禁用 transforms 再读取 tr = getattr(self.dataset, "transforms", None) self.dataset.transforms = None try: return self.dataset[index] finally: self.dataset.transforms = tr def apply(self, labels): if random.random() > self.p: return labels img = labels["img"] h, w = img.shape[:2] index = random.randint(0, len(self.dataset) - 1) second = self._load_second(index) img2 = second["img"] if img2.shape[:2] != (h, w): img2 = cv2.resize(img2, (w, h), interpolation=cv2.INTER_LINEAR) lam = np.random.beta(self.alpha, self.alpha) if self.alpha > 0 else 0.5 mixed = (img.astype(np.float32) * (1 - lam) + img2.astype(np.float32) * lam) labels["img"] = mixed.astype(np.uint8) # 坐标拼接:混合不改变任何坐标位置 cls2 = second["cls"] if "bboxes" in second: bboxes2 = second["bboxes"] else: bboxes2 = second["instances"].bboxes labels["cls"] = np.concatenate([labels["cls"], cls2], axis=0) labels["bboxes"] = np.concatenate([labels["bboxes"], bboxes2], axis=0) return labels

这段代码里有几个细节需要注意。

第一,np.random.beta(self.alpha, self.alpha)返回的lam是一个标量,会让两张图的权重在整张图上保持一致;如果你想要空间局部的 mixup,就得生成和图像同分辨率的权重图,这个更复杂,一般检测任务用全局标量就够了。

第二,我在拼接 bbox 前没有对bboxes2做归一化检查。如果两个样本都来自同一个get_image_and_label预处理流程,且坐标格式一致,那么直接拼接就没问题。但如果你的版本里一个样本的 bbox 是像素坐标,另一个是归一化坐标,拼接就会出大问题。这就是为什么我建议调试时先打印两个样本的坐标范围。

第三,get_image_and_label返回的图像不一定经过 Letterbox 和 Resize。所以我在混合前用cv2.resize把第二张图强制对齐到当前图的分辨率。这是自定义 MixUp 和框架原版 MixUp 的一个显著差别:原版通常在两个已经经历过几何变换的样本之间混合,而自定义版本要自己负责对齐。

3.3 把 MixUpOnce 注册进 v8_transforms

接下来要做的是修改v8_transforms,把自定义 MixUp 放进管线。我建议直接把原生MixUp(hyp.mixup, ...)的概率设为 0,或者在组装代码里删掉原生 MixUp,然后把自己的类插到合适位置:

def v8_transforms(dataset, imgsz, hyp, stretch=False): pre_transform = Compose([ Mosaic(hyp.mosaic, imgsz, dataset, hyp.mosaic_fraction) ]) return Compose( pre_transform, RandomHSV(hyp.hsv_h, hyp.hsv_s, hyp.hsv_v), RandomFlip(hyp.fliplr, hyp.flipud), RandomAffine(hyp.translate, hyp.scale, hyp.shear, hyp.perspective), MixUpOnce(dataset, p=0.8, alpha=0.2), # 自定义混合 # 原生 MixUp 不再启用 CopyPaste(...) if False else None, )

为什么我建议把自定义 MixUp 放在RandomAffine之后?因为几何变换阶段已经把图像尺寸、坐标系统一过了。如果 MixUp 在仿射之前执行,两个输入图的尺度可能完全不一致,你又得在几何变换里同时对混合后的图做操作,增大了坐标混乱的风险。放在仿射之后,当前样本和第二样本都已经完成尺度、翻转、平移等变换,直接线性混合是安全的。

如果你希望第二张图也经历同样的几何变换,那就得在_load_second里额外调用同一套仿射参数,这是一个更高级的玩法,牵扯到几何参数共享,实战里不建议第一次就这么搞。先让第二张图保持朴素状态,验证涨点后再考虑要不要给第二张图也加几何扰动。

4. Copy-Paste 自定义增强:把另一个样本的目标搬过来

4.1 复制粘贴的两种做法

YOLOv8 自带的 CopyPaste 主要是配合实例分割数据使用的,它会把某个实例的 mask 一起复制到当前图,并自动更新instances里的 mask 数据。但对于纯检测任务、且只有 bbox 标注的情况,我们可以实现一个简化版:只复制 bbox 内的像素块,同时把对应的 bbox 和类别追加到当前labels里。这样不需要 mask,也能获得类似效果。

这种做法很适合小目标占比低、类别不均衡的数据集。比如某个类别只出现几十个样本,Copy-Paste 能在每次迭代里不断把它的目标“复印”到各种背景上,相当于对少数类做了过采样。它的风险也很明显:当粘贴位置和原目标高度重叠时,模型看到的是叠在一起的奇怪中间状态,所以必须在粘贴前做重叠检查。

4.2 简化版 CopyPaste 的实现

下面是我实验里用的一个简化实现,它假设所有 bbox 都是归一化坐标。如果你手头的数据不是归一化格式,记得先自行转换。

import random import numpy as np from ultralytics.data.augment import BaseTransform def _bbox_area(bbox): x1, y1, x2, y2 = bbox return max(0.0, x2 - x1) * max(0.0, y2 - y1) def _bbox_iou(a, b): ax1, ay1, ax2, ay2 = a bx1, by1, bx2, by2 = b ix1 = max(ax1, bx1) iy1 = max(ay1, by1) ix2 = min(ax2, bx2) iy2 = min(ay2, by2) iw = max(0.0, ix2 - ix1) ih = max(0.0, iy2 - iy1) inter = iw * ih union = _bbox_area(a) + _bbox_area(b) - inter return inter / union if union > 0 else 0.0 class CopyPasteSimple(BaseTransform): def __init__(self, dataset, p=0.5, max_extra=3, min_area=0.001, iou_thr=0.3): super().__init__() self.dataset = dataset self.p = p self.max_extra = max_extra self.min_area = min_area self.iou_thr = iou_thr def _load_second(self, index): if hasattr(self.dataset, "get_image_and_label"): return self.dataset.get_image_and_label(index) tr = getattr(self.dataset, "transforms", None) self.dataset.transforms = None try: return self.dataset[index] finally: self.dataset.transforms = tr def apply(self, labels): if random.random() > self.p: return labels img = labels["img"] h, w = img.shape[:2] index = random.randint(0, len(self.dataset) - 1) second = self._load_second(index) img2 = second["img"] if "bboxes" in second: src_boxes = second["bboxes"] else: src_boxes = second["instances"].bboxes src_cls = second["cls"] if len(src_boxes) == 0: return labels # 计算源图像尺寸,让归一化坐标换算成目标图像坐标 h2, w2 = img2.shape[:2] pasted = 0 order = list(range(len(src_boxes))) random.shuffle(order) for i in order: if pasted >= self.max_extra: break x1, y1, x2, y2 = src_boxes[i] # 源图可能是非归一化,先做一次判断 if x2 <= 1.0 and y2 <= 1.0: sx1, sy1, sx2, sy2 = int(x1 * w2), int(y1 * h2), int(x2 * w2), int(y2 * h2) else: sx1, sy1, sx2, sy2 = int(x1), int(y1), int(x2), int(y2) pw = sx2 - sx1 ph = sy2 - sy1 if pw <= 0 or ph <= 0: continue if (pw / w) * (ph / h) < self.min_area: continue # 在目标图上找不重叠的放置位置 for _ in range(10): nw = min(pw, int(w * 0.5)) nh = min(ph, int(h * 0.5)) if nw <= 0 or nh <= 0: continue dx = random.randint(0, max(0, w - nw)) dy = random.randint(0, max(0, h - nh)) new_box = [dx / w, dy / h, (dx + nw) / w, (dy + nh) / h] overlap = False for ob in labels["bboxes"]: if _bbox_iou(new_box, ob) > self.iou_thr: overlap = True break if overlap: continue # 复制像素时,注意源像素坐标要裁剪 sx2_use = min(sx1 + nw, w2) sy2_use = min(sy1 + nh, h2) if sx2_use - sx1 <= 0 or sy2_use - sy1 <= 0: continue patch = img2[sy1:sy2_use, sx1:sx2_use].copy() img[dy:dy + patch.shape[0], dx:dx + patch.shape[1]] = patch labels["bboxes"] = np.concatenate( [labels["bboxes"], np.array([new_box])], axis=0 ) labels["cls"] = np.concatenate( [labels["cls"], np.array([src_cls[i]])], axis=0 ) pasted += 1 break return labels

这段代码里我刻意做了几个保守设计:粘贴尺寸限制在目标图的一半以内,避免复制一个大目标直接盖住全图;放置新目标前做 IoU 检查,避免和已有目标高度重叠;源图坐标和目标图坐标都做了归一化判断。这些都是在实验中容易踩坑的地方。

需要注意,上面的实现如果源图和目标图分辨率差距很大,直接按bbox缩放复制会产生明显像素马赛克。更好的做法是:如果源图比目标图清晰很多,可以先把源图的小 patch 用cv2.resize缩放到目标尺寸,再贴上去。这样视觉上更协调,也不容易让模型学到“粘贴区域特别锐利”这种规律。

4.3 Copy-Paste 在增强链表里的放置位置

我建议把 CopyPasteSimple 放在 Mosaic 之后、RandomAffine 之前。原因是:Mosaic 本身已经拼出大了大图背景,此时粘贴目标可以很好地融入混合背景;接着 RandomAffine 会做旋转、缩放、平移,粘贴进去的目标会和背景一起经历几何变换,视觉上不会出现“粘贴区域和背景形变不一致”的破绽。

如果你把 Copy-Paste 放在 RandomAffine 之后,坐标计算确实更直观,因为所有坐标都已经稳定成最终形态。但同时粘贴区域不会跟着图像一起旋转缩放,会显得像一张“贴纸”。所以两者的取舍很明确:默认优先放在仿射之前,除非你真的不想让粘贴目标发生几何变化。

5. 跑训练前先验证自定义增强是否真的生效

5.1 单批可视化脚本

很多人把增强写进训练循环,然后直接开训,等十几个小时跑完才发现增强根本没起作用,或者坐标已经错乱。我建议在任何训练前,都先跑一个单批调试脚本,把增强前后图像直接保存出来看。

import cv2 import numpy as np from ultralytics.data.dataset import YOLODataset from ultralytics.data.augment import v8_transforms def debug_augment(data_yaml, hyp, index=0, save_path="debug_aug.jpg"): ds = YOLODataset( data=data_yaml, imgsz=640, transforms=None, # 让 dataset 先不自动做增强 ) # 关键:用 get_image_and_label 拿到原始样本,再手动调用增强链 raw = ds.get_image_and_label(index) tr = v8_transforms(ds, 640, hyp) # 只跑一次增强 out = tr(raw) img = out["img"].copy() bboxes = out["bboxes"] cls = out["cls"] h, w = img.shape[:2] for i, box in enumerate(bboxes): x1, y1, x2, y2 = box # 如果坐标是归一化的就还原 if x2 <= 1.0 and y2 <= 1.0: x1, x2 = int(x1 * w), int(x2 * w) y1, y2 = int(y1 * h), int(y2 * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(int(cls[i])), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(save_path, img)

跑完以后观察两点:第一,图像上是否有明显的混合或粘贴痕迹;第二,所有 bbox 是否仍然能正确框住目标。如果 bbox 跑到无意义的位置,大概率是坐标格式没统一。

5.2 检查标签数量与 dtype

除了可视化,还需要在代码里确认标签数量。每跑一个 batch,可以打印labels["cls"].shape和labels["bboxes"].shape。对于 MixUp,第二个样本如果一次都没被取到,cls数量应该和没开增强时一模一样;如果混了,数量就会翻倍左右。对 Copy-Paste,则需要打印粘贴成功的pasted次数,防止随机数运气不好导致一次都没贴。

dtype 也是常见问题来源。很多增强在计算混合图时用float32算完,容易忘记转回uint8。一旦训练 loader 拿到 float 图像,网络可能本身不会报错,但归一化逻辑会错位,最终模型结果会很差。我通常会在写完增强类后单独加一行:

assert labels["img"].dtype == np.uint8 assert labels["bboxes"].shape[1] == 4 assert len(labels["cls"]) == len(labels["bboxes"])

这组断言虽然简单,但在多人协作时能救很多命。

5.3 常见错误排查表

症状可能原因排查方法
增强前后图像完全一样概率 p 太低或随机数没触发把 p 临时改成 1.0 再验证
混合后图像整体偏色BGR/RGB 顺序不一致打印通道均值或保存图像肉眼确认
bbox 框错位置归一化/像素坐标混用统一换算,打印 min/max
标签数量暴增好几倍从 dataset 取第二张图时再次触发了完整增强链改走 get_image_and_label 或临时禁用 transforms
训练时内存暴涨多图增强叠加了 Mosaic 和 MixUp 的递归采样检查是否在同一批数据里反复递归

6. 训练验证与调参经验

6.1 不要一上来就开满强度

自定义 MixUp 和 Copy-Paste 都是“重口味”增强,默认把概率调到 0.8、0.5 并不一定合适。我的经验是先开一个很小的概率,比如p=0.2,跑十几个 epoch 确认 loss 曲线没有异常,再逐步往上加。原因很简单:这两类增强都会显著增加单张图里的目标数量,如果模型还处在学习基本特征阶段,过大的增强强度会让优化过程很不稳定。

在 ultralytics 的配置里,你可以通过修改训练用的 yaml 文件,或者直接传参,把自定义类的概率作为超参数暴露出来。例如在 yaml 里加入mixup_custom: 0.3和copypaste_custom: 0.2,然后在组装v8_transforms时读取:

MixUpOnce(dataset, p=hyp.mixup_custom, alpha=hyp.mixup_alpha) CopyPasteSimple(dataset, p=hyp.copypaste_custom, max_extra=3)

这样做的价值是,你可以用同一套训练脚本做消融实验,而不是每次改代码。我强烈建议把增强开关全部参数化,甚至可以把alpha、max_extra、iou_thr都暴露出来,方便网格搜索。

6.2 小目标场景要慎开 Copy-Paste

如果你的数据集以微小目标为主,比如航拍图里的汽车、工厂里的螺丝缺陷,Copy-Paste 的“复制粘贴”很容易把小目标粘贴到完全不合适的背景上,比如把螺丝粘贴到纯色空白区,模型学到的可能是“只要有高对比度小块就是螺丝”,这会导致误检率上升。

针对这种情况,可以考虑只在同类目标之间做 Copy-Paste,或者限制粘贴区域只能在“目标密集区域”附近,也可以保留 IoU 阈值检查,让新目标尽量不要和已有目标重叠。不要盲目相信论文里的涨点数字,因为很多论文的数据集本身目标尺度相对均衡。

6.3 先看 train_batch 图再决定要不要保留增强

YOLOv8 训练时如果开启plots=True,框架会在runs/detect/train路径下保存每个训练阶段的 batch 图像。这是最直观的验证方式:你不需要自己写可视化脚本,训练开始后就能看到增强是否生效、目标框是否合理。我会在开任何自定义增强后,先看第一轮保存的train_batch0.jpg,确认视觉上没有明显破绽再让它继续跑。

如果发现增强把大量目标叠加在一起,导致目标框层层覆盖,那就降低概率,或者提高 Copy-Paste 的 IoU 阈值。这个判断完全靠肉眼就能完成,不需要额外写评估代码。

6.4 消融实验别只盯 mAP

自定义增强的正确性验证,最终还是要回到模型指标上。但只看 mAP 是不够的,因为 MixUp 这类全局混合增强,可能会让模型在召回率上提升、精度上下降,mAP 可能变化很小。建议同时记录mAP50、mAP50-95、precision、recall,以及几个难分类别的单独指标。

我在一次内部实验中,光把原生 CopyPaste 换成自定义版本后,整体 mAP 只涨了 0.4 个点,但某个罕见类别的召回率涨了接近 8 个点。如果不分开看类目指标,很容易得出“增强没用”的结论,然后把这个有价值的自定义增强丢掉。所以验证阶段多保留几份指标表格,会让你的判断更公允。

说到底,把自定义数据增强集成进 YOLOv8 的训练 pipeline,最大的门槛不在写增强本身,而在理清链路:v8_transforms组装了 Compose,Compose顺序执行各个 BaseTransform,BaseTransform.apply处理 labels,多图增强必须借用 dataset 的取样本能力。只要按这个思路走,继承一个类、注册进列表、关掉原生同类增强、可视化验证,四步就能完成一次可靠的自定义增强接入。真到训练时再想调整,留给你的就只有参数细节,不会再被底层调用链绊住手脚。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 22:22:11

飞机表面缺陷识别实战:从YOLO数据集构建到工业质检部署全解析

做工业AI质检一年多&#xff0c;我越来越觉得飞机表面缺陷识别是计算机视觉里最考验工程能力的落地场景之一。这几天在整理第10618期飞机表面裂痕识别yolo数据集时&#xff0c;我一边跑着训练&#xff0c;一边在想&#xff1a;为什么这类工业零部件自动化质检项目在论文里总显得…

作者头像 李华
网站建设 2026/10/21 22:21:31

Spark Task Schedule Delay五大根因与实战调优指南

1. 这不是“卡顿”而是调度失衡&#xff1a;从Task Schedule Delay看Spark作业的真实瓶颈你有没有遇到过这样的场景&#xff1a;明明集群资源看着很空&#xff0c;YARN上ApplicationMaster汇报的Container使用率不到30%&#xff0c;Executor堆内存也只用了40%&#xff0c;但一个…

作者头像 李华
网站建设 2026/10/21 22:20:14

lingbot-map:多传感器融合的语义建图系统实战解析

1. 这个项目要解决什么问题第一次看到lingbot-map这个名字&#xff0c;我第一反应是&#xff1a;这八成又是一个机器人建图相关的模块。跟几位做移动机器人的朋友聊完&#xff0c;再结合我自己在室内外巡检、仓储搬运这些小项目里折腾过的经历&#xff0c;基本可以确认&#xf…

作者头像 李华
网站建设 2026/10/21 22:19:57

Unity中利用BMFont与Mesh渲染自定义艺术字性能优化方案

1. 项目概述&#xff1a;为什么要在Unity里用BMFont做艺术字接手这个项目的时候&#xff0c;我面对的需求其实特别具体&#xff1a;游戏里有一套战斗飘字系统&#xff0c;要显示伤害数字、暴击提示、连击计数&#xff0c;还得配合一套定制的艺术字体风格。直接用Unity的Text组件…

作者头像 李华
网站建设 2026/10/21 22:19:52

AI让临电配电箱检测报告不再“走过场”:从人工巡检到智能审核

工地上的临时用电配电箱&#xff0c;大概是所有安全巡检对象里最容易沦为“走过场”的那个。脚手架有第三方检测&#xff0c;塔吊有月检复核&#xff0c;唯独配电箱&#xff0c;很多时候就是电工师傅开箱瞄一眼、随手勾个“正常”就完事。直到一次模拟演练里&#xff0c;现场编…

作者头像 李华
网站建设 2026/10/21 22:18:52

别把“知道”当“会”:技能学习的底层逻辑与刻意练习指南

直接说结论&#xff1a;大多数人缺的不是“技能”&#xff0c;而是对“skills”这个词的理解方式。我做过一段时间的团队技能梳理&#xff0c;也在招聘时见过大量简历。你会发现一个特别普遍的现象&#xff1a;简历上技能列表写得满满当当&#xff0c;一到实操就露馅。不是人不…

作者头像 李华