Handwritten Mathematical Expression Recognition Using Convolutional Neural Network(基于卷积神经网络的手写数学表达式)

Handwritten Mathematical Expression Recognition Using Convolutional Neural Network

本文仅对该论文进行翻译及分析,论文版权归原作者所有

论文译文

摘要

在栅格图像上识别数学表达式通常包括两个步骤:检测单个符号和分析其空间结构以形成相干方程。在这项工作中,我们专注于第一步,并提出一种检测方法,能够定位小而困难的手写符号。我们使用深度卷积神经网络具有鲁棒的检测性能。对于我们创建的数据集上的106个不同的数学符号,它能够获得0.65的平均精度分数。在结构分析中,我们使用了德拉古莱解析器,因为它具有较高的精度,可以正确地检测符号。

关键词数学符号检测数学表达式检测卷积神经网络


引言

数学表达式识别已经研究了半个世纪。这个问题包括可用笔画序列的在线识别和对包含数学表达式的图像进行离线识别。离线表达式识别在两者之间比较困难,因为它没有笔画数据。

这两种方法通常有两个阶段:符号识别和结构分析。在第一阶段,检测数学符号。找到它们的分类和位置,然后在第二阶段使用它们来生成一个方程字符串,通常用LaTeX格式表示。在本文中,我们将重点讨论第一阶段。

有些作品使用传统方法分割图像中的符号,如X-Y递归分割[2]、投影轮廓分割[3]或连通分量分析等。这些方法往往会丢失数学表达式的二维结构信息,从而在结构分析阶段产生误差。在[4]和[5]中,文本符号使用最大稳定的外部区域(±MSERs)来找到,这些区域具有一些用于分割的所需属性。虽然这些方法最初是针对普通文本识别提出的,但它们也可以应用于数学表达。

我们可以使用深度神经网络来完成检测符号的任务,而不是使用像上面那样的手工算法。特别是卷积神经网络在对象识别中已经非常流行。主要原因是神经网络可以产生对任何特定数据集都独一无二的高级特征。这使得它可以在较少的人工输入的情况下产生强大的性能。

我们在第一阶段使用深度卷积神经网络,以最小化识别错误,让方法在第二阶段的表现更好。特别地,我们使用[6]中提出的SSD来进行符号检测和识别。对于第二阶段,我们按照[7]中提到的过程实现解析器,以便使用检测到的符号列表,每个符号包括一个边界框和符号类,以创建与输入图像对应的LaTeX字符串。然而,在本文中,我们主要关注如何改进原来的SSD,使其在数学符号的识别上产生更好的效果。

第二节描述了我们提出的手写数学表达式识别方法和我们在SSD上的修改。第三节展示了我们实验的细节,包括数据集以及我们如何创建它们、评估指标和结果。最后,第四部分对全文进行总结。

提出的方法

图1展示了我们的ME(数学表达式)识别系统的概念图。在符号识别阶段,我们提出了四个SSD的实验版本。下面提到的每个版本都继承了前一个版本,并进行了额外的修改。修改的重点是默认框的大小和输入图像的大小。在结构分析阶段,根据Richard Zannibi等人[7]的工作,我们使用解析器利用第一阶段的输出创建词汇基线结构树(lexd - bst)。我们修改树并生成LaTeX 符串。
图1

