CASIA Handwritten Chinese Character Recognition Using Convolutional Neural Network and Similarity Ranking (with TensorFlow Source Code)

-


Additional Sources:

Source code (implemented by Tensorflow) for this article is available at Github: Go to Github.

Dataset for this article is available at CASIA: Go to CASIA.


CASIA example

Abstract:

Convolution Neural Networks (CNN) have recently achieved state-of-the art performance on handwritten Chinese character recognition (HCCR). However, most of CNN models employ the SoftMax activation function and minimize cross entropy loss, which may cause loss of inter-class information. To cope with this problem, we propose to combine cross entropy with similarity ranking function and use it as loss function. The experiments results show that the combination loss functions produce higher accuracy in HCCR. This report briefly reviews cross entropy loss function, a typical similarity ranking function: Euclidean distance, and also propose a new similarity ranking function: Average variance similarity. Experiments are done to compare the performances of a CNN model with three different loss functions. In the end, SoftMax cross entropy with Average variance similarity produce the highest accuracy on handwritten Chinese characters recognition.


Keywords:

Convolution Neural Networks (CNN); handwritten Chinese character recognition (HCCR); cross-entropy function; similarity ranking function

I. INTRODUCTION

This report is focused on the offline Handwritten Chinese character recognition, which is an important research field in pattern recognition. Recognizing Chinese characters is more challenging compared with alphabet and digits recognition. Because Chinese has over 7000 classes in the common vocabulary, and it also has more complex structure in each character. In recent years, Chinese handwriting character recognition has received much research and attention. As regular Neural network don't scale well to full image, in order to deal with the handwritten images, the most suitable approach is to use convolution neural network (CNN). The model in this report is based on CNN. However, unlike most existing methods, the method presented in this report using both classification and similarity ranking signals as supervision to train a CNN model. Classification is to classify an input image into a large number of identity classes, while similarity ranking is to minimize the intra-class distance while maximizing the inter-class distance.

The rest of this report includes the briefly reviews for convolutional neural network and cross-entropy, introduction of similarity ranking function, methodology of the experiment and the results analysis.


II. BACKGROUND

2.1 Convolutional Neural Network

Convolutional neural networks (CNNs) consist of an input and an output layer, as well as multiple hidden layers. The hidden layers of a CNN typically consist of convolutional layers, pooling layers, fully connected layers and normalization layers [1]. Avoiding complex pre-processing of the image (extracting artificial features, etc.), CNNs process input original image directly. The layers of a CNN have neurons arranged in 3 dimensions: width, height and depth, CNN can reduce the full image into a single vector of class scores, arranged along the depth dimension through complete process [2]. In visual object recognition, CNNs often achieve a good performance. The convolution layer only increases the depth and leave the height and width unchanged. For the pooling layer, it would keep the depth, and narrow the size of volume. Finally, the fully connected layer would transform the volume into 1x1xn which n is represented to different classes [3]. Unlike the regular neural networks, CNN preserves the input's neighborhood relations and spatial locality in their latent higher-level feature representations. While the architecture of common fully connected neural network do not scale well to realistic-sized high-dimensional images in terms of computational complexity, CNNs do, since the number of free parameters describing their shared weights does not depend on the input dimensionality [4].

2.2 Loss Function for Classification Problems

2.2.1 Entropy

Entropy is a measure for information contents and could be defined as the unpredictability of an event. Assume that the probability of an event occurring is p, then the unpredictability of this event is log2(1/p) * log2(1/p) [5]. So, the greater the probability is, the smaller the unpredictability is, which means the information contents is also very small. If an event occurs inevitably with the probability of 100%, then the unpredictability and information content are 0.

Assume that a non-uniform dice, the probability of rolling it to get a definite point X ( X={x1,x2,...,x6} ) is Pi = p(X=Xi). The expectation of the discrete random variable can be defined as [5]:

discrete random variable expectation

The expectation H is entropy. So, the greater the entropy is, the greater the unpredictability is.

2.2.2 Cross-entropy

For same event, p is the real probability density, while q is the probability density from a prediction model. Cross-entropy can be defined as [6]:

Cross entropy

2.2.3 Cross-entropy Used as Loss Function

In machine learning and deep learning, cross-entropy can be used to define loss function. It represents the inaccuracy of predictions.

Considering a binary classification problem. There are only two classifications for the result of one prediction, so,

two classifications problem

cross-entropy is:

cross entropy as lost function

which is the loss function of logistic regression. It computes the average cross-entropy of m samples [7]:

average cross entropy

Cross-entropy can be used in multiclassification problems with the combination of SoftMax (do not consider regularization):

