CRNN+CTC实现不定长验证码识别(keras模型-训练篇)

2023-11-10

前言

  本文为CRNN+CTC实现不定长验证码识别(keras模型-示例篇)的续篇,示例篇中使用的字符仅为数字,本文将训练集拓展到包含数字字母在内的数据集,同时替换了模型中的部分网络层试图提高效果及效率(未验证),并在训练过程使用了一些小技巧(tricks),极大程度上避免因数据集字符数量的拓展而导致模型不收敛的问题。

运行环境

  • python环境(anaconda+python3.7.4)
第三方库 版本
captcha 0.3
matplotlib 3.1.1
numpy 1.17.2
opencv-python 4.1.1
seaborn 0.9.0
tensorflow 1.14.0

p.s.: 由于以下部分代码使用了f-string格式字符串,因此要求python版本 ≥ \geq 3.6

  • 硬件环境
    cpu: Intel® Xeon® CPU E5-2630 v3 @ 2.40GHz
    硬盘:固态硬盘阵列

生成数据集

  与示例篇一样,这里我们同样利用captcha使用其默认图片大小随机生成长度从4到7的验证码各最多100000张(由于直接以验证码的标签作为文件名,可能出现文件重名而覆盖),并保存到当前工作目录的img_dir目录下,此处生成数据集代码与示例篇代码除生成数量外无异,代码如下

from captcha.image import ImageCaptcha
import os
import random
import string

chars = string.digits + string.ascii_letters # 验证码字符集

def generate_img(img_dir: '图片保存目录'='img_dir'):
    img_generator = ImageCaptcha()
    for length in range(4, 8): # 验证码长度
        if not os.path.exists(f'{img_dir}/{length}'):
            os.makedirs(f'{img_dir}/{length}')
        for _ in range(100000): 
            char = ''.join([random.choice(chars) for _ in range(length)])
            img_generator.write(chars=char, output=f'{img_dir}/{length}/{char}.jpg')
generate_img()

  生成图片后,我们使用opencv读取图片到内存中,这里为了保持验证码的特征,不再像示例篇中作缩小并灰度化的处理。读入图片后每张图片的shape为【图片高度,图片宽度,BGR通道数】,最后将图片真实标签编码为数字备用,代码如下

import cv2
import numpy as np

char_map = {chars[c]: c for c in range(len(chars))} # 验证码编码(0到len(chars) - 1)

def load_img(img_dir: '图片保存目录'='img_dir', min_length: '最小长度'=4, max_length: '最大长度'=7):
    labels = {length: [] for length in range(min_length, max_length + 1)} # 验证码真实标签{长度:标签列表}
    imgs = {length: [] for length in range(min_length, max_length + 1)} # 图片BGR数据字典{长度:BGR数据列表}
    ### 读取图片
    for length in range(min_length, max_length + 1):
        for file in os.listdir(f'{img_dir}/{length}'):
            img = cv2.imread(f'{img_dir}/{length}/{file}')
            labels[length].append(file[:-4])
            imgs[length].append(img)

    ### 编码真实标签
    labels_encode = {length: [] for length in range(min_length, max_length + 1)}
    for length in range(min_length, max_length + 1):
        for label in labels[length]:
            label = [char_map[i] for i in label]
            labels_encode[length].append(label)
    return imgs, labels, labels_encode
imgs, labels, labels_encode = load_img()

构建网络模型

  这里构建的网络模型总体结构与示例篇基本一致,区别只在于中间的卷积层和循环神经网络层,构建出的训练用网络模型如下在这里插入图片描述  这里我不再解释模型末端的设计,详情可参考示例篇。其中输入固定高度为60;循环层使用了双向(Bidirectional)GRU(据说GRU效果跟LSTM差不多,计算效率更高),由于使用的是cpu版的tensorflow,因此没有使用针对cudnn优化过的CuDNNGRU,如有条件可将里面的GRU替换为CuDNNGRU,模型的实现代码如下

import tensorflow as tf
import tensorflow.keras as keras
import tensorflow.keras.backend as K
from tensorflow.keras.layers import BatchNormalization
from tensorflow.keras.layers import Bidirectional
from tensorflow.keras.layers import Conv2D
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Flatten
from tensorflow.keras.layers import GRU
from tensorflow.keras.layers import Input
from tensorflow.keras.layers import Lambda
from tensorflow.keras.layers import MaxPooling2D
from tensorflow.keras.layers import Permute
from tensorflow.keras.layers import ReLU
from tensorflow.keras.layers import Reshape
from tensorflow.keras.layers import TimeDistributed
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.optimizers import Adadelta

