據(jù)增強(qiáng)實(shí)戰(zhàn):albumentations一鍵生成圖片與XML標(biāo)注)
這段時(shí)間被問(wèn)得最多的一個(gè)問(wèn)題就是畢設(shè)跑YOLO數(shù)據(jù)集只有一兩百?gòu)埬P陀?xùn)出來(lái)mAP低得沒(méi)法看怎么辦前陣子某同學(xué)的課題是做一個(gè)常見(jiàn)目標(biāo)的檢測(cè)系統(tǒng)原始標(biāo)注圖片一共才280張里面兩個(gè)類(lèi)別數(shù)量還不均衡。我讓他先別急著換網(wǎng)絡(luò)結(jié)構(gòu)也別去翻更大的公開(kāi)數(shù)據(jù)集先把數(shù)據(jù)增強(qiáng)做了。他照著一套腳本批量生成了圖片和對(duì)應(yīng)的XML標(biāo)注兩周后mAP從0.4出頭提到了0.6左右論文里的對(duì)比實(shí)驗(yàn)也有了著落。這篇文章就是把這套思路完整寫(xiě)出來(lái)為什么YOLO小樣本會(huì)崩、怎么用自動(dòng)數(shù)據(jù)增強(qiáng)生成圖片XML、增強(qiáng)腳本怎么設(shè)計(jì)不踩坑以及增強(qiáng)完之后訓(xùn)練參數(shù)怎么跟進(jìn)。1. 畢設(shè)小樣本困境為什么YOLO模型一訓(xùn)就廢1.1 數(shù)據(jù)量少時(shí)模型到底在學(xué)什么很多人有個(gè)誤區(qū)覺(jué)得YOLO這種模型丟進(jìn)去幾張圖就能學(xué)會(huì)目標(biāo)長(zhǎng)什么樣。實(shí)際上目標(biāo)檢測(cè)模型參數(shù)動(dòng)輒幾百萬(wàn)它需要從大量樣本中歸納出什么特征屬于目標(biāo)和什么特征屬于背景。數(shù)據(jù)量只有一兩百?gòu)垥r(shí)模型很容易走捷徑——記住訓(xùn)練圖中目標(biāo)周?chē)谋尘凹y理、顏色分布這些不該學(xué)的東西。典型表現(xiàn)就是訓(xùn)練集loss一直降驗(yàn)證集mAP波動(dòng)很大換個(gè)隨機(jī)種子結(jié)果能差好幾個(gè)點(diǎn)。我習(xí)慣打個(gè)比方給小孩看兩本帶插畫(huà)的故事書(shū)然后讓他去判斷一群照片里哪張是貓。書(shū)里兩只貓恰好都在沙發(fā)上他很可能把沙發(fā)色當(dāng)成判斷依據(jù)換到草地背景就開(kāi)始瞎猜。YOLO在小數(shù)據(jù)集上的行為和這個(gè)非常像它不是沒(méi)學(xué)會(huì)是學(xué)歪了。1.2 手做增強(qiáng)為什么不可控不少同學(xué)的直覺(jué)是先復(fù)制圖片再用圖像處理軟件或手機(jī)修圖改改亮度、加個(gè)濾鏡然后手動(dòng)重新標(biāo)注一遍。這種做法的問(wèn)題很直接。第一效率極低。300張圖手動(dòng)改完再標(biāo)XML一天能做完就算不錯(cuò)而且人的注意力有限框的位置很容易標(biāo)偏。第二不可復(fù)現(xiàn)。每張圖的變換參數(shù)都是隨緣的論文里寫(xiě)進(jìn)行了數(shù)據(jù)增強(qiáng)別人問(wèn)到底做了哪些操作、參數(shù)多少根本說(shuō)不清楚。第三容易漏標(biāo)。改圖之后忘了同步XML訓(xùn)練時(shí)YOLO把沒(méi)標(biāo)注的目標(biāo)當(dāng)背景學(xué)習(xí)反而干擾模型。自動(dòng)化的意義不光是省時(shí)間更重要的是可復(fù)現(xiàn)、可審計(jì)。腳本一跑每張?jiān)鰪?qiáng)圖怎么生成的、用了幾種變換、原圖和新圖的對(duì)應(yīng)關(guān)系全部可控這個(gè)在畢設(shè)答辯和論文實(shí)驗(yàn)描述里是很加分的點(diǎn)。標(biāo)題里說(shuō)一鍵生成圖片XML本質(zhì)上就是把數(shù)據(jù)準(zhǔn)備環(huán)節(jié)從手工勞動(dòng)變成程序化流水線(xiàn)。2. 增強(qiáng)方案選型三種常見(jiàn)路子為什么選albumentations2.1 從零手寫(xiě)增強(qiáng)腳本的代價(jià)很多同學(xué)第一反應(yīng)是用OpenCV自己寫(xiě)翻轉(zhuǎn)、改亮度、加噪聲都不難二三十行代碼能跑。問(wèn)題出在坐標(biāo)同步上。水平翻轉(zhuǎn)時(shí)框坐標(biāo)是xmin_new W - 1 - xmax_old、xmax_new W - 1 - xmin_old這個(gè)還好說(shuō)。一旦加上旋轉(zhuǎn)、隨機(jī)裁剪、縮放坐標(biāo)變換的公式就要成倍增加而且每個(gè)函數(shù)都得自己處理邊界越界、框被裁掉一半、面積過(guò)小這些情況。我見(jiàn)過(guò)一個(gè)同學(xué)手寫(xiě)增強(qiáng)翻轉(zhuǎn)和亮度都正常加了旋轉(zhuǎn)之后訓(xùn)練出來(lái)的模型檢測(cè)框全是歪的。排查半天發(fā)現(xiàn)他旋轉(zhuǎn)圖片時(shí)忘了旋轉(zhuǎn)標(biāo)注框YOLO拿錯(cuò)誤的框去算損失等于用垃圾標(biāo)簽訓(xùn)模型。這類(lèi)bug非常隱蔽因?yàn)閱慰磮D片看不出問(wèn)題標(biāo)注框只在訓(xùn)練時(shí)才被讀取。2.2 在albumentations和imgaug之間做選擇成熟的開(kāi)源增強(qiáng)庫(kù)有很多我最終選擇了albumentations主要基于以下幾點(diǎn)考慮。albumentations把變換圖片和同步變換標(biāo)注框封裝在了一起。Compose里聲明一個(gè)bbox_params傳入格式說(shuō)明和標(biāo)簽字段之后所有增強(qiáng)都會(huì)自動(dòng)更新框坐標(biāo)不用自己管理任何公式。它支持pascal_voc格式也就是XML里用的xmin, ymin, xmax, ymax可以直接對(duì)接VOC標(biāo)注不需要額外寫(xiě)轉(zhuǎn)換層。albumentations的API設(shè)計(jì)接近字典輸入和輸出結(jié)構(gòu)清晰傳入image和bboxes拿回變換后的結(jié)果每一步都能打印中間狀態(tài)排查問(wèn)題方便很多。對(duì)比imgaug它的功能確實(shí)更豐富但對(duì)bbox格式的要求更嚴(yán)格文檔維護(hù)狀態(tài)也比早期差了一些。對(duì)一個(gè)畢設(shè)項(xiàng)目來(lái)說(shuō)能快速跑通、不容易出坐標(biāo)bug、社區(qū)資料多比功能堆砌重要得多。2.3 離線(xiàn)增強(qiáng)優(yōu)于在線(xiàn)增強(qiáng)的取舍點(diǎn)YOLO訓(xùn)練本身也支持在線(xiàn)增強(qiáng)——每個(gè)epoch讀圖時(shí)隨機(jī)變換。那為什么還要離線(xiàn)生成圖片XML這里要搞清兩者定位不同。在線(xiàn)增強(qiáng)的好處是省存儲(chǔ)、epoch之間數(shù)據(jù)還能變化但壞處是訓(xùn)練時(shí)沒(méi)法直觀檢查增強(qiáng)效果而且你沒(méi)法把增強(qiáng)后的數(shù)據(jù)集單獨(dú)拿出來(lái)提交、復(fù)用或者給其他人一起訓(xùn)練。畢設(shè)場(chǎng)景里離線(xiàn)增強(qiáng)有幾個(gè)實(shí)際好處生成完可以人工抽查確保每張圖質(zhì)量正??梢栽谡撐睦锩鞔_寫(xiě)出原始數(shù)據(jù)集800張?jiān)鰪?qiáng)后3000張這種硬數(shù)字還可以把增強(qiáng)數(shù)據(jù)集直接喂給后續(xù)算法對(duì)比不用每次訓(xùn)練都重復(fù)算增強(qiáng)。我采用的策略是離線(xiàn)先擴(kuò)到3到5倍訓(xùn)練時(shí)YOLO自帶的在線(xiàn)增強(qiáng)強(qiáng)度保持默認(rèn)或稍微調(diào)低兩者不沖突。3. 核心實(shí)現(xiàn)圖片和XML如何同步變形3.1 標(biāo)注統(tǒng)一格式先看懂XML內(nèi)部結(jié)構(gòu)YOLO最常用的標(biāo)注格式有兩種YOLO的txt格式一行一個(gè)目標(biāo)內(nèi)容是類(lèi)別 cx cy w h歸一化到0到1另一種是VOC的XML格式XML里直接記錄像素坐標(biāo)。標(biāo)題說(shuō)生成圖片XML那就要按VOC格式來(lái)處理。一個(gè)標(biāo)準(zhǔn)的VOC XML結(jié)構(gòu)里有幾個(gè)關(guān)鍵節(jié)點(diǎn)size節(jié)點(diǎn)記錄圖片的width、height、depth。這個(gè)信息至關(guān)重要因?yàn)樽龇D(zhuǎn)、縮放、裁剪時(shí)所有坐標(biāo)運(yùn)算都依賴(lài)原始寬高。object節(jié)點(diǎn)每個(gè)目標(biāo)一個(gè)包含name類(lèi)別名和bndbox子節(jié)點(diǎn)里面是xmin、ymin、xmax、ymax都是像素坐標(biāo)。filename節(jié)點(diǎn)記錄對(duì)應(yīng)圖片的文件名生成增強(qiáng)圖時(shí)要同步改成新文件名。解析XML用Python自帶的xml.etree.ElementTree就夠了不需要第三方庫(kù)。很多同學(xué)在標(biāo)注階段用LabelImg導(dǎo)出XML自動(dòng)就是VOC格式可以直接拿來(lái)做輸入。3.2 坐標(biāo)跟著圖片走的兩個(gè)關(guān)鍵操作為什么增強(qiáng)后框會(huì)錯(cuò)位因?yàn)閳D片的每個(gè)像素都變了位置標(biāo)注坐標(biāo)必須跟著變。以水平翻轉(zhuǎn)為例原圖寬度為W原框區(qū)域是[xmin, ymin, xmax, ymax]翻轉(zhuǎn)之后圖片上原來(lái)在x位置的像素會(huì)跑到W - 1 - x的位置所以新坐標(biāo)是xmin_new W - 1 - xmax_old xmax_new W - 1 - xmin_old ymin_new ymin_old ymax_new ymax_old旋轉(zhuǎn)90度就麻煩一些。順時(shí)針旋轉(zhuǎn)90度后圖片長(zhǎng)寬互換原坐標(biāo)(x, y)在新圖中的位置是(H - 1 - y, x)這意味著原來(lái)的xmin會(huì)變成新圖里的ymin原來(lái)的ymax會(huì)變成新圖里的ymax同時(shí)還要加偏移和反轉(zhuǎn)。每次寫(xiě)這種映射都很容易漏掉某個(gè)方向。這也是我堅(jiān)持用albumentations的核心原因這類(lèi)幾何變換的坐標(biāo)映射是檢測(cè)領(lǐng)域的高頻bug自己手寫(xiě)一遍等于把成熟庫(kù)踩過(guò)的坑重新踩一遍。albumentations內(nèi)部已經(jīng)按變換矩陣算好了bbox映射我們要做的是正確傳參而不是重復(fù)造輪子。3.3 用albumentations安全處理bbox的四步套路用albumentations做帶標(biāo)注的增強(qiáng)流程可以固定成四步任何項(xiàng)目都是這個(gè)套路。第一步用ET.parse讀取XML把圖片寬高和所有目標(biāo)的[xmin, ymin, xmax, ymax]、類(lèi)別名提取出來(lái)。第二步把提取的boxes拼成列表類(lèi)別名拼成另一個(gè)列表順序必須一一對(duì)應(yīng)。第三步構(gòu)造A.Compose增強(qiáng)管道并傳入bbox_params聲明格式第四步調(diào)用pipeline(image..., bboxes..., labels...)從返回值里取出增強(qiáng)后的圖、新框坐標(biāo)、新標(biāo)簽。bbox_params里有三個(gè)參數(shù)很關(guān)鍵formatpascal_voc告訴庫(kù)傳入的坐標(biāo)是像素值的xmin, ymin, xmax, ymax不是歸一化格式。label_fields[labels]聲明labels列表和bboxes一一對(duì)應(yīng)這樣篩選掉被裁掉的框時(shí)對(duì)應(yīng)的標(biāo)簽也會(huì)同步移除。min_visibility和min_area濾波參數(shù)。比如min_visibility0.3表示增強(qiáng)后框至少還有30%面積可見(jiàn)否則丟棄min_area4表示框面積小于4像素就丟棄。這兩個(gè)參數(shù)能自動(dòng)剔除那些被裁剪到幾乎不可見(jiàn)的退化樣本。3.4 完整腳本一鍵批量生成圖片XML下面這個(gè)腳本是我在實(shí)際項(xiàng)目里整理過(guò)的版本核心邏輯可以直接跑。我加了比較多的注釋重點(diǎn)是展示怎么把XML解析、增強(qiáng)、XML回寫(xiě)三個(gè)環(huán)節(jié)串起來(lái)。import os import glob import xml.etree.ElementTree as ET import cv2 import albumentations as A TARGET_SHORT_SIDE 640 OUTPUT_DIR augmented_output INPUT_DIR annotations # 存放原始XML的目錄 IMAGE_DIR images # 與XML同名的原始圖片目錄 PER_IMAGE 5 # 每張?jiān)瓐D生成幾個(gè)增強(qiáng)版本 def parse_voc_xml(xml_path): 解析VOC XML返回寬高以及boxes、labels列表 tree ET.parse(xml_path) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) d int(root.find(size/depth).text) boxes [] labels [] for obj in root.iter(object): name obj.find(name).text bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) boxes.append([xmin, ymin, xmax, ymax]) labels.append(name) return w, h, d, boxes, labels def write_voc_xml(img_path, xml_path, size, boxes, labels): 把增強(qiáng)后的信息寫(xiě)回VOC XML root ET.Element(annotation) folder ET.SubElement(root, folder) folder.text os.path.basename(os.path.dirname(img_path)) filename ET.SubElement(root, filename) filename.text os.path.basename(img_path) path ET.SubElement(root, path) path.text img_path source ET.SubElement(root, source) database ET.SubElement(source, database) database.text Unknown size_el ET.SubElement(root, size) ET.SubElement(size_el, width).text str(size[0]) ET.SubElement(size_el, height).text str(size[1]) ET.SubElement(size_el, depth).text str(size[2]) ET.SubElement(root, segmented).text 0 for box, label in zip(boxes, labels): obj ET.SubElement(root, object) ET.SubElement(obj, name).text label ET.SubElement(obj, pose).text Unspecified ET.SubElement(obj, truncated).text 0 ET.SubElement(obj, difficult).text 0 bndbox ET.SubElement(obj, bndbox) # 轉(zhuǎn)成整數(shù)避免浮點(diǎn)坐標(biāo)寫(xiě)入XML ET.SubElement(bndbox, xmin).text str(int(round(box[0]))) ET.SubElement(bndbox, ymin).text str(int(round(box[1]))) ET.SubElement(bndbox, xmax).text str(int(round(box[2]))) ET.SubElement(bndbox, ymax).text str(int(round(box[3]))) tree ET.ElementTree(root) tree.write(xml_path, encodingutf-8, xml_declarationTrue) def build_pipeline(): 構(gòu)造增強(qiáng)管道。像素級(jí)增強(qiáng)幾何增強(qiáng)最后的統(tǒng)一尺寸處理 return A.Compose([ A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.6), A.HueSaturationValue(hue_shift_limit20, sat_shift_limit30, val_shift_limit20, p0.4), A.RandomGamma(gamma_limit(80, 120), p0.3), A.GaussNoise(var_limit(10.0, 40.0), p0.3), A.GaussianBlur(blur_limit(3, 5), p0.2), # 模擬遮擋讓模型學(xué)習(xí)缺了一部分的目標(biāo) A.CoarseDropout(max_holes6, max_height48, max_width48, fill_value0, p0.3), # 隨機(jī)裁剪并縮放能保留至少30%可見(jiàn)面積的框 A.RandomSizedBBoxSafeCrop(width640, height640, erosion_rate0.2, p0.5), A.HorizontalFlip(p0.5), # 只旋轉(zhuǎn)90/270整倍數(shù)避免產(chǎn)生斜框 A.Rotate(limit[90, 270], p0.2), A.RandomScale(scale_limit(-0.3, 0.3), p0.5), # 統(tǒng)一輸出短邊方便后續(xù)YOLO訓(xùn)練直接讀圖 A.LongestMaxSize(max_size1024), A.PadIfNeeded(min_height640, min_width640, border_mode0, value114), ], bbox_paramsA.BboxParams( formatpascal_voc, label_fields[labels], min_visibility0.3, min_area4, )) def process_one_xml(xml_path, output_dir, per_image): base_name os.path.splitext(os.path.basename(xml_path))[0] img_path os.path.join(IMAGE_DIR, base_name .jpg) if not os.path.exists(img_path): img_path os.path.join(IMAGE_DIR, base_name .png) image cv2.imread(img_path) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) h, w, d, boxes, labels parse_voc_xml(xml_path) # 保護(hù)原始圖片太小時(shí)先整體放大到訓(xùn)練尺寸附近 if min(h, w) TARGET_SHORT_SIDE: pre_resize A.Compose([ A.LongestMaxSize(max_sizeTARGET_SHORT_SIDE) ], bbox_paramsA.BboxParams(formatpascal_voc, label_fields[labels])) resized pre_resize(imageimage, bboxesboxes, labelslabels) image resized[image] boxes resized[bboxes] labels resized[labels] pipeline build_pipeline() generated 0 for idx in range(per_image): transformed pipeline(imageimage, bboxesboxes, labelslabels) aug_img transformed[image] aug_boxes transformed[bboxes] aug_labels transformed[labels] # 總會(huì)出現(xiàn)某些增強(qiáng)版本把目標(biāo)全裁掉的情況直接跳過(guò) if len(aug_boxes) 0: continue aug_img cv2.cvtColor(aug_img, cv2.COLOR_RGB2BGR) new_base f{base_name}_aug{idx:02d} out_img_path os.path.join(output_dir, new_base .jpg) out_xml_path os.path.join(output_dir, new_base .xml) cv2.imwrite(out_img_path, aug_img) out_h, out_w aug_img.shape[:2] write_voc_xml(out_img_path, out_xml_path, (out_w, out_h, 3), aug_boxes, aug_labels) generated 1 return generated def main(): os.makedirs(OUTPUT_DIR, exist_okTrue) xml_files glob.glob(os.path.join(INPUT_DIR, *.xml)) print(f找到 {len(xml_files)} 個(gè)標(biāo)注文件) total 0 for xml_file in xml_files: total process_one_xml(xml_file, OUTPUT_DIR, PER_IMAGE) print(f增強(qiáng)完成共生成 {total} 張圖片XML) if __name__ __main__: main()腳本里的A.LongestMaxSize(max_size1024)和A.PadIfNeeded是很多人容易忽略的組合。前者把圖片最長(zhǎng)邊限制到1024避免增強(qiáng)后的圖尺寸過(guò)大導(dǎo)致顯存爆炸后者把短邊補(bǔ)到640并填充灰色像素這樣訓(xùn)練時(shí)不會(huì)因?yàn)槌叽缣∮|發(fā)某些增強(qiáng)報(bào)錯(cuò)。填充值114是YOLO里常用的灰色實(shí)測(cè)很穩(wěn)。這里有一個(gè)實(shí)用經(jīng)驗(yàn)CoarseDropout的fill_value我寫(xiě)的是0也就是黑色塊。如果原始圖片整體偏淺色純黑塊會(huì)顯得很突兀。你可以把fill_value改成(114, 114, 114)或者用fill_valuerandom讓每個(gè)塊隨機(jī)填色這樣模擬遮擋時(shí)更接近真實(shí)場(chǎng)景。論文里如果對(duì)比了遮擋魯棒性實(shí)驗(yàn)這個(gè)參數(shù)會(huì)直接影響結(jié)果值得提前想好。4. 增強(qiáng)策略設(shè)計(jì)別讓模型吃同一盤(pán)菜4.1 哪些增強(qiáng)完全不動(dòng)坐標(biāo)很多初學(xué)者以為所有增強(qiáng)都要處理坐標(biāo)其實(shí)有一類(lèi)增強(qiáng)完全不涉及坐標(biāo)變化像素級(jí)變換。亮度、對(duì)比度、色相飽和度、伽馬校正、高斯噪聲、高斯模糊、CLAHE這些只改變像素值不改變像素位置所以標(biāo)注框原封不動(dòng)。這類(lèi)增強(qiáng)嵌入到管道里非常安全幾乎不會(huì)出bug建議優(yōu)先用足。我項(xiàng)目中常用的像素級(jí)增強(qiáng)就三個(gè)RandomBrightnessContrast模擬不同光照HueSaturationValue模擬不同色調(diào)環(huán)境GaussNoise模擬傳感器噪聲。這三個(gè)組合起來(lái)一張白天拍的圖能衍生出多種光照條件YOLO對(duì)這些變化很敏感多來(lái)幾張效果立竿見(jiàn)影。像素級(jí)增強(qiáng)的一個(gè)隱藏好處是計(jì)算開(kāi)銷(xiāo)小。跑增強(qiáng)腳本通常不需要GPUCPU幾分鐘能處理上千張圖。我之前幫某同學(xué)調(diào)增強(qiáng)一開(kāi)始他以為要等很久實(shí)際上幾百?gòu)垐D的像素級(jí)增強(qiáng)不到一分鐘就跑完了。4.2 需要重算坐標(biāo)的空間增強(qiáng)怎么搭配幾何變換類(lèi)增強(qiáng)是核心難點(diǎn)但也是提升泛化能力最猛的手段。常用組合有這么幾個(gè)思路。水平翻轉(zhuǎn)是最常見(jiàn)的對(duì)絕大多數(shù)物體都成立實(shí)現(xiàn)代價(jià)低。注意不是所有目標(biāo)都適合翻轉(zhuǎn)比如文本檢測(cè)、帶方向性的標(biāo)識(shí)物翻轉(zhuǎn)后語(yǔ)義就變了這類(lèi)場(chǎng)景要關(guān)掉HorizontalFlip。旋轉(zhuǎn)建議只做90度和270度整倍數(shù)。原因很實(shí)際VOC標(biāo)注的bndbox是軸對(duì)齊矩形不支持帶角度的傾斜框。如果旋轉(zhuǎn)一個(gè)任意角度比如37度目標(biāo)的實(shí)際區(qū)域變成傾斜的但XML里只能用垂直矩形去包住它結(jié)果是框里塞進(jìn)大量背景。模型拿著這種標(biāo)簽訓(xùn)練很容易把背景特征也學(xué)進(jìn)去不如不做。如果確實(shí)需要任意角度旋轉(zhuǎn)就得同時(shí)引入旋轉(zhuǎn)框標(biāo)注格式那屬于另一個(gè)課題了。隨機(jī)裁剪配合縮放也是很實(shí)用的組合。RandomSizedBBoxSafeCrop會(huì)隨機(jī)選一個(gè)區(qū)域裁剪后縮放到指定大小同時(shí)保證可見(jiàn)面積大于設(shè)定閾值的框才被保留。這個(gè)增強(qiáng)能模擬目標(biāo)出現(xiàn)在畫(huà)面不同位置、不同尺度的分布對(duì)提升小目標(biāo)檢測(cè)效果特別有幫助。4.3 隨機(jī)組合的概率與增強(qiáng)倍數(shù)控制管道里的每個(gè)增強(qiáng)都帶一個(gè)p概率這個(gè)參數(shù)值得認(rèn)真調(diào)而不是默認(rèn)0.5一把梭。如果一個(gè)管道里十個(gè)增強(qiáng)全部p等于0.5最終輸出的圖片可能同時(shí)疊加了七八種變換真實(shí)感會(huì)變得很差顏色像糖水片噪聲糊成一片。模型在這種異常分布上練出來(lái)遇到正常拍攝的測(cè)試圖反而容易失準(zhǔn)。我的經(jīng)驗(yàn)是像素級(jí)變換的p控制在0.3到0.6之間空間級(jí)變換的p控制在0.2到0.5之間遮擋類(lèi)控制在0.2到0.3。每一步的概率別太高讓增強(qiáng)結(jié)果偶爾發(fā)生而不是每張都發(fā)生這樣同一張?jiān)瓐D生成的幾個(gè)副本才有差異化多樣性才夠。增強(qiáng)倍數(shù)上我的建議是控制在原始數(shù)據(jù)的3到5倍。某同學(xué)原始280張每張生成5個(gè)版本得到1400張效果已經(jīng)很好。如果每張生成10倍甚至20倍你會(huì)得到大量?jī)?nèi)容高度相似的圖片模型反復(fù)看同一批增強(qiáng)副本照樣會(huì)過(guò)擬合而且訓(xùn)練時(shí)間成倍增加。邊際收益遞減非常明顯這個(gè)后面實(shí)測(cè)部分會(huì)看到數(shù)據(jù)。另外要留意類(lèi)別不均衡問(wèn)題。如果原始數(shù)據(jù)里A類(lèi)有200個(gè)框B類(lèi)只有50個(gè)框增強(qiáng)時(shí)要按類(lèi)別去看生成后的框數(shù)量而不是只看圖片數(shù)量。我一般會(huì)在增強(qiáng)后寫(xiě)個(gè)小統(tǒng)計(jì)腳本數(shù)一下每類(lèi)object出現(xiàn)的次數(shù)確保增強(qiáng)后各類(lèi)比例沒(méi)有進(jìn)一步失衡。如果某一類(lèi)在裁剪增強(qiáng)中被大量裁掉就要考慮給那類(lèi)樣本降低RandomSizedBBoxSafeCrop的觸發(fā)概率。5. 產(chǎn)出檢查與訓(xùn)練集成5.1 增強(qiáng)后必須做的三項(xiàng)質(zhì)檢腳本跑通只是第一步真正讓人放心的是增強(qiáng)之后的數(shù)據(jù)檢查。我見(jiàn)過(guò)太多同學(xué)增強(qiáng)完不做任何驗(yàn)證直接開(kāi)訓(xùn)練最后效果差了都不知道是增強(qiáng)的鍋還是模型的鍋。至少要做三項(xiàng)質(zhì)檢。第一項(xiàng)可視化抽查。寫(xiě)一個(gè)畫(huà)框腳本讀取增強(qiáng)后的圖片和XML把標(biāo)注框畫(huà)到圖上隨機(jī)抽20張肉眼檢查。重點(diǎn)看框是否還在目標(biāo)上、是否明顯偏大偏小、圖片顏色是否正常、有沒(méi)有目標(biāo)被裁掉但框還殘留的情況。這一步能過(guò)濾掉九成坐標(biāo)錯(cuò)位問(wèn)題。import cv2 import xml.etree.ElementTree as ET def draw_boxes(img_path, xml_path): img cv2.imread(img_path) tree ET.parse(xml_path) root tree.getroot() for obj in root.iter(object): label obj.find(name).text b obj.find(bndbox) xmin int(b.find(xmin).text) ymin int(b.find(ymin).text) xmax int(b.find(xmax).text) ymax int(b.find(ymax).text) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, label, (xmin, max(ymin - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) return img第二項(xiàng)規(guī)則校驗(yàn)。寫(xiě)一段腳本檢查每個(gè)XML里的框是否越界xmin 0、ymin 0、xmax width、ymax height、xmax xmin、ymax ymin。albumentations雖然會(huì)自動(dòng)過(guò)濾大部分越界框但像素級(jí)別的小越界或浮點(diǎn)取整誤差仍然可能出現(xiàn)校驗(yàn)一遍成本很低。第三項(xiàng)文件配對(duì)檢查。確保每張.jpg都有同名.xml反之亦然。訓(xùn)練腳本遍歷目錄時(shí)如果發(fā)現(xiàn)孤兒文件輕則警告重則直接中斷。這個(gè)小問(wèn)題在畢設(shè)項(xiàng)目里發(fā)生的頻率比想象中高。5.2 數(shù)據(jù)劃分與原圖泄漏問(wèn)題數(shù)據(jù)增強(qiáng)之后怎么劃分訓(xùn)練集和驗(yàn)證集是個(gè)比想象中更重要的細(xì)節(jié)。常見(jiàn)錯(cuò)誤是先把所有原始圖片和所有增強(qiáng)副本混在一起再按比例隨機(jī)劃分訓(xùn)練集、驗(yàn)證集。這會(huì)造成嚴(yán)重的信息泄漏同一張?jiān)瓐D生成的好幾個(gè)增強(qiáng)副本如果一部分在訓(xùn)練集、一部分在驗(yàn)證集模型等于提前見(jiàn)過(guò)驗(yàn)證集的內(nèi)容驗(yàn)證指標(biāo)會(huì)虛高很多。論文里寫(xiě)出來(lái)很難站住腳答辯時(shí)被問(wèn)到也不好解釋。正確的做法是先劃分原始數(shù)據(jù)集。比如原始280張按7比2比1分成訓(xùn)練、驗(yàn)證、測(cè)試然后只對(duì)訓(xùn)練集執(zhí)行增強(qiáng)驗(yàn)證集和測(cè)試集保持原始不動(dòng)。增強(qiáng)后訓(xùn)練樣本可能變成上千張但驗(yàn)證集還是那幾十張?jiān)紙D這樣評(píng)估結(jié)果才是可信的。實(shí)現(xiàn)層面我習(xí)慣給增強(qiáng)輸出加統(tǒng)一前綴比如aug_這樣os.listdir一眼能分清原始和增強(qiáng)。訓(xùn)練時(shí)YOLO內(nèi)部把增強(qiáng)副本放到訓(xùn)練數(shù)據(jù)目錄即可驗(yàn)證數(shù)據(jù)單獨(dú)放一個(gè)目錄不要混。6. 實(shí)測(cè)效果與調(diào)參建議6.1 某畢設(shè)項(xiàng)目的增強(qiáng)前后對(duì)比講一個(gè)我實(shí)際參與過(guò)的模擬項(xiàng)目課題目的是檢測(cè)某工廠(chǎng)場(chǎng)景下的兩類(lèi)目標(biāo)原始數(shù)據(jù)集280張單類(lèi)占比大概7比3。用YOLOv5s作為基準(zhǔn)圖片尺寸640x640訓(xùn)練100輪結(jié)果如下。實(shí)驗(yàn)配置訓(xùn)練樣本量mAP0.5備注原始數(shù)據(jù)集2800.47小類(lèi)目標(biāo)檢出率很低原始像素級(jí)增強(qiáng)11200.55光照變化帶來(lái)的提升原始像素級(jí)幾何增強(qiáng)14000.62翻轉(zhuǎn)、裁剪、縮放起主要作用原始全套增強(qiáng)10倍28000.63相對(duì)5倍提升不到兩個(gè)點(diǎn)可以看到從0.47到0.62這個(gè)跨越很關(guān)鍵論文里足夠作為核心對(duì)比出現(xiàn)但從0.62到0.63邊際收益幾乎消失訓(xùn)練時(shí)間翻倍卻換不來(lái)明顯回報(bào)。這就是我之前反復(fù)強(qiáng)調(diào)增強(qiáng)倍數(shù)別貪多的原因。這個(gè)數(shù)字不一定對(duì)你完全適用不同的目標(biāo)、背景復(fù)雜度下提升幅度差異很大但先加基礎(chǔ)增強(qiáng)效果好再加倍數(shù)、加復(fù)雜度這個(gè)思路是通用的。6.2 增強(qiáng)后YOLO訓(xùn)練參數(shù)怎么跟進(jìn)數(shù)據(jù)量變了訓(xùn)練參數(shù)不能原封不動(dòng)地繼續(xù)用。第一個(gè)要調(diào)整的是epoch數(shù)量。增強(qiáng)后每個(gè)epoch看到的樣本多了,如果epoch數(shù)不變相當(dāng)于訓(xùn)練總步數(shù)成倍增加有過(guò)擬合風(fēng)險(xiǎn)。我通常的做法是總epoch適當(dāng)減半比如原來(lái)100輪現(xiàn)在60到80輪然后開(kāi)啟早停盯驗(yàn)證集mAP走勢(shì)。第二個(gè)要注意的是在線(xiàn)增強(qiáng)強(qiáng)度。YOLOv5、YOLOv8自帶的在線(xiàn)增強(qiáng)mosaic、mixup、HSV擾動(dòng)、隨機(jī)仿射在這時(shí)候可以適當(dāng)調(diào)低尤其是mosaic。因?yàn)殡x線(xiàn)增強(qiáng)已經(jīng)產(chǎn)生了大量空間上的多樣性在線(xiàn)再來(lái)一次過(guò)強(qiáng)的mosaic容易讓模型在變形過(guò)的樣本上反復(fù)折騰。我一般把hsv_h、hsv_s、hsv_v調(diào)低到默認(rèn)的一半degrees保持0flipud關(guān)閉進(jìn)一步降低訓(xùn)練集和真實(shí)場(chǎng)景的分布差距。第三個(gè)是batch size。顯存沒(méi)變的情況下增強(qiáng)后圖片尺寸如果不是完全統(tǒng)一我的腳本允許一定范圍的尺寸變化batch size要保守一點(diǎn)。實(shí)際跑的時(shí)候盯著顯存占用溢出就回調(diào)不要盲目拉滿(mǎn)。6.3 進(jìn)階方向小目標(biāo)增強(qiáng)和分階段擴(kuò)圖如果做完基礎(chǔ)增強(qiáng)后發(fā)現(xiàn)小目標(biāo)檢測(cè)效果還是差可以加一個(gè)針對(duì)性增強(qiáng)思路對(duì)小目標(biāo)區(qū)域的裁剪放大。做法是遍歷XML篩選出框?qū)捀咻^小的目標(biāo)把目標(biāo)周?chē)鷧^(qū)域裁剪下來(lái)放大到640x640再和原圖一起加入訓(xùn)練集。這等于讓模型多看了幾遍小目標(biāo)特寫(xiě)能明顯改善小目標(biāo)漏檢。另一個(gè)思路是分兩階段擴(kuò)圖先只做像素級(jí)增強(qiáng)訓(xùn)一個(gè)基線(xiàn)觀察失敗樣本集中在哪些場(chǎng)景再有針對(duì)性地加幾何增強(qiáng)而不是一開(kāi)始就把所有變換全部鋪上去。我?guī)湍惩瑢W(xué)調(diào)試時(shí)就是這么一步步來(lái)的先發(fā)現(xiàn)夜間樣幾乎全靠像素級(jí)增強(qiáng)在撐之后又發(fā)現(xiàn)側(cè)視角度目標(biāo)不好檢于是給Rotate的p提高了一點(diǎn)。整個(gè)過(guò)程有依據(jù)效果也可解釋寫(xiě)完實(shí)驗(yàn)記錄后論文的數(shù)據(jù)增強(qiáng)策略部分素材非常充分。再分享一個(gè)細(xì)節(jié)增強(qiáng)腳本跑完之后把生成的增強(qiáng)圖按類(lèi)別文件數(shù)、標(biāo)注框數(shù)、每張圖目標(biāo)數(shù)量都統(tǒng)計(jì)一遍存成一個(gè)statistics.csv。論文里寫(xiě)數(shù)據(jù)預(yù)處理的表格時(shí)直接引用這個(gè)表比臨時(shí)數(shù)數(shù)靠譜得多。最后說(shuō)一點(diǎn)個(gè)人習(xí)慣。每次配好增強(qiáng)管道我不會(huì)直接全量跑幾百?gòu)埗窍忍?到5張有代表性的原圖跑一遍打開(kāi)生成的圖檢查十幾秒確認(rèn)框?qū)R、顏色正常、沒(méi)有全黑塊之后再放開(kāi)全量跑。整個(gè)檢查過(guò)程不超過(guò)五分鐘卻能避免一次錯(cuò)誤的批量生成。數(shù)據(jù)增強(qiáng)不是把圖變多就完事了它決定了模型見(jiàn)過(guò)什么樣的世界。你給YOLO看的訓(xùn)練數(shù)據(jù)越接近它將來(lái)要面對(duì)的真實(shí)畫(huà)面它給你的結(jié)果就越穩(wěn)定。帶著這個(gè)認(rèn)知去做增強(qiáng)比多開(kāi)幾個(gè)腳本參數(shù)重要得多。