使用 TensorFlow 识别简单图像验证码

公司有一个业务需要抓取某网站数据,登录需要识别验证码,类似下面这种,这应该是很多网站使用的验证码类型。

首先由于验证码比较简单,图像不复杂,而且全部是数字。于是试着采用传统方式,按照网上教程自己简单改了一个,使用 PHP 识别。大概流程就是切割二值化去噪等预处理,然后用字符串数组形式保存起来,识别传来的图片同样预处理后比较字符串的相似度,选出一个相识度最高的分类。识别率不是很理想(验证码比较简单,应该能优化得更好),隐约记得只能超过60%。

因为识别效果不理想,目标网站登录状态还是能保持很久,没必要花太多精力在这上面,于是找了一个人工打码服务。简直太便宜了,一个月花不了多少钱,效果还好,只是有时候延迟比较高。反正对于我们的业务来说是足够用了。

机器学习大潮来临,我寻思着能不能用在这上面,于是参考 TensorFlow 识别手写数字教程,开始照猫画虎。

本文描述的只是作为一个普通开发者的一些粗浅理解,所有的代码和数据均在文后的 GitHub 有存留,建议结合代码阅读本文。如果有什么理解错误或 Bug 欢迎留言交流 ^_^

TensorFlow 是什么

TensorFlow 是谷歌出的一款机器学习框架。看名字,TensorFlow 就是“张量流”。呃。。什么是张量呢?张量我的理解就是数据。张量有自己的形状,比如 0 阶张量是标量,1 阶是向量,2 阶是矩阵。。。所以在后文我们会看到在 TensorFlow 里面使用的量几乎都要定义其形状,因为它们都是张量。

我们可以把 TensorFlow 看作一个黑盒子,里面有一些架好的管道,喂给他一些“张量”,他吐出一些“张量”,吐出的东西就是我们需要的结果。

所以我们需要确定喂进去的是什么,吐出来的是什么,管道如何搭建。

更多的入门概念可以查看这个 keras新手指南 » 一些基本概念

为什么使用 TensorFlow

没别的什么原因,只是因为谷歌大名,也没想更多。先撸起袖子干起来。如果为了快速成型,我建议可以看一下 Keras,号称为人类设计的机器学习框架,也就是用户体验友好,提供好几个机器学习框架更高层的接口。

大体流程

  1. 抓取验证码
  2. 给验证码打标签
  3. 图片预处理
  4. 保存数据集
  5. 构建模型训练
  6. 提取模型使用

抓取验证码

这个简单,随便什么方式,循环下载一大堆,这里不再赘述。我这里下载了 750 张验证码,用 500 张做训练,剩下 250 张验证模型效果。

给验证码打标签

这里的验证码有750张之巨,要是手工给每个验证码打标签,那一定累尿了。这时候就可以使用人工打码服务,用廉价劳动力帮我们做这件事。人工打码后把识别结果保存下来。这里的代码就不提供了,看你用哪家的验证码服务,相信聪明的你一定能解决 :)

图片预处理

  1. 图片信息:此验证码是 68x23,JPG格式
  2. 二值化:我确信这个验证码足够简单,在丢失图片的颜色信息后仍然能被很好的识别。并且可以降低模型复杂度,因此我们可以将图片二值化。即只有两个颜色,全黑或者全白。
  3. 切割验证码: 观察验证码,没有特别扭曲或者粘连,所以我们可以把验证码平均切割成4块,分别识别,这样图片识别模型就只需要处理10个分类(如果有字母那将是36个分类而已)由于验证码外面有一圈边框,所以顺带把边框也去掉了。
  4. 处理结果: 16x21,黑白2位

相关 Python 代码如下:

img = Image.open(file).convert('L') # 读取图片并灰度化

img = img.crop((2, 1, 66, 22)) # 裁掉边变成 64x21

# 分离数字
img1 = img.crop((0, 0, 16, 21))
img2 = img.crop((16, 0, 32, 21))
img3 = img.crop((32, 0, 48, 21))
img4 = img.crop((48, 0, 64, 21))

img1 = np.array(img1).flatten() # 扁平化,把二维弄成一维
img1 = list(map(lambda x: 1 if x <= 180 else 0, img1)) # 二值化
img2 = np.array(img2).flatten()
img2 = list(map(lambda x: 1 if x <= 180 else 0, img2))
img3 = np.array(img3).flatten()
img3 = list(map(lambda x: 1 if x <= 180 else 0, img3))
img4 = np.array(img4).flatten()
img4 = list(map(lambda x: 1 if x <= 180 else 0, img4))

保存数据集

数据集有输入输入数据和标签数据,训练数据和测试数据。
因为数据量不大,简便起见,直接把数据存成python文件,供模型调用。就不保存为其他文件,然后用 pandas 什么的来读取了。

最终我们的输入模型的数据形状为 [[0,1,0,1,0,1,0,1...],[0,1,0,1,0,1,0,1...],...]
标签数据很特殊,本质上我们是对输入的数据进行分类,所以虽然标签应该是0到9的数字,但是这里我们使标签数据格式是 one-hot vectors [[1,0,0,0,0,0,0,0,0,0,0],...]
一个one-hot向量除了某一位的数字是1以外其余各维度数字都是0**,比如[1,0,0,0,0,0,0,0,0,0] 代表1,[0,1,0,0,0,0,0,0,0,0]代表2.
更进一步,这里的 one-hot 向量其实代表着对应的数据分成这十类的概率。概率为1就是正确的分类。

相关 Python 代码如下:

# 保存输入数据
def px(prefix, img1, img2, img3, img4):
    with open('./data/' + prefix + '_images.py', 'a+') as f:
        print(img1, file=f, end=",\n")
        print(img2, file=f, end=",\n")
        print(img3, file=f, end=",\n")
        print(img4, file=f, end=",\n")

