戰(zhàn):數(shù)據(jù)集構(gòu)建、Dataset編寫與訓(xùn)練調(diào)優(yōu))
1. 從“輸入和標(biāo)簽都是圖片”說起去雨任務(wù)的數(shù)據(jù)集到底特殊在哪圖像去雨這個(gè)方向很多人第一次接觸時(shí)都會(huì)卡在一個(gè)很樸素的問題上數(shù)據(jù)集到底長(zhǎng)什么樣如果你之前做過分類任務(wù)習(xí)慣的是“一張圖 一個(gè)類別標(biāo)簽”做過分割任務(wù)習(xí)慣的是“一張圖 一張掩碼圖”。而去雨任務(wù)屬于圖像到圖像的映射輸入是一張帶雨痕的圖標(biāo)簽是同一場(chǎng)景下沒有雨的那張干凈圖。聽起來簡(jiǎn)單但真正動(dòng)手搭 Dataset 的時(shí)候坑遠(yuǎn)比想象中多。我最初拿到一個(gè)去雨數(shù)據(jù)集時(shí)第一反應(yīng)是“不就是兩個(gè)文件夾一個(gè)放帶雨圖一個(gè)放干凈圖按文件名配對(duì)嘛”。結(jié)果打開目錄一看帶雨圖有 1200 張干凈圖只有 400 張文件名還完全對(duì)不上。這就是去雨數(shù)據(jù)集最典型的特征之一它不是嚴(yán)格的一一對(duì)應(yīng)關(guān)系而是“一張干凈圖對(duì)應(yīng)多張不同雨痕的合成圖”。這個(gè)結(jié)構(gòu)直接決定了你在寫 PyTorch 的Dataset類時(shí)配對(duì)邏輯不能想當(dāng)然。為什么會(huì)出現(xiàn)這種結(jié)構(gòu)因?yàn)檎鎸?shí)世界里“同一場(chǎng)景、同一角度、同一光照只有雨不一樣”的成對(duì)數(shù)據(jù)幾乎拍不到。雨是動(dòng)態(tài)的光線在變相機(jī)稍微抖一下場(chǎng)景就變了。所以主流做法是先采集干凈背景圖然后用物理渲染或者圖層疊加的方式人工合成不同密度、不同方向、不同雨滴大小的雨痕生成多張帶雨圖。這樣一來一張干凈圖就派生出了好幾張訓(xùn)練樣本。理解這一點(diǎn)非常關(guān)鍵因?yàn)樗绊懭碌谝荒愕臄?shù)據(jù)配對(duì)邏輯必須支持“一對(duì)多”第二劃分訓(xùn)練集和驗(yàn)證集時(shí)必須按“干凈圖”來劃分而不是按“帶雨圖”劃分否則同一張干凈圖派生出的帶雨圖會(huì)同時(shí)出現(xiàn)在訓(xùn)練和驗(yàn)證里造成數(shù)據(jù)泄漏第三評(píng)估指標(biāo)的可信度依賴于驗(yàn)證集里干凈圖的多樣性而不是帶雨圖的數(shù)量。我見過不少新手在這里翻車驗(yàn)證集 loss 低得離譜以為模型學(xué)得很好結(jié)果一放到真實(shí)雨天照片上就完全失效。排查半天才發(fā)現(xiàn)訓(xùn)練集和驗(yàn)證集里存在同一場(chǎng)景的不同雨痕版本模型其實(shí)是在“背場(chǎng)景”而不是在“學(xué)去雨”。這個(gè)坑非常隱蔽因?yàn)閺膸в陥D的文件名上根本看不出來它們共享同一張干凈背景。所以這一節(jié)我想先把認(rèn)知層面的事情講透去雨數(shù)據(jù)集的核心不是“圖片數(shù)量”而是“場(chǎng)景數(shù)量”和“雨痕多樣性”的平衡。你后面寫 Dataset、做增強(qiáng)、劃分?jǐn)?shù)據(jù)集所有決策都要圍繞這個(gè)認(rèn)知展開。下面我會(huì)從目錄結(jié)構(gòu)設(shè)計(jì)、配對(duì)邏輯、數(shù)據(jù)劃分三個(gè)角度把這件事拆開講。1.1 常見的兩種目錄組織方式與各自的坑去雨數(shù)據(jù)集在磁盤上的組織方式我見過的主流有兩種。第一種是雙文件夾平行結(jié)構(gòu)dataset/ rainy/ scene01_rain_a.png scene01_rain_b.png scene02_rain_a.png ... clean/ scene01.png scene02.png ...這種結(jié)構(gòu)的特點(diǎn)是干凈圖只有一份帶雨圖有多份通過文件名前綴比如scene01來關(guān)聯(lián)。寫 Dataset 時(shí)你需要從帶雨圖文件名里解析出場(chǎng)景 ID再去 clean 目錄里找對(duì)應(yīng)的干凈圖。優(yōu)點(diǎn)是磁盤占用小干凈圖不重復(fù)存儲(chǔ)缺點(diǎn)是配對(duì)邏輯依賴命名規(guī)范一旦命名不統(tǒng)一就會(huì)出錯(cuò)。第二種是成對(duì)文件夾結(jié)構(gòu)dataset/ train/ rainy/ scene01_rain_a.png scene01_rain_b.png clean/ scene01_rain_a.png scene01_rain_b.png注意這里 clean 目錄里其實(shí)是同一張圖復(fù)制了兩份只是文件名和帶雨圖一一對(duì)應(yīng)。這種結(jié)構(gòu)寫 Dataset 最簡(jiǎn)單直接按文件名配對(duì)即可但磁盤占用會(huì)成倍增加。如果你的干凈圖是 2K 分辨率、幾千張復(fù)制一份可能就多占幾個(gè) G。我個(gè)人的選擇是訓(xùn)練階段用第一種配合內(nèi)存緩存如果磁盤實(shí)在緊張第二種只在小規(guī)模調(diào)試時(shí)用。原因很簡(jiǎn)單去雨模型的訓(xùn)練輪次通常很多磁盤 IO 會(huì)成為瓶頸與其復(fù)制文件不如在 Dataset 里做一次映射緩存。這里有個(gè)細(xì)節(jié)要注意解析場(chǎng)景 ID 時(shí)不要用簡(jiǎn)單的字符串切割。比如scene01_rain_a.png和scene01_rain_b.png如果你用split(_)[0]拿到scene01看起來沒問題但如果有的數(shù)據(jù)集命名是scene_01_rain.png切割結(jié)果就錯(cuò)了。穩(wěn)妥的做法是用正則表達(dá)式提取“場(chǎng)景標(biāo)識(shí) 雨痕標(biāo)識(shí)”兩部分并且把映射關(guān)系在初始化時(shí)一次性建好存成一個(gè)字典避免每個(gè)__getitem__都去遍歷目錄。1.2 數(shù)據(jù)劃分為什么必須按干凈圖切分前面提到按干凈圖劃分這里展開說清楚操作方式。假設(shè)你有 400 張干凈圖每張派生出 3 張帶雨圖總共 1200 張帶雨圖。正確的劃分方式是先把 400 張干凈圖按 8:1:1 切成訓(xùn)練 320、驗(yàn)證 40、測(cè)試 40然后每張干凈圖對(duì)應(yīng)的 3 張帶雨圖跟著它一起進(jìn)對(duì)應(yīng)的集合。這樣訓(xùn)練集有 960 張帶雨圖驗(yàn)證集和測(cè)試集各 120 張。錯(cuò)誤的方式是直接把 1200 張帶雨圖打亂按比例切分。這樣同一張干凈圖的 3 張帶雨圖很可能被分到不同集合驗(yàn)證集里的場(chǎng)景在訓(xùn)練集里出現(xiàn)過指標(biāo)虛高。實(shí)現(xiàn)上我通常會(huì)在 Dataset 初始化之前先構(gòu)建一個(gè)scene_id - [rainy_paths]的字典然后對(duì)scene_id列表做劃分最后展開成實(shí)際的圖片路徑列表。這樣邏輯清晰也不容易出錯(cuò)。劃分時(shí)記得固定隨機(jī)種子保證每次實(shí)驗(yàn)的劃分一致否則不同輪次的對(duì)比就沒有意義了。提示如果你的數(shù)據(jù)集本身沒有提供場(chǎng)景 ID只能從文件名推斷那一定要先寫個(gè)小腳本統(tǒng)計(jì)一下“干凈圖數(shù)量”和“帶雨圖數(shù)量”的比例以及文件名前綴的分布情況。我遇到過前綴解析錯(cuò)誤導(dǎo)致 1200 張圖只匹配上 300 張的情況訓(xùn)練時(shí) loss 正常下降但模型效果極差排查了很久才發(fā)現(xiàn)是配對(duì)漏了。2. 寫一個(gè)能跑通的去雨 Dataset從__init__到__getitem__認(rèn)知理清之后進(jìn)入代碼層面。PyTorch 的Dataset類看起來簡(jiǎn)單就三個(gè)方法但去雨任務(wù)的特殊性讓每個(gè)方法都有講究。我下面給出的實(shí)現(xiàn)是我自己在多個(gè)項(xiàng)目里迭代過的版本兼顧了靈活性、性能和可讀性。先明確目標(biāo)輸入是帶雨圖路徑輸出是(rainy_tensor, clean_tensor)這樣一對(duì)張量形狀都是[3, H, W]數(shù)值范圍歸一化到[0, 1]或者標(biāo)準(zhǔn)化到[-1, 1]具體取決于你后面用的損失函數(shù)和預(yù)訓(xùn)練模型。2.1__init__里該做什么、不該做什么__init__的職責(zé)是建立索引和映射而不是加載圖片數(shù)據(jù)。這一點(diǎn)很多人會(huì)搞混把圖片讀取放在__init__里結(jié)果數(shù)據(jù)集一大內(nèi)存直接爆掉或者啟動(dòng)時(shí)間長(zhǎng)得離譜。我的做法是在__init__里完成這幾件事第一掃描目錄構(gòu)建rainy_paths和clean_paths兩個(gè)列表并且保證它們一一對(duì)應(yīng)。如果是“一對(duì)多”結(jié)構(gòu)rainy_paths會(huì)比clean_paths長(zhǎng)但每個(gè)rainy_path都能通過映射找到對(duì)應(yīng)的clean_path。第二解析場(chǎng)景 ID建立scene_to_indices字典方便后續(xù)按場(chǎng)景劃分。第三接收transform參數(shù)。注意去雨任務(wù)的 transform 有個(gè)特殊要求輸入和標(biāo)簽必須做完全相同的幾何變換。如果你用torchvision.transforms里的RandomCrop分別作用在兩張圖上很可能裁到不同位置配對(duì)就廢了。正確做法是用torchvision.transforms.functional或者自己寫一個(gè)同時(shí)接收兩張圖的 transform 函數(shù)。第四可選地接收一個(gè)cache參數(shù)。如果數(shù)據(jù)集不大比如干凈圖只有幾百張可以在__init__里把干凈圖預(yù)加載到內(nèi)存帶雨圖按需讀取。這樣能顯著減少 IO因?yàn)楦蓛魣D會(huì)被反復(fù)讀取。這里有個(gè)經(jīng)驗(yàn)不要用os.listdir直接拿文件列表因?yàn)樗槐WC順序。如果你先listdir帶雨圖目錄再listdir干凈圖目錄兩個(gè)列表的順序可能不一致導(dǎo)致配對(duì)錯(cuò)亂。穩(wěn)妥的做法是用sorted(os.listdir(...))或者自己寫一個(gè)按場(chǎng)景 ID 排序的邏輯。2.2__getitem__的配對(duì)邏輯與異常處理__getitem__是核心它接收一個(gè)索引返回一對(duì)張量。我的實(shí)現(xiàn)大致是這樣的思路def __getitem__(self, idx): rainy_path self.rainy_paths[idx] scene_id self.parse_scene_id(rainy_path) clean_path self.scene_to_clean[scene_id] rainy_img Image.open(rainy_path).convert(RGB) clean_img Image.open(clean_path).convert(RGB) # 同步幾何變換 rainy_tensor, clean_tensor self.joint_transform(rainy_img, clean_img) return rainy_tensor, clean_tensor看起來簡(jiǎn)單但有幾個(gè)坑必須處理。坑一圖片尺寸不一致。有些數(shù)據(jù)集的帶雨圖和干凈圖分辨率不同比如帶雨圖是裁剪過的干凈圖是原圖。直接送進(jìn)網(wǎng)絡(luò)會(huì)報(bào)錯(cuò)。我的做法是在__init__里做一次尺寸檢查如果發(fā)現(xiàn)不一致要么統(tǒng)一 resize要么在__getitem__里做中心裁剪。中心裁剪更安全因?yàn)樗粫?huì)引入插值偽影而去雨任務(wù)對(duì)偽影很敏感??佣D片模式不是 RGB。有些干凈圖是灰度圖或者帶 alpha 通道的 PNG。convert(RGB)能解決大部分問題但如果原圖是 16 位深度convert會(huì)截?cái)喑?8 位損失信息。如果你的數(shù)據(jù)集是 16 位需要用numpy讀取并手動(dòng)歸一化??尤募p壞或缺失。大規(guī)模數(shù)據(jù)集里偶爾會(huì)有損壞的圖片。如果__getitem__直接拋異常訓(xùn)練會(huì)中斷。我的做法是用try-except包住讀取邏輯出錯(cuò)時(shí)隨機(jī)返回另一對(duì)樣本并打印警告。這樣訓(xùn)練不會(huì)斷但你要定期檢查警告日志看看是不是有系統(tǒng)性問題??铀呐鋵?duì)失敗。如果某個(gè)帶雨圖的場(chǎng)景 ID 在干凈圖里找不到說明數(shù)據(jù)集本身有問題。這種情況我建議在__init__階段就檢查并報(bào)錯(cuò)而不是等到訓(xùn)練時(shí)才發(fā)現(xiàn)。可以在__init__里遍歷所有帶雨圖驗(yàn)證每個(gè)場(chǎng)景 ID 都有對(duì)應(yīng)的干凈圖缺失的直接列出來。2.3 同步幾何變換去雨任務(wù)最容易寫錯(cuò)的地方這一小節(jié)單獨(dú)拿出來講因?yàn)樗匾?。去雨任?wù)的輸入和標(biāo)簽是同一場(chǎng)景的兩張圖空間位置必須嚴(yán)格對(duì)齊。如果你對(duì)輸入做隨機(jī)裁剪對(duì)標(biāo)簽做另一套隨機(jī)裁剪兩張圖就錯(cuò)位了模型學(xué)到的是錯(cuò)誤映射loss 可能下降但效果完全不對(duì)。正確的做法是生成一組隨機(jī)參數(shù)然后把這組參數(shù)同時(shí)應(yīng)用到兩張圖上。比如隨機(jī)裁剪你先隨機(jī)生成裁剪的左上角坐標(biāo)和寬高然后用同一組坐標(biāo)去裁兩張圖。隨機(jī)翻轉(zhuǎn)同理生成一個(gè)布爾值兩張圖一起翻或不翻。torchvision.transforms.functional提供了crop、hflip、vflip、rotate等函數(shù)它們接收具體的參數(shù)而不是隨機(jī)生成參數(shù)正好適合這種場(chǎng)景。你可以自己封裝一個(gè)JointTransform類在__call__里生成隨機(jī)參數(shù)并應(yīng)用到兩張圖。如果你用的是albumentations庫它原生支持additional_targets可以把標(biāo)簽圖作為額外目標(biāo)保證變換同步。配置方式是在Compose里加additional_targets{image0: image}然后調(diào)用時(shí)傳入imagerainy, image0clean。這個(gè)庫在處理同步變換上比手寫更省心但要注意版本兼容性不同版本的 API 有差異。注意不要用RandomResizedCrop這類會(huì)改變圖像內(nèi)容的變換去做同步因?yàn)閮蓮垐D如果內(nèi)容不同一張有雨一張沒雨resize 后的插值結(jié)果會(huì)有細(xì)微差異雖然影響不大但嚴(yán)格來說不如直接裁剪干凈。我通常只用隨機(jī)裁剪 隨機(jī)翻轉(zhuǎn)最多加一個(gè) 90 度旋轉(zhuǎn)。3. 數(shù)據(jù)加載性能為什么你的 GPU 利用率上不去模型訓(xùn)練慢很多人第一反應(yīng)是模型太大或者 GPU 不行。但在去雨任務(wù)里更常見的原因是數(shù)據(jù)加載成了瓶頸。去雨模型的輸入輸出都是高分辨率圖片讀取、解碼、變換的開銷很大如果 DataLoader 配置不當(dāng)GPU 大部分時(shí)間在等數(shù)據(jù)。我做過一個(gè)對(duì)比實(shí)驗(yàn)同樣的模型、同樣的數(shù)據(jù)量只調(diào)整 DataLoader 的參數(shù)訓(xùn)練速度差了將近一倍。下面把我踩過的坑和調(diào)優(yōu)經(jīng)驗(yàn)整理出來。3.1num_workers不是越大越好num_workers控制數(shù)據(jù)加載的并行進(jìn)程數(shù)。理論上越大越快但實(shí)際上受限于 CPU 核心數(shù)、磁盤 IO 和內(nèi)存帶寬。我試過在 8 核機(jī)器上設(shè)num_workers16結(jié)果反而比num_workers4慢因?yàn)檫M(jìn)程切換和內(nèi)存拷貝的開銷超過了并行收益。我的經(jīng)驗(yàn)值是num_workers設(shè)為 CPU 物理核心數(shù)的 1 到 1.5 倍。比如 8 核就設(shè) 8 或 12。如果是 SSD可以適當(dāng)調(diào)大如果是機(jī)械硬盤調(diào)大反而會(huì)因?yàn)殡S機(jī)讀取導(dǎo)致尋道開銷增加。另外num_workers 0時(shí)Dataset 里的對(duì)象會(huì)被復(fù)制到子進(jìn)程如果你在__init__里緩存了大量數(shù)據(jù)每個(gè) worker 都會(huì)復(fù)制一份內(nèi)存占用會(huì)成倍增加。解決辦法是用torch.utils.data.DataLoader的persistent_workersTrue配合prefetch_factor或者把緩存改成共享內(nèi)存。3.2pin_memory和prefetch_factor的配合pin_memoryTrue會(huì)把數(shù)據(jù)放到鎖頁內(nèi)存加速 CPU 到 GPU 的傳輸。這個(gè)參數(shù)在 GPU 訓(xùn)練時(shí)幾乎必開但要注意它和num_workers的配合。如果num_workers0pin_memory的效果有限因?yàn)閿?shù)據(jù)加載和傳輸是串行的。prefetch_factor控制每個(gè) worker 預(yù)取多少個(gè) batch。默認(rèn)是 2對(duì)于去雨這種 IO 密集的任務(wù)可以調(diào)到 4 或 6。但調(diào)太大也會(huì)占內(nèi)存需要根據(jù)你的圖片分辨率和 batch size 權(quán)衡。我通常的配置是loader DataLoader( dataset, batch_size8, shuffleTrue, num_workers8, pin_memoryTrue, prefetch_factor4, persistent_workersTrue, drop_lastTrue )drop_lastTrue在去雨任務(wù)里建議開啟因?yàn)樽詈笠粋€(gè) batch 如果只有一張圖BatchNorm 的統(tǒng)計(jì)會(huì)不穩(wěn)定影響訓(xùn)練。3.3 圖片解碼的優(yōu)化從 PIL 到 OpenCV 再到 DALIPIL 是 PyTorch 生態(tài)里最常用的圖片讀取庫但它的解碼速度一般。如果你的數(shù)據(jù)集很大可以換成 OpenCV 的cv2.imread速度通常快 2 到 3 倍。注意 OpenCV 讀進(jìn)來是 BGR 順序需要轉(zhuǎn)成 RGB。更進(jìn)一步如果 CPU 解碼仍然是瓶頸可以考慮 NVIDIA 的 DALI 庫它能把解碼放到 GPU 上做。但 DALI 的配置比較復(fù)雜而且對(duì)自定義變換的支持不如 PIL 靈活。我的建議是先用 PIL 跑通確認(rèn)瓶頸在數(shù)據(jù)加載后再考慮換庫。很多時(shí)候把num_workers和prefetch_factor調(diào)好問題就解決了。還有一個(gè)容易被忽略的點(diǎn)圖片格式。PNG 是無損壓縮解碼慢但質(zhì)量高JPEG 解碼快但有壓縮偽影。去雨任務(wù)對(duì)偽影敏感我建議訓(xùn)練時(shí)用 PNG如果磁盤空間不夠可以用高質(zhì)量的 JPEGquality 95 以上。我實(shí)測(cè)過quality 95 的 JPEG 和 PNG 在去雨模型上的效果差異很小但讀取速度快不少。4. 訓(xùn)練中的那些坑從 loss 不下降到雨痕殘留數(shù)據(jù)和加載都搞定之后訓(xùn)練過程本身也有一堆坑。去雨任務(wù)和普通圖像恢復(fù)任務(wù)相比有幾個(gè)特殊之處雨痕是疊加在圖像上的不是簡(jiǎn)單遮擋雨痕有方向性不同方向的雨需要不同的處理雨痕的密度變化很大從稀疏雨滴到暴雨都有。這些特性決定了你的訓(xùn)練策略不能照搬超分或者去噪。4.1 損失函數(shù)的選擇L1、L2 還是感知損失L2 損失MSE在去雨任務(wù)里有個(gè)著名的問題它傾向于產(chǎn)生模糊的結(jié)果。因?yàn)?L2 對(duì)誤差的懲罰是平方的模型會(huì)傾向于預(yù)測(cè)所有可能值的平均導(dǎo)致雨痕區(qū)域被“抹平”而不是“去除”。你去看用純 L2 訓(xùn)練的去雨模型雨痕是沒了但原本的紋理也糊了。L1 損失比 L2 好一些因?yàn)樗鼘?duì)異常值的懲罰是線性的不會(huì)過度平滑。但 L1 也有問題它在零點(diǎn)不可導(dǎo)訓(xùn)練后期容易震蕩。實(shí)踐中我通常用SmoothL1Loss也叫 Huber Loss它在誤差小的時(shí)候表現(xiàn)為 L2誤差大的時(shí)候表現(xiàn)為 L1兼顧了穩(wěn)定性和銳度。如果你追求更好的視覺效果可以加感知損失Perceptual Loss用預(yù)訓(xùn)練的 VGG 提取特征比較去雨結(jié)果和干凈圖在特征空間的差異。感知損失能讓結(jié)果更符合人眼觀感但訓(xùn)練成本會(huì)上升而且需要小心權(quán)重設(shè)置感知損失權(quán)重太大時(shí)PSNR 會(huì)下降。我的經(jīng)驗(yàn)配置是L1 0.1 * Perceptual感知損失用 VGG16 的 relu2_2 和 relu4_3 層。這個(gè)配置在多個(gè)數(shù)據(jù)集上表現(xiàn)穩(wěn)定。4.2 雨痕殘留和過度去雨的平衡訓(xùn)練去雨模型時(shí)你會(huì)遇到兩種相反的問題一種是雨痕沒去干凈另一種是把原本的紋理當(dāng)成雨去掉了導(dǎo)致圖像過度平滑。雨痕殘留通常發(fā)生在訓(xùn)練早期模型還沒學(xué)會(huì)識(shí)別雨痕。這時(shí)候可以檢查一下學(xué)習(xí)率是不是太大或者數(shù)據(jù)增強(qiáng)是不是太激進(jìn)。如果雨痕是細(xì)密的斜線而你的隨機(jī)裁剪把大部分雨痕裁掉了模型就學(xué)不到。解決辦法是確保裁剪后的區(qū)域仍然包含足夠的雨痕可以通過統(tǒng)計(jì)雨痕像素比例來篩選。過度去雨通常發(fā)生在訓(xùn)練后期模型為了降低 loss把一些高頻紋理也當(dāng)成雨去掉了。這時(shí)候可以降低學(xué)習(xí)率或者引入一個(gè)“保真度”約束比如在干凈圖的高頻區(qū)域加大 loss 權(quán)重。另一個(gè)技巧是使用對(duì)抗損失GAN Loss讓判別器判斷結(jié)果是否像真實(shí)干凈圖但這會(huì)顯著增加訓(xùn)練復(fù)雜度新手不建議一上來就用。4.3 驗(yàn)證集指標(biāo)和肉眼觀感的差距PSNR 和 SSIM 是去雨任務(wù)最常用的指標(biāo)但它們和肉眼觀感經(jīng)常不一致。我遇到過 PSNR 很高的模型結(jié)果圖看起來還是有霧感也遇到過 PSNR 一般但觀感很干凈的模型。原因是 PSNR 對(duì)亮度偏移很敏感如果模型整體偏亮或偏暗PSNR 會(huì)掉但人眼可能覺得還好。SSIM 好一些但它對(duì)紋理的敏感度也不夠。我的做法是訓(xùn)練時(shí)看 PSNR/SSIM但最終選模型一定要肉眼對(duì)比。我會(huì)在驗(yàn)證集里挑幾張有代表性的圖稀疏雨、密集雨、不同背景把不同 epoch 的結(jié)果拼在一起看。有時(shí)候 PSNR 最高的 epoch 并不是觀感最好的選那個(gè)觀感好的指標(biāo)差 0.1 到 0.2 完全可以接受。另外驗(yàn)證時(shí)要注意把結(jié)果保存成圖片不要只看數(shù)值。我習(xí)慣每幾個(gè) epoch 就導(dǎo)出一次驗(yàn)證結(jié)果用圖片瀏覽器快速翻看比盯著數(shù)字直觀得多。5. 從單張推理到批量部署實(shí)際使用中的工程問題模型訓(xùn)練好之后真正的挑戰(zhàn)才開始。研究階段你處理的是固定尺寸的圖片實(shí)際使用中你會(huì)遇到各種尺寸、各種來源的圖片。這一節(jié)講講工程化落地時(shí)的問題。5.1 任意尺寸輸入的推理策略去雨模型通常是全卷積網(wǎng)絡(luò)理論上支持任意尺寸輸入。但實(shí)際推理時(shí)如果圖片太大顯存會(huì)爆如果圖片太小感受野不夠效果會(huì)下降。我的策略是把大圖切成有重疊的 patch分別推理后拼接。重疊區(qū)域取平均避免拼接縫。patch 大小根據(jù)你的模型和顯存來定我通常用 512x512重疊 64 像素。這個(gè)配置在 8G 顯存的卡上跑得很穩(wěn)。切 patch 時(shí)要注意不要簡(jiǎn)單地從左上角開始切因?yàn)橛旰劭赡苷寐湓谇懈罹€上。有重疊的切法能緩解這個(gè)問題但也不能完全避免。如果對(duì)質(zhì)量要求極高可以用滑動(dòng)窗口步長(zhǎng)小于 patch 大小重疊區(qū)域用高斯權(quán)重融合。5.2 批量推理時(shí)的顯存管理批量推理時(shí)顯存占用和 batch size 成正比。如果你要處理幾千張圖不要一次性全部加載而是用 DataLoader 流式讀取推理完一張釋放一張。另外推理時(shí)記得用torch.no_grad()否則會(huì)保存計(jì)算圖顯存占用會(huì)大很多。如果模型里有 BatchNorm推理時(shí)要設(shè)model.eval()否則會(huì)使用當(dāng)前 batch 的統(tǒng)計(jì)量結(jié)果不穩(wěn)定。我見過有人推理時(shí)忘了eval()結(jié)果單張推理和批量推理的結(jié)果不一致排查了半天。這個(gè)坑很隱蔽因?yàn)橛?xùn)練時(shí)一切正常只有推理時(shí)才暴露。5.3 結(jié)果后處理銳化和色彩校正模型輸出的圖片有時(shí)候會(huì)偏軟或者偏色。偏軟是因?yàn)?L1/L2 損失的平滑效應(yīng)偏色是因?yàn)橛?xùn)練數(shù)據(jù)的色彩分布和實(shí)際圖片不一致。后處理可以緩解這些問題。銳化用簡(jiǎn)單的UnsharpMask就行強(qiáng)度不要太大否則會(huì)放大噪聲。色彩校正可以用直方圖匹配把結(jié)果的色彩分布對(duì)齊到輸入圖注意是對(duì)齊到輸入圖不是干凈圖因?yàn)閷?shí)際使用時(shí)你沒有干凈圖。但后處理要謹(jǐn)慎它可能掩蓋模型本身的問題。我的建議是先看原始輸出如果問題不嚴(yán)重不要加后處理如果確實(shí)需要把后處理參數(shù)記錄下來方便復(fù)現(xiàn)。6. 一些零散但重要的經(jīng)驗(yàn)最后分享幾個(gè)我在做去雨項(xiàng)目時(shí)積累的零散經(jīng)驗(yàn)它們不屬于某個(gè)特定環(huán)節(jié)但每個(gè)都幫我省過時(shí)間。關(guān)于數(shù)據(jù)增強(qiáng)去雨任務(wù)的增強(qiáng)要克制。隨機(jī)裁剪和翻轉(zhuǎn)是安全的但顏色抖動(dòng)要小心因?yàn)轭伾兓赡茏屇P桶杨伾惓U`認(rèn)為雨痕。我通常只做輕微的亮度調(diào)整幅度控制在 0.1 以內(nèi)。關(guān)于預(yù)訓(xùn)練模型如果你的去雨網(wǎng)絡(luò)用了預(yù)訓(xùn)練 backbone比如 ResNet加載權(quán)重時(shí)要注意有些預(yù)訓(xùn)練模型是在 ImageNet 上訓(xùn)練的輸入歸一化參數(shù)不同。用錯(cuò)歸一化參數(shù)會(huì)導(dǎo)致訓(xùn)練初期 loss 爆炸。關(guān)于隨機(jī)種子去雨任務(wù)的隨機(jī)性來源很多數(shù)據(jù)劃分、數(shù)據(jù)增強(qiáng)、權(quán)重初始化固定種子能讓你復(fù)現(xiàn)實(shí)驗(yàn)。但要注意num_workers 0時(shí)DataLoader 的隨機(jī)種子需要在 worker 初始化函數(shù)里設(shè)置否則每個(gè) worker 的隨機(jī)序列可能相同導(dǎo)致增強(qiáng)結(jié)果重復(fù)。關(guān)于日志訓(xùn)練去雨模型時(shí)建議把每個(gè) epoch 的驗(yàn)證結(jié)果圖保存下來按 epoch 編號(hào)命名。這樣訓(xùn)練結(jié)束后可以快速回看模型是怎么逐步變好的也能發(fā)現(xiàn)某些 epoch 的異常比如突然變差又恢復(fù)可能是學(xué)習(xí)率調(diào)度的問題。關(guān)于顯存去雨模型的顯存占用和圖片分辨率是平方關(guān)系。512x512 的圖batch size 8 可能占 6G 顯存1024x1024 的圖同樣的 batch size 可能就要 20G 以上。如果你的顯存不夠優(yōu)先降 batch size而不是降分辨率因?yàn)榉直媛蕦?duì)去雨效果的影響更大。關(guān)于評(píng)估不要只在合成數(shù)據(jù)集上評(píng)估。合成雨和真實(shí)雨在物理特性上有差距合成雨通常是均勻的斜線真實(shí)雨有雨滴、有霧、有鏡頭上的水漬。如果你的模型只在合成數(shù)據(jù)上訓(xùn)練放到真實(shí)照片上效果會(huì)打折扣。有條件的話收集一些真實(shí)雨天照片做定性評(píng)估哪怕沒有干凈圖對(duì)照肉眼看看雨痕去得干不干凈也是有價(jià)值的。我在實(shí)際項(xiàng)目里最深的一個(gè)體會(huì)是去雨任務(wù)的上限往往不在模型結(jié)構(gòu)而在數(shù)據(jù)質(zhì)量。一個(gè)簡(jiǎn)單的 U-Net配上高質(zhì)量、多樣化的成對(duì)數(shù)據(jù)效果可能比一個(gè)復(fù)雜網(wǎng)絡(luò)配劣質(zhì)數(shù)據(jù)好得多。所以如果你剛開始做去雨先把數(shù)據(jù)集的配對(duì)、劃分、加載這三件事做扎實(shí)比急著換模型架構(gòu)重要得多。