Discuz验证码识别(编码篇)-写给程序员的TensorFlow教程

欢迎大家回到《写给程序员的TensorFlow教程》系列中来,本系列希望能给广大想转型机器学习的程序员带来一些不一样的内容,我们不讲公式,只调方法,不聊文献,只说代码。不求最好,只求有用。带大家迅速上手TensorFlow(以下简称TF。我是强迫症患者,每次都敲驼峰太累了)。


下面正式要开始了我们真正的TensorFlow编程,这篇文章主要内容分为两部分,一部分是介绍TF的基础知识和一些常用接口;第二部分是接着上节课的内容继续执行我们的解题思路。

我们先进入第一部分
Part 1、TF基础知识

虽然我们机器学习的基础概念可以先不深究,但是TF还是得讲一讲的,不然咱们就算抄再多代码也是天书,完全达不到渗透法的学习目的,所以我们先给大家讲一讲TF的基本代码结构。

对于开发网页或者移动应用来说,程序是沿着我们写出来的代码逻辑一步一步执行的,因此我们一般都是可以在代码中任意地方插入print来打印我们需要了解的变量的值。但是在TF中却不是这样,TF中我们的代码一般会被分为两个模块:

1.先预定义一张图

注意:这个图不是图片的图,而是一个节点之间相互连接的结构,为了能打击大家的学习积极性,我先放一张简单的图给大家看看:

2.再循环把数据扔进这张图中执行

举一个不恰当的比方:

机器学习任务类似与起房子,一条一条的数据就是一块一块的砖,最终训练出来的模型就是房子本身,那么相对应的起房子的流程就是:

1.先画一个设计的图纸

2.根据这个设计的图纸,用砖一点一点把房子造出来

这里稍微有些不恰当的细节,不过初学者不用在意这些细节。

那么接着我们就要认识到TF中的一个最重要的概念,那就是Session,这个Session就可以理解为施工队,Session的具体用法如下:

#创建一个施工队
sess = tf.Session()
#把图纸和砖交给施工队并让他们开始干活
sess.run(...)

是不是很简单呢?掌握了TF代码结构中这个最大的不一样,剩下的相对来说就比较直观了。

前面提到,TF代码结构主要分为两个部分。那么在真实的项目中,实际上会更加复杂一些,一般来说我们会把代码分为四个部分:

1.图定义模块

2.数据读取处理模块

3.训练执行模块

4.模型保存模块

也就是比基本流程多 "数据读取处理" 和 "模型保存" 两个模块,由于篇幅限制,今天我们不打算去讨论模型的保存,我们就把前三项完成即可。


Part 2、继续完成上篇文章的解题思路

好了,大家看完了上面的部分,起码应该对TF有了一个基本的认知,而且还学会了两个函数调用,恭喜大家,离学会TF只剩下十万八千里的距离,我们继续努力。

说完TF基础知识,我们回顾一下我们上篇文章里的问题和解题思路,我们的要解决的问题是识别Discuz系统中的验证码:

我们打算用TF来解决这个问题,那么我们的解题思路如下:

第一步:将问题分解成输入(x)到输出(y)这样的结构,如Discuz验证码的输入是图片,输出是四个字符的字符串

第二步:找到很多同时包含输入输出的数据,比如很多有识别结果的验证码图片

第三步:针对不同问题,找到算法大神们的已经定义好的算法并实现成代码

第四步:尝试使用这个算法训练这些数据,如果效果不好,算法中有一些参数可以手动调整,至于怎么调,可以参考前人经验,也可以自己瞎调积累经验。

第五步:写一个程序载入模型,接受一个新的输入值,通过模型计算出新的输出值。

粗体部分是我们上篇文章已经完成了的步骤,这篇文章主要来完成第三步。这里我们结合本文Part 1讨论的内容,我们将第三步继续分解一下:

1.图定义模块

    针对图像识别问题,我们决定使用CNN。
    大神们定义了很多CNN。但是因为我们是菜鸟,所以我们打算用最简单的那种。
    我们还不会写TF代码,所以得先抄一个CNN的实现

2.数据读取模块(如果是本地的TF,请自行解决这个部分)

    我们的数据在哪里? - 神箭手爬下来的数据
    我们怎么读取到这些数据?- 通过官方定义的接口
    读入数据怎么传给TF -抄官方的Demo

3.训练执行模块

    这个我们已经会了,创建一个施工队,并让他们干活

好了,思路已经很清晰了,大家可以按照这个思路去抄代码了...

好吧,前面这句是开玩笑的,就以大家现在的水准,怎么可能能抄的出来呢。当然是我先抄下来给大家了,代码奉上:

import tensorflow as tf
import numpy as np
import pandas as pd

