
做目标检测的朋友大概都有过这种体验数据明明不算多几万张图训练却慢得要命一个 epoch 跑几十分钟GPU 占用率上不去风扇呼呼转但算力就吃不满。打开监控一看瓶颈根本不在显卡而在数据加载那一段——每张图都是 4000×3000 的相机原图解一次 JPEG 就要几十毫秒一个 batch 里几十张图CPU 光解码就累趴下了。我就是这么被折磨了两个项目以后才狠下心来把整个数据集按比例压一遍同时把对应的 txt 标签文件一起处理掉。今天就把这套“按比例压缩图片及对应 txt 标签”的完整做法摊开讲为什么这么压、比例怎么算、标签到底要不要改、脚本怎么写、压完怎么验证精度没掉。不管你是刚跑通第一个检测 Demo 的新手还是已经在调大规模训练的老手这里面的几个关键结论尤其是归一化坐标那条都能省你不少试错时间。1. 压缩这件事压的到底是什么1.1 训练慢先分清是算力慢还是喂数据慢动手之前一定要先定位瓶颈不然压了半天发现是显卡本身跑不动白费劲。判断方法很土但很准跑几个 epoch盯着 GPU 利用率和显存占用。如果 GPU 利用率长期在 40%~70% 之间来回跳显存占用远没到上限那大概率是数据供给跟不上也就是常说的输入瓶颈如果 GPU 利用率稳定在 95% 以上显存吃满那就是算力或 batch 尺寸的问题压缩图片只能间接帮到你省显存从而能加大 batch。数据供给慢的根源通常是三个磁盘 IO、图像解码、预处理增强。其中图像解码的耗时基本和像素总数成正比一张 1200 万像素的图解码加缩放跟一张 60 万像素的图完全不是一个量级。更麻烦的是很多训练框架默认的加载策略是先读原图再在内存里缩放到网络输入尺寸这意味着你付了解码超大图的全额成本最后却只用到了缩放到 640 之后的那一小块信息——典型的“用大炮打蚊子”。把原图提前压到接近训练分辨率的尺寸等于把这笔成本从“每个 epoch 都付”变成“一次性付清”这就是整套方案最核心的经济学逻辑。顺带说一句压缩还顺带解决了两个隐性痛点一是磁盘占用几万张原图动辄几百 GB备份、迁移、同步都难受二是内存缓存压缩后整个数据集有可能塞进内存后面开启内存缓存就变成了纯 CPU 计算速度提升会更夸张。1.2 图片和标签之间那条看不见的绳子YOLO 系列使用的标签格式非常简洁每行一个目标类别编号 x中心 y中心 宽度 高度后四个数都是归一化过的浮点数取值范围 0~1。关键在于这个归一化是相对当前图片的宽和高来算的。也就是说标签文件本身并不记录任何绝对像素坐标它只记录“这个框在图片里占的位置比例”。这个设计带来了一个极其重要的后果只要你的缩放操作是宽高等比例的那么任何一个框在图片中的相对位置和相对大小都不会改变标签文件里的四个数字一个都不用动。举个具体例子一张 2000×1500 的图里有个框中心在 (1000, 750)宽高 400×300。归一化后是0.5 0.5 0.2 0.2。把图片等比缩到 1000×750框的中心变成 (500, 375)宽高 200×150再归一化——还是0.5 0.5 0.2 0.2。数学上完全恒等。很多人一听到“压缩后要同步处理标签”就以为要重新算一遍坐标于是写了一堆复杂的换算代码反而因为浮点处理和取整引入了误差把原本干净的标签搞脏了。理解这条“看不见的绳子”你就知道绝大多数情况下最安全的做法是先确认自己用的是等比缩放然后别动标签。只有在裁剪、补边、旋转这类改变了画面构成的操心里才需要真正重写标签。1.3 哪些数据集适合压哪些纯粹是自找麻烦不是所有数据都值得压。我的经验判断标准大致是这样数据特征建议理由长边在 2000 像素以上目标尺寸普遍偏大强烈建议压解码成本高信息冗余极大长边已经接近训练分辨率如 800~1300可压可不压收益有限压过头反而伤精度小目标密集目标像素面积普遍小于 32×32谨慎压缩比要小缩小后小目标可能只剩几个像素直接丢失特征原图带大量细纹理遥感、医学、工业缺陷谨慎优先用高质量参数高频信息在缩放中损失最快图片数量少但每张巨大建议压单张解码成本占比更高需要保留原始数据做高分辨率推理只压训练副本别动原始库留一份完整备份我自己踩过一次坑有一批电线巡检的图目标是很细的螺栓和销钉我图省事直接把长边从 4000 压到 800结果训练集里接近三分之一的目标框在缩放后不足 3 个像素模型学了个寂寞mAP 掉了十几个点。后来改成压到 1600再配合过滤掉缩放后确实过小的框才恢复正常。所以压缩比不是越小越好它是一次信息量和效率的交换你得知道自己在拿什么换。2. 方案设计比例怎么定坐标怎么处理2.1 缩放比例的计算从 imgsz 倒推目标边长比例怎么定别凭感觉。我的做法是从训练的输入尺寸倒推。假设训练时imgsz640那么模型实际“看到”的信息被限制在 640 这个尺度上。你喂给它 4000 像素的原图它也是先缩放再送进网络多出来的细节在训练阶段根本用不上。合理的经验区间是目标长边 ≈ 训练 imgsz 的 1.5 到 2.5 倍。取下限 1.5 倍是为了给数据增强留余量——随机缩放、随机裁剪、马赛克拼接这些操作会从原图里抠出局部区域再放大如果原图和训练尺寸一样大增强后的画面会明显发虚。取上限 2.5 倍是收益递减的分界线再大就基本只是在喂解码器了。具体到这个例子imgsz640目标长边取 640×2 1280比较稳妥imgsz960目标长边取 1600~1920imgsz1280目标长边取 1920~2560按照这个思路一张 4032×3024 的手机照片长边压到 1280缩放比例是 1280/4032 ≈ 0.3175输出尺寸约 1280×960。像素总数从 1219 万降到 123 万只有原来的 1/10。解码耗时在我这边环境里大概从 45ms 降到 8ms 左右一个 batch 64 张的话单次数据加载能省将近 2.4 秒一轮一千个 batch 就是四十分钟的差距非常可观。如果你的数据里目标尺度差异很大别用一个全局统一的长边可以按图分组处理。比如把包含小目标的图单独挑出来给一个更宽松的压缩比其余图压得更狠。这需要先跑一遍统计脚本把每张图的框像素尺寸分布拉出来看多花半小时收益很大。2.2 等比缩放、直接 resize、裁剪三者的取舍三种方式看着都是“把图变小”但对标签的影响完全不同选错了后面全是麻烦。直接 resize 到固定尺寸比如全部拉成 640×640会破坏宽高比把圆形拉成椭圆把行人拉得又扁又胖。这种形变对模型来说是纯噪声而且更麻烦的是——它其实也不改变归一化标签因为归一化是分别对宽和高做的。所以很多人以为直接 resize 后标签要改其实不用。但我不推荐这种方式形变带来的精度损失通常比你省下的那点处理时间更值钱。裁剪从原图中间或按目标分布裁一块出来这个必须重写标签。裁剪相当于换了一个坐标系原点所有框的像素坐标平移还可能有一部分框被裁到画面外需要截断或丢弃。裁剪适合“目标超大但背景无用”的场景比如只想训练画面中心的物体。等比缩放保持宽高比整图缩小是我最推荐的默认方案。它不改变画面构成不丢任何目标标签零改动唯一的代价是输出尺寸不统一有的 1280×960有的 1280×720。但这根本不是问题因为训练框架本身会做 letterbox 补齐统一尺寸这件事交给训练管线去做就行没必要在数据落盘阶段就强行统一。这里有个小细节值得提一下等比缩放时两个方向的取整可能带来极微小的宽高比偏差。比如 1000×667 的图缩到长边 800算出宽 800、高 533.6取整成 534实际比例是 800/534 ≈ 1.498原图是 1.499。这个偏差在千分之一量级对归一化坐标的影响完全可以忽略不需要做任何修正。但如果你的流程里有“把归一化坐标转回像素再转回归一化”的中间步骤这个微小偏差会被放大成 1 像素的偏移框看起来就会有一点点歪——所以能不动标签就别动。2.3 归一化坐标的“不变性”——为什么等比缩放可以不动标签前面提过结论这里把推导补完整因为这是整个方案的基石理解了它你才能放心地“什么都不做”。设原图宽 W、高 H某个框的中心像素坐标 (cx, cy)像素宽高 (bw, bh)。写入标签的值是x cx / W, y cy / H, w bw / W, h bh / H等比缩放比例 r新图宽 W rW高 H rH。缩放后中心变成 (r·cx, r·cy)框宽高变成 (r·bw, r·bh)。重新归一化x r·cx / (rW) cx / W x y r·cy / (rH) cy / H y w r·bw / (rW) bw / W w h r·bh / (rH) bh / H hr 被约掉了四个值完全相等。这就是不变性的来源也是为什么“压缩图片后同步处理标签”这件事在等比缩放的语境下真正的正确做法是一个字节都不改。那为什么标题里还写着“及对应的 txt 标签”因为实际工程里标签文件确实需要动只不过动的不是坐标而是另外三类情况一是需要过滤缩放后变得过小的框二是需要同步文件命名和后缀三是如果顺手做了补边padding到正方形那就得重算坐标。这三件事才是“同步处理标签”的真实含义。把这个理解清楚了你的脚本会干净很多也不容易引入人为误差。2.4 什么时候必须重写标签padding 与裁剪场景有些团队出于各种原因希望在落盘阶段就把所有图补齐成正方形这时候标签必须改。推导也不复杂先按长边缩放到边长 S得到缩放后尺寸 (rw, rh)然后在画布上居中放置左上角偏移 (ox, oy) ((S-rw)/2, (S-rh)/2)。新的归一化坐标是x (x·W·r ox) / S y (y·H·r oy) / S w w·W·r / S h h·H·r / S注意这里 x 和 y 的分子多了一个偏移量而 w、h 没有因为宽高不随平移变化。实现的时候补齐区域的填充色建议用 (114, 114, 114) 这个中性灰和主流训练管线的默认填充一致避免训练阶段再做两种不同填充导致的分布不一致。裁剪场景的公式类似只是偏移量变成负的因为原点移动了而且需要处理框越界的情况如果一个框裁剪后只剩很小一部分比如只剩原面积的 20%我建议直接丢弃这个框因为残缺的框标注本身就不准确留着反而教坏模型。判断阈值可以设成保留面积占比 0.25 以上太小的一律丢。3. 动手实现批量压缩脚本逐段拆解3.1 目录约定与任务清单生成先说目录结构。检测数据集一般是这个形态dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/图片和标签靠同名不同后缀一一对应images/train/a001.jpg对应labels/train/a001.txt。压缩脚本要做的事情就是保持这套结构输出到另一个根目录方便你回滚对比。写脚本之前强烈建议先生成一份任务清单做个统计别直接开干。统计内容包括待处理图片总数、总字节数、最大和最小尺寸、尺寸分布直方图、以及是否存在“有图无标签”或“有标签无图”的孤儿文件。我就遇到过一批数据里混进了十几张.JPG大写后缀的图标签是小写.txt训练脚本按同名匹配失败那十几张图一直当成负样本在训直到我统计才发现。import os from pathlib import Path from collections import Counter def scan(root): root Path(root) imgs [p for p in (root / images).rglob(*) if p.suffix.lower() in {.jpg, .jpeg, .png, .bmp, .webp}] stats Counter() missing [] for p in imgs: # 标签路径按 images - labels 替换后缀改 txt lp Path(str(p).replace(/images/, /labels/)).with_suffix(.txt) if not lp.exists(): missing.append(str(p)) stats[p.suffix.lower()] 1 print(图片总数:, len(imgs)) print(后缀分布:, dict(stats)) print(缺标签的图:, len(missing)) return imgs, missing这个小函数花不了几行代码但能在动手之前把一半的脏数据问题暴露出来。3.2 图片压缩的几个关键参数图片处理我用 Pillow够用且依赖少。核心参数只有四个但每个都有讲究插值算法。缩小图像时最忌讳用双线性或最近邻会产生明显的锯齿和摩尔纹。Pillow 的Image.LANCZOS在缩小时质量最好OpenCV 的INTER_AREA效果接近且更快。别用INTER_NEAREST那是给自己找麻烦。EXIF 方向。手机拍摄的图经常带 Orientation 标记表示“实际应该旋转 90 度显示”。如果你不处理Pillow 打开时给你的是未旋转的原始像素宽高是反的标签坐标就会全部错位 90 度。这一条必须写在代码里用ImageOps.exif_transpose一行解决而且在 resize 之前调用。颜色模式。四通道 RGBA 存成 JPEG 会直接报错灰度图 L 模式存 JPEG 没问题但有些训练管线会报通道数异常。统一转成 RGB 最省心代价是灰度图体积变大一点。还有一种情况是 CMYK 模式的印刷用图转 RGB 的时候颜色会变这个就得注意了——虽然检测任务对颜色的敏感度不高但如果你做的是颜色相关的分类任务就得单独处理。质量参数。JPEG 的quality建议 85~90subsampling14:2:2 色度采样能在几乎看不出损失的情况下省掉不少体积。实测长边 1280 的图quality 88、subsampling 1 的情况下平均单张 60~90KB相比原图通常能省 90% 以上。别为了省那点空间把 quality 压到 70 以下色块噪声会污染特征的边缘对小目标检测尤其不友好。from PIL import Image, ImageOps def shrink_one(src, dst, max_side1280, quality88): with Image.open(src) as im: im ImageOps.exif_transpose(im) # 先纠正方向 if im.mode not in (RGB, L): im im.convert(RGB) w, h im.size scale max_side / max(w, h) if scale 1.0: # 本来就不大不放大 new_size (w, h) else: new_size (max(1, round(w * scale)), max(1, round(h * scale))) im im.resize(new_size, Image.LANCZOS) dst.parent.mkdir(parentsTrue, exist_okTrue) im.save(dst, formatJPEG, qualityquality, subsampling1, optimizeTrue, progressiveTrue) return new_size注意scale 1.0时不做放大这一点很重要——数据集里往往混着一些本来就很小的图你按比例放大它们只会增加体积和模糊度一点信息都不会多出来。3.3 标签同步处理的两种策略与代码标签处理分两条路走看你的场景选。策略 A等比缩放场景只做过滤和同步。坐标一个不动但要做两件事一是过滤缩放后小到没有意义的框二是确保文件名和后缀跟图片一致。过滤的判定要在像素层面算不要用归一化值直接比因为同样 0.005 的宽度在 1280 宽的图上和 640 宽的图上完全是两回事。做法是用缩放后的新尺寸把归一化值还原成像素def filter_and_copy_label(src_txt, dst_txt, new_w, new_h, min_px2, min_area_px9): out_lines [] if not src_txt.exists(): dst_txt.parent.mkdir(parentsTrue, exist_okTrue) dst_txt.write_text() # 保留空标签别漏掉负样本 return 0, 0 for line in src_txt.read_text().strip().splitlines(): parts line.split() if len(parts) 5: continue cls parts[0] x, y, w, h map(float, parts[1:5]) bw, bh w * new_w, h * new_h if bw min_px or bh min_px or bw * bh min_area_px: continue # 小到看不见丢掉 # 顺手把越界的框夹回 [0,1]防止下一路由归一化误差导致报错 x min(max(x, 0.0), 1.0) y min(max(y, 0.0), 1.0) out_lines.append(f{cls} {x:.6f} {y:.6f} {w:.6f} {h:.6f}) dst_txt.parent.mkdir(parentsTrue, exist_okTrue) dst_txt.write_text(\n.join(out_lines) (\n if out_lines else )) return len(out_lines), 0这里的min_px2、min_area_px9是我的经验值你可以按数据特点调。有两点容易忽略一是空标签文件必须保留因为“这张图里确实没有目标”是一个有效样本如果文件缺失某些训练管线会把它当数据错误跳过或者直接报错二是坐标格式化到小数点后六位就够了再多没有意义还会让文件变大。策略 B补边到正方形场景必须重算。这段逻辑在 2.4 节推导过实现如下def letterbox_label(src_txt, dst_txt, w, h, S): scale S / max(w, h) rw, rh round(w * scale), round(h * scale) ox, oy (S - rw) / 2, (S - rh) / 2 out [] for line in src_txt.read_text().strip().splitlines(): parts line.split() if len(parts) 5: continue cls parts[0] x, y, bw, bh map(float, parts[1:5]) # 归一化 - 像素 - 缩放加偏移 - 归一化 px, py x * w * scale ox, y * h * scale oy pw, ph bw * w * scale, bh * h * scale nx, ny px / S, py / S nw, nh pw / S, ph / S # 夹到边界 nx min(max(nx, 0.0), 1.0) ny min(max(ny, 0.0), 1.0) out.append(f{cls} {nx:.6f} {ny:.6f} {nw:.6f} {nh:.6f}) dst_txt.parent.mkdir(parentsTrue, exist_okTrue) dst_txt.write_text(\n.join(out) (\n if out else ))注意我这版为了代码简洁没有处理“框被边界截断后需要缩窄”的情况。严格来说如果一个框的中心在画面内但边缘超出裁剪后应该把宽高截到边界再重新算归一化值。实际数据里这种情况不多但如果你做的是大幅裁剪就必须补上这段否则框会画出图外训练时被静默忽略。3.4 多进程、断点续跑与日志数据集大起来单进程压缩几万张图能跑到天荒地老。用multiprocessing.Pool开多进程是最简单的方式进程数设成 CPU 核数或核数减一。这里有个必须注意的坑Windows 上多进程是 spawn 模式所有任务函数必须定义在模块顶层并且入口要有if __name__ __main__:保护否则会无限递归创建进程直到把机器拖死。Linux 下是 fork没这个问题所以很多脚本在本地跑得好好的挪到别的环境就炸原因就在这。断点续跑靠一行判断就能实现目标文件存在且非空就跳过。这在批量处理里非常实用因为压缩过程可能因为一张损坏的图报错中断你不想每次都从头再来。import traceback from multiprocessing import Pool def process_one(args): src, dst_img, dst_lbl, max_side args try: if dst_img.exists() and dst_img.stat().st_size 0: return (skip, str(src)) new_w, new_h shrink_one(src, dst_img, max_sidemax_side) src_lbl Path(str(src).replace(/images/, /labels/)).with_suffix(.txt) dst_lbl dst_lbl_for(dst_img) filter_and_copy_label(src_lbl, dst_lbl, new_w, new_h) return (ok, f{src} - {new_w}x{new_h}) except Exception as e: return (fail, f{src} | {e} | {traceback.format_exc()}) if __name__ __main__: tasks build_tasks(...) # 生成 (src, dst_img, dst_lbl, max_side) 元组 with Pool(8) as pool: for status, msg in pool.imap_unordered(process_one, tasks, chunksize16): log(status, msg)日志不要只打印成功数量失败记录的完整异常必须落盘。我在一批数据上遇到过几十张图报OSError: image file is truncated原因是原图本身就是损坏的传输中断造成的半张图。这类文件你只能挑出来单独处理要么丢弃要么重新下载脚本里默默吞掉只会让最终数据集少一部分而你不知道。4. 压完之后训练侧的配套调整与效果验证4.1 清缓存、改配置别让旧的元数据坑你数据换了配置必须跟着换。第一件事是清理旧的缓存文件。很多训练管线会在数据集目录下生成标签缓存形如train.cache、labels.cache或.npy文件里面记录了上一次扫描得到的图片路径、尺寸和标签内容。如果不清掉新数据可能不被识别或者更糟——尺寸信息是旧的导致标签匹配错位训练 loss 一路飙高但你看不出原因。find /path/to/new_dataset -name *.cache -delete find /path/to/new_dataset -name *.npy -delete清完以后第一次启动训练会重新扫描一遍并生成新缓存日志里会有一行提示扫描了多少张图、多少个标注框务必核对这个数字和你脚本输出的统计是否一致不一致就是路径或文件名出了问题。第二件事是改数据配置文件里的路径指向新数据集同时检查nc类别数和names是否和压缩前一致。我习惯在配置里加一行注释记下版本号比如# v2-resized-1280方便回溯是哪一版数据。4.2 训练参数的联动调整压缩之后有几个参数值得顺手改一下能吃到额外收益数据加载进程数workers。这是个反直觉的点图片变小、解码变快之后单个 worker 的吞吐上去了如果还保持原来那么高的 worker 数量CPU 会在进程调度上白白内耗。我的经验是从原来的 8 降到 4 甚至 2先试 4看 GPU 利用率是否还能维持在高位。降下来省出的 CPU 还可以让给别的任务。内存缓存。如果压缩后整个数据集的解码后大小能塞进内存就打开内存缓存选项常见的命名是cacheram或cacheTrue。这一步的提速往往比压缩本身还明显因为彻底消除了磁盘 IO 和解码开销每个 epoch 都从同一份内存里取。要注意它吃的是系统内存不是显存几万张 1280×960 的 RGB 图解码后约 3.5MB 一张一万张就是 35GB动手前先看看机器内存够不够。矩形训练 / 非方形批处理。如果训练管线支持按 batch 内最长边动态组 batch业内常见的叫法是矩形训练那压缩后保留原始宽高比的优势就体现出来了——不用统一补成正方形padding 区域少无效计算也少。这个开关通常只是一个布尔参数但对宽高比差异大的数据集比如大量 16:9 的监控截图收益相当可观。批大小和输入尺寸。如果压缩后显存有富余可以试着把 batch 提一档或者把imgsz提一档。这两者怎么选取决于你的目标提 batch 提升训练稳定性和吞吐提 imgsz 提升小目标检测能力。我的默认选择是先提 batch因为压缩的目的是提效率imgsz 提升会吃掉一部分收益。4.3 一次压缩前后的对照实验光说不练没意义我给一套对照实验的设计你可以照着在自己数据上跑一遍。控制变量同一份原始数据、同一个模型结构、同一个随机种子、同样的 epoch 数我一般用 30~50 个小 epoch 看趋势不用跑满、同样的增强配置。唯一变量是数据集版本原始 vs 压缩到 1280 长边。记录四项指标单 epoch 平均耗时、GPU 平均利用率、验证集在同样 imgsz 下的 mAP50、以及小目标子集的召回率。第三项和第四项最关键因为压缩带来的唯一风险就是精度损失。指标原始数据长边 4032压缩后长边 1280变化数据集体积基准约 1/10大幅下降单 epoch 耗时基准约 0.45 倍明显下降GPU 平均利用率55% 左右88% 左右明显提升mAP50整体基准基本持平波动 ±0.3无显著差异小目标召回基准视压缩比而定压过头会明显下滑需重点观察我这边几次实验的普遍结论是长边压到 imgsz 的 2 倍左右mAP 基本持平速度提升 2 倍以上压到 imgsz 的 1 倍速度提升 3 倍多但小目标召回开始掉。所以那个“1.5 到 2.5 倍”的经验区间不是拍脑袋来的是速度和精度的实际拐点。还有一个容易被忽略的验证项可视化抽查。从压缩后的验证集里随机抽 20 张把标签框画到图上肉眼过一遍。这一步能抓出自动化脚本抓不到的问题比如框整体偏移几个像素、某些图的框跑到画面外面、或者方向转了 90 度。花五分钟看图比调一整天参数有用。5. 踩过的坑与排查手册5.1 框整体偏移、框跑到图外这是压缩后最高频的问题八成出在坐标系理解上。如果所有框都朝同一个方向偏先怀疑两件事一是 EXIF 方向没处理图片实际被旋转了 90 度而标签没跟着转二是做了补边但只改了填充没改坐标偏移量等于 padding 的一半。排查方法很直接挑一张图手动算出标签对应的像素框位置用图像工具画出来看。如果偏差是个固定值就是平移问题padding 忘记加偏移如果偏差随位置变化就是缩放问题比例算错通常是拿错了宽或高如果偏差是整体旋转就是方向问题。还有一种隐蔽情况某些图库里读出来的尺寸是(宽, 高)某些是(高, 宽)混用就会算反。统一约定用(w, h)顺序并且在所有函数签名里写清楚参数名能避免大部分低级错误。5.2 小目标集体消失压缩之后小目标检测性能断崖式下跌通常有两个层次的原因。表层原因是过滤阈值设得太狠2 像素以下的框都被删了训练集里的小目标样本直接没了。深层原因是即使框没被删缩放后目标只剩几个像素卷积网络也提取不出有效特征了。处理办法有三条一是把压缩比放宽从 imgsz 的 1.5 倍提到 2.5 倍二是在过滤时区分对待别把所有类别用同一套像素阈值小类别单独给更宽松的门槛三是考虑分桶处理把小目标密集的图单独放在一个子集里用更小的压缩比训练时再混合采样。这套组合拳我在两个项目里用过基本能救回大部分精度。5.3 文件层面的低级错误这类问题不涉及任何算法但排查起来最耗时间因为它们往往表现为“训练正常但结果不对”。常见的有压缩后图片后缀从.png变成了.jpg标签文件名没跟着改大写后缀.JPG和小写.txt匹配不上某些图的标签文件是空文件被误判为数据错误输出目录里残留了上一次运行的旧文件造成图片和标签混批。防御手段很简单压缩完成后跑一遍一致性校验遍历所有图片检查同名标签是否存在遍历所有标签检查同名图片是否存在两边数量必须相等且每个标签文件里每行的第一个字段必须是合法的类别编号在0到nc-1范围内。这个校验脚本三十行能挡住后面的无数麻烦。5.4 问题速查表现象最可能原因处理动作训练 loss 变成 NaN标签里有 0 宽高或越界值过滤w0或h0坐标夹到 [0,1]框整体偏移固定像素padding 后没改坐标按公式重算加上偏移量框方向错乱、宽高互换EXIF 未处理resize 前调用 exif_transpose图片数少于预期多进程异常被吞落盘完整异常日志逐条排查标签数和图片数对不上后缀不一致或路径替换出错跑一致性校验脚本第一个 epoch 特别慢旧 cache 未清理删除*.cache/*.npy后重启压缩后体积没降多少图片本身已压缩或 quality 设太高检查原始质量quality 降到 85~88训练报通道数错误RGBA 或 CMYK 未转 RGB保存前统一 convert 到 RGB显存占用反而涨了开了内存缓存或 batch 提太大检查 cache 配置与 batch 设置精度轻微下降但可接受正常的压缩信息损失压缩比放宽到 imgsz 的 2 倍以上我在实际做这套流程时最大的体会是压缩本身的技术难度几乎为零难的是确认自己压得对不对。写出压缩脚本十分钟做对照实验和可视化抽查可能要半天。但正是这半天的验证决定了你是白干一场还是真的把训练效率提上去两倍。如果时间只够做一件事我建议先做那个一致性校验加随机可视化抽查的脚本把它当成流水线的固定一环每次改数据都跑一遍——踩过几次坑之后你会发现最省时间的从来不是更聪明的算法而是更早发现错误的检查机制。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。