文章目錄
從零開始搭建深度學習驗證碼識別模型
CNN模型與圖像識別
CNN模型是圖像問題的基本深度學習算法,使用CNN算法不用人工從圖片中提取特徵,更加end2end,符合表示學習的特徵,避免繁瑣、低效的特徵工程。CNN算法目前在CV領域已經成爲基本方法之一。
CNN模型的核心在於,利用卷積核在特徵圖上進行運算,從中提取到充足的特徵。在CNN的研究發現,在淺層網絡,CNN模型可以提取到部分簡單的特徵,如輪廓等,而深層CNN則將基礎特徵進行整合,提取到更加複雜的特徵,從而能夠對圖片中的內容進行特徵提取。
CNN的核心在於卷積運算規則,其大致爲:
其中,爲特徵圖切片運算,如當爲時,則將會根據步長,從中進行切片,並與卷積核進行element-wise乘積運算後並求和。
在CV諸多任務中,卷積層往往被用來做特徵提取,而到具體的任務時,需要拼接更多的網絡,如拼接全連接網絡進行分類任務。
驗證碼識別,本質上也爲一個圖像識別任務。在對象識別模型中,通常需要從圖像中儘可能識別多的對象,並以框的形式對其位置進行標記。驗證碼識別也可採用該方式實現,該方法爲multi-stage方法,即:通常使用對象識別模型,識別圖片中的文字,並用框標記出文字所在位置,再利用CNN和FCN的結構對所識別的文字進行分類。並且,若爲文檔OCR識別,輸出層還可能借助LSTM等RNN結構網絡。
考慮到驗證碼通常位數有限,即4位、5位較爲常見,因此該模型採用end2end multi-task方法也可滿足需求,且模型複雜度並不高。multi-task任務可簡單的理解爲,有多個輸出層負責不同任務的輸出,其弊端在於擴展性低,如只能識別固定數量的對象。
驗證碼數據集介紹
所有訓練數據均以驗證碼圖片內容爲名稱命名,如2ANF.jpg
,因此可以保證訓練數據沒有重複項,根據文件名即可獲取樣本label。
數據集下載:Dataset-Google Drive for Easy Captcha
數據規格:48,320張驗證碼圖片,全由
Easy Captcha
框架生成,大小爲。
驗證碼示例:
EasyCaptcha驗證碼特點在於可以構造Gif動態驗證碼,而其他驗證碼則顯得相對簡單,主要在於該驗證碼間隔較開,易於區分,因此識別較爲簡單。根據對上例中的驗證碼分析可知,驗證碼由不定位置的1-2個圓圈與曲線構成噪音,對文本加以干擾,文字顏色可變。從佈局來看,文字的佈局位置相對固定,且間隔也相對固定,這無疑也簡化了識別過程。
數據集下載:Dataset-Google Drive for Kaptcha
數據規格:52,794張驗證碼圖片,全由
Kaptcha
生成,大小爲。
驗證碼示例:
相對而言,Kaptcha驗證碼相對而言文本排布默認更加緊湊,但是文字間距再kaptcha中是一個可以調節的超參數。Kaptcha較難識別的主要原因在於其文本存在可能的扭曲形變,並且形變狀態不定,因此模型需要能夠克服該形變,方可較爲準確的識別,因此Kaptcha識別較captcha困難,並且準確度指標會有所下降。
注:在直接使用模型時需要嚴格注意驗證碼規格,這主要在於圖片過小會導致CNN過程異常。若對圖片進行分辨率調整,長寬比不一,將導致嚴重形變,導致識別精度下降。
生成數據集
基於上述兩個驗證碼框架,可以使用其提供的開源庫進行驗證碼生成。
生成EasyCaptcha
如下代碼所示,主要是從配置中獲取驗證碼的配置,並使用給定的框架進行驗證碼生成,並最終輸出到文件中。
public boolean generate() {
String outputFolder = config.get(ConfigConstants.OUT_DIR);
int width = Integer.parseInt(config.get(ConfigConstants.WIDTH, "120"));
int height = Integer.parseInt(config.get(ConfigConstants.HEIGHT, "80"));
int len = Integer.parseInt(config.get(ConfigConstants.LENGTH));
SpecCaptcha captcha = new SpecCaptcha(width, height, len);
captcha.setCharType(Captcha.TYPE_DEFAULT);
try {
captcha.setFont(Captcha.FONT_3);
} catch (IOException | FontFormatException e1) {
e1.printStackTrace();
return false;
}
String codes = captcha.text();
if (LOG.isInfoEnabled()) {
LOG.info("Generating " + codes + "...");
}
return ImageOutputUtil.writeToFile(captcha, outputFolder, codes);
}
爲提升圖片生成的效率,我們使用多線程的方式,同時生成:
public class CaptchaTaskRunner implements Runnable {
private static final Logger LOG = Logger.getLogger(CaptchaTaskRunner.class);
private CaptchaGenerator generator;
@Override
public void run() {
boolean success = generator.generate();
if (success) {
if (LOG.isInfoEnabled()) {
LOG.info("Complete!");
}
} else {
if (LOG.isInfoEnabled()) {
LOG.info("Failed!");
}
}
}
/**
* @return CaptchaGenerator return the generator
*/
public CaptchaGenerator getGenerator() {
return generator;
}
/**
* @param generator the generator to set
*/
public void setGenerator(CaptchaGenerator generator) {
this.generator = generator;
}
}
代碼中CaptchaGenerator
即爲generate()
方法的接口類,在線程池中提交若干任務,最終都由CaptchaTaskRunner
實例進行生成。
生成Kcaptcha
相較於EasyCaptcha,Kcaptcha的配置項更多,因此其識別更加困難,爲增強最終模型的可信度與擬合能力,可隨機地產生若干配置,來生成驗證碼:
public KaptchaGeneratorWorker(me.zouzhipeng.config.Config config) {
Properties prop = new Properties();
prop.put(Constants.KAPTCHA_BORDER, true);
prop.put(Constants.KAPTCHA_BORDER_COLOR,
String.join(",", rand.nextInt(256) + "", rand.nextInt(256) + "", rand.nextInt(256) + ""));
prop.put(Constants.KAPTCHA_IMAGE_WIDTH, config.get(ConfigConstants.WIDTH, "200"));
prop.put(Constants.KAPTCHA_IMAGE_HEIGHT, config.get(ConfigConstants.HEIGHT, "50"));
String textColor = config.get(ConfigConstants.TEXT_COLOR);
if (null == textColor) {
textColor = String.join(",", rand.nextInt(256) + "", rand.nextInt(256) + "", rand.nextInt(256) + "");
}
prop.put(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR,
textColor);
prop.put(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, config.get(ConfigConstants.LENGTH, "4"));
prop.put(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "彩雲,宋體,楷體,微軟雅黑,Arial,SimHei,SimKai,SimSum");
if (Boolean.parseBoolean(config.get(ConfigConstants.NOISE_SAME_TEXT_COLOR, "true"))) {
prop.put(Constants.KAPTCHA_NOISE_COLOR, textColor);
} else {
prop.put(Constants.KAPTCHA_NOISE_COLOR,
String.join(",", rand.nextInt(256) + "", rand.nextInt(256) + "", rand.nextInt(256) + ""));
}
prop.put(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012345679");
this.output = config.get(ConfigConstants.OUT_DIR);
Config kaptchaConfig = new Config(prop);
producer = kaptchaConfig.getProducerImpl();
}
如上述構造函數,針對Kaptcha進行了所需配置項的配置。
而生成部分,同EasyCaptcha相似,如下:
public boolean generate(String folder) {
String text = producer.createText();
BufferedImage imageBuffered = producer.createImage(text);
if (LOG.isInfoEnabled()) {
LOG.info("Generating " + text + "...");
}
return ImageOutputUtil.writeToFile(imageBuffered, folder, text, "jpg");
}
同樣地,採用多線程對圖片進行生成,以得到大量驗證碼訓練圖片。
搭建模型
針對兩個不同的數據集,本項目設計了兩個不同的模型,但是總體上都是基於CNN和FCN結構的分類任務。在諸多OCR任務中,通常會使用multi-stage方法設計模型,即:通常使用對象識別模型,識別圖片中的文字,並用框標記出文字所在位置,再利用CNN和FCN的結構對所識別的文字進行分類。並且,若爲文檔OCR識別,輸出層還可能借助LSTM等RNN結構網絡。
考慮到驗證碼通常位數有限,即4位、5位較爲常見,因此該模型採用end2end multi-task方法也可滿足需求,且模型複雜度並不高。
針對EasyCaptcha驗證碼,其產生的驗證碼較容易區分,字符分隔較開,且變形選項較少,因此使用很簡單的模型即可達到較高的精度,在本項目的模型中,驗證集準確度可達到左右。
而對於Kaptcha驗證碼,其存在較多可選的配置項,並且會在驗證碼中間添加噪音擾動,因此識別較爲困難,使用EasyCaptcha的模型,精度僅能達到70%左右,準確度較低,Kaptcha模型適當地加大了CNN網絡的深度,並增加了一層全連接隱藏層,在驗證集上達到93-94%的準確度。
在訓練過程中,採用長度爲4的驗證碼,其中驗證碼中可選字符爲:a-zA-Z0-9,共62個可能字符。
下面爲兩個模型:EasyNet, KCapNet的詳細介紹。
EasyNet模型
EasyNet模型由2層卷積層和4個輸出層構成,該模型結構細節如下:
- 第一層卷積,卷積核大小爲,步長爲1,通道爲16,參數量爲,得到大小的特徵圖共16個;
- 第二層爲最大池化層,無參數,池化核大小爲,步長爲5,得到特徵圖大小爲;
- 第三層爲批歸一化層,避免過擬合併加速模型收斂。根據效果,同時還可嘗試使用shortcut方法。歸一化後,採用RReLu激活函數;
- 第四層爲卷積層,卷積核大小爲,步長爲1,通道數爲32,得到特徵圖大小爲,參數量爲;
- 第五層爲最大池化層,無參數,池化核大小爲,步長爲5,得到特徵圖大小爲,無參數;
- 第六層仍爲批歸一化層,並採用RReLu函數激活;
- 第七層爲Dropout層,經過第二個卷積後,將得到特徵圖展開,得到特徵向量維度爲256維,對256的特徵向量進行Dropout處理,避免過擬合,採用的失效概率爲;
- 第八層爲輸出層,用於得到驗證碼序列,由於模型爲multi-task,因此輸出層有4個(根據驗證碼中字符長度確定),使用softmax激活,參數量爲。
KCapNet模型
KCapNet共由3個卷積層,1個全連接層,4個輸出層組成,以下爲模型具體細節:
- 第一層爲卷積層,卷積核大小爲,步長爲1,通道數爲16,輸入圖片大小爲,因此可得到16個大小爲的特徵圖,參數量爲;
- 第二層爲最大池化層,無參數,池化核大小爲,步長爲3,得到特徵圖大小爲;
- 第三層爲批歸一化層,在歸一化結束後,使用RReLu激活函數激活;
- 第四層爲第二個卷積層,卷積核大小爲,通道數爲32,可得到大小爲的特徵圖32個,參數量爲;
- 第五層爲最大池化層,無參數,池化核大小爲$ 3 \times 3$,步長爲3,無參數,可將特徵圖壓縮爲;
- 第六層爲批歸一化層,並使用RReLU函數激活;
- 第七層爲第三個卷積層,卷積核大小爲,步長爲1,通道數爲64,可得到大小爲的特徵圖64個,參數量爲;
- 第八層爲最大池化層,池化視野爲,步長爲2,無參數,特徵圖被進一步壓縮爲;
- 第九層爲歸一化層,歸一化後使用RReLu函數激活;
- 第十層爲Dropout層,輸入爲第九層輸出展開後的特徵向量,維度爲576維,該層採用$ p = 0.15$ 的概率失效一定神經元;
- 第十一層爲全連接層,輸入爲第十層的輸出,維度爲576維,全連接層輸出維度爲128維,參數量爲,並使用RReLU函數激活;
- 第十二層爲全連接的Dropout層,神經元失效概率爲;
- 第十三層爲輸出層,根據multi-task數量,爲4個輸出層,維度爲62維,使用softmax函數激活,參數量爲;
模型部分參數未描述,由於是少量參數,相比之下可以忽略,如RReLu中的參數。
模型訓練與參數選擇
優化方法與超參數
在該模型中,採用了Adam作爲優化算法,並設定學習率爲0.001,可達到較好效果。在模型訓練過程中,嘗試使用較大學習率,如0.01, 0.1, 0.05等,均不如低學習率收斂效果好。上述兩個模型,均在Google Colab Pro上使用P100訓練,該算力可勝任batch至少爲1024的配置,在EasyNet模型中使用了512的batch,而KCapNet使用1024的batch。
該batch設置未達到算力極限,如有條件可測試,但是不推薦模型採用較大batch,而應儘可能選擇合理的batch。
模型訓練過程中,優化算法未使用學習率衰減算法。
在模型訓練過程中,對於EasyNet,採用的Dropout能達到較好效果,若採用效果略差,但精度仍然可觀,可見對於EasyNet其數據簡單因而模型即便簡單也仍能達到較好效果。
而對於KCapNet,Dropout從最初的擬合效果較差,大概穩定在$85% p_1=p_2=0.2p_1=0.15,p_2=0.1$得到最終模型,其訓練集精度爲左右,驗證集精度爲。
數據集劃分
在模型訓練過程中,默認採用的分配比切分訓練集、驗證集、測試集,切分過程大致爲:
- 按3:1第一次切分,其中爲訓練集;
- 對上述剩餘的進行切分,得到訓練集及測試集。
根據需要,開發者自行訓練模型時,可根據需要手動指定數據集切分比例。
模型分析
爲了能夠儘量評估運算所需算力,可以對模型的內存消耗進行評估,此處忽略激活函數中的參數,偏置等少量參數,模型的算力要求應等於參數量+輸入輸出+梯度與動量,根據神經網絡反向傳播理論,在更新參數時需要計算下一層輸出關於上一層參數的梯度,因此參數量==梯度,而在優化方法中需要保存動量,以記錄之前參數更新的歷史記錄,因此參數量==動量,而對於Adam優化器,則更有動量==2參數量,因此整個模型的算力要求爲:
通常網絡中使用的數據類型爲Float32類型,其佔4 Byte,於是便可通過存儲量來計算內存消耗。
EasyNet算力計算
根據下表統計,該模型算力大致要求爲:1.9 MB /sample。
層 | 參數量 | 特徵圖 | 所需內存 |
---|---|---|---|
Input | 0 | 75 KB | |
Conv1 | 569.8 KB | ||
Maxpool1 | 0 | 21.6 KB | |
BN1 | 22.0 KB | ||
Conv2 | 226.1 KB | ||
Maxpool2 | 0 | 1 KB | |
BN2 | 2 KB | ||
Output | 993.0 KB |
KCap算力計算
根據下表統計,其算力要求大致爲:2.9 MB /sample。
層 | 參數量 | 特徵圖 | 所需內存 |
---|---|---|---|
Input | 0 | 117.2 KB | |
Conv1 | 582.3 KB | ||
Maxpool2 | 0 | 60 KB | |
BN1 | 60.5 KB | ||
Conv2 | 172.75 KB | ||
Maxpool2 | 0 | 10 KB | |
BN2 | 11 KB | ||
Conv3 | 297 KB | ||
Maxpool3 | 0 | 2.3 KB | |
BN3 | 4.3 KB | ||
Fatten | 0 | 2.25 KB | |
FCN | $128 $ | 1152 KB | |
BN4 | 4.5 KB | ||
Output | 497.0 kB |
EasyNet
下圖爲EasyNet訓練過程的模型損失曲線,從圖中可以看出,模型在前10個epoch迅速收斂,在20 epoch之後,模型達到相對穩定狀態。從圖中可以看出,驗證集損失相較於訓練集損失,下降比較健康,並且手鍊曲線相對光滑,在後期也未出現驗證集損失波動情況,說明其未發生嚴重過擬合,模型可以被認爲訓練過程可信。
從精度曲線中可以看出,在訓練初期,驗證集上的精度基本能優於訓練集上的精度,這得益於正則化手段,使得模型的子模型也能具有較好的表現,在25個epoch直至更後期,驗證集精度和訓練集精度開始趨於重合,甚至驗證集精度略低於訓練集精度,並且精度不再明顯上升。從精度曲線的光滑程度來看,同樣證明模型在訓練過程中未發生嚴重過擬合,因此模型可信度及有效性較高。
KCapNet
下圖分別爲KCapNet模型的損失曲線及精度曲線,從曲線中可以看出,在epoch爲120時,曲線發生了劇烈波動,這是因爲在訓練過程中,調整了batch的緣故。通常,較大batch可以一定程度地加速模型收斂,使得梯度方向更加準確,更有利於模型收斂,但是batch過大會導致對於部分較低比例的hard sample影響被淡化,從而使得模型不具備hard sample的識別能力,制約了模型的擬合能力。因此在使用較大batch訓練模型基本收斂後,調小batch以強化模型對於小部分樣本的識別能力。根據損失曲線可以看出,模型收斂過程相對健康,在前25個epoch時,模型迅速收斂,並達到較好效果,隨後訓練集損失繼續穩定下降,而訓練集損失開始出現一定範圍內的波動,但是未呈現明顯的上升趨勢,說明模型達到一定穩定程度的擬合能力。隨着訓練集損失的持續下降,驗證集損失始終在1上下波動,無明顯的損失整體下降趨勢,因此在60 epoch之後,可以選擇性早停,即Early stopping。
在120 epoch之後,即batch調笑之後,模型損失突然小幅度上升,隨後繼續下降,但驗證集上損失較之前波動情況更加嚴重,這也一定程度地說明較大的batch相較於較小的batch,能夠使模型損失更加光滑。
在該模型中,較小的batch取爲256。
與損失曲線相反,在前25個epochs中,模型精度提升較快,並且能迅速達到0.9上下,隨後訓練集精度開始小幅度持續上升,而驗證集精度開始出現波動,在70 epochs之後,驗證集上的精度最好能達到上下。在調小batch之後,驗證集的精度波動更大,但最好精度與大batch之前相差較小,說明在較大batch下,模型收斂相對較好。
綜合損失曲線與精度曲線,可知,在70 epoch之後,選擇 epoch中損失最低的模型,可基本視爲最佳模型。而在小batch之後,推薦選擇 epoch間的最低損失模型可達到較好效果。
在本項目提供的預訓練模型中,選擇了第169個epoch的模型,其訓練集精度可達0.94。
下圖爲從測試集隨機選擇的5組驗證碼樣本,其中大部分均識別正確(標綠),小部分識別錯誤(標紅)。從標紅的案例中可以看出,該驗證碼認爲識別正確難度仍然較高,因此識別錯誤也可以接受。同時,根據更廣泛的測試集評估研究,模型對於0與O的識別準確度較低,甚至於O大部分被識別爲0,這大程度上地受驗證碼由於字體形變而引發,根據人工對這些特殊案例的對比,部分能夠被人眼正確地分辨,而少部分缺失存在人爲無法準確分辨的案例。可以認爲,認爲地區分0與O,可能有成功率,這也同樣對模型的準確度產生了干擾。
由於模型達到了基本可接受的識別準確度,因此再未將識別錯誤的樣本單獨挑出並訓練,從理論上推測, 將分類錯誤的樣本挑出重新分類,可以一定程度地提升模型效果,進行該操作的方法可有兩種:
- 將識別錯誤的訓練數據單獨挑出,並重新構成訓練集,並重新訓練,該方式可能使得模型對這些樣本過於擬合,因此訓練的迭代次數需要控制;
- 將識別錯誤的樣本標記,再下一輪訓練時,在損失函數上,爲上次識別失敗的樣本增大權重,使得分類錯誤的樣本對模型的提升影響更大,降低正確識別樣本對模型的影響,但是訓練時仍提供正確樣本,能夠避免第一種方法的過擬合。(類似於Boosting)
如下圖,計算了模型在預測中的準確率,可見其波動較大,但是對於易於識別的數據,其準確率較高。
ACC指標與Correct指標不同,ACC計算了每個task的準確性,而correct計算了四個字符全部預測正確的比例。
上述模型的提升方法,有條件地可以進一步實驗,以進一步提升模型性能。同時,對於驗證碼識別,還可以考慮使用注意力機制,針對不同的輸出層關注不同的Feature Map,從直觀上理解,應該能一定程度地提升模型的擬合能力,開發者們可以進一步嘗試。
模型源代碼及預訓練模型已經開源至Github,歡迎訪問。