一、简介

本次实验的任务是汉字识别。使用pytorch深度学习框架和HWDB手写汉字数据集进行实验。

由于数据集过于庞大,这里只选取了前500个类作为实验。

二、开发环境

目前主流的神经网络框架有Tensorflow,Pytorch,MXNET,Keras等。本次实验使用Pytroch深度学习框架。PyTorch看作加入了GPU支持的numpy,并且它是一个拥有自动求导功能的强大的深度神经网络。

三、HWDB数据集

3.1 简介

HWDB是一个手写汉字数据集,该数据集来自于中科院自动化研究所,一共有三个版本,分别为HWDB1.0、HWDB1.1和HWDB1.2。每个版本的详情如图所示。图中writers表示写字的人。

手写字体识别python_深度学习

本次实验使用HWDB1.1数据集,共计1,176,000张图像。该数据集由300个人手写而成,其中包含171个阿拉伯数字和特殊符号,3755类GB2312-80 level-1汉字。下图为样本:

手写字体识别python_手写字体识别python_02

训练集含有240个writers写的字,一共119516张图片。
测试集含有另外60个writers写的字,一共29859张图片。

3.2 数据集下载

官网:http://www.nlpr.ia.ac.cn/databases/handwriting/Offline_database.html
训练集:http://www.nlpr.ia.ac.cn/databases/download/feature_data/HWDB1.1trn_gnt.zip
测试集:http://www.nlpr.ia.ac.cn/databases/download/feature_data/HWDB1.1tst_gnt.zip

3.3 解析数据集

下载的数据集并非图片格式,而是gnt的自定义文件类型,其gnt文件格式如下:

手写字体识别python_模式识别_03

因此,需要将gnt文件转换为对应label目录下的所有png图片。

第一步:

将下载的zip文件解压成gnt文件。

注意:HWDB1.1trn_gnt.zip解压后是alz文件,需要再次解压alz文件。

解压alz文件:
Windows:需下载软件 https://alzip.en.softonic.com/download
Linux:unalz HWDB1.1trn_gnt.alz

第二步:

将gnt文件转换为对应label目录下的所有png图片。将路径需修改好就可以运行程序了。

在运行程序之前,需要创建存放png图片的文件夹,否则会报错。这里需要等待一段时间。

import os
import numpy as np
import struct
from PIL import Image

data_dir = '/home/malidong/workspace/PatternRecognition/Chinese_character_recognition-master'
train_data_dir = os.path.join(data_dir, 'HWDB1.1trn_gnt')
test_data_dir = os.path.join(data_dir, 'HWDB1.1tst_gnt')

def read_from_gnt_dir(gnt_dir=train_data_dir):
    def one_file(f):
        header_size = 10
        while True:
            header = np.fromfile(f, dtype='uint8', count=header_size)
            if not header.size: break
            sample_size = header[0] + (header[1]<<8) + (header[2]<<16) + (header[3]<<24)
            tagcode = header[5] + (header[4]<<8)
            width = header[6] + (header[7]<<8)
            height = header[8] + (header[9]<<8)
            if header_size + width*height != sample_size:
                break
            image = np.fromfile(f, dtype='uint8', count=width*height).reshape((height, width))
            yield image, tagcode
    for file_name in os.listdir(gnt_dir):
        if file_name.endswith('.gnt'):
            file_path = os.path.join(gnt_dir, file_name)
            with open(file_path, 'rb') as f:
                for image, tagcode in one_file(f):
                    yield image, tagcode
char_set = set()
for _, tagcode in read_from_gnt_dir(gnt_dir=train_data_dir):
    tagcode_unicode = struct.pack('>H', tagcode).decode('gb2312')
    char_set.add(tagcode_unicode)
