Chars74K—CNN实战项目

字符识别是经典的模式识别问题,在现实生活中也有着非常广泛的应用。目前对于特定环境下拉丁字符的识别已经取得了很好的效果,但是对于一些复杂场景下的字符识别依然还有很多困难,例如通过手持设备拍摄及自然场景中的图片等。Chars74K 正是针对这些困难点而搜集的数据集。

Chars74K 包含英语和坎那达语(Kannada)两种字符,其中,英语数据集包括 64 种字符(0~9、a~z,A~Z),有 26 个拉丁文字母和 10 个阿拉伯数字,根据采集方式的不同又分成三个不同数据集(三个英文数据集的样本数加在一起超过了 74KB,Chars74K 的名字也是由此而来的):
  1. 7705 个从自然场景中采集的字符数据集(EnglishImg.tgz);
  2. 3410 个在平板电脑上手写的字符数据集(EnglishHnd.tgz);
  3. 62992 个从计算机字体合成的字符数据集(EnglishFnt.tgz)。

本项目用的是第一个数据集,即从自然场景中采集的字符数据集,部分数据如图 1 所示。
Chars74K 数据集示例(从自然场景中采集的英文字符数据集)
图 1:Chars74K 数据集示例(从自然场景中采集的英文字符数据集)

数据集解压之后的目录结构如图 2 左所示,解压之后的数据集包括“BadImg”和“GoodImg”,而“BadImg”中的图片质量较差,数据集中每一个类别的图片单独放在一个文件夹中,如图 2 右所示。
Chars74K 数据集(自然场景中采集的英文字符数据集)
图 2:Chars74K 数据集示例(从自然场景中采集的英文字符数据集)

1. 数据预处理

Chars74K 数据集(本项目中后续提到的 Chars74K 数据集一律特指从自然场景中采集的英文字符数据集)里的图片大小不一,因此我们需要将其调整为统一大小,还要删除原始数据集中混杂的 4  张单通道的灰度图,读者可直接使用已处理好的数据集。

接下来我们开始实现数据预处理部分。首先导入需要的包:
import tensorflow as tf
from tensorflow.keras import layers
import datetime
import numpy as np
from PIL import Image
import os

接着定义一个“get_dataset”函数,用来获取数据集:
def get_dataset(path):
    """获取数据集"""
    data_x =[]
    data_y =[]

    #获取当前路径下所有文件夹(或文件)
    folder_name = os.listdir(path)

    #循环遍历每个文件夹
    for i in folder_name:
        file_path = os.path.join(path, i)
        #取文件夹名后三位整数作为类标
        label = int (i [-3:])
        #获取当前文件夹下的所有图片文件
        filenames = os.listdir(file_path)
        for filename in filenames:
            #组合得到每张图片的路径
            image_path = os.path.join(file_path, filename)

            #读取图片
            image = Image.open(image_path)
            #将image对象转为NumPy数组
            width, height = image.size
            image_matrix = np.reshape(image, [width*height*3])

            data_x.append(image_matrix)
            data_y.append(label)

    return data_x, data_y
第 24 行代码将图片转换成了 NumPy 数组,由于图片是 RGB 三通道模式的,因此转换后的数组大小为“width * height *3”。

2. 模型搭建

本项目将使用 VGG-Net 网络模型。VGG-Net 有多种级别,其网络层数从 11 层到 19 层不等(这里的层数是指有参数更新的层,例如卷积层或全连接层),其中比较常用的是 16 层(VGG-Net-16)和 19 层(VGG-Net-19)。图 3 所示的是 VGG-Net-16 网络结构。
VGG-Net-16网络结构
图 3:VGG-Net-16网络结构

VGG-Net 中全部使用大小为 3×3 的小卷积核,希望模拟出更大的“感受野”效果。VGG-Net 中的池化层使用的均是大小为 2×2 的最大池化。VGG-Net 的设计思想在 ResNet 和 Inception 模型中也都有被采用。图 4 所示的是不同层数的 VGG-Net。
不同层数的VGG-Net
图 4:不同层数的VGG-Net