符号识别
  1. 原始SSD模型:SSD最初是一个对象检测框架。在这项工作中,Wei Liu等人引入了一个模型来检测VOC数据集[8]中的对象,该数据集有20个类。

    SSD根据一组特征图预测符号,这组特征图由一个基于网络产生(这是一个改进的VGG16[9]),接着是多个额外的卷积层。每个预测都有一组相应的预定义默认框作为参考系统。预测是一个分类+4维向量。在这个向量中,类向量维数描述了所有相关类的概率。另外的4个维度指定检测到的符号包围框的位置和大小:cx ,cy,w,h。

    生成的特征图尺寸不同,可以帮助SSD检测不同尺度的符号。为了预测,输入图像将被输入到卷积层序列中。在某些层中,将应用一个分类层来产生n+1的预测(其中n是高宽比的数量)
    图2

  2. SSD修改建议:在[6]中提出的原有SSD300的基础上,调整模型的配置,新增3个版本。

    版本Ι — 原始版本:这个版本是SSD300。
    (1)分类数量:106个符号和背景
    (2)输入图像大小:300 x 300
    (3)网络:VGG16
    (4)默认框大小:smin=0.2,smax=0.9
    sk = smin + (smax - smin) * (k - 1) / (m - 1)
    其中k是用于预测的特征图的数量

    在实验中,这个版本不能检测小的符号(大约20x20像素或更小)。这是因为在匹配过程中,ground truth 边界框由于尺寸差异较大,无法与任何默认框进行匹配。没有一对能满足Jaccard重叠条件[10],因此符号被忽略,被视为“背景”,这使得SSD无法学习小符号。

    版本II - SSD调整默认盒的大小
    这个版本继承了版本I,但是默认框的大小不同。在对采集到的图像进行检测后,我们发现图像中存在许多纵横比不平衡的小符号。在SSD训练过程中无法匹配这些符号。因此,我们使用了一个新的范围参数smin=0.05和smax=0.5。这一更改将默认框的大小减半。虽然边界框的数量保持不变,但是网格结构被破坏了,因为边界框是两个小的,并且它们之间有间隙。因此,版本II可以更好地检测小符号,但整体检测能力有所下降。
    在这里插入图片描述
    版本III - SSD上更大的输入图像:这个版本继承了版本II,并改变了输入图像的大小从300 x 300到500 x 500。这反过来又增加了后续层中特征图的大小。随着特征图变大,预测框和默认框的数量也会增加。当出现更多的默认框时,网格结构会恢复,并且这个版本比之前的两个版本产生更好的预测。
    版本IV -卷积层较多的SSD:该版本继承了版本III,修改了现有的一个卷积层,在辅助结构中增加了两个新的卷积层,如图4所示。这增加了用于预测的特征图数量,提高了整体检测能力。

    在这里插入图片描述

结构分析

在生成图像中符号框的边界后,使用DRACULAE[7]解析器通过基线结构树(baseline structure tree, BST)和词汇基线结构树(Lexed-BST)生成LaTeX字符串。在原论文中,解析器有两种传递:布局传递、词法传递和表达式分析。但是,我们只应用前两次传递,因为我们的目标是生成LaTeX字符串,它从Lexed-BST比从操作树更容易创建。

  1. 布局传递:布局传递从包含位置和大小信息的边框列表中分析ME的结构。这一步骤的主要目标是创建一个BST,它是一个树结构,其中每个节点包含一个基线和表示它们在ME中的位置的notes之间的关系。为了创建BST,此布局传递执行以下步骤:

    (1)指定基线的开始符号:这个符号必须是最左边的,不能被任何其他符号所支配。
    (2)指定基线中的所有符号:在水平方向上与开始符号相邻的所有符号将保存在同一个节点中,其他的将作为适当节点的子节点放置。
    (3)重新定位节点:在此步骤中,节点的左上子节点将被重新定位到其上一个节点的右上节点。
    (4)处理子节点:经过前三步,子节点可以在一个节点中包含同一区域内不同基线的所有符号。因此,必须递归地处理这些子节点,以形成完整的BST。
    在这里插入图片描述

  2. 词法传递:这个传递的主要目标是从布局传递中生成的BST创建Lexed-BST。

    Lexed-BST是一种基于BST的树形结构;但是,BST中相邻的符号被组合成复合符号(等于号、小数、函数名等)或结构符号(分数、极限、和等)。其形成是基于空间位置和一些预定义的规则。在这项工作中,我们不使用复合符号,因为我们的目标是生成一个LaTeX 字符串,而不是计算任何实际的数学表达式。我们只需要决定哪些符号与结构符号相关,如积分或总和,以正确生成LaTeX 序列。

结论

在本文中,我们提出了一个基于序列方法的ME识别框架。在第一阶段——符号识别中,我们采用SSD,并对输入图像的大小、默认框的大小和SSD的架构进行了三次更改。这有助于我们的系统比原来的SSD更好地检测和识别小物体。在第二阶段——结构分析中,我们基于Richard Zanibbi等[7]提出的算法实现了一个解析器。解析器使用第一阶段的输出来构建经过Lexed的BST,然后使用BST生成相应的LaTeX字符串,从而帮助链接ME识别的两个阶段。此外,我们提供了一个包含手写数学符号和表情图像的数据库,用于训练SSD和评估系统。根据4个版本的映射,我们得出结论,前两个因素——调整输入图像的大小和调整默认框的大小——在提高性能方面起着主要作用。

