讨伐验证码(一):破解单数字

讨伐验证码(一):破解单数字

准备工作

讨伐验证码系列将由浅入深地教你如何用深度学习框架 Keras 搭建神经网络来破解验证码。本文为系列的第一部分,从最简单的单数字验证码入手。

我们悲惨的实验对象是 Python 验证码库 captcha。先来看看这个库生成的验证码长什么样子。

%matplotlib inline
import captcha
from captcha.image import ImageCaptcha

IMG_WIDTH = 40
IMG_HEIGHT = 80

IMG_CAPTCHA = ImageCaptcha(width=IMG_WIDTH, height=IMG_HEIGHT)
IMG_CAPTCHA.generate_image('5')

只看一张图还不是很直观,让我们定义两个辅助函数,然后画出各种不一样数字的验证码。

import numpy as np

def img_from_digit(digit):
    return IMG_CAPTCHA.generate_image(str(digit))

def img_vec_from_digit(digit):
    return np.array(img_from_digit(digit))

matplotlib 画出整整齐齐的验证码网格,每一个格子都是一个验证码。

from matplotlib import pyplot as plt
import math

def display_imgs(vec_title_pairs, num_cols=10):
    num_rows = int(math.ceil(len(vec_title_pairs) / num_cols))
    num_cols = min(num_cols, len(vec_title_pairs))
    fig, subplots = plt.subplots(num_rows, num_cols, squeeze=False)
    fig.set_dpi(300)
    for subplot in subplots.flatten():
        subplot.axis('off')
    for (img_vec, title), subplot in zip(vec_title_pairs, subplots.flatten()):
        subplot.imshow(img_vec)
        subplot.set_title(title)
    fig.tight_layout(pad=0)
    plt.show()

vec_title_pairs = [(img_vec_from_digit(i), i) for _ in range (3) for i in range(10)]
display_imgs(vec_title_pairs)

嗯,看起来不是很难,也就是比 MNIST 数据集稍微复杂些,多了图像的旋转和一些噪音。在 MNIST 数据集上,简单的全连接神经网络就可以达到 90% 以上的测试集正确率。那我们就先定一个小目标,用和 MNIST 一样大小的数据量(60k 训练,10k 测试),训练一个能达到 80% 以上正确率的神经网络。

制作数据

制作一份和 MNIST 一样大小的数据集。因为神经网络更加擅长处理较小的值,像接近 0 或 1 的这种,我们可以通过归一化样本来利用这一点。图像是用 RGB 编码的,数据类型为 uint8,所以值域是 0 ~ 255。我们通过把样本的 RGB 值都除以 255 来归一化,确保这些值都在 0 ~ 1 的区间内。同时,通过观察训练集,不难发现颜色对判断数字的影响并不大,干脆我们丢掉 RGB 的颜色信息,直接用灰度吧。通常灰度值的计算并不是直接平均 RGB,但我们偷个懒,就这么办。

import random

def one_hot_encode(digit):
    img_vec = np.zeros((10,))
    img_vec[digit] = 1
    return img_vec

def normalize_img_vec(img_vec):
    return img_vec.mean(-1, keepdims=True) / 255

def denormalize_img_vec(img_vec):
    return (img_vec * 255).astype('uint8')

def sample():
    digit = random.randint(0, 9)
    return normalize_img_vec(img_vec_from_digit(digit)), one_hot_encode(digit)

def make_data(num_samples):
    X, Y = zip(*(sample() for i in range(num_samples)))
    return np.asarray(X), np.asarray(Y)
train_x, train_y = make_data(60000)
test_x, test_y = make_data(10000)
print(train_x.shape, train_y.shape)
print(test_x.shape, test_y.shape)
(60000, 80, 40, 1) (60000, 10)
(10000, 80, 40, 1) (10000, 10)

构造网络

既然我们的目标并不是很高,那就先使用简单的双层全连接网络。因为是多分类问题(十选一),所以选择 softmax 作为最后一层的输出、categorical_crossentropy 为损失函数。优化函数则选择烂大街的 adam

至于具体的网络超参,输入拍平后有 3200 个特征信息,所以选择 512 个连接单元,确保信息量不会丢失太多。毕竟全连接层无法像卷积层那样重复学习到空间上的特征(我们第一步就是拍平输入,所以二维的空间关系也直接丢失了)。

BatchNormalization 和 Dropout 则是加速训练和防止过拟合的标配。不过注意,两者一起用时需要做数学上的调整,要看所使用的框架支不支持。你问 Keras 支不支持?Keras 是支持的。我就明确地给你告诉这一点。

from IPython.display import SVG
from keras.models import Model
from keras.layers import *
from keras.utils.vis_utils import model_to_dot

def make_model():
    visible = Input((IMG_HEIGHT, IMG_WIDTH, 1))
    x = Flatten()(visible)
    x = Dense(512, activation='relu')(x)
    x = BatchNormalization()(x)
    x = Dropout(0.5)(x)
    x = Dense(512, activation='relu')(x)
    x = BatchNormalization()(x)
    x = Dropout(0.5)(x)
    output = Dense(10, activation='softmax')(x)

    model = Model(inputs=visible, outputs=output)
    model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])
    model.summary()
    return model

model = make_model()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 80, 40, 1)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 3200)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 512)               1638912   
_________________________________________________________________
batch_normalization_1 (Batch (None, 512)               2048      
_________________________________________________________________
dropout_1 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_2 (Dense)              (None, 512)               262656    
_________________________________________________________________
batch_normalization_2 (Batch (None, 512)               2048      
_________________________________________________________________
dropout_2 (Dropout)          (None, 512)               0         
_________________________________________________________________
dense_3 (Dense)              (None, 10)                5130      
=================================================================
Total params: 1,910,794
Trainable params: 1,908,746
Non-trainable params: 2,048
_________________________________________________________________

训练网络

model.fit(train_x, train_y, epochs=5, batch_size=32)
loss, acc = model.evaluate(test_x, test_y)
print("Accuracy: ", acc)
Epoch 1/5
60000/60000 [==============================] - 17s 286us/step - loss: 0.5830 - acc: 0.8135
Epoch 2/5
60000/60000 [==============================] - 16s 271us/step - loss: 0.4046 - acc: 0.8706
Epoch 3/5
60000/60000 [==============================] - 16s 273us/step - loss: 0.3924 - acc: 0.8726
Epoch 4/5
60000/60000 [==============================] - 16s 273us/step - loss: 0.4001 - acc: 0.8719
Epoch 5/5
60000/60000 [==============================] - 16s 273us/step - loss: 0.4224 - acc: 0.8626
10000/10000 [==============================] - 1s 90us/step
Accuracy:  0.878

精度还不错

虽然网络参数还有很大的精调空间,但是我们已经完成了小目标,复杂的东西我们后面几期再加上。

给自己拉几个小呲花,我们下期再见 🎉🎉🎉