習(xí)訓(xùn)練數(shù)據(jù)集加載避坑指南:從解壓到DataLoader的完整流程)
簡介這份深度學(xué)習(xí)訓(xùn)練數(shù)據(jù)集面向圖像分類與目標(biāo)檢測方向的開發(fā)者、學(xué)生及研究者提供可直接用于模型訓(xùn)練與驗證的標(biāo)注圖像資源幫助解決入門與實驗階段缺少真實樣本的問題。壓縮包共305個文件以300張jpg圖像為主體另含2個mat數(shù)據(jù)文件、2個m腳本和1個txt說明文件整體約22.97MB體積輕便便于快速下載與本地加載。圖像內(nèi)容圍繞水質(zhì)軌跡展開涵蓋清澈、渾濁、含藻類等不同狀態(tài)可用于卷積神經(jīng)網(wǎng)絡(luò)的特征學(xué)習(xí)也可支撐Fast R-CNN、YOLO等目標(biāo)檢測算法的邊界框訓(xùn)練。目前已有693人學(xué)習(xí)下載適合作為課程實驗、畢業(yè)設(shè)計或算法對比的練手?jǐn)?shù)據(jù)。使用者可據(jù)此完成訓(xùn)練集、驗證集與測試集劃分并結(jié)合翻轉(zhuǎn)、旋轉(zhuǎn)、裁剪等數(shù)據(jù)增強手段擴充樣本進(jìn)而搭建從圖像分類到目標(biāo)定位的完整流程在環(huán)境保護(hù)與水質(zhì)監(jiān)測等場景中驗證模型效果。1. 拿到「深度學(xué)習(xí)訓(xùn)練數(shù)據(jù)集.zip」先別急著解壓三類數(shù)據(jù)集的血淚分診法你從群里、網(wǎng)盤或者某個競賽頁面拖下來一個深度學(xué)習(xí)訓(xùn)練數(shù)據(jù)集.zip雙擊解壓看到一堆文件夾和圖片心里一陣激動然后torchvision.datasets.ImageFolder一掛訓(xùn)練 loss 不降反升或者干脆報一堆找不到文件的錯。這個場景我經(jīng)歷過太多次。問題幾乎從來不在模型而在你根本沒搞清楚這個壓縮包里裝的到底是哪一類數(shù)據(jù)集。深度學(xué)習(xí)訓(xùn)練數(shù)據(jù)集大致分三種分類數(shù)據(jù)集按類別分文件夾、檢測/分割數(shù)據(jù)集圖片和標(biāo)注文件分離靠 json/xml/txt 索引、自定義格式數(shù)據(jù)集csv 索引、npy 打包、tfrecord 等。三類數(shù)據(jù)的加載方式、目錄結(jié)構(gòu)、校驗手段完全不同。這篇筆記就按「解壓后先分診 → 按類型加載 → 避坑 → 進(jìn)階驗證」的順序把深度學(xué)習(xí)訓(xùn)練數(shù)據(jù)集.zip從一堆文件變成能喂進(jìn)模型的 DataLoader適合剛拿到數(shù)據(jù)集準(zhǔn)備跑 baseline 的新手也適合被臟數(shù)據(jù)坑過的熟手。2. 解壓后第一件事用三條命令給數(shù)據(jù)集做「體檢」2.1 先看目錄樹判斷屬于哪一類不要一上來就寫 Dataset 類。先花兩分鐘把結(jié)構(gòu)看清楚這一步能省掉后面幾小時的 debug。# 只看兩層目錄避免幾萬張圖刷屏 find ./dataset -maxdepth 2 -type d | head -50 # 統(tǒng)計各目錄下的文件數(shù)量快速判斷是不是按類別分文件夾 for d in ./dataset/*/; do echo -n $d: ; find $d -type f | wc -l; done # 看文件擴展名分布判斷是純圖片還是混了標(biāo)注文件 find ./dataset -type f | sed s/.*\.// | sort | uniq -c | sort -rn第一條命令輸出的是目錄骨架。如果看到train/cat/、train/dog/這種「父目錄是劃分、子目錄是類別」的結(jié)構(gòu)基本就是 ImageFolder 能直接吃的分類數(shù)據(jù)集。第二條命令統(tǒng)計每個子目錄的文件數(shù)如果各類別數(shù)量差異巨大比如一類 5000 張、一類 200 張先別訓(xùn)練這是典型的類別不平衡后面要處理。第三條命令看擴展名分布如果出現(xiàn)大量.xml、.json、.txt說明這是檢測或分割數(shù)據(jù)集圖片和標(biāo)注是分開的不能直接 ImageFolder。提示解壓前先看壓縮包大小和文件數(shù)。unzip -l xxx.zip | tail -5能在解壓前看到總文件數(shù)避免解壓出幾十萬個碎文件把 inode 占滿。2.2 用 Python 做一次完整性校驗?zāi)夸浗Y(jié)構(gòu)對了不代表文件沒問題。壓縮包傳輸過程中損壞、圖片截斷、標(biāo)注和圖片對不上都是常見情況。下面這段腳本做三件事驗證圖片能否被正常解碼、檢查圖片尺寸分布、統(tǒng)計損壞文件。import os from PIL import Image from collections import Counter root ./dataset bad_files [] sizes Counter() exts {.jpg, .jpeg, .png, .bmp, .webp} for dirpath, _, filenames in os.walk(root): for fn in filenames: if os.path.splitext(fn)[1].lower() not in exts: continue fp os.path.join(dirpath, fn) try: # verify() 只讀文件頭比 load() 快很多適合大批量篩查 img Image.open(fp) img.verify() # verify 后對象不可再用重新打開拿尺寸 img Image.open(fp) sizes[img.size] 1 except Exception as e: bad_files.append((fp, str(e))) print(f損壞文件數(shù): {len(bad_files)}) for fp, err in bad_files[:10]: print(f {fp} - {err}) print(尺寸分布 Top10:) for size, cnt in sizes.most_common(10): print(f {size}: {cnt})verify()是關(guān)鍵它只解析文件頭不做完整解碼幾萬張圖幾十秒就能跑完。如果你直接用img.load()遇到損壞文件會拋異常而且速度慢一個數(shù)量級。尺寸分布這一欄很重要如果出現(xiàn)大量(1,1)或者極端長寬比說明有占位圖或爬蟲抓下來的垃圾圖訓(xùn)練前必須清掉。損壞文件不要直接刪先移到./quarantine/目錄留檔萬一后面發(fā)現(xiàn)是某個類別整體損壞你還能回溯。2.3 判斷是否需要重新劃分 train/val很多網(wǎng)上下載的數(shù)據(jù)集只給了一個大文件夾沒有 train/val 劃分。這時候不要用random_split隨便切因為如果數(shù)據(jù)里有同一視頻的連續(xù)幀、同一用戶的多次拍攝隨機切會導(dǎo)致訓(xùn)練集和驗證集泄漏驗證指標(biāo)虛高。正確做法是按「實體」劃分同一來源的樣本必須整體進(jìn) train 或整體進(jìn) val。import os, random, shutil from collections import defaultdict src ./dataset/all dst ./dataset/split random.seed(42) # 假設(shè)文件名前綴是實體 ID如 user001_0001.jpg groups defaultdict(list) for fn in os.listdir(src): entity fn.split(_)[0] groups[entity].append(fn) entities list(groups.keys()) random.shuffle(entities) n_val max(1, int(len(entities) * 0.2)) val_entities set(entities[:n_val]) for split in [train, val]: os.makedirs(os.path.join(dst, split), exist_okTrue) for entity, files in groups.items(): split val if entity in val_entities else train for fn in files: shutil.copy(os.path.join(src, fn), os.path.join(dst, split, fn))這里random.seed(42)是為了可復(fù)現(xiàn)團(tuán)隊協(xié)作時大家切出來的劃分必須一致否則對比實驗沒有意義。按實體劃分而不是按文件劃分是避免數(shù)據(jù)泄漏的核心。如果你的文件名沒有實體前綴那就退而求其次按時間戳或來源目錄劃分總之不要讓同一來源的樣本跨集合。3. 分類數(shù)據(jù)集ImageFolder 能直接用但這三個參數(shù)必須改3.1 目錄結(jié)構(gòu)不滿足要求時怎么補救ImageFolder 要求的標(biāo)準(zhǔn)結(jié)構(gòu)是root/class_name/xxx.jpg。但實際拿到的數(shù)據(jù)集經(jīng)常是root/train/class_name/或者root/class_name/subclass/。前者多了一層劃分目錄后者多了一層子類。補救方式有兩種寫一個輕量 Dataset 包裝或者用軟鏈接重組目錄。我一般優(yōu)先用軟鏈接因為不復(fù)制文件、不占額外磁盤。mkdir -p ./reorg/train ./reorg/val # 把 train 下的類別目錄軟鏈過去 for cls in ./dataset/train/*/; do name$(basename $cls) ln -s $(realpath $cls) ./reorg/train/$name done軟鏈接的好處是原始數(shù)據(jù)不動重組結(jié)構(gòu)隨時可改。注意realpath要寫絕對路徑否則軟鏈接會指向錯誤位置。Windows 下軟鏈接需要管理員權(quán)限這種情況就老老實實寫 Dataset 類在__getitem__里手動拼路徑。3.2 transform 里最容易翻車的三個參數(shù)分類任務(wù)的 transform 看起來簡單但下面三個參數(shù)設(shè)錯模型精度能差好幾個點。from torchvision import transforms train_tf transforms.Compose([ transforms.Resize(256), # 短邊縮到 256保持長寬比 transforms.RandomCrop(224), # 隨機裁 224做數(shù)據(jù)增強 transforms.RandomHorizontalFlip(0.5), transforms.ColorJitter(0.2, 0.2, 0.2), # 亮度/對比度/飽和度抖動 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), # 驗證集用中心裁剪不用隨機 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])第一個坑Resize直接寫(224, 224)會強行拉伸改變長寬比對細(xì)長物體識別影響很大。正確做法是先縮短邊再裁剪。第二個坑驗證集用了RandomCrop或RandomHorizontalFlip導(dǎo)致每次驗證結(jié)果都在抖你根本不知道模型是真的提升了還是隨機性。驗證集 transform 必須確定性。第三個坑Normalize的 mean/std 用了 ImageNet 的默認(rèn)值但你的數(shù)據(jù)集如果是醫(yī)學(xué)影像、衛(wèi)星圖、工業(yè)質(zhì)檢圖像素分布和自然圖像差很遠(yuǎn)應(yīng)該自己統(tǒng)計。# 統(tǒng)計自己數(shù)據(jù)集的 mean/std import torch from torch.utils.data import DataLoader loader DataLoader(train_dataset, batch_size64, shuffleFalse, num_workers4) mean torch.zeros(3) std torch.zeros(3) n 0 for imgs, _ in loader: b imgs.size(0) imgs imgs.view(b, 3, -1) mean imgs.mean(2).sum(0) std imgs.std(2).sum(0) n b print(mean:, mean / n) print(std:, std / n)這段統(tǒng)計腳本跑一遍也就幾分鐘但能讓你的歸一化真正匹配數(shù)據(jù)分布。工業(yè)質(zhì)檢和醫(yī)學(xué)影像上用錯 mean/std 導(dǎo)致收斂慢甚至不收斂的情況我見過不止一次。3.3 類別不平衡時 sampler 怎么選前面體檢發(fā)現(xiàn)類別數(shù)量差異大直接訓(xùn)練會讓模型偏向多數(shù)類。常見做法有三種重采樣、加權(quán)損失、focal loss。我一般先用WeightedRandomSampler改動最小。import numpy as np from torch.utils.data import WeightedRandomSampler targets [s[1] for s in train_dataset.samples] class_count np.bincount(targets) class_weight 1.0 / class_count sample_weight class_weight[targets] sampler WeightedRandomSampler( weightssample_weight, num_sampleslen(sample_weight), replacementTrue ) loader DataLoader(train_dataset, batch_size32, samplersampler, num_workers4)replacementTrue表示有放回采樣少數(shù)類會被反復(fù)抽到。num_samples設(shè)成和數(shù)據(jù)集一樣大保證每個 epoch 的步數(shù)和原來一致。注意用了 sampler 就不能再設(shè)shuffleTrue兩者沖突會報錯。如果少數(shù)類樣本實在太少比如只有幾十張重采樣會導(dǎo)致嚴(yán)重過擬合這時候應(yīng)該上數(shù)據(jù)增強或者直接換 focal loss。4. 檢測與分割數(shù)據(jù)集標(biāo)注文件和圖片對不上是頭號殺手4.1 COCO 格式的解析與校驗檢測數(shù)據(jù)集最常見的是 COCO 格式一個annotations.json加一個images/目錄。加載前必須校驗三件事json 里引用的圖片是否都存在、每張圖的標(biāo)注框是否越界、類別 id 是否連續(xù)。import json, os from PIL import Image with open(./dataset/annotations.json) as f: coco json.load(f) img_dir ./dataset/images id2file {im[id]: im[file_name] for im in coco[images]} missing [] for im in coco[images]: if not os.path.exists(os.path.join(img_dir, im[file_name])): missing.append(im[file_name]) print(f缺失圖片: {len(missing)}) # 校驗標(biāo)注框 bad_boxes 0 for ann in coco[annotations]: x, y, w, h ann[bbox] im_info next(i for i in coco[images] if i[id] ann[image_id]) if x 0 or y 0 or x w im_info[width] or y h im_info[height]: bad_boxes 1 print(f越界標(biāo)注框: {bad_boxes}) # 類別 id 是否連續(xù) cat_ids sorted(c[id] for c in coco[categories]) print(類別 id:, cat_ids)越界框在訓(xùn)練時會導(dǎo)致 loss 計算異常或者 ROI Align 報錯。處理方式是把框裁剪到圖像邊界內(nèi)如果裁剪后面積小于閾值比如 4 像素就直接丟棄這條標(biāo)注。類別 id 不連續(xù)是個隱蔽的坑COCO 的 id 可能從 1 開始也可能從 0 開始還可能跳號如果你直接拿 id 當(dāng)類別索引會出現(xiàn)索引越界或者類別錯位。正確做法是建一個id - 連續(xù)索引的映射表。4.2 從 COCO 轉(zhuǎn) YOLO 格式的腳本與邊界坑YOLO 格式要求每張圖對應(yīng)一個同名.txt每行是class_id cx cy w h全部歸一化到 0-1。轉(zhuǎn)換腳本本身不難坑在邊界處理。import json, os with open(./dataset/annotations.json) as f: coco json.load(f) cat_map {c[id]: i for i, c in enumerate(sorted(coco[categories], keylambda x: x[id]))} img_info {im[id]: im for im in coco[images]} out_dir ./dataset/labels os.makedirs(out_dir, exist_okTrue) for ann in coco[annotations]: im img_info[ann[image_id]] W, H im[width], im[height] x, y, w, h ann[bbox] # 裁剪到邊界內(nèi) x1, y1 max(0, x), max(0, y) x2, y2 min(W, x w), min(H, y h) if x2 - x1 2 or y2 - y1 2: continue # 太小丟棄 cx (x1 x2) / 2 / W cy (y1 y2) / 2 / H nw (x2 - x1) / W nh (y2 - y1) / H # 歸一化后必須再 clamp 一次浮點誤差可能讓值略超 1 cx, cy min(max(cx, 0), 1), min(max(cy, 0), 1) nw, nh min(max(nw, 0), 1), min(max(nh, 0), 1) name os.path.splitext(im[file_name])[0] .txt with open(os.path.join(out_dir, name), a) as f: f.write(f{cat_map[ann[category_id]]} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n)三個邊界坑第一COCO 的 bbox 是[x, y, w, h]YOLO 要的是中心點加寬高轉(zhuǎn)換公式別寫反。第二裁剪后要再 clamp 一次因為浮點運算可能讓歸一化值變成 1.0000001某些框架會直接報錯。第三用append模式寫文件因為一張圖可能有多個目標(biāo)每次覆蓋會丟標(biāo)注。轉(zhuǎn)換完記得抽查幾張圖用可視化腳本把框畫出來看一眼比任何單元測試都直觀。4.3 分割掩碼的類別對齊問題分割數(shù)據(jù)集常見的是 PNG 掩碼每個像素值代表類別。坑在于掩碼里的像素值可能是 0/1/2也可能是 0/255還可能是調(diào)色板索引。訓(xùn)練前必須確認(rèn)像素值和類別 id 的對應(yīng)關(guān)系。import numpy as np from PIL import Image mask np.array(Image.open(./dataset/masks/001.png)) print(唯一值:, np.unique(mask)) print(dtype:, mask.dtype)如果唯一值是[0, 255]說明是二值掩碼訓(xùn)練時除以 255 即可。如果是[0, 1, 2, 3]直接當(dāng)類別索引。如果出現(xiàn)[0, 128, 255]這種多半是調(diào)色板圖需要用Image.open(...).convert(P)后讀 palette或者直接找數(shù)據(jù)集說明文檔確認(rèn)映射關(guān)系。掩碼和原圖尺寸必須嚴(yán)格一致不一致的話用最近鄰插值縮放千萬別用雙線性否則會插出中間類別值把類別搞亂。5. 避坑與排查數(shù)據(jù)集加載階段最常見的五個翻車現(xiàn)場5.1 現(xiàn)象訓(xùn)練第一個 epoch 就 OOM但顯存明明夠原因通常不是模型太大而是 DataLoader 的num_workers設(shè)太高每個 worker 都復(fù)制了一份數(shù)據(jù)集元數(shù)據(jù)加上pin_memoryTrue和過大的batch_size顯存和內(nèi)存一起爆。解決先把num_workers降到 2batch_size減半確認(rèn)能跑通后再逐步往上加。另外檢查 Dataset 的__init__里是不是把整個數(shù)據(jù)集讀進(jìn)了內(nèi)存比如self.images [Image.open(p) for p in paths]這種寫法在幾萬張圖時必炸應(yīng)該只存路徑在__getitem__里按需讀取。5.2 現(xiàn)象loss 一直是 nan換小學(xué)習(xí)率也沒用先別懷疑模型去檢查數(shù)據(jù)里有沒有 NaN 或者全黑圖。全黑圖經(jīng)過 Normalize 后可能產(chǎn)生極端值全白圖同理。用前面的體檢腳本篩一遍把像素均值接近 0 或 255 的圖找出來。另一個常見原因是標(biāo)注里有面積為 0 的框?qū)е鲁恪=鉀Q在 Dataset 的__getitem__里加斷言發(fā)現(xiàn)異常樣本直接跳過并記錄不要讓臟數(shù)據(jù)進(jìn)網(wǎng)絡(luò)。5.3 現(xiàn)象驗證集準(zhǔn)確率比訓(xùn)練集還高這幾乎可以斷定是數(shù)據(jù)泄漏。檢查 train 和 val 里有沒有重復(fù)圖片尤其是同一張圖的不同增強版本被分到了兩個集合。用圖片的 md5 去重是最快的手段。find ./dataset -type f -name *.jpg -exec md5sum {} \; | sort | awk {print $1} | uniq -d如果有重復(fù) md5說明同一張圖出現(xiàn)了多次。按實體劃分那一步如果沒做好就會出這個問題。另外如果驗證集太小比如只有幾十張指標(biāo)波動大也會造成這種假象建議驗證集至少占總量的 10% 且每類不少于 30 張。5.4 現(xiàn)象Windows 上 num_workers 大于 0 就報錯這是 Windows 多進(jìn)程的經(jīng)典問題DataLoader 的 worker 在 Windows 下需要if __name__ __main__:保護(hù)否則會無限遞歸創(chuàng)建進(jìn)程。解決把訓(xùn)練入口包在 main 保護(hù)里或者干脆在 Windows 上設(shè)num_workers0調(diào)試最終訓(xùn)練放到 Linux 上跑。這不是代碼 bug是平臺差異別在這上面浪費時間。5.5 現(xiàn)象換了臺機器同樣的代碼加載數(shù)據(jù)集報路徑錯誤絕對路徑寫死在代碼里是萬惡之源。所有路徑都應(yīng)該基于一個DATA_ROOT環(huán)境變量或者配置文件用os.path.join拼接。另外注意 Linux 和 Windows 的路徑分隔符差異以及大小寫敏感問題Linux 下Cat和cat是兩個目錄Windows 下是一個。跨平臺協(xié)作時目錄名統(tǒng)一用小寫加下劃線能省掉很多玄學(xué)問題。6. 讓數(shù)據(jù)集可復(fù)現(xiàn)寫一個 dataset_card 和固定隨機種子數(shù)據(jù)集處理完別急著開訓(xùn)。我現(xiàn)在的習(xí)慣是給每個數(shù)據(jù)集寫一份dataset_card.md記錄來源、樣本總數(shù)、類別分布、劃分方式、已知問題。這份卡片在團(tuán)隊協(xié)作和三個月后自己回頭看時價值極高。配合固定隨機種子才能保證實驗可復(fù)現(xiàn)。import random, numpy as np, torch, os def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 下面兩行會讓訓(xùn)練變慢但換來確定性 torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False os.environ[PYTHONHASHSEED] str(seed) set_seed(42)cudnn.deterministicTrue會讓卷積算法選擇確定性版本速度可能慢 10%-20%但同樣的種子能跑出同樣的結(jié)果。如果追求速度且能接受微小隨機性把這兩行注釋掉即可。DataLoader 的worker_init_fn也要設(shè)種子否則每個 worker 的增強隨機性不受控。def worker_init_fn(worker_id): np.random.seed(42 worker_id) random.seed(42 worker_id) loader DataLoader(dataset, batch_size32, num_workers4, worker_init_fnworker_init_fn)最后分享一個我踩過的坑有次處理一個醫(yī)學(xué)影像數(shù)據(jù)集體檢時圖片全部正常訓(xùn)練卻一直不收斂。查了兩天才發(fā)現(xiàn)標(biāo)注文件里的類別名和文件夾名差了一個下劃線導(dǎo)致標(biāo)簽全部錯位。從那以后我拿到任何深度學(xué)習(xí)訓(xùn)練數(shù)據(jù)集.zip第一件事永遠(yuǎn)是打印前 20 個樣本的「路徑 標(biāo)簽 圖片尺寸」三元組肉眼掃一遍。這個習(xí)慣幫我攔下了至少五次類似事故。數(shù)據(jù)集這層地基沒打好上面模型再花哨都是空中樓閣。希望幫到你。本文還有配套的精品資源點擊獲取