CV學習筆記(二十二):CRNN+CTC

作者:雲時之間
來源:知乎
鏈接:https://zhuanlan.zhihu.com/p/142269888
編輯:王萌


上次的一篇文章說了下DenseNet,這一篇文章來說一下CRNN+CTC的識別原理以及實現過程。這篇文章原理部分主要參考於白裳老師的“一文讀懂CRNN+CTC文字識別”,其中的CTC原理的講解部分是我見過最清晰易懂的,值得好好讀一下。

一:OCR識別流程

CV學習筆記(二十二):CRNN+CTC
通常我們做文本識別主要做上圖中的三件事,檢測的方法比較多,這裏說一下識別,之前我們傳統的識別過程是將檢測到的數字劃分爲單字符(投影法等),然後扔到CNN裏去分類,
但是深度學習的發展,出現了end2end的識別方式,簡單的來說就是我們不需要將檢測到的數字劃分爲單字符,無論你檢測的數字長度多長,尺寸多寬,直接從頭到尾給你識別出來。
CV學習筆記(二十二):CRNN+CTC
比如上圖的銀行卡號,傳統的方式要分割成



CV學習筆記(二十二):CRNN+CTC
這樣單個字符,現在直接從左到右識別完整,這樣一來,我們就將單個字符的識別問題轉化爲序列的識別問題。
現在端到端的識別主要有兩種比較流行的方式,以銀行卡OCR識別爲例:
CV學習筆記(二十二):CRNN+CTC
CRNN+CTC,CNN+Seq2Seq+Attention是比較流行的方式,CRNN用的會更廣泛些,因爲Attention機制限制會比較大些,而這兩者最主要的區別也就在這,兩者都拋棄了softmax,而CRNN用了CTC來最後文本對齊,而CNN用了Attention機制,這也是端到端的難點所在:如何處理不定長序列對齊問題



二:CRNN+CTC結構

CRNN(卷積循環神經網絡),顧名思義就是CNN+RNN的組合,論文中也提到,模型既有CNN強大的提取特徵的能力,又有與RNN相同的性質,能夠產生一系列序列化標籤。

CV學習筆記(二十二):CRNN+CTC
整個CRNN分爲了三個部分:
①:卷積層:提取特徵(代碼輸入322561)
②:循環層:使用深層雙向RNN,預測從卷積層獲取的特徵序列的標籤(真實值)分佈(64*512)
③:轉錄層:使用CTC,代替softmax,訓練樣本無需對齊。
這篇文章的難點在於:
①:使用深度雙層RNN
②:使用CTC(CTC原理極其難懂)






三:CRNN代碼

CRNN算法輸入10032歸一化高度的詞條圖像,基於7層CNN(普遍使用VGG16)提取特徵圖,把特徵圖按列切分(Map-to-Sequence),每一列的512維特徵,輸入到兩層各256單元的雙向LSTM進行分類。在訓練過程中,通過CTC損失函數的指導,實現字符位置與類標的近似軟對齊。
以我現在使用的代碼爲例:
CV學習筆記(二十二):CRNN+CTC
我輸入的圖像爲:32


