一、前言
验证码是根据随机字符生成一幅图片,然后在图片中加入干扰象素,用户必须手动填入,防止有人利用机器人自动批量注册、灌水、发垃圾广告等等 。
验证码的作用是验证用户是真人还是机器人。
本文将使用深度学习框架Tensorflow训练出一个用于破解Discuz验证码的模型。
自动生成验证码接口关闭,原因如下:
接口是用来方便大家获取验证码图片的,已经声明至少加200ms延时,但是有些人就是不管不顾,个人网站带宽不大,直接被占满(自己写代码让别人爬自己网站真是找罪受),服务器被蹂躏十多分钟。这种情况已经出现很多次了,实在受不了,只能关闭此功能,望谅解了。
想要Discuz验证码生成代码(php)的,到网盘下载吧(密码:nf1t):
二、背景介绍
我们先看下简单的Discuz验证码。
打开下面的连接,你就可以看到这个验证码了。
https://cuijiahua.com/tutrial/discuz/index.php?label=jack
怎么获取其他验证码呢?我已经为大家准备好了api,格式如下:
1 | https://cuijiahua.com/tutrial/discuz/index.php?label=jack |
观察上述链接,你会发现label后面跟着的就是要显示的图片字母,改变label后面的值,我们就可以获得不同的Discuz验证码图片。
如果会网络爬虫,我想根据这个api获取Discuz验证码图片对你来说应该很Easy。
不会网络爬虫也没有关系,爬虫代码我已经为你准备好了。创建一个get_discuz.py文件,添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #-*- coding:utf-8 -*- from urllib.request import urlretrieve import time, random, os class Discuz(): def __init__(self): # Discuz验证码生成图片地址 self.url = 'https://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验证码图片可到文章末尾处下载。
准备好的数据集,它们都是100*30大小的图片:
什么?你说这个图片识别太简单?没关系,有高难度的!
我打开的图片如下所示:
这是一个动图,并且还带倾斜、扭曲等特效。怎么通过api获得这种图片呢?
1 | https://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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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 |
标签向量化:
既然需要将标签向量化,那么,我们也需要将向量化的标签还原回来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 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大小的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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全链接层。
继续看下我封装到类里的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | 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 # 连接个数: # (输出图像宽度*输出图像高度)(卷积核高度*卷积核宽度+1)*卷积核数量(100*30)(3*3+1)*32=100*30*320=960000个 # 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) # out = tf.nn.softmax(out) return out |
为了省事,name_scope什么都没有设定。每个网络层的功能,维度都已经在注释里写清楚了,甚至包括tensorflow相应函数的说明也注释好了。
如果对于网络结构计算不太了解,推荐看下LeNet-5网络解析:
https://cuijiahua.com/blog/2018/01/dl_3.html
LeNet-5的网络结构研究清楚了,这里也就懂了。
4、训练函数
准备工作都做好了,我们就可以开始训练了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | 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可视化)》重复的内容不再讲解,包括Tensorboard的使用方法。
这里需要强调的一点是,我们需要在迭代到500次的时候重新获取下数据集,这样做其实就是打乱了一次数据集。为什么要打乱数据集呢?因为如果不打乱数据集,在训练的时候,Tensorboard绘图会有如下现象:
可以看到,准确率曲线和Loss曲线存在跳变,这就是因为我们没有在迭代一定次数之后打乱数据集造成的。
同时,虽然我定义了dropout层,但是在训练的时候没有使用它,所以才把dropout值设置为1。
5、整体训练代码
指定GPU,指定Tensorboard数据存储路径,指定最大迭代次数,跟Tensorflow实战(一)的思想都是一致的。这里,设置最大迭代次数为100W次。
我使用的GPU是Titan X,如果是使用CPU训练估计会好几天吧....
创建train.py文件,添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 | #-*- 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)(3*3+1)*32=100*30*320=960000个 # 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文件,添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #-*- 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。
- 如有问题,请留言。如有错误,还望指正,谢谢!
PS: 如果觉得本篇本章对您有所帮助,欢迎关注、评论、赞!
本文出现的所有代码和数据集,均可在我的github上下载,欢迎Follow、Star:点击查看