######## 图定义模块 ########
x = tf.placeholder(tf.string,shape=[None,])
image_bin = tf.decode_base64(x)
image_bin_reshape = tf.reshape(image_bin,shape=[-1,])
images = tf.map_fn(lambda img: tf.image.decode_png(img), image_bin_reshape,dtype=tf.uint8)
image_gray = tf.image.rgb_to_grayscale(images)
image_resized = tf.image.resize_images(image_gray, [48, 48],tf.image.ResizeMethod.NEAREST_NEIGHBOR)
image_resized_float = tf.image.convert_image_dtype(image_resized, tf.float32)

y1 = tf.placeholder(tf.float32,shape=[None, 24])
y2 = tf.placeholder(tf.float32,shape=[None, 24])
y3 = tf.placeholder(tf.float32,shape=[None, 24])
y4 = tf.placeholder(tf.float32,shape=[None, 24])


image_x = tf.reshape(image_resized_float,shape=[-1,48,48,1])
conv1 = tf.layers.conv2d(image_x, filters=32, kernel_size=[5, 5], padding='same')
norm1 = tf.layers.batch_normalization(conv1)
activation1 = tf.nn.relu(conv1)
pool1 = tf.layers.max_pooling2d(activation1, pool_size=[2, 2], strides=2, padding='same')
hidden1 = pool1

conv2 = tf.layers.conv2d(hidden1, filters=64, kernel_size=[5, 5], padding='same')
norm2 = tf.layers.batch_normalization(conv2)
activation2 = tf.nn.relu(norm2)
pool2 = tf.layers.max_pooling2d(activation2, pool_size=[2, 2], strides=2, padding='same')
hidden2 = pool2


flatten = tf.reshape(hidden2, [-1, 12 * 12 * 64])
hidden3 = tf.layers.dense(flatten, units=1024, activation=tf.nn.relu)

letter1 = tf.layers.dense(hidden3, units=24)
letter2 = tf.layers.dense(hidden3, units=24)
letter3 = tf.layers.dense(hidden3, units=24)
letter4 = tf.layers.dense(hidden3, units=24)

letter1_cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y1, logits=letter1))
letter2_cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y2, logits=letter2))
letter3_cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y3, logits=letter3))
letter4_cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y4, logits=letter4))
loss = letter1_cross_entropy + letter2_cross_entropy+letter3_cross_entropy+letter4_cross_entropy

optimizer = tf.train.AdamOptimizer(1e-4)
train_op = optimizer.minimize(loss)

predict_concat = tf.stack([tf.argmax(letter1,1),
                           tf.argmax(letter2,1),
                           tf.argmax(letter3,1),
                           tf.argmax(letter4,1)],1)
y_concat = tf.stack([tf.argmax(y1,1),
                     tf.argmax(y2,1),
                     tf.argmax(y3,1),
                     tf.argmax(y4,1)],1)
accuracy_internal = tf.cast(tf.equal(predict_concat, y_concat),tf.float32),
accuracy = tf.reduce_mean(tf.reduce_min(accuracy_internal,2))
accuracy_letter =  tf.reduce_mean(tf.reshape(accuracy_internal,[-1]))


initer = tf.global_variables_initializer()
sess = tf.Session()
sess.run(initer)

######## 数据读取模块 ########
pickup = "BCEFGHJKMPQRTVWXY2346789"
reader = pd.read_source(souce_id=802522,iterator=True)

identity = np.identity(24)
for i in range(1000):
  df = reader.get_chunk(100)
  if df.empty:
    reader = pd.read_source(souce_id=802522,iterator=True)
    continue
  batch_y = df["code"].values
  batch_x = df["content"].values
  batch_y_1 = [identity[pickup.find(code[0])] for code in batch_y]
  batch_y_2 = [identity[pickup.find(code[1])] for code in batch_y]
  batch_y_3 = [identity[pickup.find(code[2])] for code in batch_y]
  batch_y_4 = [identity[pickup.find(code[3])] for code in batch_y]
  ######## 训练执行模块 ########
  if i%10 == 0:
    print("step:"+str(i))
    accuracy_letter_,accuracy_ = sess.run([accuracy_letter,accuracy],feed_dict={x:batch_x,y1:batch_y_1,y2:batch_y_2,y3:batch_y_3,y4:batch_y_4})
    print(accuracy_letter_)
    print("accuracy is ====>%f"%accuracy_)
    if accuracy_ > 0.80:
      break
  sess.run(train_op,feed_dict={x:batch_x,y1:batch_y_1,y2:batch_y_2,y3:batch_y_3,y4:batch_y_4})

特别说明两点!!!!!

1.这段代码由于有数据读取处理模块,这个调用了pd.read_source这个方法,因此只能在神箭手上执行,如果希望本地执行的朋友,需要替换pd.read_source这个方法

