用Tensorflow2實現卷積神經網路(CNN)識別圖片驗證碼
highlight: ocean
1. 前言
最近想要實現一個驗證碼識別的功能,在神經網路和計算機圖形學之間搖擺了一下,但是卷積神經網路能實現“端到端”,亦即輸入圖片,輸出驗證碼的驗證碼識別,就拋棄了CV選擇了CNN。
其實原本我比較熟悉pytorch
的,甚至現成的pytorch + CUDA + cudnn
環境都已經有了,但無奈Tensorflow
有Tensorflow.js
可以與原本選擇的跨平臺解決方案Electron
完美結合,就背棄了pytorch
,轉投Tensorflow
懷抱。
經過一段時間綜合研(mo)究(gai)了官方文件+github+stackoverflow+知乎+csdn的各方程式碼,終於可以用我的GTX1660煉丹了!
2. 實現
2.1 圖片預處理
首先第一步要做的就是圖片預處理,起碼一個灰度模式要有吧?
來看看要處理的驗證碼圖片:
可以看見裡面有兩條線一條紅的,一條綠的,貫穿了圖片。理論上來說,可以通過(原圖-紅-綠)的影象處理手段去掉這兩條線。
然而既然都用上卷積神經網路了,就不搞這麼多花裡胡哨的了,直接莽上去。
幾行程式碼實現把圖片灰度化然後去掉周圍的黑線:
python
img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)
np_img = np.array(img)
flat_img = np_img.flatten()
ind = np.argmax(np.bincount(flat_img))
if(ind < 255):
np_img[0] = ind
np_img[-1] = ind
np_img[:, 0] = ind
np_img[:, -1] = ind
img = np_img / 255.0
輸出圖片:
當然這裡的圖片處理只是因為我的搭的網路模型對輸入資料的shape沒有要求,事實上有不少神經網路對輸入資料的形狀有要求的,比如InceptionV4(299,299)
,ResNet_18(224,224)
等,因此也可以基於opencv-python
擴充套件圖片,之前做了一種實現是先用cv2.copyMakeBorder
把短邊弄到和長邊一樣長,然後再cv2.resize
。
很好,接下來就是採集足夠多的圖片驗證碼以供訓練了。
後來原本還做了二值化處理,但後來發現只灰度表現也不錯,就去掉了二值化。
2.2 卷積神經網路搭建與訓練
2.2.1 訓練過程
經過幾天的採集,手工標註了幾百張圖(後來擴充套件到了18000+張)作為訓練集,可以開始嘗試煉丹了。
不得不說[email protected]
時代太輝煌了,現在找到的大多數是1.x
版本的原始碼。
於是看了看github上面一些倉庫後就參考官方文件的tutorial開始手擼(魔改)。
思路就是把訓練集中標註好的驗證碼圖片按照對應的字符集用One-Hot
喂進模型。從前面的驗證碼圖可以看出,它一個圖有六個字元,每個字元共有10個數字+26個大寫字母=36種可能性。因此它是一個多分類(multi-class
)模型,總的類別是6*36=216
,損失函式categorical_crossentropy
,啟用函式softmax
。
以下是搭建模型的程式碼部分(當然主體是在一個類裡面的,略去了)
python
def generate_model(self):
self.model = model = models.Sequential()
input_shape=(self.image_height, self.image_width, 1)
# conventional
model.add(layers.Conv2D(96, (3, 3), activation='relu',padding="SAME"))
model.add(layers.Conv2D(96, (3, 3), activation='relu',padding="SAME"))
model.add(layers.MaxPooling2D((2, 2)))
# conv2
model.add(layers.Conv2D(128, (3, 3), activation='relu',padding="SAME"))
model.add(layers.Conv2D(128, (3, 3), activation='relu',padding="SAME"))
model.add(layers.Conv2D(128, (3, 3), activation='relu',padding="SAME"))
model.add(layers.MaxPooling2D((2, 2)))
# conv3
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
# conv4
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
# flatten
model.add(layers.Flatten())
# fc1
model.add(layers.Dense(384, activation='relu'))
model.add(layers.AlphaDropout(rate=0.2))
# fc2
model.add(layers.Dense(512, activation='relu'))
model.add(layers.AlphaDropout(rate=0.2))
# output
model.add(layers.Dense(self.max_captcha*self.char_set_len, activation='softmax'))
model.add(layers.Reshape((self.max_captcha,self.char_set_len)))
model.summary()
model.compile(optimizer=tf.keras.optimizers.Nadam(learning_rate=1e-5, clipnorm=1),
loss='categorical_crossentropy',
metrics=['accuracy'])
return model
compile出來的網路模型長這樣:
```markdown Model: "sequential"
Layer (type) Output Shape Param #
conv2d (Conv2D) (None, 80, 300, 96) 960
conv2d_1 (Conv2D) (None, 80, 300, 96) 83040
conv2d_2 (Conv2D) (None, 80, 300, 96) 83040
max_pooling2d (MaxPooling2D (None, 40, 150, 96) 0 )
conv2d_3 (Conv2D) (None, 40, 150, 128) 110720
conv2d_4 (Conv2D) (None, 40, 150, 128) 147584
conv2d_5 (Conv2D) (None, 40, 150, 128) 147584
max_pooling2d_1 (MaxPooling (None, 20, 75, 128) 0 2D)
conv2d_6 (Conv2D) (None, 18, 73, 128) 147584
conv2d_7 (Conv2D) (None, 16, 71, 128) 147584
max_pooling2d_2 (MaxPooling (None, 8, 35, 128) 0 2D)
conv2d_8 (Conv2D) (None, 6, 33, 128) 147584
conv2d_9 (Conv2D) (None, 4, 31, 128) 147584
max_pooling2d_3 (MaxPooling (None, 2, 15, 128) 0 2D)
flatten (Flatten) (None, 3840) 0
dense (Dense) (None, 384) 1474944
alpha_dropout (AlphaDropout (None, 384) 0 )
dense_1 (Dense) (None, 512) 197120
alpha_dropout_1 (AlphaDropo (None, 512) 0 ut)
dense_2 (Dense) (None, 216) 110808
reshape (Reshape) (None, 6, 36) 0
================================================================= Total params: 2,946,136 Trainable params: 2,946,136 Non-trainable params: 0 ```
眾所周知,對網路的優化可以通過增加網路層數(depth,比如從 ResNet (He et al.)從resnet18到resnet200 ), 也可以通過增加寬度,比如WideResNet (Zagoruyko & Komodakis, 2016)和Mo-bileNets (Howard et al., 2017) 可以擴大網路的width (#channels), 還有就是更大的輸入影象尺寸(resolution)也可以幫助提高精度。——知乎老哥
原本魔改出來的程式碼應用的模型是複用了Alexnet的,但是計算複雜,層數多,引數太多,模型檔案也大,練了一會發現收斂也超級慢。後來結合應用場景進行了一定的trade-off得出了目前這個模型。
不得不說tf2比tf1好上手多了!應用model.fit
方法可以開始煉丹了!
我在model.fit
裡面加了兩個callback,一個是儲存斷點,一個是儲存歷史資料。
```python def train_cnn(self): x_train, y_train = self.get_train_data() cp_path = './cp.h5' print(np.any(np.isnan(y_train))) print(y_train[0]) save_chec_points = tf.keras.callbacks.ModelCheckpoint( filepath=cp_path, save_weights_only=False, save_best_only=True) try: model = tf.keras.models.load_model(self.model_save_dir) except Exception as e: model = self.generate_model() try: model.load_weights(cp_path) except Exception as e: pass filename = 'log.csv' history_logger = tf.keras.callbacks.CSVLogger( filename, separator=",", append=True)
history = model.fit(x_train, y_train, batch_size=24,
epochs=200, validation_split=0.1, callbacks=[save_chec_points, history_logger],
shuffle=True)
model.save(self.model_save_dir)
plt.plot(history.history['accuracy'], label='accuracy')
plt.plot(history.history['val_accuracy'], label='val_accuracy')
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.ylim([0, 1])
plt.legend(loc='lower right')
plt.show()
```
在callback裡面加了兩個函式,主要是要儲存checkpoint和每個epoch的資料。
找了個一天長時間開著電腦煉丹,練了200個epoch,畫出來的影象如下(忘了畫loss
, nevermind):
可以看到訓練完兩百個epoch,模型的accuracy
已經有0.9左右。然後我又再來了50個epoch,穩定在0.94。
一百張圖片這個人工智障它能大概看懂94張,這個效能可以了!作為沒有調參的產物還要什麼腳踏車。
2.2.2 踩坑記錄
寫程式碼過程中,總是難免要踩坑。
首先是踩了個天坑,每次一訓練,在第一個epoch裡面loss
就會極速增大直到變成NaN
...這是啥子情況呢?明明損失函式和啟用函式好像都沒錯呀!
找了好久才發現!
原來是因為我的model
程式碼是改官方文件tutorial
的,裡面的最後一步到啟用函式softmax
就結束了,而training
的部分又是從別人的tf1
程式碼魔改的...但是找出來categorical_crossentropy
的文件,它是需要reshape
的,亦即輸出的不應該是(1,216)
而是形如(6,36)
。
在complie
前加一層model.add(layers.Reshape((self.max_captcha,self.char_set_len)))
,把它改成(6,36)
形狀的輸出,問題可解。
然後,又發現總是煉到一半隨機在某一個epoch裡面loss
會變成NaN
,把歷史記錄拿出來看毫無頭緒;按照一些網站查了輸入和label是不是NaN
,發現也不關事。
結果發現優化演算法是Adam
,損失函式是categorical_crossentropy
的時候,才會有這種無端變NaN
的現象發生,把優化演算法改成sgd
或者其他都不會復現...
後來又訓練了別的驗證碼識別模型,發現加上BatchNormalization
層收斂更快,每個epoch的lost和accuracy變化更穩定。
如果要predict也很簡單,直接model.predit
就可,輸入(N,80,300,1)
的張量,輸出(N,6,36)
,再把輸出的tensor按照onehot的順序匹配即可。
3. 總結
雖然只是一個Toy Project
,但我還是傾注了很多心血的,從配環境到寫程式碼到煉丹,第一次成功炮通Tensorflow2
的專案。這一路走來,很感激我的GTX1660的默默付出!
這應該是我2021年最後一篇技術類文章惹,完結撒花!