Captchatool是一个简单易用的验证码识别的小型模块,其开发之初就是以快捷,简单为目的的工具模块,利用了python语言和tensorflow深度学习框架开发而成!
- 其原理是通过深度学习,来完成验证码识别
- 支持训练,识别各种验证码格式的模型,例如:gif,png格式的验证码
- 支持导入模型继续训练或直接使用已有模型来识别
- 操作简单,模型训练,验证码识别一行代码调用完成
第一部分
安装
- 环境要求: python3
- 点击我把项目克隆或直接下载
- 在项目目录下输入命令
pip3 install -r r-cpu.txt
或者是pip3 install -r r-gpu.txt
,后者会安装tensorflow-gpu版本.(强烈推荐gpu版本,gpu版本会在模型训练时会比cpu版本快很多,但是会多了一些安装步骤,具体请百度)
使用
- 导入模块
from cnn_code_tool import Code_tool
- 创建类
ct=Code_tool(charset,model_path,train_path,test_path,label_resolve)
# charset 验证码的所有字符集
# model_path 模型路径,训练完后模型将会保存在此路径下,推测验证码时模型将会从该路径下导入模型
# train_path 训练数据路径,模型训练时使用
# test_path 测试数据路径,模型训练时使用
# label_resolve 从文件名解析出标签的函数,模型训练时使用
- 训练模型
ct.train()
- 训练完后推测验证码
captcha=ct.infer_file('captcha_img_filename') # 导入并推测验证码图片的验证码
帮助
- 上面的使用说明已经是一个完整的例子了,更多运行例子可以运行项目demo,我已经添加了大量注释来说明
- 一般完成一个验证码的识别的过程有如下步骤
- 手工打码,把打码后的图片,放到训练路径下,测试图片同理(一般对于只含数字和26个字母长度为4的验证来说,并且可以切割出一个个字符的话,500~1000张打码后的图片即可达到0.8左右的命中率,如果不能分割的话一般需要几千张图片来训练模型)
- 数据准备好后创建Code_tool类调用
train()
方法训练模型 - 模型训练完后,一般调用
infer_file()
或者infer_bytes()
来推测验证码,infer_file()
用于从文件导入数据来推测其验证码,infer_bytes()
一般用于网络获取验证码的字节对象后直接推测其验证码,例如使用requests库请求验证码后马上推测其验证码
r=requests.get('获取验证码的url') captcha=ct.infer_bytes(r.content) print(captcha) # 打印模型推测的验证码
- 值得注意的是推测的图片要和模型训练时的图片的宽度,高度,通道,格式要完全一致
第二部分
致爱学习的程序员的一部分
说明
- 如果只是使用的话,大可不必看这部分
- 主要是讲解怎么利用tensorflow,cnn来完成识别验证码
- 前置准备
- cnn是什么?这一步非常有必要,要不然下面章节会看的糊里糊涂。 点击我了解什么是cnn
- 由于我会默认你已经运行过demo,所以强烈建议你运行Captchatool项目下的demo,了解Captchatool模块识别验证码的过程 项目地址
- 了解tensorflow的运作方式
开始
- Captchatool模块其实是由两部分组成,一部分是数据读取处理,占代码量2/3左右,一部分是和tensorflow和cnn相关的部分,占代码量1/3左右.
- 数据读取处理部分:所有读取处理的数据最终都会汇集到
generate_next_batch()
和generate_test_batch()
上,其功能分别是向模型提供一批次训练数据和一批次测试数据,由于主要讲解tensorflow和cnn故不展开太大篇幅,在后面的讲解中把这两个方法看出提供数据的黑盒即可(这部分以后再写篇文章说明吧) - tensorflow和cnn部分将会从Captchatool的
train()
为入口,以定义cnn模型,训练模型为过程来进行说明
- 数据读取处理部分:所有读取处理的数据最终都会汇集到
train()代码
```python
'''
参数:
epochs: 训练集迭代次数
target_ac: target_ac目标命中率,当模型达到该命中率时不管是否达到迭代次数都会退出训练
retrain: True表示重新训练,False表示导入self.__model_path路径下最近更新的一个模型继续训练
keep_prob: 即tf.nn.dropout(x, keep_prob)的第二个参数,该参数在测试集测试时将不会生效
'''
def train(self, epochs=10, target_ac=1.,keep_prob=1.,retrain=True):
if self.__input_path is None or self.__test_input_path is None:
print('__input_path或__test_input_path缺失')
return
self.__startwork(epochs)
# 开始训练
with tf.Session() as sess:
if retrain:
self.__init_cnn()
save = tf.train.Saver()
sess.run(tf.global_variables_initializer())
step = 0
else:
cp = tf.train.latest_checkpoint(self.__model_path)
save = tf.train.import_meta_graph(cp + '.meta')
save.restore(sess, cp)
step = int(sess.run('g_v:0'))
while True:
try:
img, label = self.__generate_next_batch()
except NullDataException:
break
loss, _ = sess.run(['loss:0', 'train'],
feed_dict={'i_p:0': img, 'l_p:0': label, 'k_p:0': keep_prob})
print('步数为:{}\tloss:{}'.format(step, loss))
if step % 50 == 0:
test_img, test_label = self.__test_queue.get()
# 这里的命中率是针对单个字符的不代表真正的命中率
actual_ac = sess.run('accuracy:0',
feed_dict={'i_p:0': test_img, 'l_p:0': test_label,
'k_p:0': 1.})
print('步数为:{}--------------------------------------命中率:{}'.format(step, actual_ac))
if actual_ac >= target_ac:
break
step += 1
# 保存
tf.summary.FileWriter(self.__model_path, sess.graph)
g_step = tf.get_default_graph().get_tensor_by_name('g_v:0')
tf.assign(g_step, step, name='update')
sess.run('update:0')
self.__endwork()
print('保存模型中请等待!')
save.save(sess, self.__model_path + 'model', global_step=step)
print('完成')
```
定义模型
- 定义模型相关的代码
if retrain:
self.__init_cnn()
save = tf.train.Saver() # 在这里不重要
sess.run(tf.global_variables_initializer())
step = 0 # 在这里不重要
retrain等于True的情况下:
self.__init_cnn()
将会描述一个模型(或者叫图),注意这里是描述不是创建,只有调用sess.run(tf.global_variables_initializer())
这行代码,tensorflow才是真正的创建模型,这与tensorflow框架的运行方式有关,只有调用sess.run('需要运行的operations或者tensors')
tensorflow才会执行运算操作。例如Captchatool项目的demo1中self.__init_cnn()
将会描述一个这样的模型(点击查看模型图),sess.run(tf.global_variables_initializer())
将会初始化该模型,在模型没有初始化之前,请不要用sess.run()
调用任何模型中的operations或者tensors,否则tensorflow会报错。那么
self.__init_cnn()
是怎么描述模型的呢?(点击我查看代码)-
首先是卷积层,卷积完后马上使用激活函数relu,利用最大池化函数下采样减小大小,并接上dropout层,反复三次,总共三次卷积层,这三层卷积只是卷积核的数量不同而已,大小都是一样的。可视化后就像这样的:卷积层
# 卷积层 h = conv2(img_b, [3, 3, self.__channel, 32],'weight','bais') h = tf.nn.relu(h) h = max_pool_2x2(h) h = tf.nn.dropout(h, keed)
-
三层卷积层后,接上连接层,全连接层可视化后是这样的:fc
# 全连接层 shape = h.get_shape().as_list() dense=conv2(h,[shape[1] , shape[2] , shape[3],1024],'fc_weight','fc_bais',padding='VALID') dense = tf.nn.relu(dense) dense = tf.nn.dropout(dense, keed)
是不是觉得和卷积层很像?其实全连接层可以用卷积操作来实现,在这里对前层的的输入进行全局卷积,卷积完后将会输出一个[batch_size,1,1,1024]的特征映射
-
全连接层后面接着就是最后的输出层
# 输出层 out=conv2(dense,[1,1,1024,self.__max_captcha_len * self.__charset_len],'out_weight','out_bais') out=tf.reshape(out,[-1,self.__max_captcha_len,self.__charset_len],name='out')
说简单点就是利用卷积操作来实现降维,降的是什么呢?降的是卷积核的数量。使其卷积核数量等于验证码长度*验证码的字符集字符个数,这样做主要和分类有关系。例如对于一个验证长度为1,验证码字符集只含数字和字母(不分大小写)的验证码来说:那么输出层就应该有36个卷积核来对应36个验证码字符。这样做有什么用?假设现在有一张这样的验证码
验证码
其输出层输出结果会是这样的(这里是用了没有训练的模型):没训练前的结果
其中每个输出的数字代表着每个卷积核卷积后输出的结果,这时只需找出最大值的索引,再进行相应的转换即可得出对应的标签。那为什么是最大值而不是最小值呢?这里面涉及到一个激活的问题,举个小小的例子,这个字符e对应的卷积核在遇到特定特征(在这里就是e字符的高级特征)时,该卷积核将会被激活,此时该卷积核卷积后的输出将会很大,而其他字符的卷积核并不会被激活因此卷积后会输出一个很小的值。那么卷积核怎么知道对那些特征进行激活那些特征不需要激活呢?没错就是通过训练来训练卷积核来学习某些特定特征。
训练模型
-
训练后的结果
这张图是用了已经训练好的模型,对比上面那张图,值与值差距变大了且出现了一个正数,这个正数就是e字符对应的卷积核卷积后输出的值。 - 训练模型其实是通过一个名为反向传播的过程来进行对模型的训练,整个过程中会不断调整各个卷积核的形状(权重),使某些卷积核学习某些特征,一旦遇到某些相似的特征会被激活。 (这是一篇通俗易懂介绍反向传播的文章,点击查看)
- 想要每个卷积核学到的特征不同,就要随机初始化各个卷积核,使他们拥有不同的初始形状(权重),请不要使用相同的数值来初始卷积核,这样会使每个卷积核都会学到相同的特征
# self.__init_cnn()方法内部使用了标准的正态分布来初始化卷积核
weight = tf.Variable(w_alpha * tf.random_normal(ksize),name=w_name) # 初始化卷积核
# weight = tf.Variable(w_alpha * tf.zeros(ksize),name=w_name) 全初始化为0,请不要这做!!
- 利用tensorflow只需调用相关api即可描述整个训练过程,例如
self.__init_cnn()
方法中与训练有关的代码
loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=label_b, logits=out),name='loss')
train_op = tf.train.AdamOptimizer().minimize(loss, name='train') # 训练的operations
- 想要真正训练,只需使用
tf.run()
调用训练的operations即可
tf.run(train_op,args......) # 把返回的变量直接传进run方法内
# tf.run('train',args......) 或者通过operations的name属性
结语
这篇文章断断续续写了一个星期,基本上把我认为比较重要都写了,有的地方觉得写的不好,反复改了几次。如果有什么写错,或者写的不好请多多谅解,或者你可以通过简书私信告知我,我会虚心接受。