LSTM+CTC识别变长验证码(人工智能第4次试验)

1. 实验题目

基于RNN——LSTM+CTC的注册码识别实验

2. 实验过程

本次实验一共产生了三个文件

  • generate_verification_code.py:用于产生训练集、验证集、测试集,也就是产生大量验证码
  • train_model.py:训练模型
  • predict_test.py:用训练好的模型来识别新输入的验证码

2.1 产生验证码

本次实验产生验证码的方法与上次直接使用captcha库产生验证码的实验不同,本次使用的是freetype的方法。

对应文件:generate_verification_code.py:

2.1.1 首先导入所需要的包

1
2
3
4
5
6
import string
import numpy as np
import copy
import random
import cv2
import freetype

其中:

  • string在产生验证码内容时会用到,用它来控制产生的验证码包含哪些内容,比如数字、大写字母、小写字母

  • freetype用于对给定的字符产生相应的图片

  • cv2是opencv在python中包的名字,本次实验为了加大难度,使用freetype产生完一张验证码图片后,可以在这张图片的基础之上再加上高斯噪声,这样能加大实验模型的训练难度

    多提一句,当产生的验证码中有噪声时,同样也可以使用opencv的均值模糊、高斯模糊等方法来降低噪声,总之,opencv是一个非常强大的计算机视觉库,它能对图片加噪,也能去噪

  • random库用于产生随机数,因为本实验要求验证码的长度是不确定的

2.1.2 定义put_chinese_text类

put_chinese_text类用于将某一个串字符“画”到图上去

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
class put_chinese_text(object):
def __init__(self, ttf):
self._face = freetype.Face(ttf)

def draw_text(self, image, pos, text, text_size, text_color):
self._face.set_char_size(text_size * 64)
metrics = self._face.size
ascender = metrics.ascender/64.0

ypos = int(ascender)

if not isinstance(text, str):
text = text.decode('utf-8')
img = self.draw_string(image, pos[0], pos[1]+ypos, text, text_color)
return img

def draw_string(self, img, x_pos, y_pos, text, color):
prev_char = 0
pen = freetype.Vector()
pen.x = x_pos << 6 # div 64
pen.y = y_pos << 6

hscale = 1.0
matrix = freetype.Matrix(int(hscale)*0x10000, int(0.2*0x10000),\
int(0.0*0x10000), int(1.1*0x10000))
cur_pen = freetype.Vector()
pen_translate = freetype.Vector()

image = copy.deepcopy(img)
for cur_char in text:
self._face.set_transform(matrix, pen_translate)

self._face.load_char(cur_char)
kerning = self._face.get_kerning(prev_char, cur_char)
pen.x += kerning.x
slot = self._face.glyph
bitmap = slot.bitmap

cur_pen.x = pen.x
cur_pen.y = pen.y - slot.bitmap_top * 64
self.draw_ft_bitmap(image, bitmap, cur_pen, color)

pen.x += slot.advance.x
prev_char = cur_char

return image

def draw_ft_bitmap(self, img, bitmap, pen, color):
x_pos = pen.x >> 6
y_pos = pen.y >> 6
cols = bitmap.width
rows = bitmap.rows

glyph_pixels = bitmap.buffer

for row in range(rows):
for col in range(cols):
if glyph_pixels[row*cols + col] != 0:
img[y_pos + row][x_pos + col][0] = color[0]
img[y_pos + row][x_pos + col][1] = color[1]
img[y_pos + row][x_pos + col][2] = color[2]

2.1.3 定义generateVerificationCode类

generateVerificationCode类用于产生验证码,具体代码如下

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
class generateVerificationCode(object):
def __init__(self,
width=256, # 验证码图片的宽
height=32, # 验证码图片的高
char_max_size=5, # 验证码最多的字符个数
characters=string.digits):# + string.ascii_uppercase + string.ascii_lowercase):#验证码组成,数字+大写字母+小写字母
self.width = width
self.height = height
self.char_max_size = char_max_size
self.characters = characters
self.classes = len(characters) # 一共有多少类
self.char_set = list(self.characters)
self.ft = put_chinese_text('fonts/OCR-B.ttf')

