實戰(zhàn):DeepLabv3+全流程與避坑指南)
簡介本資源為基于DeepLabv3的高分辨率航拍圖像語義分割畢業(yè)設(shè)計項目面向計算機視覺方向的高年級本科生與研究生幫助解決遙感影像地物分類、像素級標注與模型復現(xiàn)等實際問題。壓縮包共184個文件以95個Python源碼與84個pyc編譯文件為主體另含3個Jupyter Notebook、1個說明文檔與1個README整體約477KB結(jié)構(gòu)緊湊便于直接運行與二次開發(fā)。內(nèi)容涵蓋DeepLabv3基線訓練與推理流程并集成Swin、ResNet、HRNet、Twins、BiSeNetV2、BEiT等多種骨干網(wǎng)絡(luò)實現(xiàn)配套離線與在線推理腳本方便對比不同架構(gòu)在航拍場景下的分割表現(xiàn)。已有204人學習下載適合需要完整畢設(shè)方案、模型選型參考與排錯思路的讀者可據(jù)此快速搭建實驗環(huán)境、復現(xiàn)結(jié)果并撰寫論文。1. 航拍語義分割畢設(shè)為什么 DeepLabv3 仍是性價比最高的起點做遙感圖像處理的同學多半遇到過這個場景拿到一批無人機航拍圖想把建筑、道路、植被、水體逐像素分開翻了一圈論文發(fā)現(xiàn) Transformer 類模型效果確實好但顯存吃緊、訓練周期長實驗室那臺 3060 跑起來直冒煙。這時候 DeepLabv3 往往是最穩(wěn)的落點——它用空洞卷積擴大感受野用編碼器-解碼器結(jié)構(gòu)恢復邊界細節(jié)在航拍這種目標尺度差異極大的數(shù)據(jù)上精度和速度的平衡點卡得相當準。這個項目要解決的核心問題就一句話用 Python 把 DeepLabv3 跑通在自建航拍數(shù)據(jù)集上產(chǎn)出一套可復現(xiàn)的訓練、評估、推理流程夠撐起一篇畢業(yè)設(shè)計的實驗章節(jié)。適合誰適合已經(jīng)會 Python 基礎(chǔ)語法、裝過 PyTorch、但還沒完整做過一個分割項目的本科生也適合想快速驗證某個航拍場景可行性的研究生。2. 數(shù)據(jù)先行航拍語義分割數(shù)據(jù)集怎么造才不返工2.1 航拍標注的三種來源與選型邏輯航拍數(shù)據(jù)的獲取方式直接決定后面標注成本。常見做法有三條路一是用公開數(shù)據(jù)集如 LoveDA、Potsdam、Vaihingen優(yōu)點是標注質(zhì)量高、有論文背書缺點是場景固定想換城市或換季節(jié)就抓瞎二是自己飛無人機采集靈活性最高但需要處理畸變校正、拼接、勻色前期投入大三是用 Google Earth 或天地圖截屏成本最低但分辨率受限、有拼接縫做出來的模型泛化性差。我一般建議畢設(shè)場景下走「公開數(shù)據(jù)集打底 少量自采數(shù)據(jù)微調(diào)」的路線。公開數(shù)據(jù)集保證 baseline 能跑出合理指標自采數(shù)據(jù)用來體現(xiàn)工作量。選公開數(shù)據(jù)集時重點看三件事類別定義是否和你的課題一致、影像分辨率是否接近你的應(yīng)用場景、標注是單類還是多類。LoveDA 有 7 類、0.3 米分辨率適合城市與農(nóng)村混合場景Potsdam 是 5 厘米分辨率、6 類適合精細城市地物提取。2.2 從原始影像到可訓練 mask 的完整轉(zhuǎn)換拿到數(shù)據(jù)后第一步是統(tǒng)一格式。DeepLabv3 的標準輸入是 RGB 三通道圖像加單通道灰度 maskmask 像素值就是類別索引0,1,2...不是 RGB 彩色。很多新手在這里翻車用 labelme 標出來的是 JSON轉(zhuǎn)成 PNG 后是彩色圖直接喂給模型導致 loss 不下降。下面這段腳本把彩色 mask 轉(zhuǎn)成索引 mask并做訓練集/驗證集劃分import os import numpy as np from PIL import Image from sklearn.model_selection import train_test_split # 類別定義像素顏色 - 類別索引 COLOR_MAP { (0, 0, 0): 0, # 背景 (128, 0, 0): 1, # 建筑 (128, 128, 0): 2, # 道路 (0, 128, 0): 3, # 植被 (0, 0, 128): 4, # 水體 } def color_to_index(mask_path, save_path): 將彩色 mask 轉(zhuǎn)為單通道索引 mask img np.array(Image.open(mask_path).convert(RGB)) index_mask np.zeros(img.shape[:2], dtypenp.uint8) for color, idx in COLOR_MAP.items(): # 允許一定顏色容差避免 JPEG 壓縮導致顏色偏移 match np.all(np.abs(img.astype(int) - np.array(color)) 10, axis-1) index_mask[match] idx Image.fromarray(index_mask).save(save_path) # 批量處理 img_dir ./raw/images mask_dir ./raw/masks out_mask_dir ./dataset/masks os.makedirs(out_mask_dir, exist_okTrue) names [f for f in os.listdir(img_dir) if f.endswith(.png)] for name in names: color_to_index(os.path.join(mask_dir, name), os.path.join(out_mask_dir, name)) # 劃分數(shù)據(jù)集固定隨機種子保證可復現(xiàn) train_names, val_names train_test_split(names, test_size0.2, random_state42) with open(./dataset/train.txt, w) as f: f.write(\n.join(train_names)) with open(./dataset/val.txt, w) as f: f.write(\n.join(val_names))邏輯說明COLOR_MAP是顏色到索引的映射表容差設(shè)為 10 是為了對抗標注工具導出時的輕微色偏。train_test_split的random_state42必須固定否則每次跑出來的驗證集不同指標沒法對比。劃分比例 8:2 是航拍場景的常見起點數(shù)據(jù)量少于 500 張時建議改成 7:3。參數(shù)說明容差 10 不是萬能值如果標注工具用了抗鋸齒邊緣會出現(xiàn)過渡色這時候要么把容差調(diào)大要么在標注階段就關(guān)閉抗鋸齒。test_size根據(jù)數(shù)據(jù)量調(diào)整小于 300 張時驗證集比例可以降到 0.15。2.3 數(shù)據(jù)增強在航拍場景的特殊處理航拍圖像和自然圖像有個關(guān)鍵差異旋轉(zhuǎn)不變性更強。地面拍貓貓倒過來就不像貓了航拍拍建筑旋轉(zhuǎn) 90 度還是建筑。所以增強策略要加隨機旋轉(zhuǎn)90/180/270 度和隨機翻轉(zhuǎn)這兩項對航拍分割的 mIoU 提升通常有 2-3 個點。但要注意隨機裁剪要謹慎。航拍圖里建筑和道路的上下文關(guān)系很重要裁得太碎會讓模型學不到空間布局。我一般用 512×512 的裁剪尺寸原圖小于這個尺寸就做 padding 而不是縮放縮放會破壞地物的尺度特征。3. 模型搭建DeepLabv3 在航拍數(shù)據(jù)上的關(guān)鍵改動3.1 骨干網(wǎng)絡(luò)選型Xception 還是 ResNet原版 DeepLabv3 用 Xception 做骨干但 Xception 在航拍小目標上表現(xiàn)一般因為它的深度可分離卷積對紋理細節(jié)的提取能力弱于標準卷積。實際做下來ResNet-50 或 ResNet-101 做骨干更穩(wěn)尤其是建筑邊緣和細窄道路的分割。換骨干的操作在 PyTorch 里很簡單用 torchvision 的 resnet 替換掉原 backbone 即可。但要注意兩點一是 ResNet 的 output stride 要改成 16 或 8否則特征圖太小小目標全丟二是要加載 ImageNet 預訓練權(quán)重航拍數(shù)據(jù)量通常不夠從零訓。import torch import torch.nn as nn from torchvision.models import resnet50, ResNet50_Weights class DeepLabHead(nn.Module): def __init__(self, num_classes): super().__init__() # ASPP 模塊多尺度空洞卷積 self.aspp nn.ModuleList([ nn.Conv2d(2048, 256, 1, padding0, dilation1), nn.Conv2d(2048, 256, 3, padding6, dilation6), nn.Conv2d(2048, 256, 3, padding12, dilation12), nn.Conv2d(2048, 256, 3, padding18, dilation18), ]) self.global_pool nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(2048, 256, 1) ) self.project nn.Sequential( nn.Conv2d(256 * 5, 256, 1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.Dropout(0.5) ) self.classifier nn.Conv2d(256, num_classes, 1) def forward(self, x): feats [branch(x) for branch in self.aspp] gp self.global_pool(x) gp nn.functional.interpolate(gp, sizex.shape[2:], modebilinear, align_cornersFalse) feats.append(gp) x torch.cat(feats, dim1) x self.project(x) return self.classifier(x) def build_model(num_classes): backbone resnet50(weightsResNet50_Weights.IMAGENET1K_V2) # 去掉最后兩層保留到 layer4output stride 16 return nn.Sequential( backbone.conv1, backbone.bn1, backbone.relu, backbone.maxpool, backbone.layer1, backbone.layer2, backbone.layer3, backbone.layer4, DeepLabHead(num_classes) )邏輯說明ASPP 的四個分支分別用 dilation1/6/12/18 捕捉不同尺度的上下文這對航拍場景尤其重要——同一張圖里既有幾十像素寬的主干道也有幾像素寬的田埂。全局池化分支補上圖像級信息最后 concat 后 1×1 卷積降維。參數(shù)說明Dropout(0.5)在數(shù)據(jù)量小于 2000 張時建議保留大于 5000 張可以降到 0.3。dilation的取值和 output stride 有關(guān)stride16 時用 6/12/18stride8 時改成 12/24/36。3.2 損失函數(shù)交叉熵不夠用的時候怎么辦航拍數(shù)據(jù)有個典型問題類別極度不平衡。背景和植被可能占 70%建筑和道路加起來不到 30%。純交叉熵會讓模型偏向多數(shù)類建筑邊緣糊成一片。常見做法是交叉熵 Dice Loss 加權(quán)組合。Dice 對前景敏感能拉回小類別的召回。權(quán)重我一般設(shè) 0.5:0.5 起步如果小類別 mIoU 還是低把 Dice 權(quán)重提到 0.7。class CombinedLoss(nn.Module): def __init__(self, ce_weight0.5, dice_weight0.5, ignore_index255): super().__init__() self.ce nn.CrossEntropyLoss(ignore_indexignore_index) self.ce_weight ce_weight self.dice_weight dice_weight def dice_loss(self, pred, target, eps1e-6): # pred: [B, C, H, W] logits, target: [B, H, W] pred torch.softmax(pred, dim1) num_classes pred.shape[1] target_onehot nn.functional.one_hot(target, num_classes) target_onehot target_onehot.permute(0, 3, 1, 2).float() intersection (pred * target_onehot).sum(dim(0, 2, 3)) union pred.sum(dim(0, 2, 3)) target_onehot.sum(dim(0, 2, 3)) dice (2 * intersection eps) / (union eps) return 1 - dice.mean() def forward(self, pred, target): ce self.ce(pred, target) dice self.dice_loss(pred, target) return self.ce_weight * ce self.dice_weight * dice邏輯說明ignore_index255用于忽略標注邊界的不確定像素這是分割任務(wù)的標配。Dice 計算時對每個類別單獨算再取平均避免大類別主導。參數(shù)說明eps1e-6防止除零。如果顯存不夠可以把 Dice 計算改成只在部分類別上做但會損失精度。3.3 訓練配置學習率、batch size 與迭代次數(shù)的三角關(guān)系航拍分割的訓練配置沒有萬能公式但有一條經(jīng)驗線batch size 8、初始學習率 0.007、poly 衰減、迭代 30k-40k。這套配置在 512×512 輸入、ResNet-50 骨干下單卡 3060 大約跑 6-8 小時。學習率策略用 poly 比 step 更穩(wěn)公式是lr base_lr * (1 - iter/max_iter)^0.9。優(yōu)化器用 SGD momentum 0.9比 Adam 收斂慢但最終精度通常高 1-2 個點。如果趕時間Adam 1e-4 也能用但要在論文里說明。import torch.optim as optim def poly_lr(base_lr, iter, max_iter, power0.9): return base_lr * (1 - iter / max_iter) ** power optimizer optim.SGD(model.parameters(), lr0.007, momentum0.9, weight_decay1e-4) for it in range(max_iter): lr poly_lr(0.007, it, max_iter) for param_group in optimizer.param_groups: param_group[lr] lr # ... 訓練步驟邏輯說明weight_decay1e-4是分割任務(wù)常用值太大欠擬合太小過擬合。poly 的 power 取 0.9 是原論文默認改成 1.0 衰減更快適合小數(shù)據(jù)集。參數(shù)說明base_lr 和 batch size 要聯(lián)動batch size 減半時 lr 也減半。max_iter 根據(jù)數(shù)據(jù)量定1000 張圖大約 30k 迭代收斂。4. 避坑與排查航拍分割訓練中最容易翻車的五個點4.1 現(xiàn)象loss 降到 0.2 就不動了mIoU 卡在 0.4原因最常見的是 mask 索引和類別數(shù)對不上。比如 mask 里有 5 類但模型輸出設(shè)成 4 類交叉熵會把第 5 類當背景學loss 看著降但指標上不去。解決訓練前先跑一遍np.unique(mask)確認最大索引值 1 等于num_classes。另外檢查ignore_index是否和某個真實類別沖突255 是安全值但如果 mask 里有 255 就會出問題。4.2 現(xiàn)象驗證集 mIoU 比訓練集低 20 個點原因航拍數(shù)據(jù)如果按整圖劃分同一區(qū)域的不同裁剪塊會同時出現(xiàn)在訓練和驗證集造成數(shù)據(jù)泄漏。模型記住了這片區(qū)域的紋理換一片就崩。解決按地理區(qū)域劃分同一塊區(qū)域的圖要么全在訓練集要么全在驗證集。如果數(shù)據(jù)來自多個城市或多次飛行按飛行架次劃分最干凈。4.3 現(xiàn)象建筑邊緣分割呈鋸齒狀道路斷斷續(xù)續(xù)原因output stride 設(shè)成 32特征圖太小上采樣時細節(jié)丟失。或者沒加解碼器低層特征融合。解決把 output stride 改成 16并在解碼器里把 layer1 的特征stride4和 ASPP 輸出融合。融合前記得用 1×1 卷積把通道數(shù)對齊再雙線性上采樣 4 倍。4.4 現(xiàn)象訓練到一半 loss 突然變 NaN原因?qū)W習率太大導致梯度爆炸或者 Dice Loss 的 eps 太小遇到全零預測。解決先把學習率降到 1e-3 跑幾百步看是否穩(wěn)定穩(wěn)定后再逐步升。Dice 的 eps 從 1e-6 提到 1e-4。另外加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。4.5 現(xiàn)象推理時顯存溢出但訓練時正常原因推理沒開torch.no_grad()或者輸入尺寸比訓練時大。解決推理代碼包在with torch.no_grad():里大圖用滑窗推理窗口 512×512、重疊 64 像素最后拼接?;巴评淼拇a比整圖推理多 20 行但能處理任意尺寸輸入。5. 從 mIoU 到可交付評估、可視化與一個提點技巧評估不能只看 mIoU。畢設(shè)答辯時老師常問每個類別的表現(xiàn)怎么樣建筑和道路的邊界準不準這時候需要逐類 IoU 表和混淆矩陣。逐類 IoU 直接反映小類別是否被淹沒混淆矩陣能看出哪兩類容易混——航拍里建筑和道路的混淆通常最高因為屋頂和路面的紋理在低分辨率下接近??梢暬?matplotlib 疊三張圖原圖、GT mask、預測 mask。顏色映射用固定調(diào)色板保證不同圖之間顏色一致。這一步在論文里占 2-3 頁但能直觀體現(xiàn)模型能力。提點技巧測試時增強TTA。對同一張圖做水平翻轉(zhuǎn)、垂直翻轉(zhuǎn)、90 度旋轉(zhuǎn)分別推理后把 softmax 概率平均再取 argmax。這個操作不增加訓練成本推理時間翻 4 倍但 mIoU 通常能漲 1.5-2.5 個點。畢設(shè)里加上 TTA指標會好看不少。def tta_inference(model, img_tensor): 測試時增強四方向推理后概率平均 model.eval() probs [] with torch.no_grad(): # 原始 probs.append(torch.softmax(model(img_tensor), dim1)) # 水平翻轉(zhuǎn) probs.append(torch.softmax(model(torch.flip(img_tensor, [3])), dim1).flip([3])) # 垂直翻轉(zhuǎn) probs.append(torch.softmax(model(torch.flip(img_tensor, [2])), dim1).flip([2])) # 180 度旋轉(zhuǎn) probs.append(torch.softmax(model(torch.rot90(img_tensor, 2, [2, 3])), dim1).rot90(-2, [2, 3])) avg_prob torch.stack(probs, dim0).mean(dim0) return avg_prob.argmax(dim1)邏輯說明每次翻轉(zhuǎn)后推理再把輸出翻轉(zhuǎn)回原方向保證空間對齊。四組概率平均后取 argmax比單次推理更魯棒。參數(shù)說明TTA 的組數(shù)可以加到 8 組加 90 度和 270 度旋轉(zhuǎn)但收益遞減4 組是性價比拐點。顯存不夠時逐組推理再累加不要一次性 stack。最后說個血淚教訓畢設(shè)代碼一定要在答辯前兩周做一次完整復現(xiàn)——從空環(huán)境裝 PyTorch 開始跑通數(shù)據(jù)準備、訓練、評估、推理全流程。我見過太多人代碼在本地能跑換臺機器就報錯答辯現(xiàn)場演示翻車。把環(huán)境依賴寫進requirements.txt隨機種子固定數(shù)據(jù)路徑用相對路徑這三件事做完能省掉 80% 的現(xiàn)場事故。希望幫到你。本文還有配套的精品資源點擊獲取