2561,W=256,經過CNN後,W=W/4,此時的W變爲64,此時輸入RNN的圖像爲164*512,此時的T=(W/4)=64,D=512,這裏的T可以認爲是RNN最大時間長度 ,依照本文代碼就是有64個時間時間輸入,且每個輸入的列向量有512.
CNN代碼:7層VGG

   #CNN part
    inputs = Input(shape=(picture_height, picture_width, 1), name='pic_inputs') # H×W×1 32*256*1
    x = Conv2D(64, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_1')(inputs) # 32*256*64 
    x = BatchNormalization(name="BN_1")(x)
    x = Activation("relu", name="relu_1")(x)
    x = MaxPooling2D(pool_size=(2,2), strides=2, padding='valid', name='maxpl_1')(x) # 16*128*64

    x = Conv2D(128, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_2')(x) # 16*128*128
    x = BatchNormalization(name="BN_2")(x)
    x = Activation("relu", name="relu_2")(x)
    x = MaxPooling2D(pool_size=(2,2), strides=2, padding='valid', name='maxpl_2')(x) # 8*64*128

    #卷積兩次以後池化
    x = Conv2D(256, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_3')(x)  # 8*64*256
    x = BatchNormalization(name="BN_3")(x)
    x = Activation("relu", name="relu_3")(x)
    x = Conv2D(256, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_4')(x) # 8*64*256
    x = BatchNormalization(name="BN_4")(x)
    x = Activation("relu", name="relu_4")(x)
    x = MaxPooling2D(pool_size=(2,1), strides=(2,1), name='maxpl_3')(x) # 4*64*256
    # 卷積兩次以後池化
    x = Conv2D(512, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_5')(x) # 4*64*512
    x = BatchNormalization(axis=-1, name='BN_5')(x)
    x = Activation("relu", name='relu_5')(x)
    x = Conv2D(512, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_6')(x) # 4*64*512
    x = BatchNormalization(axis=-1, name='BN_6')(x)
    x = Activation("relu", name='relu_6')(x)
    x = MaxPooling2D(pool_size=(2,1), strides=(2,1), name='maxpl_4')(x) # 2*64*512
    #卷積一層
    x = Conv2D(512, (2,2), strides=(1,1), padding='same', activation='relu', kernel_initializer=initializer, use_bias=True, name='conv2d_7')(x) # 2*64*512
    x = BatchNormalization(name="BN_7")(x)
    x = Activation("relu", name="relu_7")(x)
    conv_otput = MaxPooling2D(pool_size=(2, 1), name="conv_output")(x) # 1*64*512

    # Map2Sequence part
    x = Permute((2, 3, 1), name='permute')(conv_otput) # 64*512*1
    rnn_input = TimeDistributed(Flatten(), name='for_flatten_by_time')(x) # 64*512

RNN代碼:雙向LSTM

    # RNN part,雙向LSTM
    y = Bidirectional(LSTM(256, kernel_initializer=initializer, return_sequences=True), merge_mode='sum', name='LSTM_1')(rnn_input) # 64*512
    y = BatchNormalization(name='BN_8')(y)
    y = Bidirectional(LSTM(256, kernel_initializer=initializer, return_sequences=True), name='LSTM_2')(y) # 64*512

這裏用到了keras中的Bidirectional函數構建雙向LSTM,這裏要說一下深層BLSTM,
CV學習筆記(二十二):CRNN+CTC
首先我們在輸入層之上。套上一層雙向LSTM層。相比RNN,能夠更有效地處理句子中單詞間的長距離影響。而雙向LSTM就是在隱層同一時候有一個正向LSTM和反向LSTM,正向LSTM捕獲了上文的特徵信息,而反向LSTM捕獲了下文的特徵信息,這樣相對單向LSTM來說能夠捕獲很多其它的特徵信息。所以通常情況下雙向LSTM表現比單向LSTM或者單向RNN要好。上圖輸入層之上的那個BLSTM層就是這個第一層雙向LSTM層神經網絡。
我們能夠把神經網絡的深度不斷拓展,就是在第一層BLSTM基礎上。再疊加一層BLSTM,疊加方法就是把每一個輸入相應的BLSTM層的輸出作爲下一層BLSTM神經網絡層相應節點的輸入,由於兩者序列長度是一一相應的,所以非常好疊加這兩層神經網絡。假設你覺得有必要,全然能夠如此不斷疊加更深一層的BLSTM來構造多層深度的BLSTM神經網絡。


三:CTC

CTC的推導部分在白裳的文章中,貼上鍊接:
https://zhuanlan.zhihu.com/p/43534801
這裏我談一下我的理解:
看CTC的訓練過程,CTC在這個階段其實不關心對齊,這一點從ctc_loss的表達式可看出
CV學習筆記(二十二):CRNN+CTC
CTC在訓練時更多的考慮是將可能映射(去重、去空)出的標籤包含的路徑的概率之和來最大化(CTC假設每個時間片的輸出是相互獨立的,則路徑的後驗概率是每個時間片概率的累積),那麼在輸出時根據給定輸入搜索概率最大的路徑時就更可能搜索出能映射到正確結果的路徑。且搜索時考慮了“多對一”的情況,進一步增加了解碼出正確結果的可能性。所以我理解的CTC其實並不在意是否學習好了對齊這個過程,對齊只是尋找結果的一個手段,而CTC只在乎是結果,CTC是可以不需要對齊而能解碼得到正確結果的方法。至少CTC在訓練時不是對齊,但CTC在解碼時,特別是搜索解碼時,參與解碼的部分合法路徑可能是“比較整齊的界限分明的多對一對齊”。
CTC代碼實現方式:
這裏用的keras,keras中ctc_batch_cost函數可以實現CTC:







這裏輸入:args = (y_true, y_pred, pred_length, label_length)
y_true, y_pred分別是預測的標籤和真實的標籤
shape分別是(batch_size,max_label_length)和(batch_size, time_steps, num_categories)
perd_length, label_length分別是保存了每一個樣本所對應的預測標籤長度和真實標籤長度
shape分別是(batch_size, 1)和(batch_size, 1)
輸出:batch_cost 每一個樣本所對應的loss shape是(batch_size, 1)






def ctc_loss_layer(args):
    y_true, y_pred, pred_length, label_length = args
    batch_cost = K.ctc_batch_cost(y_true, y_pred, pred_length, label_length)
    return batch_cost

四:實現效果

CV學習筆記(二十二):CRNN+CTC

CV學習筆記(二十二):CRNN+CTC

訓練速度,迭代速度還是可以的,實際測試:
CV學習筆記(二十二):CRNN+CTC
在圖片分辨率較爲清晰且卡面不花裏胡哨的情況下識別準確度以及很高,但是遇到一些定製卡,效果就差強人意,還需要標註數據,多訓練,不然沒辦法。
PS:這段時間標註了幾千張銀行卡照片,等項目過了之後有條件公佈出來,深度學習離不開數據,理解萬歲~


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