別:CNN二分類實(shí)戰(zhàn)指南)
最近不少做計(jì)算機(jī)畢設(shè)的同學(xué)來問我同一個(gè)問題深度學(xué)習(xí)方向的課題選什么既能讓數(shù)字說話、又不用在答辯臺(tái)上被問穿底我給出的回答里總有一個(gè)用Python的PyTorch框架訓(xùn)練卷積神經(jīng)網(wǎng)絡(luò)做混凝土是否含有裂縫的識(shí)別。這個(gè)課題有意思的地方在于它不屬于純算法玩具題而是有真實(shí)工程背景的視覺分類任務(wù)——輸入一張混凝土表面圖像模型判斷是否存在裂縫。對(duì)畢設(shè)來說難度適中、公開數(shù)據(jù)可獲取、技術(shù)鏈路完整還能夠在模型改進(jìn)和可視化上做出不少讓評(píng)委眼前一亮的點(diǎn)。這篇文章我就把這個(gè)項(xiàng)目從數(shù)據(jù)準(zhǔn)備、模型搭建、訓(xùn)練調(diào)參到答辯展示的完整鏈條講透。我會(huì)把每個(gè)環(huán)節(jié)的為什么這樣做也一并說清楚而不是只丟給你一段能跑通的代碼。畢竟畢設(shè)答辯時(shí)評(píng)委最常問的不是你的準(zhǔn)確率是多少而是你為什么這么設(shè)計(jì)、這個(gè)指標(biāo)說明了什么、你有沒有踩過坑。文章里的內(nèi)容基于我實(shí)際復(fù)現(xiàn)和帶實(shí)驗(yàn)時(shí)的經(jīng)驗(yàn)?zāi)阍谧约簞?dòng)手時(shí)可以參考但也要結(jié)合自己的數(shù)據(jù)集和實(shí)驗(yàn)環(huán)境調(diào)整。1. 破題與選題背景為什么裂縫識(shí)別是畢設(shè)的好賽道1.1 混凝土裂縫檢測(cè)的工程剛需混凝土結(jié)構(gòu)在橋梁、隧道、大壩、建筑外墻里無處不在而裂縫是結(jié)構(gòu)健康狀態(tài)最直觀的表征之一。傳統(tǒng)檢測(cè)高度依賴人工目檢效率低、主觀性強(qiáng)一些高空或高危環(huán)境人根本過不去。把看圖判斷裂縫這件事交給深度學(xué)習(xí)模型來自動(dòng)化正是工業(yè)界實(shí)實(shí)在在的痛點(diǎn)。這個(gè)工程背景對(duì)畢設(shè)非常重要因?yàn)樗馕吨愕捻?xiàng)目不是自娛自樂而是有落地價(jià)值的。答辯時(shí)你只需要一句話交代痛點(diǎn)——傳統(tǒng)人工巡檢效率低且主觀我想用卷積神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)混凝土裂縫的自動(dòng)識(shí)別——比泛泛說我訓(xùn)練了一個(gè)分類器有說服力得多。1.2 深度學(xué)習(xí)做裂縫識(shí)別的底層邏輯為什么要用卷積神經(jīng)網(wǎng)絡(luò)而不是傳統(tǒng)圖像處理這是很多評(píng)委必問的問題。你要能講清楚傳統(tǒng)方法依賴Canny邊緣檢測(cè)、Sobel算子這類手動(dòng)設(shè)計(jì)的特征提取器再配合閾值、形態(tài)學(xué)操作做判斷。這類方法的致命弱點(diǎn)是閾值敏感、對(duì)光照變化和復(fù)雜背景極度脆弱換個(gè)現(xiàn)場(chǎng)就失效了。卷積神經(jīng)網(wǎng)絡(luò)的學(xué)習(xí)方式完全不同。它從大量標(biāo)注數(shù)據(jù)中自動(dòng)學(xué)習(xí)特征淺層卷積核學(xué)到邊緣、紋理深層卷積核學(xué)到裂縫特有的細(xì)長形態(tài)和局部模式。整個(gè)過程是數(shù)據(jù)驅(qū)動(dòng)的不需要人工設(shè)計(jì)特征。用一句話概括就是傳統(tǒng)方法在告訴機(jī)器裂縫長什么樣深度學(xué)習(xí)讓機(jī)器自己看明白裂縫長什么樣。這個(gè)區(qū)別是理解整個(gè)項(xiàng)目的鑰匙。1.3 任務(wù)定位二分類而非檢測(cè)或分割很多初學(xué)者會(huì)把裂縫識(shí)別的任務(wù)目標(biāo)搞混。這里要明確一點(diǎn)本項(xiàng)目做的是圖像二分類輸入一張圖片輸出含裂縫或不含裂縫的概率。它不做裂縫位置的框定也不做每個(gè)像素的語義分割。這個(gè)定位非常重要。二分類是計(jì)算機(jī)視覺里最基礎(chǔ)的任務(wù)技術(shù)鏈路短、評(píng)價(jià)標(biāo)準(zhǔn)清晰非常適合作為畢設(shè)的工作量主體。而且它不是一個(gè)死胡同——你可以在這個(gè)基礎(chǔ)上橫向擴(kuò)展。比如把分類結(jié)果進(jìn)一步做成裂縫寬度估算、裂縫區(qū)域標(biāo)注那就是從分類走向回歸或分割工作量可以自然疊加。2. 數(shù)據(jù)籌備與預(yù)處理決定模型上限的環(huán)節(jié)2.1 數(shù)據(jù)從哪來、要多少、怎么篩選混凝土裂縫識(shí)別的公開數(shù)據(jù)集在網(wǎng)絡(luò)上是可以找到的。這類數(shù)據(jù)集的常見形式是從混凝土表面拍攝的各類圖像已經(jīng)裁剪成大小一致的ROI區(qū)域并按有裂縫/無裂縫分好了文件夾。數(shù)據(jù)規(guī)模一般從幾千到幾萬張不等。有兩點(diǎn)我要特別提醒。第一公開數(shù)據(jù)集的干凈程度參差不齊下載后必須人工抽檢把明顯標(biāo)錯(cuò)的樣本清理掉第二即便數(shù)據(jù)集自帶標(biāo)注你也要注意類別分布。理想狀態(tài)下正負(fù)樣本接近一比一如果某個(gè)類特別多后面訓(xùn)練時(shí)容易出現(xiàn)偷懶行為——模型把所有樣本都判成多數(shù)類也能拿到很高準(zhǔn)確率。我建議的數(shù)據(jù)使用量是每類五千到一萬張起步。如果數(shù)據(jù)量明顯不夠就要靠下一節(jié)的增強(qiáng)策略來補(bǔ)。不過對(duì)于二分類這種相對(duì)簡單的任務(wù)配合預(yù)訓(xùn)練模型幾千張每類一般已經(jīng)能訓(xùn)練出可用的效果。2.2 圖像尺寸與輸入策略公開數(shù)據(jù)集里的圖像尺寸通常比較統(tǒng)一但不同數(shù)據(jù)源之間可能有差異。PyTorch里的數(shù)據(jù)加載流程中一般會(huì)通過transforms.Resize統(tǒng)一縮放到某個(gè)固定尺寸。尺寸選擇上常見的是224乘以224或256乘以256。為什么偏偏是這些數(shù)字因?yàn)镮mageNet預(yù)訓(xùn)練模型默認(rèn)輸入就是224/256你選擇這個(gè)尺寸才可以直接加載預(yù)訓(xùn)練權(quán)重做遷移學(xué)習(xí)。如果完全不使用預(yù)訓(xùn)練模型、從頭手寫小網(wǎng)絡(luò)選更小尺寸也問題不大。但需要注意CNN里卷積和池化會(huì)不斷降低特征圖分辨率輸入太小會(huì)導(dǎo)致深層特征過于粗糙輸入太大又浪費(fèi)顯存和算力。2.3 數(shù)據(jù)集劃分小心看不見的數(shù)據(jù)泄漏訓(xùn)練集、驗(yàn)證集、測(cè)試集的劃分建議采用6:2:2或7:2:1。這個(gè)比例本身沒有絕對(duì)標(biāo)準(zhǔn)但有一個(gè)常見陷阱如果你對(duì)整張大圖做了滑窗裁剪裁剪出的patch之間有大量重疊隨機(jī)打散劃分時(shí)同一張?jiān)紙D的patch可能同時(shí)出現(xiàn)在訓(xùn)練集和測(cè)試集里。這會(huì)導(dǎo)致一種假象驗(yàn)證集、測(cè)試集準(zhǔn)確率都非常高但模型真正面對(duì)獨(dú)立場(chǎng)景時(shí)表現(xiàn)平庸。因?yàn)闇y(cè)試集中包含了與訓(xùn)練樣本高度相似的patch信息泄露了特征。正確的做法是按照原始圖像的來源分組保證一個(gè)原始來源的patch只出現(xiàn)在一個(gè)集合里。這種細(xì)節(jié)答辯時(shí)主動(dòng)講出來反而是加分項(xiàng)。2.4 數(shù)據(jù)增強(qiáng)有邊界地?zé)o中生有數(shù)據(jù)增強(qiáng)是深度學(xué)習(xí)項(xiàng)目里性價(jià)比最高的環(huán)節(jié)之一?;A(chǔ)操作包括隨機(jī)水平/垂直翻轉(zhuǎn)、隨機(jī)旋轉(zhuǎn)、隨機(jī)亮度對(duì)比度擾動(dòng)、隨機(jī)裁剪、添加少量噪聲。但裂縫識(shí)別有個(gè)特殊性裂縫是細(xì)長結(jié)構(gòu)語義對(duì)空間形態(tài)非常敏感。極端的拉伸、極端的寬高比例變化可能讓裂縫變得完全不像是裂縫反而給模型引入錯(cuò)誤的先驗(yàn)。我的實(shí)踐經(jīng)驗(yàn)是旋轉(zhuǎn)角度控制在15度以內(nèi)、亮度擾動(dòng)幅度在0.8到1.2之間比較穩(wěn)妥重度增強(qiáng)反而可能讓訓(xùn)練loss抖動(dòng)劇烈。提示數(shù)據(jù)增強(qiáng)的本質(zhì)是引入先驗(yàn)知識(shí)。你要讓模型對(duì)真實(shí)世界中可能出現(xiàn)的拍攝變化不敏感但不要讓它學(xué)習(xí)到現(xiàn)實(shí)中不存在的畸形形態(tài)。3. 模型選型與網(wǎng)絡(luò)結(jié)構(gòu)設(shè)計(jì)從零搭建還是用預(yù)訓(xùn)練3.1 三路線對(duì)比手寫淺層CNN、經(jīng)典預(yù)訓(xùn)練模型、輕量化網(wǎng)絡(luò)很多學(xué)生糾結(jié)模型選型其實(shí)核心就三條路線我直接給對(duì)比路線代表結(jié)構(gòu)參數(shù)量訓(xùn)練難度精度上限答辯價(jià)值手寫淺層CNN3至5個(gè)卷積塊加全連接幾十萬到幾百萬低容易收斂中等偏上能講清結(jié)構(gòu)原理經(jīng)典預(yù)訓(xùn)練遷移ResNet18/ResNet50千萬級(jí)低收斂快高成熟穩(wěn)定改動(dòng)空間大輕量化/改進(jìn)網(wǎng)絡(luò)MobileNetV3、EfficientNet百萬級(jí)中高可做創(chuàng)新點(diǎn)對(duì)比實(shí)驗(yàn)個(gè)人建議畢設(shè)主線選擇路線二用ResNet18或ResNet50做遷移學(xué)習(xí)。理由有兩條。其一ResNet的殘差結(jié)構(gòu)是深度學(xué)習(xí)經(jīng)典范式你可以在答辯時(shí)把殘差連接的意義講清楚——它解決了深層網(wǎng)絡(luò)梯度消失和退化問題這是評(píng)委希望你掌握的。其二預(yù)訓(xùn)練權(quán)重提供了良好的初始化讓模型從一開始就有不錯(cuò)的特征提取能力訓(xùn)練時(shí)間和數(shù)據(jù)需求量都大幅降低。3.2 遷移學(xué)習(xí)在畢設(shè)里的性價(jià)比之王地位為什么不建議從頭訓(xùn)練一個(gè)深層網(wǎng)絡(luò)因?yàn)槟愕臄?shù)據(jù)量撐不起來。ImageNet那一千多萬張圖像訓(xùn)練出的參數(shù)不是幾萬張混凝土圖就能從頭學(xué)出來的。遷移學(xué)習(xí)的思想是模型在ImageNet上學(xué)到的邊緣、紋理、形狀等通用特征可以直接搬運(yùn)到裂縫識(shí)別任務(wù)中你只需要在高層做微調(diào)。實(shí)操層面我建議分兩步走前期凍結(jié)預(yù)訓(xùn)練模型的大部分層只訓(xùn)練最后的全連接分類層這叫做特征提取器模式等到損失不再明顯下降再解凍部分深層用較小的學(xué)習(xí)率做全局微調(diào)。這種兩階段策略比直接從頭開始訓(xùn)練穩(wěn)定得多而且基本不會(huì)跑飛。3.3 想加工作量注意力機(jī)制和輕量化改進(jìn)是兩條捷徑如果導(dǎo)師要求畢設(shè)里體現(xiàn)一定創(chuàng)新性你不必設(shè)計(jì)一個(gè)全新的網(wǎng)絡(luò)結(jié)構(gòu)那對(duì)本科生來說風(fēng)險(xiǎn)太高。穩(wěn)妥的改進(jìn)方向是用以下兩類小改動(dòng)第一類是注意力模塊。在ResNet的每個(gè)殘差塊后面插入Squeeze-and-Excitation模塊也就是SE模塊。它通過全局平均池化得到每個(gè)通道的權(quán)重再對(duì)通道重新加權(quán)。直觀地說它讓模型更關(guān)注哪些通道對(duì)判別裂縫更重要。這個(gè)改動(dòng)只有幾十行代碼但確實(shí)能在實(shí)驗(yàn)中看到準(zhǔn)確率或F1指標(biāo)有可感知的提升。第二類是輕量化改造。把普通卷積替換為深度可分離卷積參考MobileNet的思路大幅降低參數(shù)量。然后你可以做一組對(duì)比實(shí)驗(yàn)標(biāo)準(zhǔn)ResNet18與輕量化改進(jìn)版在精度和推理耗時(shí)上的差異。這種對(duì)比本身就是好素材答辯時(shí)評(píng)委非常吃這一套。3.4 輸出層設(shè)計(jì)兩類輸出和兩類損失函數(shù)二分類任務(wù)的輸出層有兩種常見做法需要你自己心里有數(shù)。第一種是輸出兩個(gè)節(jié)點(diǎn)接CrossEntropyLoss推理時(shí)用softmax得到兩個(gè)類別的概率。第二種是只輸出一個(gè)節(jié)點(diǎn)接BCEWithLogitsLoss推理時(shí)用sigmoid把輸出映射到0到1之間大于0.5判為正類。兩種數(shù)學(xué)上等價(jià)的但要注意BCE版本最后一層不需要激活函數(shù)因?yàn)锽CEWithLogitsLoss內(nèi)部已經(jīng)包含了sigmoidCrossEntropy版本也不要手動(dòng)再接softmax因?yàn)镻yTorch的CrossEntropyLoss同樣把softmax內(nèi)置了。這個(gè)細(xì)節(jié)看似太小但實(shí)際項(xiàng)目中很常見。很多人踩過我加了softmax又用了CrossEntropyLoss為什么結(jié)果很差的坑。要記住PyTorch的這兩個(gè)損失函數(shù)都內(nèi)置了激活步驟你只需要給模型原始輸出logits。4. 訓(xùn)練流程與關(guān)鍵配置讓模型真正收斂4.1 環(huán)境準(zhǔn)備與依賴清單整個(gè)項(xiàng)目在PyTorch生態(tài)下非常順依賴項(xiàng)也就這幾個(gè)torch、torchvision、opencv-python、numpy、matplotlib、scikit-learn、tensorboard可選用來畫曲線。如果你在本地沒有合適的GPU可以用云端的免費(fèi)GPU環(huán)境或CPU跑小模型。但要注意ResNet18在CPU上訓(xùn)練一個(gè)epoch可能要幾分鐘到十幾分鐘如果數(shù)據(jù)上萬張CPU訓(xùn)練會(huì)讓你崩潰。我的建議是代碼開發(fā)和調(diào)試階段用CPU正式訓(xùn)練用GPU。4.2 訓(xùn)練腳本核心邏輯示例下面給一個(gè)精簡但能直接跑通流程的PyTorch訓(xùn)練腳本骨架。注意這只是一個(gè)框架你需要根據(jù)自己的數(shù)據(jù)集路徑做調(diào)整import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset import torchvision from torchvision import transforms from sklearn.metrics import accuracy_score, confusion_matrix transform_train transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) transform_test transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 使用ImageFolder加載按類別分好的目錄結(jié)構(gòu) train_dataset torchvision.datasets.ImageFolder(rootdataset/train, transformtransform_train) val_dataset torchvision.datasets.ImageFolder(rootdataset/val, transformtransform_test) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers2) model torchvision.models.resnet18(weightstorchvision.models.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 2) # 替換最后一層為二分類 device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, patience3, factor0.5) epochs 30 best_acc 0.0 for epoch in range(epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) train_loss running_loss / len(train_dataset) model.eval() val_preds [] val_labels [] with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) val_preds.extend(preds.cpu().tolist()) val_labels.extend(labels.tolist()) val_acc accuracy_score(val_labels, val_preds) scheduler.step(train_loss) print(fEpoch {epoch1}/{epochs}, Loss: {train_loss:.4f}, Val Acc: {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)這段代碼的每一步都有講究。比如用model.fc nn.Linear(...)替換最后一層是因?yàn)轭A(yù)訓(xùn)練模型原本的1000類輸出與我們的二分類不匹配用ReduceLROnPlateau是因?yàn)樗茉隍?yàn)證集指標(biāo)不降時(shí)自動(dòng)降低學(xué)習(xí)率比固定步長衰減省心只在驗(yàn)證集準(zhǔn)確率提升時(shí)保存模型是為了防止最后一個(gè)epoch的權(quán)重反而不是最好的。4.3 訓(xùn)練中的動(dòng)態(tài)觀測(cè)與loss曲線解讀訓(xùn)練不是把代碼跑完就完事你要會(huì)看曲線。訓(xùn)練時(shí)建議同時(shí)記錄訓(xùn)練集loss和驗(yàn)證集準(zhǔn)確率至少每兩個(gè)epoch輸出一次。正常收斂的曲線是訓(xùn)練loss平滑下降驗(yàn)證集準(zhǔn)確率逐漸上升并趨于平穩(wěn)。如果訓(xùn)練loss持續(xù)下降但驗(yàn)證準(zhǔn)確率不升反降恭喜你過擬合開始了。如果loss在你調(diào)整參數(shù)后依然不降或一開始就是nan優(yōu)先檢查學(xué)習(xí)率是否過大、數(shù)據(jù)標(biāo)簽是否錯(cuò)亂、歸一化參數(shù)是否匹配。TensorBoard可以幫你實(shí)時(shí)看曲線。不想折騰的話matplotlib手動(dòng)保存曲線圖也足夠畢設(shè)論文里需要的就是最后那張訓(xùn)練曲線圖。4.4 防過擬合的整套組合拳過擬合在裂縫識(shí)別項(xiàng)目里很容易發(fā)生因?yàn)榱芽p圖像形態(tài)相對(duì)單一。我常用的組合拳按照優(yōu)先級(jí)排序是數(shù)據(jù)增強(qiáng)打底、Dropout和權(quán)重衰減輔助、早停兜底。數(shù)據(jù)增強(qiáng)的作用前面已經(jīng)說過是最有效的正則化手段。權(quán)重衰減即L2正則化在Adam優(yōu)化器里對(duì)應(yīng)weight_decay參數(shù)一般設(shè)置在1e-4到1e-5之間。Dropout通常加在全連接層之前但預(yù)訓(xùn)練模型里很少加如果你手寫淺層CNN可以在兩個(gè)全連接層之間加一個(gè)nn.Dropout(p0.5)。早停指的是驗(yàn)證集指標(biāo)連續(xù)若干個(gè)epoch沒有提升就提前終止訓(xùn)練防止無意義的繼續(xù)跑。4.5 訓(xùn)練配置中的常見錯(cuò)誤訓(xùn)練配置里最容易被忽視的是隨機(jī)種子固定。PyTorch的很多操作帶有隨機(jī)性如果不固定隨機(jī)種子每次跑出來的結(jié)果都會(huì)小幅波動(dòng)。你論文里寫的那張結(jié)果表今天跑一遍是95.2%明天跑一遍變成94.5%這種不穩(wěn)定的數(shù)據(jù)拿到答辯臺(tái)上容易被追問。固定隨機(jī)種子的方式很簡單import random import numpy as np def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False每次實(shí)驗(yàn)前調(diào)用set_seed()得到的指標(biāo)才是可復(fù)現(xiàn)的。這一點(diǎn)我在實(shí)際項(xiàng)目中吃過虧有一次調(diào)了一晚上參數(shù)最后發(fā)現(xiàn)是隨機(jī)種子沒固定前幾次的提升完全來自運(yùn)氣。固定種子之后那些改進(jìn)的效果立刻現(xiàn)了原形。5. 評(píng)估指標(biāo)與可視化答辯時(shí)拿得出手的圖5.1 準(zhǔn)確率并不是唯一指標(biāo)很多畢設(shè)論文從頭到尾只報(bào)告一個(gè)準(zhǔn)確率這在答辯時(shí)很容易被一票否決。準(zhǔn)確率高不代表模型好用。假設(shè)數(shù)據(jù)集里負(fù)樣本占80%模型只需把所有圖都判為負(fù)類準(zhǔn)確率就有80%。但在裂縫檢測(cè)的真實(shí)場(chǎng)景里這種模型一個(gè)裂縫都發(fā)現(xiàn)不了完全不可用。混凝土裂縫檢測(cè)里漏檢是比誤報(bào)嚴(yán)重得多的錯(cuò)誤。把人話翻譯一下把安全區(qū)域誤判為有裂縫最多浪費(fèi)一次現(xiàn)場(chǎng)復(fù)核的人力把真實(shí)裂縫漏掉可能導(dǎo)致結(jié)構(gòu)安全隱患被忽略。所以你應(yīng)該重點(diǎn)看召回率也就是所有真實(shí)裂縫圖里模型找回了多少。同時(shí)用F1分?jǐn)?shù)來平衡精確率和召回率。5.2 混淆矩陣與分類報(bào)告混淆矩陣和classification_report是答辯時(shí)最有說服力的圖表。它們把四類預(yù)測(cè)情況清清楚楚列出來TP實(shí)際有裂縫預(yù)測(cè)也有裂縫TN實(shí)際無裂縫預(yù)測(cè)也無裂縫FP實(shí)際無裂縫但預(yù)測(cè)有裂縫誤報(bào)FN實(shí)際有裂縫但預(yù)測(cè)無裂縫漏檢代碼上用scikit-learn幾行就能生成from sklearn.metrics import classification_report, confusion_matrix import matplotlib.pyplot as plt import numpy as np cm confusion_matrix(val_labels, val_preds) report classification_report(val_labels, val_preds, target_names[無裂縫, 有裂縫]) print(report) plt.figure(figsize(6, 5)) plt.imshow(cm, interpolationnearest, cmapplt.cm.Blues) plt.colorbar() tick_marks np.arange(2) plt.xticks(tick_marks, [無裂縫, 有裂縫]) plt.yticks(tick_marks, [無裂縫, 有裂縫]) plt.xlabel(預(yù)測(cè)標(biāo)簽) plt.ylabel(真實(shí)標(biāo)簽) for i in range(2): for j in range(2): plt.text(j, i, str(cm[i][j]), hacenter, vacenter) plt.savefig(confusion_matrix.png, dpi150, bbox_inchestight)這張圖放到論文里、答辯PPT里都非常直觀。你要能看著這張圖解釋模型的誤報(bào)主要來自哪些樣本漏報(bào)主要集中在哪些形態(tài)的裂縫。5.3 ROC曲線與AUC評(píng)價(jià)任意閾值下的能力ROC曲線的橫軸是假正例率縱軸是真正例率它刻畫的是模型在不同判定閾值下的整體表現(xiàn)。AUC則是曲線下面積越高代表模型區(qū)分正負(fù)樣本的能力越強(qiáng)。二分類任務(wù)畫ROC曲線的代碼是標(biāo)準(zhǔn)的from sklearn.metrics import roc_curve, auc # 用模型輸出的概率而不是類別標(biāo)簽 probs [] model.eval() with torch.no_grad(): for images, _ in val_loader: images images.to(device) # 對(duì)二分類取正類概率若輸出兩節(jié)點(diǎn)用softmax取第1列 outputs model(images) p torch.softmax(outputs, dim1)[:, 1].cpu().numpy() probs.extend(p) fpr, tpr, _ roc_curve(val_labels, probs) roc_auc auc(fpr, tpr) plt.figure(figsize(6, 5)) plt.plot(fpr, tpr, labelfROC (AUC {roc_auc:.4f})) plt.plot([0, 1], [0, 1], r--, label隨機(jī)猜測(cè)) plt.xlabel(假正例率 (False Positive Rate)) plt.ylabel(真正例率 (True Positive Rate)) plt.legend() plt.savefig(roc_curve.png, dpi150, bbox_inchestight)答辯時(shí)你可以提一句AUC為0.98意味著隨機(jī)抽一個(gè)有裂縫樣本和一個(gè)無裂縫樣本模型給出正確排序前者概率更高的概率是98%。這句話一出口評(píng)委就知道你不只是會(huì)跑代碼。5.4 Grad-CAM類激活圖讓模型解釋自己的判斷依據(jù)這是我認(rèn)為整個(gè)項(xiàng)目里性價(jià)比最高的可視化。Grad-CAM的核心思想是利用最后一層卷積特征圖的梯度計(jì)算出每個(gè)通道對(duì)分類結(jié)果的重要性權(quán)重然后生成一張熱力圖顯示模型在做決策時(shí)主要看了圖像的哪些區(qū)域。如果你給模型輸入一張含裂縫的圖像Grad-CAM熱力圖高亮區(qū)域應(yīng)該集中在裂縫上而不是圖像角落的陰影或雜物。這就提供了模型決策的可解釋性證據(jù)說明模型確實(shí)是看裂縫而不是在找顏色統(tǒng)計(jì)規(guī)律。Grad-CAM的實(shí)現(xiàn)思路大致是對(duì)指定卷積層輸出做前向拿到特征圖對(duì)目標(biāo)類別的得分做反向傳播拿到梯度把梯度和特征圖做加權(quán)求和經(jīng)過ReLU和歸一化得到熱力圖。具體代碼我建議你參考PyTorch官方社區(qū)里的一些實(shí)現(xiàn)再結(jié)合自己的模型結(jié)構(gòu)調(diào)整。這個(gè)可視化圖放到論文里比任何指標(biāo)都能打動(dòng)評(píng)委。經(jīng)驗(yàn)如果Grad-CAM熱力圖不集中在裂縫上反而到處亂高亮說明模型沒有真正學(xué)到裂縫特征這時(shí)候你再高的準(zhǔn)確率也要打個(gè)問號(hào)。這也是你排查模型學(xué)偏問題的利器。5.5 典型誤判案例分析把模型預(yù)測(cè)錯(cuò)的圖片單獨(dú)篩出來分析它們的共同特征這是體現(xiàn)工作量和對(duì)問題理解深度的地方。我復(fù)現(xiàn)時(shí)看到的誤判樣本主要有三類。第一類是假陽性樣本無裂縫圖像里恰好有模板接縫、排水管陰影、鋼筋銹跡模型把它們的紋理誤判成裂縫。第二類是假陰性樣本裂縫非常細(xì)、對(duì)比度極低人眼都要湊近看才能確認(rèn)模型直接漏掉了。第三類是模糊樣本拍攝時(shí)失焦或強(qiáng)光反光圖像本身信息量不足任何模型都很難判斷。這三類誤判對(duì)應(yīng)三種改進(jìn)方向假陽性多說明數(shù)據(jù)集里類裂縫干擾物不足需要擴(kuò)充負(fù)樣本假陰性多說明細(xì)裂縫特征沒有被充分學(xué)到可以嘗試增強(qiáng)細(xì)裂縫樣本或提升輸入分辨率模糊樣本多則說明采集質(zhì)量才是天花板不是模型問題。你把這些分析寫進(jìn)論文評(píng)委就無話可說了。6. 踩坑實(shí)錄訓(xùn)練崩掉與指標(biāo)虛高的瞬間6.1 坑一圖像和標(biāo)簽順序錯(cuò)亂loss紋絲不動(dòng)一次訓(xùn)練時(shí)我注意到訓(xùn)練loss始終在0.69附近震蕩準(zhǔn)確率也卡在50%。排查到最后問題出在數(shù)據(jù)讀取階段某個(gè)加載腳本使用os.listdir()直接掃文件夾而文件系統(tǒng)的返回順序每次運(yùn)行都可能不同導(dǎo)致圖片和標(biāo)簽在第幾個(gè)epoch之后就對(duì)不上了。這個(gè)問題的隱蔽性在于它不一定每次復(fù)現(xiàn)可能前幾個(gè)epoch正常后面開始亂。解決方式也很簡單不要依賴listdir的順序用torchvision.datasets.ImageFolder它會(huì)基于文件夾結(jié)構(gòu)自動(dòng)維護(hù)標(biāo)簽映射或者自定義Dataset類時(shí)在初始化階段固定類別的索引表而不是動(dòng)態(tài)排序。6.2 坑二訓(xùn)練和推理時(shí)的預(yù)處理不一致還有一個(gè)我印象很深的翻車經(jīng)歷訓(xùn)練時(shí)用了ImageNet的mean和std做歸一化準(zhǔn)確率一路漲到95%。但到了推理階段我寫了一段簡化代碼加載圖片之后直接ToTensor()忘了歸一化結(jié)果同樣的模型在測(cè)試集上準(zhǔn)確率掉到了60%多。原因其實(shí)很簡單模型訓(xùn)練時(shí)看到的圖像是減均值除方差之后的分布推理時(shí)如果輸入分布完全不同模型的表現(xiàn)自然崩潰。這個(gè)問題出現(xiàn)的頻率遠(yuǎn)比你想象得高因?yàn)橛?xùn)練代碼和部署代碼常常分開寫兩邊大概率會(huì)有差異。排查思路也很簡單把一段輸入圖像直接打印出來看像素值范圍。如果訓(xùn)練流水線里像素值在-2到2之間而推理流水線在0到1之間那就是預(yù)處理不一致趕緊對(duì)齊。6.3 坑三類別不平衡與懶惰模型雖然裂縫識(shí)別數(shù)據(jù)集通常相對(duì)平衡但有些公開數(shù)據(jù)源的負(fù)樣本明顯多于正樣本或者反過來。模型遇到明顯不平衡時(shí)會(huì)走捷徑把所有樣本都判為多數(shù)類因?yàn)檫@樣整體的準(zhǔn)確率損失更小。應(yīng)對(duì)策略有兩個(gè)一是給DataLoader設(shè)置WeightedRandomSampler讓每個(gè)batch里正負(fù)樣本比例均衡二是對(duì)損失函數(shù)設(shè)置類別權(quán)重比如CrossEntropyLoss(weighttorch.tensor([0.5, 2.0]))表示正類分錯(cuò)要付出4倍代價(jià)。我的建議是優(yōu)先用加權(quán)采樣因?yàn)樗跀?shù)據(jù)層面就保證了均衡對(duì)模型訓(xùn)練過程更自然。6.4 坑四模型記住了背景噪聲而不是裂縫有一次模型在訓(xùn)練集和驗(yàn)證集上都表現(xiàn)不錯(cuò)但我把Grad-CAM熱力圖一畫就發(fā)現(xiàn)問題了模型關(guān)注的區(qū)域不在裂縫上而在每張圖的角落——原來數(shù)據(jù)集的無裂縫類圖像是在固定角落放了水印或時(shí)間戳模型學(xué)到了有角落標(biāo)記就是無裂縫這種荒謬規(guī)律。這就是典型的捷徑學(xué)習(xí)問題。深度學(xué)習(xí)模型極其擅長尋找訓(xùn)練數(shù)據(jù)里的隱藏相關(guān)信號(hào)。驗(yàn)證方式就是可視化你永遠(yuǎn)不要只靠準(zhǔn)確率數(shù)字做判斷。解決方式是檢查數(shù)據(jù)里是否存在與類別強(qiáng)相關(guān)的背景干擾比如水印、光源方向、拍攝設(shè)備差異必要時(shí)對(duì)這些空間位置做裁剪或遮擋。6.5 坑五顯存溢出(OOM)訓(xùn)練時(shí)遇到OOM是最容易處理但最容易慌的場(chǎng)景。常見原因無非三點(diǎn)batch size太大、輸入分辨率太高、模型太大。排查順序是先降低batch size看能否跑通如果還不行就降低輸入分辨率最后再考慮換輕量模型。如果你想兼顧大batch size和有限的顯存可以試試PyTorch自帶的混合精度訓(xùn)練。只需要在訓(xùn)練循環(huán)里用torch.cuda.amp.autocast()包裹前向和反向過程再用GradScaler()縮放梯度。這個(gè)技巧能在不犧牲訓(xùn)練效果的前提下把顯存占用降一半左右。6.6 坑六標(biāo)注里的標(biāo)簽噪聲公開數(shù)據(jù)集的標(biāo)簽質(zhì)量同樣值得懷疑尤其是那種從拍攝視頻自動(dòng)裁剪生成的數(shù)據(jù)集。分辨率極低的小圖裂縫可能只占幾個(gè)像素標(biāo)注者可能漏標(biāo)也可能把真實(shí)裂縫標(biāo)為無裂縫。訓(xùn)練時(shí)這些錯(cuò)誤標(biāo)簽會(huì)讓模型無所適從表現(xiàn)為loss下降緩慢、驗(yàn)證集指標(biāo)忽高忽低。我最推薦的做法是抽一個(gè)訓(xùn)練周期結(jié)束后把預(yù)測(cè)置信度極高但預(yù)測(cè)錯(cuò)誤的樣本打印出來人工看一遍。置信度高卻錯(cuò)了大概率是標(biāo)簽本身就是錯(cuò)的。花一個(gè)小時(shí)清理標(biāo)簽比盲目調(diào)參有效得多。7. 答辯呈現(xiàn)策略與實(shí)際項(xiàng)目組織經(jīng)驗(yàn)7.1 從模型跑通了到把項(xiàng)目講清楚答辯和寫代碼是兩個(gè)完全不同的能力。評(píng)委不一定有時(shí)間細(xì)讀你的代碼但一定會(huì)通過PPT和現(xiàn)場(chǎng)表述判斷你項(xiàng)目水平和投入程度。我的建議是采用一條清晰的敘事線痛點(diǎn)與背景、數(shù)據(jù)分析與預(yù)處理、方案選型對(duì)比、實(shí)驗(yàn)過程與結(jié)果分析、創(chuàng)新點(diǎn)與可擴(kuò)展方向。每一個(gè)環(huán)節(jié)都要能回答為什么。為什么用ResNet18而不是ResNet50可以說裂縫二分類任務(wù)復(fù)雜度有限ResNet50在精度上不占明顯優(yōu)勢(shì)但參數(shù)量和推理時(shí)間翻倍我做了對(duì)比實(shí)驗(yàn)確認(rèn)ResNet18已經(jīng)滿足需求。這種回答既展示你對(duì)比過又展示你有工程權(quán)衡意識(shí)。7.2 讓工作量顯性化畢設(shè)評(píng)審里最怕的不是項(xiàng)目做得簡單而是做了很多卻讓人感覺不出。項(xiàng)目目錄要組織清晰建議包含data_preprocess/數(shù)據(jù)清洗和統(tǒng)計(jì)、models/網(wǎng)絡(luò)定義、train.py訓(xùn)練入口、evaluate.py評(píng)估與可視化、utils/通用工具、README.md運(yùn)行說明。README里寫清環(huán)境配置、數(shù)據(jù)格式、運(yùn)行步驟讓評(píng)委覺得你交付的是完整工程而不只是一個(gè)腳本。7.3 可擴(kuò)展方向幾句話讓項(xiàng)目活起來答辯最后評(píng)委常問未來可以怎么發(fā)展。這時(shí)候你要亮出你思考過但來不及做的東西而不是說如果時(shí)間更多我會(huì)繼續(xù)調(diào)參。對(duì)裂縫識(shí)別項(xiàng)目合理的擴(kuò)展方向包括把二分類升級(jí)為裂縫像素級(jí)分割使用U-Net或DeepLabV3做語義分割這會(huì)引入逐像素標(biāo)注數(shù)據(jù)的準(zhǔn)備問題和更強(qiáng)的計(jì)算需求把靜態(tài)圖片識(shí)別擴(kuò)展到視頻流實(shí)時(shí)檢測(cè)需要引入ONNX或TensorRT做推理加速引入裂縫寬度、長度的回歸估計(jì)從檢測(cè)有沒有走向評(píng)估有多嚴(yán)重。任何一個(gè)方向都能體現(xiàn)你對(duì)整個(gè)技術(shù)棧的深入理解。7.4 我復(fù)現(xiàn)這個(gè)項(xiàng)目后最想告訴你的事整個(gè)項(xiàng)目走下來我覺得最有價(jià)值的能力不是調(diào)包和跑模型而是建立數(shù)據(jù)-模型-評(píng)估的閉環(huán)思維。數(shù)據(jù)決定上限模型負(fù)責(zé)逼近這個(gè)上限評(píng)估讓你知道逼近了多少。所有訓(xùn)練技巧本質(zhì)上都是在這三個(gè)環(huán)節(jié)之間找平衡。一個(gè)模型效果不好不一定是網(wǎng)絡(luò)結(jié)構(gòu)的問題可能數(shù)據(jù)就有嚴(yán)重缺陷也可能是評(píng)估方式掩蓋了真實(shí)水平。如果你在做這個(gè)畢設(shè)我的建議是先花兩到三天把數(shù)據(jù)徹底看一遍。統(tǒng)計(jì)各類數(shù)量、隨機(jī)抽樣打印圖片、記錄圖像的共性干擾。這個(gè)前置工作看似枯燥但能在后面幫你省下大量調(diào)參時(shí)間。很多人一上來就抱著模型調(diào)學(xué)習(xí)率折騰三天最后發(fā)現(xiàn)數(shù)據(jù)集本身就有問題那才是真正的浪費(fèi)時(shí)間。另外一個(gè)實(shí)操層面的大實(shí)話是運(yùn)行實(shí)驗(yàn)時(shí)一定把時(shí)間戳加進(jìn)日志或模型文件名。比如checkpoints/resnet18_20250107_1330.pt不要用model_final.pt這種名字。你后續(xù)想對(duì)比幾次實(shí)驗(yàn)時(shí)會(huì)發(fā)現(xiàn)光是重命名就能救你的命。這個(gè)習(xí)慣看起來微不足道但我見過太多同學(xué)訓(xùn)練了三五個(gè)模型之后完全分不清哪個(gè)文件對(duì)應(yīng)哪組參數(shù)了。