一、前言
验证码是根据随机字符生成一幅图片,然后在图片中加入干扰象素,用户必须手动填入,防止有人利用机器人自动批量注册、灌水、发垃圾广告等等 。
验证码的作用是验证用户是真人还是机器人。
本文将使用深度学习框架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:点击查看

2019年10月20日 下午7:49 51楼
你好博主
ZeroDivisionError: integer division or modulo by zero
这个问题在哪
2019年10月21日 上午9:52 1层
@蔓越莓曲奇 这个应该是除数为零了,检查下报错位置的除数是否正确。
2019年11月5日 下午5:18 52楼
博主,那个怎么填模型保存的地址啊,是320行吗?还有如果果换其他像素的图片,怎么修改参数呢
2019年11月5日 下午6:59 1层
@lemon 320行是保存日志,用于tensorboard的,saver.save是保存模型。
其它像素点的图片,需要修改输入shape,其它应该不会变化。
2019年11月8日 下午6:47 53楼
请问我如果换40×120的图片应该怎么修改网络呢?
2019年11月9日 下午4:04 1层
@Déjà Vu 在__init__函数里设置应该就行。
2019年11月9日 下午4:31 2层
@Jack Cui 我把自己的resize成30×100了,我只有5000张带标签的数据集,迭代了9000多次acc就0.99了,然后用这个模型预测另外5000张没标签的图片准确率很低,请问有什么好办法吗?我是初学者
2019年11月10日 下午2:35 2层
@Jack Cui 楼主,用这个模型预测验证集效果挺好的,但当用这个模型预测测试集的时候效果很差,可能是过拟合了,有什么解决办法吗
2019年11月10日 下午3:20 3层
@112 换个网络试试吧,这个是最基础的网络,换下mobilenet这类的网络提取特征,效果应该会好很多。过拟合了可以在损失函数加入惩罚项。
2019年11月11日 下午8:16 4层
@Jack Cui 楼主,我觉得应该不需要换网络了,因为我只有5000张训练集,但是训练出的模型预测测试集5000张只对了十几张,而且我也用了dropout了,方便留个联系方式吗?我想多请教你一下
2019年11月11日 下午10:00 4层
@Jack Cui 还有一个问题就是为什么预测的结果是随机的呢,我是从1-5000.jpg的名字,结果打印vec2text(label)顺序就全乱了 就是3666 147 569 那种乱序,这是怎么回事
2019年11月12日 上午10:04 3层
@112 因为有个shuffle操作,打乱了顺序。模型训练还不够,多迭代迭代,看下loss曲线,还能继续下降就继续训练着。侧边栏有我的联系方式。
2020年1月24日 下午4:33 54楼
图片位深度是24,为什么通道数是1?
2020年1月27日 上午10:41 1层
@锟斤拷锟斤拷 没看到你说的地方啊,哪块的代码呢?
2020年3月29日 上午11:26 55楼
请问现在验证码图片是要自己生成吗,是要运行Discuz下的哪个文件呢
2020年3月29日 下午12:46 1层
@松松松松松 自己生成的,生成的代码,我也分享了,文章中有链接。
2020年3月30日 下午7:57 2层
@Jack Cui 你好博主,我之前没用过php,我下了wampserver,然后运行了你给的discuz下的index.php文件,只生成了一张jack。。。难道不是运行这个文件吗?
2020年3月30日 下午8:01 3层
@松松松松松 修改参数,拼接url,就你生成不同的图片了。
2020年4月8日 下午9:25 4层
@Jack Cui 你好博主,我改了参数,可以生成别的图片了。但是我难道要一张一张生成吗。。
2020年4月8日 下午10:32 4层
@松松松松松 嗯,用爬虫生成下载呗。
2020年5月4日 上午11:20 56楼
怎么直接用模型识别单独指定的验证码啊?
2020年5月4日 下午2:36 1层
@partnick 单独指定是什么意思?我这个必须是4个字符的。
2020年5月7日 下午7:46 2层
@Jack Cui 我的意思是除了验证模型意外,识别指定的一张验证码,虽然生成了模型但是不会用。
2020年5月8日 上午12:26 57楼
楼主您好,请问您当时运行的checkpoint还有吗,我的电脑运行太慢了。非常感谢
2020年5月8日 下午8:37 1层
@xx 没了~
2020年5月13日 下午1:51 58楼
没仔细看:)
2020年5月21日 下午3:01 59楼
博主,你用的是什么型号的GPU呀,我用pytorch复写了你的代码但是acc一直升不上去,看你一个小时就跑完了9万多epoch,为啥我一个小时只能运行几千个呀,我的batch_size也是100呀
2020年5月21日 下午10:31 1层
@雅利安 我是rtx 2060s,就是因为迭代次数不够~
2020年5月22日 下午4:20 2层
@Jack Cui 如果我想把batch_size调高一点,学习率要做改动吗?
2020年5月23日 上午10:11 3层
@雅利安 不用的
2021年4月2日 上午10:57 60楼
博主您好,在运行train.py时遇到AttributeError: ‘Discuz’ object has no attribute ‘config’问题怎么解决呢?在网上没有搜到解决这个问题的方法。