典CNN在滿文識(shí)別中的實(shí)戰(zhàn):從數(shù)據(jù)集構(gòu)造到泛化陷阱)
簡(jiǎn)介面向滿文文字識(shí)別與計(jì)算機(jī)視覺研究者的圖像分類資源以666類多字體滿文單詞圖像為基礎(chǔ)圍繞AlexNet、VGG19、GoogLeNet三種經(jīng)典深度網(wǎng)絡(luò)給出完整的模型構(gòu)建、訓(xùn)練和可視化分析過程。整個(gè)資料包約220.5MB共2000個(gè)文件其中jpg圖像占據(jù)主體另有9個(gè)Python腳本用于數(shù)據(jù)加載與模型對(duì)比1份PPTX展示文稿匯總了實(shí)驗(yàn)結(jié)果并含簡(jiǎn)要說明文檔結(jié)構(gòu)清晰便于按需查閱。目前已有224人學(xué)習(xí)下載適合作為OCR方向課程設(shè)計(jì)或科研入門的參考樣例。學(xué)習(xí)者可從腳本中看到數(shù)據(jù)預(yù)處理、網(wǎng)絡(luò)微調(diào)和特征可視化的具體寫法從PPTX中獲取不同模型的準(zhǔn)確率對(duì)比與可視化結(jié)果從而快速復(fù)現(xiàn)滿文單詞識(shí)別實(shí)驗(yàn)深入理解多模型遷移學(xué)習(xí)在少數(shù)民族文字識(shí)別任務(wù)中的實(shí)際表現(xiàn)。 去年年初接過一個(gè)公共文化數(shù)字化方向的圖像識(shí)別需求館藏有一批滿文資料需要盡快轉(zhuǎn)成可檢索的文本。我原本以為這類拼音文字識(shí)別不算太難上手之后才發(fā)現(xiàn)滿文這個(gè)任務(wù)的麻煩程度遠(yuǎn)超預(yù)期。滿文是拼音文字一個(gè)詞由多個(gè)音素字母橫向連寫而成字母在詞首、詞中、詞尾的位置不同寫法也會(huì)跟著變化視覺上很像阿拉伯文的連寫形態(tài)。更棘手的是手上的數(shù)據(jù)是多種字體混合的——古籍刻本、鉛印本、手寫抄本、現(xiàn)代字庫渲染都有字形風(fēng)格差異極大。于是我們把問題拆成了最樸素的圖像分類任務(wù)滿文數(shù)據(jù)集666類中的多種字體單詞圖像分別用AlexNet、VGG19、GoogLeNet三個(gè)經(jīng)典CNN去識(shí)別。這篇文章就復(fù)盤一下從數(shù)據(jù)集構(gòu)造、模型選型到訓(xùn)練排錯(cuò)的完整過程尤其適合正在做低資源文字識(shí)別、冷門OCR或小樣本圖像分類的讀者參考。1. 為什么拿三個(gè)“老牌CNN”去啃滿文單詞識(shí)別1.1 滿文識(shí)別到底難在哪先說文字本身。滿文屬于音素文字系統(tǒng)常見說法是6個(gè)元音字母加22個(gè)輔音字母共28個(gè)基礎(chǔ)音位。但這只是字母層面的數(shù)量落到字形上同一個(gè)字母在詞首、詞中、詞尾往往有獨(dú)立寫法學(xué)界對(duì)滿文字形變體的統(tǒng)計(jì)普遍認(rèn)為超過130個(gè)。也就是說模型看到的不是28個(gè)簡(jiǎn)單符號(hào)而是130多種局部形狀這些形狀還要按詞內(nèi)順序橫向連寫組合出成百上千種整體外形。滿文還有個(gè)綽號(hào)叫“圈點(diǎn)滿文”——大量字母之間的區(qū)分全靠一個(gè)圈或一個(gè)點(diǎn)的位置差異。比如某些輔音字母唯一區(qū)別是右側(cè)有沒有圈、圈點(diǎn)在哪個(gè)位置。這種級(jí)別的細(xì)節(jié)放到古籍掃描件里就是災(zāi)難紙張泛黃、筆畫斷裂、圈點(diǎn)模糊人眼有時(shí)候都得湊近看。數(shù)據(jù)里再混入手寫體和印刷體同一個(gè)詞的筆順、連筆、筆畫粗細(xì)又會(huì)繼續(xù)放大差異。這也是我堅(jiān)持把它當(dāng)成細(xì)粒度圖像分類問題而不是普通OCR來看待的原因。1.2 為什么偏偏選了這三個(gè)模型市面上比這三個(gè)更強(qiáng)的網(wǎng)絡(luò)一抓一把ResNet、EfficientNet、Vision Transformer都能跑。但我做這個(gè)項(xiàng)目的選型邏輯很樸素第一這個(gè)方向缺可復(fù)現(xiàn)的baseline。查文獻(xiàn)能發(fā)現(xiàn)少數(shù)民族文字識(shí)別論文里最常出現(xiàn)的就是AlexNet、VGG19、GoogLeNet這一代經(jīng)典網(wǎng)絡(luò)。用它們建立基線后續(xù)無論誰提出新方法都能放到同一套指標(biāo)下比較不用各自憑空?qǐng)?bào)數(shù)。第二實(shí)際部署環(huán)境限制了模型規(guī)模。館藏單位通常只有普通辦公電腦沒有GPU集群。AlexNet和GoogLeNet的參數(shù)量都在可控范圍訓(xùn)練和推理成本不高VGG19即使參數(shù)量很大也還能靠著顯存勉強(qiáng)跑起來。第三數(shù)據(jù)量是硬約束。完整數(shù)據(jù)集只有8萬多張還要分成666類類別多且分布不均。Transformer沒有海量數(shù)據(jù)做預(yù)訓(xùn)練很難在這種規(guī)模下發(fā)揮優(yōu)勢(shì)硬遷移ImageNet上的ViT權(quán)重也無法撐起細(xì)粒度字形特征的學(xué)習(xí)。這三個(gè)網(wǎng)絡(luò)的結(jié)構(gòu)差異剛好覆蓋了三種思路AlexNet是典型的“深度卷積全連接”奠基結(jié)構(gòu)VGG19把卷積網(wǎng)絡(luò)推到極深的堆疊路線GoogLeNet則在寬度和多尺度特征上做文章。用同一份滿文數(shù)據(jù)集橫向?qū)Ρ人鼈兡芤淮涡钥辞搴脦最悊栴}。2. 數(shù)據(jù)集不是下載的是自己拼出來的2.1 666個(gè)類別怎么定項(xiàng)目啟動(dòng)時(shí)最尷尬的一件事沒有現(xiàn)成的滿文數(shù)據(jù)集。公開渠道能下載到的都是漢字或英文OCR數(shù)據(jù)集滿文這塊幾乎是空白。我和團(tuán)隊(duì)只能自己造。類別選的是滿漢詞典和高頻詞表里出現(xiàn)頻次靠前的666個(gè)詞盡量覆蓋名詞、動(dòng)詞、數(shù)詞、格助詞等不同語法形態(tài)。每個(gè)類別的圖像數(shù)并不均勻少的只有60來張多的能到260張整份數(shù)據(jù)加起來大約8.2萬張長(zhǎng)尾分布非常明顯。這其實(shí)是低資源文字?jǐn)?shù)據(jù)集的常態(tài)——常用詞樣本多冷門詞樣本少。如果動(dòng)手之前沒意識(shí)到這一點(diǎn)模型后面偏科是必然的。2.2 字體來源與圖像預(yù)處理這批數(shù)據(jù)的“多種字體”主要來自四個(gè)渠道古籍刻本掃描、鉛印出版物掃描、人工手寫樣本、現(xiàn)代TrueType滿文字體渲染圖。四種渠道的字形風(fēng)格差異很大——刻本有木紋噪聲鉛印體筆畫均勻手寫體有連筆字庫渲染最規(guī)整。原始圖像預(yù)處理沒有做太復(fù)雜的操作。先把彩色掃描圖轉(zhuǎn)灰度因?yàn)楣P畫顏色對(duì)文字識(shí)別幾乎沒有幫助然后做一次降噪去掉掃描產(chǎn)生的孤立噪點(diǎn)再做簡(jiǎn)單的對(duì)比度增強(qiáng)讓筆畫更突出。這里我刻意沒有做全局二值化——二值化會(huì)把灰度抗鋸齒信息直接丟掉筆畫邊緣在非標(biāo)準(zhǔn)字體下會(huì)變得毛糙反而影響后續(xù)識(shí)別。預(yù)處理最關(guān)鍵的一步是尺寸歸一。滿文單詞是典型的橫向排布圖像大多是細(xì)長(zhǎng)條高約64像素寬卻可能到300甚至500像素。而AlexNet、VGG19、GoogLeNet的常見輸入都是224×224正方形。最省事的辦法是直接resize但那樣單詞會(huì)被壓成矮胖形狀圈點(diǎn)細(xì)節(jié)全糊。我最后采用“等比縮放白邊padding”單詞圖像按比例縮放到合適大小周邊補(bǔ)白邊湊成224×224再送進(jìn)網(wǎng)絡(luò)。這樣字形比例不丟模型也能正常吃標(biāo)準(zhǔn)輸入。2.3 切分原則按字體切而不是隨機(jī)切這是整份數(shù)據(jù)組織里我認(rèn)為最重要的一次決策。很多人做圖像分類時(shí)習(xí)慣直接隨機(jī)切訓(xùn)練集和測(cè)試集但這個(gè)項(xiàng)目隨機(jī)切會(huì)出大問題同一種字體的圖像會(huì)同時(shí)出現(xiàn)在訓(xùn)練集和驗(yàn)證集里。由于字體風(fēng)格特征太明顯模型完全可能靠“這是鉛印體”“這是手寫體”這種淺層線索做出判斷驗(yàn)證準(zhǔn)確率虛高一到真實(shí)掃描場(chǎng)景就崩。所以訓(xùn)練集、驗(yàn)證集、測(cè)試集的劃分是按字體來源做的比如字庫渲染和部分鉛印體進(jìn)訓(xùn)練集手寫體、刻本掃描和另一部分鉛印體進(jìn)驗(yàn)證集和測(cè)試集盡量保證測(cè)試集里有訓(xùn)練時(shí)沒見過的字體組合。這個(gè)做法會(huì)讓測(cè)試準(zhǔn)確率比隨機(jī)切分低不少但它才是評(píng)價(jià)泛化能力的正確方式。我后面專門留了一份隨機(jī)切分的數(shù)據(jù)做對(duì)照結(jié)果非常有說服力實(shí)驗(yàn)部分會(huì)具體說。3. 三套網(wǎng)絡(luò)的改動(dòng)與訓(xùn)練配置3.1 灰度圖像的首層通道問題torchvision里的預(yù)訓(xùn)練模型都是按RGB三通道設(shè)計(jì)的。滿文圖像只有單通道如果直接把灰度圖復(fù)制成三通道送進(jìn)去等于讓網(wǎng)絡(luò)用三倍顯存去學(xué)完全相同的特征還會(huì)引入冗余。更穩(wěn)妥的做法是把預(yù)訓(xùn)練首層卷積核從3通道融合成1通道。這里有個(gè)成熟的小技巧對(duì)ImageNet預(yù)訓(xùn)練權(quán)重首層卷積核形狀是[輸出通道數(shù), 3, 卷積核高, 卷積核寬]直接對(duì)通道維求平均把RGB三個(gè)通道的卷積核合并成一個(gè)平均核偏置保持不變。代碼量很小import torch from torchvision import models def fuse_first_conv(model): state model.state_dict() # AlexNet / VGG19 的首層在 features.0.weightGoogLeNet 在 conv1.weight weight_key next(k for k in state if k.startswith(features.0) or k.startswith(conv1.)) w state[weight_key] # [C, 3, K, K] fused w.mean(dim1, keepdimTrue) # [C, 1, K, K] state[weight_key] fused model.load_state_dict(state) return model這個(gè)方法比灰度圖復(fù)制成三通道更省內(nèi)存實(shí)測(cè)在滿文數(shù)據(jù)上幾乎沒有精度損失。對(duì)AlexNet和VGG19改的是features[0]對(duì)GoogLeNet改的是conv1。改完首層后還需要把最后的全連接層替換成666類輸出。3.2 三個(gè)模型的差異性在哪里模型主干卷積層數(shù)約參數(shù)量全連接層處理本項(xiàng)目測(cè)試集準(zhǔn)確率AlexNet5約6000萬三層全連接約62.8%VGG1916約1.43億三層全連接約70.4%GoogLeNet22約670萬全局平均池化約76.1%這張表能看出不少東西。VGG19的深度讓它有更強(qiáng)的特征表達(dá)能力但它那三個(gè)全連接層占了絕大部分參數(shù)在8萬張級(jí)別的數(shù)據(jù)上過擬合壓力很大。AlexNet五層卷積結(jié)構(gòu)最樸素訓(xùn)練最快但特征抽象程度明顯不夠。GoogLeNet用Inception模塊在同一層里做多尺度卷積再用1×1卷積大幅降維參數(shù)總量只相當(dāng)于VGG19的零頭最后用全局平均池化替代全連接這種結(jié)構(gòu)在低資源場(chǎng)景下幾乎是為抗過擬合量身定做的。3.3 訓(xùn)練配置與數(shù)據(jù)增強(qiáng)運(yùn)行環(huán)境是PyTorch 1.13單張RTX 3090。優(yōu)化器用SGDmomentum0.9weight decay5e-4初始學(xué)習(xí)率0.01訓(xùn)練80個(gè)epoch前5個(gè)epoch做線性warmup后續(xù)跟余弦退火。batch size設(shè)128。驗(yàn)證集準(zhǔn)確率連續(xù)15個(gè)epoch不提升就提前停止。數(shù)據(jù)增強(qiáng)我用RandomAffine但旋轉(zhuǎn)角度只給±3°平移給0.05縮放0.9到1.1。滿文圈點(diǎn)位置對(duì)角度極度敏感旋轉(zhuǎn)超過5°基本就是在制造錯(cuò)誤樣本。我還以p0.25的概率加了RandomErasing模擬古籍掃描時(shí)局部筆畫被污漬遮擋的情況。有一點(diǎn)要特別提醒不要對(duì)滿文單詞圖像做隨機(jī)裁剪增強(qiáng)。漢字那種方形單字可以隨機(jī)裁但滿文單詞是橫向連寫的隨機(jī)裁很容易把詞的頭部或尾部切掉幾個(gè)不同詞被裁完可能長(zhǎng)得差不多模型越訓(xùn)越糊涂。GoogLeNet還有一個(gè)特殊細(xì)節(jié)torchvision默認(rèn)加載的模型在訓(xùn)練階段會(huì)輸出主logits和兩個(gè)輔助logits總損失按loss_main 0.3 × (loss_aux1 loss_aux2)計(jì)算。輔助分類器對(duì)這個(gè)數(shù)據(jù)集的梯度回傳有明顯正則作用開著比關(guān)掉整體高約1.5個(gè)點(diǎn)。但推理時(shí)一定要調(diào)model.eval()否則輔助分支還會(huì)參與計(jì)算白白浪費(fèi)時(shí)間。4. 結(jié)果不是越高越好泛化才是4.1 按字體劃分的真實(shí)成績(jī)經(jīng)過上面這些處理和訓(xùn)練配置三個(gè)模型在按字體劃分的測(cè)試集上的表現(xiàn)以及隨機(jī)切分對(duì)照組的成績(jī)匯總?cè)缦履P桶醋煮w劃分測(cè)試集準(zhǔn)確率隨機(jī)切分對(duì)照組準(zhǔn)確率AlexNet62.8%95.2%VGG1970.4%96.7%GoogLeNet76.1%97.3%兩組數(shù)據(jù)放在一起對(duì)比非常刺激。隨機(jī)切分時(shí)三套模型都輕松上95%看起來好像“滿文識(shí)別已經(jīng)搞定了”。但按字體劃分后AlexNet直接掉到62.8%說明之前高出的那一大部分準(zhǔn)確率并不是真的認(rèn)識(shí)滿文單詞而是認(rèn)出了字體風(fēng)格。這正驗(yàn)證了前面堅(jiān)持按來源切分的必要性。GoogLeNet在這份數(shù)據(jù)上領(lǐng)先其他兩個(gè)模型5個(gè)多點(diǎn)。我的判斷有三層原因一是Inception結(jié)構(gòu)用多種卷積核同時(shí)關(guān)注局部細(xì)節(jié)和整體輪廓對(duì)多字體帶來的尺度變化更魯棒二是1×1卷積降維配合全局平均池化參數(shù)少在8萬張數(shù)據(jù)上不容易過擬合三是輔助分類器帶來的隱式正則效果讓梯度更平穩(wěn)地回傳。4.2 錯(cuò)誤集中在哪里準(zhǔn)確率之外更能說明問題的是混淆矩陣。我把所有誤分類對(duì)拉出來看發(fā)現(xiàn)錯(cuò)誤高度集中在幾類情況。第一是字形相近的單詞對(duì)。滿文里有一批詞區(qū)別只在詞末字母的圈點(diǎn)位置。鉛印體還算清楚刻本掃描一旦筆畫斷裂模型基本只能靠猜。第二是手寫體表現(xiàn)明顯更差。手寫連筆會(huì)讓字母邊界糊在一起這三個(gè)模型對(duì)連續(xù)連筆的容忍度都不高。第三是長(zhǎng)尾類別問題。樣本數(shù)只有六七十張的冷門詞準(zhǔn)確率普遍比高頻詞低十幾個(gè)百分點(diǎn)。我還特意用訓(xùn)練時(shí)完全沒見過的字體做了次小測(cè)試。比如只用字庫渲染圖訓(xùn)練然后用刻本掃描圖測(cè)試GoogLeNet準(zhǔn)確率掉了將近20個(gè)點(diǎn)。這個(gè)結(jié)果不意外但它提醒我多字體混合識(shí)別本質(zhì)上比單字體識(shí)別難上一個(gè)維度。真實(shí)場(chǎng)景里不可能只遇到訓(xùn)練過的字體這個(gè)損失必須從一開始就納入預(yù)期。5. 訓(xùn)練過程中踩過的幾個(gè)坑5.1 字體泄露最隱蔽的坑這個(gè)坑前面反復(fù)提但還是要單獨(dú)說一遍因?yàn)樗鼘?shí)在太隱蔽了。第一次跑實(shí)驗(yàn)時(shí)我在驗(yàn)證集上看到94%的準(zhǔn)確率一度以為項(xiàng)目可以提前交差。后來把模型拿到另一批手寫掃描件上測(cè)試準(zhǔn)確率驟降到68%。復(fù)盤時(shí)才發(fā)現(xiàn)驗(yàn)證集隨機(jī)切分后同一種字體同時(shí)出現(xiàn)在訓(xùn)練和驗(yàn)證里模型在驗(yàn)證時(shí)就是把“字形風(fēng)格”當(dāng)成了主要特征。按字體來源重新劃分后準(zhǔn)確率才回到真實(shí)水平。如果你的任務(wù)也是多來源、多風(fēng)格的數(shù)據(jù)強(qiáng)烈建議先按來源分組再切分。寧可訓(xùn)練集看起來少一些也不要讓評(píng)估結(jié)果自欺欺人。5.2 細(xì)長(zhǎng)條圖像被resize得面目全非滿文單詞圖像是橫長(zhǎng)條。最初我圖省事直接把短邊resize成224×224長(zhǎng)邊被壓扁結(jié)果三個(gè)模型全在55%左右徘徊。改成等比縮放加白邊padding后準(zhǔn)確率立刻提升。這里還可以再優(yōu)化一步單詞本身寬高比差別很大等比縮放后有的圖實(shí)際占的面積很小白白浪費(fèi)計(jì)算量。我后來在同一套流程里加了動(dòng)態(tài)適配先統(tǒng)計(jì)所有訓(xùn)練圖寬高比的分布把常見比例對(duì)應(yīng)的縮放尺寸算出來讓單詞主體盡量多占像素其余空白繼續(xù)padding。這一步對(duì)細(xì)長(zhǎng)型文字的OCR類任務(wù)幾乎是通用方案。5.3 全連接分類頭成了過擬合重災(zāi)區(qū)AlexNet和VGG19最后的全連接層是為1000類ImageNet設(shè)計(jì)的改成666類后參數(shù)依然龐大。實(shí)驗(yàn)中發(fā)現(xiàn)VGG19的驗(yàn)證loss通常在40個(gè)epoch后開始反彈而GoogLeNet的驗(yàn)證loss能一路壓到最后。后來我把AlexNet和VGG19的fc6維度從4096降到2048dropout從0.5提到0.6準(zhǔn)確率反而小幅提升0.3到0.8個(gè)點(diǎn)訓(xùn)練時(shí)間也縮短了。這說明在中小規(guī)模數(shù)據(jù)集上模型容量不是越大越好。對(duì)小數(shù)據(jù)文字識(shí)別全局平均池化這類無參降維方案明顯更省心。5.4 數(shù)據(jù)增強(qiáng)不是萬能藥我一度希望通過瘋狂增強(qiáng)來彌補(bǔ)數(shù)據(jù)不足結(jié)果旋轉(zhuǎn)加強(qiáng)到10°之后三個(gè)模型準(zhǔn)確率集體下降——滿文字母一旦帶圈點(diǎn)轉(zhuǎn)幾度后圈點(diǎn)位置就可能和另一個(gè)字母的形態(tài)重合。這個(gè)教訓(xùn)讓我明白對(duì)局部符號(hào)極其敏感的文字?jǐn)?shù)據(jù)增強(qiáng)必須保守??杉拥氖禽p微平移、縮放、擦除和噪聲不可加的是大角度旋轉(zhuǎn)和隨意裁剪。做任何增強(qiáng)前最好先自己看一批增強(qiáng)后的圖確認(rèn)它看起來還是“那個(gè)單詞”再?zèng)Q定是否上線。6. 如果繼續(xù)往下做我會(huì)怎么走6.1 從分類到序列化識(shí)別詞級(jí)分類只是baseline。滿文是拼音文字一個(gè)單詞的內(nèi)部結(jié)構(gòu)本質(zhì)上是音素序列天然適合序列模型。后續(xù)真要落地更合理的方向是把單詞的字母序列預(yù)測(cè)出來用CRNN或Transformer encoder-decoder做端到端識(shí)別而不是讓網(wǎng)絡(luò)從666類里硬選一個(gè)。分類只能處理固定詞表序列生成能推到?jīng)]有見過的詞對(duì)真實(shí)古籍的開放詞表適配性高得多。代價(jià)是標(biāo)注成本上漲——每張圖都要標(biāo)字母級(jí)序列但這是技術(shù)路線上必然要邁的一步。6.2 合成數(shù)據(jù)是低資源文字識(shí)別的生命線如果讓我重新做一個(gè)滿文識(shí)別項(xiàng)目第一步不會(huì)急著找更多人工標(biāo)注而是先把合成數(shù)據(jù)流水線搭起來。現(xiàn)在滿文字庫和輸入法已經(jīng)比較成熟可以用腳本批量渲染任意詞表在不同字體、不同字號(hào)、不同噪聲下的圖像幾個(gè)小時(shí)就能生成幾十萬張。合成數(shù)據(jù)雖然和真實(shí)掃描存在域差距但用來解決長(zhǎng)尾類別樣本不足的問題綽綽有余。跑通之后再配合少量真實(shí)數(shù)據(jù)做領(lǐng)域適應(yīng)性價(jià)比極高。6.3 一點(diǎn)個(gè)人體會(huì)這個(gè)項(xiàng)目最大的收獲不是“GoogLeNet贏了”而是讓我確認(rèn)了一件事在低資源文字識(shí)別里數(shù)據(jù)組織方式對(duì)最終結(jié)果的影響常常比模型結(jié)構(gòu)更明顯。按字體切分、等比padding、保守增強(qiáng)——每一項(xiàng)調(diào)整帶來的收益都比單純換網(wǎng)絡(luò)更大。如果你也在做類似的文字圖像分類別急著上大模型先把數(shù)據(jù)的來源、劃分方式和預(yù)處理研究透。這幾個(gè)點(diǎn)做好了哪怕只用GoogLeNet這種上一代網(wǎng)絡(luò)也能在真實(shí)場(chǎng)景里扛住一陣子。如果后續(xù)算力和數(shù)據(jù)量都上來了再從這個(gè)強(qiáng)baseline出發(fā)往序列化模型和合成數(shù)據(jù)方向迭代路會(huì)順很多。本文還有配套的精品資源點(diǎn)擊獲取