def ctc_loss(args):
    return K.ctc_batch_cost(*args)
def ctc_decode(softmax):
    return K.ctc_decode(softmax, K.tile([K.shape(softmax)[1]], [K.shape(softmax)[0]]))[0][0]
def char_decode(label_encode): 
    return [''.join([idx_map[column] for column in row]) for row in label_encode]

labels_input = Input([None], dtype='int32')
sequential = Sequential([
    Conv2D(filters=64, kernel_size=(3, 3), activation='relu', padding='same', input_shape=[60, None, 3]),
    Conv2D(filters=64, kernel_size=(3, 3), activation='relu', padding='same'),
    MaxPooling2D(pool_size=(2, 2)),
    Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding='same'),
    Conv2D(filters=128, kernel_size=(3, 3), activation='relu', padding='same'),
    MaxPooling2D(pool_size=(2, 2)),
    Conv2D(filters=256, kernel_size=(3, 3), activation='relu', padding='same'),
    Conv2D(filters=256, kernel_size=(3, 3), activation='relu', padding='same'),
    MaxPooling2D(pool_size=(2, 2)),
    Conv2D(filters=512, kernel_size=(3, 3), activation='relu', padding='same'),
    Conv2D(filters=512, kernel_size=(3, 3), activation='relu', padding='same'),
    MaxPooling2D(pool_size=(2, 1)),
    Permute((2, 1, 3)),
    TimeDistributed(Flatten()),
    Bidirectional(GRU(128,return_sequences=True)),
    Bidirectional(GRU(128,return_sequences=True)),
    TimeDistributed(Dense(len(chars) + 1, activation='softmax'))
])
input_length = Lambda(lambda x: K.tile([[K.shape(x)[1]]], [K.shape(x)[0], 1]))(sequential.output)
label_length = Lambda(lambda x: K.tile([[K.shape(x)[1]]], [K.shape(x)[0], 1]))(labels_input)
output = Lambda(ctc_loss)([labels_input, sequential.output, input_length, label_length])
fit_model = Model(inputs=[sequential.input, labels_input], outputs=output)
ctc_decode_output = Lambda(ctc_decode)(sequential.output)
model = Model(inputs=sequential.input, outputs=ctc_decode_output)
adadelta = Adadelta(lr=0.05)
fit_model.compile(
    loss=lambda y_true, y_pred: y_pred,
    optimizer=adadelta)
fit_model.summary()

初步训练模型

  同示例篇,这里我们利用keras.Model的训练接口fit_generator来随机生成数据训练模型,该函数需要传入一个数据生成器,生成器函数代码如下

def generate_data_fixed_length(imgs, labels_encode, batch_size):
    imgs = np.array(imgs) # 图片BGR数据字典{长度:BGR数据数组}
    labels_encode = np.array(labels_encode) # 验证码真实标签{长度:标签数组}
    while True:
        test_idx = np.random.choice(range(len(imgs)), batch_size)
        batch_imgs = imgs[test_idx]
        batch_labels = labels_encode[test_idx]
        yield ([batch_imgs, batch_labels], None) # 元组的第一个元素为输入,第二个元素为训练标签,即自定义loss函数时的y_true

  下面可以开始训练模型了,只是如果直接利用生成器使用所有数据进行训练,模型会一直不收敛,因此必须采取另外一种训练的策略,即先在一个数据量小的训练集中训练模型直到loss下降到一定程度,再扩大训练集的规模,并重复训练的步骤,一直在所有训练数据都有较低的loss为止。为了能够控制模型训练过程中当loss下降到某一阀值就停止,我们还需利用keras的训练接口中的参数callbacks,实现一个自定义停止器1并传参给它,以下是停止器的代码

class StopTraining(keras.callbacks.Callback):
    def __init__(self, thres):
        super(StopTraining, self).__init__()
        self.thres = thres
    def on_epoch_end(self, batch, logs={}):
        if logs.get('loss') < self.thres:
            self.model.stop_training = True

  该停止器初始化时需要一个参数thres,其代表训练过程中当一轮的平均loss小于该值就停止训练。之后我们便可以利用这个停止器来控制训练过程是否提前停止,训练模型的代码如下

import math
import time

