志識(shí)別源碼包實(shí)戰(zhàn):從訓(xùn)練到評(píng)估完整鏈路)
簡(jiǎn)介這份資源是一套基于卷積神經(jīng)網(wǎng)絡(luò)識(shí)別交通標(biāo)志的Python項(xiàng)目源碼配套GTSRB數(shù)據(jù)集使用面向計(jì)算機(jī)、人工智能、通信、電子信息等相關(guān)專業(yè)的學(xué)生及企業(yè)員工。它可用于課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)、大作業(yè)或初期項(xiàng)目立項(xiàng)演示也適合作為深度學(xué)習(xí)入門實(shí)戰(zhàn)練習(xí)。壓縮包共9個(gè)文件約310KB包含5個(gè)py源碼文件、2個(gè)csv數(shù)據(jù)文件、1個(gè)xml配置文件和1個(gè)md說明文檔分別承擔(dān)模型構(gòu)建、訓(xùn)練評(píng)估、數(shù)據(jù)預(yù)處理與項(xiàng)目說明等職責(zé)。項(xiàng)目圍繞交通標(biāo)志識(shí)別任務(wù)提供從數(shù)據(jù)讀取、圖像預(yù)處理到CNN模型搭建、訓(xùn)練與評(píng)估的完整流程代碼經(jīng)過測(cè)試可正常運(yùn)行。目前已有170人學(xué)習(xí)下載讀者可借此理解卷積神經(jīng)網(wǎng)絡(luò)在圖像分類中的實(shí)際應(yīng)用掌握數(shù)據(jù)組織方式與訓(xùn)練評(píng)估思路并在此基礎(chǔ)上進(jìn)行二次開發(fā)或遷移到其他分類任務(wù)。1. 從一份 GTSRB 交通標(biāo)志識(shí)別源碼包說起它到底能跑出什么如果你正在為畢業(yè)設(shè)計(jì)或課程大作業(yè)找一個(gè)能跑通、有數(shù)據(jù)集、有完整訓(xùn)練評(píng)估鏈路的深度學(xué)習(xí)項(xiàng)目這份基于 CNN 識(shí)別交通標(biāo)志的 Python 源碼包值得先看一眼。它用的是 GTSRBGerman Traffic Sign Recognition Benchmark數(shù)據(jù)集這是交通標(biāo)志識(shí)別領(lǐng)域最經(jīng)典的公開基準(zhǔn)之一包含 43 類標(biāo)志、超過 5 萬張實(shí)拍圖片光照變化、遮擋、運(yùn)動(dòng)模糊全都有不是那種清洗得干干凈凈的玩具數(shù)據(jù)。整個(gè)包的結(jié)構(gòu)很直白TSRCnn.py定義網(wǎng)絡(luò)TSRTrain.py負(fù)責(zé)訓(xùn)練TSREval.py做評(píng)估TSRInput.py和Preprocessing.py處理數(shù)據(jù)輸入與預(yù)處理外加train_data.csv、test_data.csv兩個(gè)索引文件和一份 README。適合誰計(jì)算機(jī)、人工智能、通信、電子信息方向的同學(xué)拿來做課設(shè)或畢設(shè)底座也適合剛學(xué)完 CNN 卷積神經(jīng)網(wǎng)絡(luò)理論、想找一個(gè)完整項(xiàng)目把「數(shù)據(jù)讀入→預(yù)處理→建?!?xùn)練→評(píng)估」串起來的人。它不炫技但鏈路完整改起來有抓手。2. 拆開源碼包文件分工與 CNN 結(jié)構(gòu)怎么對(duì)應(yīng)2.1 每個(gè) .py 文件在訓(xùn)練鏈路里的位置拿到一個(gè)源碼包我習(xí)慣先不跑先把文件按數(shù)據(jù)流排一遍。這份包的邏輯線是清楚的Preprocessing.py圖像預(yù)處理通常做 resize、歸一化、灰度或通道調(diào)整。GTSRB 原始圖片尺寸不統(tǒng)一這一步是必須的。TSRInput.py數(shù)據(jù)輸入封裝把 CSV 里的路徑和標(biāo)簽讀進(jìn)來組織成訓(xùn)練/驗(yàn)證可用的批次。TSRCnn.pyCNN 網(wǎng)絡(luò)定義卷積層、池化層、全連接層的堆疊都在這里。TSRTrain.py訓(xùn)練主循環(huán)包含損失函數(shù)、優(yōu)化器、epoch 控制、模型保存。TSREval.py加載保存的模型在測(cè)試集上算準(zhǔn)確率、混淆矩陣之類的指標(biāo)。train_data.csv/test_data.csv不是圖片本身是「圖片路徑 標(biāo)簽」的索引表GTSRB 官方劃分好的訓(xùn)練/測(cè)試列表。這種拆分方式在課程設(shè)計(jì)里很常見好處是每個(gè)環(huán)節(jié)獨(dú)立你想換網(wǎng)絡(luò)結(jié)構(gòu)只動(dòng)TSRCnn.py想換預(yù)處理策略只動(dòng)Preprocessing.py不會(huì)牽一發(fā)動(dòng)全身。常見做法是先把 CSV 讀進(jìn) pandas 看一眼列名和標(biāo)簽分布確認(rèn)路徑字段和標(biāo)簽字段叫什么再?zèng)Q定后面怎么改。2.2 CNN 結(jié)構(gòu)卷積、池化、全連接的三段式交通標(biāo)志識(shí)別的 CNN 基本結(jié)構(gòu)跑不出「特征提取 分類頭」兩段。特征提取靠卷積層和池化層交替分類頭靠全連接層加 softmax。以 GTSRB 的 43 類為例一個(gè)能用的基礎(chǔ)結(jié)構(gòu)大概是這樣import torch import torch.nn as nn class TSRCnn(nn.Module): def __init__(self, num_classes43): super(TSRCnn, self).__init__() # 第一段卷積提取邊緣、顏色塊等低級(jí)特征 self.conv1 nn.Conv2d(3, 32, kernel_size3, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.pool nn.MaxPool2d(2, 2) # 分類頭把特征圖拉平后映射到 43 類 self.fc1 nn.Linear(64 * 8 * 8, 256) self.fc2 nn.Linear(256, num_classes) self.relu nn.ReLU() self.dropout nn.Dropout(0.5) def forward(self, x): x self.pool(self.relu(self.conv1(x))) # 32x32 - 16x16 x self.pool(self.relu(self.conv2(x))) # 16x16 - 8x8 x x.view(x.size(0), -1) # 拉平 x self.dropout(self.relu(self.fc1(x))) x self.fc2(x) return x這段代碼的關(guān)鍵參數(shù)num_classes43對(duì)應(yīng) GTSRB 的類別數(shù)改數(shù)據(jù)集必須同步改kernel_size3, padding1保證卷積后空間尺寸不變尺寸變化只發(fā)生在池化64 * 8 * 8是第二段卷積輸出拉平后的維度輸入圖片尺寸變了這個(gè)數(shù)就得重算。Dropout(0.5)是防過擬合的常規(guī)手段交通標(biāo)志數(shù)據(jù)量不算特別大加上它訓(xùn)練曲線會(huì)穩(wěn)一些。實(shí)際源碼里的層數(shù)、通道數(shù)可能和這里不同但結(jié)構(gòu)邏輯一致你照著這個(gè)骨架去讀TSRCnn.py就能對(duì)上號(hào)。2.3 數(shù)據(jù)預(yù)處理GTSRB 的圖片為什么不能直接喂GTSRB 原始圖片尺寸從 15×15 到 250×250 不等直接送進(jìn)網(wǎng)絡(luò)會(huì)因?yàn)槌叽绮灰恢聢?bào)錯(cuò)。Preprocessing.py要解決的就是這件事。常見做法是統(tǒng)一 resize 到 32×32 或 48×48然后歸一化到 [0,1] 或做標(biāo)準(zhǔn)化。下面是一個(gè)可抄的預(yù)處理片段from PIL import Image import numpy as np def preprocess_image(img_path, target_size(32, 32)): img Image.open(img_path).convert(RGB) # 統(tǒng)一轉(zhuǎn) RGB防止灰度圖通道數(shù)不一致 img img.resize(target_size) # 統(tǒng)一尺寸 arr np.array(img, dtypenp.float32) / 255.0 # 歸一化到 [0,1] return arrconvert(RGB)這步容易被忽略GTSRB 里混有灰度圖不轉(zhuǎn)的話通道數(shù)對(duì)不上后面卷積直接崩。target_size要和網(wǎng)絡(luò)輸入層匹配你改成 48×48TSRCnn.py里全連接層的輸入維度也得跟著改。歸一化用/255.0是最簡(jiǎn)單的也有用均值方差標(biāo)準(zhǔn)化的效果差異不大但歸一化這步不能省否則訓(xùn)練初期 loss 會(huì)跳得很難看。3. 從零跑通訓(xùn)練環(huán)境、命令與參數(shù)怎么設(shè)3.1 環(huán)境準(zhǔn)備與依賴安裝這份包是純 Python 深度學(xué)習(xí)框架的路線從文件名和結(jié)構(gòu)看大概率是 PyTorch 或 TensorFlow 其中之一。不管哪個(gè)環(huán)境準(zhǔn)備邏輯一樣。我一般會(huì)先建虛擬環(huán)境避免和系統(tǒng)里的包打架python -m venv tsr_env source tsr_env/bin/activate # Linux/Mac # tsr_env\Scripts\activate # Windows pip install torch torchvision pillow numpy pandas matplotlib如果你用的是 PyTorchtorch和torchvision是必須的pillow負(fù)責(zé)讀圖numpy和pandas處理 CSV 和數(shù)組matplotlib用來畫訓(xùn)練曲線。裝完之后先別急著跑訓(xùn)練用python -c import torch; print(torch.__version__)確認(rèn)一下版本CUDA 是否可用用torch.cuda.is_available()看一眼。沒有 GPU 也能跑GTSRB 在 CPU 上訓(xùn)練會(huì)慢但 32×32 的小圖不至于跑不動(dòng)只是 epoch 時(shí)間從秒級(jí)變成分鐘級(jí)。3.2 數(shù)據(jù)路徑配置CSV 里的路徑怎么對(duì)上train_data.csv和test_data.csv里存的是圖片路徑和標(biāo)簽但路徑是相對(duì)路徑還是絕對(duì)路徑直接決定你能不能跑起來。常見翻車場(chǎng)景是CSV 里寫的是Train/00000/00000_00000.png這種相對(duì)路徑但你的工作目錄不對(duì)讀圖全部失敗。解決辦法是先確認(rèn) CSV 的列結(jié)構(gòu)import pandas as pd df pd.read_csv(train_data.csv) print(df.columns.tolist()) print(df.head()) print(df[ClassId].value_counts().sort_index()) # 看類別分布如果列名是Path和ClassId那讀圖時(shí)就要拼上數(shù)據(jù)集根目錄。我一般會(huì)在TSRInput.py里加一個(gè)root_dir參數(shù)把 CSV 里的相對(duì)路徑和根目錄拼成絕對(duì)路徑。類別分布那行也值得看GTSRB 各類樣本數(shù)不均衡少的類只有幾百張多的有兩千多張訓(xùn)練時(shí)如果發(fā)現(xiàn)某些類識(shí)別率特別低大概率是樣本少導(dǎo)致的可以考慮加權(quán)重或做數(shù)據(jù)增強(qiáng)。3.3 訓(xùn)練腳本的關(guān)鍵參數(shù)epoch、batch size、學(xué)習(xí)率TSRTrain.py是訓(xùn)練入口跑之前先把幾個(gè)核心參數(shù)過一遍。下面是一個(gè)典型的訓(xùn)練循環(huán)骨架import torch import torch.nn as nn import torch.optim as optim from TSRCnn import TSRCnn device torch.device(cuda if torch.cuda.is_available() else cpu) model TSRCnn(num_classes43).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) for epoch in range(30): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}) torch.save(model.state_dict(), ftsr_epoch{epoch1}.pth)參數(shù)說明lr0.001是 Adam 的常用起點(diǎn)loss 不降就調(diào)到 0.0001震蕩就再降batch_size一般在 32 到 128 之間顯存小就調(diào)小epoch30是經(jīng)驗(yàn)值GTSRB 上 20 到 50 個(gè) epoch 通常能收斂看驗(yàn)證集準(zhǔn)確率不再漲就可以停。每個(gè) epoch 存一次模型是后悔藥萬一后面訓(xùn)崩了還能回退。CrossEntropyLoss自帶 softmax所以網(wǎng)絡(luò)最后一層不要再加 softmax加了反而數(shù)值不穩(wěn)定。3.4 評(píng)估與指標(biāo)準(zhǔn)確率之外還要看什么TSREval.py負(fù)責(zé)在測(cè)試集上評(píng)估。準(zhǔn)確率是最直觀的但只看準(zhǔn)確率不夠。GTSRB 類別不均衡一個(gè)把所有樣本都預(yù)測(cè)成多數(shù)類的模型也能有不錯(cuò)的準(zhǔn)確率所以混淆矩陣和每類召回率更值得看from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) outputs model(imgs) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, digits4)) print(confusion_matrix(all_labels, all_preds))classification_report會(huì)給出每類的 precision、recall、f1-score哪類識(shí)別差一目了然?;煜仃嚹芸闯瞿P桶涯念愬e(cuò)認(rèn)成哪類比如限速 50 和限速 60 容易混這是交通標(biāo)志識(shí)別的經(jīng)典難點(diǎn)因?yàn)閿?shù)字區(qū)域差異小。如果某兩類混淆嚴(yán)重可以考慮針對(duì)性做數(shù)據(jù)增強(qiáng)或者把輸入分辨率提上去。4. 避坑與排查跑這份源碼最容易翻車的五個(gè)地方4.1 圖片路徑讀不到訓(xùn)練一開始就報(bào) FileNotFoundError現(xiàn)象運(yùn)行TSRTrain.py后立刻報(bào)FileNotFoundError提示某張圖片不存在。原因CSV 里的路徑是相對(duì)路徑而你的工作目錄不是數(shù)據(jù)集根目錄或者 CSV 里的路徑分隔符和系統(tǒng)不匹配Windows 用反斜杠Linux 用正斜杠。解決在TSRInput.py里統(tǒng)一用os.path.join(root_dir, rel_path)拼路徑root_dir通過命令行參數(shù)或配置文件傳入跨平臺(tái)的話用pathlib.Path替代字符串拼接它會(huì)自動(dòng)處理分隔符。4.2 通道數(shù)不匹配卷積層直接報(bào)錯(cuò)現(xiàn)象RuntimeError: Given groups1, weight of size [32, 3, 3, 3], expected input[16, 1, 32, 32] to have 3 channels, but got 1 channels。原因GTSRB 里混有灰度圖Preprocessing.py沒做convert(RGB)讀進(jìn)來是單通道而網(wǎng)絡(luò)第一層定義的是 3 通道輸入。解決在預(yù)處理里強(qiáng)制Image.open(path).convert(RGB)保證所有圖片都是三通道。這個(gè)坑很隱蔽因?yàn)榇蟛糠謭D片是彩色的只有少數(shù)灰度圖會(huì)觸發(fā)訓(xùn)練跑到一半才崩。4.3 loss 不降或變成 nan訓(xùn)練直接廢掉現(xiàn)象訓(xùn)練幾個(gè) epoch 后 loss 變成nan或者一直卡在 2.3 左右不降。原因?qū)W習(xí)率太大導(dǎo)致梯度爆炸或者輸入沒有歸一化像素值在 0 到 255 之間梯度尺度失控。解決先把輸入歸一化到 [0,1]確認(rèn)Preprocessing.py里有/255.0然后把學(xué)習(xí)率從 0.001 降到 0.0001 試一輪如果還不行加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)。loss 變 nan 基本就是學(xué)習(xí)率和歸一化這兩個(gè)原因逐個(gè)排查。4.4 顯存不夠batch size 調(diào)小還是 OOM現(xiàn)象CUDA out of memory把 batch size 從 64 降到 32 還是報(bào)。原因可能是模型沒釋放中間變量或者圖片分辨率設(shè)太大32×32 的圖 batch 32 一般不會(huì) OOM除非你改成了 128×128。解決先確認(rèn)輸入尺寸是不是被改大了然后在訓(xùn)練循環(huán)里用with torch.no_grad()包住評(píng)估部分如果還不行用torch.cuda.empty_cache()手動(dòng)清緩存。CPU 訓(xùn)練的話不存在這個(gè)問題只是慢。4.5 評(píng)估準(zhǔn)確率虛高測(cè)試集混進(jìn)了訓(xùn)練集現(xiàn)象測(cè)試集準(zhǔn)確率 99% 以上但實(shí)際用新圖片測(cè)試時(shí)錯(cuò)得離譜。原因train_data.csv和test_data.csv有重疊或者預(yù)處理時(shí)對(duì)訓(xùn)練集和測(cè)試集做了不同的歸一化參數(shù)。解決先檢查兩個(gè) CSV 的圖片路徑有沒有交集用集合運(yùn)算set(train_paths) set(test_paths)看一眼然后確認(rèn)訓(xùn)練和測(cè)試用的是同一套預(yù)處理邏輯歸一化參數(shù)必須從訓(xùn)練集統(tǒng)計(jì)出來再應(yīng)用到測(cè)試集不能各算各的。這個(gè)坑在課程設(shè)計(jì)里很常見準(zhǔn)確率好看但模型沒用。5. 進(jìn)階玩法把這份源碼改成你自己的項(xiàng)目底座5.1 換數(shù)據(jù)集從 GTSRB 遷移到自定義交通標(biāo)志GTSRB 跑通之后很多人想換成自己采集的交通標(biāo)志數(shù)據(jù)。遷移的核心改動(dòng)就三處類別數(shù)、輸入尺寸、CSV 格式。類別數(shù)改TSRCnn.py里的num_classes輸入尺寸改Preprocessing.py的target_size和網(wǎng)絡(luò)全連接層的輸入維度CSV 格式保持「路徑 標(biāo)簽」兩列標(biāo)簽從 0 開始連續(xù)編號(hào)。如果自定義數(shù)據(jù)量少建議先用 GTSRB 預(yù)訓(xùn)練再在自己的數(shù)據(jù)上微調(diào)只訓(xùn)練最后幾層學(xué)習(xí)率設(shè)小一點(diǎn)比如 0.0001。這樣比從零訓(xùn)練效果好得多也是常見做法。5.2 數(shù)據(jù)增強(qiáng)讓模型對(duì)光照和角度更魯棒GTSRB 本身已經(jīng)包含不少變化但如果你要部署到實(shí)際場(chǎng)景數(shù)據(jù)增強(qiáng)能明顯提升魯棒性。常見做法是在預(yù)處理里加隨機(jī)亮度、隨機(jī)旋轉(zhuǎn)、隨機(jī)裁剪from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((32, 32)), transforms.RandomRotation(10), # 隨機(jī)旋轉(zhuǎn) ±10 度 transforms.ColorJitter(brightness0.3), # 隨機(jī)亮度調(diào)整 transforms.RandomAffine(degrees0, translate(0.1, 0.1)), # 隨機(jī)平移 transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ])注意增強(qiáng)只用在訓(xùn)練集測(cè)試集和驗(yàn)證集不要加隨機(jī)變換否則評(píng)估結(jié)果不可比。Normalize的均值和方差可以用訓(xùn)練集統(tǒng)計(jì)出來也可以用 0.5 這個(gè)經(jīng)驗(yàn)值差異不大。加了增強(qiáng)之后訓(xùn)練 epoch 要適當(dāng)增加因?yàn)槟P涂吹降淖兓嗍諗繒?huì)慢一些。5.3 模型保存與復(fù)現(xiàn)怎么保證下次還能跑出同樣結(jié)果深度學(xué)習(xí)項(xiàng)目最頭疼的是「這次跑通了下次跑結(jié)果不一樣」。要保證可復(fù)現(xiàn)三件事必須做固定隨機(jī)種子、保存模型權(quán)重和優(yōu)化器狀態(tài)、記錄超參數(shù)。隨機(jī)種子固定import torch import numpy as np import random 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 Falsecudnn.deterministic True會(huì)讓訓(xùn)練慢一點(diǎn)但結(jié)果可復(fù)現(xiàn)。保存模型時(shí)除了state_dict最好把 epoch 數(shù)、優(yōu)化器狀態(tài)、當(dāng)前 loss 一起存方便斷點(diǎn)續(xù)訓(xùn)。我一般會(huì)存成字典torch.save({epoch: epoch, model: model.state_dict(), optimizer: optimizer.state_dict(), loss: loss}, checkpoint.pth)。這樣即使訓(xùn)練中斷下次加載還能接著跑不用從頭再來。5.4 一個(gè)具體技巧用混淆矩陣定位難分類別評(píng)估時(shí)別只看一個(gè)準(zhǔn)確率數(shù)字。把混淆矩陣畫出來找出錯(cuò)得最多的那幾類針對(duì)性處理。比如限速 50 和限速 60 混淆嚴(yán)重可以把這兩類的訓(xùn)練樣本單獨(dú)拎出來做增強(qiáng)或者提高輸入分辨率讓數(shù)字區(qū)域更清晰。我自己的習(xí)慣是每次訓(xùn)練完都跑一遍classification_report把 f1-score 低于 0.9 的類記下來下一輪重點(diǎn)看。這個(gè)習(xí)慣幫我省了很多盲目調(diào)參的時(shí)間。從那以后我每次跑完訓(xùn)練都強(qiáng)制走一遍混淆矩陣不看一眼不放心。希望這份源碼包能幫你把交通標(biāo)志識(shí)別這個(gè)項(xiàng)目真正跑起來而不是停在「下載了但沒打開」的狀態(tài)。本文還有配套的精品資源點(diǎn)擊獲取