據(jù)集VOC轉(zhuǎn)YOLO與去泄漏切分實(shí)踐)
簡介這份docx文檔面向電路板PCB缺陷檢測的學(xué)習(xí)者與算法開發(fā)者用于說明一套VOC與YOLO雙格式數(shù)據(jù)集的獲取方式及基本構(gòu)成幫助讀者在訓(xùn)練目標(biāo)檢測模型前快速判斷數(shù)據(jù)規(guī)模、類別分布與適用性。壓縮包內(nèi)共1個docx文件大小約3.38MB正文給出數(shù)據(jù)集的百度云下載地址并記錄9666張jpg圖片、9666個VOC格式xml標(biāo)注和9666個YOLO格式txt標(biāo)注涵蓋falsecopper、missinghole、mousebite、opencircuit等8類缺陷總標(biāo)注框數(shù)51549同時說明使用labelImg按類別繪制矩形框。文檔還提示數(shù)據(jù)集中包含較多椒鹽噪聲生成的增強(qiáng)圖片并逐類列出標(biāo)注框數(shù)便于讀者提前了解樣本特點(diǎn)、類別不均衡情況與潛在噪聲影響也方便按類別抽樣、設(shè)計(jì)損失函數(shù)和劃分訓(xùn)練驗(yàn)證集。目前已有232人學(xué)習(xí)適合需要核對數(shù)據(jù)規(guī)模、評估數(shù)據(jù)質(zhì)量或搭建PCB缺陷檢測實(shí)驗(yàn)的讀者參考。1. 9666張PCB缺陷圖能干什么先看清這8類和51549個框拿到一個 PCB 缺陷檢測數(shù)據(jù)集第一件事不是急著敲訓(xùn)練命令而是算清楚它到底能撐起什么量級的任務(wù)。這份資源是 VOC YOLO 雙格式9666 張 jpg 對應(yīng) 9666 個 xml、9666 個 txt8 個類別合計(jì) 51549 個矩形框平均每張圖 5.3 個目標(biāo)屬于典型的多目標(biāo)小目標(biāo)工業(yè)質(zhì)檢場景——一塊板子上同時出現(xiàn)毛刺、鼠咬、開路、針孔很正常。產(chǎn)出方用 labelImg 拉框類別名全英文格式干凈但不含 ImageSets 路徑文件切分要自己做。它適合三類人想跑通工業(yè)缺陷檢測全流程的算法工程師、需要一個真實(shí)多類別數(shù)據(jù)集做課程設(shè)計(jì)的在校學(xué)生、以及想驗(yàn)證小目標(biāo)檢測方案在實(shí)際產(chǎn)線上能不能用的人。需要提前說清一點(diǎn)包里摻了數(shù)量不小的椒鹽噪聲增強(qiáng)圖這件事直接決定了后面的切分策略第 4 章會專門拆。2. 拆開數(shù)據(jù)包VOC與YOLO雙格式的目錄結(jié)構(gòu)與一次全量體檢解壓之后最先看到的是裸露的 jpg 和 xml/txt 混在一起還是已經(jīng)按子目錄分好這取決于打包方式。不管哪種先把目錄擺放成兩邊都能讀的結(jié)構(gòu)后面所有腳本都基于這套路徑寫中途改路徑是自找麻煩。2.1 兩套目錄一次擺清別讓路徑成為第一個坑VOC 的慣例是JPEGImages存圖、Annotations存 xml、ImageSets/Main存劃分清單。這份資源沒有 ImageSets所以劃分清單得自己生成。YOLO 的慣例是images與labels平行且兩者內(nèi)部路徑結(jié)構(gòu)必須完全一致images/train/xxx.jpg對應(yīng)labels/train/xxx.txt差一層目錄都讀不到。我一般整理成這個形狀VOC 那份保留做溯源和回讀校驗(yàn)YOLO 那份給訓(xùn)練框架吃pcb8/ ├── JPEGImages/ # 9666 張 jpg文件名不帶后綴的 stem 作為主鍵 ├── Annotations/ # 9666 個 xml與 jpg 同名 ├── images/ │ ├── train/ val/ test/ └── labels/ ├── train/ val/ test/有一處容易忽略jpg 和 xml 的 stem 必須嚴(yán)格一致。實(shí)際打包里常出現(xiàn)xxx.JPG大寫后綴、或者 xml 里filename寫的是原圖名而磁盤文件名被改過的情況這兩種都會讓標(biāo)注工具和訓(xùn)練腳本對不上號后面第 5 章會具體說癥狀。2.2 八個類別定死 id映射表與框數(shù)分布類別順序是整條鏈路上最容易翻車的地方。YOLO 的 txt 里只有數(shù)字 id沒有類別名id 和名字的綁定關(guān)系完全靠data.yaml里的names列表維持。轉(zhuǎn)換腳本用的順序、yaml 里寫的順序、評估時打印的順序三處必須完全一致錯一位就變成模型訓(xùn)得很好但 mAP 極低的玄學(xué)現(xiàn)場。按資源給出的類別名我固定成下面這張表id 從 0 開始按原始順序排不做任何按框數(shù)重排的小聰明id類別名常見中文叫法框數(shù)占全部框比例0falsecopper冗余銅/殘銅48529.4%1missinghole缺孔47439.2%2mousebite鼠咬邊緣缺口788315.3%3opencircuit開路803315.6%4pinhole針孔745314.5%5scratch劃傷554310.8%6shortcircuit短路673713.1%7spur毛刺630512.2%框數(shù)最多的 opencircuit 8033 與最少的 missinghole 4743 之間大約 1.7 倍差距在工業(yè)數(shù)據(jù)集里算相當(dāng)均衡了不需要一上來就上重采樣。真正麻煩的是框的尺寸——pinhole 和 missinghole 這類目標(biāo)在整板圖上經(jīng)常只占十幾到幾十像素這才是影響召回的主因而不是類別不平衡。2.3 全量體檢腳本一次性把數(shù)量、越界框、空標(biāo)簽、尺寸分布查干凈在寫任何轉(zhuǎn)換邏輯之前先跑一遍體檢。這一步花三分鐘能省掉后面半天的瞎猜。腳本只依賴標(biāo)準(zhǔn)庫xml.etree足夠解析 labelImg 的輸出。import os from collections import Counter from xml.etree.ElementTree import parse ROOT pcb8 ANN os.path.join(ROOT, Annotations) IMG os.path.join(ROOT, JPEGImages) CLASSES [falsecopper, missinghole, mousebite, opencircuit, pinhole, scratch, shortcircuit, spur] # stem - 文件名用集合運(yùn)算查缺圖、缺標(biāo)注 img_stems {os.path.splitext(f)[0] for f in os.listdir(IMG) if f.lower().endswith(.jpg)} xml_stems {os.path.splitext(f)[0] for f in os.listdir(ANN) if f.lower().endswith(.xml)} print(jpg:, len(img_stems), xml:, len(xml_stems)) print(有圖無標(biāo)注:, len(img_stems - xml_stems)) print(有標(biāo)注無圖:, len(xml_stems - img_stems)) cls_cnt Counter() # 每類框數(shù) size_cnt Counter() # 圖像分辨率分布 boxes_per_img [] # 每圖目標(biāo)數(shù) bad_boxes [] # 越界、退化框 empty_xml [] # 零目標(biāo)的 xml for stem in xml_stems: root parse(os.path.join(ANN, stem .xml)).getroot() W float(root.findtext(size/width)) H float(root.findtext(size/height)) size_cnt[(int(W), int(H))] 1 objs root.findall(object) boxes_per_img.append(len(objs)) if not objs: empty_xml.append(stem) for o in objs: name (o.findtext(name) or ).strip() cls_cnt[name] 1 b o.find(bndbox) x1, y1, x2, y2 (float(b.findtext(k)) for k in (xmin, ymin, xmax, ymax)) # 三個判據(jù)未知類別、零/負(fù)面積、超出圖像邊界 if name not in CLASSES: bad_boxes.append((stem, unknown_cls, name)) elif x2 - x1 1 or y2 - y1 1: bad_boxes.append((stem, degenerate, (x1, y1, x2, y2))) elif x1 0 or y1 0 or x2 W or y2 H: bad_boxes.append((stem, out_of_bound, (x1, y1, x2, y2), (W, H))) print(框數(shù)統(tǒng)計(jì):, dict(cls_cnt), 總計(jì):, sum(cls_cnt.values())) print(分辨率 Top5:, size_cnt.most_common(5)) print(每圖目標(biāo)數(shù) min/avg/max:, min(boxes_per_img), round(sum(boxes_per_img) / len(boxes_per_img), 2), max(boxes_per_img)) print(空標(biāo)注 xml:, len(empty_xml)) print(異???, len(bad_boxes))邏輯很直白用集合差查配對用size節(jié)點(diǎn)查分辨率是否統(tǒng)一用bndbox四個角查退化框和越界框。參數(shù)上唯一要改的是ROOT其余全是從 xml 里現(xiàn)讀的不寫死。跑完重點(diǎn)看三個數(shù)。第一有圖無標(biāo)注是否為 0不為 0 說明打包時丟了 xml那部分圖只能當(dāng)負(fù)樣本或者直接剔掉別硬塞進(jìn)訓(xùn)練集。第二分辨率 Top5是不是只有一項(xiàng)如果出現(xiàn)兩三種分辨率混用letterbox 之外的框架比如按固定尺寸 resize 的會把目標(biāo)拉變形框的位置隨之偏移。第三異??蚶飋ut_of_bound的數(shù)量labelImg 手拉框貼邊時xmax等于寬度甚至多 1 像素很常見這類框不致命但要 clamp 掉否則歸一化后大于 1部分框架會直接報(bào)錯退出??諛?biāo)注 xml也別忽略。零目標(biāo)的圖有兩種用法丟掉或者保留成負(fù)樣本幫助降誤檢。工業(yè)質(zhì)檢里背景干凈的板子多保留一部分負(fù)樣本通常有益但比例別超過 5%否則正樣本梯度會被稀釋。3. VOC轉(zhuǎn)YOLO歸一化換算與一份可回讀校驗(yàn)的轉(zhuǎn)換腳本格式轉(zhuǎn)換本身不復(fù)雜麻煩全在邊界上。資源同時給了 xml 和 txt理論上可以直接用 txt但我仍然建議自己從 xml 轉(zhuǎn)一遍原因是你能控制 id 順序、能順手 clamp 越界框、能記錄轉(zhuǎn)換前后的框數(shù)對賬出了問題有據(jù)可查。3.1 歸一化不是除以寬高那么簡單VOC 的bndbox是像素絕對坐標(biāo)左上角(xmin, ymin)、右下角(xmax, ymax)labelImg 存的是 0 基坐標(biāo)。YOLO 要的是中心點(diǎn)加寬高、并且歸一化到 0~1xc (xmin xmax) / 2 / W yc (ymin ymax) / 2 / H bw (xmax - xmin) / W bh (ymax - ymin) / H三個細(xì)節(jié)決定成敗。一是xmax允許等于W此時xc bw/2恰好等于 1浮點(diǎn)誤差可能算出 1.0000001得 clamp 到[0, 1]。二是寬度或高度算出來小于 1 像素的框必須先剔除再歸一化否則會生成w0.00003這種在特征圖上完全消失的框訓(xùn)練時表現(xiàn)為 loss 突然 NaN 或者某類 AP 恒為 0。三是寫文件時保留 6 位小數(shù)足夠YOLO 解析器不在乎精度但保留太多位會讓 txt 體積膨脹。3.2 轉(zhuǎn)換腳本從 xml 轉(zhuǎn) txt單文件對單文件順手做 clamp 和退化框過濾。這份資源已經(jīng)帶了 txt腳本跑完先備份原 txt再覆蓋方便對比兩邊框數(shù)是否一致。import os from xml.etree.ElementTree import parse ROOT pcb8 ANN os.path.join(ROOT, Annotations) LBL os.path.join(ROOT, labels_all) # 先輸出到臨時目錄校驗(yàn)通過再替換 CLASSES [falsecopper, missinghole, mousebite, opencircuit, pinhole, scratch, shortcircuit, spur] CLS2ID {c: i for i, c in enumerate(CLASSES)} os.makedirs(LBL, exist_okTrue) stat {imgs: 0, boxes_in: 0, boxes_out: 0, drop_degenerate: 0, drop_unknown: 0} for fn in sorted(os.listdir(ANN)): if not fn.endswith(.xml): continue stem os.path.splitext(fn)[0] root parse(os.path.join(ANN, fn)).getroot() W float(root.findtext(size/width)) H float(root.findtext(size/height)) lines [] for o in root.findall(object): stat[boxes_in] 1 name (o.findtext(name) or ).strip() if name not in CLS2ID: # 拼寫不一致、大小寫混用都在這里被攔下 stat[drop_unknown] 1 continue b o.find(bndbox) x1 float(b.findtext(xmin)); y1 float(b.findtext(ymin)) x2 float(b.findtext(xmax)); y2 float(b.findtext(ymax)) # 先裁到圖像范圍內(nèi)再判退化順序不能反 x1 max(0.0, min(x1, W)); x2 max(0.0, min(x2, W)) y1 max(0.0, min(y1, H)); y2 max(0.0, min(y2, H)) if x2 - x1 1.0 or y2 - y1 1.0: stat[drop_degenerate] 1 continue xc (x1 x2) / 2.0 / W yc (y1 y2) / 2.0 / H bw (x2 - x1) / W bh (y2 - y1) / H xc min(max(xc, 0.0), 1.0); yc min(max(yc, 0.0), 1.0) bw min(max(bw, 0.0), 1.0); bh min(max(bh, 0.0), 1.0) lines.append(f{CLS2ID[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) stat[boxes_out] 1 # 空文件也要寫出來表示這是一張負(fù)樣本而不是缺失標(biāo)注 with open(os.path.join(LBL, stem .txt), w, encodingutf-8) as f: f.write(\n.join(lines)) stat[imgs] 1 print(stat)參數(shù)上只有ROOT和CLASSES需要按實(shí)際改。CLASSES的順序就是最終的 id改這里等于改標(biāo)簽語義一旦定了就別再動。腳本做了兩件多余的事都是血淚經(jīng)驗(yàn)換的。第一未知類別不靜默跳過而是計(jì)數(shù)跑完drop_unknown必須是 0不為 0 就說明 xml 里存在拼寫變體比如false_copper、ShortCircuit得回頭清洗不能讓它變成一批無標(biāo)注圖悄悄進(jìn)入訓(xùn)練集。第二空標(biāo)注寫空文件而不是不寫文件。訓(xùn)練框架區(qū)分這張圖沒有目標(biāo)和這張圖的標(biāo)注丟了前者是合法負(fù)樣本后者是數(shù)據(jù)錯誤少寫文件會讓框架把這張圖當(dāng)背景丟掉等于白扔。3.3 回讀校驗(yàn)把 txt 反算回像素框再和 xml 對賬轉(zhuǎn)換完直接開訓(xùn)是最常見的偷懶。我一般加一道反向校驗(yàn)讀 txt按W/H反算回像素坐標(biāo)和原 xml 的四個角比誤差超過 1 像素就報(bào)出來。這一步能抓住兩類隱蔽錯誤——?dú)w一化時把寬高用反拿 H 除 x以及某個類別的 id 映射整體偏移一位。import os from xml.etree.ElementTree import parse ROOT pcb8 ANN os.path.join(ROOT, Annotations) LBL os.path.join(ROOT, labels_all) max_err 0.0 mismatch 0 for fn in os.listdir(ANN): if not fn.endswith(.xml): continue stem os.path.splitext(fn)[0] root parse(os.path.join(ANN, fn)).getroot() W float(root.findtext(size/width)); H float(root.findtext(size/height)) gt [] for o in root.findall(object): b o.find(bndbox) gt.append(tuple(float(b.findtext(k)) for k in (xmin, ymin, xmax, ymax))) with open(os.path.join(LBL, stem .txt), encodingutf-8) as f: rows [ln.split() for ln in f.read().strip().splitlines() if ln.strip()] if len(rows) ! len(gt): mismatch 1 continue for row, (gx1, gy1, gx2, gy2) in zip(rows, gt): xc, yc, bw, bh map(float, row[1:5]) px1 (xc - bw / 2) * W; py1 (yc - bh / 2) * H px2 (xc bw / 2) * W; py2 (yc bh / 2) * H err max(abs(px1 - gx1), abs(py1 - gy1), abs(px2 - gx2), abs(py2 - gy2)) max_err max(max_err, err) print(框數(shù)不一致的圖:, mismatch, 最大像素誤差:, round(max_err, 3))正常結(jié)果應(yīng)該是mismatch0、最大像素誤差小于 0.01。如果誤差在 1 像素上下說明某處做了 round 取整不影響訓(xùn)練如果誤差到了幾十像素基本就是寬高用反了回去查公式。4. 椒鹽噪聲增強(qiáng)圖分組切分、采樣與去泄漏這份資源最需要當(dāng)心的地方不在格式在數(shù)據(jù)構(gòu)成。原始說明里寫得很清楚里面有很多椒鹽噪聲生成的增強(qiáng)圖片。這句話意味著同一張?jiān)及鍒D對應(yīng)多張加噪版本它們的內(nèi)容幾乎相同、標(biāo)注完全相同。如果按文件隨機(jī)切分同一張?jiān)瓐D的噪聲版會同時落進(jìn)訓(xùn)練集和驗(yàn)證集驗(yàn)證指標(biāo)會漂亮得不像話上線一測直接打回原形。4.1 同源增強(qiáng)圖跨集指標(biāo)虛高能有多離譜假設(shè) 9666 張里有 3000 張是噪聲增強(qiáng)版隨機(jī)按 8:1:1 切粗略估算會有三成左右的驗(yàn)證圖能在訓(xùn)練集里找到雙胞胎。模型不需要學(xué)會識別缺陷只要記住噪聲紋理和大致位置就能給出高分。這種虛高在驗(yàn)證集上可能反映為 mAP 漲 10 個點(diǎn)以上而換一個真實(shí)產(chǎn)線批次去測掉回來的幅度同樣驚人。判斷依據(jù)很簡單把驗(yàn)證集里的每張圖拿去訓(xùn)練集做一次近似檢索縮略圖哈希、感知哈希都行命中率高就說明切分有泄漏。做這個檢查花不了多少時間比訓(xùn)完再懷疑人生劃算得多。4.2 先探測命名規(guī)律再定分組鍵去泄漏的前提是能認(rèn)出哪些圖同源。多數(shù)增強(qiáng)腳本會在文件名尾部留下痕跡比如board_0123_1.jpg、board_0123_noise.jpg、board_0123_sp.jpg。但也有人直接重排成img_00001.jpg到img_09666.jpg把原始對應(yīng)關(guān)系抹掉那就只能靠內(nèi)容相似度分組。先打印一批文件名看看規(guī)律別上來就寫死正則import os, re from collections import Counter IMG pcb8/JPEGImages stems [os.path.splitext(f)[0] for f in os.listdir(IMG) if f.lower().endswith(.jpg)] for s in stems[:15]: print(s) # 統(tǒng)計(jì)最后一個下劃線后面那段的分布能快速看出增強(qiáng)后綴長什么樣 tail Counter(re.split(r[_\-], s)[-1] for s in stems) print(tail.most_common(20))看tail的分布如果出現(xiàn)大量noise、sp、aug、純數(shù)字這類尾部基本可以確定后綴規(guī)則如果尾部全是隨機(jī)字符或者根本看不出模式說明命名被重排過只能走內(nèi)容哈希。資源里給的是網(wǎng)盤打包命名風(fēng)格沒法提前斷言所以這一步不能省。4.3 分組切分腳本確定后綴規(guī)則后把 stem 剝掉增強(qiáng)后綴得到group_key同一個 key 下的所有圖必須整組進(jìn)入同一個集合。按 8:1:1 分測試集可以留也可以不留做課程設(shè)計(jì)建議留一份完全不碰的 holdout。import os, re, random from collections import defaultdict IMG pcb8/JPEGImages OUT pcb8/splits SUFFIX re.compile(r[_\-](noise|salt|pepper|sp|aug\d*|aug|n\d|\d{1,3})$, re.I) stems sorted(os.path.splitext(f)[0] for f in os.listdir(IMG) if f.lower().endswith(.jpg)) groups defaultdict(list) for s in stems: groups[SUFFIX.sub(, s)].append(s) # 剝掉增強(qiáng)后綴同源圖歸到一組 keys sorted(groups) random.seed(42) # 固定種子保證每次切分結(jié)果可復(fù)現(xiàn) random.shuffle(keys) n len(keys) n_val max(1, int(n * 0.1)) n_test max(1, int(n * 0.1)) split { val: keys[:n_val], test: keys[n_val:n_val n_test], train: keys[n_val n_test:], } os.makedirs(OUT, exist_okTrue) for name, ks in split.items(): with open(os.path.join(OUT, f{name}.txt), w, encodingutf-8) as f: for k in ks: for s in groups[k]: f.write(s \n) print(name, 組數(shù):, len(ks), 圖片數(shù):, sum(len(groups[k]) for k in ks))關(guān)鍵參數(shù)是SUFFIX正則和seed。正則一定要對照 4.2 打印出來的真實(shí)后綴寫寫寬了會把正常編號當(dāng)后綴剝掉導(dǎo)致大量不相干的圖被強(qiáng)行歸成一組訓(xùn)練集縮水寫窄了等于沒分組泄漏照舊。判斷標(biāo)準(zhǔn)很實(shí)在跑完看train/val/test的圖片數(shù)比例是不是接近 8:1:1如果某一組圖片數(shù)異常膨脹多半是正則剝過頭了。4.4 采樣與增強(qiáng)策略噪聲圖該占多少分組切分只解決了泄漏沒解決分布問題。如果噪聲增強(qiáng)圖占了總量的一半訓(xùn)練集里就會有一半樣本帶著同一種人造噪聲紋理模型很容易把它當(dāng)成判別特征。我的做法是分兩步。第一步訓(xùn)練集內(nèi)部對同源組做上限控制同一group_key最多保留 2 到 3 張?jiān)鰪?qiáng)版其余只在需要做魯棒性對比實(shí)驗(yàn)時按需取用。第二步驗(yàn)證集和測試集只放原始圖不放噪聲版這樣報(bào)出來的指標(biāo)對應(yīng)的是真實(shí)場景而不是在噪聲分布上的自評。增強(qiáng)策略上也要相應(yīng)調(diào)整。既然數(shù)據(jù)里已經(jīng)有人造椒鹽噪聲訓(xùn)練側(cè)就不該再無腦疊加同類型噪聲增強(qiáng)否則真實(shí)分布被推得更遠(yuǎn)。常見做法是訓(xùn)練增強(qiáng)以幾何變換為主——隨機(jī)縮放、平移、馬賽克、HSV 抖動噪聲類增強(qiáng)只在做消融時單獨(dú)開關(guān)。這個取舍在工業(yè)質(zhì)檢里很重要產(chǎn)線上的噪聲類型和增強(qiáng)腳本生成的噪聲未必一致過度擬合某一種噪聲紋理反而降低泛化。5. 避坑與排查這份數(shù)據(jù)集落地時最容易翻車的五處下面五條都是跑這類雙格式工業(yè)數(shù)據(jù)集時反復(fù)出現(xiàn)的癥狀按現(xiàn)象 → 原因 → 解決記遇到了直接對號入座。5.1 訓(xùn)練日志里正樣本數(shù)遠(yuǎn)小于 51549甚至提示大量背景圖現(xiàn)象開訓(xùn)第一個 epoch日志里的 instances 數(shù)量只有幾萬甚至更少或者框架提示backgrounds占比異常高。原因images/與labels/的目錄層級沒對齊。YOLO 系列的標(biāo)簽路徑是把 images 路徑里的images替換成labels推出來的如果一邊是images/train/xxx.jpg、另一邊是labels/xxx.txt推出來的路徑全部落空框架會把所有圖當(dāng)負(fù)樣本。解決用一條命令對賬兩邊 stem 集合必須完全相等且相對路徑一致cd pcb8 diff (find images -name *.jpg | sed s#images/##; s#\.jpg$## | sort) \ (find labels -name *.txt | sed s#labels/##; s#\.txt$## | sort) | head輸出為空才繼續(xù)往下走有輸出就按提示補(bǔ)目錄或改結(jié)構(gòu)。5.2 訓(xùn)練 loss 正常下降但驗(yàn)證 mAP 一直在 0.01 附近現(xiàn)象分類 loss 和框回歸 loss 都收斂得不錯評估時各類 AP 幾乎全為 0混淆矩陣一整片糊在背景上。原因id 順序錯位。轉(zhuǎn)換腳本按字母序排了類別data.yaml里按資源原始順序?qū)懙膬蛇厡Σ簧夏P蛯W(xué)到的0 號類和評估時的0 號類根本不是同一個缺陷。解決把 yaml 里的names和轉(zhuǎn)換腳本里的CLASSES抄到同一份文件里做唯一來源兩邊都從這個文件讀。改完重轉(zhuǎn)一遍 txt別在原 txt 上打補(bǔ)丁。5.3 圖像分辨率不統(tǒng)一框位置整體偏移現(xiàn)象可視化預(yù)測框時框的大小看著對位置整體平移了一段或者框被拉成長條。原因數(shù)據(jù)集里混了兩種以上分辨率而訓(xùn)練配置用的是固定尺寸直接 resize 而不是 letterbox。直接 resize 會改變寬高比歸一化坐標(biāo)雖然不變但圖像內(nèi)容被拉伸等效于框位置錯位。解決先看 2.3 腳本輸出的分辨率分布。多分辨率共存時優(yōu)先用 letterbox 的框架配置或者統(tǒng)一預(yù)處理到同一尺寸再入庫絕不靠訓(xùn)練時隱式 resize 兜底。5.4 驗(yàn)證集指標(biāo)好得離譜換一批真實(shí)圖就崩現(xiàn)象驗(yàn)證集 mAP 0.9 以上拿產(chǎn)線新拍的板子去測漏檢一大片。原因椒鹽噪聲增強(qiáng)圖和原圖被隨機(jī)切分打散同源圖跨了訓(xùn)練集和驗(yàn)證集驗(yàn)證集實(shí)際上是訓(xùn)練分布的近鄰。解決按第 4 章的分組切分重做一遍驗(yàn)證集只留原圖。重做完指標(biāo)一般會掉幾個點(diǎn)那才是真實(shí)水平。掉得越多說明之前的泄漏越嚴(yán)重不是模型退步了。5.5 針孔、缺孔兩類召回明顯偏低其他類正?,F(xiàn)象8 個類里 pinhole 和 missinghole 的 AP 比其余類低一截誤檢倒是不多就是檢不出來。原因這兩類目標(biāo)在原圖上普遍只有幾十像素縮到 640 輸入后特征圖上不到 8 像素卷積下采樣幾輪就沒了。解決優(yōu)先提高輸入尺寸到 960 或 1280 試一輪對比這兩類的 AP 變化如果顯存不夠改用切圖訓(xùn)練加切圖推理大圖切片、重疊區(qū)做 NMS 合并。這兩條路我在小目標(biāo)缺陷上都試過切圖對小目標(biāo)召回提升最直接代價是推理耗時翻倍得按產(chǎn)線節(jié)拍權(quán)衡。6. 訓(xùn)練與復(fù)核把8類mAP拆開看的三個動作6.1 先跑一個不加料的基線基線的作用是給你一個參照系。數(shù)據(jù)按第 4 章切好、yaml 寫對之后先跑一個不動任何超參的版本輸入尺寸取 640增強(qiáng)只開幾何變換記錄總 mAP 和每類 AP。配置寫成 yaml 放在項(xiàng)目根目錄和轉(zhuǎn)換腳本共用同一份類別列表# pcb8.yaml path: /data/pcb8 train: splits/train.txt val: splits/val.txt test: splits/test.txt names: 0: falsecopper 1: missinghole 2: mousebite 3: opencircuit 4: pinhole 5: scratch 6: shortcircuit 7: spur訓(xùn)練命令按你本地的框架來以常見的檢測器 CLI 為例# 權(quán)重?fù)Q成你本地已有的檢測權(quán)重imgsz 先用 640 出基線 yolo detect train datapcb8.yaml modelyolov8s.pt imgsz640 epochs100 batch16 workers8 seed42seed42和切分腳本里的種子保持一致保證可復(fù)現(xiàn)workers按機(jī)器核數(shù)給給太大會在讀取大量小文件時卡 IO?;€跑完把results.csv存一份后面任何一個改動都拿它對比不做無參照的調(diào)參。6.2 盯住 mousebite 和 spur 的混淆而不是總 mAP總 mAP 漲 0.5 個點(diǎn)沒什么信息量按類別拆開才有。這兩類最容易互相串鼠咬是板邊被咬掉一塊的缺口毛刺是銅箔邊緣多出來的尖出物在低分辨率下都表現(xiàn)為邊緣的小塊異?;煜仃?yán)锝?jīng)常成對出現(xiàn)。我的習(xí)慣是先看混淆矩陣如果mousebite → spur或反向的誤分超過該類樣本的 10%就去檢查標(biāo)注本身——翻二十張這類圖的原圖和框看有沒有同一個缺陷被標(biāo)成兩個類的情況。數(shù)據(jù)集標(biāo)注以矩形框?yàn)闇?zhǔn)邊界模糊的目標(biāo)在不同標(biāo)注者手里本來就有分歧這類問題靠調(diào)模型解決不了。6.3 小目標(biāo)的后悔藥提高輸入尺寸與切圖推理pinhole 和 missinghole 的 AP 是這套數(shù)據(jù)的天花板所在。先做一輪imgsz對比640 / 960 / 1280 各跑一次只看這兩類的 AP 變化其余類當(dāng)對照。如果 960 相比 640 有明顯提升而 1280 收益遞減說明分辨率已經(jīng)夠用瓶頸在別處如果一直線性漲那就得考慮切圖。切圖推理的實(shí)現(xiàn)要點(diǎn)是重疊率給到 0.2 到 0.3切片邊緣的目標(biāo)在兩個相鄰切片里都會出現(xiàn)合并時靠 NMS 去重。重疊率給低了會切掉目標(biāo)給高了顯存和耗時都上去。我一般先在驗(yàn)證集上掃 0.1 / 0.2 / 0.3 三檔挑召回最高的那一檔再上產(chǎn)線。從那以后我每次拿到帶增強(qiáng)圖片字樣的數(shù)據(jù)集都強(qiáng)制先跑一遍同源分組和跨集近似檢索再動訓(xùn)練腳本——這個習(xí)慣救過我不止一次虛高的驗(yàn)證指標(biāo)比低指標(biāo)更害人。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取