for length in imgs.keys():
    for size in range(2, int(math.ceil(math.log10(len(imgs[length])))) + 1):
        sample_size = min(len(imgs[length]), 10 ** size)
        fit_model.fit_generator(
            generate_data_fixed_length(imgs[length][: sample_size], labels_encode[length][: sample_size], 32), 
            epochs=100, 
            steps_per_epoch=100, 
            verbose=1,
            callbacks=[StopTraining(1)])
    time_str = time.strftime("%Y_%m_%d_%H_%M_%S")
    fit_model.save_weights(f'model/fit_model_{time_str}.ckpt')
    model.save_weights(f'model/model_{time_str}.ckpt')

  这里我分别对每一种长度的训练数据集作训练,从长度为4的训练数据集开始,初始训练集定为当前长度训练集的前100个样本,每次训练到平均loss小于1后就停止并将训练集的数量级增加1,直到训练完当前长度的所有训练数据,再使用其他长度的训练数据集进行训练,最后每训练完一种长度的数据集就保存当前模型的参数。这里的训练过程花费了我近13个小时,该数据仅供参考。

测试模型

  训练结束后,该是测试模型的时候了,测试之前,我们同样定义一个测试数据生成器生成测试数据,代码如下

width = 160
height = 60  

def generate_captcha():
    img_generator = ImageCaptcha(width=width, height=height)   
    def generator(char): 
        return np.asarray(img_generator.generate_image(char))
    return generator

def generate_test_data(generate_img, batch_size):
    while True:
        test_labels_batch = []
        test_imgs_batch = []
        length = random.randint(4, 7)
        for _ in range(batch_size):
            char = ''.join([random.choice(chars) for _ in range(length)])
            img = generate_img(char)
            test_labels_batch.append(char)
            test_imgs_batch.append(img)
        yield([np.array(test_imgs_batch), np.array(test_labels_batch)])

  其中generate_captcha函数将返回一个用于生成指定字符验证码样本的函数。而测试数据生成器generate_test_data需要两个参数,其中的generate_img代表生成指定字符验证码的函数,可传入generate_captcha();batch_size代表每批生成多少样本。
  接下来可以开始测试训练好的模型了,训练代码如下

import matplotlib.pyplot as plt
import seaborn as sns

plt.rcParams['font.sans-serif'] = ['SimHei'] 
plt.rcParams['axes.unicode_minus'] = False

char_map = {chars[c]: c for c in range(len(chars))} # 验证码编码(0到len(chars) - 1)
idx_map = {value: key for key, value in char_map.items()} # 编码映射到字符
idx_map[-1] = '' # -1映射到空

def test(generator, test_iter_num=10):
    error_cnt = 0
    iterator = generator
    sample_num = 0
    loss_pred_all = None
    for _ in range(test_iter_num): 
        ### 生成测试数据
        test_imgs_batch, test_labels_batch = next(iterator)
        test_labels_encode_batch = []
        for label in test_labels_batch:
            label = [char_map[i] for i in label]
            test_labels_encode_batch.append(label) 
        
        ### 统计错误样本数
        labels_pred = model.predict_on_batch(np.array(test_imgs_batch))
        labels_pred = char_decode(labels_pred)   
        for label, label_pred in zip(test_labels_batch, labels_pred): 
            if label != label_pred:
                error_cnt += 1
#                 print(f'{label} -> {label_pred}')  

        ### 保存测试数据以便绘制loss的概率密度函数
        loss_pred = fit_model.predict([test_imgs_batch, np.array(test_labels_encode_batch)])
        if loss_pred_all is None:
            loss_pred_all = loss_pred
        else:
            loss_pred_all = np.vstack([loss_pred_all, loss_pred])
        sample_num += len(loss_pred)
          
    ### 绘制loss的概率密度函数
    sns.distplot(loss_pred_all)
    plt.title(f'mean: {loss_pred_all.mean():.3f} | '
              f'max: {loss_pred_all.max():.3f} | '
              f'median: {np.median(loss_pred_all):.3f}\n'
              f'总样本数:{sample_num} | '
              f'错误数:{error_cnt} | '
              f'准确率:{1 - error_cnt / sample_num:.3f}', fontsize = 20)
    plt.xlabel('loss', fontsize = 20)
    plt.ylabel('PDF', fontsize = 20)
    plt.xticks(fontsize=15)
    plt.yticks(fontsize=15)
    plt.show()
test(generate_test_data(generate_captcha(), 32), test_iter_num=100)

  test函数中有两个参数,generator为测试数据生成器,test_iter_num表示生成多少批次测试数据。该函数最后绘制出训练集样本loss的概率分布图(PDF),其能够体现模型的测试效果,如下
初次训练后的模型效果
  通过这张图,我们能够得知测试样本的loss主要分布在0到10之间,小部分样本的loss超过了10,模型的准确率只有0.4。该模型虽然在训练集中能够达到平均loss小于1,但在这里的平均loss却达到了2.765,可见模型在训练集上发生了过拟合现象。

