日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

使用 Keras搭建一個深度卷積神經(jīng)網(wǎng)絡(luò)來識別 c驗證碼

 LibraryPKU 2018-07-18


大數(shù)據(jù)挖掘DT機器學(xué)習(xí)  公眾號: datayx


本文會通過 Keras 搭建一個深度卷積神經(jīng)網(wǎng)絡(luò)來識別驗證碼,建議使用顯卡來運行該項目。

下面的可視化代碼都是在 jupyter notebook 中完成的,如果你希望寫成 python 腳本,稍加修改即可正常運行,當(dāng)然也可以去掉這些可視化代碼。Keras 版本:1.2.2。

captcha

captcha 是用 python 寫的生成驗證碼的庫,它支持圖片驗證碼和語音驗證碼,我們使用的是它生成圖片驗證碼的功能。

首先我們設(shè)置我們的驗證碼格式為數(shù)字加大寫字母,生成一串驗證碼試試看:




數(shù)據(jù)生成器

訓(xùn)練模型的時候,我們可以選擇兩種方式來生成我們的訓(xùn)練數(shù)據(jù),一種是一次性生成幾萬張圖,然后開始訓(xùn)練,一種是定義一個數(shù)據(jù)生成器,然后利用 fit_generator 函數(shù)來訓(xùn)練。


第一種方式的好處是訓(xùn)練的時候顯卡利用率高,如果你需要經(jīng)常調(diào)參,可以一次生成,多次使用;第二種方式的好處是你不需要生成大量數(shù)據(jù),訓(xùn)練過程中可以利用 CPU 生成數(shù)據(jù),而且還有一個好處是你可以無限生成數(shù)據(jù)。

我們的數(shù)據(jù)格式如下:


X

X 的形狀是 (batch_size, height, width, 3),比如一批生成32個樣本,圖片寬度為170,高度為80,那么形狀就是 (32, 80, 170, 3),取第一張圖就是 X[0]。


y

y 的形狀是四個 (batch_size, n_class),如果轉(zhuǎn)換成 numpy 的格式,則是 (n_len, batch_size, n_class),比如一批生成32個樣本,驗證碼的字符有36種,長度是4位,那么它的形狀就是4個 (32, 36),也可以說是 (4, 32, 36),解碼函數(shù)在下個代碼塊。


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


上面就是一個可以無限生成數(shù)據(jù)的例子,我們將使用這個生成器來訓(xùn)練我們的模型。

使用生成器

生成器的使用方法很簡單,只需要用 next 函數(shù)即可。下面是一個例子,生成32個數(shù)據(jù),然后顯示第一個數(shù)據(jù)。當(dāng)然,在這里我們還對生成的 One-Hot 編碼后的數(shù)據(jù)進(jìn)行了解碼,首先將它轉(zhuǎn)為 numpy 數(shù)組,然后取36個字符中最大的數(shù)字的位置,因為神經(jīng)網(wǎng)絡(luò)會輸出36個字符的概率,然后將概率最大的四個字符的編號轉(zhuǎn)換為字符串。

1
2
3
4
5
6
7
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))

構(gòu)建深度卷積神經(jīng)網(wǎng)絡(luò)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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'])

模型結(jié)構(gòu)很簡單,特征提取部分使用的是兩個卷積,一個池化的結(jié)構(gòu),這個結(jié)構(gòu)是學(xué)的 VGG16 的結(jié)構(gòu)。之后我們將它 Flatten,然后添加 Dropout ,盡量避免過擬合問題,最后連接四個分類器,每個分類器是36個神經(jīng)元,輸出36個字符的概率。

模型可視化

得益于 Keras 自帶的可視化,我們可以使用幾句代碼來可視化模型的結(jié)構(gòu):

1
2
3
4
5
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 系統(tǒng)上安裝方法如下:

1
2
brew install graphviz
pip install pydot-ng


我們可以看到最后一層卷積層輸出的形狀是 (1, 6, 256),已經(jīng)不能再加卷積層了。

訓(xùn)練模型

