一次基于Tensorflow+CNN的驗證碼識別之旅

對于本次基于卷積神經(jīng)網(wǎng)絡(luò)識別驗證碼有著非常大的興趣,所以嘗試性地去做了測試,過程當中踩了不少坑,也參考了許多前輩的博客和教程,最終識別率可達到98.25%

一、下圖是訓練的過程:


二、實驗的情況簡介:

實驗環(huán)境:Python3.6、Centos 7.3、Tensorflow 1.9

訓練的過程是放到遠程服務(wù)器上跑的,1H1G的配置,沒有GPU,所以訓練總耗時5小時

在本地機器Win10+MX150+Python3.6+Tensorflow1.9環(huán)境下,CUDA總是找不到xxx90.dll,按照網(wǎng)上的大佬博客記錄來還是無法解決,遂放棄而轉(zhuǎn)移到服務(wù)器上慢慢跑

驗證碼的來源是 Python中一個用于生成驗證碼的captcha庫,其生成的驗證碼UCkV如下:

本次實驗中為了驗證訓練效果,從速度上考慮,只做了數(shù)字驗證碼的識別


三、實驗過程:

需要驗證碼,首先我想到自己用PHP簡單寫了一個驗證碼的生成程序,也是為了驗證模型的準確率,發(fā)現(xiàn)如果讀文件的形式效率不是很高,故直接采用在python下的captcha庫來生成有一定復雜度的驗證碼。

如下是生成驗證碼的程序,可生成數(shù)字+字母大小寫的任意長度驗證碼

# coding:utf-8
# name:captcha_gen.py

import random
import numpy as np
from PIL import Image
from captcha.image import ImageCaptcha


NUMBER = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
LOW_CASE = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
            'v', 'w', 'x', 'y', 'z']
UP_CASE = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
           'V', 'W', 'X', 'Y', 'Z']

CAPTCHA_LIST = NUMBER
CAPTCHA_LEN = 4         # 驗證碼長度
CAPTCHA_HEIGHT = 60     # 驗證碼高度
CAPTCHA_WIDTH = 160     # 驗證碼寬度


def random_captcha_text(char_set=CAPTCHA_LIST, captcha_size=CAPTCHA_LEN):
    """
    隨機生成定長字符串
    :param char_set: 備選字符串列表
    :param captcha_size: 字符串長度
    :return: 字符串
    """
    captcha_text = [random.choice(char_set) for _ in range(captcha_size)]
    return ''.join(captcha_text)


def gen_captcha_text_and_image(width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT, save=None):
    """
    生成隨機驗證碼
    :param width: 驗證碼圖片寬度
    :param height: 驗證碼圖片高度
    :param save: 是否保存(None)
    :return: 驗證碼字符串,驗證碼圖像np數(shù)組
    """
    image = ImageCaptcha(width=width, height=height)
    # 驗證碼文本
    captcha_text = random_captcha_text()
    captcha = image.generate(captcha_text)
    # 保存
    if save:
        image.write(captcha_text, './img/' + captcha_text + '.jpg')
    captcha_image = Image.open(captcha)
    # 轉(zhuǎn)化為np數(shù)組
    captcha_image = np.array(captcha_image)
    return captcha_text, captcha_image


if __name__ == '__main__':
    t, im = gen_captcha_text_and_image(save=True)
    print(t, im.shape)      # (60, 160, 3)

然后編寫一個工具庫,用于調(diào)用驗證碼生成程序來生成訓練集

# -*- coding:utf-8 -*-
# name: util.py

import numpy as np
from captcha_gen import gen_captcha_text_and_image
from captcha_gen import CAPTCHA_LIST, CAPTCHA_LEN, CAPTCHA_HEIGHT, CAPTCHA_WIDTH


def convert2gray(img):
    """
    圖片轉(zhuǎn)為黑白,3維轉(zhuǎn)1維
    :param img: np
    :return:  灰度圖的np
    """
    if len(img.shape) > 2:
        img = np.mean(img, -1)
    return img