char_list = list(char_set)
char_dict = dict(zip(sorted(char_list), range(len(char_list))))
print(len(char_dict))
import pickle
f = open('char_dict', 'wb')
pickle.dump(char_dict, f)
f.close()
train_counter = 0
test_counter = 0
for image, tagcode in read_from_gnt_dir(gnt_dir=train_data_dir):
    tagcode_unicode = struct.pack('>H', tagcode).decode('gb2312')
    im = Image.fromarray(image)
    dir_name = '/home/malidong/workspace/PatternRecognition/Chinese_character_recognition-master/data/train/' + '%0.5d'%char_dict[tagcode_unicode]
    if not os.path.exists(os.path.join(dir_name)):
        os.mkdir(os.path.join(dir_name))
    im.convert('RGB').save(os.path.join(dir_name)+'/' + str(train_counter) + '.png')
    train_counter += 1
for image, tagcode in read_from_gnt_dir(gnt_dir=test_data_dir):
    tagcode_unicode = struct.pack('>H', tagcode).decode('gb2312')
    im = Image.fromarray(image)
    dir_name = '/home/malidong/workspace/PatternRecognition/Chinese_character_recognition-master/data/test/' + '%0.5d'%char_dict[tagcode_unicode]
    if not os.path.exists(os.path.join(dir_name)):
        os.mkdir(os.path.join(dir_name))
    im.convert('RGB').save(os.path.join(dir_name)+'/' + str(test_counter) + '.png')
    test_counter += 1

四、模型

本次实验选择了MobilenetV2作为学习的模型。该模型是一个轻量级的模型,具有大量减少参数数量和计算量,内存占用少,网络表达能力强,模型在低精度计算下具有比较强的鲁棒性等特点。

4.1 网络结构

手写字体识别python_神经网络_04

手写字体识别python_深度学习_05

4.2 程序实现

import torch
import torch.nn as nn
import torch.nn.functional as F

class Block(nn.Module):
    '''expand + depthwise + pointwise'''
    def __init__(self, in_planes, out_planes, expansion, stride):
        super(Block, self).__init__()
        self.stride = stride

        planes = expansion * in_planes
        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, groups=planes, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        self.conv3 = nn.Conv2d(planes, out_planes, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn3 = nn.BatchNorm2d(out_planes)

        self.shortcut = nn.Sequential()
        if stride == 1 and in_planes != out_planes:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=1, padding=0, bias=False),
                nn.BatchNorm2d(out_planes),
            )

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = F.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        out = out + self.shortcut(x) if self.stride==1 else out
        return out