2018年1月29日 上午9:21 沙发
大佬你好,我是你的忠实fans
2018年1月29日 上午9:23 1层
@HeyMan 哈哈,感谢支持~
2018年1月29日 上午9:26 2层
@Jack Cui 我觉得学机器学习看你的教程就够了,不仅通俗易懂还时常更新加上代码实战检验,最最最主要的是还有真人随时解答。啊哈哈
2018年1月29日 上午9:28 3层
@HeyMan 哈哈,继续一起加油
2018年1月29日 下午7:28 板凳
好赞呀~棒棒哒~
2018年1月29日 下午8:48 1层
@卷卷
2018年1月30日 下午3:57 地板
File “E:/untitled1/Discuz/train.py”, line 367, in
dz.train_crack_captcha_cnn()
File “E:/untitled1/Discuz/train.py”, line 338, in train_crack_captcha_cnn
batch_x_test, batch_y_test = self.get_next_batch(False, 100)
IndexError: tuple index out of range
博主,Windows运行您的代码train.py出现以上错误,希望能解答
2018年1月30日 下午4:04 1层
@2495332611 你下载数据集了吗?
2018年1月30日 下午4:07 2层
@Jack Cui 嗯,我是用你的爬虫爬的6000张图片作为数据集的
2018年1月30日 下午4:10 3层
@2495332611 你这个应该是加载的数据有问题,你试试单独调用get_next_batch函数,看看加载的数据是否有问题。分析下是不是路径问题。
2018年1月30日 下午4:13 2层
@Jack Cui 不好意思,是我路径的问题,但现在运行又出了一个问题:
The TensorFlow library wasn’t compiled to use SSE4.1 instructions, but these are available on your machine and could speed up CPU computations.
…
Process finished with exit code -1073740791 (0xC0000409)
它没训练,是我的电脑不支持gpu跑的原因吗,如何改成cpu训练
2018年1月30日 下午4:16 3层
@2495332611 嗯,这个是因为你没有GPU,去掉GPU的配置项即可。在def __init__函数中。
2018年1月30日 下午4:26 4层
@Jack Cui 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
我注释掉了那几条gpu的配置,但还是不训练,请问还有哪些地方需要改的
2018年1月30日 下午4:27 3层
@2495332611 config也去掉,然后在创建sess中的config也去掉。
2018年1月30日 下午4:35 4层
@Jack Cui with tf.Session() as sess:
# 写到指定的磁盘路径中
train_writer = tf.summary.FileWriter(self.log_dir + ‘/train’, sess.graph)
之前的config去掉了,然后上面Session()中的config也去掉了,还是不训练,实在很麻烦你了
2018年1月30日 下午4:40 1层
@2495332611 还训练不了?呃,我不知道这回你是哪里报错了啊。根据错误百度下,看自己哪里没有配置好。
2018年1月30日 下午4:42 2层
@Jack Cui 2018-01-30 16:39:56.282371: W c:\l\tensorflow_1501918863922\work\tensorflow-1.2.1\tensorflow\core\platform\cpu_feature_guard.cc:45] The TensorFlow library wasn’t compiled to use SSE4.2 instructions, but these are available on your machine and could speed up CPU computations.
2018-01-30 16:39:56.282666: W c:\l\tensorflow_1501918863922\work\tensorflow-1.2.1\tensorflow\core\platform\cpu_feature_guard.cc:45] The TensorFlow library wasn’t compiled to use AVX instructions, but these are available on your machine and could speed up CPU computations.
Process finished with exit code -1073740791 (0xC0000409)
还是这个错误
2018年2月1日 上午10:28 3层
@2495332611 呃,没有读写权限?我没有遇到过这种情况,你搜搜看tf的模型保存,看看按照别人windows下的写法有问题没?
2018年1月30日 下午4:50 2层
@Jack Cui 谢谢你的耐心回复,剩下的我自己多查查
2018年1月30日 下午4:53 3层
@2495332611 哦,那应该还是cpu那里设置的问题,我没有用过cpu版本的,你百度看看~
2018年1月30日 下午5:03 4层
@Jack Cui 嗯,十分感谢
2018年2月1日 上午10:05 4层
@Jack Cui 代码现在能跑通了,但是最后保存出了问题,网上查了半天也没解决,请您帮我看下
2018-02-01 09:59:30.409286: … Not found: Failed to create a directory: ; No such file or directory
…
ValueError: Parent directory of crack_capcha.model doesn’t exist, can’t save.
出错的位置应该是这个:saver.save(sess, ‘crack_capcha.model’, global_step=i)
我改成相对路径saver.save(sess, ‘./crack_capcha.model’, global_step=i)还是不行
2018年3月9日 下午9:00 3层
@2495332611 你好,在嘛?你跑的验证码出的IndexError: tuple index out of range 这个训练问题你是如何解决的,还有我也是用windows的cpu跑的,能说下如何进行训练嘛
2018年3月9日 下午9:07 1层
@2495332611 在嘛?
2018年3月9日 下午10:29 1层
@2495332611 你好,问下你那个路径是怎么处理的
2018年3月10日 上午8:31 2层
@jcxytt_s 我都是在同一个目录下,没有路径处理。
2018年2月13日 上午3:05 4楼
test_show_img 这个转灰度图像的函数 貌似没有用到哦
2018年2月13日 上午3:15 5楼
test_show_img 这个转灰度图像的函数 貌似没有用到哦,
所以这个函数其实可以不用写出来 对吗
2018年2月13日 上午7:54 1层
@差很的真语英我 这个函数没有用到,就是为了测试用的。看下转换效果。
2018年2月13日 下午2:29 6楼
get_next_batch函数中,
img = np.mean(cv2.imread(self.data_path + test), -1)
请问这个-1代表什么,有什么意义,为什么要加-1
2018年2月13日 下午2:53 1层
@差很的真语英我 numpy的mean函数,这里就是变成灰度图,用我那个没使用的函数 对比下就知道了。
2018年2月13日 下午2:59 2层
@Jack Cui 谢谢
2018年2月13日 下午8:56 7楼
请问下 如果我想从中自己选一张图片 让他识别出来 该怎么做
2018年2月13日 下午9:19 8楼
刚才问的问题我会了,如果能把测试代码 用文字描述下就更好了,感谢博主
2018年2月14日 上午9:58 1层
@差很的真语英我 看懂就好,加油~
2018年2月18日 下午10:12 9楼
你好啊,有一个问题,既然是不分大小写,而且我看生成的label也全是小写的,为啥后面转成vector的时候要加上大写的呢,只用小写把向量维度减小到37个是否能行,另外,如果不考虑’_’这个无法识别的字符的话,把向量化的维度减小到36行不行(0-9和a-z)。
2018年2月18日 下午10:49 1层
@小青蛙 这个向量化是通用的向量化方法,其实可以较少到0-9和a-z不需要大写即可,为了考虑别人可能用大写,就都写好了
2018年2月19日 上午9:36 2层
@Jack Cui 好的,另外还有一个问题,你给的连接里面关于LeNet-5的说明中,指定第一个卷积层是5×5的,然后你这里使用的是3×3的,我想问下这是不是一种使用CNN的技巧呢,对于一些已经验证的网络,只要层使用对了就可以,里面的一些细节和超参数都是可以根据实际情况调整的吗,比如这个验证码的例子,是不是只要网络结构是conv-pool-conv-pool-conv-dense-dense就行,具体里面有多少个卷积核,卷积核的大小这一些都不会影响最后的效果?
2018年2月19日 上午9:54 3层
@小青蛙 这个都是有影响的,具体的东西其实有很多,这个都是参数。推荐看下吴恩达的视频进行了解。
2018年2月20日 下午2:10 4层
@Jack Cui 好的,还有我按你的思路自己搭了一个,但是没全抄你的代码,发现在test set上准确率只有60%,我没你训练那么多次,但是到了第20个epoch之后准确率就这样了,也不提高,有什么方法能提升准确率吗,另外,最后你说使用其他网络效果会更好,具体是哪些网络效果会更好呢。
2018年2月20日 下午3:37 3层
@小青蛙 就是一些调参方法了。网络的话,我也没有测试,我就用了这个最简单的网络结构。你可以换个网络试试。
2018年2月21日 下午12:32 10楼
hi,问一下,为什么图像你没做灰度处理。还有就是没有做灰度处理,但图像通道你用的是1???
2018年2月21日 下午2:14 1层
@aaa 有做灰度处理,在读取图像的时候就做了。使用np.mean