基于TensorFlow的CAPTCHA注册码识别实验

  • 实验任务:使用TensorFlow实现CAPTCHA注册码的识别
  • 基本思路:采用 Captcha 库生成验证码,将验证码识别问题转化为分类问题,采用 CNN 网络模型进行训练,最终实现对验证码的破解。
  • 实验步骤:获取验证码训练集 → 构建卷积神经网络和全连接神经网络 → 定义损失函数以及优化方式 → 进行训练并保存训练结果。

获取验证码训练集

使用captcha库来生成验证码,captcha库可以使用anaconda很轻松下载,captcha库不仅可以用于生成图片验证码,还可以用来生成语音验证码。

  1. 我们将ImageCaptcha类实例化为image对象,构造函数的参数里指定宽度和高度。

image = ImageCaptcha(width=self.width, height=self.height)

  1. 存储训练数据。

    我们创建两个矩阵X和Y用以表示训练集的输入部分和输出部分,也就是验证码的图片数据存储以及验证码上所写的内容存储。

    1
    2
    X = np.zeros([batch_size, self.height, self.width, 1])
    Y = np.zeros([batch_size, self.char_num, self.classes])

    这两个矩阵的第一维参数都是batch_size,表示这一组训练集的个数。

    X的第二和第三位参数分别表示验证码的高度和宽度,最后一个参数表示这是1通道的黑白图片。

    Y的第二位参数表示此验证码上写了几个字符,第三位参数表示每个字符共有多少种选择,这是一条长为self.classes的向量,以独热编码的形式记录信息。

  2. 生成验证码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    while True:
    for i in range(batch_size):
    captcha_str = ''.join(random.sample(self.characters, self.char_num))
    img = image.generate_image(captcha_str).convert('L')
    img = np.array(img.getdata())
    X[i] = np.reshape(img, [self.height, self.width, 1]) / 255.0
    for j, ch in enumerate(captcha_str):
    Y[i, j, self.characters.find(ch)] = 1
    Y = np.reshape(Y, (batch_size, self.char_num * self.classes))
    yield X, Y

    首先,这个总体的无限循环是一个训练集的生成器,执行此代码后,会在最后的yield语句返回训练集X和Y,然后循环结束。下次再想生成验证码训练集时,会从yield语句(最后一句)开始,回到开头再执行一次循环。

    这样的好处是,以前都是将所有的训练数据全部准备好,然后手动分出很多batch,一个一个的训练,这样的话要求把所有的训练数据全部装入内存,很浪费内存空间。而使用这种生成器语句,如果我训练哪一个batch的数据,就立即生成数据并装入内存,用完再撤出内存,紧接着训练下一个batch的数据时,再立即生成然后将其装入内存。分批装入内存,节省了大量的内存空间。

    1
    random.sample(self.characters, self.char_num)

    此代码生成一个验证码字符串的随机变量,self.characters为62位的字符串(0~9A~Za~z),self.char_num=4(生成4个字符)。

    1
    img = image.generate_image(captcha_str).convert('L')

    这句代码使用的是ImageCaptcha类的内置方法,将字符串变为图片。convert(‘L’):表示生成的是灰度图片,就是通道数为1的黑白图片。

    1
    X[i] = np.reshape(img, [self.height, self.width, 1]) / 255.0

    每个像素值都要除以255,这是为了归一化处理,因为灰度的范围是0~255,这里除以255就让每个像素的值在0~1之间,目的是为了加快收敛速度。

    1
    2
    for j, ch in enumerate(captcha_str):
    Y[i, j, self.characters.find(ch)] = 1

    这里用以生成对应的测试集Y,j和ch用以遍历刚刚生成的随机字符串,j记录index(0~3,表示第几个字符),ch记录字符串中的字符。找到Y的第i条数据中的第j个字符,然后把62长度的向量和ch相关的那个置为1。

构建卷积神经网络和全连接神经网络

  1. conv2D

    1
    2
    def conv2d(self, x, W):
    return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

    这是2维卷积函数。x表示传入的待处理图片,W表示卷积核,strides=[1, 1, 1, 1],其中第二个和第三个1分别表示x方向步长和y方向步长,padding=’SAME’表示边界处理策略设为’SAME’,这样卷积处理完图片大小不变。

  2. max_pool_2x2

    1
    2
    def max_pool_2x2(self, x):
    return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    这是2x2最大值池化函数。x表示待被池化处理的图片,ksize=[1, 2, 2, 1],其中第二个和第三个2分别表示池化窗口高度和池化窗口宽度,strides和padding意义同上。

  3. weight_variable

    1
    2
    3
    def weight_variable(self, shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

    这就是一个很有意思的函数了,用这个函数的目的是从截断的正态分布中输出随机值。听着名字就不知道是个什么鬼,我们先看看正太分布的一个性质:

    在正态分布的曲线中,横轴区间(μ-σ,μ+σ)内的面积为68.268949%。

    横轴区间(μ-2σ,μ+2σ)内的面积为95.449974%。

    横轴区间(μ-3σ,μ+3σ)内的面积为99.730020%。

    X落在(μ-3σ,μ+3σ)以外的概率小于千分之三,在实际问题中常认为相应的事件是不会发生的,基本上可以把区间(μ-3σ,μ+3σ)看作是随机变量X实际可能的取值区间,这称之为正态分布的“3σ”原则。

    正态分布

    而从截断的正态分布中输出随机值的意思就是,同样也是随机生成的值,但是生成的值必须服从具有指定平均值和标准偏差的正态分布,如果生成的值大于平均值2个标准偏差的值则丢弃重新选择。换句话说,就是如果随机生成的值如果落在了(μ-2σ,μ+2σ)之外,就重新生成值,这样就保证了随机生成的值都在均值附近。

    扯了这么多,其实就是随机生成一组数,要求不要离中心点μ太远。

  4. bias_variable

    1
    2
    3
    def bias_variable(self, shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)

    生成一组全部都是0.1的常量数,没啥好说的。

  5. 构建第一层卷积神经网络

    1
    2
    3
    4
    5
    6
    7
    w_conv1 = self.weight_variable([5, 5, 1, 32])
    b_conv1 = self.bias_variable([32])
    h_conv1 = tf.nn.relu(tf.nn.bias_add(self.conv2d(x_images, w_conv1), b_conv1))
    h_pool1 = self.max_pool_2x2(h_conv1)
    h_dropout1 = tf.nn.dropout(h_pool1, keep_prob)
    conv_width = math.ceil(self.width / 2)
    conv_height = math.ceil(self.height / 2)

    w_conv1是卷积核,可以理解为一共有32个卷积核,每个卷积核的尺寸是(5, 5, 1),即长度和宽度都是5,通道是1。每个卷积核对图片处理完就会产生一张特征图,32个卷积核对图片处理完后就会产生32个特征图,将这些特征图叠加排列,那么原本通道数为1的图片现在通道数变为图片的个数,也就是32。图片的尺寸变化为(?, 60, 160, 1) –> (?, 60, 160, 32)。

    随后又对图片进行一次池化处理,池化窗口为2×2,所以图片的长度和宽度都会变为原来的一半。图片的尺寸变化为(?, 60, 160, 32) → (?, 30, 80, 32)。

    随后又进行了一次dropout以防止过拟合,同时也是为了加大个别神经元的训练强度。

  6. 构建第二层卷积神经网络

    1
    2
    3
    4
    5
    6
    7
    w_conv2 = self.weight_variable([5, 5, 32, 64])
    b_conv2 = self.bias_variable([64])
    h_conv2 = tf.nn.relu(tf.nn.bias_add(self.conv2d(h_dropout1, w_conv2), b_conv2))
    h_pool2 = self.max_pool_2x2(h_conv2)
    h_dropout2 = tf.nn.dropout(h_pool2, keep_prob)
    conv_width = math.ceil(conv_width / 2)
    conv_height = math.ceil(conv_height / 2)

    w_conv2是第二层卷积神经网络的卷积核,共有64个,每个卷积核的尺寸是(5, 5, 32),处理之后图片的尺寸变化为(?, 30, 80, 32) → (?, 30, 80, 64)。

    随后又对图片进行一次池化处理,池化窗口为2×2,所以图片的长度和宽度都会变为原来的一半。图片的尺寸变化为(?, 30, 80, 64) → (?, 15, 40, 64)。

    再进行一次dropout以防止过拟合,同时也是为了加大个别神经元的训练强度。

  7. 构建第三层卷积神经网络

    1
    2
    3
    4
    5
    6
    7
    w_conv3 = self.weight_variable([5, 5, 64, 64])
    b_conv3 = self.bias_variable([64])
    h_conv3 = tf.nn.relu(tf.nn.bias_add(self.conv2d(h_dropout2, w_conv3), b_conv3))
    h_pool3 = self.max_pool_2x2(h_conv3)
    h_dropout3 = tf.nn.dropout(h_pool3, keep_prob)
    conv_width = math.ceil(conv_width / 2)
    conv_height = math.ceil(conv_height / 2)

    w_conv3是第三层卷积神经网络的卷积核,共有64个,每个卷积核的尺寸是(5, 5, 64),处理之后图片的尺寸变化为(?, 15, 40, 64) → (?, 15, 40, 64)。

    随后又对图片进行一次池化处理,池化窗口为2×2,所以图片的长度和宽度都会变为原来的一半。图片的尺寸变化为(?, 15, 40, 64) → (?, 8, 20, 64),这里的15 / 2 = 8,是因为边界策略为SAME,那么遇到剩下还有不足4个像素的时候同样采取一次最大值池化处理。

    再进行一次dropout以防止过拟合,同时也是为了加大个别神经元的训练强度。

  8. 构建第一层全连接神经网络

    1
    2
    3
    4
    5
    6
    7
    conv_width = int(conv_width)
    conv_height = int(conv_height)
    w_fc1 = self.weight_variable([64 * conv_width * conv_height, 1024])
    b_fc1 = self.bias_variable([1024])
    h_dropout3_flat = tf.reshape(h_dropout3, [-1, 64 * conv_width * conv_height])
    h_fc1 = tf.nn.relu(tf.nn.bias_add(tf.matmul(h_dropout3_flat, w_fc1), b_fc1))
    h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

    这里就把刚刚卷积神经网络的输出作为传统神经网络的输入了,w_fc1(10240, 1024)和b_fc1(1024)分别是这一层神经网络的参数以及bias。上面代码第五行将卷积神经网络的输出数据由(?, 8, 20, 64)转为了(?, 64 20 8),可以很明显感觉出来把所有的数据拉成了一条一维向量,然后经过矩阵处理,这里的数据变为了(1024, 1)的形状。

  9. 构建第二层全连接神经网络

    1
    2
    3
    w_fc2 = self.weight_variable([1024, self.char_num * self.classes])
    b_fc2 = self.bias_variable([self.char_num * self.classes])
    y_conv = tf.add(tf.matmul(h_fc1_drop, w_fc2), b_fc2)

    再连接一次神经网络,这次不再需要添加激励函数了ReLu了,因为已经到达输出层,线性相加后直接输出就可以了,结果保存在y_conv变量里,最后将y_conv返回给调用函数。

    1
    y_conv = model.create_model(x, keep_prob)

    这是外层函数调用model后所得到的训练结果。

定义损失函数以及优化方式

1
cross_entropy = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=y_, logits=y_conv))

由于识别验证码本质上是对验证码中的信息进行分类,所以我们这里使用cross_entropy的方法来衡量损失。

1
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

优化方式选择的是AdamOptimizer,学习率设置比较小,为1e-4,防止学习的太快而训练不好。

进行训练并保存训练结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
saver = tf.train.Saver()
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
step = 1
while True:
batch_x, batch_y = next(captcha.gen_captcha(64))
_, loss = sess.run([train_step, cross_entropy], feed_dict={x: batch_x, y_: batch_y, keep_prob: 0.75})
print('step:%d,loss:%f' % (step, loss))
if step % 100 == 0:
batch_x_test, batch_y_test = next(captcha.gen_captcha(100))
acc = sess.run(accuracy, feed_dict={x: batch_x_test, y_: batch_y_test, keep_prob: 1.})
print('###############################################step:%d,accuracy:%f' % (step, acc))
if acc > 0.99:
saver.save(sess, "capcha_model.ckpt")
break
step += 1

从第二行开始进入训练部分,首先我们需要初始化变量,然后进入一个循环,直到训练的准确度高于99%才会停止训练,否则会一直训练下去。

1
batch_x, batch_y = next(captcha.gen_captcha(64))

用于生成训练集,每次生成64条训练数据,输入部分保存在batch_x里,输出部分保存在batch_y里,每次循环到这里就会再次读入64条训练数据。

1
_, loss = sess.run([train_step, cross_entropy], feed_dict={x: batch_x, y_: batch_y, keep_prob: 0.75})

开始训练,loss记录损失,_变量不记录任何东西,我debug下来每次_里面都是空,可能只是为了语法的需要,这里必须有个变量接受点什么。

1
2
3
4
5
6
7
if step % 100 == 0:
batch_x_test, batch_y_test = next(captcha.gen_captcha(100))
acc = sess.run(accuracy, feed_dict={x: batch_x_test, y_: batch_y_test, keep_prob: 1.})
print('###############################################step:%d,accuracy:%f' % (step, acc))
if acc > 0.99:
saver.save(sess, "capcha_model.ckpt")
break

每训练一百次,就会进行一次Test。具体操作是,生成100条测试数据,和之前生成训练数据是一个方法,然后直接进行测试,如果准确度高于99%那么就停止训练,把训练好的模型放在”capcha_model.ckpt”这个文件里面,可以供以后使用。如果没有达到99%的准确度就继续训练。

六、完整代码

train_captcha.py

训练的主代码

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import tensorflow as tf
import generate_captcha
import captcha_model

if __name__ == '__main__':
captcha = generate_captcha.GenerateCaptcha() # 创建一个验证码对象
width, height, char_num, characters, classes = captcha.get_parameter() # 获取此验证码对象的各种属性

# x:训练集输入占位符
# y:训练集输出占位符
# keep_prob:dropout处理保留此神经元的概率占位符
x = tf.placeholder(tf.float32, [None, height, width, 1])
y_ = tf.placeholder(tf.float32, [None, char_num * classes])
keep_prob = tf.placeholder(tf.float32)

# 生成训练模型对象
model = captcha_model.CaptchaModel(width, height, char_num, classes)
# y_conv: 模型训练完的结果
y_conv = model.create_model(x, keep_prob)
# 损失函数,用cross_entropy来描述损失
cross_entropy = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=y_, logits=y_conv))
# 优化器,选用AdamOptimizer
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

# 将训练完的结果reshape为(?, 4, 62)的形状
predict = tf.reshape(y_conv, [-1, char_num, classes])
# 将真实值也reshape为(?, 4, 62)的形状
real = tf.reshape(y_, [-1, char_num, classes])
# 计算预测正确的个数
correct_prediction = tf.equal(tf.argmax(predict, 2), tf.argmax(real, 2))
# 转型为浮点数格式
correct_prediction = tf.cast(correct_prediction, tf.float32)
# 求均值,就是准确率
accuracy = tf.reduce_mean(correct_prediction)

# 等模型训练好后,可以用saver保存模型,便于以后使用
saver = tf.train.Saver()
with tf.Session() as sess:
# 初始化参数
sess.run(tf.global_variables_initializer())
step = 1
while True:
# batch_x和batch_y存储64条训练数据的输入和输出值
batch_x, batch_y = next(captcha.gen_captcha(64))
# loss存储本次训练的损失值
_, loss = sess.run([train_step, cross_entropy], feed_dict={x: batch_x, y_: batch_y, keep_prob: 0.75})
print('step:%d,loss:%f' % (step, loss))
# 每进行100次,就进行一次测试
if step % 100 == 0:
# 获取100条测试数据
batch_x_test, batch_y_test = next(captcha.gen_captcha(100))
acc = sess.run(accuracy, feed_dict={x: batch_x_test, y_: batch_y_test, keep_prob: 1.})
print('###############################################step:%d,accuracy:%f' % (step, acc))
# 如果测试准确度在99%以上就退出,否则继续训练
if acc > 0.99:
saver.save(sess, "capcha_model.ckpt")
break
step += 1

generate_captcha.py

生成验证码的类

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from captcha.image import ImageCaptcha
import numpy as np
import random
import string


class GenerateCaptcha:
def __init__(self,
width=160,
height=60,
char_num=4,
characters=string.digits + string.ascii_uppercase + string.ascii_lowercase):
"""
GenerateCaptcha类的构造函数
:param width: 验证码图片的宽
:param height: 验证码图片的高
:param char_num: 验证码字符个数
:param characters: 验证码组成字符串,其中包含总数字串(0~9)+大写字母串(A~Z)+小写字母串(a~z)
"""
self.width = width
self.height = height
self.char_num = char_num
self.characters = characters
self.classes = len(characters)

def gen_captcha(self, batch_size=50):
"""
生成一组验证码训练数据
:param batch_size:用以指定这一组训练数据的个数
:return:X(这一组训练数据的输入X), Y(这一组训练数据的输出Y)
"""

# 验证码图片对象,这里只限定了尺寸,还没有写内容
image = ImageCaptcha(width=self.width, height=self.height)
# X:用以存放训练数据的输入部分
# batch_size: 这一组训练数据共有batch_size个
# self.height: 每一个训练数据(验证码)的高度
# self.width: 每一个训练数据(验证码)的宽度
# 1: 每一个训练数据(验证码)的通道数,因为是黑白图片所以为1
X = np.zeros([batch_size, self.height, self.width, 1])

# Y:用以存放训练数据的输出部分
# batch_size: 这一组训练数据共有batch_size个
# self.char_num: 每一个训练数据(验证码)的内容中包含几个字符
# self.classes: 这个字符的内容是什么(这是一个长度为62的向量,因为10个数字加26个大写字母加26个小写字母长度为62,只有一个值为1,表示这个字符内容,其他都是0)
Y = np.zeros([batch_size, self.char_num, self.classes])

while True:
for i in range(batch_size):
# random.sample(self.characters, self.char_num)用于生成一个验证码字符串随机变量
# self.characters:62位字符串(0~9A~Za~z)
# self.char_num:4(生成4个字符)
# captcha_str用以将转好的随机变量转为字符串类型
captcha_str = ''.join(random.sample(self.characters, self.char_num))

# 用内置方法,将字符串变为图片
# convert('L'):表示生成的是灰度图片,就是通道数为1的黑白图片
img = image.generate_image(captcha_str).convert('L')
# 将生成好的图片数据转成np.array类型
img = np.array(img.getdata())
# reshape一下此图片的格式,让他的高度和宽度符合要求
# 每个像素值都要除以255,这是为了归一化处理,因为灰度的范围是0~255,这里除以255就让每个像素的值在0~1之间,加快收敛速度
# 将此图片信息放入X的第i个位置,表示这是训练集的第i条数据
X[i] = np.reshape(img, [self.height, self.width, 1]) / 255.0

# 这里用以生成对应的测试集
# j和ch用以遍历刚刚生成的随机字符串,j记录index(0~3,表示第几个字符),ch记录字符串中的字符
for j, ch in enumerate(captcha_str):
# 找到Y的第i条数据中,第j个字符,然后把62长度的向量和ch相关的那个置位1
Y[i, j, self.characters.find(ch)] = 1
# 重新整理一下Y,第一维度依然是batch_size,后面把char_num和classes相乘,整合成一条向量
Y = np.reshape(Y, (batch_size, self.char_num * self.classes))
yield X, Y

def get_parameter(self):
return self.width, self.height, self.char_num, self.characters, self.classes

captcha_model.py

训练模型的类

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import tensorflow as tf
import math


class CaptchaModel:
def __init__(self,
width=160,
height=60,
char_num=4,
classes=62):
"""
训练模型类的构造函数
:param width: 验证码宽度
:param height: 验证码高度
:param char_num: 验证码内字符数
:param classes: 验证码待选字符个数
"""
self.width = width
self.height = height
self.char_num = char_num
self.classes = classes

def conv2d(self, x, W):
"""
2维卷积函数
strides=[1, 1(x方向步长), 1(y方向步长), 1]
padding='SAME'(边界处理策略,设为'SAME'卷积处理完图片大小不变)
:param x: 被卷积处理的图片
:param W: 卷积核
:return: 卷积处理完的图片
"""
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(self, x):
"""
2x2最大值池化函数
ksize=[1, 2(池化窗口高度), 2(池化窗口高度), 1]
strides=[1, 1(x方向步长), 1(y方向步长), 1]
padding='SAME'
:param x: 待被池化处理的图片
:return: 处理好的图片
"""
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

def weight_variable(self, shape):
# 截断式正态分布生成变量
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)

def bias_variable(self, shape):
# 生成一组常量
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial)

def create_model(self, x_images, keep_prob):
# first layer
w_conv1 = self.weight_variable([5, 5, 1, 32])
b_conv1 = self.bias_variable([32])
# 第一次卷积 (?, 60, 160, 1) --> (?, 60, 160, 32)
h_conv1 = tf.nn.relu(tf.nn.bias_add(self.conv2d(x_images, w_conv1), b_conv1))
# 第一次池化 (?, 60, 160, 32) --> (?, 30, 80, 32)
h_pool1 = self.max_pool_2x2(h_conv1)
# 第一层dropout
h_dropout1 = tf.nn.dropout(h_pool1, keep_prob)
conv_width = math.ceil(self.width / 2)
conv_height = math.ceil(self.height / 2)

# second layer
w_conv2 = self.weight_variable([5, 5, 32, 64])
b_conv2 = self.bias_variable([64])
# 第二次卷积 (?, 30, 80, 32) --> (?, 30, 80, 64)
h_conv2 = tf.nn.relu(tf.nn.bias_add(self.conv2d(h_dropout1, w_conv2), b_conv2))
# 第二次池化 (?, 30, 80, 64) --> (?, 15, 40, 64)
h_pool2 = self.max_pool_2x2(h_conv2)
# 第二层dropout
h_dropout2 = tf.nn.dropout(h_pool2, keep_prob)
conv_width = math.ceil(conv_width / 2)
conv_height = math.ceil(conv_height / 2)

# third layer
w_conv3 = self.weight_variable([5, 5, 64, 64])
b_conv3 = self.bias_variable([64])
# 第三次卷积 (?, 15, 40, 64) --> (?, 15, 40, 64)
h_conv3 = tf.nn.relu(tf.nn.bias_add(self.conv2d(h_dropout2, w_conv3), b_conv3))
# 第三次池化 (?, 15, 40, 64) --> (?, 8, 20, 64)
h_pool3 = self.max_pool_2x2(h_conv3)
# 第三层dropout
h_dropout3 = tf.nn.dropout(h_pool3, keep_prob)
conv_width = math.ceil(conv_width / 2)
conv_height = math.ceil(conv_height / 2)

# 第一层全连接神经网络
conv_width = int(conv_width)
conv_height = int(conv_height)
# 第一层权值 (64 * 20 * 8, 1024)
w_fc1 = self.weight_variable([64 * conv_width * conv_height, 1024])
# 第一层bias (1, 1024)
b_fc1 = self.bias_variable([1024])
# 将CNN的输出结果展开成一条线,shape为(64 * 20 * 8, 1)
h_dropout3_flat = tf.reshape(h_dropout3, [-1, 64 * conv_width * conv_height])
# 神经网络处理,激发函数选择relu (64 * 20 * 8, 1) --> (1024, 1)
h_fc1 = tf.nn.relu(tf.nn.bias_add(tf.matmul(h_dropout3_flat, w_fc1), b_fc1))
# dropout处理
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

# second fully layer
w_fc2 = self.weight_variable([1024, self.char_num * self.classes])
b_fc2 = self.bias_variable([self.char_num * self.classes])
# 神经网络处理, (1024, 1) --> (4 * 62, 1)
y_conv = tf.add(tf.matmul(h_fc1_drop, w_fc2), b_fc2)

return y_conv

predict_captcha.py

我们把训练好的模型保存起来,之后可以使用此代码去识别任意的验证码,只要图片输入的要求符合高60宽160且1通道黑白图片即可。这个类上面我没讲解,我也没机会试,因为训练时间太久了我还没训出来呢,据说GPU训练需要4~5个小时,CPU训练的话需要20小时,大家训练好了可以拿去试试。

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
from PIL import Image
import tensorflow as tf
import numpy as np
import sys
import generate_captcha
import captcha_model

if __name__ == '__main__':
captcha = generate_captcha.GenerateCaptcha()
width, height, char_num, characters, classes = captcha.get_parameter()

gray_image = Image.open(sys.argv[1]).convert('L')
img = np.array(gray_image.getdata())
test_x = np.reshape(img, [height, width, 1]) / 255.0
x = tf.placeholder(tf.float32, [None, height, width, 1])
keep_prob = tf.placeholder(tf.float32)

model = captcha_model.CaptchaModel(width, height, char_num, classes)
y_conv = model.create_model(x,keep_prob)
predict = tf.argmax(tf.reshape(y_conv, [-1,char_num, classes]),2)
init_op = tf.global_variables_initializer()
saver = tf.train.Saver()
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.95)
with tf.Session(config=tf.ConfigProto(log_device_placement=False, gpu_options=gpu_options)) as sess:
sess.run(init_op)
saver.restore(sess, "capcha_model.ckpt")
pre_list = sess.run(predict, feed_dict={x: [test_x], keep_prob: 1})
for i in pre_list:
s = ''
for j in i:
s += characters[j]
print(s)