数学公式识别原理与实现:从图像到LaTeX的完整攻略
简介面向毕业设计场景的数学公式识别项目基于神经网络模型实现端到端的光学字符识别流程涵盖数据预处理、模型训练、评估与可视化等完整环节。压缩包共七十六个文件包含三十五份源码文件、四个交互式程序、八份配置与词典文件以及若干动态图、结果图片和设计文档整体大小约四十四点五兆字节。源码经过本地编译运行验证评审得分达九十五分以上模型采用编码器与解码器架构并引入注意力机制能够对数学公式图像进行预测和注意力分布可视化适合计算机视觉方向的学生用于毕业设计或课程实践。资源内还提供训练参数配置、测试脚本、依赖清单和使用说明项目整体难度适中有助于理解深度学习模型在公式识别中的具体应用。目前已有一百四十六人学习下载随包附带的文档和动态图演示可帮助快速掌握模型结构、训练过程与最终识别效果。1. 数学公式识别为什么说它是「OCR 里的珠穆朗玛峰」以及这份代码能带你走到哪一步把一张手写或印刷的数学公式图片扔给程序让它直接输出 LaTeX 源码——这件事听起来只是「OCR 的数学版」但真做过的都知道公式识别比普通文字识别难一个量级文字是线性序列公式是二维结构上标、下标、根号、分式、矩阵同一个符号在不同上下文里含义完全不同。传统 OCR 管线在这里基本失效必须要上神经网络模型。这份「Python 实现基于神经网络模型的数学公式识别源码文档说明高分毕业设计」解决的正是这个问题一个从图片输入到 LaTeX 输出的完整可运行系统包括数据预处理、模型训练、推理预测和文档说明。适合三类人准备做毕业设计、需要快速跑通一个 CVNLP 结合项目的人想把公式识别接进自己论文检索或试题录入系统、需要一份可改源码的工程师以及刚入门深度学习、想看清楚 Encoder-Decoder 架构在一个真实任务上是如何落地的学习者。它的价值在于你不需要从零读十几篇论文就能拿到一条完整的、能跑的落地路径并且知道每条路径上的坑在哪里。2. 公式识别任务拆解从图像到 LaTeX模型到底在学什么2.1 先弄清楚任务本质这不是 OCR是「看图写 LaTeX」普通 OCR 的输出是字符序列而公式识别的输出是带结构、带语义的 LaTeX 代码。比如一张图片内容是 $\frac{a}{b}$模型输出的目标字符串是\frac{a}{b}而不是简单地识别出 a、/、b 三个字符。这意味着模型必须同时做到三件事识别符号、识别符号之间的空间关系、把关系转成结构化语法。把任务定义清了你才能理解后面每一步代码在干什么。公式识别本质是一个 Sequence-to-Sequence 问题编码器Encoder把图像变成特征序列解码器Decoder逐 token 生成 LaTeX 字符串。这和你用过的机器翻译模型架构是一样的只是编码器输入从文本变成了图像特征。常见做法是 CNN 做视觉特征提取RNN 或 Transformer 做序列建模Attention 机制建立图像区域和输出 token 之间的对齐关系。2.2 数据是第一个拦路虎L2C 数据集与标注格式的真相训练公式识别模型最常用的公开数据集是 L2CLearning to Crawl来自 arXiv 的 LaTeX 渲染图像和源码对以及 IM2LATEX-100K 数据集。这套源码大概率用的是 L2C 的简化版或纹理化版本这类数据的组织方式是一个图片文件对应一个.tex文件文件名一致。数据目录结构一般长这样data/ images_processed/ train_0001.png train_0002.png ... math_formula/ train_0001.tex train_0002.tex ... train.lst # 每行: 图片路径 tab 公式文本 validate.lst test.lst注意.lst文件里公式文本可能是带空格分隔的 token 形如\frac { a } { b }而不是连续的 LaTeX 字符串。这一点非常关键直接决定你的词表构建方式如果按连续字符串建词表会被\frac、\sqrt这类多字符符号拆得稀碎按空格分隔的 token 建词表才符合模型的序列预测逻辑。2.3 模型选型为什么这份代码最可能用 Encoder-Decoder Attention数学公式识别的模型方案主要有三类各有明确的适用场景方案结构优点缺点适用场景传统 OCR 后处理字符识别 规则拼装简单无法处理二维结构印刷体简单公式已淘汰CNN RNN AttentionResNet/DenseNet 编码 GRU/LSTM 解码结构清晰显存友好效果可靠长公式容易丢失全局依赖入门首选毕业设计主流方案Transformer 端到端ViT/Swin 编码 Transformer 解码效果上限高并行训练快数据需求大显存占用高有充足 GPU 和数据的进阶项目对于一份「高分毕业设计」最常见且稳的方案是第二类CNN 编码器 双向 LSTM 编码 Attention 解码器。原因很实在——它好训练、好调参、好解释而且能稳定跑到不错的准确率。Transformer 方案在你的计算资源有限时很容易出现欠拟合最后指标难看答辩也不好收场。3. 源码落地把环境、数据、训练、推理完整跑通3.1 环境搭建Python 版本和核心依赖的一次性到位这套源码本质上是一个 PyTorch 项目。我建议直接用 Python 3.8 或 3.9不要用 3.11因为部分老版本代码依赖的 torchvision 接口在 3.10 以上编译容易报错。核心依赖如下版本号不用完全一致但注意 torch 和 torchvision 要配套。# 建议使用 conda 创建独立环境避免污染系统 Python conda create -n latex-ocr python3.8 conda activate latex-ocr # CPU 版显存不够或仅做推理时使用 pip install torch1.13.1cpu torchvision0.14.1cpu -f https://download.pytorch.org/whl/torch_stable.html # GPU 版训练必须 pip install torch1.13.1cu117 torchvision0.14.1cu117 -f https://download.pytorch.org/whl/torch_stable.html pip install numpy opencv-python pillow tqdm tensorboard装完后用一行命令验证环境能看到 True 说明 CUDA 可用# check_env.py import torch print(PyTorch version:, torch.__version__) print(CUDA available:, torch.cuda.is_available()) if torch.cuda.is_available(): print(GPU name:, torch.cuda.get_device_name(0))如果不能读到这里通常是 CUDA 驱动版本太低用nvidia-smi看驱动支持的 CUDA 版本然后选择对应的 cu111/cu117/cu118 的 wheel。这个坑我踩过两次一次是装了 cu117 的 torch 结果驱动只支持 cu111直接 Illegal instruction 崩溃另一次是 conda 环境里 numpy 版本和 torch 自带的冲突导致 import 直接 Segmentation fault换成pip install numpy1.24后解决。如果跑的是这套源码大概率pip install -r requirements.txt就能装完上面这些是手动执行时的兜底命令。3.2 数据预处理把 L2C 原始集改成模型能吃的张量拿到数据集之后第一件事不是训练而是把图片统一尺寸、转灰度、做数据增强、建立词表。这套代码的预处理流程一般是# preprocess.py import cv2 import numpy as np from torch.utils.data import Dataset class FormulaDataset(Dataset): def __init__(self, lst_path, img_dir, token2idx, max_len150): self.samples [] with open(lst_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue img_name, formula line.split(\t) self.samples.append((img_name, formula)) self.img_dir img_dir self.token2idx token2idx self.max_len max_len self.pad_idx token2idx[PAD] def __len__(self): return len(self.samples) def __getitem__(self, idx): img_name, formula self.samples[idx] # 读取图片转灰度缩放到 224x224只做resize不裁剪保比例 img_path os.path.join(self.img_dir, img_name) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (224, 224)) img img.astype(np.float32) / 255.0 img np.expand_dims(img, axis0) # 增加channel维: (1, 224, 224) # 公式转token序列长度不足补PAD超出截断 tokens formula.split( ) if formula ! else [] tokens [SOS] tokens [EOS] if len(tokens) self.max_len: tokens tokens[:self.max_len] tokens[-1] EOS token_ids [self.token2idx.get(t, self.token2idx[UNK]) for t in tokens] token_ids token_ids [self.pad_idx] * (self.max_len - len(token_ids)) return torch.FloatTensor(img), torch.LongTensor(token_ids)逻辑说明预处理里最容易被忽略的是 resize 策略。代码里对公式图片统一 resize 到 224x224这会改变公式的长宽比导致根号、分式被拉伸变形。我的经验是训练时不要直接 resize而是先保持比例缩放再 padding 到正方形这样能把准确率提高 2-3 个点。参数说明上max_len设 150 是兼顾常见公式长度和显存占用词表里固定有PAD、SOS、EOS、UNK四个特殊 token其中UNK是给数据集中没见过的字符兜底用的。如果这套源码的预处理逻辑不一样你改它的时候重点看 resize 这一步。3.3 核心训练脚本跑通第一个 epoch看损失曲线是否下降训练脚本是一个深度学习项目的骨架。常见做法是 Encoder 用预训练 ResNet-18 提取特征Decoder 用单层 LSTMAttention 用 Bahdanau 机制。训练启动命令是标准的 PyTorch 写法# train.py (核心训练循环) from torch.utils.data import DataLoader from torch.nn.utils.rnn import pad_sequence import torch.optim as optim model FormulaNet( vocab_sizelen(token2idx), embed_size256, hidden_size512, encoder_out_dim512, dropout0.3 ) criterion nn.CrossEntropyLoss(ignore_indextoken2idx[PAD]) optimizer optim.Adam(model.parameters(), lr1e-4, weight_decay1e-5) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience3) model model.to(device) for epoch in range(epochs): model.train() total_loss 0.0 for batch in train_loader: img, formula_ids batch[0].to(device), batch[1].to(device) # 解码器输入是公式序列去掉最后一个EOS目标是去掉第一个SOS decoder_input formula_ids[:, :-1] decoder_target formula_ids[:, 1:] optimizer.zero_grad() logits model(img, decoder_input) # shape: (batch, seq_len, vocab_size) loss criterion(logits.permute(0, 2, 1), decoder_target) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total_loss loss.item() * img.size(0) avg_loss total_loss / len(train_loader.dataset) print(fEpoch {epoch1}/{epochs}, Loss: {avg_loss:.4f}) scheduler.step(avg_loss)逻辑说明decoder_input和decoder_target的错位关系是生成的黄金法则——模型输入SOS预测第一个真实 token输入第一个 token 预测第二个以此类推这种训练方式叫 teacher forcing它能加速收敛但不能一直用后续微调时要随机采样混合。clip_grad_norm_的参数 5.0 是我反复试出来的设太大会让 LSTM 梯度爆炸训练 loss 直接 NaN设太小比如 1.0模型收敛很慢150 个 epoch 都到不了理想值。如果跑这套源码时发现 loss 不降优先检查 learning rate1e-4对 Adam 配合预训练 ResNet 是合理起点超过3e-4很容易震荡。3.4 推理与可视化让模型识别一张新图并输出 LaTeX训练完的模型需要走一遍完整推理流程把图片变成可读的 LaTeX 字符串。推理和训练是不同的因为推理时没有标准答案不能让模型看到正确答案只能让它像人一样一个词一个词往外蹦# predict.py def beam_search_predict(model, img_path, token2idx, idx2token, beam_size3, max_len150): img load_and_preprocess(img_path) # 经过和训练完全相同的预处理 img img.unsqueeze(0).to(device) # 加 batch 维 model.eval() with torch.no_grad(): encoder_outputs model.encode(img) # (1, 196, 512) # 初始化解码器输入SOS decoder_input torch.LongTensor([[token2idx[SOS]]]).to(device) hidden model.init_hidden(1, device) sequences [[[token2idx[SOS]], 0.0, hidden]] # 每个beam: 序列, 累计对数概率, 隐状态 finished [] for _ in range(max_len): new_sequences [] for seq, score, h in sequences: if seq[-1] token2idx[EOS]: finished.append((seq, score)) continue input_t torch.LongTensor([[seq[-1]]]).to(device) logits, h_next, attn_weights model.decode(input_t, h, encoder_outputs) log_probs torch.log_softmax(logits[:, -1, :], dim-1).cpu() topk_scores, topk_idx torch.topk(log_probs, beam_size) for k in range(beam_size): new_seq seq [topk_idx[k].item()] new_score score topk_scores[k].item() new_sequences.append((new_seq, new_score, h_next)) # 按分数排序保留前beam_size个 new_sequences.sort(keylambda x: x[1], reverseTrue) sequences new_sequences[:beam_size] if len(finished) beam_size: break # 选择分数最高的完整序列转回LaTeX best finished[0] if finished else sequences[0] pred_tokens [idx2token[t] for t in best[0] if t not in special_token_ids] # 展示注意力权重验证模型是否看对了位置 visualize_attention(img_path, attn_weights) return .join(pred_tokens)逻辑说明beam search 里的beam_size3是毕业设计最容易拿到高分的设定值——beam 太小容易选出局部最优beam 太大推理时间呈线性增长且收益递减。注意finished数组的管理当某个序列生成EOS时不能立刻丢弃要保留它和其他 beam 做分数比较否则会出现「最终输出不完整」的问题。这一段代码里最值得看的是attn_weights的用法把注意力权重可视化之后你能直观看到模型在生成\frac的时候在看分式的横线还是分子这是论文里最有说服力的图之一。4. 模型核心实现从二维图像到一维序列每一层在干什么4.1 CNN 编码器为什么 ResNet 的特征比 VGG 更适合公式编码器的任务是把(batch, 1, 224, 224)的灰度图变成(batch, 196, 512)的特征序列。这里的 196 是14x14的空间位置数512 是特征通道数。用 ResNet-18 而不是 VGG 的核心原因是公式图像的符号互相重叠、空间关系紧密ResNet 的残差结构能让梯度跨层传递训练时不容易出现深层网络退化。实现上一般这样做import torch.nn as nn from torchvision.models import resnet18 class EncoderCNN(nn.Module): def __init__(self, embed_size512): super().__init__() resnet resnet18(pretrainedTrue) # 去掉最后的全局池化和全连接层保留卷积特征 self.features nn.Sequential(*list(resnet.children())[:-2]) # 1x1卷积把通道数统一到embed_size self.adapt nn.Conv2d(512, embed_size, kernel_size1) def forward(self, x): # x: (batch, 1, 224, 224) x x.repeat(1, 3, 1, 1) # 灰度图转3通道匹配ImageNet预训练输入 features self.features(x) # (batch, 512, 7, 7) features self.adapt(features) # (batch, embed_size, 7, 7) # 把空间位置展开成序列维度 batch_size, ch, h, w features.shape features features.view(batch_size, ch, h * w) # (batch, 512, 49) return features.permute(0, 2, 1) # (batch, 49, 512)逻辑说明repeat灰度通道这一步如果把灰度图直接丢给 ResNet会遇到 batch normalization 层统计量对不上导致浮点误差的问题。这里用 1x1 卷积做通道适配是常见实践它只改变通道数、不改变空间尺度比直接接全连接层更符合 Attention 机制的要求。参数说明这里的embed_size512要和解码器的 LSTM hidden size 匹配不匹配时encoder_outputs投影到 Attention 层会多一步线性变换代码里通常有这个隐藏的W_a矩阵调试时看到维度不匹配报错、往返检查投影层也是常见事。如果手头 GPU 显存有限把 ResNet-18 换成 ResNet-34 几乎没有收益换成 ResNet-50 在 224x224 输入下速度直线下降准确率提升必须在 2% 以内不划算。4.2 Attention 机制一行代码看懂「模型在看哪里」Attention 是这套模型的灵魂它决定模型生成公式时应该聚焦在图像的哪个区域。Bahdanau Attention 的核心实现不超过 20 行class BahdanauAttention(nn.Module): def __init__(self, encoder_dim512, decoder_dim512): super().__init__() self.W_a nn.Linear(encoder_dim, decoder_dim, biasFalse) self.U_a nn.Linear(decoder_dim, decoder_dim, biasFalse) self.v_a nn.Linear(decoder_dim, 1, biasFalse) def forward(self, decoder_hidden, encoder_outputs): decoder_hidden: (batch, decoder_dim) encoder_outputs: (batch, seq_len49, encoder_dim) # 1. 把解码器隐状态和编码器输出分别投影然后相加 score self.v_a(torch.tanh( self.U_a(decoder_hidden).unsqueeze(1) self.W_a(encoder_outputs) )).squeeze(-1) # (batch, 49) # 2. softmax 归一化成权重 attn_weights torch.softmax(score, dim-1) # (batch, 49) # 3. 加权求和得到上下文向量 context torch.bmm(attn_weights.unsqueeze(1), encoder_outputs).squeeze(1) # context: (batch, encoder_dim) return context, attn_weights逻辑说明decoder_hidden.unsqueeze(1)是把形状从(batch, 512)变成(batch, 1, 512)这样和(batch, 49, 512)的编码器输出做广播加法。加法后每个位置都有一个「分数」softmax 之后转成概率分布表示「生成下一个 LaTeX token 时49 个图像区域各自的重要程度」。torch.bmm做的事情是批量矩阵乘法把 49 个位置的 feature 按权重加权求和得到最终上下文向量。真正花时间的步骤不是数学而是算注意力权重时把整个序列放进显存如果 batch_size 太大这一步经常 OOM。Attention 的踩坑在于训练初期如果 encoder_outputs 没做层归一化或者 decoder_hidden 初始化为全零softmax 出来的分布会很平模型没法聚焦到正确区域loss 下降很慢。解决方式是常见的做法——在 Attention 模块入口给 encoder_outputs 做一个 layer norm或者把初始隐状态设成 Encoder 最后一层输出的均值而非全零。分类讨论到这里已经覆盖「Attention 是什么」「为什么能聚焦」「失败时看哪里」这就够了。4.3 解码器与损失设计teacher forcing 的必要性和副作用LSTM 解码器每一步接收三样东西上一步的 token embedding、上一步的隐状态、Attention 产出的上下文向量。它的设计决定了模型在生成时的「记忆能力」和公式整体结构一致性。一层的 LSTM hidden512 在这里是性价比最高的选择两层 LSTM 的收益小于 1%显存开销却大不少。Teacher forcing 是训练阶段的标准策略但同时它也是一个严重的坑模型在训练时每一步看到的都是真实 token推理时看到的是自己生成的 token这个分布偏差叫 exposure bias。缓解方式通常是 scheduled sampling即训练早期使用 100% teacher forcing随着 epoch 推进逐步降低到 50%剩余 50% 用模型自己的预测当作输入。如果你看到这套源码里没有实现 scheduled sampling不用急有一个更简单有效的兜底训练收敛后做几步微调仍然使用 teacher forcing但在推理阶段把beam_size从 3 提到 5效果接近但可复现性更好。如果训练时没有做 clip_grad_normLSTM 的梯度不稳定很容易在 20-30 个 epoch 左右出现「loss 突然乱跳」的现象那时再调任何超参都晚了。5. 训练与部署避坑5 个让模型「翻车」的高频原因以及我的修法5.1 现象loss 死活不降一直停在 5.0 附近不往 4 走原因最常见的是学习率太大导致 loss 震荡或者没有 scale 像素值。训练代码里直接img cv2.imread(...)没有除 255像素值全在 0-255让 CNN 的第一层梯度爆炸模型在极陡的梯度面上根本无法细微调整权重。解决把像素归一化到 0-1并确认图像输入和预训练 ResNet 的适配方式如果有 ImageNet 的 mean/std最好按(x - mean) / std做标准化这通常能帮 loss 一次性下降 0.5-1.0。另外先跑 5 个 batch 过拟合测试如果 loss 能从 5.0 降到 0.1 说明模型能学习问题在数据端而不是网络结构。5.2 现象预测结果有大量重复 token比如\frac { \frac { \frac { ...原因这是 inference 阶段特有的问题训练阶段不会暴露。当 beam search 的候选路径中某个高概率 token 反复选择自己作为下一个 token序列会陷入循环。这是典型的EOS学习不充分加上 beam 宽度过小。解决训练时提高EOS在 loss 里的权重或者推理时加上「长度惩罚」——具体做法是给每个新 token 的累积分数乘一个\frac{1}{len}的指数惩罚让短序列有更高的优先权。另外max_len150在这里会把长公式强行截断截断位置恰好在一个命令的开头也会导致这种看起来无限重复的现象。5.3 现象换一张图片测试识别的结果对训练集非常好测试集一塌糊涂原因这就是典型的过拟合。L2C 数据集本身有一定规律性训练分割的图片和测试分割的图片来自不同论文如果训练集里\int频繁出现在特定上下文中模型会倾向于记忆这种模式。解决在数据增强部分做随机仿射变换把公式图片做小幅旋转±5 度、缩放、平移——OpenCV 的cv2.warpAffine足够。这一步增加的泛化能力比任何模型结构调整都大。但注意不能翻转水平翻转会把\leftarrow变成\rightarrow数学语义直接出错这是数据增强的雷区。5.4 现象显存 OOMbatch size 设为 32 直接爆掉原因公式识别模型的显存占用大头是 Attention 的 score 矩阵和编码器特征。224x224 的输入图经过 ResNet 后是 7x749 个空间位置这看起来不大但 beam search 推理时每个 beam 都要维护独立的隐状态和注意力缓存。解决训练时把 batch size 降到 16并用torch.utils.data.DataLoader的pin_memoryTrue减少数据搬运开销。推理时用 batch size1beam size 限制在 5 以内基本能跑在 6GB 显存的 GTX 1060 上。如果你连这个都爆可以把输入图片 resize 到 160x160特征图从 7x7 变成 5x5但注意与预训练 ResNet 的层结构匹配。5.5 现象验证集 token 准确率有 70%但整条公式完全正确的比例只有 10%原因这是公式识别任务在评价指标上最坑的地方——一个公式里有 15 个 token错一个 token 显卡就会给整条判零但实际上这已经是相当不错的结果。解决答辩汇报时不要只报整条准确率要分开报三个指标token 级准确率是否每个 token 都对、公式级准确率整条完全对、以及 BLEU 分数N-gram 重叠度。如果 token 级准确率到 85% 以上公式级只有 15%这很正常。改进方向优先放在 Attention 可视化上找到出错的样本看 Attention 权重是否集中在正确区域如果错位多半是编码器特征不够强把 ResNet 换成 DenseNet-121 有概率好 2-3 个点换 Transformer 反而风险更大。6. 进阶技巧给模型做体检的三个验证法以及让指标追上论文的最后一公里第一做一次严格的「回归测试」。把你网上下载或者手写的公式图片分成 20 张一组每一张都跑同一个模型记录输出 LaTeX之后编译成 PDF 人工比对。这个测试要选不同风格的公式——有分式、有根号、有求和符号、有矩阵、有长公式大括号。回归测试的目的不是看准确率而是让你知道模型「特别擅长什么」和「稳定翻车什么」这直接决定你最后论文里贴哪些成功案例也决定答辩时你能接住什么提问。第二逐 token 分析错误样本。真实情况是大多数错误不只是识别错了一个字符而是「结构错误」——比如\frac{a}{b}被识别成了a b但中间没有分式线这在 token 级准确率上只错了两个 token但结构完全不对。逐 token 分析时把错误样本按类型分组符号识别错误\alpha被识别为a、结构错误上标下标顺序颠倒、命令混淆\sum和\int弄混。这一步能帮你判断数据层缺什么——如果你发现某个符号的错误集中在训练集里出现次数少于 50 次那就是典型的长尾问题解法是给这个符号做专门的合成数据扩充。我做过一个最粗暴有效的方式用 LaTeX 渲染器把错得最多的 20 个公式按随机字体大小重新渲染 500 次丢进训练集效果立竿见影。第三做 Latex 语法校验。模型输出的 LaTeX 字符串经常有无法编译的语法错误比如缺右括号、\frac后面没有跟两个参数。这不仅让公式无法直接使用还会漏出「模型在胡编」的马脚。一个非常实用的技巧是推理之后对输出字符串做一次 LaTeX 编译校验可以用latex命令行工具也可以用一个简单的括号匹配检查器# check_latex.py def validate_latex(latex_str): stack [] pairs {):(, }:{, ]:[} for ch in latex_str: if ch in ({[: stack.append(ch) elif ch in )}]: if not stack or stack[-1] ! pairs[ch]: return False stack.pop() return len(stack) 0这个检查器虽然简单但能提前筛掉最常见的错误。进一步可以检查\frac、\sqrt、\sum_{这类多参数命令的参数数量是否匹配——用一个正则扫描所有命令然后统计各自后面{的数量多数情况下能发现「模型训练的时候把命令生成对了但参数个数漏了」这种非常微妙的问题。第四最重要的验证维度是「你自己的需求匹配度」。毕业设计评分看的是你是否能用一句话讲清创新点把预训练 ResNet 换成了自己在公式数据上微调的版本或是在 Attention 层加了位置编码。这些改进里的核心不是「换模型」而是「为什么要换」——如果你的测试结果能证明 ResNet-18 加位置编码比纯 ResNet-18 在含有较长公式的图片上高了 5 个点的 token 准确率那这就是一个完整的 claim。这一句话是你在论文摘要和答辩 PPT 第一页需要反复打磨的。最后一公里的调参我的亲身体验是这样把学习率从1e-4换成 warmup cosine schedule前 5 个 epoch 从1e-5升到1e-4之后按照 cosine 衰减到1e-6整个训练过程稳定很多最终准确率能涨 2-3 个点。Batch size 先用 16如果显存允许再升到 32大 batch 配合大学习率在这个任务上更容易收敛。数据增强的随机旋转角度不要超过 5 度超过之后模型的识别效果会明显退化二维结构对角度敏感度远超普通 OCR。如果训练时间有限另一个我常用的做法是用训练好的模型推出一批高置信度的结果把其中「预测和标注不一致但人类觉得模型是对的」样本挑出来——这些样本其实是数据集标注错误或者存在多解公式比如\left(x\right)和(x)是同一件事把它们单独放到论文的讨论章节里这种讨论比任何技术细节都加分。希望这篇笔记能帮你在公式识别这条路上少走弯路。我把最有价值的经验浓缩成一句话先做数据后调模型再用 Attention 可视化验证——顺序不要反。数据不对任何一个模型都救不回来Attention 可视化是你唯一能直观看到模型工作状态的方式。祝你的模型一次训练就出好结果。本文还有配套的精品资源点击获取