class MobileNetV2(nn.Module):
    # (expansion, out_planes, num_blocks, stride)
    cfg = [(1,  16, 1, 1),
           (6,  24, 2, 1),  # NOTE: change stride 2 -> 1 for CIFAR10
           (6,  32, 3, 2),
           (6,  64, 4, 2),
           (6,  96, 3, 1),
           (6, 160, 3, 2),
           (6, 320, 1, 1)]

    def __init__(self, num_classes=3755):
        super(MobileNetV2, self).__init__()
        # NOTE: change conv1 stride 2 -> 1 for CIFAR10
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(32)
        self.layers = self._make_layers(in_planes=32)
        self.conv2 = nn.Conv2d(320, 1280, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn2 = nn.BatchNorm2d(1280)
        self.linear = nn.Linear(1280, num_classes) # 5120

    def _make_layers(self, in_planes):
        layers = []
        for expansion, out_planes, num_blocks, stride in self.cfg:
            strides = [stride] + [1]*(num_blocks-1)
            for stride in strides:
                layers.append(Block(in_planes, out_planes, expansion, stride))
                in_planes = out_planes
        return nn.Sequential(*layers)

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.layers(out)
        out = F.relu(self.bn2(self.conv2(out)))
        # NOTE: change pooling kernel_size 7 -> 4 for CIFAR10
        out = F.avg_pool2d(out, 4)
        out = out.view(out.size(0), -1)
        out = self.linear(out)
        return out

五、实验程序

5.1 导入包

import os
import torch
import torch.nn as nn
import torch.optim as optim
from tensorboardX import SummaryWriter
from torchvision import transforms
from hwdb import HWDB
from mobilenetv2 import MobileNetV2

5.2 参数设置

# 超参数
    epochs = 100 # 学习次数
    batch_size = 100 # batchsize
    lr = 0.1 # 学习率
    data_path = r'/home/malidong/workspace/PatternRecognition/Chinese_character_recognition-master/data' # 数据集路径
    log_path = r'logs/batch_{}_lr_{}'.format(batch_size, lr) # 日志路径
    save_path = r'checkpoints/' # 模型保存路径

5.3 数据集加载

数据集加载是训练模型之前的一个重要步骤,并且在这一步中可以进行图片增强,例如图片随机翻转,来提高模型准确率。

# 图片预处理
transform = transforms.Compose([transforms.Resize((args.image_size, args.image_size)),
                               	transforms.ToTensor()])
# 读取图片和对应的标签
class HWDB(object):
    def __init__(self,path, transform):
        # 预处理过程
        traindir = os.path.join(path, 'train')
        testdir = os.path.join(path, 'test')
        # 读取训练集和测试集
        self.trainset = datasets.ImageFolder(traindir, transform)
        self.testset = datasets.ImageFolder(testdir, transform)
        self.train_size = len(self.trainset) # 训练集大小
        self.test_size = len(self.testset) # 测试集大小
        self.num_classes = len(self.trainset.classes) # 类别数
        self.class_to_idx = self.trainset.class_to_idx # 索引

    def get_sample(self, index=0):
        sample = self.trainset[index]
        sample_img, sample_label = sample # 获取图片和标签
        return sample_img, sample_label

    def get_loader(self, batch_size=100):
        trainloader = DataLoader(self.trainset, batch_size=batch_size, shuffle=True)
        testloader = DataLoader(self.testset, batch_size=batch_size, shuffle=True)
        return trainloader, testloader

5.4 可视化

可视化是查看训练的一个重要步骤,我们可以通过损失函数的曲线,准确率的曲线很好的了解到模型训练过程中存在的问题。

本次实验使用tensorboardX作为可视化工具。

5.5 训练程序

训练程序完成模型的训练任务。该过程主要包含三个部分,第一个是训练集的加载,第二个是训练模型,第三个是训练结果可视化。

def train(epoch, net, criterion, optimizer, train_loader, writer, scheduler, save_iter=100):
    '''
    :param epoch: 第n次学习
    :param net: 网络模型
    :param criterion: 损失函数
    :param optimizer: 优化器
    :param train_loader: 训练集
    :param writer: 可视化
    :param scheduler: 调整学习率
    :param save_iter: 每n次保存模型
    :return:
    '''
    print("epoch %d 开始训练..." % epoch)
    net.train()
    sum_loss = 0.0
    total = 0
    correct = 0
    # 数据读取
    for i, (inputs, labels) in enumerate(train_loader):
        # 梯度清零
        optimizer.zero_grad()
        if torch.cuda.is_available():
            inputs = inputs.cuda()
            labels = labels.cuda()
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        # 取得分最高的那个类
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        loss.backward()
        optimizer.step()

        # 每训练100个batch打印一次平均loss与acc
        sum_loss += loss.item()
        if (i + 1) % save_iter == 0:
            batch_loss = sum_loss / save_iter
            # 每跑完一次epoch测试一下准确率
            acc = 100 * correct / total
            print('epoch: %d, batch: %d loss: %.03f, acc: %.04f'
                  % (epoch, i + 1, batch_loss, acc))
            writer.add_scalar('train_loss', batch_loss, global_step=i + len(train_loader) * epoch)
            writer.add_scalar('train_acc', acc, global_step=i + len(train_loader) * epoch)
            for name, layer in net.named_parameters():
                writer.add_histogram(name + '_grad', layer.grad.cpu().data.numpy(),
                                     global_step=i + len(train_loader) * epoch)
                writer.add_histogram(name + '_data', layer.cpu().data.numpy(),
                                     global_step=i + len(train_loader) * epoch)
            total = 0
            correct = 0
            sum_loss = 0.0
        scheduler.step()

5.6 测试程序

测试程序在训练程序之后,完成对模型评估的任务,这是一个必不可少的过程,通过该过程可以了解到模型的训练情况。

该过程主要包含三个部分,第一个是测试集的加载,第二个是通过模型得到测试结果,第三个是测试结果可视化。

def valid(epoch, net, test_loarder, writer):
    '''
    测试程序
    :param epoch: 学习次数
    :param net: 网络模型
    :param test_loarder: 测试集
    :param writer: 可视化
    '''
    print("epoch %d 开始验证..." % epoch)
    with torch.no_grad():
        correct = 0
        total = 0
        for images, labels in test_loarder:
            images, labels = images.cuda(), labels.cuda()
            outputs = net(images)
            # 取得分最高的那个类
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
        print('correct number: ', correct)
        print('totol number:', total)
        acc = 100 * correct / total
        print('第%d个epoch的识别准确率为:%d%%' % (epoch, acc))
        writer.add_scalar('valid_acc', acc, global_step=epoch)

5.7 主程序

if __name__ == "__main__":
    # 超参数
    epochs = 100 # 学习次数
    batch_size = 100 # batchsize
    lr = 0.1 # 学习率
    data_path = r'/home/malidong/workspace/PatternRecognition/Chinese_character_recognition-master/data' # 数据集路径
    log_path = r'logs/batch_{}_lr_{}'.format(batch_size, lr) # 日志路径
    save_path = r'checkpoints/' # 模型保存路径
    if not os.path.exists(save_path):
        os.mkdir(save_path)

    # 数据集加载
    transform = transforms.Compose([
        transforms.Resize((32, 32)),
        transforms.ToTensor(),
    ])
    dataset = HWDB(path=data_path, transform=transform)
    print("训练集数据:", dataset.train_size)
    print("测试集数据:", dataset.test_size)
    trainloader, testloader = dataset.get_loader(batch_size)

    # 模型加载
    net = MobileNetV2()
    if torch.cuda.is_available():
        net = net.cuda()
    # 损失函数
    criterion = nn.CrossEntropyLoss()
    # 优化器
    optimizer = optim.SGD(net.parameters(), lr=lr)
    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
    writer = SummaryWriter(log_path)
    for epoch in range(epochs):
        # 训练
        train(epoch, net, criterion, optimizer, trainloader, writer=writer, scheduler=scheduler)
        # 测试
        valid(epoch, net, testloader, writer=writer)
        print("epoch%d 结束, 正在保存模型..." % epoch)
        torch.save(net.state_dict(), save_path + 'handwriting_iter_%03d.pth' % epoch)

六、实验分析

6.1 损失函数

手写字体识别python_原力计划_06

上图为训练的损失,由图可知,随着训练的进行,损失逐渐减小,这表明了训练的有效性。

6.2 准确率

手写字体识别python_深度学习_07

上图为测试集的准确率,由图可知,随着训练的进行,其准确率逐渐变大,这表明了模型精度的提高,鲁棒性变强。

6.3 打印结果

手写字体识别python_手写字体识别python_08

由图可知,在运行到第29个epochs时,模型已经达到了较好的效果,在测试集上有92%的精度,共29859个样本成功预测27651个样本。而在训练集上已经达到了100%的预测精度,如果再继续训练下去,可能会造成过拟合的问题。

七、总结

本次实验是对HWDB1.1数据集进行手写汉字识别,由于数据集过于庞大,一共3755个字符的数据集只选取了前500个字符用于测试训练。实验使用pytorch深度学习框架,mobilenetV2作为特征提取网络,可以较好的完成手写汉字识别的任务。

在实验过程中,在使用tensorboard可视化工具时遇到了一些麻烦,第一个问题是输入可视化命令却报错无法使用,在长时间的解决该bug过程中,发现该问题是由于tensorboard访问权限被限制所导致。第二个问题是输入可视化命令后,在显示图表的时候一直无法加载出数据,经过官网的提示发现并无任何问题,最终发现原因可能是加载过于缓慢,需要长时间的加载才能显示出数据。这个问题现在只能通过等待来解决,尚无更好的解决方案。

在编写训练程序的时候也遇到了一些问题,当训练程序和测试程序一起使用的时候,运行到中途出现了显存报错的问题,该问题并不是显存溢出问题,应该是cudnn的问题,但这个问题仍然没有解决。

八、工程下载链接

链接:https://pan.baidu.com/s/17DZDE3QUVhS14mPZjl99Xg
提取码:y9d1