进一步训练模型

  完成了模型的初步训练之后,我们还能更进一步针对某种风格的验证码进行训练,此时我们已不必再生成训练样本保存到本地之后重复使用同一样本集进行训练,而可直接随机生成各种长度的验证码进行训练,最后都能使平均loss下降到1以下。在python上的验证码库并不算多,我了解到的还有wheezy.captcha和claptcha,而java下的有不少验证码生成库,关于验证码库可参考https://github.com/nickliqian/cnn_captcha,有能力的可使用java自行搭建一个tomcat+servlet的简易后台生成验证码,而在python这边利用requests请求获取验证码数据进行训练。
  这里我仍旧使用captcha随机生成训练集并进行训练,训练之前,先定义一个训练集的生成器,代码如下

def generate_data_random(generator, batch_size): 
    while True:
        labels_batch = []
        imgs_batch = []
        length = random.randint(4, 7)
        for _ in range(batch_size):
            char = ''.join([random.choice(chars) for _ in range(length)])
            img = generator(char)
            labels_batch.append(char)
            imgs_batch.append(img)
        labels_encode_batch = []
        for label in labels_batch:
            label = [char_map[i] for i in label]
            labels_encode_batch.append(label)
        yield([np.array(imgs_batch), np.array(labels_encode_batch)], None)

  generate_data_random函数中有两个参数,generator为训练数据生成器,可传入generate_captcha();batch_size代表每批生成多少样本。我们使用该生成器进行训练,代码如下

fit_model.fit_generator(
    generate_data_random(generate_captcha(), 32), 
    epochs=100, 
    steps_per_epoch=100, 
    verbose=1
)
time_str = time.strftime("%Y_%m_%d_%H_%M_%S")
fit_model.save_weights(f'testmodel/fit_model_{time_str}.ckpt')
model.save_weights(f'testmodel/model_{time_str}.ckpt')

  这里模型会使用随机样本训练100轮,但我训练的过程中发现训练到50轮之后模型的平均loss基本在0.55上下波动,没有再下降的迹象,这100轮的训练总共花了我近6小时。再次利用test测试模型,最终训练效果如下图
再次训练后的模型效果
  通过随机样本的训练,大部分样本的loss已经下降到2以下,只有一小部分样本的loss超过了2,且模型的准确率达到了0.846。

结语

  以上模型可通过训练其他风格验证码数据得到一个能适应多种风格验证码的较为通用的模型,只是要想使模型具有高可用性或许还得在训练过程或在网络结构上再下功夫。


2020.01.13更新: 新增了部分内容,修改StopTraining使得能够自定义停止阀值。


  1. 参考自https://codeday.me/bug/20180605/176510.html ↩︎

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

CRNN+CTC实现不定长验证码识别(keras模型-训练篇) 的相关文章

