從零開始搭建深度學習驗證碼識別模型

從零開始搭建深度學習驗證碼識別模型

CNN模型與圖像識別

CNN模型是圖像問題的基本深度學習算法,使用CNN算法不用人工從圖片中提取特徵,更加end2end,符合表示學習的特徵,避免繁瑣、低效的特徵工程。CNN算法目前在CV領域已經成爲基本方法之一。

CNN模型的核心在於,利用卷積核在特徵圖上進行運算,從中提取到充足的特徵。在CNN的研究發現,在淺層網絡,CNN模型可以提取到部分簡單的特徵,如輪廓等,而深層CNN則將基礎特徵進行整合,提取到更加複雜的特徵,從而能夠對圖片中的內容進行特徵提取。

CNN的核心在於卷積運算規則,其大致爲:

CNN(map)=i=0fhj=0fwfilterh,wslice(map)h,wCNN(map)=\sum\limits_{i=0}^{f_h} \sum\limits_{j=0}^{f_w} filter_{h,w} \cdot slice(map)_{h,w}

其中,slice()slice(\cdot)爲特徵圖切片運算,如當filterfilter33時,則mapmap將會根據步長,從中進行切片,並與卷積核進行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框架生成,大小爲120×80120 \times 80

驗證碼示例:
easycaptcha

EasyCaptch項目主頁

EasyCaptcha驗證碼特點在於可以構造Gif動態驗證碼,而其他驗證碼則顯得相對簡單,主要在於該驗證碼間隔較開,易於區分,因此識別較爲簡單。根據對上例中的驗證碼分析可知,驗證碼由不定位置的1-2個圓圈與曲線構成噪音,對文本加以干擾,文字顏色可變。從佈局來看,文字的佈局位置相對固定,且間隔也相對固定,這無疑也簡化了識別過程。

數據集下載:Dataset-Google Drive for Kaptcha

數據規格:52,794張驗證碼圖片,全由Kaptcha生成,大小爲200×50200 \times 50

驗證碼示例:
kaptchas

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驗證碼,其產生的驗證碼較容易區分,字符分隔較開,且變形選項較少,因此使用很簡單的模型即可達到較高的精度,在本項目的模型中,驗證集準確度可達到989998-99%左右。

而對於Kaptcha驗證碼,其存在較多可選的配置項,並且會在驗證碼中間添加噪音擾動,因此識別較爲困難,使用EasyCaptcha的模型,精度僅能達到70%左右,準確度較低,Kaptcha模型適當地加大了CNN網絡的深度,並增加了一層全連接隱藏層,在驗證集上達到93-94%的準確度。

在訓練過程中,採用長度爲4的驗證碼,其中驗證碼中可選字符爲:a-zA-Z0-9,共62個可能字符。

下面爲兩個模型:EasyNet, KCapNet的詳細介紹。

EasyNet模型

easynet

EasyNet模型由2層卷積層和4個輸出層構成,該模型結構細節如下:

  1. 第一層卷積,卷積核大小爲5×55 \times 5,步長爲1,通道爲16,參數量爲3×5×5×163 \times 5 \times 5 \times 16,得到大小76×11676\times 116的特徵圖共16個;
  2. 第二層爲最大池化層,無參數,池化核大小爲5×55\times 5,步長爲5,得到特徵圖大小爲15×2315 \times 23
  3. 第三層爲批歸一化層,避免過擬合併加速模型收斂。根據效果,同時還可嘗試使用shortcut方法。歸一化後,採用RReLu激活函數;
  4. 第四層爲卷積層,卷積核大小爲5×55 \times 5,步長爲1,通道數爲32,得到特徵圖大小爲11×1911 \times 19,參數量爲16×5×5×3216 \times 5 \times 5 \times 32
  5. 第五層爲最大池化層,無參數,池化核大小爲5×55 \times 5,步長爲5,得到特徵圖大小爲2×42 \times 4,無參數;
  6. 第六層仍爲批歸一化層,並採用RReLu函數激活;
  7. 第七層爲Dropout層,經過第二個卷積後,將得到特徵圖展開,得到特徵向量維度爲256維,對256的特徵向量進行Dropout處理,避免過擬合,採用的失效概率爲p=0.3p=0.3
  8. 第八層爲輸出層,用於得到驗證碼序列,由於模型爲multi-task,因此輸出層有4個(根據驗證碼中字符長度確定),使用softmax激活,參數量爲4×256×624\times 256 \times 62

KCapNet模型

kaptcha-net

