一、引言
OCR識別目前來說已經很成熟了,像百度、騰訊、訊飛等大佬都有自己的OCR識別技術,比如騰訊,可以登入騰訊AI開放平臺:https://ai.qq.com/product/ocr.shtml#handwrite
上傳一張圖片,它可以識別出圖片中的文字及其位置,其準確率還是不錯的。
OCR文字識別主要有兩種經典的方法:
1、字符分割+字符識別
最開始人們想到的是先將一張圖片中的文本中的每個字分割出來,然後對每個字進行單獨識別,最後組合起來形成完整文本。
2、文本定位+文本識別
隨著目標檢測算法的發展,漸漸出現了一種以「文本定位」+「文本識別」的不定長文本識別方法,利用目標檢測算法(如ssd、yolo)定位出每一行字,然後利用CRNN+CTC的方式進行不定長文本識別,直接識別出一行文字。這種方法目前應該是主流的ocr方法。
這裡先講第一種方法,CRNN+CTC的方法下次再詳細討論,這裡不做介紹了。OCR文字識別主要分為對於「印刷體字符」和「手寫體字符」,其中「手寫體字符」更不容易識別,想想自己寫的字就知道了(連自己都認不出來)
好了,下面讓我們開始吧~~~~~~~~~~~~
主要流程如下:
二、準備工作
在做OCR手寫漢字識別之前,需要準備手寫漢字數據集,目前免費公開的數據集有中科院的「CASIA-HWDB脫機手寫漢字數據集」以及北京郵電大學的「HCL2000漢字資料庫」,下載地址如下:
https://pan.baidu.com/s/1ZmQP8hx8DYiYG5i5dr0aGg 提取碼:rxm4
數據集很大,包含了國標一級漢字,一個有3755個漢字,由不同職業的人書寫的,有寫的工整的,也有潦草的,每個數據集包含100w+張手寫漢字圖片。
三、數據預處理
下載下來的數據集是個壓縮包,這裡以HWDB1.1數據集為例,包含一個訓練數據集與一個測試數據集,分別解壓,得到多個xxx.gnt的文件,這裡需要用代碼讀取。
用腳本解析完後可以得到3755個文件夾,每一個文件夾下包含240張手寫漢字圖片。
漢字個數
圖片總數
訓練集
3755
901200
測試集
3755
225300
四、網絡構建
顯而易見,這是一個單標籤多分類問題,就按照分類問題解決即可,類別為3755類,其實可以採用VGG16、resnet等網絡,對其進行稍微修改,進行特徵提取,並將最後一層改成3755類,就可以直接訓練了。
不過基於這裡的漢字圖片均為灰度圖,這裡採用一個更簡單的網絡,參考 https://arxiv.org/abs/1702.07975
這篇文章所講的進行實現。
文中作者構建的網絡結構如下:
Input-96C3-MP3-128C3-MP3-160C3-MP3-256C3-256C3-MP3-384C3-384C3-MP3-1024FC-Output
註:96C3表示96個大小為3x3的卷積層;MP3表示大小為3x3的最大池化層;1024FC表示1024個全連接層。
文中還提到了一種新Trick:
一般卷積層中卷積核大小設置為(3x3)大小,那麼一個卷積核中需要優化的參數就有3x3=9個;如果將卷積核拆成(3x1)和(1x3)兩個卷積核呢?同樣是對原圖片(3x3)區域進行卷積,然而這樣的話所需要優化的參數就變成了1x3+3x1=6個,相比減少了3個(即減少了1/3),這樣不就在達到相同目的的同時降低了模型的複雜度,提高訓練與識別效率。
介紹幾個其它trick:
1)加入BN層:BN層將卷積之後的結果進行歸一化避免卷積之後的數據過大或過小,影響收斂。BN層最初提出是加在卷積層與激活層之間的,但是現在也有許多研究者將其加在激活層之後,效果各有優劣。
2)激活函數:目前CNN中大部分情況下激活函數都是ReLU一族雄霸天下吧,默認都採用ReLU激活函數,不過可以嘗試其變種「ReLU6」或「PReLU(文中方案)」。
3)加入正則化:在機器學習中正則化一直都是防止過擬合的有效手段,常用的有L1和L2正則化,若在訓練過程中出現過擬合現象,試試在全連接層處加入L2正則化試試。
4)優化器:在反向傳播過程中更新參數的策略,由最初的SGD到後來的AdaGrad、RMSProp、Adam等,不能說哪一種最好,需要依據不同問題而定,一般來說後面幾種算法在前期收斂效果要比SGD快,但是很多時候SGD+Momentum可能比其他算法在最後的精度上要好。
5)學習率:學習率的設定也是一個技巧,學習率太大會導致不收斂,太小又會使得收斂速度太慢或者很快收斂到較差的局部最優點。一般學習率策略採用遞減的方式:在初始期間設定較大的學習率(如0.1,可以讓算法快速在全局進行探索,尋找最優目標),然後隨著迭代慢慢減小學習率(後期減小學習率有利於在最優處進行精細探索,免得找到了最優位置一不小心又跳出去了)。
6)初始化權重:權重的初始化方法對於收斂也有很大影響,選擇的不好可能會發生梯度爆炸或梯度消失,導致結果很差。目前各種深度學習框架都提供了很多初始化方法,可以都採用對比。
7)數據增強:數據增強也是一種防止過擬合的有效方法,通過對圖像進行旋轉、偏移、裁剪等操作,提高了樣本的多樣性,使算法更加健壯。
8)Dropout:Dropout層也是防止過擬合的一種方法,它是通過抑制部分網絡節點(將其值置為0)來實現提高網絡健壯性,一般抑制概率設置為0.2-0.5左右,一般放在全連接層之後。
採用Keras構建網絡如下:
from keras.models import Modelfrom keras.layers import Input, Dropout, Reshape, Activation, Flatten, Densefrom keras.layers.convolutional import Conv2D, MaxPooling2Dfrom keras.initializers import orthogonal, constant, he_normalfrom keras.regularizers import l2from keras import backend as Kfrom keras.layers.normalization import BatchNormalizationfrom keras.layers.advanced_activations import PReLUimport configdef relu6(x): """Relu 6 """ return K.relu(x, max_value=6.0)
def net(): inputs = Input(shape=(config.IMAGE_SIZE, config.IMAGE_SIZE, config.NUM_CHANNELS)) x = Conv2D(config.FILTER_NUM[0], (1, 3), padding='same', kernel_initializer=he_normal())(inputs) x = Conv2D(config.FILTER_NUM[0], (3, 1), padding='same', kernel_initializer=he_normal())(x) x = BatchNormalization()(x) x = PReLU()(x) x = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(x)
x = Conv2D(config.FILTER_NUM[1], (1, 3), padding='same', kernel_initializer=he_normal())(x) x = Conv2D(config.FILTER_NUM[1], (3, 1), padding='same', kernel_initializer=he_normal())(x) x = BatchNormalization()(x) x = PReLU()(x) x = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(x)
x = Conv2D(config.FILTER_NUM[2], (1, 3), padding='same', kernel_initializer=he_normal())(x) x = Conv2D(config.FILTER_NUM[2], (3, 1), padding='same', kernel_initializer=he_normal())(x) x = BatchNormalization()(x) x = PReLU()(x) x = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(x)
x = Conv2D(config.FILTER_NUM[3], (1, 3), padding='same', kernel_initializer=he_normal())(x) x = Conv2D(config.FILTER_NUM[3], (3, 1), padding='same', kernel_initializer=he_normal())(x) x = BatchNormalization()(x) x = PReLU()(x) x = Conv2D(config.FILTER_NUM[4], (1, 3), padding='same', kernel_initializer=he_normal())(x) x = Conv2D(config.FILTER_NUM[4], (3, 1), padding='same', kernel_initializer=he_normal())(x) x = BatchNormalization()(x) x = PReLU()(x) x = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(x)
x = Conv2D(config.FILTER_NUM[5], (1, 3), padding='same', kernel_initializer=he_normal())(x) x = Conv2D(config.FILTER_NUM[5], (3, 1), padding='same', kernel_initializer=he_normal())(x) x = BatchNormalization()(x) x = PReLU()(x) x = Conv2D(config.FILTER_NUM[6], (1, 3), padding='same', kernel_initializer=he_normal())(x) x = Conv2D(config.FILTER_NUM[6], (3, 1), padding='same', kernel_initializer=he_normal())(x) x = BatchNormalization()(x) x = PReLU()(x) x = MaxPooling2D(pool_size=(3, 3), strides=2, padding='same')(x)
x = Flatten()(x) x = Dense(config.FILTER_NUM[7], kernel_regularizer=l2(0.005), kernel_initializer=he_normal())(x) x = BatchNormalization()(x) x = PReLU()(x) x = Dropout(0.5)(x)
y = Dense(config.NUM_LABELS, activation='softmax', kernel_initializer=he_normal())(x) model = Model(inputs=inputs, outputs=y) return model五、訓練
模型定義完成,就可以進行訓練了,數據讀取還是採用迭代的方式,用多少讀多少,不然一下子全部讀進內存內存不夠,有近100w張圖片。
優化器這裡採用SGD+Momentum的方式,初始學習率設為0.1,然後隨著迭代慢慢衰減,如下圖所示:
def step_decay(epoch): initial_lrate = 0.1 initial_lrate = initial_lrate * 0.1**(epoch//10) if initial_lrate < 1.0e-5: initial_lrate = 1.0e-5 return initial_lrate
def main(): #----參數設置---- train_size = np.loadtxt(config.TRAIN_FILE, dtype=str).shape[0] val_size = np.loadtxt(config.TEST_FILE, dtype=str).shape[0]
model = net()
# 設置變化的學習率 # learning_rate_reduction = ReduceLROnPlateau(monitor='val_acc', patience=3, verbose=1, factor=0.5, min_lr=0.00001) learning_rate_reduction = LearningRateScheduler(step_decay) learning_rate = 0.1 model.compile(loss='categorical_crossentropy', # optimizer=Adam(lr=1e-4), optimizer=SGD(lr=learning_rate, decay=0, momentum=0.9, nesterov=True), metrics=['accuracy']) # plot_model(model, to_file='model.png', show_shapes=True) model.summary() # 生成圖片讀取迭代器,用多少讀多少,不用一次性全部把圖片讀進內存,這樣可以節省內存。 train = generator(config.TRAIN_FILE, config.BATCH_SIZE, config.IMAGE_SIZE, config.IMAGE_SIZE, True) val = generator(config.TEST_FILE, config.BATCH_SIZE, config.IMAGE_SIZE, config.IMAGE_SIZE, False)
if not os.path.exists('./results/'): os.mkdir('./results') if not os.path.exists('./weights/'): os.mkdir('./weights') # 每一輪保存歷史最好模型,以驗證集準確率為依據 save_file = './weights/best_weights.h5' checkpoint = ModelCheckpoint(save_file, monitor='val_acc', save_best_only=True, save_weights_only=True, verbose=1) history = model.fit_generator(train, steps_per_epoch=train_size // config.BATCH_SIZE, validation_data=val, validation_steps=val_size // config.BATCH_SIZE, epochs=config.EPOCHS, callbacks=[checkpoint, learning_rate_reduction] )訓練過程很漫長,而且對於學習率與優化器的設置很重要,最初採用Adam,學習率設為0.001效果很差,一直沒收斂。
六、測試
訓練時間太長了,這裡我只取訓練中途的模型進行測試(準確率為93.7%)
測試一:
首先需要進行字符分割(這裡不細講,到時候單獨寫一篇關於分割的文章,分割其實是整個模塊中最難的...),將文字分割成如下:
識別結果如下:
只有一個字識別錯了:「嬋」 錯誤識別為 「嬸」(字體結構太相似)
測試二:
如果拿來識別印刷字又如何呢?我們試試看:
識別結果如下:
可見,對於印刷體漢字也是可以準確識別的。
七、總結
總的來說,對於單個字的識別還是很簡單的,無論是印刷還是手寫,漢字、數字還是英文都可以用這種方法,難點是在於分割,如何提高字符分割的準確性是提高整體識別準確率的關鍵,尤其是手寫字符,分割起來尤為困難,正因為如此,這種方法漸漸被淘汰,被CRNN+CTC這類的「文字定位+不定長文本識別」所代替,對於文字定位目前的方法也很多,yolo,ssd之類的都很有名,這部分下次再細講。