个人理解

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个基于卷神经网络数字图片识别的Python代码实现,同时包含GUI界面的设计: ```python import numpy as np import tkinter as tk from PIL import Image, ImageDraw # 加载MNIST数据集 def load_data(): train_data = np.load('mnist_train_data.npy') train_label = np.load('mnist_train_label.npy') test_data = np.load('mnist_test_data.npy') test_label = np.load('mnist_test_label.npy') return train_data, train_label, test_data, test_label # 卷神经网络设计 class CNN: def __init__(self): self.conv1_filters = 8 self.conv1_kernel = 3 self.conv2_filters = 16 self.conv2_kernel = 3 self.hidden_units = 128 self.learning_rate = 0.01 self.batch_size = 32 self.epochs = 10 self.input_shape = (28, 28, 1) self.output_shape = 10 self.conv1_weights = np.random.randn(self.conv1_kernel, self.conv1_kernel, self.input_shape[-1], self.conv1_filters) * 0.1 self.conv1_bias = np.zeros((1, 1, 1, self.conv1_filters)) self.conv2_weights = np.random.randn(self.conv2_kernel, self.conv2_kernel, self.conv1_filters, self.conv2_filters) * 0.1 self.conv2_bias = np.zeros((1, 1, 1, self.conv2_filters)) self.dense_weights = np.random.randn(self.hidden_units, self.output_shape) * 0.1 self.dense_bias = np.zeros((1, self.output_shape)) def relu(self, x): return np.maximum(x, 0) def softmax(self, x): exp_x = np.exp(x - np.max(x, axis=1, keepdims=True)) return exp_x / np.sum(exp_x, axis=1, keepdims=True) def convolution(self, x, w, b): h, w_, in_channels, out_channels = w.shape pad = (h - 1) // 2 x_pad = np.pad(x, ((0, 0), (pad, pad), (pad, pad), (0, 0)), 'constant') conv = np.zeros((x.shape[0], x.shape[1], x.shape[2], out_channels)) for i in range(x.shape[1]): for j in range(x.shape[2]): for k in range(out_channels): conv[:, i, j, k] = np.sum(x_pad[:, i:i+h, j:j+h, :] * w[:, :, :, k], axis=(1, 2, 3)) conv = conv + b return conv def max_pooling(self, x, pool_size=(2, 2)): h, w = pool_size pool = np.zeros((x.shape[0], x.shape[1] // h, x.shape[2] // w, x.shape[3])) for i in range(pool.shape[1]): for j in range(pool.shape[2]): pool[:, i, j, :] = np.max(x[:, i*h:i*h+h, j*w:j*w+w, :], axis=(1, 2)) return pool def forward(self, x): conv1 = self.convolution(x, self.conv1_weights, self.conv1_bias) relu1 = self.relu(conv1) pool1 = self.max_pooling(relu1) conv2 = self.convolution(pool1, self.conv2_weights, self.conv2_bias) relu2 = self.relu(conv2) pool2 = self.max_pooling(relu2) flatten = np.reshape(pool2, (pool2.shape[0], -1)) dense = np.dot(flatten, self.dense_weights) + self.dense_bias softmax = self.softmax(dense) return softmax def backward(self, x, y, y_pred): error = y_pred - y dense_grad = np.dot(x.T, error) / len(x) dense_bias_grad = np.mean(error, axis=0, keepdims=True) error = error.dot(self.dense_weights.T) error = np.reshape(error, (-1, int(np.sqrt(error.shape[-1])), int(np.sqrt(error.shape[-1])), self.conv2_filters)) error = error * (self.conv2_weights[np.newaxis, :, :, :, :]) error = np.sum(error, axis=3) error = error * (relu2 > 0) conv2_grad = np.zeros(self.conv2_weights.shape) h, w, in_channels, out_channels = self.conv2_weights.shape pad = (h - 1) // 2 x_pad = np.pad(pool1, ((0, 0), (pad, pad), (pad, pad), (0, 0)), 'constant') for i in range(pool1.shape[1]): for j in range(pool1.shape[2]): for k in range(out_channels): conv2_grad[:, :, :, k] += np.sum(x_pad[:, i:i+h, j:j+h, :] * error[:, i:i+1, j:j+1, k:k+1], axis=0) conv2_grad /= len(x) conv2_bias_grad = np.mean(np.mean(np.mean(error, axis=1, keepdims=True), axis=2, keepdims=True), axis=0, keepdims=True) error = error * (self.conv1_weights[np.newaxis, :, :, :, :]) error = np.sum(error, axis=3) error = error * (relu1 > 0) conv1_grad = np.zeros(self.conv1_weights.shape) h, w, in_channels, out_channels = self.conv1_weights.shape pad = (h - 1) // 2 x_pad = np.pad(x, ((0, 0), (pad, pad), (pad, pad), (0, 0)), 'constant') for i in range(x.shape[1]): for j in range(x.shape[2]): for k in range(out_channels): conv1_grad[:, :, :, k] += np.sum(x_pad[:, i:i+h, j:j+h, :] * error[:, i:i+1, j:j+1, k:k+1], axis=0) conv1_grad /= len(x) conv1_bias_grad = np.mean(np.mean(np.mean(error, axis=1, keepdims=True), axis=2, keepdims=True), axis=0, keepdims=True) return dense_grad, dense_bias_grad, conv1_grad, conv1_bias_grad, conv2_grad, conv2_bias_grad def train(self, x_train, y_train, x_val, y_val): num_batches = len(x_train) // self.batch_size for epoch in range(self.epochs): print('Epoch {}/{}'.format(epoch+1, self.epochs)) for batch in range(num_batches): x_batch = x_train[batch*self.batch_size:(batch+1)*self.batch_size] y_batch = y_train[batch*self.batch_size:(batch+1)*self.batch_size] y_pred = self.forward(x_batch) dense_grad, dense_bias_grad, conv1_grad, conv1_bias_grad, conv2_grad, conv2_bias_grad = self.backward(x_batch, y_batch, y_pred) self.dense_weights -= self.learning_rate * dense_grad self.dense_bias -= self.learning_rate * dense_bias_grad self.conv1_weights -= self.learning_rate * conv1_grad self.conv1_bias -= self.learning_rate * conv1_bias_grad self.conv2_weights -= self.learning_rate * conv2_grad self.conv2_bias -= self.learning_rate * conv2_bias_grad y_train_pred = self.predict(x_train) y_val_pred = self.predict(x_val) train_acc = np.mean(np.argmax(y_train, axis=1) == np.argmax(y_train_pred, axis=1)) val_acc = np.mean(np.argmax(y_val, axis=1) == np.argmax(y_val_pred, axis=1)) print('Train accuracy: {}, Validation accuracy: {}'.format(train_acc, val_acc)) def predict(self, x): y_pred = self.forward(x) return y_pred # GUI界面设计 class GUI: def __init__(self, cnn): self.cnn = cnn self.window = tk.Tk() self.window.title('Handwritten Digit Recognition') self.canvas = tk.Canvas(self.window, width=200, height=200, bg='white') self.canvas.grid(row=0, column=0, padx=10, pady=10) self.canvas.bind('<B1-Motion>', self.draw) self.button_recognize = tk.Button(self.window, text='Recognize', command=self.recognize) self.button_recognize.grid(row=0, column=1, padx=10, pady=10) self.button_clear = tk.Button(self.window, text='Clear', command=self.clear) self.button_clear.grid(row=1, column=1, padx=10, pady=10) self.label_result = tk.Label(self.window, text='Please draw a digit', font=('Helvetica', 18)) self.label_result.grid(row=1, column=0, padx=10, pady=10) def draw(self, event): x = event.x y = event.y r = 8 self.canvas.create_oval(x-r, y-r, x+r, y+r, fill='black') def clear(self): self.canvas.delete('all') self.label_result.config(text='Please draw a digit') def recognize(self): image = Image.new('L', (200, 200), 'white') draw = ImageDraw.Draw(image) draw.rectangle((0, 0, 200, 200), fill='white') self.canvas.postscript(file='tmp.eps', colormode='color') eps_image = Image.open('tmp.eps') image.paste(eps_image, (0, 0)) image = image.resize((28, 28)) image = np.array(image) image = image.reshape((1, 28, 28, 1)) y_pred = self.cnn.predict(image) label = np.argmax(y_pred) self.label_result.config(text='Result: {}'.format(label)) def run(self): self.window.mainloop() # 主程序 if __name__ == '__main__': train_data, train_label, test_data, test_label = load_data() cnn = CNN() cnn.train(train_data, train_label, test_data, test_label) gui = GUI(cnn) gui.run() ``` 请注意,该代码实现需要下载MNIST数据集(包括四个.npy文件),并且需要安装Python的`numpy`、`tkinter`和`Pillow`库。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值