Captchatool一个验证码识别模块(python,tensorflow)

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属性

结语

这篇文章断断续续写了一个星期,基本上把我认为比较重要都写了,有的地方觉得写的不好,反复改了几次。如果有什么写错,或者写的不好请多多谅解,或者你可以通过简书私信告知我,我会虚心接受。

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

推荐阅读更多精彩内容