Cross entropy combine softmax

Compared with quadratic loss function, cross-entropy loss function gives better training performance on neural networks.

2.3 Similarity Ranking Function

In classification problems, besides differences in different classes, another important information can be learned is the similarities among different samples. Different similarity measurements may have impacts on the accuracy of the prediction.

2.3.1 Euclidean Distance

Euclidean Distance defines the true distance between two points in an m-dimensional space. In supervised learning, it can be used to measure the similarity between two samples with n features [8]:

Euclidean Distance

where d12 refers to the similarity between first and second samples.

2.3.2 Average Variance Similarity

In the statistical description, variance is used to calculate the difference between each sample and the mean of all samples. It is defined as:

variance

III. METHODOLOGY

The data used in this experiment is from CASIA online and offline Chinese handwriting databases. We selected data from the offline database. Original CASIA offline dataset is a list of images labeled with corresponding Chinese character, with size 6.7GB, contains 3160 different characters and total 1055440 samples, each character has average 334 corresponding samples. Due to the limitation of our machine, we only use a subset of the dataset, with 300 different characters and total 100342 samples. CASIA also provide offline character test dataset for evaluation, with average 81 different samples corresponding to each character. There are some handwriting samples from different writers shown in figure 1.

CASIA example

3.1 Data Preprocessing

The original CASIA dataset is in "gnt" format, which is hard for TensorFlow to process, so we transform the dataset to "sqlite" format, which is easy to access through python package "sqlite3". Because the size of each image sample is different, so we perform cropping, scaling, and padding for each image, the result images have same size (128*128 pixels). Our group also perform normalization to the data, so the network is easier to converge in training period. There are some images for each character after preprocess in figure 2.

preprocess CASIA
import numpy as np
import tensorflow as tf
import pickle, sqlite3
import config