def text2vec(text, captcha_len=CAPTCHA_LEN, captcha_list=CAPTCHA_LIST):
    """
    驗證碼文本轉(zhuǎn)為向量
    :param text:
    :param captcha_len:
    :param captcha_list:
    :return: vector 文本對應(yīng)的向量形式
    """
    text_len = len(text)    # 欲生成驗證碼的字符長度
    if text_len > captcha_len:
        raise ValueError('驗證碼最長4個字符')
    vector = np.zeros(captcha_len * len(captcha_list))      # 生成一個一維向量 驗證碼長度*字符列表長度
    for i in range(text_len):
        vector[captcha_list.index(text[i])+i*len(captcha_list)] = 1     # 找到字符對應(yīng)在字符列表中的下標值+字符列表長度*i 的 一維向量 賦值為 1
    return vector


def vec2text(vec, captcha_list=CAPTCHA_LIST, captcha_len=CAPTCHA_LEN):
    """
    驗證碼向量轉(zhuǎn)為文本
    :param vec:
    :param captcha_list:
    :param captcha_len:
    :return: 向量的字符串形式
    """
    vec_idx = vec
    text_list = [captcha_list[int(v)] for v in vec_idx]
    return ''.join(text_list)


def wrap_gen_captcha_text_and_image(shape=(60, 160, 3)):
    """
    返回特定shape圖片
    :param shape:
    :return:
    """
    while True:
        t, im = gen_captcha_text_and_image()
        if im.shape == shape:
            return t, im


def get_next_batch(batch_count=60, width=CAPTCHA_WIDTH, height=CAPTCHA_HEIGHT):
    """
    獲取訓練圖片組
    :param batch_count: default 60
    :param width: 驗證碼寬度
    :param height: 驗證碼高度
    :return: batch_x, batch_yc
    """
    batch_x = np.zeros([batch_count, width * height])
    batch_y = np.zeros([batch_count, CAPTCHA_LEN * len(CAPTCHA_LIST)])
    for i in range(batch_count):    # 生成對應(yīng)的訓練集
        text, image = wrap_gen_captcha_text_and_image()
        image = convert2gray(image)     # 轉(zhuǎn)灰度numpy
        # 將圖片數(shù)組一維化 同時將文本也對應(yīng)在兩個二維組的同一行
        batch_x[i, :] = image.flatten() / 255
        batch_y[i, :] = text2vec(text)  # 驗證碼文本的向量形式
    # 返回該訓練批次
    return batch_x, batch_y


if __name__ == '__main__':
    x, y = get_next_batch(batch_count=1)    # 默認為1用于測試集
    print(x, y)

然后編輯訓練程序

# -*- coding:utf-8 -*-
# name: model_train.py

import tensorflow as tf
from datetime import datetime
from util import get_next_batch
from captcha_gen import CAPTCHA_HEIGHT, CAPTCHA_WIDTH, CAPTCHA_LEN, CAPTCHA_LIST


def weight_variable(shape, w_alpha=0.01):
    """
    初始化權(quán)值
    :param shape:
    :param w_alpha:
    :return:
   """
    initial = w_alpha * tf.random_normal(shape)
    return tf.Variable(initial)


def bias_variable(shape, b_alpha=0.1):
    """
    初始化偏置項
    :param shape:
    :param b_alpha:
    :return:
    """
    initial = b_alpha * tf.random_normal(shape)
    return tf.Variable(initial)


def conv2d(x, w):
    """
    卷基層 :局部變量線性組合,步長為1,模式‘SAME’代表卷積后圖片尺寸不變,即零邊距
    :param x:
    :param w:
    :return:
    """
    return tf.nn.conv2d(x, w, strides=[1, 1, 1, 1], padding='SAME')


def max_pool_2x2(x):
    """
    池化層:max pooling,取出區(qū)域內(nèi)最大值為代表特征, 2x2 的pool,圖片尺寸變?yōu)?/2
    :param x:
    :return:
    """
    return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')