訓(xùn)練模型反而是所有步驟里面最簡單的一個,直接使用 model.fit_generator 即可,這里的驗證集使用了同樣的生成器,由于數(shù)據(jù)是通過生成器隨機生成的,所以我們不用考慮數(shù)據(jù)是否會重復(fù)。注意,這段代碼在筆記本上可能要耗費一下午時間。如果你想讓模型預(yù)測得更準(zhǔn)確,可以將 nb_epoch改為 10 或者 20,但它也將耗費成倍的時間。注意我們這里使用了一個小技巧,添加 nb_worker=2參數(shù)讓 Keras 自動實現(xiàn)多進(jìn)程生成數(shù)據(jù),擺脫 python 單線程效率低的缺點。

1
2
3
model.fit_generator(gen(), samples_per_epoch=51200, nb_epoch=5,
                   nb_worker=2, pickle_safe=True,
                   validation_data=gen(), nb_val_samples=1280)


測試模型

當(dāng)我們訓(xùn)練完成以后,可以識別一個驗證碼試試看:

1
2
3
4
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')



計算模型總體準(zhǔn)確率


模型在訓(xùn)練的時候只會顯示每一個字符的準(zhǔn)確率,為了統(tǒng)計模型的總體準(zhǔn)確率,我們可以寫下面的函數(shù):

1
2
3
4
5
6
7
8
9
10
11
12
13
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,它是一個進(jìn)度條的庫,為的是能夠?qū)崟r反饋進(jìn)度。然后我們通過一些 numpy 計算去統(tǒng)計我們的準(zhǔn)確率,這里計算規(guī)則是只要有一個錯,那么就不算它對。經(jīng)過計算,我們的模型的總體準(zhǔn)確率在經(jīng)過五代訓(xùn)練就可以達(dá)到 90%,繼續(xù)訓(xùn)練還可以達(dá)到更高的準(zhǔn)確率。

模型總結(jié)

模型的大小是16MB,在我的筆記本上跑1000張驗證碼需要用20秒,當(dāng)然,顯卡會更快。對于驗證碼識別的問題來說,哪怕是10%的準(zhǔn)確率也已經(jīng)稱得上破解,畢竟假設(shè)100%識別率破解要一個小時,那么10%的識別率也只用十個小時,還算等得起,而我們的識別率有90%,已經(jīng)可以稱得上完全破解了這類驗證碼。


改進(jìn)

對于這種按順序書寫的文字,我們還有一種方法可以使用,那就是循環(huán)神經(jīng)網(wǎng)絡(luò)來識別序列。下面我們來了解一下如何使用循環(huán)神經(jīng)網(wǎng)絡(luò)來識別這類驗證碼。

CTC Loss

這個 loss 是一個特別神奇的 loss,它可以在只知道序列的順序,不知道具體位置的情況下,讓模型收斂。在這方面百度似乎做得很不錯,利用它來識別音頻信號。(warp-ctc)

https://github.com/baidu-research/warp-ctc



那么在 Keras 里面,CTC Loss 已經(jīng)內(nèi)置了,我們直接定義這樣一個函數(shù),即可實現(xiàn) CTC Loss,由于我們使用的是循環(huán)神經(jīng)網(wǎng)絡(luò),所以默認(rèn)丟掉前面兩個輸出,因為它們通常無意義,且會影響模型的輸出。

  • y_pred 是模型的輸出,是按順序輸出的37個字符的概率,因為我們這里用到了循環(huán)神經(jīng)網(wǎng)絡(luò),所以需要一個空白字符的概念;

  • labels 是驗證碼,是四個數(shù)字;

  • input_length 表示 y_pred 的長度,我們這里是15;

  • label_length 表示 labels 的長度,我們這里是4。

1
2
3
4
5
6
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)

模型結(jié)構(gòu)

我們的模型結(jié)構(gòu)是這樣設(shè)計的,首先通過卷積神經(jīng)網(wǎng)絡(luò)去識別特征,然后經(jīng)過一個全連接降維,再按水平順序輸入到一種特殊的循環(huán)神經(jīng)網(wǎng)絡(luò),叫 GRU,它具有一些特殊的性質(zhì),為什么用 GRU 而不用 LSTM 呢?總的來說就是它的效果比 LSTM 好,所以我們用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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')


