Tensorflow實戰:Discuz驗證碼識別
作者:崔家華
編輯:黃俊嘉
寫在最前面
驗證碼是根據隨機字元生成一幅圖片,然後在圖片中加入干擾象素,使用者必須手動填入,防止有人利用機器人自動批量註冊、灌水、發垃圾廣告等等 。驗證碼的作用是驗證使用者是真人還是機器人。本文將使用深度學習框架Tensorflow訓練出一個用於破解Discuz驗證碼的模型。
背景介紹
- 我們先看下簡單的Discuz驗證碼
開啟下面的連線,你就可以看到這個驗證碼了。 http://cuijiahua.com/tutrial/discuz/index.php?label=jack
- 觀察上述連結,你會發現label後面跟著的就是要顯示的圖片字母,改變label後面的值,我們就可以獲得不同的Discuz驗證碼圖片。如果會網路爬蟲,我想根據這個api獲取Discuz驗證碼圖片對你來說應該很Easy。不會網路爬蟲也沒有關係,爬蟲程式碼我已經為你準備好了。建立一個get_discuz.py檔案,新增如下程式碼:
#-*- coding:utf-8 -*- from urllib.request import urlretrieve import time, random, os class Discuz(): def __init__(self): # Discuz驗證碼生成圖片地址 self.url = 'http://cuijiahua.com/tutrial/discuz/index.php?label=' def random_captcha_text(self, captcha_size = 4): """ 驗證碼一般都無視大小寫;驗證碼長度4個字元 Parameters: captcha_size:驗證碼長度 Returns: captcha_text:驗證碼字串 """ number = ['0','1','2','3','4','5','6','7','8','9'] alphabet = ['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'] char_set = number + alphabet captcha_text = [] for i in range(captcha_size): c = random.choice(char_set) captcha_text.append(c) captcha_text = ''.join(captcha_text) return captcha_text def download_discuz(self, nums = 5000): """ 下載驗證碼圖片 Parameters: nums:下載的驗證碼圖片數量 """ dirname = './Discuz' if dirname not in os.listdir(): os.mkdir(dirname) for i in range(nums): label = self.random_captcha_text() print('第%d張圖片:%s下載' % (i + 1,label)) urlretrieve(url = self.url + label, filename = dirname + '/' + label + '.jpg') # 請至少加200ms延時,避免給我的伺服器造成過多的壓力,如發現影響伺服器正常工作,我會關閉此功能。 # 你好我也好,大家好才是真的好! time.sleep(0.2) print('恭喜圖片下載完成!') if __name__ == '__main__': dz = Discuz() dz.download_discuz()
- 執行上述程式碼,你就可以下載5000張Discuz驗證碼圖片到本地,但是要注意的一點是:請至少加200ms延時,避免給我的伺服器造成過多的壓力,如發現影響伺服器正常工作,我會關閉此功能。
- 你好我也好,大家好才是真的好!
- 驗證碼下載過程如下圖所示:
當然,如果你想省略麻煩的下載步驟也是可以的,我已經為大家準備好了6萬張的Discuz驗證碼圖片。我想應該夠用了吧,如果感覺不夠用,可以自行使用爬蟲程式下載更多的驗證碼。6萬張的Discuz驗證碼圖片可到
http://cuijiahua.com/wp-content/themes/begin/down.php?id=2772下載。準備好的資料集,它們都是100*30大小的圖片:
- 什麼?你說這個圖片識別太簡單?沒關係,有高難度的! 我開啟的圖片如下所示:
- 這是一個動圖,並且還帶傾斜、扭曲等特效。怎麼通過api獲得這種圖片呢?
http://cuijiahua.com/tutrial/discuz/index.php?label=jack&width=100&height=30&background=1&adulterate=1&ttf=1&angle=1&warping=1&scatter=1&color=1&size=1&shadow=1&animator=1
- 沒錯,只要新增一些引數就可以了,格式如上圖所示,每個引數的說明如下:
- - label:驗證碼
- - width:驗證碼寬度
- - height:驗證碼高度
- - background:是否隨機圖片背景
- - adulterate:是否隨機背景圖形
- - ttf:是否隨機使用ttf字型
- - angle:是否隨機傾斜度
- - warping:是否隨機扭曲
- - scatter:是否圖片打散
- - color:是否隨機顏色
- - size:是否隨機大小
- - shadow:是否文字陰影
- - animator:是否GIF動畫
- 你可以根據你的喜好,定製你想要的驗證碼圖片。
- 不過,為了簡單起見,我們只使用最簡單的驗證碼圖片進行驗證碼識別。資料集已經準備好,那麼接下來進入本文的重點,Tensorflow實戰。
Discuz驗證碼識別
我們已經將驗證碼下載好,並且檔名就是對應圖片的標籤。這裡需要注意的是:我們忽略了圖片中英文的大小寫。
1.資料預處理
首先,資料預處理分為兩個部分,第一部分是讀取圖片,並劃分訓練集和測試集。因為整個資料集為6W張圖片,所以我們可以讓訓練集為5W張,測試集為1W張。隨後,雖然標籤是檔名,我們認識,但是機器是不認識的,因此我們要使用text2vec,將標籤進行向量化。明確了目的,那開始實踐吧!
- 讀取資料: 我們通過定義rate,來確定劃分比例。例如:測試集1W張,訓練集5W張,那麼rate=1W/5W=0.2。
def get_imgs(rate = 0.2):
"""
獲取圖片,並劃分訓練集和測試集
Parameters:
rate:測試集和訓練集的比例,即測試集個數/訓練集個數
Returns:
test_imgs:測試集
test_labels:測試集標籤
train_imgs:訓練集
test_labels:訓練集標籤
"""
data_path = './Discuz'
# 讀取圖片
imgs = os.listdir(data_path)
# 打亂圖片順序
random.shuffle(imgs)
# 資料集總共個數
imgs_num = len(imgs)
# 按照比例求出測試集個數
test_num = int(imgs_num * rate / (1 + rate))
# 測試集
test_imgs = imgs[:test_num]
# 根據檔名獲取測試集標籤
test_labels = list(map(lambda x: x.split('.')[0], test_imgs))
# 訓練集
train_imgs = imgs[test_num:]
# 根據檔名獲取訓練集標籤
train_labels = list(map(lambda x: x.split('.')[0], train_imgs))
return test_imgs, test_labels, train_imgs, train_labels
- 標籤向量化: 既然需要將標籤向量化,那麼,我們也需要將向量化的標籤還原回來。
import numpy as np
def text2vec(text):
"""
文字轉向量
Parameters:
text:文字
Returns:
vector:向量
"""
if len(text) > 4:
raise ValueError('驗證碼最長4個字元')
vector = np.zeros(4 * 63)
def char2pos(c):
if c =='_':
k = 62
return k
k = ord(c) - 48
if k > 9:
k = ord(c) - 55
if k > 35:
k = ord(c) - 61
if k > 61:
raise ValueError('No Map')
return k
for i, c in enumerate(text):
idx = i * 63 + char2pos(c)
vector[idx] = 1
return vector
def vec2text(vec):
"""
向量轉文字
Parameters:
vec:向量
Returns:
文字
"""
char_pos = vec.nonzero()[0]
text = []
for i, c in enumerate(char_pos):
char_at_pos = i #c/63
char_idx = c % 63
if char_idx < 10:
char_code = char_idx + ord('0')
elif char_idx < 36:
char_code = char_idx - 10 + ord('A')
elif char_idx < 62:
char_code = char_idx - 36 + ord('a')
elif char_idx == 62:
char_code = ord('_')
else:
raise ValueError('error')
text.append(chr(char_code))
return "".join(text)
print(text2vec('abcd'))
print(vec2text(text2vec('abcd')))
執行上述測試程式碼,你會發現,文字向量化竟如此簡單:
這裡我們包括了63個字元的轉化,0-9 a-z A-Z _(驗證碼如果小於4,用_補齊)。
2.根據batch_size獲取資料
我們在訓練模型的時候,需要根據不同的batch_size"喂"資料。這就需要我們寫個函式,從整體資料集中獲取指定batch_size大小的資料。
def get_next_batch(self, train_flag=True, batch_size=100):
"""
獲得batch_size大小的資料集
Parameters:
batch_size:batch_size大小
train_flag:是否從訓練集獲取資料
Returns:
batch_x:大小為batch_size的資料x
batch_y:大小為batch_size的資料y
"""
# 從訓練集獲取資料
if train_flag == True:
if (batch_size + self.train_ptr) < self.train_size:
trains = self.train_imgs[self.train_ptr:(self.train_ptr + batch_size)]
labels = self.train_labels[self.train_ptr:(self.train_ptr + batch_size)]
self.train_ptr += batch_size
else:
new_ptr = (self.train_ptr + batch_size) % self.train_size
trains = self.train_imgs[self.train_ptr:] + self.train_imgs[:new_ptr]
labels = self.train_labels[self.train_ptr:] + self.train_labels[:new_ptr]
self.train_ptr = new_ptr
batch_x = np.zeros([batch_size, self.heigth*self.width])
batch_y = np.zeros([batch_size, self.max_captcha*self.char_set_len])
for index, train in enumerate(trains):
img = np.mean(cv2.imread(self.data_path + train), -1)
# 將多維降維1維
batch_x[index,:] = img.flatten() / 255
for index, label in enumerate(labels):
batch_y[index,:] = self.text2vec(label)
# 從測試集獲取資料
else:
if (batch_size + self.test_ptr) < self.test_size:
tests = self.test_imgs[self.test_ptr:(self.test_ptr + batch_size)]
labels = self.test_labels[self.test_ptr:(self.test_ptr + batch_size)]
self.test_ptr += batch_size
else:
new_ptr = (self.test_ptr + batch_size) % self.test_size
tests = self.test_imgs[self.test_ptr:] + self.test_imgs[:new_ptr]
labels = self.test_labels[self.test_ptr:] + self.test_labels[:new_ptr]
self.test_ptr = new_ptr
batch_x = np.zeros([batch_size, self.heigth*self.width])
batch_y = np.zeros([batch_size, self.max_captcha*self.char_set_len])
for index, test in enumerate(tests):
img = np.mean(cv2.imread(self.data_path + test), -1)
# 將多維降維1維
batch_x[index,:] = img.flatten() / 255
for index, label in enumerate(labels):
batch_y[index,:] = self.text2vec(label)
return batch_x, batch_y
上述程式碼無法執行,這是我封裝到類裡的函式,整體程式碼會在文末放出。現在理解下這程式碼,我們通過train_flag來確定是從訓練集獲取資料還是測試集獲取資料,通過batch_size來獲取指定大小的資料。獲取資料之後,將batch_size大小的圖片資料和經過向量化處理的標籤存放到numpy陣列中。
3.CNN模型
網路模型如下:3卷積層+1全連結層。
繼續看下我封裝到類裡的函式:
def crack_captcha_cnn(self, w_alpha=0.01, b_alpha=0.1):
"""
定義CNN
Parameters:
w_alpha:權重係數
b_alpha:偏置係數
Returns:
out:CNN輸出
"""
# 卷積的input: 一個Tensor。資料維度是四維[batch, in_height, in_width, in_channels]
# 具體含義是[batch大小, 影象高度, 影象寬度, 影象通道數]
# 因為是灰度圖,所以是單通道的[?, 100, 30, 1]
x = tf.reshape(self.X, shape=[-1, self.heigth, self.width, 1])
# 卷積的filter:一個Tensor。資料維度是四維[filter_height, filter_width, in_channels, out_channels]
# 具體含義是[卷積核的高度, 卷積核的寬度, 影象通道數, 卷積核個數]
w_c1 = tf.Variable(w_alpha*tf.random_normal([3, 3, 1, 32]))
# 偏置項bias
b_c1 = tf.Variable(b_alpha*tf.random_normal([32]))
# conv2d卷積層輸入:
# strides: 一個長度是4的一維整數型別陣列,每一維度對應的是 input 中每一維的對應移動步數
# padding:一個字串,取值為 SAME 或者 VALID 前者使得卷積後圖像尺寸不變, 後者尺寸變化
# conv2d卷積層輸出:
# 一個四維的Tensor, 資料維度為 [batch, out_width, out_height, in_channels * out_channels]
# [?, 100, 30, 32]
# 輸出計算公式H0 = (H - F + 2 * P) / S + 1
# 對於本卷積層而言,因為padding為SAME,所以P為1。
# 其中H為影象高度,F為卷積核高度,P為邊填充,S為步長
# 學習引數:
# 32*(3*3+1)=320
# 連線個數:
# 100*30*30*100=9000000個連線
# bias_add:將偏差項bias加到value上。這個操作可以看做是tf.add的一個特例,其中bias是必須的一維。
# 該API支援廣播形式,因此value可以是任何維度。但是,該API又不像tf.add,可以讓bias的維度和value的最後一維不同,
conv1 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(x, w_c1, strides=[1, 1, 1, 1], padding='SAME'), b_c1))
# max_pool池化層輸入:
# ksize:池化視窗的大小,取一個四維向量,一般是[1, height, width, 1]
# 因為我們不想在batch和channels上做池化,所以這兩個維度設為了1
# strides:和卷積類似,視窗在每一個維度上滑動的步長,一般也是[1, stride,stride, 1]
# padding:和卷積類似,可以取'VALID' 或者'SAME'
# max_pool池化層輸出:
# 返回一個Tensor,型別不變,shape仍然是[batch, out_width, out_height, in_channels]這種形式
# [?, 50, 15, 32]
# 學習引數:
# 2*32
# 連線個數:
# 15*50*32*(2*2+1)=120000
conv1 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
w_c2 = tf.Variable(w_alpha*tf.random_normal([3, 3, 32, 64]))
b_c2 = tf.Variable(b_alpha*tf.random_normal([64]))
# [?, 50, 15, 64]
conv2 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(conv1, w_c2, strides=[1, 1, 1, 1], padding='SAME'), b_c2))
# [?, 25, 8, 64]
conv2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
w_c3 = tf.Variable(w_alpha*tf.random_normal([3, 3, 64, 64]))
b_c3 = tf.Variable(b_alpha*tf.random_normal([64]))
# [?, 25, 8, 64]
conv3 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(conv2, w_c3, strides=[1, 1, 1, 1], padding='SAME'), b_c3))
# [?, 13, 4, 64]
conv3 = tf.nn.max_pool(conv3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# [3328, 1024]
w_d = tf.Variable(w_alpha*tf.random_normal([4*13*64, 1024]))
b_d = tf.Variable(b_alpha*tf.random_normal([1024]))
# [?, 3328]
dense = tf.reshape(conv3, [-1, w_d.get_shape().as_list()[0]])
# [?, 1024]
dense = tf.nn.relu(tf.add(tf.matmul(dense, w_d), b_d))
dense = tf.nn.dropout(dense, self.keep_prob)
# [1024, 63*4=252]
w_out = tf.Variable(w_alpha*tf.random_normal([1024, self.max_captcha*self.char_set_len]))
b_out = tf.Variable(b_alpha*tf.random_normal([self.max_captcha*self.char_set_len]))
# [?, 252]
out = tf.add(tf.matmul(dense, w_out), b_out)
return out
為了省事,name_scope什麼都沒有設定。每個網路層的功能,維度都已經在註釋裡寫清楚了,甚至包括tensorflow相應函式的說明也註釋好了。如果對於網路結構計算不太瞭解,推薦看下LeNet-5網路解析:http://cuijiahua.com/blog/2018/01/dl_3.html
LeNet-5的網路結構研究清楚了,這裡也就懂了。
4.訓練函式
準備工作都做好了,我們就可以開始訓練了。
def train_crack_captcha_cnn(self):
"""
訓練函式
"""
output = self.crack_captcha_cnn()
# 建立損失函式
# loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=output, labels=self.Y))
diff = tf.nn.sigmoid_cross_entropy_with_logits(logits=output, labels=self.Y)
loss = tf.reduce_mean(diff)
tf.summary.scalar('loss', loss)
# 使用AdamOptimizer優化器訓練模型,最小化交叉熵損失
optimizer = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)
# 計算準確率
y = tf.reshape(output, [-1, self.max_captcha, self.char_set_len])
y_ = tf.reshape(self.Y, [-1, self.max_captcha, self.char_set_len])
correct_pred = tf.equal(tf.argmax(y, 2), tf.argmax(y_, 2))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
tf.summary.scalar('accuracy', accuracy)
merged = tf.summary.merge_all()
saver = tf.train.Saver()
with tf.Session(config=self.config) as sess:
# 寫到指定的磁碟路徑中
train_writer = tf.summary.FileWriter(self.log_dir + '/train', sess.graph)
test_writer = tf.summary.FileWriter(self.log_dir + '/test')
sess.run(tf.global_variables_initializer())
# 遍歷self.max_steps次
for i in range(self.max_steps):
# 迭代500次,打亂一下資料集
if i % 499 == 0:
self.test_imgs, self.test_labels, self.train_imgs, self.train_labels = self.get_imgs()
# 每10次,使用測試集,測試一下準確率
if i % 10 == 0:
batch_x_test, batch_y_test = self.get_next_batch(False, 100)
summary, acc = sess.run([merged, accuracy], feed_dict={self.X: batch_x_test, self.Y: batch_y_test, self.keep_prob: 1})
print('迭代第%d次 accuracy:%f' % (i+1, acc))
test_writer.add_summary(summary, i)
# 如果準確率大於90%,則儲存模型並退出。
if acc > 0.90:
train_writer.close()
test_writer.close()
saver.save(sess, "crack_capcha.model", global_step=i)
break
# 一直訓練,不實用dropout
else:
batch_x, batch_y = self.get_next_batch(True, 100)
loss_value, _ = sess.run([loss, optimizer], feed_dict={self.X: batch_x, self.Y: batch_y, self.keep_prob: 1})
print('迭代第%d次 loss:%f' % (i+1, loss_value))
curve = sess.run(merged, feed_dict={self.X: batch_x_test, self.Y: batch_y_test, self.keep_prob: 1})
train_writer.add_summary(curve, i)
train_writer.close()
test_writer.close()
saver.save(sess, "crack_capcha.model", global_step=self.max_steps)
- 上述程式碼依舊是我封裝到類裡的函式,與我的一篇文章《[Tensorflow實戰(一):打響深度學習的第一槍 – 手寫數字識別(Tensorboard視覺化)》(http://cuijiahua.com/blog/2018/01/dl_4.html)重複的內容不再講解,包括Tensorboard的使用方法。
- 這裡需要強調的一點是,我們需要在迭代到500次的時候重新獲取下資料集,這樣做其實就是打亂了一次資料集。為什麼要打亂資料集呢?因為如果不打亂資料集,在訓練的時候,Tensorboard繪圖會有如下現象:
- 可以看到,準確率曲線和Loss曲線存在跳變,這就是因為我們沒有在迭代一定次數之後打亂資料集造成的。
- 同時,雖然我定義了dropout層,但是在訓練的時候沒有使用它,所以才把dropout值設定為1。
5.整體訓練程式碼
指定GPU,指定Tensorboard資料儲存路徑,指定最大迭代次數,跟Tensorflow實戰(一)的思想都是一致的。這裡,設定最大迭代次數為100W次。我使用的GPU是Titan X,如果是使用CPU訓練估計會好幾天吧....
- 建立train.py檔案,新增如下程式碼:
#-*- coding:utf-8 -*-
import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
import os, random, cv2
class Discuz():
def __init__(self):
# 指定GPU
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
self.config = tf.ConfigProto(allow_soft_placement = True)
gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction = 1)
self.config.gpu_options.allow_growth = True
# 資料集路徑
self.data_path = './Discuz/'
# 寫到指定的磁碟路徑中
self.log_dir = '/home/jack_cui/Work/Discuz/Tb'
# 資料集圖片大小
self.width = 30
self.heigth = 100
# 最大迭代次數
self.max_steps = 1000000
# 讀取資料集
self.test_imgs, self.test_labels, self.train_imgs, self.train_labels = self.get_imgs()
# 訓練集大小
self.train_size = len(self.train_imgs)
# 測試集大小
self.test_size = len(self.test_imgs)
# 每次獲得batch_size大小的當前訓練集指標
self.train_ptr = 0
# 每次獲取batch_size大小的當前測試集指標
self.test_ptr = 0
# 字元字典大小:0-9 a-z A-Z _(驗證碼如果小於4,用_補齊) 一共63個字元
self.char_set_len = 63
# 驗證碼最長的長度為4
self.max_captcha = 4
# 輸入資料X佔位符
self.X = tf.placeholder(tf.float32, [None, self.heigth*self.width])
# 輸入資料Y佔位符
self.Y = tf.placeholder(tf.float32, [None, self.char_set_len*self.max_captcha])
# keepout佔位符
self.keep_prob = tf.placeholder(tf.float32)
def test_show_img(self, fname, show = True):
"""
讀取圖片,顯示圖片資訊並顯示其灰度圖
Parameters:
fname:圖片檔名
show:是否展示灰度圖
"""
# 獲得標籤
label = fname.split('.')
# 讀取圖片
img = cv2.imread(fname)
# 獲取圖片大小
width, heigth, _ = img.shape
print("影象寬:%s px" % width)
print("影象高:%s px" % heigth)
if show == True:
# plt.imshow(img)
#將fig畫布分隔成1行1列,不共享x軸和y軸,fig畫布的大小為(13,8)
#當nrow=3,nclos=2時,代表fig畫布被分為六個區域,axs[0][0]表示第一行第一列
fig, axs = plt.subplots(nrows=2, ncols=1, sharex=False, sharey=False, figsize=(10,5))
axs[0].imshow(img)
axs0_title_text = axs[0].set_title(u'RGB img')
plt.setp(axs0_title_text, size=10)
# 轉換為灰度圖
gray = np.mean(img, axis=-1)
axs[1].imshow(gray, cmap='Greys_r')
axs1_title_text = axs[1].set_title(u'GRAY img')
plt.setp(axs1_title_text, size=10)
plt.show()
def get_imgs(self, rate = 0.2):
"""
獲取圖片,並劃分訓練集和測試集
Parameters:
rate:測試集和訓練集的比例,即測試集個數/訓練集個數
Returns:
test_imgs:測試集
test_labels:測試集標籤
train_imgs:訓練集
test_labels:訓練集標籤
"""
# 讀取圖片
imgs = os.listdir(self.data_path)
# 打亂圖片順序
random.shuffle(imgs)
# 資料集總共個數
imgs_num = len(imgs)
# 按照比例求出測試集個數
test_num = int(imgs_num * rate / (1 + rate))
# 測試集
test_imgs = imgs[:test_num]
# 根據檔名獲取測試集標籤
test_labels = list(map(lambda x: x.split('.')[0], test_imgs))
# 訓練集
train_imgs = imgs[test_num:]
# 根據檔名獲取訓練集標籤
train_labels = list(map(lambda x: x.split('.')[0], train_imgs))
return test_imgs, test_labels, train_imgs, train_labels
def get_next_batch(self, train_flag=True, batch_size=100):
"""
獲得batch_size大小的資料集
Parameters:
batch_size:batch_size大小
train_flag:是否從訓練集獲取資料
Returns:
batch_x:大小為batch_size的資料x
batch_y:大小為batch_size的資料y
"""
# 從訓練集獲取資料
if train_flag == True:
if (batch_size + self.train_ptr) < self.train_size:
trains = self.train_imgs[self.train_ptr:(self.train_ptr + batch_size)]
labels = self.train_labels[self.train_ptr:(self.train_ptr + batch_size)]
self.train_ptr += batch_size
else:
new_ptr = (self.train_ptr + batch_size) % self.train_size
trains = self.train_imgs[self.train_ptr:] + self.train_imgs[:new_ptr]
labels = self.train_labels[self.train_ptr:] + self.train_labels[:new_ptr]
self.train_ptr = new_ptr
batch_x = np.zeros([batch_size, self.heigth*self.width])
batch_y = np.zeros([batch_size, self.max_captcha*self.char_set_len])
for index, train in enumerate(trains):
img = np.mean(cv2.imread(self.data_path + train), -1)
# 將多維降維1維
batch_x[index,:] = img.flatten() / 255
for index, label in enumerate(labels):
batch_y[index,:] = self.text2vec(label)
# 從測試集獲取資料
else:
if (batch_size + self.test_ptr) < self.test_size:
tests = self.test_imgs[self.test_ptr:(self.test_ptr + batch_size)]
labels = self.test_labels[self.test_ptr:(self.test_ptr + batch_size)]
self.test_ptr += batch_size
else:
new_ptr = (self.test_ptr + batch_size) % self.test_size
tests = self.test_imgs[self.test_ptr:] + self.test_imgs[:new_ptr]
labels = self.test_labels[self.test_ptr:] + self.test_labels[:new_ptr]
self.test_ptr = new_ptr
batch_x = np.zeros([batch_size, self.heigth*self.width])
batch_y = np.zeros([batch_size, self.max_captcha*self.char_set_len])
for index, test in enumerate(tests):
img = np.mean(cv2.imread(self.data_path + test), -1)
# 將多維降維1維
batch_x[index,:] = img.flatten() / 255
for index, label in enumerate(labels):
batch_y[index,:] = self.text2vec(label)
return batch_x, batch_y
def text2vec(self, text):
"""
文字轉向量
Parameters:
text:文字
Returns:
vector:向量
"""
if len(text) > 4:
raise ValueError('驗證碼最長4個字元')
vector = np.zeros(4 * self.char_set_len)
def char2pos(c):
if c =='_':
k = 62
return k
k = ord(c) - 48
if k > 9:
k = ord(c) - 55
if k > 35:
k = ord(c) - 61
if k > 61:
raise ValueError('No Map')
return k
for i, c in enumerate(text):
idx = i * self.char_set_len + char2pos(c)
vector[idx] = 1
return vector
def vec2text(self, vec):
"""
向量轉文字
Parameters:
vec:向量
Returns:
文字
"""
char_pos = vec.nonzero()[0]
text = []
for i, c in enumerate(char_pos):
char_at_pos = i #c/63
char_idx = c % self.char_set_len
if char_idx < 10:
char_code = char_idx + ord('0')
elif char_idx < 36:
char_code = char_idx - 10 + ord('A')
elif char_idx < 62:
char_code = char_idx - 36 + ord('a')
elif char_idx == 62:
char_code = ord('_')
else:
raise ValueError('error')
text.append(chr(char_code))
return "".join(text)
def crack_captcha_cnn(self, w_alpha=0.01, b_alpha=0.1):
"""
定義CNN
Parameters:
w_alpha:權重係數
b_alpha:偏置係數
Returns:
out:CNN輸出
"""
# 卷積的input: 一個Tensor。資料維度是四維[batch, in_height, in_width, in_channels]
# 具體含義是[batch大小, 影象高度, 影象寬度, 影象通道數]
# 因為是灰度圖,所以是單通道的[?, 100, 30, 1]
x = tf.reshape(self.X, shape=[-1, self.heigth, self.width, 1])
# 卷積的filter:一個Tensor。資料維度是四維[filter_height, filter_width, in_channels, out_channels]
# 具體含義是[卷積核的高度, 卷積核的寬度, 影象通道數, 卷積核個數]
w_c1 = tf.Variable(w_alpha*tf.random_normal([3, 3, 1, 32]))
# 偏置項bias
b_c1 = tf.Variable(b_alpha*tf.random_normal([32]))
# conv2d卷積層輸入:
# strides: 一個長度是4的一維整數型別陣列,每一維度對應的是 input 中每一維的對應移動步數
# padding:一個字串,取值為 SAME 或者 VALID 前者使得卷積後圖像尺寸不變, 後者尺寸變化
# conv2d卷積層輸出:
# 一個四維的Tensor, 資料維度為 [batch, out_width, out_height, in_channels * out_channels]
# [?, 100, 30, 32]
# 輸出計算公式H0 = (H - F + 2 * P) / S + 1
# 對於本卷積層而言,因為padding為SAME,所以P為1。
# 其中H為影象高度,F為卷積核高度,P為邊填充,S為步長
# 學習引數:
# 32*(3*3+1)=320
# 連線個數:
# 100*30*30*100=9000000個連線
# bias_add:將偏差項bias加到value上。這個操作可以看做是tf.add的一個特例,其中bias是必須的一維。
# 該API支援廣播形式,因此value可以是任何維度。但是,該API又不像tf.add,可以讓bias的維度和value的最後一維不同,
conv1 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(x, w_c1, strides=[1, 1, 1, 1], padding='SAME'), b_c1))
# max_pool池化層輸入:
# ksize:池化視窗的大小,取一個四維向量,一般是[1, height, width, 1]
# 因為我們不想在batch和channels上做池化,所以這兩個維度設為了1
# strides:和卷積類似,視窗在每一個維度上滑動的步長,一般也是[1, stride,stride, 1]
# padding:和卷積類似,可以取'VALID' 或者'SAME'
# max_pool池化層輸出:
# 返回一個Tensor,型別不變,shape仍然是[batch, out_width, out_height, in_channels]這種形式
# [?, 50, 15, 32]
# 學習引數:
# 2*32
# 連線個數:
# 15*50*32*(2*2+1)=120000
conv1 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
w_c2 = tf.Variable(w_alpha*tf.random_normal([3, 3, 32, 64]))
b_c2 = tf.Variable(b_alpha*tf.random_normal([64]))
# [?, 50, 15, 64]
conv2 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(conv1, w_c2, strides=[1, 1, 1, 1], padding='SAME'), b_c2))
# [?, 25, 8, 64]
conv2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
w_c3 = tf.Variable(w_alpha*tf.random_normal([3, 3, 64, 64]))
b_c3 = tf.Variable(b_alpha*tf.random_normal([64]))
# [?, 25, 8, 64]
conv3 = tf.nn.relu(tf.nn.bias_add(tf.nn.conv2d(conv2, w_c3, strides=[1, 1, 1, 1], padding='SAME'), b_c3))
# [?, 13, 4, 64]
conv3 = tf.nn.max_pool(conv3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# [3328, 1024]
w_d = tf.Variable(w_alpha*tf.random_normal([4*13*64, 1024]))
b_d = tf.Variable(b_alpha*tf.random_normal([1024]))
# [?, 3328]
dense = tf.reshape(conv3, [-1, w_d.get_shape().as_list()[0]])
# [?, 1024]
dense = tf.nn.relu(tf.add(tf.matmul(dense, w_d), b_d))
dense = tf.nn.dropout(dense, self.keep_prob)
# [1024, 37*4=148]
w_out = tf.Variable(w_alpha*tf.random_normal([1024, self.max_captcha*self.char_set_len]))
b_out = tf.Variable(b_alpha*tf.random_normal([self.max_captcha*self.char_set_len]))
# [?, 148]
out = tf.add(tf.matmul(dense, w_out), b_out)
# out = tf.nn.softmax(out)
return out
def train_crack_captcha_cnn(self):
"""
訓練函式
"""
output = self.crack_captcha_cnn()
# 建立損失函式
# loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=output, labels=self.Y))
diff = tf.nn.sigmoid_cross_entropy_with_logits(logits=output, labels=self.Y)
loss = tf.reduce_mean(diff)
tf.summary.scalar('loss', loss)
# 使用AdamOptimizer優化器訓練模型,最小化交叉熵損失
optimizer = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)
# 計算準確率
y = tf.reshape(output, [-1, self.max_captcha, self.char_set_len])
y_ = tf.reshape(self.Y, [-1, self.max_captcha, self.char_set_len])
correct_pred = tf.equal(tf.argmax(y, 2), tf.argmax(y_, 2))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
tf.summary.scalar('accuracy', accuracy)
merged = tf.summary.merge_all()
saver = tf.train.Saver()
with tf.Session(config=self.config) as sess:
# 寫到指定的磁碟路徑中
train_writer = tf.summary.FileWriter(self.log_dir + '/train', sess.graph)
test_writer = tf.summary.FileWriter(self.log_dir + '/test')
sess.run(tf.global_variables_initializer())
# 遍歷self.max_steps次
for i in range(self.max_steps):
# 迭代500次,打亂一下資料集
if i % 499 == 0:
self.test_imgs, self.test_labels, self.train_imgs, self.train_labels = self.get_imgs()
# 每10次,使用測試集,測試一下準確率
if i % 10 == 0:
batch_x_test, batch_y_test = self.get_next_batch(False, 100)
summary, acc = sess.run([merged, accuracy], feed_dict={self.X: batch_x_test, self.Y: batch_y_test, self.keep_prob: 1})
print('迭代第%d次 accuracy:%f' % (i+1, acc))
test_writer.add_summary(summary, i)
# 如果準確率大於85%,則儲存模型並退出。
if acc > 0.85:
train_writer.close()
test_writer.close()
saver.save(sess, "crack_capcha.model", global_step=i)
break
# 一直訓練
else:
batch_x, batch_y = self.get_next_batch(True, 100)
loss_value, _ = sess.run([loss, optimizer], feed_dict={self.X: batch_x, self.Y: batch_y, self.keep_prob: 1})
print('迭代第%d次 loss:%f' % (i+1, loss_value))
curve = sess.run(merged, feed_dict={self.X: batch_x_test, self.Y: batch_y_test, self.keep_prob: 1})
train_writer.add_summary(curve, i)
train_writer.close()
test_writer.close()
saver.save(sess, "crack_capcha.model", global_step=self.max_steps)
if __name__ == '__main__':
dz = Discuz()
dz.train_crack_captcha_cnn()
- 程式碼跑了一個多小時終於跑完了,Tensorboard顯示的資料:
- 準確率達到百分之90以上吧。
6.測試程式碼
已經有訓練好的模型了,怎麼載入已經訓練好的模型進行預測呢?在和train.py相同目錄下,建立test.py檔案,新增如下程式碼:
#-*- coding:utf-8 -*-
import tensorflow as tf
import numpy as np
import train
def crack_captcha(captcha_image, captcha_label):
"""
使用模型做預測
Parameters:
captcha_image:資料
captcha_label:標籤
"""
output = dz.crack_captcha_cnn()
saver = tf.train.Saver()
with tf.Session(config=dz.config) as sess:
saver.restore(sess, tf.train.latest_checkpoint('.'))
for i in range(len(captcha_label)):
img = captcha_image[i].flatten()
label = captcha_label[i]
predict = tf.argmax(tf.reshape(output, [-1, dz.max_captcha, dz.char_set_len]), 2)
text_list = sess.run(predict, feed_dict={dz.X: [img], dz.keep_prob: 1})
text = text_list[0].tolist()
vector = np.zeros(dz.max_captcha*dz.char_set_len)
i = 0
for n in text:
vector[i*dz.char_set_len + n] = 1
i += 1
prediction_text = dz.vec2text(vector)
print("正確: {} 預測: {}".format(dz.vec2text(label), prediction_text))
if __name__ == '__main__':
dz = train.Discuz()
batch_x, batch_y = dz.get_next_batch(False, 5)
crack_captcha(batch_x, batch_y)
執行程式,隨機從測試集挑選5張圖片,效果還行,錯了一個字母:
總 結
通過修改網路結構,以及超引數,學習如何調參。
- 可以試試其他的網路結構,準確率還可以提高很多的。
- Discuz驗證碼可以使用更復雜的,這僅僅是個小demo。
- 如有問題,請留言。如有錯誤,還望指正,謝謝!
- 本文出現的所有程式碼和資料集,均可在我的github上下載,歡迎Follow、Star: http://cuijiahua.com/wp-content/themes/begin/inc/go.php?url=https://github.com/Jack-Cherish/Deep-Learning
- 6W張驗證碼下載地址(密碼:d3iq):https://pan.baidu.com/s/1mjI2Gxq
- 個人網站:http://cuijiahua.com