def cnn_graph(x, keep_prob, size, captcha_list=CAPTCHA_LIST, captcha_len=CAPTCHA_LEN):
    """
    三層卷積神經(jīng)網(wǎng)絡(luò)
    :param x:   訓練集 image x
    :param keep_prob:   神經(jīng)元利用率
    :param size:        大小 (高,寬)
    :param captcha_list:
    :param captcha_len:
    :return: y_conv
    """
    # 需要將圖片reshape為4維向量
    image_height, image_width = size
    x_image = tf.reshape(x, shape=[-1, image_height, image_width, 1])

    # 第一層
    # filter定義為3x3x1, 輸出32個特征, 即32個filter
    w_conv1 = weight_variable([3, 3, 1, 32])    # 3*3的采樣窗口,32個(通道)卷積核從1個平面抽取特征得到32個特征平面
    b_conv1 = bias_variable([32])
    h_conv1 = tf.nn.relu(conv2d(x_image, w_conv1) + b_conv1)    # rulu激活函數(shù)
    h_pool1 = max_pool_2x2(h_conv1)     # 池化
    h_drop1 = tf.nn.dropout(h_pool1, keep_prob)      # dropout防止過擬合

    # 第二層
    w_conv2 = weight_variable([3, 3, 32, 64])
    b_conv2 = bias_variable([64])
    h_conv2 = tf.nn.relu(conv2d(h_drop1, w_conv2) + b_conv2)
    h_pool2 = max_pool_2x2(h_conv2)
    h_drop2 = tf.nn.dropout(h_pool2, keep_prob)

    # 第三層
    w_conv3 = weight_variable([3, 3, 64, 64])
    b_conv3 = bias_variable([64])
    h_conv3 = tf.nn.relu(conv2d(h_drop2, w_conv3) + b_conv3)
    h_pool3 = max_pool_2x2(h_conv3)
    h_drop3 = tf.nn.dropout(h_pool3, keep_prob)

    """
    原始:60*160圖片 第一次卷積后 60*160 第一池化后 30*80
    第二次卷積后 30*80 ,第二次池化后 15*40
    第三次卷積后 15*40 ,第三次池化后 7.5*20 = > 向下取整 7*20
    經(jīng)過上面操作后得到7*20的平面
    """

    # 全連接層
    image_height = int(h_drop3.shape[1])
    image_width = int(h_drop3.shape[2])
    w_fc = weight_variable([image_height*image_width*64, 1024])     # 上一層有64個神經(jīng)元 全連接層有1024個神經(jīng)元
    b_fc = bias_variable([1024])
    h_drop3_re = tf.reshape(h_drop3, [-1, image_height*image_width*64])
    h_fc = tf.nn.relu(tf.matmul(h_drop3_re, w_fc) + b_fc)
    h_drop_fc = tf.nn.dropout(h_fc, keep_prob)

    # 輸出層
    w_out = weight_variable([1024, len(captcha_list)*captcha_len])
    b_out = bias_variable([len(captcha_list)*captcha_len])
    y_conv = tf.matmul(h_drop_fc, w_out) + b_out
    return y_conv


def optimize_graph(y, y_conv):
    """
    優(yōu)化計算圖
    :param y: 正確值
    :param y_conv:  預(yù)測值
    :return: optimizer
    """
    # 交叉熵代價函數(shù)計算loss 注意logits輸入是在函數(shù)內(nèi)部進行sigmod操作
    # sigmod_cross適用于每個類別相互獨立但不互斥,如圖中可以有字母和數(shù)字
    # softmax_cross適用于每個類別獨立且排斥的情況,如數(shù)字和字母不可以同時出現(xiàn)
    loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=y_conv))
    # 最小化loss優(yōu)化 AdaminOptimizer優(yōu)化
    optimizer = tf.train.AdamOptimizer(1e-3).minimize(loss)
    return optimizer


