一次基於Tensorflow+CNN的驗證碼識別之旅
對於本次基於卷積神經網路識別驗證碼有著非常大的興趣,所以嘗試性地去做了測試,過程當中踩了不少坑,也參考了許多前輩的部落格和教程,最終識別率可達到98.25%
一、下圖是訓練的過程:
二、實驗的情況簡介:
實驗環境: Python3.6
、 Centos 7.3
、 Tensorflow 1.9
訓練的過程是放到遠端伺服器上跑的,1H1G的配置,沒有GPU,所以訓練總耗時5小時
在本地機器 Win10+MX150+Python3.6+Tensorflow1.9
環境下, CUDA
總是找不到 xxx90.dll
,按照網上的大佬部落格記錄來還是無法解決,遂放棄而轉移到伺服器上慢慢跑
驗證碼的來源是 Python
中一個用於生成驗證碼的captcha庫,其生成的驗證碼 UCkV
如下:
Line"/>
本次實驗中為了驗證訓練效果,從速度上考慮,只做了數字驗證碼的識別
三、實驗過程:
需要驗證碼,首先我想到自己用PHP簡單寫了一個驗證碼的生成程式,也是為了驗證模型的準確率,發現如果讀檔案的形式效率不是很高,故直接採用在python下的captcha庫來生成有一定複雜度的驗證碼。
如下是生成驗證碼的程式,可生成數字+字母大小寫的任意長度驗證碼
# 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陣列 """ 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) # 轉化為np陣列 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)
然後編寫一個工具庫,用於呼叫驗證碼生成程式來生成訓練集
# -*- 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): """ 圖片轉為黑白,3維轉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): """ 驗證碼文字轉為向量 :param text: :param captcha_len: :param captcha_list: :return: vector 文字對應的向量形式 """ 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# 找到字元對應在字元列表中的下標值+字元列表長度*i 的 一維向量 賦值為 1 return vector def vec2text(vec, captcha_list=CAPTCHA_LIST, captcha_len=CAPTCHA_LEN): """ 驗證碼向量轉為文字 :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):# 生成對應的訓練集 text, image = wrap_gen_captcha_text_and_image() image = convert2gray(image)# 轉灰度numpy # 將圖片陣列一維化 同時將文字也對應在兩個二維組的同一行 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): """ 初始化權值 :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,取出區域內最大值為代表特徵, 2x2 的pool,圖片尺寸變為1/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): """ 三層卷積神經網路 :param x:訓練集 image x :param keep_prob:神經元利用率 :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啟用函式 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 經過上面操作後得到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個神經元 全連線層有1024個神經元 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): """ 優化計算圖 :param y: 正確值 :param y_conv:預測值 :return: optimizer """ # 交叉熵代價函式計算loss 注意logits輸入是在函式內部進行sigmod操作 # sigmod_cross適用於每個類別相互獨立但不互斥,如圖中可以有字母和數字 # softmax_cross適用於每個類別獨立且排斥的情況,如數字和字母不可以同時出現 loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=y_conv)) # 最小化loss優化 AdaminOptimizer優化 optimizer = tf.train.AdamOptimizer(1e-3).minimize(loss) return optimizer def accuracy_graph(y, y_conv, width=len(CAPTCHA_LIST), height=CAPTCHA_LEN): """ 偏差計算圖,正確值和預測值,計算準確度 :param y: 正確值 標籤 :param y_conv:預測值 :param width:驗證碼預備字元列表長度 :param height:驗證碼的大小,預設為4 :return:正確率 """ # 這裡區分了大小寫 實際上驗證碼一般不區分大小寫,有四個值,不同於手寫體識別 # 預測值 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:驗證碼預備字元列表長度*驗證碼長度(預設為4) :return: """ # cnn在影象大小是2的倍數時效能最高, 如果影象大小不是2的倍數,可以在影象邊緣補無用畫素 # 在影象上補2行,下補3行,左補2行,右補2行 # np.pad(image,((2,3),(2,2)), 'constant', constant_values=(255,)) acc_rate = 0.95# 預設模型準確率標準 # 按照圖片大小申請佔位符 x = tf.placeholder(tf.float32, [None, height * width]) y = tf.placeholder(tf.float32, [None, y_size]) # 防止過擬合 訓練時啟用 測試時不啟用 神經元使用率 keep_prob = tf.placeholder(tf.float32) # cnn模型 y_conv = cnn_graph(x, keep_prob, (height, width)) # 優化 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# 步數 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()
訓練程式將準確率超過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): """ 驗證碼圖片轉化為文字 :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, ' 模型預測值:', pre_text) img.show()
在測試幾次中均100%預測正確:
如上 32
黏在一起是不好分辨的,但是訓練出來的模型效果還不錯!
四、踩坑之痛:
1.驗證碼生成之痛:
這部分就很痛了,生成驗證碼首先去研究了一下PHP驗證碼的生成,還是貼上一下程式碼吧:
<!-- code.php--> <?php header("Content-type: image/PNG"); //生成驗證碼,用於樣本 function getCode($num,$w,$h,$code) { //建立圖片,定義顏色值 $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); }*/ //將數字隨機顯示在畫布上,字元的水平間距和位置都按一定波動範圍隨機生成 $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);//釋放圖片所佔記憶體 } $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了,而且經過灰度處理,降噪,識別正確率90%左右
2. CUDA 安裝之痛
CPU來跑訓練模型確實太慢了,想要使用GPU來加速,遂選擇安裝CUDA
反反覆覆,下載了 cuda9.0
9.1
10.0
三個版本,每個基本在 1.5G
大小,反反覆覆地安裝,最終選擇放棄, CSDN
上的那些 只複製轉載卻標原創
的文章能不能少點(誤人子弟),555~
cuda
對應著不同的 tensorflow
版本,和 cuDNN
,都需要反反覆覆解除安裝重灌!
下次去網咖跑一下吧,感覺網咖用的顯示卡效能不錯!
推薦一篇不錯的文章: ofollow,noindex">《windows10下安裝Tensorflow-GPU跑深度學習(Nvidia-MX150)》
五、總結:
這次對於驗證碼識別技術的探索過程,歷時5天,受益匪淺。機器學習真是個好東西,後續會深入研究,好好學習,天天向上!