一、前言
验证码是根据随机字符生成一幅图片,然后在图片中加入干扰象素,用户必须手动填入,防止有人利用机器人自动批量注册、灌水、发垃圾广告等等 。
验证码的作用是验证用户是真人还是机器人。
本文将使用深度学习框架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:点击查看
2021年5月11日 上午10:10 61楼
终于理解了如何从图片到字符串的直接识别,学了tensorflow上mnist之后都是拆分了一个一个字符的识别,谢谢
2021年5月23日 上午11:37 1层
@bunny 感谢支持~~
2021年5月23日 上午9:30 62楼
如果使用 M1 MacBook 来训练的话,如何修改相关代码呢?
2021年5月23日 上午11:37 1层
@bbyte 都是一样的,改为cpu就行。
2021年12月24日 上午5:09 63楼
merged = tf.compat.v1.summary.merge_all()现在在tersoeflow2.0这个版本下那行命令会出现merged为none的BUG 导致后面的sess.run无法运行,请问楼主可以解决一下这个问题吗 换一个可以兼容tf2.0的函数
2021年12月24日 上午5:13 1层
@|锟截o拷) 我找到类似于这种
weights_summary = tf.histogram_summary(“weights”, W)
biases_summary = tf.histogram_summary(“biases”, b)
y_summary = tf.histogram_summary(“y”, y)
merged = tf.merge_summary([weights_summary, biases_summary, y_summary])
的方法 但是上面这些函数依旧不能再tf2.0上面使用
我几乎把整个train的tf函数都改了一遍 为了适配tf2.0希望这是最后一个坎
2024年2月28日 下午8:45 2层
@|锟截o拷) 佬你跑通了吗,能不能救救后来者,我改的相似
2022年5月19日 下午6:34 64楼
怎么识别指定图片阿?
2023年10月13日 下午5:09 65楼
你好,博主,请问你是用训练过的数据来进行测试的吗,训练集和测试集是一样的吗
2024年2月25日 下午5:57 66楼
大佬好,下面这种验证码图片应该怎么生成呢?在背景图上还有一串白色的英文
不懂php,如果需要修改代码,应该如何修改呀?
https://img.xwyue.com/i/2024/02/25/65db0e5fc5576.png
2024年2月25日 下午6:00 67楼
补充图片https://img.xwyue.com/i/2024/02/25/65db0e5fc5576.png