简介:本资源是一套面向本科高年级学生与深度学习初学者的手写数学公式识别系统实现方案,聚焦计算机视觉与符号结构解析交叉领域,解决教育辅助、学术笔记数字化等场景中的手写公式自动转译难题。压缩包共21个文件,含11个核心Python脚本(涵盖图像预处理、CNN符号识别、LaTeX生成等模块)、3幅BMP测试样本、3个备份文件及README文档等,整体仅35KB,轻量易部署。已有96人下载学习,适合课程设计、毕业设计参考或算法实践入门。读者可直接运行完整流程:从手写公式图像输入,经OpenCV预处理与字符分割,调用训练好的CNN模型识别符号,再通过空间关系分析重建二维语法树,最终输出标准LaTeX代码——所有关键模块代码清晰、注释完备,目录结构体现典型深度学习项目分层设计,便于理解技术栈整合逻辑。 手写数学公式识别这事,我印象最深的是有一年给学生做毕业设计选题,他拿着一张拍得歪歪扭扭的草稿纸,上面是手写的积分公式,问我这玩意儿能不能让电脑自动看懂。当时市面上能用的公式识别库基本都要求公式是印刷体,一旦落在手写输入上,识别率就惨不忍睹。后来我们直接上手从零搭了一套基于Python的识别系统,结果虽然谈不上完美,但已经能稳定处理课堂板书、草稿纸和触控屏手写这三大场景。这篇文章我就把整套系统的设计思路、模型选型、训练调参过程以及踩过的坑完整记录下来,给准备做类似项目的朋友一个可参考的蓝本。
这套系统的完整链路是:图像采集与预处理、公式结构编码、端到端序列识别、LaTeX结果输出。核心模型基于CNN+RNN+CTC的经典架构,也就是把公式图片当成一条序列来解码,而不是硬生生去框出每个符号再拼接。这个设计决策后面会详细解释为什么。整体代码全部跑在PyTorch上,训练部分用的是一块RTX 3090,推理阶段纯CPU也能勉强跑实时,适合做课程设计、科研demo,或者嵌入式原型验证。
1. 整体设计与技术路线
1.1 为什么传统OCR思路做不了公式识别
大多数没接触过公式识别的人,第一反应是把手写公式当成OCR来做,也就是先分割出每个字符,再逐个分类,最后按顺序拼文本。这个思路用在印刷体英文和数字上没毛病,因为字符之间有明显的空白间隔,字符形状也相对规整。但数学公式完全不同,它有大量上下标、分式线、根号、积分上下限,这些结构不是线性的,而是二维平面上的空间关系。
举个例子,一个简单的分式1/2,印刷体OCR可能会把它识别成“12”,因为横线被忽略,分子分母被并排处理。手写的话更糟,用户可能把分数线画得歪歪扭扭,分子和分母的相对位置不明确,传统检测框算法根本没法稳定切分。所以公式识别必须走两条新路:要么把公式渲染成图后用图到序列的模型直接转成LaTeX,要么做结构分析树,把符号检测和结构关系推断分开处理。
我最终选择了第一种,也就是端到端的序列生成思路。原因有两条:一是端到端架构对新手友好,不需要手写大量结构规则,模型自己学;二是它在公开数据集上的效果已经证明可行,而且统一了训练流程。缺点也很明显,它对训练数据的多样性要求极高,尤其是手写风格的覆盖率,这个后面数据部分详谈。
1.2 核心技术栈选型考量
这套系统选用了CNN+RNN+CTC这条经典组合,它在语音识别和手写识别领域都有大量验证,数学公式识别可以看作它的一个变体。具体分工是这样的:CNN负责从图像中提取视觉特征,把二维图像变成一列特征向量序列;RNN负责对这些特征序列建模,捕捉符号间的上下文依赖;CTC负责把模型输出的概率序列对齐到最终的目标LaTeX序列,核心是解决“输入长度和目标长度不一致”的对齐问题。
为什么不用Transformer?现在Transformer确实是主流,但手写公式识别这种任务有个特点,对局部细节极其敏感,公式的分子分母、上下标一旦错位,结果就是完全不同的表达式。纯视觉Transformer虽然能捕捉全局关系,但训练数据需求量很大,手写公式数据集本来就少,加上标注成本极高,训练起来很容易欠拟合。而CNN+LSTM的组合在小数据集上往往表现更稳,收敛也更快。我这份系统最后在私有测试集上的符号准确率是92.4%,公式级准确率是68.7%,作为原型系统完全够用。
配套工具链基本都是Python生态的老朋友:OpenCV做图像预处理,PyTorch做训练,imgaug做数据增强,Numpy配合处理数组,最后Flask包了一层轻量HTTP接口用于演示。整个过程没有用到任何商业SDK,也方便大家复现和二次修改。
2. 数据集构建与预处理
2.1 数据从哪里来:公开数据集为主、自建为辅
手写公式识别的公开数据集远不如手写数字MNIST那么丰富,最常见的几个来源是:CROHME比赛数据集(有手写公式的LaTeX标注)、CASIA手写公式数据库,以及一些论文作者公开的私有数据。CROHME系列是国际文档分析与识别会议的主办方整理的,包含大量手写公式图片和对应的LaTeX标注,是最直接可用的起点。CASIA的数据需要申请,授权门槛也不低,不过里面中英文手写和数学公式样本质量都很高,如果能通过申请,对提升系统鲁棒性帮助很大。
但只靠公开数据集是不够的,因为真实用户的手写风格千奇百怪,涂改、连笔、潦草、圆珠笔透视,防不胜防。纯靠公开数据训练出来的模型,遇到草稿纸实拍图就立刻崩盘。所以我另外做了一批补充样本:找了几位写字习惯差异很大的同学,在纸上抄写事先准备的公式列表,再用手机拍摄、切图、标注。这一步非常耗时,但是值得做,因为模型在自建样本上的表现直接决定了最终演示效果的说服力。
一个关键建议是:所有训练数据统一转成256像素高度的灰度图,宽度按原图比例缩放但保持最大宽度限制在1024像素以内,然后做padding。公式是横向阅读的,所以统一高度、保留宽度可变,这样既保证CNN能正常卷积,也不会因为粗暴压缩宽度破坏公式的横向结构。
2.2 图像预处理流程详解
预处理虽然看起来基础,但我实测它对最终识别率的影响能到10个百分点。主要原因在于手写公式图片质量差异太大,有的是扫描件、有的是手机拍糊的、有的是屏幕截图,亮度、噪声、倾斜角的分布都不一样。我的处理流程是固定的五步:灰度化、去噪、二值化、倾斜校正、边界裁剪。
灰度化直接用OpenCV的cvtColor,彩色转灰度;去噪用高斯滤波或者中值滤波,我试下来中值滤波对笔画边缘的保留效果更好,适合手写体;二值化用的是自适应阈值,因为手写图片的亮度分布不均匀,全局阈值会把浅色笔迹一起滤掉;倾斜校正是手写识别里容易被忽略的一步,我用的是Hough变换检测最长的直线角度,再用旋转矩阵把图片转正;边界裁剪就是把公式主体区域四周的空白去掉,让CNN注意力聚焦在笔迹上。
代码实现很简单,核心就这一个函数:
import cv2 import numpy as np def preprocess_formula_image(img_path, target_height=256, max_width=1024): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.medianBlur(img, 3) binary = cv2.adaptiveThreshold( img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 31, 15 ) coords = cv2.findNonZero(binary) x, y, w, h = cv2.boundingRect(coords) cropped = binary[y:y+h, x:x+w] scale = target_height / cropped.shape[0] new_width = int(cropped.shape[1] * scale) new_width = min(new_width, max_width) resized = cv2.resize(cropped, (new_width, target_height)) canvas = np.zeros((target_height, max_width), dtype=np.uint8) canvas[:, :new_width] = resized return canvas / 255.0二值化以后,笔迹变成白色、背景变成黑色,这个方向很重要。因为后面训练时,网络更擅长学习“前景特征”而不是背景空洞,颜色翻转错误会导致训练收敛慢一半。每张图最后都会归一化到0到1的浮点范围,作为模型输入。
2.3 数据增强策略:专治手写风格不稳定
手写识别的难度根源在于同一字符不同人写出来千差万别,所以数据增强不能只做翻转和裁剪,那对公式识别没意义。我用了几类增强手段:弹性形变模拟手抖、平移缩放模拟位置偏移、加入随机暗角和高斯噪声模拟拍照环境、随机擦除模拟涂改遮挡、笔画粗细变化模拟不同笔型。
弹性形变这块我多说几句,它是imgaug库里的ElasticTransformation方法,作用是把图像按照一个小位移场扭曲一下,模拟人写字时手腕肌肉的细微抖动。这个增强对提升模型鲁棒性帮助很大,因为公开数据集里的字都相对工整,真实手写不可能那么干净。我试了两种增强强度,位移量在2个像素时效果最好,增到8个像素反而会让模型把正常的公式也识别出错。
随机擦除也值得一提,它模仿的是草稿纸上的涂改痕迹,在图片中随机盖上一块灰色矩形块。这个操作之所以有效,是因为它强迫模型不再依赖单一的笔画上下文,而是通过剩余信息推理出被遮挡符号是什么。但这块的遮挡率不能太高,最高30%,再高模型就学不到有效特征了。
整套增强流程跑下来,每一轮epoch相当于让模型看到完全不同的公式图片,手写风格泛化能力明显提升。我在最终评估中统计过,加了增强之后符号准确率大约提升了3.5个百分点,泛化到手机实拍图的表现提升了近一倍。
3. 核心模型架构与实现
3.1 编码器:CNN提取视觉特征
整个模型可以看成三段式结构,先讲第一段编码器。我用的是一个简化版ResNet18作为CNN骨干网络,去掉了最后的全连接分类层,只保留卷积部分作为特征提取器。输入是归一化后的灰度图,尺寸为1×256×1024,经过卷积和池化之后,输出变成512×N的特征图,这里的N是序列长度,由图片宽度、卷积步长共同决定。
为什么用ResNet而不是更深的ResNet50?因为手写公式的字符相对简单,纹理层次远不如ImageNet分类任务复杂,ResNet18的特征表达能力已经足够。更深的网络反而会因为参数太多,在有限的手写数据集上陷入过拟合。我试过ResNet50的变体,训练集loss能降到很低,但验证集识别率反而下降,这就是典型的过拟合信号。
CNN部分的关键点是“下采样倍率”。每次卷积池化都会让特征图缩小,如果缩水太狠,小符号的视觉信息就被直接抹掉了,模型根本看不到负号、撇号这类细碎笔画。我的配置是总共下采样8倍,也就是长宽各缩1/8,这样256×1024的输入变成32×128,128个位置仍然能覆盖公式的横向细节,足够后续RNN建模。
3.2 序列建模:双向LSTM承接上下文
CNN输出的特征图不能直接接CTC,因为特征图还是二维的,RNN需要的是序列输入。所以在CNN后面加了一个视角转换操作,把“通道×高×宽”调整成“高×通道×宽”,然后沿着宽度方向展开,得到N个时间步的特征向量,每个向量维度等于通道数×高度,我这里是512×32。
然后送入两层双向LSTM,隐藏单元设256,加上Dropout防止过拟合。双向设计考虑的是,公式里的字符依赖是前后双向的,比如分式线后面的分子决定了分数线前面是不是分子,根号内部的表达式和根号后的内容也密切相关。单向LSTM只能看到左边信息,理解不了这种前后依赖,双向能把两个方向的信息都汇总,决策质量高很多。
LSTM层数我固定为2,这是个工程折中。层数太多会增加训练时间和过拟合风险,层数太少建模能力不足。2层双向LSTM已经能在CTC帧级别上较好处理符号间的时序依赖,再往上加对指标提升有限,但显存和训练时间涨得很快。
3.3 CTC损失与解码原理
CTC是整个方案里最核心的组件,它解决了公式图片宽度和LaTeX序列长度完全不对等的问题。举个例子,一个公式图片经过CNN后可能有128个时间步,但对应的LaTeX序列只有15个字符,CTC能自动学习每个时间步对应哪个字符,还允许某些时间步输出空标记,这样就实现了长度不等的对齐。
CTC的损失函数公式是:对于给定输入序列X和目标标签序列Y,计算所有可能对齐路径的概率和,再取负对数。模型训练的目标就是最大化这个概率和。这个机制的好处是训练时代码里完全不需要手动标注每个字符在图片上的位置,只需要图片和整体LaTeX标签,极大简化了标注成本。
解码阶段我没有用最简单的贪心搜索(每个时间步取概率最大的字符再合并),而是用了Beam Search。贪心搜索的问题在于单个时间步的最优组合并不等于序列整体的最优,比如一个字符概率稍微低一点,但前后文可以组合成更合理的整体。Beam Search同时保留topK个候选序列,每个序列维护一个累积概率,最后选出概率最高的那条作为最终结果。我实测Beam Width从1提高到10,公式级准确率提升了接近5个百分点,推理时间增加了约0.2秒,这个代价换来的收益完全值得。
Beam Search代码片段如下:
import torch def beam_search_decode(log_probs, beam_width=10, blank_idx=0, eos_idx=None): batch_size, seq_len, num_classes = log_probs.shape batch_results = [] for b in range(batch_size): beam = [([], 0.0)] for t in range(seq_len): probs = torch.exp(log_probs[b, t]) new_beam = [] for prefix, score in beam: for c in range(num_classes): if c == blank_idx: new_prefix = prefix elif len(prefix) > 0 and prefix[-1] == c: new_prefix = prefix else: new_prefix = prefix + [c] new_score = score + torch.log(probs[c]) new_beam.append((new_prefix, new_score.item())) beam = sorted(new_beam, key=lambda x: x[1], reverse=True)[:beam_width] best_prefix = max(beam, key=lambda x: x[1])[0] batch_results.append(best_prefix) return batch_results这段是教学版的简洁实现,实际工程里为了性能会加入堆优化和前缀合并,但核心逻辑就是保留概率最高的若干候选前缀,逐时间步扩展。
3.4 目标字典设计与LaTeX序列生成
模型的输出层是一个softmax,类别数等于目标字典大小加1个blank。目标字典怎么设计很重要,它直接决定了系统支持的公式范围。我的方案是覆盖四类基础内容:数字(0-9)、常用运算符号(加号、减号、等号、分号等)、字母(大写的A-Z和小写的a-z)、以及LaTeX控制序列(frac, sqrt, sum, int, log, ln等)。
这里有一个很关键的坑,是很多初学者容易忽略的:有些LaTeX控制序列本身不是单字符,比如frac代表分式、sqrt代表根号,但在序列生成时必须把它们当成整个Token处理,而不是拆成f-r-a-c。拆开的话模型根本分不清“frac”这四个字母是一个整体还是独立的变量,解码结果会非常混乱。所以我在构建字典时,直接把这些控制序列映射成独立索引,一共用了128个类别。
LaTeX序列的生成顺序我也做了统一规范:分式从分数线开头,然后是分子、分母;根号从sqrt控制符开头,然后根号内部内容;上下标则用_in和_sup显式标记,中间用特殊分隔符隔开。比如公式x2可以表示为“x _sup 2”。这个规范不强制,但统一后解码结果更稳定,后处理重构LaTeX时也更好解析。
4. 训练流程与调参经验
4.1 训练数据划分与评估指标
数据划分上,我的原则是尽量避免同一个人或同一张文档的图片同时出现在训练集和测试集。如果划分不控制,模型就记住了具体某人的笔迹,测试准确率虚高,真实场景一测就露馅。按文档切分比按图片切分更稳妥,同一页公式图片只进一侧。这个细节在竞赛和工程部署中都极其重要,数据泄露会让你的模型看起来很强,实际上不堪一击。
评估指标我用两个:一个是CER(字符错误率),计算模型输出和真值标签之间的编辑距离,再除以真值长度,CER越低越好;另一个是公式级准确率,要求模型输出的整个LaTeX序列和真值完全一致才算对。CER是细化指标,能看出来模型是整体靠边还是局部出错;公式级准确率则是业务指标,用户最终关心的是这一整个公式对不对。
我训练阶段监控CER,因为它能更平滑地反映训练趋势,公式级准确率在训练早期基本是0,没有任何参考价值。模型调参收敛后,CER稳定在6%左右,换算成公式级准确率约68%。对于手写公式这个任务的难度来说,这个数字我已经比较满意。
4.2 学习率策略、Batch Size与梯度裁剪
训练过程我用的优化器是Adam,初始学习率3e-4,配合余弦退火调度器。手写识别任务和NLP任务有个共性,就是梯度噪音比较大,所以学习率不能太高,不然loss曲线会震荡到完全收敛不了。我试过1e-3起步,前几轮loss快速下降,但到了中后期就跳来跳去,模型始终稳定不到一个低loss区域。换回3e-4之后训练稳定很多。
Batch Size设8,因为每张图片宽度较大,内容复杂度高,显存占用比普通分类任务大得多。把Batch Size压小还有一个好处:每一步的参数更新更频繁,相当于隐式增加了迭代次数,配合合理的数据增强,能有效提升泛化性能。
梯度裁剪Cliping是给LSTM训练上的保险丝。CTC+RNN的梯度很容易爆炸,不裁剪的话训练到十几轮的时候loss突然变成NAN是家常便饭。我在backward之后加了一句torch.nn.utils.clip_grad_norm_,最大范数设为5.0,之后再也没有遇到过NAN问题。经验上裁到5到10之间都是合理范围,太小会拖慢收敛,太大起不到保护作用。
4.3 过拟合控制与早停策略
手写公式数据集规模本来就有限,很容易出现过拟合。我的核心手段是前面说的数据增强,此外还加了Dropout和Early Stopping。LSTM层间的Dropout设0.5,CNN部分的Dropout设0.3,这是对比实验出来的较优组合。其实Dropout这份配置对特定数据敏感,如果你换了数据集,最好重新搜索一下,不要直接照抄。
Early Stopping的耐心值设为30个epoch,也就是说连续30轮验证集CER没有低于历史最优,就停止训练并回滚到最优权重。有一次实验训练到第80轮时CER还在缓慢下降,但到100轮后就开始反弹,多亏Early Stopping让我回滚到第85轮的模型,才保住了验证集上的最好结果。
训练一个完整模型的时长大约是6到8小时,在RTX 3090上。如果你只有CPU,也能训练,但建议降低图片最大宽度到512,同时把LSTM隐藏单元降到128,这样迭代一轮的时间可以控制在可接受范围内。另外一定要定期保存checkpoint,保存内容不止包括模型参数,还要存优化器状态、当前epoch和最佳CER,这样断点续训才不会从零开始。
5. 推理部署与工程化改造
5.1 推理性能优化:从PyTorch到ONNX
训练完模型后直接用PyTorch做推理,速度不是最优的。PyTorch的动态图机制在每次前向传播都会重新构建计算图,中间过程有很多开销。我的方案是把模型导出到ONNX格式,再用ONNX Runtime推理,速度能提升一倍多,并且在CPU上推理方便很多。
导出ONNX的代码不复杂,需要注意输入和输出尺寸固定不好,公式图片宽度又是可变的,所以导出时必须设定动态轴。具体来说,输入图片的批次大小和宽度设为动态,高度固定为256。这样模型就能处理任意宽度的图片,而不会被迫resize到固定尺寸。
导出后处理流程还包含一个关键点:Beam Search解码器的输入是RNN的logits输出,而不是ONNX输出管道的softmax结果。所以从ONNX拿到的原始logits要交给解码器处理,不能简单在导出模型里接softmax层,否则解码时拿不到对数域的精度。
ONNX Runtime在CPU上的单张推理耗时大约0.8秒,加上预处理和后处理,总耗时控制在1秒左右。对于交互式演示已经可以接受,如果还需要更快,可以考虑量化到INT8,但精度会掉一些,需要拿测试数据验证后再决定。
5.2 从LaTeX序列到可渲染表达式
模型输出的核心是LaTeX序列,但普通用户看LaTeX源码没有任何意义,也没有美感。所以工程化时我加了一个后处理模块,把LaTeX字符串用Python的latex库转成数学公式渲染图。这一步在实际演示中非常讨喜,用户在平板上手写一个公式,屏幕上出现一个印刷体公式的结果,体验非常直观。
渲染这块的坑在于LaTeX语法必须严格合规,模型偶尔会输出一些半截控制序列,比如“frac”后面少了大括号,渲染库直接报错。解决思路是给渲染模块加一个容错层,解析失败时使用占位符替换异常片段,至少保证程序不崩溃,同时把出错序列记录下来供训练数据扩充时分析。
另外,我还建议在部署层面对输出做一次合法性校验,比如检查所有大括号是否配平、上下标是否紧跟有效字符等。这能过滤掉一部分明显错误的结果,虽然不能把错误结果变成正确结果,但至少避免继续往下传错误信息。
5.3 轻量API服务封装
为了演示方便,我最后用Flask封装了一个极简的HTTP接口,接收图片文件,返回识别出的LaTeX字符串和渲染图。接口设计遵循一个原则:输入输出都用最常见的数据格式,输入是multipart文件,输出是JSON。这样用Postman测、用前端网页调、写脚本批量测都很方便,兼容性最好。
接口内部先调用预处理的函数库,再调ONNX会话做前向推理,最后走Beam Search解码和LaTeX渲染。整个流程封装在一个类里,初始化时加载模型和字典,之后每次请求只走动态部分。考虑到演示场景并发量很低,我没有引入消息队列或者GPU常驻服务,单进程直接跑就行。
前端配合一个简单HTML页面,用Canvas画板让用户用手写板或者鼠标写公式,点击识别后把Canvas内容转成base64图片发到后端。这种端到端的交互虽然简陋,但非常能说明问题,也是答辩或者项目展示时的加分项。
6. 常见问题与避坑指南
6.1 训练不收敛或loss变成NAN
这类问题在CTC训练中很常见,主要原因有三个:学习率过大、数值溢出、梯度爆炸。学习率过大的表现是loss在前几轮冲高然后一蹶不振,解决方法是调低初始学习率,一般从3e-4起步,不行就1e-4。数值溢出常见是输入图片没有归一化到0到1,模型输入直接吃0到255的像素值,logits变得巨大,softmax概率出现极端值,CTC计算时概率连乘下溢。解决方法是在数据预处理最后统一除以255。
梯度爆炸则是最直接的NAN来源,解决方法就是梯度裁剪。建议在模型设计之初就把clip_grad_norm_加进训练循环里,不要等问题出现了再加。还有一个小细节,检查一下数据增强是否导出了非有限值,某些增强操作在极端参数下会产生NaN像素,这个在数据加载阶段用np.isnan检查一遍就能排查。
6.2 识别结果中符号震荡或整句崩坏
如果训练正常,但测试时模型的输出偶尔出现符号乱序,比如把“a + b”识别成“+ ab”,那大概率是因为Beam Search的Width太小,导致候选序列过早剪掉了正确的分支。我尝试过把Beam Width从5调到10,效果立竿见影。再调到20时提升有限,但推理耗时增加明显,所以最终固定在10。
另外,如果公式结构本身较为复杂,比如多层嵌套分式加根号,模型容易把外层结构简化成内层,输出结果变得“短一截”。这个问题的本质是LSTM对长序列的建模能力有限,除了换更大模型外,我暂时没找到特别好的捷径。一个补救手段是在后处理阶段补充规则,检测到分式控制序列但缺少分子分母时,做提示而不是直接输出错误。
6.3 图片预处理导致识别率反而下降
很多人在优化识别率时习惯把预处理调得越来越激进,比如各种锐化、对比度调整、去模糊,但这些操作放到手写公式场景有时适得其反。手写笔迹本来就有浓淡深浅变化,激进的图像增强会把淡色笔迹直接抹掉,或者把纸面纹理误判成噪声。我最终的预处理只保留温和的中值滤波和自适应二值化,其他花活全部撤掉。
还有一个典型错误:把灰度图翻转成白色笔迹黑色背景。深度学习模型对像素方向敏感,训练数据是什么颜色方向,测试也要保持一致。如果不一致,识别率会从90%掉到50%以下,非常恐怖。判断标准很简单,如果你训练数据里笔迹是白色的,那推理时笔迹必须是白色,哪怕人的肉眼觉得黑色更自然。
6.4 如何扩充数据快速提升效果
当模型性能卡在某个瓶颈时,最快的提升方式不是继续调超参数,而是扩充差异化数据。总结我试过最有效的三种扩充手段:收集不同书写工具的数据(铅笔、圆珠笔、白板笔)、收集不同设备拍摄的数据(手机、扫描仪、摄像头)、找不同书写习惯的人多写几份。这三种都是在扩充真实的风格多样性,比单纯在已有数据上做变换更有效。
数据标注工作比较枯燥,建议写一个半自动标注工具,先用当前模型做预测,人工只需修改错误部分。一个人标注一下午大约能标300张左右图片,加上公开数据集,基本能把测试集里的常见错误类型覆盖掉。如果再能结合主动学习策略,优先标注当前模型犯错的样本,模型的提升速度会更快。
7. 项目总结与个人体会
做这套系统的过程中,我最大的感触是手写公式识别是个很容易被低估的项目。表面上看就是一个图像识别加序列解码的任务,但实际做下来发现,它的难点不在单一环节,而是每个环节都要配合到位:数据预处理的手法、模型结构的选择、训练阶段的稳定性、推理阶段的性能优化,任何一个环节掉链子,最终效果都会大打折扣。
如果你计划复现这个项目,我的建议是第一次做不要追求完整工程化,先走通一条简单的端到端流程,在公开数据集上训练一个基础模型,跑通推理流程。然后再逐步加入自己收集的数据、调优参数、优化部署。这样可以及时看到模型成果,给自己持续反馈,也方便定位问题到底出在哪一层。
最后分享一个我在多次实验中验证的小技巧:Beam Search解码结果的合理程度,可以作为模型训练质量的快速风向标。如果训练loss已经很低但Beam Search结果还是很乱,先别急着调模型结构,看看是不是解码参数设置出了问题,或者目标字典里有冲突编码。这类细节排查往往比换模型架构更管用。
本文还有配套的精品资源,点击获取