拓冰建站拓冰建站
首页 / 资讯中心 / 正文

手写数学公式识别实战:基于ResNet与Transformer的完整方案

简介本资源是一套基于ResNet与Transformer混合架构的手写数学公式识别Python实现面向深度学习初学者与计算机视觉方向进阶学习者解决教育、科研场景中手写公式图像到LaTeX序列的端到端识别问题。压缩包共40个文件含19个核心Python源码涵盖数据模块datamodule、编码器encoder、解码器decoder、位置编码pos_enc、训练脚本train.py及推理脚本reco-v1.1.py等、8个编译缓存文件、6个备份文件.zbak、3个说明类文本含使用指南与结果示例以及配置文件config.yaml、setup.cfg和模型权重相关zip附件整体大小4.21MB结构清晰、模块职责分明便于理解多模态特征提取与序列建模协同机制。已有151人学习下载代码为高分课程大作业成果经导师认可并完成全链路调试附带完整训练/验证/测试流程与词汇表dictionary.txt、词典构建vocab.py等关键组件可直接运行复现是深入掌握CNN-Transformer融合设计在符号布局理解任务中应用的优质实践范例。 手写数学公式识别这个方向我前后做了三轮迭代从最初的定式识别到最终的端到端序列生成踩了不少坑也实实在在跑通了一条完整路线。如果你正在做类似的课设、毕设或者想在简历上放一个“有技术含量、能讲清楚、代码还拿得出手”的视觉项目那这篇基于ResNet与Transformer的Python实现值得你从头跟到尾。这个项目做的是把一张手写数学公式的图片直接转成对应的LaTeX代码。比如你拍下来或者手写板写出来的积分式、分式、根式模型输出一段字符串渲染出来就是标准公式。它解决的不是“识别某个数字/字母”这种单字符问题而是把公式当作一个结构化的序列问题来处理——这恰恰是手写公式识别最核心的难点。适合看这篇的人有两类一是正在做课程设计、毕业设计需要一套能跑、能讲、能调参的完整代码二是对视觉特征提取和序列生成结合感兴趣想用一套编码器-解码器架构练手深度学习基本功的。我会把从数据准备、模型搭建、训练调优到踩坑修复的完整流程都拆开讲包括很多代码注释里看不到的细节。1. 任务拆解为什么手写公式识别不是“多一个分类任务”很多人第一次接触这个项目以为和手写数字识别差不多图片进去标签出来。但手写公式完全不同它本质上是“视觉结构理解 序列生成”的复合问题。1.1 手写公式识别的核心难点在哪先看一张公式图比如∫(x²1)dx它本身是一个二维结构积分号、上下限、被积表达式、微分符号这些元素之间有空间位置关系。传统的OCR管道很难处理这种二维布局因为字符之间的“关系”比字符本身更重要。你可以把公式想象成一句话但这句话的词序不是从左到右排列的而是上下左右嵌套的。具体难点有三块其一字符类别本身很多包括数字、字母、希腊字符α、β、Σ、运算符号∫、∑、√、±、括号等类别数量轻松上百其二手写变体大同一个“x”在不同人笔下可能形状差很多而且还存在和“α”这种形近字的混淆其三也是最重要的——结构识别。比如一个分式模型必须知道“上面是分子、下面是分母、横线在中间”而不是简单地输出一串字符。这条横线所在的位置、长度直接决定了公式的树状结构。1.2 为什么选ResNet Transformer而不是纯CNN或RNN上上轮我用的是CNN BiLSTM CTC的思路效果还行但对公式这种强结构任务CTC的局限很明显。CTC适合“序列对序列”的对齐问题比如语音识别或印刷体行文字它假设输出是按时间步单调排列的。但公式的树状结构决定了视觉特征不能简单地按从左到右的“时间流”来组织——分子可能在分母的上面而LaTeX序列的生成顺序却是“\frac{分子}{分母}”这种东西跳跃CTC处理起来很吃力。后来换成ResNet Transformer主要是因为Transformer的解码器天生适合自回归生成而且它的注意力机制能显式建模“当前要生成的token该看图片的哪个位置”。这个特性对公式识别极其友好生成分式时解码器会把注意力从分式线位置转移到分子区域再转移到分母区域每一步都有明确的“视觉依据”。ResNet作为编码器则负责把图像降维、提特征在保持空间信息的同时抽取出高层语义。简单说ResNet负责“看图”Transformer负责“写LaTeX”两者分工明确各干各最擅长的事。1.3 项目的整体技术路线一句话概括流程图像输入 → ResNet卷积特征提取 → 特征图展平 → Transformer解码器自回归生成LaTeX token序列 → 后处理渲染。整体架构我画了个简化的数据流图像先过ResNet得到C×H×W的特征图C是通道数H和W是特征图的高宽然后把H×W的每个位置当作一个“视觉token”加上位置编码后交给解码器。解码器每一个时间步生成一个token字符或LaTeX命令的片段生成到结束符停止。训练时用teacher forcing加速收敛推理时用beam search提升精度。2. 数据准备与预处理CROHME数据集和tokenizer的设计细节聊完任务紧接着就是数据。很多第一次做这个项目的人拿不到靠谱数据就在本地手写了几十张图片去训练结果模型完全没见过世面效果自然一塌糊涂。这个项目必须用标准数据集才有可比性。2.1 CROHME数据集手写公式识别的标准基准我用的是CROHMECompetition on Recognition of Online Handwritten Mathematical Expressions这是手写公式识别领域最常用的公开基准。它的图片来自真实手写样本带完整的LaTeX标注。CROHME有多个年份版本做项目建议直接用CROHME 2016或2019的拆分因为这两个版本的在网上能找到现成的图片格式转换工具不用自己折腾iPad采集。拿到数据后要做两件事第一把原始的在线轨迹数据inkml格式渲染成离线图像渲染时统一用白底黑线线条宽度保持固定这样模型不会受到笔刷粗细干扰第二把LaTeX标注统一标准化比如去掉多余空格、统一花括号写法确保同一句话只有唯一的标准输出形式。2.2 LaTeX序列的tokenizer设计模型输出是token序列怎么把一段LaTeX字符串切分成token直接影响训练效果和生成质量。我的做法是先用一个常见的LaTeX公式预料统计出高频片段把“\frac”“\sqrt”“\int”这类高频latex命令作为整体token保留而不是按字符切开。示例说明一下按字符切分\frac{1}{2}会变成\frac{1}{2}序列长度长且语义破碎。按词表切分\frac是一个token{可能是一个token1又是一个token序列短且语义完整。实际训练中我发现按字符切分会让Transformer很难学到“结构跳转”的规律因为它的每一步推理都需要跨越多个细碎token才能完成一次结构变化。而按命令级切分解码器学起来明显更轻松。tokenizer用现成的BPE库比如sentencepiece就能做但训练语料只限于LaTeX公式不要拿别的文本语料去训练。2.3 图像预处理的几个细节图像归一化和尺寸调整直接决定训练能否收敛。我用的方案是先把所有图片统一缩放到128×128或160×160保持宽高比不变剩余部分用白色填充。缩放过程使用双线性插值保持文字边缘不过度模糊。归一化时按均值0.5、方差0.5做标准化把像素值拉到[-1,1]区间。这点很常规但有个容易被忽略的细节测试时如果图片是手机拍的背景不够白、有阴影最好先做一次阈值化预处理否则识别率会明显下降。我在实际使用中遇到过训练集都是白底黑字测试集来了个灰度底、带噪点的图Beam Search出来的结果完全跑偏。先做Otsu二值化能缓解这个问题。3. 模型架构实现ResNet编码器和Transformer解码器的代码拆解接下来是整篇文章的核心部分模型怎么搭。这一段我直接贴核心代码逻辑并逐层解释设计原因而不是简单给个完整文件了事。3.1 ResNet编码器不是简单的分类网络ResNet可以直接用torchvision里预训练好的ResNet18或ResNet34但必须做两头修改。第一头把最后的全连接层和全局平均池化拿掉。公式识别不是图像分类我们不需要一个长度为1000的特征向量而是需要保留空间位置信息的特征图。拿掉池化和全连接后如果输入是3×128×128经过ResNet18的四个stage后输出大概是512×8×8的特征图。这个8×8的每个格子都对应原图16×16的一个区域包含局部视觉信息。第二头把输入层从3通道改成灰度输入。因为手写公式图本质是二值或灰度图3通道是冗余的。可以把图片直接复制成3通道喂给预训练模型省事也可以用Conv2d(1, 64, ...)替换第一层并加载权重时跳过严格匹配。代码里我两种都写过推荐后者因为能节省一点计算量。再说下为什么选用ResNet而不是VGG。深度网络的梯度消失问题在VGG里很严重训练特别吃力。ResNet的残差结构恒等映射 卷积非线性变换让梯度可以跨层直传训练更稳同时ImageNet预训练权重就是ResNet系列的迁移学习效果比随机初始化好很多。3.2 特征图到序列的桥接位置编码很关键ResNet输出的特征图是C×H×WTransformer需要的是序列输入。我的做法是直接把H×W个位置展平每个位置的特征向量是C维序列长度就是H×W比如8×864。但问题是卷积特征没有天然的顺序信息——解码器在生成公式时需要知道“当前看的是图片左上角还是右下角”。如果序列长度只有64时间步的位置编码表征力还够用但实际图片高宽较大时比如H32W32序列长度到1024位置信息就更重要了。这里我用了可学习的绝对位置编码不是原始的sinusoidal编码。原因很简单公式图像的空间位置对结构理解很关键可学习的位置编码能按数据自动调整用Transformer的标准实现就行。另外有人会把位置编码加到每个时间步的特征上也有人会拼接。我用的是加法这是主流的做法在自注意力的结构里加法足够让模型区分不同位置了。3.3 Transformer解码器自回归生成LaTeX序列解码器输入是“已经生成的token序列”输出是“下一个token的概率”。训练的时候我们把目标序列做一个shift操作输入前k个token预测第k1个token。整体是一个标准的语言模型任务配合因果mask防止信息泄漏。具体组件包括token embedding层把token id映射成512维向量位置编码层同上使用可学习位置编码6层TransformerDecoderLayer每层包含自注意力、交叉注意力、前馈网络最后接一个线性层映射到词表大小输出概率分布。交叉注意力Cross-Attention是编码器-解码器模型的重头戏。自注意力只关注已经生成的token之间的关系交叉注意力负责在生成每一步时去“看图”的相关区域。比如当前生成\frac之后下一步可能是分子区域的字符模型通过交叉注意力把权重集中在图片上部区域生成分子内容。这个机制就是整个项目最值得在答辩时展开讲的部分。我用的是nn.TransformerDecoder和nn.TransformerDecoderLayer有现成实现但需要自己构造一个tgt_mask来做因果mask。这个mask矩阵是上三角为负无穷的矩阵确保第i个token不能看到第i1个token的信息。3.4 损失函数与label smoothing训练用交叉熵损失即可但在生成任务里我强烈建议加label smoothing参数设为0.1。原因有两个第一数据集规模不大时模型容易过拟合到训练集的具体写法导致生成死板、出现重复片段第二手写公式的LaTeX标注本身存在多解性同一个公式可能有多种等价LaTeX写法label smoothing能让模型对“不是唯一答案”这件事更宽容少一点过于自信的误判。关于label smoothing的代码实现PyTorch的nn.CrossEntropyLoss从前几个版本开始内置了label_smoothing参数直接传进去就行不需要自己手写。4. 训练策略与调参心得让模型真正收敛的实用经验这套架构看起来不复杂但真正把它训到“拿得出手”的精度需要一些实操层面的讲究。这一节全是经验之谈每一条都是我在实验中对比过、踩过坑后留下的。4.1 训练参数配置参考我的配置是输入图像128×128batch size 64优化器用AdamW学习率初始3e-4用了warmup cosine decay。warmup做了1000步。epoch数设40~50配合early stopping。为什么用AdamW而不是AdamAdamW的权重衰减和Adam解耦正则化更干净。在我的实验里用AdamW比Adam在验证集上高了1个点左右区别不算大但更稳定。学习率warmup的必要性主要在于Transformer结构对学习率波动比较敏感刚起步时梯度方向信息量少用小学习率先走稳再进入理想区间快速收敛。我列个精简配置表参数值说明图像尺寸128×128过大增加计算量过小丢失细节ResNetResNet18预训练加一个第5个stage也试过性能提升有限解码器隐藏维512配合ResNet输出维度做投影解码器层数64层稍差6层是平衡点学习率3e-4用warmup时可以从1e-4起学习率调度warmup cosine经验上比StepLR稳label smoothing0.1提升泛化减少过度自信Beam size10推理时用训练不影响4.2 训练时的一个大坑teacher forcing和曝光偏差训练用teacher forcing每一步的输入都是真实的前序token不会把模型自己生成的错误token喂进去。这样训练快、收敛稳但推理的时候模型必须用自己的输出作为下一步输入一旦某一步生成错了错误会向后传播导致整条序列崩掉。这就是exposure bias问题。缓解办法有几个我试下来最简单有效的是计划采样scheduled sampling训练时有概率用模型自己的预测替换真实token作为下一步输入概率从1.0逐渐降到0.5左右。注意不能直接降太低否则训练不稳定。不过要提醒一点scheduled sampling并不总是提升最终结果如果数据集小反而可能让模型混淆。我的建议是以teacher forcing为主训练最后5个epoch再开启计划采样让它适应自己的错误分布。4.3 评估指标ExpRate和编辑距离手写公式识别领域最核心的指标叫ExpRateExpression Recognition Rate就是“整条LaTeX序列完全正确”的比例。这个指标很严格一个字符错就算错。它能直观反映识别效果发布论文、做对比时都用它。除了ExpRate我还额外跟踪字符级编辑距离。因为上课答辩时ExpRate可能只有60%听众会觉得“哦才及格”但你看编辑距离平均每句只错1.2个字符这说明大部分输出已经很接近60%的ExpRate只是被“差一点点全对”的样本拉低了。答辩前把这两个指标一起讲既能体现你对问题的理解也能避免导师对模型能力产生误判。4.4 Beam Search解码如何用一次多投提高正确率Greedy解码每次挑概率最高的token往往不是最优路径。比如生成\alpha时第一步可能已经偏向a后续无法回头。Beam Search的思路是同时维护多个候选序列beam每个时间步保留top-k个候选最终从k个完整序列里挑得分最高的那个。我设的beam size是10。增大beam size会提高准确率但推理时间线性上涨设到10以后再涨收益就很小了。另外长度惩罚参数需要调一下默认是1.0如果生成结果偏保守总是输出短公式可以提高一下长度惩罚鼓励模型给出更完整的公式。有一个关于beam search的小细节长度归一化很重要。默认的累计log概率越长的序列概率越低因为乘了很多小于1的数。如果不做长度归一化模型会倾向于生成短序列严重漏掉复杂的积分、矩阵公式。长度惩罚的公式建议用Google NMT那套score log_prob / (length ** alpha)alpha取0.7左右。4.5 测试时增强一种免费的涨点手段这是最容易忽略的涨点技巧。推理时不只用原图预测一次而是对图片做多组轻量数据增强比如小幅度旋转、缩放、平移每一组都送入模型得到一条输出最后用投票或策略选择得分最高的作为最终结果。我试过对公式图像做±2度旋转增强ExpRate能涨1~1.5个点。代价是推理时间乘以N倍但项目场景完全扛得住。如果你要把这个项目做到极致这个手段值得加进去。5. 常见问题与排查技巧实录我踩过的那些坑每个项目做完最有价值的往往不是成果本身而是中间踩过的坑。这一节我按“现象 → 排查思路 → 解决方式”来写都是实战经验。5.1 损失不下降或下降非常慢先检查数据是不是正确地从0到1标准化了。如果不是模型一开始学到的特征分布就有问题。再做一次tokenizer sanity check手动把一串LaTeX转成token id再转回来看是不是完全一致。这两个没问题就看学习率Transformer对学习率很敏感试试0.0003和0.0001的区别如果下降幅度差别很大说明是对的数量级。另外一个常见原因是ResNet预训练权重没正确加载。如果加载失败但程序没报错模型等于从头训损失会从非常高的值开始。5.2 训练收敛但生成结果总是重复输出这是典型的模型生成了局部循环比如输出“\frac{1}{1}{1}{1}...”。可能的原因第一label smoothing太大模型过度保守频繁产出高频token第二训练数据里的LaTeX版本不统一同一个公式标注多样模型学到的是“混搭”输出。我的解决方式是数据清洗时把LaTeX结构做归一化比如把\dfrac统一成\frac、去掉冗余括号同时把label smoothing从0.1降到0.05试试。还有一种针对强结构公式的补救训练时对部分简单重复token做随机丢弃(dropout)让模型减少盲目的惯性生成。5.3 识别正确的LaTeX但渲染出来是错位这种问题一般出在{和}的配对关系上。比如模型生成了\frac{a}{b少了一个}LaTeX编译器会报错或渲染异常。我在后处理里加了一个括号配对检查自动补全缺失的}和}。具体做法是统计开括号数大于闭括号数时在尾部补上等量的闭括号。这个技巧看起来简单但对最终效果展示帮助巨大尤其是答辩现场用LaTeX渲染演示时。5.4 显存不够或训练速度太慢如果显卡只有6~8GB显存batch size可以降到16~32同时把Transformer的decoder层数从6降到4hidden size从512降到256。精度会掉1~2个点但至少能跑通全流程。另外开启混合精度训练AMP也能显著减少显存占用并提速PyTorch原生支持很好。如果连GPU都没有也不是完全不能做。ResNet18 6层decoder小维度配置在CPU上训练一轮可能要好几个小时但可以把图像缩到96×96、batch size调到8跑通一个demo是完全可行的。5.5 手写数据和新场景的适配问题CROHME是标准手写体但如果你自己写的字比较潦草或者公式里混入了图片噪声测试效果会下降。这属于分布外场景。处理思路是数据增强训练时给图片加随机高斯噪声、笔画扰动、随机缩放旋转让模型对真实场景更鲁棒。我自己的体会是这项改动对“拿去识别自己手写公式”这一实际用途帮助很大。如果不做增强你拿手机拍一下然后去识别结果很可能让你怀疑模型白训了加上增强后容错空间明显变大。6. 项目展示与答辩加分技巧这个项目叫“高分项目”不只是把模型跑通就行你要能让别人快速理解并信服。分享一些经验。6.1 可视化注意力图把“黑盒”讲清楚Transformer的交叉注意力权重是可以直接可视化的。在生成某个token时把解码器最后一层交叉注意力的均值权重画在原图上你会看到生成分子的时候注意力集中在分子区域生成分母的时候注意力跳到分母区域。这种图放PPT里非常直观。用代码实现也简单在forward函数里返回注意力权重或者注册hook取出attention weight。我一般是叠加一个高斯核做平滑再转成热力图。6.2 消融实验证明每个模块都有用答辩的时候导师大概率会问“为什么用ResNet而不是别的”“Transformer编码器真的有必要吗”。这时候你有消融实验的数据就是最好的回答。我做了三组对比完整模型、去掉位置编码、ResNet不加载预训练。结果显示完整模型ExpRate最高去掉位置编码掉2~3个点去掉预训练权重掉5个点以上。这些数据可以做成柱状图直接展示每个组件的贡献。6.3 错误分析展示你的工程思维除了报告正确率主动展示错误案例更显水平。把模型识别错误的结果分三类字符混淆把\alpha认成a、结构错误分式嵌套乱掉、长公式截断或重复。在报告中先说这三类各占多少再针对占比最高的那类做一个针对性的改进——比如字符混淆问题可以增大训练集的字形多样性或添加入混淆数据增强。这种“发现问题→定位原因→给出方案”的闭环比单纯晒一个高accuracy更有说服力。6.4 代码工程化几个注意事项代码组织上我建议按职责分模块数据加载与预处理、模型定义、训练脚本、推理与可视化、评估脚本。不要全塞进一个main.py里。另外一定要固定随机种子否则训练结果不可复现保存最佳模型时同时保存模型权重和配置信息方便后续加载。提示如果在答辩时要现场演示建议先跑一遍离线测试确定模型已加载、推理脚本无报错再开始展示。现场临时跑会出现各种意外提前准备好演示脚本是底线。写在最后这个项目我完整做过三轮迭代换过数据集、调过模型结构、改过推理策略。每次重跑都能看到新的问题也能体会到一套好的编码器-解码器架构在处理“复杂结构内容生成”这个问题上的通用性。我觉得这个项目最值钱的不是最终那串准确率数字而是它逼着你去理解特征提取、序列建模、注意力机制、解码策略这些模块是怎么在一条流水线上协作的。这种“从图像到结构化文本”的能力无论是做论文还是今后做业务场景都是一个很底层的技能。如果你打算用这套方案去做自己的项目建议先从CROHME数据集的子集跑通小模型再到全量数据。不要一上来就追求最高精度先把整个流程走通再去调节奏和结构。遇到问题没关系后处理的检查逻辑和错误分析框架都在上面了照着排查就行。祝你跑出一个满意的效果。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门