天池CV学习赛:街景字符识别-思路与上分技巧汇总
Datawhale 和 天池 合作的零基礎入門CV - 街景字符編碼識別比賽的正式賽已經結束。本文對一些比賽思路和上分技巧進行了匯總和整理,希望對大家深入學習CV能夠有幫助。
本文分為以下幾部分:
-
如何優化官方baseline的效果?
-
其它解題思路的整理和分析
-
字符級目標檢測的優化技巧整理
在這里要特別感謝多位前排選手對于比賽技巧的無私分享,那么不多bb,下面直接進入正題
一、如何優化官方baseline的效果?
本次入門賽的官方baseline入門材料,相信大家肯定都看過了:
Task1 賽題理解
Task2 數據讀取與數據擴增
Task3 字符識別模型
Task4 模型訓練與驗證
Task5 模型集成
本質上,baseline的思路就是將賽題轉換為了一個定長的字符識別問題,用包含多個輸出的分類問題來進行求解。
1.1 改進版baseline
那么如何進行進一步優化呢?在比賽進行的過程中,我在天池進行了一次如何調參上分的直播分享
直播對應的代碼可以在我們的動手學CV項目的2.5節找到
這份代碼相當于一個加強版的baseline,簡短來說,介紹了以下幾點:
- 重新回顧baseline的代碼
- 階段性下降的學習率調整策略
- 分析了很多人提交出0.3-0.4分成績的原因和解決方案
- 加入數據增強策略
這份代碼我相信是幫到了一些剛入門的同學的,提交的成績大概在0.75分左右。
那么在這樣一個baseline的基礎上,如何進一步的優化呢?
1.2 改進backbone
baseline中我們的網絡結構是這樣定義的:
class SVHN_Model1(nn.Module):def __init__(self):super(SVHN_Model1, self).__init__()model_conv = models.resnet18(pretrained=True)model_conv.avgpool = nn.AdaptiveAvgPool2d(1)model_conv = nn.Sequential(*list(model_conv.children())[:-1]) # 去除最后一個fc layerself.cnn = model_convself.fc1 = nn.Linear(512, 11)self.fc2 = nn.Linear(512, 11)self.fc3 = nn.Linear(512, 11)self.fc4 = nn.Linear(512, 11)self.fc5 = nn.Linear(512, 11)def forward(self, img): feat = self.cnn(img)#print(feat.shape)feat = feat.view(feat.shape[0], -1)c1 = self.fc1(feat)c2 = self.fc2(feat)c3 = self.fc3(feat)c4 = self.fc4(feat)c5 = self.fc5(feat)return c1, c2, c3, c4, c5我們可以對使用的backbone網絡進行一系列的改進:
- 由resnet18換為更大的resnet50
- 為每一個分類模塊加上一層全連接隱藏層
- 為隱含層添加dropout
由resnet18換為resnet50,更深的模型就擁有更好的表達能力,添加一層隱含層同樣起到了增加模型擬合能力的作用,與此同時為隱含層添加dropout來進行一個balance,一定程度上防止過擬合。(這只是我個人對于baseline的改進方案,不一定是最優的)
改進后的模型定義代碼如下:
class SVHN_Model2(nn.Module):def __init__(self):super(SVHN_Model2, self).__init__()# resnet18model_conv = models.resnet18(pretrained=True)model_conv.avgpool = nn.AdaptiveAvgPool2d(1)model_conv = nn.Sequential(*list(model_conv.children())[:-1]) # 去除最后一個fc layerself.cnn = model_convself.hd_fc1 = nn.Linear(512, 128)self.hd_fc2 = nn.Linear(512, 128)self.hd_fc3 = nn.Linear(512, 128)self.hd_fc4 = nn.Linear(512, 128)self.hd_fc5 = nn.Linear(512, 128)self.dropout_1 = nn.Dropout(0.25)self.dropout_2 = nn.Dropout(0.25)self.dropout_3 = nn.Dropout(0.25)self.dropout_4 = nn.Dropout(0.25)self.dropout_5 = nn.Dropout(0.25)self.fc1 = nn.Linear(128, 11)self.fc2 = nn.Linear(128, 11)self.fc3 = nn.Linear(128, 11)self.fc4 = nn.Linear(128, 11)self.fc5 = nn.Linear(128, 11)def forward(self, img):feat = self.cnn(img)feat = feat.view(feat.shape[0], -1)feat1 = self.hd_fc1(feat)feat2 = self.hd_fc2(feat)feat3 = self.hd_fc3(feat)feat4 = self.hd_fc4(feat)feat5 = self.hd_fc5(feat)feat1 = self.dropout_1(feat1)feat2 = self.dropout_2(feat2)feat3 = self.dropout_3(feat3)feat4 = self.dropout_4(feat4)feat5 = self.dropout_5(feat5)c1 = self.fc1(feat1)c2 = self.fc2(feat2)c3 = self.fc3(feat3)c4 = self.fc4(feat4)c5 = self.fc5(feat5)return c1, c2, c3, c4, c5改進后的模型訓起來會慢很多,不過增加了模型的表達力,自然效果也會好一些。此外,你也可以嘗試一些更"state-of-the-arts"的模型,比如SENet,EfficientNet等。
1.3 數據增強優化
關于數據增強,我們在直播中已經探討過了,從原理上分析我們更應該用一些基于空間、位置相關的數據增強,比如Randomcrop,平移,旋轉等。而顏色空間相關的變換,也可以嘗試,但很可能會起到副作用。
數據增強是非常普遍的訓練技巧了,肯定要用,但對這個賽題的結果提升不會很顯著。關于這部分,這位小伙伴寫的比賽實驗記錄對相關的實驗進行了很詳細的記錄,大家感興趣可以閱讀一下~
1.4 和文本長度相關的探索
baseline方案將識別問題轉化為了定長識別問題,那么定多長合適?就是個值得思考的問題,有的小伙伴通過一個小腳本進行了統計,訓練集中的樣本的字符長度分別為1,2,3,4,5,6的樣本數量分別為4636,16262,7813,1280,8,1。
可以看到,數據集中長度為5和6的圖片都是可以忽略不計的,因此主動放棄這部分極少數情況的case可以很好的為模型“減負”,從而獲得更好的效果。
baseline模型設定定長len=5,不妨嘗試下len=4,會帶來進一步的提升。
我們的這個方案需要CNN自己找到文字在圖中的位置,還要自己判斷出字符的個數并完成正確的識別,感覺上是相當難的任務,之所以能夠work是因為場景相對來說比較簡單。
如果你做了一些更進一步得分析工作你會發現,模型對于長度不同的圖片的效果是不一樣的。更詳細來說,根據我的訓練日志的記錄,訓練時第3個字符的loss是相對比較小的,而預測時長度為3的圖片出現的預測錯誤是比較多的,主要是漏檢。
我分析這是因為(個人觀點可能存在誤導):對于輸出第一個字符結果的fc layer來說,所有圖片它都會參于訓練,而輸出第二個字符結果的fc layer,只有當圖片字符數>=2,它才會參于“有效”訓練,以此類推。所以越是對應靠后的fc layer,訓練的越不充分,越容易過擬合導致實際效果越差。
因此可以圍繞不同長度的圖片效果不同來做些文章,依然是這篇比賽實驗記錄
他嘗試了兩個方案,可供大家參考,提供些思路:
- 損失加權
- 樣本加權
其中第二個方案,樣本加權看起來是更合理來解決這個問題的,我們可以通過重復采樣,提高較長的字符數量的圖片出現的比例,來讓對應第3個字符和第4個字符對應的輸出層訓練的更充分。
這是個蠻有新意的角度,你是否還可以碰撞出其它的idea來解決這個問題呢~
1.5 集成學習
對于這個比賽來說,比較適合baseline的集成方案是:
首先將單模型盡可能訓到最高,然后單模型的輸出使用TTA(test time augmentation),可以大幅提升單模型的預測效果。
然后訓練多個有差異化的模型,將多個單模型的預測結果進行投票,得到最終的預測結果。單模型間的差異化可以從數據的角度通過重新劃分訓練集和驗證集來達到,也可以從模型的角度,使用不同的backbone來達到。從原理上講,單模型間越是獨立和具有差異化,融合的效果就越好。
1.6 讓baseline進入Top 2%的6行代碼
最后再偷偷告訴你一個,只需修改6行代碼,就能讓目前的優化后baseline單模型進入Top 2%的方法。
不知大家有沒有發現,測試集相比于訓練數據,要更簡單。
具體地,體現在測試集最終的分數反而要比驗證集高一些,如果你直接觀察數據,也可以看出來,測試集的圖片中字符在圖片中的占比更大,而訓練集中圖片的字符占比更小。直觀感受就是訓練數據和測試集對應的場景不一致,訓練集的字符感覺更“遠”,預處難度也更高。
提示到這里,你可以先停下來思考下,如果是你,會如何來針對這個問題進行優化呢?
驗證集圖片示例:
測試集圖片示例:
對于這個問題,很多人很自然的想到了進行目標檢測,這也是幾乎所有前排選手的一致選擇。但是,我們的baseline就沒有一戰之力了嗎?當然不是,我個人用resnet50作為backbone將單模型的分數訓到了0.91,相當于正式賽Top2%的分數,而且因為我沒有時間做太多的超參數調整實驗,這個成績也并沒有達到baseline的上限。
那么baseline要如何相應的進行改造來解決這個問題呢?
文字描述出來就是:訓練時把場景拉近,測試基本保持不變,這樣一定程度上讓訓練和測試的數據的場景更加一致,從而讓模型學到的預測能力完全發揮出來。
可以有很多方案來達到這個目的,最簡單有效的方法僅僅需要修改數據增強相關的6行代碼,我用TODO作為后綴標注出來,代碼如下:
train_loader = torch.utils.data.DataLoader(SVHNDataset(train_path, train_label,transforms.Compose([transforms.Resize((80, 160)), # TODOtransforms.RandomCrop((64, 128)), # TODOtransforms.ColorJitter(0.3, 0.3, 0.2),transforms.RandomRotation(5),transforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])),batch_size=40,shuffle=True,num_workers=2, ) val_loader = torch.utils.data.DataLoader(SVHNDataset(val_path, val_label,transforms.Compose([transforms.Resize((80, 160)), # TODOtransforms.CenterCrop((64, 128)), # TODOtransforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])),batch_size=40,shuffle=False,num_workers=2, ) test_loader = torch.utils.data.DataLoader(SVHNDataset(test_path, test_label,transforms.Compose([transforms.Resize((68, 136)), # TODOtransforms.RandomCrop((64, 128)), # TODOtransforms.ToTensor(),transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])),batch_size=40,shuffle=False,num_workers=2, )這個優化雖然很trick,但是效果顯著,大家可以思考下這6行代碼的修改是如何帶來成績的巨大提升的。
二、不同解題思路的整理與分析
除了baseline的思路以外,還有多種不同的解題思路,這里簡單進行總結。
2.1 CRNN
純識別的思路,除了baseline的定長字符識別方案外,還可以用CRNN來做,屬于一種端到端的不定長字符識別的解決方案。
關于CRNN,賽事組織者 阿水 已經為我們提供了一個CRNN baseline,感謝去可以學習一下~
2.2 檢測+識別
除了兩種端到端的識別方案,還可以引入目標檢測來解題,根據具體使用中檢測框粒度的不同,還可以細分為三種不同的方案:
方案一:文本行檢測+文本行識別
方案二:字符級目標檢測+字符識別模型
方案三:純目標檢測方案
根據我最近兩周持續對前排選手進行的騷擾+在線乞討收集到的情報來看,前排選手使用的普遍是方案三,但是方案一、方案二也有人用,而且成績不差。也就是說,對于這個賽題,從實際結果上看這三個方案上限差不多。
方案一是更標準的字符識別類問題的解決方案,如果我們的問題不是數字之間無關聯的門牌號識別,而是比如場景文字識別,那么方案一由于可以對不同字符間的關聯進行建模,效果將會顯著優于其它方案,但是本賽題這種優勢無法發揮出來。
而方案三作為一種端到端的解決方案,思路更直接,整個訓練流程更簡單,更容易在有限的比賽時間內優化出好的效果,再加上有眾多簡單好用的開源庫,因此也是絕大多數前排選手選擇的原因。
三、字符級目標檢測的優化技巧整理
本文的最后一部分,再針對大家使用最多的字符級目標檢測的方案,進行一些簡單的整理。
網絡框架選擇方面,前排普遍采用的是YOLOv3-v5的版本,還有一位選手使用的CenterNet獲得了非常好的效果。
除了模型訓練的各種小的trick以外,如何對模型結果進行后處理,以及如何融合多個模型的結果,會對最終結果有很大影響。關于這部分,眾多選手都在天池的論壇熱心分享了自己的經驗,細節太多很難一一列舉,我也有很多要學習的地方,這里就不班門弄斧了,感興趣的小伙伴趕快去學習吧~
天池街景字符識別總結
第五名 yolov4 加 投票法方案
街景字符編碼識別-第6名 線上0.938 方案分享
yolov5加全局nms 第八名方案分享
零基礎入門CV賽事-分享一些適合新手的0.92+的上分技巧吧~
真正零基礎,單模型非融合,上93的最簡單技巧
參賽歷程以及方案分享
零基礎CV賽–街景字符識別,小小白分享,從0.002~0.926
寫在最后
如果覺得有收獲,可否給我們的 動手學CV 項目點個star呢,我的老火雞~
總結
以上是生活随笔為你收集整理的天池CV学习赛:街景字符识别-思路与上分技巧汇总的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: STM32F0使用LL库实现MS5536
- 下一篇: leetcode No.15-16 三数