出品 | 磐创AI团队
字符识别是一种经典的模式识别问题,字符识别在现实生活中也有着非常广泛的应用,目前对于特定环境下的拉丁字符识别已经取得了很好的效果,但是对于一些复杂场景下的字符识别依然还有很多困难,例如通过手持设备拍摄以及自然场景中的图片等,Chars74K正是针对这些困难点搜集的数据集(http://www.ee.surrey.ac.uk/CVSSP/demos/chars74k/)
Chars74K包含英语和坎那达语(Kannada)两种字符,在英文数据集中包括26个拉丁文字母和10个阿拉伯数字,整个英文数据集包括64种字符(0-9,a-z,A-Z),英文数据集根据采集方式又有三种不同数据集(三种英文数据集的样本数加在一起超过了74K,Chars74K的名字也是由此而来。):
1) 7705个自然图像中采集的字符数据集(EnglishImg.tgz);
2) 3410个在平板电脑上手写的字符数据集(EnglishHnd.tgz);
3) 62992个在从计算机字体合成的字符数据集(EnglishFnt.tgz)。
在本项目中我们使用第一个数据集,即从自然场景中采集到的字符数据集,部分数据如图1所示。
图1 Chars74K数据集示例(自然场景中采集的英文字符数据集)
数据集解压之后的目录结构如图2所示,解压之后的数据集包括“BadImg”和“GoodImg”,“BadImg”中的图片质量较差,因此我们使用“GoodImg”。数据集中每一个类别的图片单独放在一个文件夹中,如图2右所示。
图2 Chars74K数据集(自然场景中采集的英文字符数据集)
数据预处理
Chars74K数据集(自然场景中采集的英文字符数据集,本项目中后续提到的Chars74K数据集一律特指该数据集)里的图片大小不一,因此我们需要将其调整为统一大小,调整图像大小的代码可以在本书配套的GitHub项目中找到,这里不做介绍。另外需要注意,原始数据集中混杂了4张单通道的灰度图,需要删除这4张图片。这里可以直接使用作者处理好的数据集。
下载地址:https://pan.baidu.com/s/1KP5JRO-M87fN-93VOsSCag
接下来我们开始实现数据处理部分,首先导入需要的包:
1 import tensorflow as tf
2 from tensorflow.keras import layers
3 import datetime
4 import numpy as np
5 from PIL import Image
6 import os
接着我们定义一个“get_dataset”函数用来获取数据集:
7 def get_dataset(path):
8 """获取数据集"""
9 data_x = []
10 data_y = []
11
12 # 获取当前路径下所有文件夹(或文件)
13 folder_name = os.listdir(path)
14
15 # 循环遍历每个文件夹
16 for i in folder_name:
17 file_path = os.path.join(path, i)
18
19 # 取文件夹名后三位整数作为类标
20 label = int(i[-3:])
21
22 # 获取当前文件夹下的所有图片文件
23 filenames = os.listdir(file_path)
24
25 for filename in filenames:
26 # 组合得到每张图片的路径
27 image_path = os.path.join(file_path, filename)
28
29 # 读取图片
30 image = Image.open(image_path)
31 # 将image对象转为numpy数组
32 width, height = image.size
33 image_matrix = np.reshape(image, [width*height*3])
34
35 data_x.append(image_matrix)
36 data_y.append(label)
37
38 return data_x, data_y
第33行代码中我们将图片转换成了numpy数组,由于我们的图像是三通道的RGB图像,因此转换后的数组大小为“width * height * 3”。
模型搭建
在本项目中我们将使用VGG-Net网络模型。VGG-Net有多种级别,其网络层数从11层到19层不等(这里的层数是指有参数更新的层,例如卷积层或全连接层),其中比较常用的是16层(VGG-Net-16)和19层(VGG-Net-19)。如图3所示是VGG-Net-16的网络结构。
图3 VGG-Net-16网络结构
VGG-Net中全部使用大小为3X3的小卷积核,希望模拟出更大的“感受野”效果,VGG-Net中的池化层均使用的是大小为2X2的最大池化。VGG-Net的设计思想在ResNet和Inception模型中也都有被采用。图4所示是不同层数的VGG-Net。
图4 不同层数的VGG-Net
本项目中我们使用的是VGG-Net-13,具体实现如下:
39 def vgg13_model(input_shape, classes):
40 model = tf.keras.Sequential()
41
42 model.add(layers.Conv2D(64, 3, 1, input_shape=input_shape,
43 padding='same',
44 activation='relu',
45 kernel_initializer='uniform'))
46 model.add(layers.Conv2D(64, 3, 1, padding='same',
47 activation='relu',
48 kernel_initializer='uniform'))
49 model.add(layers.MaxPooling2D(pool_size=(2, 2)))
50
51 model.add(layers.Conv2D(128, 3, 1, padding='same',
52 activation='relu',
53 kernel_initializer='uniform'))
54 model.add(layers.Conv2D(128, 3, 1, padding='same',
55 activation='relu',
56 kernel_initializer='uniform'))
57 model.add(layers.MaxPooling2D(pool_size=(2, 2)))
58
59 model.add(layers.Conv2D(256, 3, 1, padding='same',
60 activation='relu',
61 kernel_initializer='uniform'))
62 model.add(layers.Conv2D(256, 3, 1, padding='same',
63 activation='relu',
64 kernel_initializer='uniform'))
65 model.add(layers.MaxPooling2D(pool_size=(2, 2)))
66
67 model.add(layers.Conv2D(512, 3, 1, padding='same',
68 activation='relu',
69 kernel_initializer='uniform'))
70 model.add(layers.Conv2D(512, 3, 1, padding='same',
71 activation='relu',
72 kernel_initializer='uniform'))
73 model.add(layers.MaxPooling2D(pool_size=(2, 2)))
74
75 model.add(layers.Conv2D(512, 3, 1, padding='same',
76 activation='relu',
77 kernel_initializer='uniform'))
78 model.add(layers.Conv2D(512, 3, 1, padding='same',
79 activation='relu',
80 kernel_initializer='uniform'))
81 model.add(layers.MaxPooling2D(pool_size=(2, 2)))
82
83 model.add(layers.Flatten())
84 model.add(layers.Dense(4096, activation='relu'))
85
86 model.add(layers.Dropout(0.5))
87 model.add(layers.Dense(4096, activation='relu'))
88
89 model.add(layers.Dropout(0.5))
90 model.add(layers.Dense(classes, activation='softmax'))
91
92 # 模型编译
93 model.compile(loss='categorical_crossentropy',
94 optimizer='sgd',
95 metrics=['accuracy'])
96 return model
模型训练
定义好模型后我们加载数据集并开始训练:
97 if __name__ == '__main__':
98 path = './chars74k_data'
99 data_x, data_y = get_dataset(path)
100
101 train_x = np.array(data_x).reshape(-1, 224, 224, 3)
102 train_y = [i - 1 for i in data_y]
103 train_y = tf.keras.utils.to_categorical(train_y, 62)
104
105 # 随机打乱数据集顺序
106 np.random.seed(116)
107 np.random.shuffle(train_x)
108 np.random.seed(116)
109 np.random.shuffle(train_y)
110
111 cnn_model = vgg13_model(input_shape=(224,224,3), classes=62)
112 cnn_model.summary()
113
114 # 设置TensorBoard
115 log_dir="logs/fit/"+datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
116 tensorboard_callback=tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1)
117
118 # 当验证集上的loss不再下降时就提前结束训练
119 early_stop=tf.keras.callbacks.EarlyStopping(monitor='val_loss', min_delta=0.002, patience=10, mode='auto')
120
121 callbacks = [tensorboard_callback, early_stop]
122 cnn_model.fit(train_x, train_y,
123 batch_size=100, epochs=300,
124 verbose=1, validation_split=0.2,
125 callbacks=callbacks)
在第103行代码中,由于我们之前根据目录名得到的类标是从“1”开始的,因此需要对所有类标减1,让类标从“0”开始,以便在第104代码中将类标转换为one-hot编码。
第120行代码中,我们设置了一个callback函数“EarlyStopping”,该函数可以用来设置模型自动停止训练的条件。例如这里我们设置当“val_loss”的值有10次变化不超过0.002时则提前停止训练。参数“monitor”是要监测的指标;“min_delta”是监测指标的最小变化值;“patience”是没有变化的训练回合数;“mode”有三个值,分别是“auto”、“min”和“max”,当“mode”设置为“min”时,如果监测的指标有“patience”次没有达到“min_delta”的变化量,则停止训练,“max”同理。模型训练的结果如图5所示:
图5 训练过程中的Accuracy和Loss变化(红色为训练集,蓝色为验证集)