随机推荐

  • springmvc 使用JSR-303进行数据校验

    项目中 通常使用较多的是前端的校验 比如页面中js校验以及form表单使用bootstrap校验 然而对于安全要求较高点建议在服务端进行校验 服务端校验 控制层controller 校验页面请求的参数的合法性 在服务端控制层controll
  • php+中defined,phpdefined()函数的用法总结

    The define function defines a constant define 函数的作用是 定义一个常量 Constants are much like variables except for the following d
  • AMS磁编码器:AS5048与AS5047区别,伺服电机闭环系统位置反馈

    本文只简介 AMS的磁编码器 其他厂商磁编IC见另一篇 https blog csdn net Mark md article details 100181701 新增补了一篇 详细介绍了GMR磁编码器原理 同类对比优劣 安装方式 设计注意
  • Excel中万能的查询函数——VLOOKUP(使用方法+实操)

    Vlookup函数 基础用法并不难 尤其遇到需要查询匹配的问题 简直不要太好用 但想要掌握全部用法还是需要时间的 对vlookup用法做了超强汇总表 检索 vlookup函数用法大全https link zhihu com target h
  • java中定时任务 schedule 分布式下没有锁住 时间不同步 执行滞后 相对时间 系统时间 spring springboot

    java util Timer计时器可以进行 管理任务延迟执行 如1000ms后执行任务 及周期性执行 如每500ms执行一次该任务 但是 Timer存在一些缺陷 应考虑使用ScheduledThreadPoolExecutor代替 Tim
  • 每一个C++开发者都应该知道的线上工具

    要想代码写得丝滑 怎么可以不熟练各种开发工具呢 锤子用的好 烦恼会减少 这里推荐几个C 开发中用于编译 构建 调试和性能分析的线上工具 最初的资料来源于Lightning Talk Online Tools Every C Develope
  • docker 安装完全分布式Hadoop集群

    一 搭建原因 鉴于本人机器性能较低 在机器上运行多个Linux虚拟机比较吃力 如果再在其上运行分布式计算环境 想必更加吃力 我想这也是很多同学的普遍问题 通过百度 我了解到了docker技术 网上有很多docker搭建Hadoop的教程 总
  • 微信小程序登陆账号验证隐私协议验证页面及代码

    微信小程序登陆页 页面主要是需要校验账号手机号 验证勾选同意使用协议和隐私政策 效果如下 1 账号密码部分页面代码
  • 移动Web开发入门(一) -- 像素、媒体查询、em、rem

    文章目录 一 移动Web开发 二 基本概念 分辨率 物理像素 CSS像素 物理像素和CSS像素的关系 设备像素比 dpr 获取dpr PPI DPI 视口 viewport 设置 Viewport 获取视口宽度 三 媒体查询 媒体类型 媒体
  • Java中Logger类应用

    类 Logger java lang Object java util logging LoggerLogger 对象用来记录特定系统或应用程序组件的日志消息 一般使用圆点分隔的层次名称空间来命名 Logger Logger 名称可以是任意
  • 最小费用最大流问题与算法实现(Bellman-Ford、SPFA、Dijkstra)

    摘要 今日 对最小费用最大流问题进行了一个简单的研究 并针对网上的一些已有算法进行了查找和研究 博客和资料很多 但是留下的算法很多运行失败 出错 或者意义不明 这里 个人对其中的Bellman Ford SPFA 改进的Dijkstra三种
  • CompletableFuture线程池执行多个任务进行链式、组合等助理使用

    2 1 CompletableFuture简介 使用线程池执行任务没法直接对多个任务进行链式 组合等处理 或者说实现起来比较麻烦需要借助并发工具类才能完成 CompletableFuture实现了对任务编排的能力 借助这项能力 可以轻松地组
  • Pycharm 出现报错:Failed building wheel for XXX

    报错原因 是因为Python解释器的版本太高 与Pycharm版本不符 解决办法 安装一个比pycharm低一个版本的python解释器 比如pycharm3 10 那Python就3 9或以下版本 如果答案 您满意 请采纳意见和点赞关注
  • 【操作系统原理】01-操作系统概览

    一 What Why 操作系统是管理计算机硬件和软件资源的计算机程序 管理配置内存 决定资源供需顺序 控制输入输出设备等 操作系统提供让用户和系统交互的操作界面 从手机到超级计算机 操作系统可简单也可复杂 操作系统的种类是多种多样的 不局限
  • SVN服务器搭建和使用(一)

    Subversion是优秀的版本控制工具 其具体的的优点和详细介绍 这里就不再多说 首先来下载和搭建SVN服务器 现在Subversion已经迁移到apache网站上了 下载地址 http subversion apache org pac
  • C#复制构造函数学习

    通过从另一个对象复制变量或将一个对象的数据复制到另一个对象来创建对象的构造函数称为复制构造函数 复制构造函数是一个参数化构造函数 包含相同类类型的参数 它的主要用途是将新实例初始化为现有实例的值 using System namespace
  • 电脑提示MSVCP140.dll文件丢失的解决方法

    打开软件或者游戏出现运行出现报错 提示 由于找不到 MSVCP140 dll 无法继续执行代码 重新安装程序可能会解决此问题 这一般是什么原因导致了这个问题 我们要如何解决 下面小编分享一下由于找不到MSVCP140 dll无法继续执行代码
  • linux卸载zookeeper_Zookeeper学习

    zookeeper 是一个分布式协调服务的开源框架 主要用来解决分布式集群中应用系统的一致性问题 本质上是一个分布式的小文件存储系统 提供基于类似文件系统的目录树方式的数据存储 并且可以对树中的节点进行有效管理 从而用来维护和监控你存储的数
  • nosql 之认识篇

    使用sql server n年后 发现mysql这个开源数据库也很好用 于是投身到这个行列中 最近开发个sns类型的网站 随着用户的增多 感觉数据库所承受的压力成为了整个网站继续发展的瓶颈 为了更好的解决此问题 发现twitter face
  • CRNN+CTC实现不定长验证码识别(keras模型-训练篇)

    目录 前言 运行环境 生成数据集 构建网络模型 初步训练模型 测试模型 进一步训练模型 结语 前言 本文为CRNN CTC实现不定长验证码识别 keras模型 示例篇 的续篇 示例篇中使用的字符仅为数字 本文将训练集拓展到包含数字字母在内的