class DataManager:
    def __init__(self):
        self.label_array = pickle.load(open(config.CASIA_label_file_path, 'rb'))  # train label array length = 300
        self.init_dataset()

    def init_dataset(self):
        if (config.MODE == tf.estimator.ModeKeys.TRAIN):
            self.CASIA_train_sqlite_connection = sqlite3.connect(config.CASIA_train_sqlite_file_path)
            self.CASIA_train_sqlite_cursor = self.CASIA_train_sqlite_connection.cursor()
            # save sample index into array
            self.CASIA_train_sqlite_cursor.execute("SELECT ID,Character_in_gb2312 FROM TrainDataset")
            train_dataset_index = self.CASIA_train_sqlite_cursor.fetchall()
            self.train_dataset_id_array = []
            self.train_dataset_character_dict = dict()
            for ID, Character_in_gb2312 in train_dataset_index:
                self.train_dataset_id_array.append(ID)
                if Character_in_gb2312 not in self.train_dataset_character_dict:
                    self.train_dataset_character_dict[Character_in_gb2312] = [ID]
                else:
                    self.train_dataset_character_dict[Character_in_gb2312].append(ID)
        if (config.MODE == tf.estimator.ModeKeys.EVAL):
            self.CASIA_test_sqlite_connection = sqlite3.connect(config.CASIA_test_sqlite_file_path)
            self.CASIA_test_sqlite_cursor = self.CASIA_test_sqlite_connection.cursor()
            self.CASIA_test_sqlite_cursor.execute("SELECT ID,Character_in_gb2312 FROM TestDataset")
            test_dataset_index = self.CASIA_test_sqlite_cursor.fetchall()
            self.test_dataset_id_array = []
            for ID, Character_in_gb2312 in test_dataset_index:
                self.test_dataset_id_array.append(ID)

    # ramdom select certain amount of training data
    def next_train_batch_random_select(self, batch_size=200):
        select_ids = np.random.choice(self.train_dataset_id_array, batch_size, replace=False)
        select_sql = "SELECT * FROM TrainDataset WHERE ID IN " + str(tuple(select_ids))
        self.CASIA_train_sqlite_cursor.execute(select_sql)
        select_data = self.CASIA_train_sqlite_cursor.fetchall()
        feature_batch, target_batch = self.process_sqlite_data(select_data)
        return feature_batch, target_batch

    # ramdom select x sample in each y classes
    def next_train_batch_fix_character_amount(self, character_amount=90, each_character_sample_amount=2):
        select_characters = np.random.choice(self.label_array, character_amount, replace=False)
        select_ids = []
        for character in select_characters:
            select_id = np.random.choice(self.train_dataset_character_dict[character], each_character_sample_amount,
                                         replace=False)
            select_ids.extend(select_id)
        select_sql = "SELECT * FROM TrainDataset WHERE ID IN " + str(
            tuple(select_ids)) + " ORDER BY Character_in_gb2312"
        self.CASIA_train_sqlite_cursor.execute(select_sql)
        select_data = self.CASIA_train_sqlite_cursor.fetchall()
        feature_batch, target_batch = self.process_sqlite_data(select_data)
        return feature_batch, target_batch

    def next_test_batch(self, batch_size=200):
        if len(self.test_dataset_id_array) >= batch_size:
            select_ids = np.random.choice(self.test_dataset_id_array, batch_size, replace=False)
        else:
            select_ids = self.test_dataset_id_array
        self.test_dataset_id_array = list(set(self.test_dataset_id_array) - set(select_ids))
        select_sql = "SELECT * FROM TestDataset WHERE ID IN " + str(tuple(select_ids))
        self.CASIA_test_sqlite_cursor.execute(select_sql)
        select_data = self.CASIA_test_sqlite_cursor.fetchall()
        feature_batch, target_batch = self.process_sqlite_data(select_data)
        return feature_batch, target_batch

    def process_sqlite_data(self, sqlite_data):
        feature_batch = np.zeros(shape=(len(sqlite_data), config.image_hight, config.image_width), dtype=np.float32)
        target_batch = np.zeros(shape=(len(sqlite_data), len(self.label_array)), dtype=np.float32)
        for i in range(len(sqlite_data)):
            # construct features
            img = pickle.loads(sqlite_data[i][5])
            height, width = np.shape(img)
            # 1. crop by (target_image_hight * target_image_width)
            if height > config.image_hight:
                img = img[(height - config.image_hight) // 2:(height + config.image_hight) // 2, :]
            if width > config.image_width:
                img = img[:, (width - config.image_width) // 2:(width + config.image_width) // 2]
            # pad to (target_image_hight * target_image_width)
            height, width = np.shape(img)
            top_pad = (config.image_hight - height) // 2
            bottom_pad = (config.image_hight - height) // 2 + np.mod(config.image_hight - height, 2)
            left_pad = (config.image_width - width) // 2
            right_pad = (config.image_width - width) // 2 + np.mod(config.image_width - width, 2)
            img = np.pad(img, ((top_pad, bottom_pad), (left_pad, right_pad)), 'constant', constant_values=255)
            # 3. scale graylevel 255. to 0. and graylevel 0. to 1.
            img = (255 - img)
            img = img.astype(np.float32)
            feature_batch[i, :, :] = img
            # construct target
            label = [int(x == sqlite_data[i][1]) for x in self.label_array]
            target_batch[i, :] = label
        return feature_batch, target_batch

3.2 Model design

Our group designed 3 different models, all 3 models have the same neural network structure, consists of 6 convolutional layers, 6 pooling layers, 2 fully connected layers and 2 dropout layers. For each convolutional layer and fully connected layer, we use leaky Relu as activation function to speed up the training process. However, they use different loss functions respectively. Model architecture can be summarized as the table 1.

import tensorflow as tf
import config


def cnn(X, Y):
    input_layer = tf.reshape(X, [-1, config.image_hight, config.image_width, 1])

    # Input Tensor Shape: [batch_size, 128, 128, 1]
    # Output Tensor Shape: [batch_size, 128, 128, 32]
    with tf.name_scope("conv1"):
        conv1 = tf.layers.conv2d(inputs=input_layer, filters=32, kernel_size=[5, 5], padding="same",
                                 activation=tf.nn.leaky_relu)

    # Input Tensor Shape: [batch_size, 128, 128, 32]
    # Output Tensor Shape: [batch_size, 64, 64, 32]
    with tf.name_scope("pool1"):
        pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

    # Input Tensor Shape: [batch_size, 64, 64, 32]
    # Output Tensor Shape: [batch_size, 64, 64, 64]
    with tf.name_scope("conv2"):
        conv2 = tf.layers.conv2d(inputs=pool1, filters=64, kernel_size=[5, 5], padding="same",
                                 activation=tf.nn.leaky_relu)

    # Input Tensor Shape: [batch_size, 64, 64, 64]
    # Output Tensor Shape: [batch_size, 32, 32, 64]
    with tf.name_scope("pool2"):
        pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)

    # Input Tensor Shape: [batch_size, 32, 32, 64]
    # Output Tensor Shape: [batch_size, 32, 32, 128]
    with tf.name_scope("conv3"):
        conv3 = tf.layers.conv2d(inputs=pool2, filters=128, kernel_size=[5, 5], padding="same",
                                 activation=tf.nn.leaky_relu)

    # Input Tensor Shape: [batch_size, 32, 32, 128]
    # Output Tensor Shape: [batch_size, 16, 16, 128]
    with tf.name_scope("pool3"):
        pool3 = tf.layers.max_pooling2d(inputs=conv3, pool_size=[2, 2], strides=2)

    # Input Tensor Shape: [batch_size, 16, 16, 128]
    # Output Tensor Shape: [batch_size, 16, 16, 256]
    with tf.name_scope("conv4"):
        conv4 = tf.layers.conv2d(inputs=pool3, filters=256, kernel_size=[5, 5], padding="same",
                                 activation=tf.nn.leaky_relu)

    # Input Tensor Shape: [batch_size, 16, 16, 256]
    # Output Tensor Shape: [batch_size, 8, 8, 256]
    with tf.name_scope("pool4"):
        pool4 = tf.layers.max_pooling2d(inputs=conv4, pool_size=[2, 2], strides=2)

    # Input Tensor Shape: [batch_size, 8, 8, 256]
    # Output Tensor Shape: [batch_size, 8, 8, 256]
    with tf.name_scope("conv5"):
        conv5 = tf.layers.conv2d(inputs=pool4, filters=256, kernel_size=[5, 5], padding="same",
                                 activation=tf.nn.leaky_relu)

    # Input Tensor Shape: [batch_size, 8, 8, 256]
    # Output Tensor Shape: [batch_size, 4, 4, 256]
    with tf.name_scope("pool5"):
        pool5 = tf.layers.max_pooling2d(inputs=conv5, pool_size=[2, 2], strides=2)

    # Input Tensor Shape: [batch_size, 4, 4, 256]
    # Output Tensor Shape: [batch_size, 4, 4, 512]
    with tf.name_scope("conv6"):
        conv6 = tf.layers.conv2d(inputs=pool5, filters=512, kernel_size=[5, 5], padding="same",
                                 activation=tf.nn.leaky_relu)

    # Input Tensor Shape: [batch_size, 4, 4, 512]
    # Output Tensor Shape: [batch_size, 2, 2, 512]
    with tf.name_scope("pool6"):
        pool6 = tf.layers.max_pooling2d(inputs=conv6, pool_size=[2, 2], strides=2)

    # Input Tensor Shape: [batch_size, 2, 2, 512]
    # Output Tensor Shape: [batch_size, 2 * 2 * 512]
    feature_layer = tf.reshape(pool6, [-1, 2 * 2 * 512])

    return feature_layer

def full_connected_classifier(feature_layer):
    with tf.name_scope("dense1"):
        dense1 = tf.layers.dense(inputs=feature_layer, units=1024, activation=tf.nn.leaky_relu)
    with tf.name_scope("dropout1"):
        dropout1 = tf.layers.dropout(dense1, rate=0.25, training=config.MODE == tf.estimator.ModeKeys.TRAIN)
    with tf.name_scope("dense2"):
        dense2 = tf.layers.dense(inputs=dropout1, units=1024, activation=tf.nn.leaky_relu)
    with tf.name_scope("dropout2"):
        dropout2 = tf.layers.dropout(dense2, rate=0.25, training=config.MODE == tf.estimator.ModeKeys.TRAIN)
    with tf.name_scope("out_layer"):
        classification_layer = tf.layers.dense(inputs=dropout2, units=config.label_array_length, activation=tf.nn.leaky_relu)

    return classification_layer

3.3 Validation process

Due to the limitation of our machine, we didn't perform cross validation in our experiment, all training batch is randomly selected from the training dataset. However, there is another separated validation dataset provided by CASIA, which we only use in the validation process.

For model 1(SoftMax only), we use SGD (stochastic gradient descent) with batch size 200, every sample is random selected from the whole dataset. We use SoftMax Cross Entropy as the loss function to train model 1. SoftMax Cross Entropy can maximize the distance between different classes.

THREE MODELS OF CNN

For model 2(SoftMax plus Euclidean), we use batch size 180, with 90 characters and 2 samples for each character. Later use 2 samples of same class to calculate the Euclidean distance between them, we use Euclidean distance plus SoftMax Cross Entropy loss as the final loss function. Minimize the final loss function not only can minimize the Euclidean distance between same class, but also capable of minimize the SoftMax Cross Entropy loss.

def calculate_euclidean(classification_layer):
    character_layer_1 = tf.strided_slice(classification_layer, begin=[0, 0],
                                         end=[character_amount * each_character_sample_amount,
                                              config.label_array_length],
                                         strides=[each_character_sample_amount, 1])
    character_layer_2 = tf.strided_slice(classification_layer, begin=[1, 0],
                                         end=[character_amount * each_character_sample_amount,
                                              config.label_array_length],
                                         strides=[each_character_sample_amount, 1])
    #discance between 2 image
    loss = tf.pow(tf.nn.l2_loss(character_layer_1 - character_layer_2), 0.5)
    return loss

For model 3(SoftMax plus variance), we use batch size 200, with 5 characters and 40 samples for each character. Later use 40 samples of same class to calculate the variance for each class, we use variance plus SoftMax Cross Entropy loss as the final loss function. Minimize the final loss function not only can minimize the variance between same class, but also capable of minimize the SoftMax Cross Entropy loss.

def calculate_variance(classification_layer):
    character_layer = tf.reshape(classification_layer,
                                 shape=[character_amount, each_character_sample_amount, config.label_array_length])
    #variance in all same class image
    character_layer_mean, character_layer_variance = tf.nn.moments(character_layer, [1])
    loss = tf.reduce_mean(character_layer_variance)
    return loss

IV. EVALUATION AND RESULTS

This section first discusses the implementation details, then presents evaluation results comparing the proposed algorithm to 2 other competing models.

4.1 Implementation details

Our group use python package "pickle" and "sqlite" to manage dataset, which can access the database by database SQL. We implement all 3 different models with machine learning framework "TensorFlow", which is the new standard in deep learning industry. We choose "TensorFlow" because its capable of GPU acceleration. All 3 models are trained on GTX 1060 discrete GPU w/6GB GDDR5 graphics memory. It took 3 hours for model 1 to converge, and 5 hours for model 2 and model 3 to converge.

4.2 Results

After the training process, we perform evaluation process using another separated evaluation dataset. the recognition rates are applied to evaluate the accuracy of models with different loss functions. The results are shown in the table 2.

RECOGNITION RATES result

For model without the usage of similarity ranking function, the recognition rate is 93.70%. After using Euclidean as similarity ranking function, the recognition rate is up to 94.99%. After using variance as similarity ranking function, the recognition rate is up to 95.58%.

Table 2 shows that the model with similarity ranking function has a small but consistent advantage compare to model without similarity ranking function. In addition, variance similarity ranking function is better than Euclidean similarity ranking function.


V. CONCLUSION

Overall, handwritten Chinese character recognition is divided into two categories. One is online handwritten Chinese character, and the other is offline handwritten Chinese character. In this project, experiments are implemented on the dataset of offline handwritten Chinese character. This report focuses on the loss functions of a CNN model and demonstrates that the character classification and similarity ranking supervisory signals are complementary for each other, which can increase inter-class variations and reduce intra-class variations, and therefore achieve a much better classification performance. Combination of the two supervisory signals leads to significantly better results than only SoftMax cross-entropy based character classification. In addition, variance similarity ranking function have better performance than Euclidean similarity ranking function.


REFERENCES

[1] "Convolutional Neural Networks (LeNet) – DeepLearning 0.1 documentation". DeepLearning 0.1. LISA Lab. Retrieved 31 August 2013.

[2] Krizhevsky, Alex. "ImageNet Classification with Deep Convolutional Neural Networks". Retrieved 17 November 2013.

[3] "CS231n Convolutional Neural Networks for Visual Recognition". cs231n.github.io. Retrieved 2017-04-25.

[4] Le Callet, Patrick; Christian Viard-Gaudin; Dominique Barba (2006). "A Convolutional Neural Network Approach for Objective Video Quality Assessment". IEEE Transactions on Neural Networks. 17 (5): 1316–1327.

[5] Liang, J. and Qian, Y., 2008. Information granules and entropy theory in information systems. Science in China Series F: Information Sciences, 51(10), pp.1427-1444.

[6] De Boer, P.T., Kroese, D.P., Mannor, S. and Rubinstein, R.Y., 2005. A tutorial on the cross-entropy method. Annals of operations research, 134(1), pp.19-67.

[7] Vincent, P., Larochelle, H., Lajoie, I., Bengio, Y. and Manzagol, P.A., 2010. Stacked denoising autoencoders: Learning useful representations in a deep network with a local denoising criterion. Journal of Machine Learning Research, 11(Dec), pp.3371-3408.

[8] Qian, G., Sural, S., Gu, Y. and Pramanik, S., 2004, March. Similarity between Euclidean and cosine angle distance for nearest neighbor queries. In Proceedings of the 2004 ACM symposium on Applied computing (pp. 1232-1237). ACM.


More Blog: