目录
摘要
手写汉字识别作为人机交互的重要部分,解决这一关键难题具有非常重要的实用价值。汉字识别由于类别数量巨大,字形结构复杂,同时存在大量字形相似的汉字,书写方式因人而异等,在模式识别领域一直是难点和研究热点。深度学习模型卷积神经网络(Convolutional Neural Networks, CNNs)具有自动获取样本概率分布或者学习样本特征的优势,可以避免手写汉字字形特征提取的难题,其在在脱机手写汉字识别领域取得了杰出的研究成果。但深层次的复合模型又具有训练困难的难题,其计算复杂度比较大(特征太多)。本文采用keras神经网络编程框架,实现经典的深度残差网络,并在此基础上实现深度残差收缩网络,通过参数调节在一定程度上提高手写体字符识别的识别效率。
数据集
CASIA-OLHWDB和CASIA-HWDB在线和离线中文手写数据库是由中国科学院自动化研究所模式识别国家实验室(NLPR) 建立的(CASIA),手写样本是由1,020位作家使用Anoto笔在纸上制作的。该数据集一共包含3750个常见字体的书写,我们从该数据集中随机抽取部分样本组成训练子集进行模型的训练(因为没有GPU,毕竟要考虑机器实际,只是为了实现模型)原始数据样本如图所示(经过分割处理):
本文实验使用随机抽取的10个字体样本进行训练模型,并完成对模型的测试。
模型构建与实现
实验环境:python3.5.2、Keras 2.1.3 、tensorflow 1.4.0 、Flask1.0.2 等;
深度卷积神经网络如图所示:
- 输入为32x32的灰度图像,那么输入的
input_shape=(32,32,1)
- 第一层有6个5x5的卷积核,no padding;
- 第二层为2x2的MaxPooling层,stride 为 2;
- 第三层为16个5x5的卷积层,no padding;
- 第四层为2x2的MaxPooling层,stride 为2;
- 第五层为Flatten和Dense层,连接120个节点;
- 第六层为Dense层,连接84个节点;
- 第七层为Dense层,activation是softmax。
对于上述网络,如果简单地增加深度,会导致梯度弥散或梯度爆炸。对于该问题的解决方法是正则化初始化和中间的正则化层(Batch Normalization),这样的话可以训练几十层的网络。虽然通过上述方法能够训练了,但是又会出现另一个问题,就是退化问题,网络层数增加,但是在训练集上的准确率却饱和甚至下降了。这个不能解释为overfitting,因为overfit应该表现为在训练集上表现更好才对。退化问题说明了深度网络不能很简单地被很好地优化。而残差网络的特点是容易优化,并且能够通过增加相当的深度来提高准确率。其内部的残差块使用了跳跃连接,缓解了在深度神经网络中增加深度带来的梯度消失问题。下图展示了深度残差网络的基本模块,包括一些非线性层(残差路径)和一个跨层的恒等连接。恒等连接是深度残差网络的核心,是其优异性能的一个保障。
深度残差收缩网络,就是对深度残差网络的残差路径进行收缩的一种网络。在深度残差网络的基础上,深度残差收缩网络引入了一个小型的子网络,用这个子网络学习得到一组阈值,对特征图的各个通道进行软阈值化。这个过程其实可以看成一个可训练的特征选择的过程。具体而言,前面的卷积层将重要的特征转换成绝对值较大的值,将冗余信息所对应的特征转换成绝对值较小的值;通过子网络学习得到二者之间的界限,并且通过软阈值化将冗余特征置为零,同时使重要的特征有着非零的输出。 如图所示:
软阈值化是许多信号降噪方法的核心步骤,它是将接近于零(或者说绝对值低于某一阈值τ)的特征置为0,也就是将[-τ, τ]区间内的特征置为0,让其他的、距0较远的特征也朝着0进行收缩。如果和前一个卷积层的偏置b放在一起看的话,这个置为零的区间就变成了[-τ+b, τ+b]。因为τ和b都是可以自动学习得到的参数,这个角度看的话,软阈值化其实是可以将任意区间的特征置为零,是一种更灵活的、删除某个取值范围特征的方式,也可以理解成一种更灵活的非线性映射。从另一个方面来看,前面的两个卷积层、两个批标准化和两个激活函数,将冗余信息的特征,变换成接近于零的值;将有用的特征,变换成远离零的值。之后,通过自动学习得到一组阈值,利用软阈值化将冗余特征剔除掉,将有用特征保留下来。通过堆叠一定数量的基本模块,可以构成完整的深度残差收缩网络,如下图所示:
代码实现部分
加载图像数据代码:
def load_data():
label_file = open('data/labels.txt', "r", encoding="UTF-8")
klasses = [a.strip() for a in label_file.readlines()]
label_file.close()
norm_size = 64
path = "F:/计算机论文/计算机/手写汉字识别-系统/anchor/data/test"
print("[INFO] loading images...")
# data_dir = 'Mushrooms'
train_datagen = ImageDataGenerator(
rescale=1. / 255,
shear_range=0.2,
zoom_range=0.2,
validation_split=0.3,
horizontal_flip=True)
train_generator = train_datagen.flow_from_directory(
path,
target_size=(norm_size, norm_size),
batch_size=32,
class_mode='categorical',
subset='training')
validation_generator = train_datagen.flow_from_directory(
path, # same directory as training data
target_size=(norm_size, norm_size),
batch_size=32,
class_mode='categorical',
subset='validation') # set as validation data
# scale the raw pixel intensities to the range [0, 1]
num = len(klasses)
input_shape = (norm_size,norm_size, 3)
return train_generator, validation_generator,input_shape, num
ResNet模型构建:
def pad_backend(inputs, in_channels, out_channels):
pad_dim = (out_channels - in_channels)//2
return K.spatial_3d_padding(inputs, padding = ((0,0),(0,0),(pad_dim,pad_dim)))
def residual_block(incoming, nb_blocks, out_channels, downsample=False,
downsample_strides=2):
residual = incoming
in_channels = incoming.get_shape().as_list()[-1]
for i in range(nb_blocks):
identity = residual
if not downsample:
downsample_strides = 1
residual = BatchNormalization()(residual)
residual = Activation('relu')(residual)
residual = Conv2D(out_channels, 3, strides=(downsample_strides, downsample_strides),
padding='same', kernel_initializer='he_normal',
kernel_regularizer=l2(1e-4))(residual)
residual = BatchNormalization()(residual)
residual = Activation('relu')(residual)
residual = Conv2D(out_channels, 3, padding='same', kernel_initializer='he_normal',
kernel_regularizer=l2(1e-4))(residual)
# Downsampling (it is important to use the pooL-size of (1, 1))
if downsample_strides > 1:
identity = AveragePooling2D(pool_size=(1, 1), strides=(2, 2))(identity)
# Zero_padding to match channels (it is important to use zero padding rather than 1by1 convolution)
if in_channels != out_channels:
identity = Lambda(pad_backend)(identity, in_channels, out_channels)
residual = keras.layers.add([residual, identity])
return residual
# define and train a model
inputs = Input(shape=input_shape)
net = Conv2D(8, 3, padding='same', kernel_initializer='he_normal', kernel_regularizer=l2(1e-4))(inputs)
net = residual_block(net, 1, 8, downsample=True)
net = BatchNormalization()(net)
net = Activation('relu')(net)
net = GlobalAveragePooling2D()(net)
outputs = Dense(10, activation='softmax', kernel_initializer='he_normal', kernel_regularizer=l2(1e-4))(net)
model = Model(inputs=inputs, outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer=Adam(), metrics=['accuracy'])
model.fit(x_train, y_train, batch_size=100, epochs=5, verbose=1, validation_data=(x_test, y_test))
DRSN模型构建:
def abs_backend(inputs):
return K.abs(inputs)
def expand_dim_backend(inputs):
return K.expand_dims(K.expand_dims(inputs, 1), 1)
def sign_backend(inputs):
return K.sign(inputs)
def pad_backend(inputs, in_channels, out_channels):
pad_dim = (out_channels - in_channels) // 2
return K.spatial_3d_padding(inputs, padding=((0, 0), (0, 0), (pad_dim, pad_dim)))
class KMaxPooling(Layer):
def __init__(self, k=3, **kwargs):
super().__init__(**kwargs)
self.input_spec = InputSpec(ndim=4)
self.k = k
def compute_output_shape(self, input_shape):
return (input_shape[0], (input_shape[2] * self.k))
def call(self, inputs):
# swap last two dimensions since top_k will be applied along the last dimension
shifted_input = tf.transpose(inputs, [0, 3,2, 1])
# extract top_k, returns two tensors [values, indices]
top_k = tf.nn.top_k(shifted_input, k=self.k, sorted=True, name=None)[0]
# return flattened output
return Flatten()(top_k)
# Residual Shrinakge Block
def residual_shrinkage_block(incoming, nb_blocks, out_channels, downsample=False,
downsample_strides=2):
residual = incoming
in_channels = incoming.get_shape().as_list()[-1]
for i in range(nb_blocks):
identity = residual
if not downsample:
downsample_strides = 1
residual = BatchNormalization()(residual)
residual = Activation('relu')(residual)
residual = Conv2D(out_channels, 3, strides=(downsample_strides, downsample_strides),
padding='same', kernel_initializer='he_normal',
kernel_regularizer=l2(1e-4))(residual)
residual = BatchNormalization()(residual)
residual = Activation('relu')(residual)
residual = Conv2D(out_channels, 3, padding='same', kernel_initializer='he_normal',
kernel_regularizer=l2(1e-4))(residual)
# Calculate global means
residual_abs = Lambda(abs_backend)(residual)
abs_mean = GlobalAveragePooling2D()(residual_abs)
# Calculate scaling coefficients
scales = Dense(out_channels, activation=None, kernel_initializer='he_normal',
kernel_regularizer=l2(1e-4))(abs_mean)
scales = BatchNormalization()(scales)
scales = Activation('relu')(scales)
scales = Dense(out_channels, activation='sigmoid', kernel_regularizer=l2(1e-4))(scales)
scales = Lambda(expand_dim_backend)(scales)
# Calculate thresholds
thres = keras.layers.multiply([abs_mean, scales])
# Soft thresholding
sub = keras.layers.subtract([residual_abs, thres])
zeros = keras.layers.subtract([sub, sub])
n_sub = keras.layers.maximum([sub, zeros])
residual = keras.layers.multiply([Lambda(sign_backend)(residual), n_sub])
# Downsampling (it is important to use the pooL-size of (1, 1))
if downsample_strides > 1:
identity = AveragePooling2D(pool_size=(1, 1), strides=(2, 2))(identity)
# Zero_padding to match channels (it is important to use zero padding rather than 1by1 convolution)
if in_channels != out_channels:
identity = Lambda(pad_backend)(identity, in_channels, out_channels)
residual = keras.layers.add([residual, identity])
return residual
def renet(x_train, y_train,x_test, y_test,input_shape, num):
# define and train a model
inputs = Input(shape=input_shape)
net = Conv2D(4, 10, padding='same', kernel_initializer='he_normal', kernel_regularizer=l2(1e-4))(inputs)
# net4 = KMaxPooling(4)(net)
# print(net4.shape)
net1 = residual_shrinkage_block(net, 1, 4, downsample=True)
net2 = BatchNormalization()(net1)
net3 = Activation('relu',name="Dense")(net2)
print(net3.shape)
net4 = KMaxPooling(5)(net3)
outputs = Dense(num, activation='softmax', kernel_initializer='he_normal', kernel_regularizer=l2(1e-4))(net4)
model = Model(inputs=inputs, outputs=outputs)
model.compile(loss='categorical_crossentropy', optimizer=Adam(), metrics=['accuracy'])
H =model.fit(x_train, y_train, batch_size=100, epochs=50, verbose=1, validation_data=(x_test, y_test))
plt.plot(H .history['acc'], label="train_accuracy")
plt.plot(H .history['val_acc'], label='test_val_accuracy')
plt.title('DRSN accuracy_chart', family='Times New Roman', fontsize=16) # 添加标题
结果分析:
数据样本为10个类别,每个类别236个样本,所以在小样本下容易出现过拟合现象,但是仍旧可以得出两个模型的实验结果,由结果可以看出深度残差网络的结果明显要好。(大家可以下载全部的数据集进行训练和测试。到底哪个网络结构比较适用于该数据集。在这里由于电脑原因我只进行了大样本训练,但是没有测试,因为跑的太慢,但是这样测试集得到的结果准确率更高)
深度残差收缩网络结果:
深度残差网络结果:
实验结果应用:
将下载好的HWDB数据集解压处理好全部用于训练,训练训练深度残差网络500次之后,将训练参数保存在checkpoint
文件夹中,启动服务的时候会把这个model和模型的保存参数加载进来,当用户访问某一个页面A的时候,会调用model的预测函数实现汉字的识别,其实现结果如下所示:(还是有一定的准确率能够精准识别)
结论
本文两种深度残差网络只设置了一个基本模块,在更复杂的数据集上,可适当增加。也可能是这个原因,残差收缩网络测试集准确率远不如残差网络。而深度残差收缩网络的结构比普通的深度残差网络复杂,也许更难训练。所以最终到底哪个网络结构更适用于手写汉字的识别问题,可能需要对大量数据集进行训练和测试,才能找到真正的原因。联系Q:525894654
参考文献
1.M. Zhao, S. Zhong, X. Fu, et al., Deep residual shrinkage networks for fault diagnosis, IEEE Transactions on Industrial Informatics, 2019, DOI: 10.1109/TII.2019.2943898
2.任晓文, 王涛, 李健宇,等. 基于深度学习的异噪声下手写汉字识别的研究[J]. 计算机应用研究, 2019, 036(012):3878-3881.
3.佚名. 深度学习在手写汉字识别中的应用综述[J]. 自动化学报(8期):1125-1141.