def gen_verification_code(self, is_random=False, batch_size=50):
X = np.zeros([batch_size, self.height, self.width, 3]) # 3个通道
Y = np.zeros([batch_size, self.char_max_size, self.classes]) # one_hot编码
text_list=[]
while True:
for i in range(batch_size):
# 随机设定长度
if is_random == True:
size = random.randint(1, self.char_max_size)
else:
size = self.char_max_size
# 产生size长度的随机串
text = ''.join(random.sample(self.characters, size))
# 保存所有str类型的label
text_list.append(list(text))
# 产生一张图片
img = np.zeros([self.height, self.width, 3]) # 三个通道
color_ = (255,255,255) # Write
pos = (0, 3)
text_size = 25
image = self.ft.draw_text(img, pos, text, text_size, color_)
X[i] = np.reshape(image, [self.height, self.width, 3]) # /255.0 # 将数据全部变换到 [0,1] 范围内
for j, ch in enumerate(text):
Y[i, j, self.characters.find(ch)] = 1
# print("X.shape = ", X.shape)
# print(X)
# print("text_list = ", text_list)
# print("Y.shape = ", Y.shape)
# print(Y)
yield X, text_list, Y

首先看__init__()函数,它一共有5个参数,分别如下:

  • width:设置产生的验证码图片的宽
  • height:设置产生的验证码图片的高
  • char_max_size:设置产生的验证码最多的字符个数, 我这里为了训练速度快一些设成了5
  • characters:设置验证码中字符串的组成,我的代码中设置只产生了数字,可以讲它改为string.digits + string.ascii_uppercase + string.ascii_lowercase,这样就可以产生数字、大写字母、小写字母混合的验证码

再看gen_verification_code()函数,它一共有3各参数,分别如下:

  • is_random:设置验证码的长度是否随机,默认是False,此时产生的验证码为定长验证码,长度为char_max_size
  • batch_size:设置一次产生验证码的张数,例如产生一个batch的训练集时,可以用于指定batch_size

需要特别说明的是,第31行代码使用numpy产生一个shape为[height, width, 3]的矩阵,相当于是生成一张图片,但图片上什么信息都没有,待会调用put_chinese_text类中的方法来将随机字符串text中的内容“画”上去

color_变量用于设置“画”验证码时验证码上的字符颜色,我这里设置成白色了,pos标量用于设置“画”验证码的起点,text_size用于设置验证码中字符的字体大小

参数设置好后,再调用put_chinese_text类中的draw_text()方法来“画”验证码,画好之后的图赋值给image变量

gen_verification_code()函数最后返回的变量一共三个,分别如下:

  • X:shape=[batch_size, width, height],产生的验证码的矩阵形式
  • text_list:一个长度为batch_size的列表,每个元素对应每张图片上的字符串
  • Y:它与text_list的意义相同,只是形式不同,它是text_list的one-hot编码形式

最后,来测试一下函数的执行情况

为了方便查看,我将batch_size设为3,将上面代码的39-43行的打印信息代码的注释打开,再执行下方的代码

1
2
3
4
5
6
if __name__ == '__main__':
genObj = generateVerificationCode()
X, codes, Y = next(genObj.gen_verification_code(is_random=True, batch_size=3))
for i in range(3):
cv2.imshow(str(codes[i]), X[i])
cv2.waitKey(0)

代码中的4-5行使用opencv显示了验证码的图像

结果如下:

除此之外,我还定义了一个生成测试集的gen_test_verification_code()函数,它能够方便的生成100(默认)张图片,保存在你指定的目录dir下,在做测试的时候可以使用opencv库读入他们并导入训练好的模型来做测试,后面测试模型时会用到,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def gen_test_verification_code(self, dir, is_random=False, num=100):
if not os.path.exists(dir):
os.makedirs(dir)
X = np.zeros([self.height, self.width, 3]) # 3个通道
for i in range(num):
# 随机设定长度
if is_random == True:
size = random.randint(1, self.char_max_size)
else:
size = self.char_max_size
# 产生size长度的随机串
text = ''.join(random.sample(self.characters, size))
# 产生一张图片
img = np.zeros([self.height, self.width, 3]) # 三个通道
color_ = (255,255,255) # Write
pos = (0, 3)
text_size = 25
image = self.ft.draw_text(img, pos, text, text_size, color_)
X = np.reshape(image, [self.height, self.width, 3]) # /255.0 # 将数据全部变换到 [0,1] 范围内
cv2.imwrite(dir+'/'+text+'.png', X[:, :, 2])
print("\rGenerating.........(%d/%d)"%(i+1, num), end='')
print("\nTest verification code generated.........")

2.2 训练模型

本次实验采用的网络结构为RNN(循环神经网络)中的LSTM(长短时记忆网络),采用CTC(联结主义时间分类器 ,Connectionist Temporal Classifier)作为损失函数,它适合于输入特征和输出标签之间对齐关系不确定的时间序列问题,CTC可以自动端到端地同时优化模型参数和对齐切分的边界。

对应文件:train_model.py

首先导入相关的包

1
2
3
4
5
from generate_verification_code import *
import numpy as np
import time
import os
import tensorflow as tf

2.2.1 定义一些超参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#图片大小,32 x 256
OUTPUT_SHAPE = (32,256)

# LSTM 循环体个数=64 层数=1
num_hidden = 64
num_layers = 1

obj = generateVerificationCode()
num_classes = obj.classes + 1 + 1 # 10位数字 + blank + ctc blank

#训练最大轮次
num_epochs = 10000

#初始化学习速率
INITIAL_LEARNING_RATE = 1e-3
DECAY_STEPS = 5000
REPORT_STEPS = 100
LEARNING_RATE_DECAY_FACTOR = 0.9 # The learning rate decay factor

DIGITS='0123456789'
BATCHES = 10
BATCH_SIZE = 64
TRAIN_SIZE = BATCHES * BATCH_SIZE

这些超参数在后面用到的时候会陆陆续续提到,这里就不过多解释了,把它放在这里写是因为这些超参数一般都放在文件的头部

2.2.2 关于ctc_loss

上面提到了我们的损失函数是ctc_loss,它在tensorflow中的定义如下:

1
2
3
4
5
6
7
8
9
tf.nn.ctc_loss(
labels,
inputs,
sequence_length,
preprocess_collapse_repeated=False,
ctc_merge_repeated=True,
ignore_longer_outputs_than_inputs=False,
time_major=True
)
  • labels: An int32 SparseTensor,用来传入你训练数据的真实值,要求是int32类型的稀疏矩阵

    稀疏矩阵:在矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律时,则称该矩阵稀疏矩阵;与之相反,若非0元素数目占大多数时,则称该矩阵为稠密矩阵。(百度百科)

    一个稀疏矩阵由三个要素要确定:

    • indices:二维int64的矩阵,代表非0的坐标点
    • values:二维tensor,代表indice位置的数据值
    • dense_shape:一维,代表稀疏矩阵的大小

    举例:

    拿上面产生的三张验证码的字符串’196’、‘76924’和‘58407’来举例,他们的dense tensor形式如下:

    1
    2
    3
    4
    > [[1,9,6,0,0]
    > [7,6,9,2,4]
    > [5,8,4,0,7]]
    >

    >

    把他们转换成系数矩阵的形式就像这样:

    1
    2
    3
    4
    > indecs = [[0,0],[0,1],[0,2],[1,0],[1,1],[1,2],[1,3],[1,4],[2,0],[2,1],[2,2],[2,3],[2,4]]
    > values = [1,9,6,7,6,9,2,4,5,8,4,0,7]
    > dense_shape = [3,5]
    >
  • inputs: 3-D float Tensor,用来传入你模型预测出的结果,要求是一个三维float型的数据结构

    • 如果 time_major = Falseinputs要求的形状为: [batch_size, max_time, num_classes].

    • 如果 time_major = True (默认)inputs要求的形状为: [max_time, batch_size, num_classes].

  • sequence_length: 1-D int32 vector, size [batch_size].它表示一个batch中每个输入LSTM网络的数据的序列的长度,例如本实验中都是256

    格式要求:一维int32类型的向量,内容为[seq_len,…,seq_len],长度为batch_size

了解完ctc_loss之后,发现需要做如下工作:

  • 稀疏矩阵的编码

    我们之前使用generateVerificationCode类中用gen_verification_code()函数产生的数据集,它的返回值中text_list序列列表的形式,并不是SparseTensor的形式,所以我们需要有函数来将它转换成SparseTensor形式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