本项目使用的是 VGG-Net-13,具体实现如下:
def vgg13_model(input_shape, classes):
    model = tf.keras.Sequential()
    model.add(layers.Conv2D(64, 3, 1, input_shape=input_shape,
                            padding= 'same',
                            activation= 'relu',
                            kernel_initializer='uniform'))
    model.add(layers.Conv2D(64, 3, 1, padding='same',
                            activation='relu',
                            kernel_initializer= 'uniform'))
    model.add(layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(layers.Conv2D(128, 3, 1, padding= 'same ',
                            activation='relu',
                            kernel_initializer='uniform'))
    model.add(layers.Conv2D(128, 3, 1, padding= 'same',
                            activation= 'relu',
                            kernel_initializer= 'uniform'))
    model.add(layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(layers.Conv2D(256, 3, 1, padding= 'same',
                            activation= 'relu',
                            kernel_initializer='uniform'))
    model.add(layers.Conv2D(256, 3, 1, padding='same',
                            activation= 'relu',
                            kernel_initializer= 'uniform'))
    model.add(layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(layers.Conv2D(512, 3, 1, padding='same',
                            activation= 'relu',
                            kernel_initializer='uniform'))
    model.add(layers.Conv2D(512, 3, 1, padding= 'same',
                            activation= 'relu',
                            kernel_initializer= 'uniform'))
    model.add(layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(layers.Conv2D(512, 3, 1, padding='same',
                            activation= 'relu',
                            kernel_initializer= 'uniform'))
    model.add(layers.Conv2D(512, 3, 1, padding= 'same',
                            activation= 'relu',
                            kernel_initializer='uniform'))
    model.add(layers.MaxPooling2D(pool_size=(2, 2)))
    model.add(layers.Flatten())
    model.add(layers.Dense(40 96, activation= 'relu'))
    model.add(layers.Dropout(0.5))

    model.add(layers.Dense(4096, activation= 'relu'))
    model.add(layers.Dropout(0.5))
    model.add(layers.Dense(classes, activation= 'softmax'))
    #模型编译
    model.compile(loss='categorical_crossentropy',
                  optimizer= 'sgd',
                  metrics=['accuracy'])
    return model

3. 模型训练

定义好模型后开始加载数据集并训练:
if __name__ == '__main__' :
    path = './chars74k_data'
    data_x, data_y = get_dataset(path)
    train_x = np.array(data_x).reshape(-1, 224, 224, 3)
    train_y = [i - 1 for i in data_y]
    train_y = tf.keras.utils.to_categorical(train_y, 62)
    #随机打乱数据集顺序
    np.random.seed(116)
    np.random.shuffle(train_x)
    np.random.seed(116)
    np.random.shuffle(train_y)

    cnn_model = vgg13_model(input_shape=(224,224, 3), classes=62)
    cnn_model.summary()

    # 设置 TensorBoard
    log_dir="logs/fit/"+datetime.datetime.now().strftime("%Y%m%d-%H%M%S") tensorboard_callback=tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1)

    #当验证集上的损失不再下降时就提前结束训练
early_stop=tf.keras.callbacks.EarlyStopping(monitor= 'val_loss', min_delta=0.002, patience=10, mode='auto')
    callbacks = [tensorboard_callback, early_stop]
    cnn_model.fit(train_x, train_y,
                  batch_size=100, epochs=300,
                  verbose=1, validation_split=0.2,
                  callbacks=callbacks)
在第 5 行代码中,由于我们之前根据目录名得到的类标是从“1”开始的,因此需要对所有类标减 1,让类标从“0”开始,以便在第 6 代码中将类标转换为 One-Hot 编码。

在第 21 行代码中,我们设置了一个 callback 函数,用来设置模型提前停止训练的条件,例如这里设置当“val_loss”的值有 10 次变化不超过 0.002 时则提前停止训练。

参数具体介绍如下:
  • “monitor”是要监测的指标;
  • “min_delta”是监测指标的最小变化值;
  • “patience”是没有变化的训练回合数;
  • “mode”有三个值,分别是“auto”“min”和“max”。当“mode”设置为“min”时,如果监测指标有“patience”次没有达到“min_delta”的变化量,则停止训练。“max”同理。

模型训练的结果如图 5 所示。
训练过程中的正确率和损失的变化
图 1:训练过程中的正确率和损失的变化