# 保存标签数据
def py(prefix, code):
    with open('./data/' + prefix + '_labels.py', 'a+') as f:
        for x in range(4):
            tmp = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
            tmp[int(code[x])] = 1
            print(tmp, file=f, end=",\n")

经过上面两步,我们在就获得了训练和测试用的数据和标签数据,呐,就像这样

构建模型训练

数据准备好啦,到了要搭建“管道”的时候了。
也就是你需要告诉 TensorFlow:

1. 输入数据的形状是怎样的?

x = tf.placeholder(tf.float32, [None, DLEN])

None 表示不定义我们有多少训练数据,DLEN是 16*21,即一维化的图片的大小。

2. 输出数据的形状是怎样的?

y_ = tf.placeholder("float", [None, 10])

同样None 表示不定义我们有多少训练数据,10 就是标签数据的维度,即图片有 10 个分类。每个分类对应着一个概率,所以是浮点类型。

3. 输入数据,模型,标签数据怎样拟合?

W = tf.Variable(tf.zeros([DLEN, 10])) # 权重
b = tf.Variable(tf.zeros([10])) # 偏置

y = tf.nn.softmax(tf.matmul(x, W) + b)

是不是一个很简单的模型?大体就是
y = softmax(Wx+b)
其中 W 和 b 是 TensorFlow 中的变量,他们保存着模型在训练过程中的数据,需要定义出来。而我们模型训练的目的,也就是把 W 和 b 的值确定,使得这个式子可以更好的拟合数据。
softmax 是所谓的激活函数,把线性的结果转换成我们需要的样式,也就是分类概率的分布。
关于 softmax 之类更多解释请查看参考链接。

4. 怎样评估模型的好坏?

模型训练就是为了使模型输出结果和实际情况相差尽可能小。所以要定义评估方式。
这里用所谓的交叉熵来评估。

cross_entropy = -tf.reduce_sum(y_*tf.log(y))

5. 怎样最小化误差

现在 TensorFlow 已经知道了足够的信息,它要做的工作就是让模型的误差足够小,它会使出各种方法使上面定义的交叉熵 cross_entropy 变得尽可能小。
TensorFlow 内置了不少方式可以达到这个目的,不同方式有不同的特点和适用条件。在这里使用梯度下降法来实现这个目的。

train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

训练准备

大家知道 Python 作为解释型语言,运行效率不能算是太好,而这种机器学习基本是需要大量计算力的场合。TensorFlow 在底层是用 C++ 写就,在 Python 端只是一个操作端口,所有的计算都要交给底层处理。这自然就引出了会话的概念,底层和调用层需要通信。也正是这个特点,TensorFlow 支持很多其他语言接入,如 Java, C,而不仅仅是 Python。
和底层通信是通过会话完成的。我们可以通过一行代码来启动会话:

sess = tf.Session()
# 代码...
sess.close()

别忘了在使用完后关闭会话。当然你也可以使用 Python 的 with 语句来自动管理。

在 TensorFlow 中,变量都是需要在会话启动之后初始化才能使用。

sess.run(tf.global_variables_initializer())

开始训练

for i in range(DNUM):
    batch_xs = [train_images.data[i]]
    batch_ys = [train_labels.data[i]]
    sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

我们把模型和训练数据交给会话,底层就自动帮我们处理啦。
我们可以一次传入任意数量数据给模型(上面设置None的作用),为了训练效果,可以适当调节每一批次训练的数据。甚至于有时候还要随机选择数据以获得更好的训练效果。在这里我们就一条一条训练了,反正最后效果还可以。要了解更多可以查看参考链接。

检验训练结果

这里我们的测试数据就要派上用场了

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
print(sess.run(accuracy, feed_dict={x: test_images.data, y_: test_labels.data}))

我们模型输出是一个数组,里面存着每个分类的概率,所以我们要拿出概率最大的分类和测试标签比较。看在这 250 条测试数据里面,正确率是多少。当然这些也是定义完操作步骤,交给会话来运行处理的。


提取模型使用

在上面我们已经把模型训练好了,而且效果还不错哦,近 99% 的正确率,或许比人工打码还高一些呢(获取测试数据时候常常返回有错误的值)。但是问题来了,我现在要把这个模型用于生产怎么办,总不可能每次都训练一次吧。在这里,我们就要使用到 TensorFlow 的模型保存和载入功能了。

保存模型

先在模型训练的时候保存模型,定义一个 saver,然后直接把会话保存到一个目录就好了。

saver = tf.train.Saver()
# 训练代码
# ...
saver.save(sess, 'model/model')
sess.close()

当然这里的 saver 也有不少配置,比如保存最近多少批次的训练结果之类,可以自行查资料。

恢复模型

同样恢复模型也很简单

saver.restore(sess, "model/model")

当然你还是需要定义好模型,才能恢复。我的理解是这里模型保存的是训练过程中各个变量的值,权重偏置什么的,所以结构架子还是要事先搭好才行。


最后

这里只是展示了使用 TensorFlow 识别简单的验证码,效果还不错,上机器学习应该也不算是杀鸡用牛刀。毕竟模型无脑,节省很多时间。如果需要识别更加扭曲,更加变态的验证码,或许需要上卷积神经网络之类,图片结构和颜色信息都不能丢掉了。另一方面,做网站安全这块,纯粹的图形验证码恐怕不能作为判断是不是机器人的依据。对抗到最后,就变成这样的变态验证码哈哈哈。


相关链接

https://github.com/purocean/tensorflow-simple-captcha
https://keras-cn.readthedocs.io/en/latest/for_beginners/concepts/
http://wiki.jikexueyuan.com/project/tensorflow-zh/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容