def accuracy_graph(y, y_conv, width=len(CAPTCHA_LIST), height=CAPTCHA_LEN):
    """
    偏差計算圖,正確值和預(yù)測值,計算準確度
    :param y: 正確值 標簽
    :param y_conv:  預(yù)測值
    :param width:   驗證碼預(yù)備字符列表長度
    :param height:  驗證碼的大小,默認為4
    :return:    正確率
    """
    # 這里區(qū)分了大小寫 實際上驗證碼一般不區(qū)分大小寫,有四個值,不同于手寫體識別
    # 預(yù)測值
    predict = tf.reshape(y_conv, [-1, height, width])   #
    max_predict_idx = tf.argmax(predict, 2)
    # 標簽
    label = tf.reshape(y, [-1, height, width])
    max_label_idx = tf.argmax(label, 2)
    correct_p = tf.equal(max_predict_idx, max_label_idx)    # 判斷是否相等
    accuracy = tf.reduce_mean(tf.cast(correct_p, tf.float32))
    return accuracy


def train(height=CAPTCHA_HEIGHT, width=CAPTCHA_WIDTH, y_size=len(CAPTCHA_LIST)*CAPTCHA_LEN):
    """
    cnn訓練
    :param height: 驗證碼高度
    :param width:   驗證碼寬度
    :param y_size:  驗證碼預(yù)備字符列表長度*驗證碼長度(默認為4)
    :return:
    """
    # cnn在圖像大小是2的倍數(shù)時性能最高, 如果圖像大小不是2的倍數(shù),可以在圖像邊緣補無用像素
    # 在圖像上補2行,下補3行,左補2行,右補2行
    # np.pad(image,((2,3),(2,2)), 'constant', constant_values=(255,))

    acc_rate = 0.95     # 預(yù)設(shè)模型準確率標準
    # 按照圖片大小申請占位符
    x = tf.placeholder(tf.float32, [None, height * width])
    y = tf.placeholder(tf.float32, [None, y_size])
    # 防止過擬合 訓練時啟用 測試時不啟用 神經(jīng)元使用率
    keep_prob = tf.placeholder(tf.float32)
    # cnn模型
    y_conv = cnn_graph(x, keep_prob, (height, width))
    # 優(yōu)化
    optimizer = optimize_graph(y, y_conv)
    # 計算準確率
    accuracy = accuracy_graph(y, y_conv)
    # 啟動會話.開始訓練
    saver = tf.train.Saver()
    sess = tf.Session()
    sess.run(tf.global_variables_initializer())     # 初始化
    step = 0    # 步數(shù)
    while 1:
        batch_x, batch_y = get_next_batch(64)
        sess.run(optimizer, feed_dict={x: batch_x, y: batch_y, keep_prob: 0.75})
        # 每訓練一百次測試一次
        if step % 100 == 0:
            batch_x_test, batch_y_test = get_next_batch(100)
            acc = sess.run(accuracy, feed_dict={x: batch_x_test, y: batch_y_test, keep_prob: 1.0})
            print(datetime.now().strftime('%c'), ' step:', step, ' accuracy:', acc)
            # 準確率滿足要求,保存模型
            if acc > acc_rate:
                model_path = "./model/captcha.model"
                saver.save(sess, model_path, global_step=step)
                acc_rate += 0.01
                if acc_rate > 0.99:     # 準確率達到99%則退出
                    break
        step += 1
    sess.close()


if __name__ == '__main__':
    train()

訓練程序?qū)蚀_率超過0.95的模型保存到./model/文件夾下

測試模型效果:

# -*- coding:utf-8 -*-
# name: model_test.py

import tensorflow as tf
from model_train import cnn_graph
from captcha_gen import gen_captcha_text_and_image
from util import vec2text, convert2gray
from util import CAPTCHA_LIST, CAPTCHA_WIDTH, CAPTCHA_HEIGHT, CAPTCHA_LEN
from PIL import Image