#转化一个序列列表为稀疏矩阵    
def sparse_tuple_from(sequences, dtype=np.int32):
indices = []
values = []
for n, seq in enumerate(sequences):
indices.extend(zip([n] * len(seq), range(len(seq))))
values.extend(seq)
indices = np.asarray(indices, dtype=np.int64)
values = np.asarray(values, dtype=dtype)
shape = np.asarray([len(sequences), np.asarray(indices).max(0)[1] + 1], dtype=np.int64)
return indices, values, shape
  • 系数矩阵的解码

    SparseTensor形式的数据是ctc_loss的要求,讲预测值与真实值送进ctc_loss做损失计算,但是最后我们在测试阶段需要看预测的数据对不对,这时需要将SparseTensor形式的数据转换回去(序列列表),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def decode_sparse_tensor(sparse_tensor):
decoded_indexes = list()
current_i = 0
current_seq = []
for offset, i_and_index in enumerate(sparse_tensor[0]):
i = i_and_index[0]
if i != current_i:
decoded_indexes.append(current_seq)
current_i = i
current_seq = list()
current_seq.append(offset)
decoded_indexes.append(current_seq)
result = []
for index in decoded_indexes:
result.append(decode_a_seq(index, sparse_tensor))
return result

def decode_a_seq(indexes, spars_tensor):
decoded = []
for m in indexes:
str = DIGITS[spars_tensor[1][m]]
decoded.append(str)
return decoded

2.2.3 训练数据集的生成

前面已经有生成验证码的函数了,为什么这里还要写生成训练数据集的函数呢?

原因前面已经讨论到了,即使用generateVerificationCode类中用gen_verification_code()函数产生的数据集的label(标签)并不符合ctc_loss的格式要求,上面也只是定义了可以将序列列表的形式的标签转换成SparseTensor的形式标签的函数,还没有调用,现在做的工作是调用上面的函数来生成数据格式符合ctc_loss要求的训练集,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生成一个训练batch
def get_next_batch(batch_size=128, is_random=True):
obj = generateVerificationCode()
# X.shape=[batch_size, height, width, 3]
# 获取一个batch的数据集
# codes为一个列表,所有str类型的label
X, text_list, Y = next(obj.gen_verification_code(is_random=is_random, batch_size=batch_size))
# 这一步的作用:改变形状[batch_size, height, width, 1]=>[batch_size, height, width]=>[batch_size, width, height]
X = np.transpose(np.reshape(X[:,:,:,2], [batch_size,OUTPUT_SHAPE[0],OUTPUT_SHAPE[1]]), (0, 2, 1))
#targets转成稀疏矩阵
# 返回三个值,非零元素的位置信息,非零元素对应的值,稀疏矩阵的形状
sparse_targets = sparse_tuple_from(text_list)
#(batch_size,) sequence_length值都是256,最大划分列数
seq_len = np.ones(X.shape[0]) * OUTPUT_SHAPE[1]
return X, sparse_targets, seq_len

这个函数的第9行将验证码的颜色变成了单通道,原因也是我希望训练速度能快一些

函数里面涉及到了很多数据格式的转换,在本实验中的总体的数据流如下:

gen_verification_code()—->[batch_size, seq_len, num_features]

—->LSTM—->[batch_size, seq_len, cell.output_size]—->reshape

—->[batch_size*seq_len, num_hidden]—->affine projection A*W+b

—->[batch_size*seq_len, num_classes]—->reshape

—->[batch_size, seq_len, num_classes]—->transpose

—->[seq_len, batch_size, num_classes]

至于为什么要做这么繁琐的数据转换,下面马上要提到啦

2.2.4 定义LSTM网络模型

整体的网络结构是:输入单元—>64个隐藏单元(循环体)—>12个输出单元

所以在第10行将得到的LSTM输出值reshape成[batch_size\*seq_len, num_hidden]之后,又继续将他们连接到了输出层的12个神经元,最终返回的logits是reshape成[seq_len, batch_size, num_classes]之后的数据

为什么要这样一直reshape?

因为这个返回的logis即将要作为inputs数据被喂送到ctc_loss里面啦,上面已经写到了ctc_lossinputs需要[max_time, batch_size, num_classes]这样的数据格式

涉及到的超参数:

  • num_hidden = 64
  • num_layers = 1

所以这个LSTM网络中间循环体的个数是64个,层数是1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def RNN(inputs, seq_len):
#定义LSTM网络
cell = tf.contrib.rnn.LSTMCell(num_hidden, state_is_tuple=True)
stack = tf.contrib.rnn.MultiRNNCell([cell] * num_layers, state_is_tuple=True)
#output的shape=(64,256,64)(batch_size*256条*64个output)[batch_size,seq_len,cell.output_size]
outputs, _ = tf.nn.dynamic_rnn(cell, inputs, seq_len, dtype=tf.float32)
shape = tf.shape(inputs)
batch_s, max_timesteps = shape[0], shape[1]
# 为方便矩阵运算,reshape一下outputs,outputs的size是(64*256,64)
outputs = tf.reshape(outputs, [-1, num_hidden])
# 初始化权值 生成一个截断的正态分布
weights = tf.Variable(tf.truncated_normal([num_hidden, num_classes], stddev=0.1), name="W")
bias = tf.Variable(tf.constant(0., shape=[num_classes]), name="b")
# 计算结果
# result形状:[64*256, 12]
logits = tf.matmul(outputs, weights) + bias
# reshape之后logits的shape是(64,256,12) [batch_size,seq_len,num_classes]
logits = tf.reshape(logits, [tf.shape(inputs)[0], -1, num_classes])
#交换坐标轴,axis0和axis1互换,logits的shape是(256,64,12) [seq_len,batch_size,num_classes]
logits = tf.transpose(logits, (1, 0, 2))
return logits

2.2.5 开始训练

在开始训练之前,先定义一个函数来检测待会儿模型预测结果的正确率,并且打印出预测的结果和真实的label

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def do_report():
test_inputs,test_targets,test_seq_len = get_next_batch(BATCH_SIZE)
test_feed = {inputs: test_inputs, targets: test_targets, seq_len: test_seq_len}
decoded_list, log_probs, accuracy = session.run([decoded[0], log_prob, acc], test_feed)

original_list = decode_sparse_tensor(test_targets)
detected_list = decode_sparse_tensor(decoded_list)

true_numer = 0
if len(original_list) != len(detected_list):
print("len(original_list)", len(original_list), "len(detected_list)", len(detected_list),
" test and detect length desn't match")
return -1
print("T/F: original(length) <-------> detectcted(length)")
for idx, number in enumerate(original_list):
detect_number = detected_list[idx]
hit = (number == detect_number)
print(hit, number, "(", len(number), ") <-------> ", detect_number, "(", len(detect_number), ")")
if hit:
true_numer = true_numer + 1
Accuracy = true_numer * 1.0 / len(original_list)
print("Test Accuracy:", Accuracy)
return Accuracy

接下来看开始写训练的代码

涉及到的超参数:

  • epoch:训练的轮数,由前面的num_epochs = 10000控制,表示最多训练10000轮
  • BATCHES:表示一轮训练里batch的数量,本实验中我设置的是10
  • BATCH_SIZE:表示每个batch中的验证码个数
  • TRAIN_SIZE:表示一个epoch训练下来,一共使用了多少张验证码
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
if __name__ == '__main__':
# 用来记录全局step,每更新一次参数就会+1
global_step = tf.Variable(0, trainable=False)
inputs = tf.placeholder(tf.float32, [None, OUTPUT_SHAPE[1], OUTPUT_SHAPE[0]])
#targets是标签(稀疏矩阵的形式),因为定义ctc_loss需要的稀疏矩阵
targets = tf.sparse_placeholder(tf.int32)
#1维向量 序列长度 [batch_size,]
#是一个长度=BATCH_SIZE=64的一维向量,每个里面都是256,表示一个batch中每一张有256条输入序列
seq_len = tf.placeholder(tf.int32, [None])
# 定义指数下降的学习率
learning_rate = tf.train.exponential_decay(INITIAL_LEARNING_RATE, global_step, DECAY_STEPS, LEARNING_RATE_DECAY_FACTOR, staircase=True)
# 调用前面定义好的模型,并传入值
logits = RNN(inputs, seq_len)
# 定义CTC损失函数 需要喂入数据:targets,logits,seq_len
cost = tf.reduce_mean(tf.nn.ctc_loss(labels=targets, inputs=logits, sequence_length=seq_len))
# 使用AdamOptimizer进行优化
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost, global_step=global_step)
# 解码,获得结果
decoded, log_prob = tf.nn.ctc_beam_search_decoder(logits, seq_len, merge_repeated=False)
# 求准确率
acc = tf.reduce_mean(tf.edit_distance(tf.cast(decoded[0], tf.int32), targets))

with tf.Session() as session:
session.run(tf.global_variables_initializer())
# 载入模型
saver = tf.train.Saver(tf.global_variables(), max_to_keep=3)
ckpt_dir = "./ckpt_dir"
if not os.path.exists(ckpt_dir):
os.makedirs(ckpt_dir)
ckpt = tf.train.get_checkpoint_state(ckpt_dir)
if ckpt and ckpt.model_checkpoint_path:
saver.restore(session, ckpt.model_checkpoint_path)
print("Restore succcess! path:", ckpt.model_checkpoint_path)

# 开始训练
# 定义训练结束的标志,当正确率达到某一值时,可以通过设置它来提前结束训练
train_end_flag = False
start_time = time.time() # 记录时间
for curr_epoch in range(num_epochs):
print("Epoch.......", curr_epoch)
train_cost = 0
for batch in range(BATCHES):
b_start_time = time.time()
# 产生mini-batch=64的训练集数据
train_inputs, train_targets, train_seq_len = get_next_batch(BATCH_SIZE)
feed = {inputs: train_inputs, targets: train_targets, seq_len: train_seq_len}
train_acc, _, b_cost, steps, = session.run([acc, optimizer, cost, global_step], feed)
train_cost += b_cost
print("Step:", steps, ", batch_time:", time.time()-b_start_time, 's')
# 每REPORT_STEPS个step测试一下正确率,并打印预测结果和真实label
if steps > 0 and steps % REPORT_STEPS == 0:
if(do_report()>0.9):
save_path = saver.save(session, ckpt_dir+"/model.ckpt", global_step=steps)
print("save succcess! path:", save_path)
print("=============================>Train succcess, Time:", time.time()-start_time, 's!')
# 如果正确率超过了0.9就可以提起停止训练啦
train_end_flag = True
break
# 提前停止训练
if train_end_flag:
break
# 每过一个epoch,计算一个训练误差
train_cost /= BATCHES
# 每过一个epoch,调用一下验证集测试
val_inputs, val_targets, val_seq_len = get_next_batch(BATCH_SIZE)
val_feed = {inputs: val_inputs, targets: val_targets, seq_len: val_seq_len}
val_cost, vald_acc, lr, steps = session.run([cost, acc, learning_rate, global_step], feed_dict=val_feed)
log = "Epoch {}/{}, steps = {}, train_cost = {:.3f}, val_cost = {:.3f}, time = {:.3f}s, learning_rate = {}"
print(log.format(curr_epoch + 1, num_epochs, steps, train_cost, val_cost, time.time()-b_start_time, lr))

2.3 测试模型

对应文件:predict_test.py

首先导入相关的包

1
2
3
4
5
6
7
# 首先导入需要的库
import tensorflow as tf
import cv2
import sys
import os
from generate_verification_code import *
from train_model import *

调用gen_test_verification_code()函数产生100张验证码

1
2
3
4
5
# 设置产生测试集文件夹的名字
Testing_set_dir = 'TestingSet'
obj = generateVerificationCode()
# 生成验证码
obj.gen_test_verification_code(Testing_set_dir, is_random=True, num=100)

生成验证码后可以打开TestingSet目录查看看生成的验证码

虽然希望产生的是100张验证码,但是可能在产生的过程中会有重名的情况,这样就把之前的验证码覆盖掉了,所以最终产生的测试集数量可能会小于100

gen_test_verification_code()函数生成的验证码使用该验证码里面的字符命名,这样可以方便读入验证码的时候一起读入他们的label

下方的代码是使用opencv读入刚才产生的验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 获取这个目录里的所有图片名字列表
file_list = os.listdir(Testing_set_dir) # 获取此路径下的所有文件的名字列表
# print(file_list)
# 将100个训练样本打包到test_x
test_x = np.zeros([len(file_list), obj.height, obj.width]) # 1个通道
text_list = []
for i, file in enumerate(file_list):
image = cv2.imread(Testing_set_dir + '/' + file)
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
test_x[i] = gray_image
text = list(file)[:-4] # remove '.png'
text_list.append(text)
# [num, height, width]=>[num, width, height]
test_x = np.transpose(test_x, (0, 2, 1))
test_targets = sparse_tuple_from(text_list)
test_seq_len = np.ones(test_x.shape[0]) * test_x.shape[1]

跟之前产生训练集一样,需要对读入的图片的数据做相应的数据变换(opencv读入后的数据格式为numpyarray

最后产生的数据保存在test_xtest_targetstest_seq_len

  • test_x:shape=[num, width, height]
  • test_targets:稀疏矩阵形式的label
  • test_seq_len:LSTM网络的时间序列长度,也就是每张图片的长度

测试数据产生完后,接下来开始测试训练好的模型

1
2
3
4
5
6
7
8
9
10
11
# 定义一些placeholder作为输入数据的入口
inputs = tf.placeholder(tf.float32, [None, OUTPUT_SHAPE[1], OUTPUT_SHAPE[0]])
#targets是标签(稀疏矩阵的形式),因为定义ctc_loss需要的稀疏矩阵
targets = tf.sparse_placeholder(tf.int32)
#1维向量 序列长度 [batch_size,]
#是一个长度=BATCH_SIZE=64的一维向量,每个里面都是256,表示一个batch中每一张有256条输入序列
seq_len = tf.placeholder(tf.int32, [None])
# 调用前面定义好的模型,并传入值
logits = RNN(inputs, seq_len)
# 解码,获得结果
decoded, log_prob = tf.nn.ctc_beam_search_decoder(logits, seq_len, merge_repeated=False)

定义会话开始测试

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
with tf.Session() as session:
session.run(tf.global_variables_initializer())
# 载入模型
saver = tf.train.Saver(tf.global_variables(), max_to_keep=3)
ckpt_dir = "./ckpt_dir"
if not os.path.exists(ckpt_dir):
os.makedirs(ckpt_dir)
ckpt = tf.train.get_checkpoint_state(ckpt_dir)
if ckpt and ckpt.model_checkpoint_path:
saver.restore(session, ckpt.model_checkpoint_path)
print("Restore succcess! path:", ckpt.model_checkpoint_path)
else:
# 如果没有找到保存的模型参数,打印出错信息
print("Error:Model not found")

# 开始喂入测试数据进行预测,结果保存到pre_list列表中
test_feed = {inputs: test_x, targets: test_targets, seq_len: test_seq_len}
decoded_list = session.run(decoded[0], test_feed)
# 解码并开始作对比
original_list = decode_sparse_tensor(test_targets)
detected_list = decode_sparse_tensor(decoded_list)
true_numer = 0
if len(original_list) != len(detected_list):
print("len(original_list)", len(original_list), "len(detected_list)", len(detected_list),
" test and detect length desn't match")
print("T/F: original(length) <-------> detectcted(length)")
for idx, number in enumerate(original_list):
detect_number = detected_list[idx]
hit = (number == detect_number)
print(hit, number, "(", len(number), ") <-------> ", detect_number, "(", len(detect_number), ")")
if hit:
true_numer = true_numer + 1
Accuracy = true_numer * 1.0 / len(original_list)
print("Test Accuracy:", Accuracy)

我的测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Generating.........(100/100)
Test verification code generated
Restore succcess! path: ./ckpt_dir/model.ckpt-1900
T/F: original(length) <-------> detectcted(length)
True ['8', '4'] ( 2 ) <-------> ['8', '4'] ( 2 )
True ['0', '9', '4', '3'] ( 4 ) <-------> ['0', '9', '4', '3'] ( 4 )
True ['0'] ( 1 ) <-------> ['0'] ( 1 )
True ['8', '2'] ( 2 ) <-------> ['8', '2'] ( 2 )
True ['5', '0', '6', '4', '1'] ( 5 ) <-------> ['5', '0', '6', '4', '1'] ( 5 )
...(省略)
True ['1', '7', '8', '2', '4'] ( 5 ) <-------> ['1', '7', '8', '2', '4'] ( 5 )
True ['0', '8', '1', '4', '9'] ( 5 ) <-------> ['0', '8', '1', '4', '9'] ( 5 )
True ['1', '5'] ( 2 ) <-------> ['1', '5'] ( 2 )
Test Accuracy: 1.0

2.4 遇到的问题

unicode编码报错:NameError: name 'unicode' is not defined

错误原因:在python3中才会报此错误,所以应该是ython版本不兼容所致,python3将unicode改为str

解决办法:如果你使用的是python3,将unicode改为str即可解决

参考资料:

Tensorflow官网(可能需要科学上网)

http://ilovin.me/2017-04-23/tensorflow-lstm-ctc-input-output/

https://www.jianshu.com/p/45828b18f133

https://github.com/jimmyleaf/ocr_tensorflow_cnn

如果你觉得此页面对你有帮助,或者想资瓷我一下,欢迎点击下面打赏哦,谢谢~