KCapNet共由3個卷積層,1個全連接層,4個輸出層組成,以下爲模型具體細節:

  1. 第一層爲卷積層,卷積核大小爲5×55 \times 5,步長爲1,通道數爲16,輸入圖片大小爲50×20050 \times 200,因此可得到16個大小爲46×19646 \times 196的特徵圖,參數量爲3×5×5×163\times 5 \times 5 \times 16
  2. 第二層爲最大池化層,無參數,池化核大小爲3×33 \times 3,步長爲3,得到特徵圖大小爲15×6415 \times 64
  3. 第三層爲批歸一化層,在歸一化結束後,使用RReLu激活函數激活;
  4. 第四層爲第二個卷積層,卷積核大小爲3×33 \times 3,通道數爲32,可得到大小爲13×6213 \times 62的特徵圖32個,參數量爲16×3×3×3216 \times 3 \times 3 \times 32
  5. 第五層爲最大池化層,無參數,池化核大小爲$ 3 \times 3$,步長爲3,無參數,可將特徵圖壓縮爲4×204 \times 20
  6. 第六層爲批歸一化層,並使用RReLU函數激活;
  7. 第七層爲第三個卷積層,卷積核大小爲3×33 \times 3,步長爲1,通道數爲64,可得到大小爲2×182 \times 18的特徵圖64個,參數量爲32×3×3×6432 \times 3 \times 3 \times 64
  8. 第八層爲最大池化層,池化視野爲2×22 \times 2,步長爲2,無參數,特徵圖被進一步壓縮爲1×91 \times 9
  9. 第九層爲歸一化層,歸一化後使用RReLu函數激活;
  10. 第十層爲Dropout層,輸入爲第九層輸出展開後的特徵向量,維度爲576維,該層採用$ p = 0.15$ 的概率失效一定神經元;
  11. 第十一層爲全連接層,輸入爲第十層的輸出,維度爲576維,全連接層輸出維度爲128維,參數量爲576×128576 \times 128,並使用RReLU函數激活;
  12. 第十二層爲全連接的Dropout層,神經元失效概率爲p=0.1p=0.1
  13. 第十三層爲輸出層,根據multi-task數量,爲4個輸出層,維度爲62維,使用softmax函數激活,參數量爲4×128×624 \times 128 \times 62

模型部分參數未描述,由於是少量參數,相比之下可以忽略,如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,採用p=0.3p=0.3的Dropout能達到較好效果,若採用0.40.50.4 \sim 0.5效果略差,但精度仍然可觀,可見對於EasyNet其數據簡單因而模型即便簡單也仍能達到較好效果。

而對於KCapNet,Dropout從最初的0.50.5擬合效果較差,大概穩定在$85% Dropout上下,而逐步降低Dropout擬合能力逐漸提升,最終在p_1=p_2=0.2時效果較好,最終採用p_1=0.15,p_2=0.1$得到最終模型,其訓練集精度爲95%95\%左右,驗證集精度爲9394%93\sim 94 \%

數據集劃分

在模型訓練過程中,默認採用6:1:16:1:1的分配比切分訓練集、驗證集、測試集,切分過程大致爲:

  1. 按3:1第一次切分,其中75%75\%爲訓練集;
  2. 對上述剩餘的25%25\%進行1:11:1切分,得到訓練集及測試集。

根據需要,開發者自行訓練模型時,可根據需要手動指定數據集切分比例。

模型分析

爲了能夠儘量評估運算所需算力,可以對模型的內存消耗進行評估,此處忽略激活函數中的參數,偏置等少量參數,模型的算力要求應等於參數量+輸入輸出+梯度與動量,根據神經網絡反向傳播理論,在更新參數時需要計算下一層輸出關於上一層參數的梯度,因此參數量==梯度,而在優化方法中需要保存動量,以記錄之前參數更新的歷史記錄,因此參數量==動量,而對於Adam優化器,則更有動量==2參數量,因此整個模型的算力要求爲:

MEM=W4+I+OMEM = W*4+I+O

通常網絡中使用的數據類型爲Float32類型,其佔4 Byte,於是便可通過存儲量來計算內存消耗。

EasyNet算力計算

根據下表統計,該模型算力大致要求爲:1.9 MB /sample。

參數量 特徵圖 所需內存
Input 0 3×80×120=192003 \times 80 \times 120=19200 75 KB
Conv1 3×5×5×16=12003 \times 5 \times 5 \times 16 = 1200 76×116×16=14105676\times 116 \times 16=141056 569.8 KB
Maxpool1 0 16×15×23=552016 \times 15 \times 23=5520 21.6 KB
BN1 2×16=322 \times 16=32 16×15×23=552016 \times 15 \times 23=5520 22.0 KB
Conv2 16×5×5×32=1280016 \times 5 \times 5 \times 32=12800 11×19×32=668811 \times 19 \times 32 = 6688 226.1 KB
Maxpool2 0 2×4×32=2562 \times 4 \times 32 = 256 1 KB
BN2 2×32=642\times 32=64 2×4×32=2562 \times 4 \times 32 = 256 2 KB
Output 4×256×62=634884\times 256 \times 62=63488 4×62=2484 \times 62=248 993.0 KB

KCap算力計算

根據下表統計,其算力要求大致爲:2.9 MB /sample。

