captcha_break驗(yàn)證碼識(shí)別
使用深度學(xué)習(xí)來破解 captcha 驗(yàn)證碼
本項(xiàng)目會(huì)通過 Keras 搭建一個(gè)深度卷積神經(jīng)網(wǎng)絡(luò)來識(shí)別 captcha 驗(yàn)證碼,建議使用顯卡來運(yùn)行該項(xiàng)目。
下面的可視化代碼都是在 jupyter notebook 中完成的,如果你希望寫成 python 腳本,稍加修改即可正常運(yùn)行,當(dāng)然也可以去掉這些可視化代碼。
2019 年更新了:
- 適配了新版 API
- 提高了數(shù)據(jù)生成器的效率
- 使用了 CuDNNGRU 提高了訓(xùn)練和預(yù)測(cè)效率
- 更新了文檔
環(huán)境
本項(xiàng)目使用的環(huán)境如下:
- captcha 0.3
- tensorflow-gpu 1.13.1
- numpy 1.16.4
- tqdm 4.28.1
下面幾個(gè)包是用于可視化的:
- matplotlib 2.2.2
- pandas 0.23.0
- pydot 1.4.1
- graphviz 2.38.0-12ubuntu2.1
captcha
captcha 是用 python 寫的生成驗(yàn)證碼的庫(kù),它支持圖片驗(yàn)證碼和語音驗(yàn)證碼,我們使用的是它生成圖片驗(yàn)證碼的功能。
首先我們?cè)O(shè)置我們的驗(yàn)證碼格式為數(shù)字加大寫字母,生成一串驗(yàn)證碼試試看:
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)
防止 tensorflow 占用所有顯存
眾所周知 tensorflow 默認(rèn)占用所有顯存,這樣不利于我們同時(shí)進(jìn)行多項(xiàng)實(shí)驗(yàn),因此我們可以使用下面的代碼當(dāng) tensorflow 使用它需要的顯存,而不是直接占用所有顯存。
import tensorflow as tf
import tensorflow.keras.backend as K
config = tf.ConfigProto()
config.gpu_options.allow_growth=True
sess = tf.Session(config=config)
K.set_session(sess)
數(shù)據(jù)生成器
訓(xùn)練模型的時(shí)候,我們可以選擇兩種方式來生成我們的訓(xùn)練數(shù)據(jù),一種是一次性生成幾萬張圖,然后開始訓(xùn)練,一種是定義一個(gè)數(shù)據(jù)生成器,然后利用 fit_generator 函數(shù)來訓(xùn)練。
第一種方式的好處是訓(xùn)練的時(shí)候顯卡利用率高,如果你需要經(jīng)常調(diào)參,可以一次生成,多次使用;第二種方式的好處是你不需要生成大量數(shù)據(jù),訓(xùn)練過程中可以利用 CPU 生成數(shù)據(jù),而且還有一個(gè)好處是你可以無限生成數(shù)據(jù)。
我們的數(shù)據(jù)格式如下:
X
X 的形狀是 (batch_size, height, width, 3),比如一批生成 128 個(gè)樣本,圖片寬度為170,高度為80,那么 X 的形狀就是 (128, 64, 128, 3),如果你想取第一張圖,代碼可以這樣寫 X[0]。
y
y 的形狀是四個(gè) (batch_size, n_class),如果轉(zhuǎn)換成 numpy 的格式,則是 (n_len, batch_size, n_class),比如一批生成 128 個(gè)樣本,驗(yàn)證碼的字符有 36 種,長(zhǎng)度是 4 位,那么它的形狀就是 4 個(gè) (128, 36) 的矩陣,也可以說是 (4, 32, 36)。
數(shù)據(jù)生成器
為了讓 Keras 能夠使用多進(jìn)程并行生成數(shù)據(jù),我們需要使用 Keras 的 Sequence 類實(shí)現(xiàn)一個(gè)我們自己的數(shù)據(jù)類。
在 __init__ 初始化函數(shù)里,我們定義數(shù)據(jù)所需的參數(shù),然后這個(gè)數(shù)據(jù)的長(zhǎng)度就是 steps 數(shù)。在 __getitem__ 里,我們不用理會(huì)索引號(hào),直接隨機(jī)生成一批樣本送去訓(xùn)練即可。
from tensorflow.keras.utils import Sequence
class CaptchaSequence(Sequence):
def __init__(self, characters, batch_size, steps, n_len=4, width=128, height=64):
self.characters = characters
self.batch_size = batch_size
self.steps = steps
self.n_len = n_len
self.width = width
self.height = height
self.n_class = len(characters)
self.generator = ImageCaptcha(width=width, height=height)
def __len__(self):
return self.steps
def __getitem__(self, idx):
X = np.zeros((self.batch_size, self.height, self.width, 3), dtype=np.float32)
y = [np.zeros((self.batch_size, self.n_class), dtype=np.uint8) for i in range(self.n_len)]
for i in range(self.batch_size):
random_str = ''.join([random.choice(self.characters) for j in range(self.n_len)])
X[i] = np.array(self.generator.generate_image(random_str)) / 255.0
for j, ch in enumerate(random_str):
y[j][i, :] = 0
y[j][i, self.characters.find(ch)] = 1
return X, y
使用生成器
生成器的使用方法很簡(jiǎn)單,只需要用對(duì)它取第一個(gè) batch 即可。下面是一個(gè)例子,初始化一個(gè)數(shù)據(jù)集,設(shè)置 batch_size 和 steps 都為 1,然后取出來第一個(gè)數(shù)據(jù),對(duì)它可視化。
在這里我們對(duì)生成的 One-Hot 編碼后的標(biāo)簽進(jìn)行了解碼,首先將它轉(zhuǎn)為 numpy 數(shù)組,然后取36個(gè)字符中最大的數(shù)字的位置(axis=2代表字符的軸),實(shí)際上神經(jīng)網(wǎng)絡(luò)會(huì)輸出36個(gè)字符的概率,我們需要將概率最大的四個(gè)字符的編號(hào)取出來,轉(zhuǎn)換為字符串。
def decode(y):
y = np.argmax(np.array(y), axis=2)[:,0]
return ''.join([characters[x] for x in y])
data = CaptchaSequence(characters, batch_size=1, steps=1)
X, y = data[0]
plt.imshow(X[0])
plt.title(decode(y))
構(gòu)建深度卷積神經(jīng)網(wǎng)絡(luò)
from tensorflow.keras.models import *
from tensorflow.keras.layers import *
input_tensor = Input((height, width, 3))
x = input_tensor
for i, n_cnn in enumerate([2, 2, 2, 2, 2]):
for j in range(n_cnn):
x = Conv2D(32*2**min(i, 3), kernel_size=3, padding='same', kernel_initializer='he_uniform')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = MaxPooling2D(2)(x)
x = Flatten()(x)
x = [Dense(n_class, activation='softmax', name='c%d'%(i+1))(x) for i in range(n_len)]
model = Model(inputs=input_tensor, outputs=x)
模型結(jié)構(gòu)很簡(jiǎn)單,特征提取部分使用的是兩個(gè)卷積,一個(gè)池化的結(jié)構(gòu),這個(gè)結(jié)構(gòu)是學(xué)的 VGG16 的結(jié)構(gòu)。我們重復(fù)五個(gè) block,然后我們將它 Flatten,連接四個(gè)分類器,每個(gè)分類器是36個(gè)神經(jīng)元,輸出36個(gè)字符的概率。
模型可視化
得益于 Keras 自帶的可視化,我們可以使用幾句代碼來可視化模型的結(jié)構(gòu):
from tensorflow.keras.utils import plot_model
from IPython.display import Image
plot_model(model, to_file='cnn.png', show_shapes=True)
Image('cnn.png')
這里需要使用 pydot 這個(gè)庫(kù),以及 graphviz 這個(gè)庫(kù),在 macOS 系統(tǒng)上安裝方法如下:
brew install graphviz
pip install pydot-ng
我們可以看到最后一層卷積層輸出的形狀是 (1, 6, 256),已經(jīng)不能再加卷積層了。
訓(xùn)練模型
訓(xùn)練模型反而是所有步驟里面最簡(jiǎn)單的一個(gè),直接使用 model.fit_generator 即可,這里的驗(yàn)證集使用了同樣的生成器,由于數(shù)據(jù)是通過生成器隨機(jī)生成的,所以我們不用考慮數(shù)據(jù)是否會(huì)重復(fù)。
為了避免手動(dòng)調(diào)參,我們使用了 Adam 優(yōu)化器,它的學(xué)習(xí)率是自動(dòng)設(shè)置的,我們只需要給一個(gè)較好的初始學(xué)習(xí)率即可。
EarlyStopping 是一個(gè) Keras 的 Callback,它可以在 loss 超過多少個(gè) epoch 沒有下降以后,就自動(dòng)終止訓(xùn)練,避免浪費(fèi)時(shí)間。
ModelCheckpoint 是另一個(gè)好用的 Callback,它可以保存訓(xùn)練過程中最好的模型。
CSVLogger 可以記錄 loss 為 CSV 文件,這樣我們就可以在訓(xùn)練完成以后繪制訓(xùn)練過程中的 loss 曲線。
注意,這段代碼在筆記本電腦上可能要較長(zhǎng)時(shí)間,建議使用帶有 NVIDIA 顯卡的機(jī)器運(yùn)行。注意我們這里使用了一個(gè)小技巧,添加 workers=4 參數(shù)讓 Keras 自動(dòng)實(shí)現(xiàn)多進(jìn)程生成數(shù)據(jù),擺脫 python 單線程效率低的缺點(diǎn)。
from tensorflow.keras.callbacks import EarlyStopping, CSVLogger, ModelCheckpoint
from tensorflow.keras.optimizers import *
train_data = CaptchaSequence(characters, batch_size=128, steps=1000)
valid_data = CaptchaSequence(characters, batch_size=128, steps=100)
callbacks = [EarlyStopping(patience=3), CSVLogger('cnn.csv'), ModelCheckpoint('cnn_best.h5', save_best_only=True)]
model.compile(loss='categorical_crossentropy',
optimizer=Adam(1e-3, amsgrad=True),
metrics=['accuracy'])
model.fit_generator(train_data, epochs=100, validation_data=valid_data, workers=4, use_multiprocessing=True,
callbacks=callbacks)
載入最好的模型繼續(xù)訓(xùn)練一會(huì)
為了讓模型充分訓(xùn)練,我們可以載入之前最好的模型權(quán)值,然后降低學(xué)習(xí)率為原來的十分之一,繼續(xù)訓(xùn)練,這樣可以讓模型收斂得更好。
model.load_weights('cnn_best.h5')
callbacks = [EarlyStopping(patience=3), CSVLogger('cnn.csv', append=True),
ModelCheckpoint('cnn_best.h5', save_best_only=True)]
model.compile(loss='categorical_crossentropy',
optimizer=Adam(1e-4, amsgrad=True),
metrics=['accuracy'])
model.fit_generator(train_data, epochs=100, validation_data=valid_data, workers=4, use_multiprocessing=True,
callbacks=callbacks)
測(cè)試模型
當(dāng)我們訓(xùn)練完成以后,可以識(shí)別一個(gè)驗(yàn)證碼試試看:
X, y = data[0]
y_pred = model.predict(X)
plt.title('real: %s\npred:%s'%(decode(y), decode(y_pred)))
plt.imshow(X[0], cmap='gray')
plt.axis('off')
計(jì)算模型總體準(zhǔn)確率
模型在訓(xùn)練的時(shí)候只會(huì)顯示每一個(gè)字符的準(zhǔn)確率,為了統(tǒng)計(jì)模型的總體準(zhǔn)確率,我們可以寫下面的函數(shù):
from tqdm import tqdm
def evaluate(model, batch_num=100):
batch_acc = 0
with tqdm(CaptchaSequence(characters, batch_size=128, steps=100)) as pbar:
for X, y in pbar:
y_pred = model.predict(X)
y_pred = np.argmax(y_pred, axis=-1).T
y_true = np.argmax(y, axis=-1).T
batch_acc += (y_true == y_pred).all(axis=-1).mean()
return batch_acc / batch_num
evaluate(model)
這里用到了一個(gè)庫(kù)叫做 tqdm,它是一個(gè)進(jìn)度條的庫(kù),為的是能夠?qū)崟r(shí)反饋進(jìn)度。然后我們通過一些 numpy 計(jì)算去統(tǒng)計(jì)我們的準(zhǔn)確率,這里計(jì)算規(guī)則是只要有一個(gè)錯(cuò),那么就不算它對(duì)。經(jīng)過計(jì)算,我們的模型的總體準(zhǔn)確率在經(jīng)過充分訓(xùn)練以后,可以達(dá)到 98.26% 的總體準(zhǔn)確率。
模型總結(jié)
模型的大小是10.7MB,總體準(zhǔn)確率是 98.26%,基本上可以確定破解了此類驗(yàn)證碼。
改進(jìn)
對(duì)于這種按順序書寫的文字,我們還有一種方法可以使用,那就是循環(huán)神經(jīng)網(wǎng)絡(luò)來識(shí)別序列。下面我們來了解一下如何使用循環(huán)神經(jīng)網(wǎng)絡(luò)來識(shí)別這類驗(yàn)證碼。
CTC Loss
這個(gè) loss 是一個(gè)特別神奇的 loss,它可以在只知道序列的順序,不知道具體位置的情況下,讓模型收斂。這里有一個(gè)非常好的文章介紹了 CTC Loss: Sequence Modeling With CTC
在 Keras 里面已經(jīng)內(nèi)置了 CTC Loss ,我們實(shí)現(xiàn)下面的代碼即可在模型里使用 CTC Loss。
-
y_pred是模型的輸出,是按順序輸出的37個(gè)字符的概率,因?yàn)槲覀冞@里用到了循環(huán)神經(jīng)網(wǎng)絡(luò),所以需要一個(gè)空白字符的概念; -
labels是驗(yàn)證碼,是四個(gè)數(shù)字,每個(gè)數(shù)字代表字符在字符集里的位置 -
input_length表示y_pred的長(zhǎng)度,我們這里是16 -
label_length表示labels的長(zhǎng)度,我們這里是4
import tensorflow.keras.backend as K
def ctc_lambda_func(args):
y_pred, labels, input_length, label_length = args
return K.ctc_batch_cost(labels, y_pred, input_length, label_length)
模型結(jié)構(gòu)
我們的模型結(jié)構(gòu)是這樣設(shè)計(jì)的,首先通過卷積神經(jīng)網(wǎng)絡(luò)去識(shí)別特征,然后按水平順序輸入到 GRU 進(jìn)行序列建模,最后使用一個(gè)分類器對(duì)每個(gè)時(shí)刻輸出的特征進(jìn)行分類。
from tensorflow.keras.models import *
from tensorflow.keras.layers import *
input_tensor = Input((height, width, 3))
x = input_tensor
for i, n_cnn in enumerate([2, 2, 2, 2, 2]):
for j in range(n_cnn):
x = Conv2D(32*2**min(i, 3), kernel_size=3, padding='same', kernel_initializer='he_uniform')(x)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = MaxPooling2D(2 if i < 3 else (2, 1))(x)
x = Permute((2, 1, 3))(x)
x = TimeDistributed(Flatten())(x)
rnn_size = 128
x = Bidirectional(CuDNNGRU(rnn_size, return_sequences=True))(x)
x = Bidirectional(CuDNNGRU(rnn_size, return_sequences=True))(x)
x = Dense(n_class, activation='softmax')(x)
base_model = Model(inputs=input_tensor, outputs=x)
為了訓(xùn)練這個(gè)模型,我們還需要搭建一個(gè) loss 計(jì)算網(wǎng)絡(luò),代碼如下:
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(inputs=[input_tensor, labels, input_length, label_length], outputs=loss_out)
真正訓(xùn)練出來的模型是 base_model,由于 Keras 的限制,我們沒辦法直接使用 base_model 搭建 CTCLoss,所以我們只能按照上面的方法,讓模型直接輸出 loss。
模型可視化
可視化的代碼同上,這里只貼圖。
可以看到模型比上一個(gè)模型復(fù)雜了許多,但實(shí)際上只是因?yàn)檩斎氡容^多,所以它顯得很大。
首先模型輸入一個(gè) (height, width, 3) 維度的圖片,然后經(jīng)過一系列的層降維到了 (2, 16, 256),之后我們使用 Permute 把 width 軸調(diào)整到第一個(gè)維度以適配 RNN 的輸入格式。調(diào)整以后的維度是 (16, 2, 256),然后使用 TimeDistributed(Flatten()) 把后兩個(gè)維度壓成一維,也就是 (16, 512),之后經(jīng)過 2 層雙向的 GRU 對(duì)序列橫向建模,最后經(jīng)過 Dense 分類器輸出水平方向上每個(gè)字符的概率分布。
使用 CuDNNGRU 是因?yàn)樗?NVIDIA 顯卡上可以加速非常多倍,如果你使用的是 CPU,改為 GRU 即可。
使用 RNN 的原因是,如果你看到一句話是 今天我*了一個(gè)非常好吃的蘋果,有一個(gè)字看不清,你很容易猜到這個(gè)字是“吃”,但是使用 CNN,你就很難有這么大的感受野,從蘋果推測(cè)出前面的字是吃。
數(shù)據(jù)生成器
數(shù)據(jù)生成器和 CNN 的差不多,這里需要多幾個(gè)矩陣,一個(gè)是 input_length,代表序列長(zhǎng)度,一個(gè)是 label_length,代表驗(yàn)證碼長(zhǎng)度,還有一個(gè) np.ones,沒有意義,只是為了適配 Keras 訓(xùn)練需要的矩陣輸入。
from tensorflow.keras.utils import Sequence
class CaptchaSequence(Sequence):
def __init__(self, characters, batch_size, steps, n_len=4, width=128, height=64,
input_length=16, label_length=4):
self.characters = characters
self.batch_size = batch_size
self.steps = steps
self.n_len = n_len
self.width = width
self.height = height
self.input_length = input_length
self.label_length = label_length
self.n_class = len(characters)
self.generator = ImageCaptcha(width=width, height=height)
def __len__(self):
return self.steps
def __getitem__(self, idx):
X = np.zeros((self.batch_size, self.height, self.width, 3), dtype=np.float32)
y = np.zeros((self.batch_size, self.n_len), dtype=np.uint8)
input_length = np.ones(self.batch_size)*self.input_length
label_length = np.ones(self.batch_size)*self.label_length
for i in range(self.batch_size):
random_str = ''.join([random.choice(self.characters) for j in range(self.n_len)])
X[i] = np.array(self.generator.generate_image(random_str)) / 255.0
y[i] = [self.characters.find(x) for x in random_str]
return [X, y, input_length, label_length], np.ones(self.batch_size)
評(píng)估模型
from tqdm import tqdm
def evaluate(model, batch_size=128, steps=20):
batch_acc = 0
valid_data = CaptchaSequence(characters, batch_size, steps)
for [X_test, y_test, _, _], _ in valid_data:
y_pred = base_model.predict(X_test)
shape = y_pred.shape
out = K.get_value(K.ctc_decode(y_pred, input_length=np.ones(shape[0])*shape[1])[0][0])[:, :4]
if out.shape[1] == 4:
batch_acc += (y_test == out).all(axis=1).mean()
return batch_acc / steps
我們會(huì)通過這個(gè)函數(shù)來評(píng)估我們的模型,和上面的評(píng)估標(biāo)準(zhǔn)一樣,只有全部正確,我們才算預(yù)測(cè)正確,中間有個(gè)坑,就是模型最開始訓(xùn)練的時(shí)候,并不一定會(huì)輸出四個(gè)字符,所以我們?nèi)绻龅剿械淖址疾坏剿膫€(gè)的時(shí)候,就不計(jì)算了,相當(dāng)于加0,遇到多于4個(gè)字符的時(shí)候,只取前四個(gè)。
評(píng)估回調(diào)
因?yàn)?Keras 沒有針對(duì)這種輸出計(jì)算準(zhǔn)確率的選項(xiàng),因此我們需要自定義一個(gè)回調(diào)函數(shù),它會(huì)在每一代訓(xùn)練完成的時(shí)候計(jì)算模型的準(zhǔn)確率。
from tensorflow.keras.callbacks import Callback
class Evaluate(Callback):
def __init__(self):
self.accs = []
def on_epoch_end(self, epoch, logs=None):
logs = logs or {}
acc = evaluate(base_model)
logs['val_acc'] = acc
self.accs.append(acc)
print(f'\nacc: {acc*100:.4f}')
訓(xùn)練模型
我們還是按照之前的訓(xùn)練策略,先訓(xùn)練 100 代,等 loss 不降低以后,降低學(xué)習(xí)率,再訓(xùn)練 100 代,代碼如下:
from tensorflow.keras.callbacks import EarlyStopping, CSVLogger, ModelCheckpoint
from tensorflow.keras.optimizers import *
train_data = CaptchaSequence(characters, batch_size=128, steps=1000)
valid_data = CaptchaSequence(characters, batch_size=128, steps=100)
callbacks = [EarlyStopping(patience=5), Evaluate(),
CSVLogger('ctc.csv'), ModelCheckpoint('ctc_best.h5', save_best_only=True)]
model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer=Adam(1e-3, amsgrad=True))
model.fit_generator(train_data, epochs=100, validation_data=valid_data, workers=4, use_multiprocessing=True,
callbacks=callbacks)
model.load_weights('ctc_best.h5')
callbacks = [EarlyStopping(patience=5), Evaluate(),
CSVLogger('ctc.csv', append=True), ModelCheckpoint('ctc_best.h5', save_best_only=True)]
model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer=Adam(1e-4, amsgrad=True))
model.fit_generator(train_data, epochs=100, validation_data=valid_data, workers=4, use_multiprocessing=True,
callbacks=callbacks)
可以看到 loss 一開始下降很快,后面就很平了,但是我們把在對(duì)數(shù)尺度下繪制 loss 圖的話,還是能看到 loss 一直在下降的。acc 上升得也很快,雖然前期訓(xùn)練的時(shí)候 acc 很抖動(dòng),但是后期學(xué)習(xí)率降下來以后就不會(huì)再跌下來了。
最終模型的準(zhǔn)確率達(dá)到了 99.21%,訓(xùn)練過程中的準(zhǔn)確率最高達(dá)到了 99.49%。
測(cè)試模型
characters2 = characters + ' '
[X_test, y_test, _, _], _ = data[0]
y_pred = base_model.predict(X_test)
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])
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])))
這里隨機(jī)出來的驗(yàn)證碼很厲害,是O0OP,不過更厲害的是模型認(rèn)出來了。
有趣的問題
我又用之前的模型做了個(gè)測(cè)試,對(duì)于 O0O0 這樣喪心病狂的驗(yàn)證碼,模型偶爾也能正確識(shí)別,這讓我非常驚訝,它是真的能識(shí)別 O 與 0 的差別呢,還是猜出來的呢?這很難說。
generator = ImageCaptcha(width=width, height=height)
random_str = 'O0O0'
X = generator.generate_image(random_str)
X = np.expand_dims(X, 0) / 255.0
y_pred = base_model.predict(X)
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]])
plt.title('real: %s\npred:%s'%(random_str, out))
plt.imshow(X[0], cmap='gray')
總結(jié)
模型的大小是12.8MB,準(zhǔn)確率達(dá)到了驚人的 99.21%,即使連 0 和 O 都能精準(zhǔn)區(qū)分,非常成功。
擴(kuò)展
如果你比較喜歡 PyTorch,可以看 ctc_pytorch.ipynb,精度更高,達(dá)到了 99.57%。
如果你想查看更多經(jīng)驗(yàn),可以看看我在百度云魅族深度學(xué)習(xí)應(yīng)用大賽的代碼和思路:https://github.com/ypwhs/baiduyun_deeplearning_competition