2.pd.read_source里面的source_id是我的数据id,大家是读取不了了,需要用我上一篇文章中的爬虫代码跑出来的数据对应的数据id替换进去即可。

好了,肯定有朋友要说了,这么一长串,我抄下来还是不理解啊。没关系,咱们先把代码跑通其实最重要,后面我们会需要不断的改动这段代码,改着改着就懂了。


关于这段代码,我在补充以下几点,大家可以带着这些问题去把代码看一看,尝试简单理解下:

一、关于图定义部分

从x到accuracy_letter都是定义图的部分,这张图看着很大,不过其实包含了三个部分,

1.从x到image_x是数据预处理部分(等等,图里面怎么也有预处理?这个我在下一篇文章中会讲到,网上一般代码是不会这么写的)

2.从image_x到train_op是核心需要训练的图部分,这部分图是核心的CNN,是我根据从github中找来的代码简化的,值得注意的是这里的CNN跟传统的CNN不一样,因为我们要输出4个结果(就是验证码的4个字符),一般的识别只输出一个结果(比如是猫,还是狗,还是兔子),不过这个不一样还是很简单的,后面我们进一步过代码的时候,大家就会明白。(另外,由于我们数据集可以是无穷大,所以我扔掉防止过拟合的代码,让训练能快一点)

3.从train_op到accuracy_letter是我们计算我们训练结果准确率的部分,是用来判断目前模型的表现的,有没有不影响核心功能。

二、关于数据处理部分

神箭手官方的接口是给python的数据清洗类库pandas增加了一个read_source方法,通过get_chunk返回的是一个标准的pandas的dataframe对象,用过pandas的朋友应该很熟悉,没用过的直接抄就行,这段代码在大部分时候都不用改。

特别注意的是,我们数据中的输出部分y(就是验证码的识别结果),是一个四个字符的字符串,但是这样子是不能传给TF的,TF需要另外一种格式的数据y(one-hot encoding),转换代码就是中间一段,我单独截取出来看下,暂时不理解的也无所谓(不过建议大家可以打印下这个one-hot encoding一般是什么样子的,有个更直观的理解)。

pickup = "BCEFGHJKMPQRTVWXY2346789"
identity = np.identity(24)
batch_y_1 = [identity[pickup.find(code[0])] for code in batch_y]
batch_y_2 = [identity[pickup.find(code[1])] for code in batch_y]
batch_y_3 = [identity[pickup.find(code[2])] for code in batch_y]
batch_y_4 = [identity[pickup.find(code[3])] for code in batch_y]

三、关于训练执行模块

传统的话我们应该把我们的数据集分成三部分,训练集,验证集和测试集。是不是听着就很麻烦,不过因为我们的数据集可以是无限大,那就不用这么麻烦了,直接就用训练集做验证了。正常情况来说起码还是得分一个验证集,这个我们后面再讨论。

我们计算了两个准确值accuracy_letter和accuracy,accuracy_letter是计算所有字符的准确度,accuracy是计算组合成字符串后的准确度,例如我们的标准答案是:5CD4,8ACF 两个结果,而我们计算出来的答案是3CE4,8ACF,那么我们accuracy_letter就是6/8=0.75,因为只有第一个5和第三个D算错了,但是accuracy就是1/2=0.5,因为从字符串角度来说,整个第一个字符串识别错了,只有第二个是对的。这里我们关心的实际上是accuracy,但是由于accuracy变动很慢,我们通过观察accuracy_letter来判断我们的模型到底跑怎么样了。

好了,如果大家之前已经把数据爬好了,那么这个代码直接复制到神箭手应用中(本地朋友替换掉read_source代码既可直接运行),再修改数据id(source_id)就可以直接运行了,我们可以从日志中查看准确率:

====================================================================

如果是刚入门的朋友,今天看到这么多代码肯定还是很蒙的。不要紧,大家只需要掌握文章中提到的以下几个要点既可:

1.TF的代码结构

2.核心类Session的使用

3.了解除了图定义以外的其他代码的含义。代码不多,最大的难点是one-hot encoding的转换,大家可以参考代码多写几次就既可,至于为什么要转换成这么诡异的样子,主要是因为这样子更适合训练中的向量计算,当然你也可以理解为大家都说这么做好,所以我们也这么做就行了,其实深度学习中有大量的这种经验性的东西,不太用深究。

目前的代码虽然可以训练模型,但是不会保存,无法继续训练,更无法上线使用。下篇文章将在这篇文章的代码的基础上,完成最后一棒:就是模型的部署上线。

猜你喜欢

转载自blog.csdn.net/ctrigger/article/details/89884762