:基于深度卷積神經網絡與CRNN的端到端方案)
簡介面向深度學習、機器學習、計算機視覺與模式識別領域的研究者和學生這份PDF專業(yè)文獻聚焦自然場景中街景門牌號識別難題。針對背景復雜、字符風格多樣、人工特征難以適用的問題作者基于AlexNet改進網絡結構加深網絡深度并增加卷積核數量在激活函數后引入批歸一化BN于全連接層采用低比例Dropout策略同時將圖像灰度化以弱化背景和光照干擾。在SVHN街景門牌號數據集上該網絡訓練約13小時識別率達94.58%在精度與訓練成本之間實現了良好平衡。資源為單篇PDF共1個文件壓縮包僅1.78MB包含論文全文、網絡結構圖、實驗對比數據及參考文獻可完整還原從數據預處理、網絡設計到實驗驗證的整個研究過程。文中還梳理了CNN、深度學習、AlexNet、BN、Dropout、OCR、LeNet-5及GPU加速等關鍵知識點適合作為課程設計、科研入門或算法復現的參考資料。目前已有480人學習下載是相關領域學習者值得參考的專業(yè)資源。1. 街景門牌號識別為什么深度學習比傳統(tǒng)OCR更靠譜街景門牌號識別說白了就是讓算法看懂街景照片里那串門牌數字。過去用傳統(tǒng)OCR在干凈掃描件上還能跑一遇到傾斜、模糊、反光、遮擋的實拍門牌就翻車——閾值分割對光照敏感模板匹配扛不住形變字符切分在粘連數字面前直接啞火?;谏疃染矸e神經網絡的街景門牌號識別方法把特征提取、序列建模和字符映射端到端塞進同一個網絡在SVHN這類基準集上通常能到95%以上的準確率關鍵是它對真實街景的退化魯棒性要好得多。這篇筆記不講論文復現的虛話直接從任務拆解、模型搭建、訓練調參到落地加速把每一步的參數和坑都擺出來適合正在做車牌識別、門牌識別、快遞單號識別這類實拍數字識別項目的工程師和學生。2. 門牌號識別任務拆解從SVHN到真實街景的差異2.1 先搞清SVHN數據集與真實街景的邊界SVHNStreet View House Number是從谷歌街景里裁剪出來的門牌號圖片每張圖含有1到5位數字。它的標注是每個數字的邊界框和類別標簽所以很多人把它當多目標檢測或分類任務來做。但要注意SVHN里的圖片已經經過預處理門牌號大致在畫面中心背景干擾相對可控光照分布也相對均勻。真實街景完全不同——門牌可能出現在畫面角落被樹枝或車燈遮擋字體可能是藝術字墻面反光、陰影、夜間照明都會讓數字紋理徹底改變。所以嚴格來說門牌號識別在工程上分兩條路線一條是檢測識別級聯(lián)先用目標檢測把門牌區(qū)域框出來再對區(qū)域做數字識別另一條是端到端序列識別直接輸入整張街景圖輸出數字序列。后者對數據量和模型容量的要求更高但省去了檢測框的誤差傳遞。我一般建議如果你手里的街景圖是全景掃描件門牌占比小且背景復雜優(yōu)先用檢測識別如果門牌已經裁切好或者門牌在畫面中占據主要區(qū)域直接上端到端序列識別更省事。下面的代碼是常見的數據預處理從SVHN的mat格式標注中提取數字序列和bounding box并生成訓練用的圖片列表。SVHN官方提供digitStruct.mat里面存了每個樣本的bbox和label用Python讀出來轉成自己的格式。import h5py import numpy as np from pathlib import Path # 讀取SVHN的digitStruct.mat # 該文件結構每個樣本有name, bbox包含height,left,top,width,label def load_svhn_labels(mat_path): with h5py.File(mat_path, r) as f: names f[digitStruct][name] bboxes f[digitStruct][bbox] records [] for i in range(len(names)): # 逐樣本解析處理單個數字和多個數字的存儲差異 name .join(chr(c) for c in f[names[i]][()].flatten()) bbox_group f[bboxes[i]][()].flatten() bbox_list [] for item in bbox_group: if item.ndim 0: # 單個數字時是標量 item np.array([item]) for j in item: bbox_list.append({k: f[j][k][()].reshape(-1)[0] for k in [height,left,top,width,label]}) labels [int(b[label]) % 10 for b in bbox_list] # 10代表0 records.append({name: name, labels: labels}) return records records load_svhn_labels(digitStruct.mat) print(f樣本總數: {len(records)}, 示例標簽: {records[0]})這段代碼的邏輯是h5py讀取SVHN官方mat文件names和bboxes都是對象引用需要逐級解除引用。特別注意單個數字和多個數字的存儲結構不同ndim 0時得手動包一層數組。label里用10表示數字0取??梢赃€原成0-9。參數說明mat_path是digitStruct.mat的路徑返回的records列表里每個元素包含原始文件名和數字序列后續(xù)按name去圖片目錄里找對應png即可。2.2 門牌號識別是序列標注問題不是簡單分類對一張包含 502 三個數字的門牌圖如果只做單標簽分類你得預定義所有可能的組合這顯然不現實。正確做法是把它當序列標注輸入圖像輸出一個不定長的數字序列。常用的建模方式有兩種CTCConnectionist Temporal Classification和Attention。CTC不需要逐字符對齊標注只需要最終的序列標簽所以訓練省事Attention需要在訓練時讓模型逐步生成字符解碼更靈活但容易漂移。工程上我傾向CTC尤其數字序列長度不超過5位時CTC的穩(wěn)定性和速度都更優(yōu)。既然要做序列識別圖像輸入就要有序列的形態(tài)。常見做法是把圖片縮放成固定高度比如32寬度按比例縮放但不超過某個上限比如256然后沿著寬度方向把特征圖切成一列一列的時間步。深度卷積神經網絡在這里的作用是提取每列的特征向量再用循環(huán)網絡BiLSTM建模列與列之間的上下文關系最后接入CTC損失。這個過程可以用一個公式概括CNN提取視覺特征 - 特征圖按列展開成序列 - BiLSTM建模列間依賴 - CTC解碼出數字串。在動手寫模型之前先要把訓練數據整理成統(tǒng)一格式。下面是一個輕量的數據加載器把圖片高度固定到32寬度padding到固定值同時生成對應的標簽序列和序列長度。import torch from torch.utils.data import Dataset from torchvision import transforms from PIL import Image class HouseNumberDataset(Dataset): def __init__(self, records, img_dir, fixed_height32, max_width128): self.records records self.img_dir img_dir self.fixed_height fixed_height self.max_width max_width self.tf transforms.Compose([ transforms.Resize((fixed_height, max_width)), # 高度固定寬度強制縮放 transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ]) self.char_map {str(i): i1 for i in range(10)} # 0-9映射到1-100位留給CTC blank def __len__(self): return len(self.records) def __getitem__(self, idx): rec self.records[idx] img Image.open(f{self.img_dir}/{rec[name]}).convert(RGB) img self.tf(img) labels [self.char_map[str(d)] for d in rec[labels]] label_len len(labels) return { img: img, labels: torch.tensor(labels, dtypetorch.long), label_len: label_len }這里有一個容易被忽略的參數Resize((fixed_height, max_width))直接把所有圖壓成 32x128。這樣做的好處是張量形狀統(tǒng)一方便batch訓練壞處是長寬比失真尤其把細長門牌橫向拉寬后數字會變形。更穩(wěn)的做法是保持高度32寬度按原圖比例等比縮放不夠max_width的部分用0填充右邊加padding這個坑我們放到第4章詳細說。char_map里用0作為CTC的blank符號所以數字1-9映射到1-9數字0映射到10這樣模型輸出通道數設為1110個數字1個blank。3. 搭建深度卷積神經網絡模型選型與訓練配置3.1 從LeNet到CRNN門牌號識別的主流模型骨架深度卷積神經網絡在門牌號識別上的進化路線很清晰最早是LeNet這種淺層CNN直接輸出數字類別只適合單數字后來SVHN比賽帶火了多數字識別大家開始用多個CNN分支或滑動窗口再后來序列識別思路普及CNNRNNCTC的組合成為標配也就是常說的CRNN結構。對街景門牌號來說CRNN比純CNN多了一個關鍵能力它能建模數字之間的順序關系。比如 12 和 21純CNN把整張圖當成一個整體特征容易混淆CRNN通過時間序列把左右位置的自然順序編碼進特征里準確率明顯提升。卷積層怎么選我在實際項目里用過ResNet18、VGG16和MobileNetV3做特征提取結論是參數規(guī)模不是第一位的感受野和特征圖寬度才是。門牌數字筆畫細卷積核不能太大3x3足夠網絡深度至少要有4個下采樣把32高圖片縮到2x2或1x1的feature map否則序列長度不夠。推薦一個實用的backbone配置4個卷積階段每階段兩個3x3卷積加一個2x2 maxpool通道數從64翻倍到512。這樣輸入32x128的圖最終特征圖是4x32高4寬32序列長度就是32。循環(huán)層用BiLSTM還是GRU門牌號序列短用雙向LSTM一層或兩層都夠。隱藏單元數一般取256或512。如果怕過擬合可以加dropout。關鍵參數是batch_firstTrue這樣tensor形狀是(batch, seq_len, feature_dim)方便和CNN輸出對齊。CTC損失函數在PyTorch里是torch.nn.CTCLoss需要傳入三個東西模型的logits、目標標簽序列、每個樣本的序列長度和目標長度。3.2 最小可復現訓練腳本數據加載、損失函數與優(yōu)化器下面這段代碼是一個完整的CRNN訓練循環(huán)PyTorch版注釋里標了每個參數的推薦值和調整方向。它可以直接跑通SVHN數據集的子集但你要把它當成模板根據自己的數據規(guī)模改batch size和迭代輪數。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader class CRNN(nn.Module): CNN BiLSTM CTC 的門牌號識別網絡 def __init__(self, num_classes11, hidden_size256): super().__init__() # 卷積特征提取輸入3x32x128輸出512x4x32 self.cnn nn.Sequential( nn.Conv2d(3, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 16x64 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 8x32 nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, (2, 1)), # 池化時不縮寬度保持序列長度 4x32 ) self.lstm nn.LSTM(256*4, hidden_size, bidirectionalTrue, num_layers2, batch_firstTrue, dropout0.3) self.fc nn.Linear(hidden_size*2, num_classes) def forward(self, x): x self.cnn(x) # [B, 256, 4, 32] b, c, h, w x.size() x x.view(b, c*h, w) # 合并高度和通道變成 [B, 1024, 32] x x.permute(0, 2, 1) # [B, 32, 1024] 序列長度32 x, _ self.lstm(x) # [B, 32, 512] x self.fc(x) # [B, 32, 11] return x # logits后面接CTCLoss # 訓練參數設置 batch_size 32 epochs 20 learning_rate 1e-3 device torch.device(cuda if torch.cuda.is_available() else cpu) model CRNN(num_classes11, hidden_size256).to(device) optimizer optim.Adam(model.parameters(), lrlearning_rate, weight_decay1e-5) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) criterion nn.CTCLoss(blank0, zero_infinityTrue) # 假設 dataset 是上一節(jié)的 HouseNumberDataset dataloader DataLoader(dataset, batch_sizebatch_size, shuffleTrue, num_workers4, collate_fncollate_fn) for epoch in range(epochs): model.train() total_loss 0 for batch in dataloader: imgs, targets, target_lengths batch[img].to(device), batch[labels], batch[label_len] logits model(imgs) # [B, T, C] log_probs logits.log_softmax(2).permute(1, 0, 2) # CTC需要[T, B, C] input_lengths torch.full((batch_size,), logits.size(1), dtypetorch.long) loss criterion(log_probs, targets, input_lengths, target_lengths) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 5.0) # 梯度裁剪防止LSTM梯度爆炸 optimizer.step() total_loss loss.item() scheduler.step() print(fEpoch {epoch1}/{epochs}, Loss: {total_loss/len(dataloader):.4f})這段代碼的邏輯拆解如下CRNN前向時CNN輸出特征圖是[B, C, H, W]其中H4W32。為了把特征圖轉成序列把C和H合并成一個維度得到[B, 1024, 32]再permute成[B, 32, 1024]這樣32個時間步每步1024維特征。BiLSTM雙向輸出是512維hidden_size256雙向翻倍最后接線性層映射到11類。幾個參數值得單獨說。nn.MaxPool2d(2, (2, 1))是故意讓高度縮減一半、寬度不變因為高度從8縮到4寬度保持32這樣序列長度仍是32。如果你把寬度也縮了比如變成16序列長度就太短數字密集時會丟信息。CTCLoss的blank0必須和char_map里為blank預留的位置一致否則解碼會錯位。zero_infinityTrue的作用是當loss出現inf比如某樣本目標長度大于輸入長度時置零避免訓練崩潰但這種情況應該靠數據檢查杜絕。collate_fn沒有寫全實際需要把不同長度的標簽序列pad到同一長度并記錄真實長度。常見做法是用torch.nn.utils.rnn.pad_sequence加上左邊padding并在標簽前插入一個值比如-1供CTCLoss忽略。這里不再展開第4章會提到標簽對齊的坑。學習率策略建議用warmup余弦退火。上面的腳本里直接用余弦退火但前幾個epoch可能因為學習率過大導致振蕩。一個更穩(wěn)的做法是前3個epoch用線性warmup從1e-4升到1e-3之后再按余弦衰減。優(yōu)化器選Adam沒問題但weight_decay別太大1e-5足夠太大會把卷積核的權重壓得過小。batch size在顯存允許范圍內越大越好但BatchNorm的參數會受影響建議用32到64之間。4. 訓練與調參避坑5條真實踩坑記錄4.1 現象驗證集loss不降精度卡在80%上不去最常見的原因是學習率設置不當。我見過有人用默認的1e-3跑SVHN一開始loss下降很快到了第5個epoch就開始震蕩驗證集準確率一直在80%左右徘徊。查下來發(fā)現是學習率太大模型在最優(yōu)解附近來回跳躍。另一個原因是數據歸一化不一致訓練時用ImageNet的mean/std但推理時忘了轉成相同預處理。解決方法是先跑兩三個epoch觀察loss曲線。如果loss在初始值附近波動劇烈把學習率降到1e-4或1e-5。如果loss下降極慢說明學習率太小適當上調。更系統(tǒng)的方式是使用學習率預熱前三個epoch用1/10的目標學習率然后線性漲到目標值之后自然衰減。另外固定一套預處理pipeline訓練和推理都使用完全相同的Resize和Normalize參數不要混用。4.2 現象模型永遠輸出空序列或只輸出重復數字CTC訓練時很容易出現模型傾向輸出blank字符導致預測結果全是空的。尤其當訓練集中很多圖片只有一個數字而blank通道的概率遠大于數字通道時模型學到的捷徑就是全輸出blank。另一個表現是輸出一串相同的數字比如111而不是12。原因是BiLSTM的時間步之間相關性過強或者梯度在時間維度上傳播不均勻。我當時的處理辦法有三步第一步檢查標簽字符映射確認blank索引是0且數字標簽從1開始第二步在損失函數上動刀CTCLoss的blank參數不能錯同時可以用一個簡單的正則項懲罰blank概率的平均值過大第三步調整模型結構把BiLSTM的隱藏單元數從512降到256增加dropout強迫模型不過度依賴時序記憶。還有一個經驗如果訓練數據里不同長度的樣本分布不均盡量按長度分batch避免一個batch里全是單數字樣本。4.3 現象訓練時隨機裁剪效果好但推理時結果變差我踩過最深的坑是數據增強和推理預處理不一致。訓練時我用了RandomResizedCrop把門牌號隨機縮放裁剪使得模型對尺度變化更魯棒但推理時我用的是直接Resize((32, 128))把整張圖壓扁。結果訓練指標93%推理時面對同樣來源的圖片只有85%差了一大截。原因很簡單模型學到的特征對長寬比和絕對位置敏感推理時破壞了這種分布。解決方法是讓推理和訓練的尺寸策略統(tǒng)一。要么訓練時也強制所有圖片Resize((32, 128))放棄尺度增強要么推理時保持高度32寬度按比例縮放后padding到128。我推薦后者寫一個inference_transform先用Resize只傳高度再手動計算寬度并pad這樣既保持了寬高比也兼容了batch輸入。另外對門牌這種小字符水平翻轉增強千萬別用數字翻轉后語義全變。4.4 現象整圖輸入模型小門牌根本認不出來如果輸入圖片是整條街景門牌號只占幾十個像素上面那種整圖識別模型基本會翻車。因為卷積網絡下采樣后小目標特征幾乎丟失。有人試過把輸入分辨率從32x128調成64x256精度提升有限顯存卻翻倍。正確的做法是先用目標檢測定位門牌區(qū)域再對裁剪區(qū)域做識別。檢測器可以用YOLO或Faster R-CNN門牌類別的anchor長寬比偏扁需要調整anchor尺寸。我做過一個方案用YOLOv8檢測門牌置信度閾值設0.3NMS IoU閾值設0.5把檢測框擴10%再送CRNN識別。這樣整圖識別準確率從70%漲到91%。代價是兩階段延遲大約多15ms但收益明顯。如果你不想引入檢測器可以嘗試在CNN中使用特征金字塔FPN讓淺層高分辨率特征圖也能接觸到最后分類但實現復雜度會上升不一定劃算。4.5 現象SVHN上表現很好換真實街景數據就崩這是所有公開數據集訓練的宿命SVHN雖然來自街景但已經過裁剪和篩選背景多樣性遠小于真實場景。我的真實測試集里有夜間長曝光、玻璃反光、綠色藤蔓遮擋、傾斜角度接近45°的門牌模型直接崩潰。這說明模型學到的特征是“SVHN風格”的不是通用的數字紋理。后悔藥有三丸。第一丸是收集至少500張真實街景圖片用半自動標注檢測模型預標記人工修正加入訓練集做finetune。第二丸是合成數據增強找一批不同風格的字體隨機旋轉、加模擬陰影、貼到真實街景背景上生成大量偽樣本。第三丸是使用灰度圖或邊緣圖作為額外輸入通道減少對顏色的依賴。我最后靠75%真實25%合成數據把真實街景的精度從62%拉到了84%。別指望模型自己泛化數據上必須下功夫。5. 從單張圖片到街景視頻流推斷加速與后處理技巧門牌號識別在實際產品里往往要處理連續(xù)視頻流而不是單張靜態(tài)圖。我的經驗是先做batch推斷再把同一門牌在多幀中的識別結果做時序融合。CTC解碼時常用貪婪搜索每幀輸出一個數字序列但街道上車輛角度變化單幀可能截到殘缺門牌。一種輕量做法是滑動窗口收集最近5幀的識別結果對每個位置的數字做投票票數最高且超過3票才輸出否則等待下一幀。這個技巧能把誤檢率降低一半代價只是延遲5幀。模型加速方面CRNN的瓶頸通常在BiLSTM因為LSTM是串行計算。可以把它替換成BiGRU或直接用因果卷積TCN推理速度能快一截。如果精度損失可接受可以把模型導出成ONNX用TensorRT或OpenVINO做FP16量化。我在Jetson Nano上把CRNN從TensorFlow轉到TensorRT后單幀推斷從45ms降到18ms顯存占用也降了一半。注意量化時對BatchNorm層的處理最好是先折疊BN再量化不然誤差會放大。驗證模型是否真正落地我有個習慣不看單張圖準確率而是錄一段真實街景視頻跑一個端到端demo統(tǒng)計“門牌號連續(xù)5幀識別一致”的比例。這個比例比單幀精度更能反映產品體驗。另外別忘了做置信度校準——CTC輸出的概率往往過于自信稍微加一個溫度系數temperature scaling會讓閾值調節(jié)更靠譜。最后說個教訓別急著優(yōu)化模型結構先把數據管線搞干凈。我見過太多人花一個月調網絡最后發(fā)現是訓練和推理的預處理不一致白白浪費時間。希望這篇筆記能幫你少走這幾步冤枉路。本文還有配套的精品資源點擊獲取