參數量 特徵圖 所需內存
Input 0 3×50×200=300003 \times 50 \times 200=30000 117.2 KB
Conv1 3×5×5×16=12003 \times 5 \times 5 \times 16=1200 46×196×16=14425646 \times 196 \times 16=144256 582.3 KB
Maxpool2 0 15×64×16=1536015 \times 64 \times 16=15360 60 KB
BN1 2×16=322 \times 16=32 15×64×16=1536015 \times 64 \times 16=15360 60.5 KB
Conv2 16×3×3×32=460816 \times 3 \times 3 \times 32=4608 13×62×32=2579213\times 62 \times 32=25792 172.75 KB
Maxpool2 0 4×20×32=25604\times 20 \times 32=2560 10 KB
BN2 2×32=642 \times 32=64 4×20×32=25604\times 20 \times 32=2560 11 KB
Conv3 32×3×3×64=1843232 \times 3 \times 3 \times 64=18432 2×18×64=23042\times 18 \times 64=2304 297 KB
Maxpool3 0 1×9×64=5761 \times 9 \times 64=576 2.3 KB
BN3 2×64=1282\times 64=128 1×9×64=5761 \times 9 \times 64=576 4.3 KB
Fatten 0 576576 2.25 KB
FCN 576×128=73728576\times 128=73728 $128 $ 1152 KB
BN4 2×128=2562 \times 128=256 128128 4.5 KB
Output 128×62×4=31744128 \times 62 \times 4=31744 4×62=2484 \times 62 = 248 497.0 kB

EasyNet

下圖爲EasyNet訓練過程的模型損失曲線,從圖中可以看出,模型在前10個epoch迅速收斂,在20 epoch之後,模型達到相對穩定狀態。從圖中可以看出,驗證集損失相較於訓練集損失,下降比較健康,並且手鍊曲線相對光滑,在後期也未出現驗證集損失波動情況,說明其未發生嚴重過擬合,模型可以被認爲訓練過程可信。

easy-loss

從精度曲線中可以看出,在訓練初期,驗證集上的精度基本能優於訓練集上的精度,這得益於正則化手段,使得模型的子模型也能具有較好的表現,在25個epoch直至更後期,驗證集精度和訓練集精度開始趨於重合,甚至驗證集精度略低於訓練集精度,並且精度不再明顯上升。從精度曲線的光滑程度來看,同樣證明模型在訓練過程中未發生嚴重過擬合,因此模型可信度及有效性較高。

easynet-acc

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。

loss

與損失曲線相反,在前25個epochs中,模型精度提升較快,並且能迅速達到0.9上下,隨後訓練集精度開始小幅度持續上升,而驗證集精度開始出現波動,在70 epochs之後,驗證集上的精度最好能達到0.930.940.93\sim 0.94上下。在調小batch之後,驗證集的精度波動更大,但最好精度與大batch之前相差較小,說明在較大batch下,模型收斂相對較好。

綜合損失曲線與精度曲線,可知,在70 epoch之後,選擇7012070 \sim 120 epoch中損失最低的模型,可基本視爲最佳模型。而在小batch之後,推薦選擇130170130 \sim 170 epoch間的最低損失模型可達到較好效果。

在本項目提供的預訓練模型中,選擇了第169個epoch的模型,其訓練集精度可達0.94。

acc

下圖爲從測試集隨機選擇的5組驗證碼樣本,其中大部分均識別正確(標綠),小部分識別錯誤(標紅)。從標紅的案例中可以看出,該驗證碼認爲識別正確難度仍然較高,因此識別錯誤也可以接受。同時,根據更廣泛的測試集評估研究,模型對於0與O的識別準確度較低,甚至於O大部分被識別爲0,這大程度上地受驗證碼由於字體形變而引發,根據人工對這些特殊案例的對比,部分能夠被人眼正確地分辨,而少部分缺失存在人爲無法準確分辨的案例。可以認爲,認爲地區分0與O,可能有6070%60\sim 70\%成功率,這也同樣對模型的準確度產生了干擾。

由於模型達到了基本可接受的識別準確度,因此再未將識別錯誤的樣本單獨挑出並訓練,從理論上推測, 將分類錯誤的樣本挑出重新分類,可以一定程度地提升模型效果,進行該操作的方法可有兩種:

  1. 將識別錯誤的訓練數據單獨挑出,並重新構成訓練集,並重新訓練,該方式可能使得模型對這些樣本過於擬合,因此訓練的迭代次數需要控制;
  2. 將識別錯誤的樣本標記,再下一輪訓練時,在損失函數上,爲上次識別失敗的樣本增大權重,使得分類錯誤的樣本對模型的提升影響更大,降低正確識別樣本對模型的影響,但是訓練時仍提供正確樣本,能夠避免第一種方法的過擬合。(類似於Boosting)

test
如下圖,計算了模型在預測中的準確率,可見其波動較大,但是對於易於識別的數據,其準確率較高。

ACC指標與Correct指標不同,ACC計算了每個task的準確性,而correct計算了四個字符全部預測正確的比例。

eval

上述模型的提升方法,有條件地可以進一步實驗,以進一步提升模型性能。同時,對於驗證碼識別,還可以考慮使用注意力機制,針對不同的輸出層關注不同的Feature Map,從直觀上理解,應該能一定程度地提升模型的擬合能力,開發者們可以進一步嘗試。

模型源代碼及預訓練模型已經開源至Github,歡迎訪問。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章