def captcha2text(image_list, height=CAPTCHA_HEIGHT, width=CAPTCHA_WIDTH):
    """
    驗證碼圖片轉(zhuǎn)化為文本
    :param image_list:
    :param height:
    :param width:
    :return:
    """
    x = tf.placeholder(tf.float32, [None, height * width])
    keep_prob = tf.placeholder(tf.float32)
    y_conv = cnn_graph(x, keep_prob, (height, width))
    saver = tf.train.Saver()
    with tf.Session() as sess:
        saver.restore(sess, tf.train.latest_checkpoint('model/'))
        predict = tf.argmax(tf.reshape(y_conv, [-1, CAPTCHA_LEN, len(CAPTCHA_LIST)]), 2)
        vector_list = sess.run(predict, feed_dict={x: image_list, keep_prob: 1})
        vector_list = vector_list.tolist()
        text_list = [vec2text(vector) for vector in vector_list]
        return text_list


if __name__ == '__main__':
    text, image = gen_captcha_text_and_image()
    img = Image.fromarray(image)
    image = convert2gray(image)
    image = image.flatten() / 255
    pre_text = captcha2text([image])
    print("驗證碼正確值:", text, ' 模型預(yù)測值:', pre_text)
    img.show()

在測試幾次中均100%預(yù)測正確:

如上 32 黏在一起是不好分辨的,但是訓練出來的模型效果還不錯!


四、踩坑之痛:

1.驗證碼生成之痛:

這部分就很痛了,生成驗證碼首先去研究了一下PHP驗證碼的生成,還是粘貼一下代碼吧:

<!-- code.php  -->

<?php
header("Content-type: image/PNG");


//生成驗證碼,用于樣本
function getCode($num,$w,$h,$code) {

  //創(chuàng)建圖片,定義顏色值
  $im = imagecreate($w, $h);
  $red = imagecolorallocate($im, 255, 0, 0);
  $black = imagecolorallocate($im, 0, 0, 0);
  $bgcolor = imagecolorallocate($im, 255, 255, 255);
  //填充背景
  imagefill($im, 0, 0, $bgcolor);

  //在畫布上隨機生成大量紅點,起干擾作用;
  /*for ($i = 0; $i < 80; $i++) {
    imagesetpixel($im, rand(0, $w), rand(0, $h), $red);
  }*/
  //將數(shù)字隨機顯示在畫布上,字符的水平間距和位置都按一定波動范圍隨機生成
  $strx = rand(3, 8);
  for ($i = 0; $i < $num; $i++) {
    $strpos = rand(1, 6);
    imagestring($im, 8, $strx, $strpos, substr($code, $i, 1), $black);
    $strx += rand(10, 12);
  }
  imagepng($im,'img/'.$code.'.png');//輸出圖片
  imagedestroy($im);//釋放圖片所占內(nèi)存
}

$count = 0;
$code_num = 1000;
while($count < 3000){
  getCode(4,50,22,$code_num);
  $count += 1;
  $code_num += 1;
}

exit(0);
?>

運行后訪問./img/ 目錄:

為了簡單所以生成驗證碼比較簡單,就感覺沒什么學習的價值,到文件讀取驗證碼效率還是很低的

如上圖生成的驗證碼可以直接利用 Tesseract OCR 識別OK了,而且經(jīng)過灰度處理,降噪,識別正確率90%左右


2. CUDA 安裝之痛

CPU來跑訓練模型確實太慢了,想要使用GPU來加速,遂選擇安裝CUDA

反反復復,下載了cuda9.0 9.1 10.0 三個版本,每個基本在 1.5G 大小,反反復復地安裝,最終選擇放棄,CSDN 上的那些 只復制轉(zhuǎn)載卻標原創(chuàng) 的文章能不能少點(誤人子弟),555~

cuda 對應(yīng)著不同的 tensorflow 版本,和 cuDNN,都需要反反復復卸載重裝!

下次去網(wǎng)吧跑一下吧,感覺網(wǎng)吧用的顯卡性能不錯!

推薦一篇不錯的文章:《windows10下安裝Tensorflow-GPU跑深度學習(Nvidia-MX150)》


五、總結(jié):

這次對于驗證碼識別技術(shù)的探索過程,歷時5天,受益匪淺。機器學習真是個好東西,后續(xù)會深入研究,好好學習,天天向上!


六、文章參考:


七、附件下載

Github: https://github.com/dyboy2017/captcha_check
博客地址:https://blog.dyboy.cn/program/100.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容