https://blog.csdn.net/c2a2o2/article/details/68490189
本文會通過 Keras 搭建一個深度卷積神經網絡來識別 captcha 驗證碼,建議使用顯卡來運行該項目。
下面的可視化代碼都是在 jupyter notebook 中完成的,如果你希望寫成 python 腳本,稍加修改即可正常運行,當然也可以去掉這些可視化代碼。Keras 版本:1.2.2。
GitHub 地址:https://github.com/ypwhs/captcha_breakhttps://github.com/ypwhs/captcha_break
captcha 是用 python 寫的生成驗證碼的庫,它支持圖片驗證碼和語音驗證碼,我們使用的是它生成圖片驗證碼的功能。
首先我們設置我們的驗證碼格式為數字加大寫字母,生成一串驗證碼試試看:
-
from captcha.image import ImageCaptcha
-
import matplotlib.pyplot as plt
-
import numpy as np
-
import random
-
-
%matplotlib inline
-
%config InlineBackend.figure_format = 'retina'
-
-
import string
-
characters = string.digits + string.ascii_uppercase
-
print(characters)
-
-
width, height, n_len, n_class = 170, 80, 4, len(characters)
-
-
generator = ImageCaptcha(width=width, height=height)
-
random_str = ''.join([random.choice(characters) for j in range(4)])
-
img = generator.generate_image(random_str)
-
-
plt.imshow(img)
-
plt.title(random_str)
數據生成器
訓練模型的時候,我們可以選擇兩種方式來生成我們的訓練數據,一種是一次性生成幾萬張圖,然后開始訓練,一種是定義一個數據生成器,然后利用 fit_generator 函數來訓練。
第一種方式的好處是訓練的時候顯卡利用率高,如果你需要經常調參,可以一次生成,多次使用;第二種方式的好處是你不需要生成大量數據,訓練過程中可以利用 CPU 生成數據,而且還有一個好處是你可以無限生成數據。
我們的數據格式如下:
X
X 的形狀是 (batch_size, height, width, 3),比如一批生成32個樣本,圖片寬度為170,高度為80,那么形狀就是 (32, 80, 170, 3),取第一張圖就是 X[0]。
y
y 的形狀是四個 (batch_size, n_class),如果轉換成 numpy 的格式,則是 (n_len, batch_size, n_class),比如一批生成32個樣本,驗證碼的字符有36種,長度是4位,那么它的形狀就是4個 (32, 36),也可以說是 (4, 32, 36),解碼函數在下個代碼塊。
-
def gen(batch_size=32):
-
X = np.zeros((batch_size, height, width, 3), dtype=np.uint8)
-
y = [np.zeros((batch_size, n_class), dtype=np.uint8) for i in range(n_len)]
-
generator = ImageCaptcha(width=width, height=height)
-
while True:
-
for i in range(batch_size):
-
random_str = ''.join([random.choice(characters) for j in range(4)])
-
X[i] = generator.generate_image(random_str)
-
for j, ch in enumerate(random_str):
-
y[j][i, :] = 0
-
y[j][i, characters.find(ch)] = 1
-
yield X, y
上面就是一個可以無限生成數據的例子,我們將使用這個生成器來訓練我們的模型。
使用生成器
生成器的使用方法很簡單,只需要用 next 函數即可。下面是一個例子,生成32個數據,然后顯示第一個數據。當然,在這里我們還對生成的 One-Hot 編碼后的數據進行了解碼,首先將它轉為 numpy 數組,然后取36個字符中最大的數字的位置,因為神經網絡會輸出36個字符的概率,然后將概率最大的四個字符的編號轉換為字符串。
-
def decode(y):
-
y = np.argmax(np.array(y), axis=2)[:,0]
-
return ''.join([characters[x] for x in y])
-
-
X, y = next(gen(1))
-
plt.imshow(X[0])
-
plt.title(decode(y))
構建深度卷積神經網絡
from keras.models import *
from keras.layers import *
input_tensor = Input((height, width, 3))
x = input_tensor
for i in range(4):
x = Convolution2D(32*2**i, 3, 3, activation='relu')(x)
x = Convolution2D(32*2**i, 3, 3, activation='relu')(x)
x = MaxPooling2D((2, 2))(x)
x = Flatten()(x)
x = Dropout(0.25)(x)
x = [Dense(n_class, activation='softmax', name='c%d'%(i+1))(x) for i in range(4)]
model = Model(input=input_tensor, output=x)
model.compile(loss='categorical_crossentropy',
optimizer='adadelta',
metrics=['accuracy'])
模型結構很簡單,特征提取部分使用的是兩個卷積,一個池化的結構,這個結構是學的 VGG16 的結構。之后我們將它 Flatten,然后添加 Dropout ,盡量避免過擬合問題,最后連接四個分類器,每個分類器是36個神經元,輸出36個字符的概率。
模型可視化
得益於 Keras 自帶的可視化,我們可以使用幾句代碼來可視化模型的結構:
-
from keras.utils.visualize_util import plot
-
from IPython.display import Image
-
-
plot(model, to_file="model.png", show_shapes=True)
-
Image('model.png')
這里需要使用 pydot 這個庫,以及 graphviz 這個庫,在 macOS 系統上安裝方法如下:
-
brew install graphviz
-
pip install pydot-ng
我們可以看到最后一層卷積層輸出的形狀是 (1, 6, 256),已經不能再加卷積層了。
訓練模型
訓練模型反而是所有步驟里面最簡單的一個,直接使用 model.fit_generator 即可,這里的驗證集使用了同樣的生成器,由於數據是通過生成器隨機生成的,所以我們不用考慮數據是否會重復。注意,這段代碼在筆記本上可能要耗費一下午時間。如果你想讓模型預測得更准確,可以將 nb_epoch改為 10 或者 20,但它也將耗費成倍的時間。注意我們這里使用了一個小技巧,添加 nb_worker=2 參數讓 Keras 自動實現多進程生成數據,擺脫 python 單線程效率低的缺點。如果不添加,耗時120秒,添加則只需80秒。
-
model.fit_generator(gen(), samples_per_epoch=51200, nb_epoch=5,
-
nb_worker=2, pickle_safe=True,
-
validation_data=gen(), nb_val_samples=1280)
測試模型
當我們訓練完成以后,可以識別一個驗證碼試試看:
-
X, y = next(gen(1))
-
y_pred = model.predict(X)
-
plt.title('real: %s\npred:%s'%(decode(y), decode(y_pred)))
-
plt.imshow(X[0], cmap='gray')
計算模型總體准確率
模型在訓練的時候只會顯示第幾個字符的准確率,為了統計模型的總體准確率,我們可以寫下面的函數:
-
from tqdm import tqdm
-
def evaluate(model, batch_num=20):
-
batch_acc = 0
-
generator = gen()
-
for i in tqdm(range(batch_num)):
-
X, y = next(generator)
-
y_pred = model.predict(X)
-
y_pred = np.argmax(y_pred, axis=2).T
-
y_true = np.argmax(y, axis=2).T
-
batch_acc += np.mean(map(np.array_equal, y_true, y_pred))
-
return batch_acc / batch_num
-
-
evaluate(model)
這里用到了一個庫叫做 tqdm,它是一個進度條的庫,為的是能夠實時反饋進度。然后我們通過一些 numpy 計算去統計我們的准確率,這里計算規則是只要有一個錯,那么就不算它對。經過計算,我們的模型的總體准確率在經過五代訓練就可以達到 90%,繼續訓練還可以達到更高的准確率。
模型總結
模型的大小是16MB,在我的筆記本上跑1000張驗證碼需要用20秒,當然,顯卡會更快。對於驗證碼識別的問題來說,哪怕是10%的准確率也已經稱得上破解,畢竟假設100%識別率破解要一個小時,那么10%的識別率也只用十個小時,還算等得起,而我們的識別率有90%,已經可以稱得上完全破解了這類驗證碼。
改進
對於這種按順序書寫的文字,我們還有一種方法可以使用,那就是循環神經網絡來識別序列。下面我們來了解一下如何使用循環神經網絡來識別這類驗證碼。
CTC Loss
這個 loss 是一個特別神奇的 loss,它可以在只知道序列的順序,不知道具體位置的情況下,讓模型收斂。在這方面百度似乎做得很不錯,利用它來識別音頻信號。(warp-ctc)
那么在 Keras 里面,CTC Loss 已經內置了,我們直接定義這樣一個函數,即可實現 CTC Loss,由於我們使用的是循環神經網絡,所以默認丟掉前面兩個輸出,因為它們通常無意義,且會影響模型的輸出。
- y_pred 是模型的輸出,是按順序輸出的37個字符的概率,因為我們這里用到了循環神經網絡,所以需要一個空白字符的概念;
- labels 是驗證碼,是四個數字;
- input_length 表示 y_pred 的長度,我們這里是15;
- label_length 表示 labels 的長度,我們這里是4。
-
from keras import backend as K
-
-
def ctc_lambda_func(args):
-
y_pred, labels, input_length, label_length = args
-
y_pred = y_pred[:, 2:, :]
-
return K.ctc_batch_cost(labels, y_pred, input_length, label_length)
模型結構
我們的模型結構是這樣設計的,首先通過卷積神經網絡去識別特征,然后經過一個全連接降維,再按水平順序輸入到一種特殊的循環神經網絡,叫 GRU,它具有一些特殊的性質,為什么用 GRU 而不用 LSTM 呢?總的來說就是它的效果比 LSTM 好,所以我們用它。
-
from keras.models import *
-
from keras.layers import *
-
rnn_size = 128
-
-
input_tensor = Input((width, height, 3))
-
x = input_tensor
-
for i in range(3):
-
x = Convolution2D(32, 3, 3, activation='relu')(x)
-
x = Convolution2D(32, 3, 3, activation='relu')(x)
-
x = MaxPooling2D(pool_size=(2, 2))(x)
-
-
conv_shape = x.get_shape()
-
x = Reshape(target_shape=(int(conv_shape[1]), int(conv_shape[2]*conv_shape[3])))(x)
-
-
x = Dense(32, activation='relu')(x)
-
-
gru_1 = GRU(rnn_size, return_sequences=True, init='he_normal', name='gru1')(x)
-
gru_1b = GRU(rnn_size, return_sequences=True, go_backwards=True,
-
init='he_normal', name='gru1_b')(x)
-
gru1_merged = merge([gru_1, gru_1b], mode='sum')
-
-
gru_2 = GRU(rnn_size, return_sequences=True, init='he_normal', name='gru2')(gru1_merged)
-
gru_2b = GRU(rnn_size, return_sequences=True, go_backwards=True,
-
init='he_normal', name='gru2_b')(gru1_merged)
-
x = merge([gru_2, gru_2b], mode='concat')
-
x = Dropout(0.25)(x)
-
x = Dense(n_class, init='he_normal', activation='softmax')(x)
-
base_model = Model(input=input_tensor, output=x)
-
-
labels = Input(name='the_labels', shape=[n_len], dtype='float32')
-
input_length = Input(name='input_length', shape=[1], dtype='int64')
-
label_length = Input(name='label_length', shape=[1], dtype='int64')
-
loss_out = Lambda(ctc_lambda_func, output_shape=(1,),
-
name='ctc')([x, labels, input_length, label_length])
-
-
model = Model(input=[input_tensor, labels, input_length, label_length], output=[loss_out])
-
model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer='adadelta')
模型可視化
可視化的代碼同上,這里只貼圖。
可以看到模型比上一個模型復雜了許多,但實際上只是因為輸入比較多,所以它顯得很大。還有一個值得注意的地方,我們的圖片在輸入的時候是經過了旋轉的,這是因為我們希望以水平方向輸入,而圖片在 numpy 里默認是這樣的形狀:(height, width, 3),因此我們使用了 transpose 函數將圖片轉為了(width, height, 3)的格式,然后經過各種卷積和降維,變成了 (17, 32),這里的每個長度為32的向量都代表一個豎條的圖片的特征,從左到右,一共有17條。然后我們兵分兩路,一路從左到右輸入到 GRU,一路從右到左輸入到 GRU,然后將他們輸出的結果加起來。再兵分兩路,還是一路正方向,一路反方向,只不過第二次我們直接將它們的輸出連起來,然后經過一個全連接,輸出每個字符的概率。
數據生成器
-
def gen(batch_size=128):
-
X = np.zeros((batch_size, width, height, 3), dtype=np.uint8)
-
y = np.zeros((batch_size, n_len), dtype=np.uint8)
-
while True:
-
generator = ImageCaptcha(width=width, height=height)
-
for i in range(batch_size):
-
random_str = ''.join([random.choice(characters) for j in range(4)])
-
X[i] = np.array(generator.generate_image(random_str)).transpose(1, 0, 2)
-
y[i] = [characters.find(x) for x in random_str]
-
yield [X, y, np.ones(batch_size)*int(conv_shape[1]-2),
-
np.ones(batch_size)*n_len], np.ones(batch_size)
評估模型
-
def evaluate(model, batch_num=10):
-
batch_acc = 0
-
generator = gen()
-
for i in range(batch_num):
-
[X_test, y_test, _, _], _ = next(generator)
-
y_pred = base_model.predict(X_test)
-
shape = y_pred[:,2:,:].shape
-
ctc_decode = K.ctc_decode(y_pred[:,2:,:],
-
input_length=np.ones(shape[0])*shape[1])[0][0]
-
out = K.get_value(ctc_decode)[:, :4]
-
if out.shape[1] == 4:
-
batch_acc += ((y_test == out).sum(axis=1) == 4).mean()
-
return batch_acc / batch_num
我們會通過這個函數來評估我們的模型,和上面的評估標准一樣,只有全部正確,我們才算預測正確,中間有個坑,就是模型最開始訓練的時候,並不一定會輸出四個字符,所以我們如果遇到所有的字符都不到四個的時候,就不計算了,相當於加0,遇到多於4個字符的時候,只取前四個。
評估回調
因為 Keras 沒有針對這種輸出計算准確率的選項,因此我們需要自定義一個回調函數,它會在每一代訓練完成的時候計算模型的准確率。
-
from keras.callbacks import *
-
class Evaluate(Callback):
-
def __init__(self):
-
self.accs = []
-
-
def on_epoch_end(self, epoch, logs=None):
-
acc = evaluate(base_model)*100
-
self.accs.append(acc)
-
print
-
print 'acc: %f%%'%acc
-
evaluator = Evaluate()
訓練模型
由於 CTC Loss 收斂很慢,所以我們需要設置比較大的代數,這里我們設置了100代,然后添加了一個早期停止的回調和我們上面定義的回調,但是第一次訓練只訓練37代就停了,測試准確率才95%,我又在這個基礎上繼續訓練了一次,停在了25代,得到了98%的准確率,所以一共訓練了62代。
model.fit_generator(gen(), samples_per_epoch=51200, nb_epoch=100,
callbacks=[evaluator],
nb_worker=2, pickle_safe=True)
測試模型
-
characters2 = characters + ' '
-
[X_test, y_test, _, _], _ = next(gen(1))
-
y_pred = base_model.predict(X_test)
-
y_pred = y_pred[:,2:,:]
-
out = K.get_value(K.ctc_decode(y_pred, input_length=np.ones(y_pred.shape[0])*y_pred.shape[1], )[0][0])[:, :4]
-
out = ''.join([characters[x] for x in out[0]])
-
y_true = ''.join([characters[x] for x in y_test[0]])
-
-
plt.imshow(X_test[0].transpose(1, 0, 2))
-
plt.title('pred:' + str(out) + '\ntrue: ' + str(y_true))
-
-
argmax = np.argmax(y_pred, axis=2)[0]
-
list(zip(argmax, ''.join([characters2[x] for x in argmax])))
這次隨機出來的驗證碼很厲害,是O0OP,不過更厲害的是模型認出來了。
有趣的問題
我又用之前的模型做了個測試,對於 O0O0 這樣喪心病狂的驗證碼,模型偶爾也能正確識別,這讓我非常驚訝,它是真的能識別 O 與 0 的差別呢,還是猜出來的呢?這很難說。
-
generator = ImageCaptcha(width=width, height=height)
-
random_str = 'O0O0'
-
X = generator.generate_image(random_str)
-
X = np.expand_dims(X, 0)
-
-
y_pred = model.predict(X)
-
plt.title('real: %s\npred:%s'%(random_str, decode(y_pred)))
-
plt.imshow(X[0], cmap='gray')
總結
模型的大小是4.7MB,在我的筆記本上跑1000張驗證碼需要用14秒,平均一秒識別71張,估計可以拼過網速。至於深度學習到底能不能識別雙胞胎,相信各位已經有了答案。
參考鏈接: