使用tensorflow识别验证码

Posted by grt1stnull on 2018-03-30

0x01.前言

练习项目,使用tensorflow实现简单的验证码识别。

0x01.数据收集

通常来说,验证码识别项目中,数据的来源有两种:

  • 使用代码生成
  • 使用爬虫爬取

第一种方法比较实用,因为相比于第二种,它不需要人工手动打标签。在python中,常用的两种验证码生成的方法是使用captcha库,或使用pillow库手动来画。

这里我采用的方法为第二种,因为数据比较原汁原味。缺点就是,数据量比较少(我只标记了146份数据)。

爬虫脚本很简单:

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
import requests
from lxml import etree
def save_file(url, file_name):
if url is None: return
file_name = 'capture/' + file_name
url = 'http://zfw.xidian.edu.cn' + url
r = requests.get(url, timeout = 20)
with open(file_name, "wb") as code:
code.write(r.content)
def get_url():
url = 'http://zfw.xidian.edu.cn/'
r = requests.get(url)
html = etree.HTML(r.text)
weibo_list = html.xpath("//img[@id='loginform-verifycode-image']")
try:
src = weibo_list[0].attrib['src']
except e as Exception:
src = None
finally:
return src
for i in range(0, 100):
urll = get_url()
save_file(urll, '%i.png' % i)

结果如下(部分):

verifi1

验证码尺寸为50 x 120

0x02.数据预处理

1.去噪

对于验证码,为了加强机器识别的难度,都加上了比如阴影、小色块、横线等等,所以通常都需要对收集到的验证码再进一步的处理:去噪。去噪可以分为机器学习的方法和常规的方法。

这里验证码比较简单,不用考虑这里。

2.裁剪

因为验证码中都不止有一个字符,所以需要通过裁剪来将验证码裁开,使裁开后的图片中只有一个字符。

可能会有人说能不能不裁剪,直接整体进行识别。当然可以整体识别,但是这意味结果分类会很多,网络的结构很复杂。即需要大量的样本及非常复杂的神经网络。倒也不是说这样不可以,只是我觉得杀鸡焉用宰牛刀。如此庞大的神经网络,是对资源的一种浪费,以及缺乏简单模型调优的优雅。

话说回来,与验证码的噪声相似,为了增加机器的识别难度,通常验证码中的字符并不是简单的排列在图片中,而是有粘连,有压缩、展开的,表现为宽度不定、高度不定与可能与相邻字符连接。

这里与上一节相同,是数据处理过程中很重要的一步。

我在这里的处理比较简单,通过对爬取样本的比较,我选择了最优的裁剪宽高。

如上所述,爬取的验证码大小为50 x 120,观察得到的四个字符的截取最佳x轴长度为5-3225-5243-7070-97,y轴长度为统一的0-40。考虑到中间字符易于前后字符粘连,所以只保留了前后字符的截取,同时去掉了过于粘连的字符图片。

代码如下:

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
import os
import random
import string
from PIL import Image
# 生成随机字符串,用于命名裁剪后图片
def rrr():
return ''.join(random.sample(string.ascii_letters + string.digits, 3))
# 验证码图片裁剪函数,只保留了前后字符
def cut(img):
iii = []
iii.append(img.crop((5, 0, 32, 40)))
#iii.append(img.crop((25, 0, 52, 40)))
#iii.append(img.crop((43, 0, 70, 40)))
iii.append(img.crop((70, 0, 97, 40)))
for i in iii:
i.save(os.path.join('deepuse/capture/deal1', '%s.jpg' % rrr()))
# 遍历爬取文件,进行裁剪
for i in os.listdir('deepuse/capture'):
b = os.path.join('deepuse/capture', i)
if os.path.isdir(b): continue;
a = Image.open(b)
cut(a)

经过手工标记后(将类别放在名字前),样本数量为146个。部分效果图:

verifi2

裁剪后的尺寸为27 x 40

3.准备数据

内容是读取裁剪后的图片,处理成可以被模型接受的数据。

首先将数据随机划分,这里用手工的方式,没有使用其他库。

1
2
3
4
5
6
7
8
9
10
11
12
13
import random
# 生成随机序列,用于读取数据
def data_iter(batch_size, c, d):
# batch大小, 图片序列, 图片类别
num_examples = c.shape[0]
# 产生一个随机索引
idx = list(range(num_examples))
random.shuffle(idx)
# 拼凑后生成数据
for i in range(0, num_examples, batch_size):
mm = min(i+batch_size, num_examples)
yield c[i: mm], d[i: mm]

然后处理数据,第一个是类别,将类别从文件名中取出后,得到one-hot型的向量。

1
2
3
4
5
6
7
8
9
10
import numpy as np
# 处理标签
def set_label(num):
a = []
for i in range(10):
if i == num: a.append(1.)
else: a.append(0.)
# 返回独热编码,如[0,0,0,0,0,0,0,0,1,0]表丝类别8
return np.array(a)

处理数据的第二个是,将图像转化为归一化的灰度图像,即与mnist手写字符识别的图像类似。

通过Image.open()函数打开的图像,尺寸为(y, x, 3),3为RGB通道的颜色。而我们都知道,mnist的数据不是这样的,它数据的尺寸通常为(28, 28)784。所以这里需要对图像数据做一个处理。

处理的过程参考自知乎:如何将图片转换为mnist格式的数据?

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
from PIL import Image
# 处理数据
def set_data(imm):
arr = []
for i in range(40):
for j in range(27):
# mnist 里的颜色是0代表白色(背景),1.0代表黑色
pixel = 1.0 - float(imm.getpixel((j, i)))/255.0
# pixel = 255.0 - float(img.getpixel((j, i))) # 如果是0-255的颜色值
arr.append(pixel)
return np.array(arr)

因为这里是序列化数据,所以不用再进一步resize成原图像的shape了。

最后是读取裁剪后的图像,准备喂给模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np
from PIL import Image
# 读取数据
def read_data():
xx = []
yy = []
for i in os.listdir('/home/grt1st/deepuse/capture/deal1'):
p = os.path.join('/home/grt1st/deepuse/capture/deal1', i)
# 转化为灰度图
xx.append(set_data(Image.open(p).convert('L')))
yy.append(set_label(int(i[0])))
# 返回图像、标签
return np.array(xx), np.array(yy)

原图与处理后对比:

verifi3

0x03.选择模型

不知道为什么,在jupter book中,竟然无法引入tensorflow模块,于是这样:

1
2
3
import sys
sys.path.append('/usr/lib/python3.6/site-packages')
import tensorflow as tf

至于为什么使用tensorflow而不使用其他上层框架,一方面是因为我对框架的使用还不熟悉,另一方面我不想错过一些细节,虽然框架提供了便利。

1.定义模型

使用的是简单的两层神经网络模型。使用cnn效果会更好。

首先定义超参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Parameters
learning_rate = 0.5
training_epochs = 20
batch_size = 7
display_step = 1
examples_to_show = 5
# Network Parameters
n_hidden_1 = 256 # 1st layer num features
n_hidden_2 = 10 # 2nd layer num features\
n_input = 1080 # pic data input (img shape: 40*27)
# tf Graph input (only pictures)
x_ = tf.placeholder(tf.float32, [None, n_input])
y_ = tf.placeholder(tf.float32, [None, 10])

然后定义模型细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
weights = {
'layer1_h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])),
'layer2_h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])),
}
biases = {
'layer1_b1': tf.Variable(tf.random_normal([n_hidden_1])),
'layer2_b2': tf.Variable(tf.random_normal([n_hidden_2])),
}
x = tf.nn.sigmoid(tf.add(tf.matmul(x_, weights['layer1_h1']), biases['layer1_b1']))
y = tf.nn.softmax(tf.matmul(x, weights['layer2_h2']) + biases['layer2_b2'])

最后是损失与优化器:

1
2
3
4
5
6
7
# Define loss and optimizer, minimize the squared error
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(cross_entropy)
# Initializing the variables
init = tf.global_variables_initializer()

2.训练模型

首先读取数据,然后运行tensorflow会话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 读取数据
c, d = read_data()
# 建立会话
sess = tf.Session()
# init
sess.run(init)
#控制计数
j = 0
# 读取数据
for (a, b) in data_iter(batch_size, c, d):
# Loop over
if j > training_epochs: break;
else: j += 1;
# Run optimization op (backprop) and cost op (to get loss value)
# 因为数据量比较少,多运行几次...
for k in range(5):
_, co = sess.run([train_step, cross_entropy], feed_dict={x_: a, y_: b})
# Display logs per epoch step
# 展示效果
print("Epoch:", '%04d' % j, "cost=", "{:.9f}".format(co))
print("Optimization Finished!")

3.评估模型与预测

因为数据量比较少,在batch中定义准确率浮动会很大,所以没有定义。

y的输出为维度为10的向量,即10种数字类别,其中每个维度最大的为对应类别。

定义类别函数:

1
2
3
4
5
6
7
8
def get_label(ll):
init = 0
ma = ll[0]
for num in range(len(ll)):
if ll[num] > ma:
ma = ll[num]
init = num
return init

以一个验证码为例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from PIL import Image
# 打开图片
img = Image.open('/home/grt1st/deepuse/capture/captcha.png')
# 裁剪后显示图片
img2 = img.crop((70, 0, 97, 40))
plt.imshow(img2)
# 处理后,显示
img2 = set_data(img2.convert('L'))
plt.imshow(img2.reshape(40,27))
# 验证结果
yyy = sess.run(y, feed_dict={x_: np.array([img2])})
print(get_label(yyy[0].tolist()))
print(yyy[0].tolist())

0x04.总结与反思

在写完代码后,顺带看了其他人的实现,发现了一个神奇的东西:有人计算损失时,使用了tf.nn.sigmoid_cross_entropy_with_logits()函数。因为在我印象里,通常都是tf.nn.softmax_cross_entropy_with_logits嘛。

于是查阅资料,我发现是这样的。sigmoid即逻辑回归,作二元分类,而softmax是作多元分类的。除此之外,对于多分类问题,可以有多个逻辑回归分类器与softmax两种形式,差别在于逻辑回归中各分类不互斥,即某一事物可以属于多个类,而softmax中各个分类是互斥的。还有差异表现在实现上,softmax将一个向量压缩到表示概率,相加为1,而逻辑回归中,则分为各个标量,每个表示属于这种类别的概率,即很多种不同的概率。

参考自tensorflow中交叉熵系列函数[Tensorflow]1.交叉熵(Cross Entropy)算法实现及应用