模型可視化

可視化的代碼同上,這里只貼圖。


可以看到模型比上一個模型復(fù)雜了許多,但實際上只是因為輸入比較多,所以它顯得很大。還有一個值得注意的地方,我們的圖片在輸入的時候是經(jīng)過了旋轉(zhuǎn)的,這是因為我們希望以水平方向輸入,而圖片在 numpy 里默認(rèn)是這樣的形狀:(height, width, 3),因此我們使用了 transpose 函數(shù)將圖片轉(zhuǎn)為了(width, height, 3)的格式,然后經(jīng)過各種卷積和降維,變成了 (17, 32),這里的每個長度為32的向量都代表一個豎條的圖片的特征,從左到右,一共有17條。然后我們兵分兩路,一路從左到右輸入到 GRU,一路從右到左輸入到 GRU,然后將他們輸出的結(jié)果加起來。再兵分兩路,還是一路正方向,一路反方向,只不過第二次我們直接將它們的輸出連起來,然后經(jīng)過一個全連接,輸出每個字符的概率。

數(shù)據(jù)生成器

1
2
3
4
5
6
7
8
9
10
11
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)


評估模型

1
2
3
4
5
6
7
8
9
10
11
12
13
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

我們會通過這個函數(shù)來評估我們的模型,和上面的評估標(biāo)準(zhǔn)一樣,只有全部正確,我們才算預(yù)測正確,中間有個坑,就是模型最開始訓(xùn)練的時候,并不一定會輸出四個字符,所以我們?nèi)绻龅剿械淖址疾坏剿膫€的時候,就不計算了,相當(dāng)于加0,遇到多于4個字符的時候,只取前四個。

評估回調(diào)

因為 Keras 沒有針對這種輸出計算準(zhǔn)確率的選項,因此我們需要自定義一個回調(diào)函數(shù),它會在每一代訓(xùn)練完成的時候計算模型的準(zhǔn)確率。

1
2
3
4
5
6
7
8
9
10
11
12
13
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()

訓(xùn)練模型

由于 CTC Loss 收斂很慢,所以我們需要設(shè)置比較大的代數(shù),這里我們設(shè)置了100代,然后添加了一個早期停止的回調(diào)和我們上面定義的回調(diào),但是第一次訓(xùn)練只訓(xùn)練37代就停了,測試準(zhǔn)確率才95%,我又在這個基礎(chǔ)上繼續(xù)訓(xùn)練了一次,停在了25代,得到了98%的準(zhǔn)確率,所以一共訓(xùn)練了62代。

1
2
3
model.fit_generator(gen(128), samples_per_epoch=51200, nb_epoch=200,
                   callbacks=[EarlyStopping(patience=10), evaluator],
                   validation_data=gen(), nb_val_samples=1280)



測試模型

1
2
3
4
5
6
7
8
9
10
11
12
13
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,不過更厲害的是模型認(rèn)出來了。


有趣的問題

我又用之前的模型做了個測試,對于 O0O0 這樣喪心病狂的驗證碼,模型偶爾也能正確識別,這讓我非常驚訝,它是真的能識別 O 與 0 的差別呢,還是猜出來的呢?這很難說。

1
2
3
4
5
6
7
8
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')




總結(jié)

模型的大小是4.7MB,在我的筆記本上跑1000張驗證碼需要用14秒,平均一秒識別71張,估計可以拼過網(wǎng)速。

最后附上一張本模型識別 HACK 。


參考鏈接

  • http://keras-cn./en/latest/getting_started/functional_API/

  • https://github.com/fchollet/keras/blob/master/examples/image_ocr.py

  • http://cs231n./convolutional-networks/

       https://ypwhs./captcha/



不斷更新資源

深度學(xué)習(xí)、機器學(xué)習(xí)、數(shù)據(jù)分析、python

 搜索公眾號添加: datayx  

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多