
简介本资源是一份面向深度学习初学者与课程实践者的神经网络课程作业代码包聚焦数学公式图像识别这一典型结构化视觉-序列联合建模任务。资源提供CNNRNN与ResNetTransformer两套完整可运行方案覆盖数据预处理、模型构建、训练调优及推理预测全流程适用于学术文献OCR、教育辅助系统等实际场景。压缩包共27个文件含12个Python核心脚本如train_model.py、transfomer.py、inference_image_caption.py、5张示例图像png、3个文本配置文件vocab.txt、lbl2id_map.txt等、2个结果CSV、1份答辩PPTX、1份PDF课程设计报告及1份Word实验报告整体仅3.72MB轻量易部署。已有116人学习下载内容组织清晰主干模块分层明确data_utils、train_utils、precess_utils附带README.md说明与解压提示还包含history.npy训练记录、formula_predict.csv预测结果等实操产出物便于复现、对比与二次开发。1. 公式识别不是OCR翻版CNNRNN与ResNetTransformer双路径为何必须并存公式识别Mathematical Formula Recognition, MFR常被误认为是普通OCR的延伸但实际挑战远超字符切分与识别——它要理解符号语义、结构层级如上下标嵌套、分式堆叠、积分限位置、运算优先级甚至手写变体。一个“Emc²”在LaTeX中对应E m c^{2}而手写稿可能把平方号写成小而偏右的“2”或把等号拉长变形。单纯用CNN做端到端分类会丢失结构关系只靠RNN序列建模又难以捕捉二维空间布局。因此当前主流方案明确分化为两条技术路径CNNRNN编码器-解码器结构专注像素到序列的映射ResNetTransformer视觉-语言联合建模则直接学习符号间几何与语义对齐。本作业要求对比实现这两类模型本质是在训练你判断何时该用局部感受野时序归纳何时该用全局注意力空间感知。适合已掌握PyTorch基础、能跑通MNIST但尚未处理过结构化输出如LaTeX序列的深度学习实践者。2. CNNRNN路径从图像特征提取到LaTeX序列生成的端到端流程2.1 为什么选CNNRNN而非纯CNN结构约束决定模型骨架公式图像具有强空间依赖性分母必须在分数线正下方上标紧贴右上角根号覆盖范围由横线长度决定。CNN擅长提取局部特征如“∫”的竖线、“∑”的希腊字母但无法显式建模符号间的相对位置关系RNN尤其是LSTM/GRU天然适合处理序列输出LaTeX字符串且其隐状态可携带前序符号的上下文信息例如看到\frac{后下一个token大概率是{。因此CNN作为编码器压缩图像为特征图RNN解码器逐token生成LaTeX构成经典Encoder-Decoder范式。ResNet作为CNN主干比VGG更优——其残差连接缓解深层网络梯度消失使模型能稳定训练至34层以上这对高分辨率公式图像如含多层嵌套的矩阵表达式至关重要。提示不要用原始ResNet-50全连接层。需移除最后的Global Average Pooling和Classifier保留Stage4输出的特征图shape:[B, 2048, H//32, W//32]再通过1×1卷积降维至512通道供RNN编码器使用。这是避免信息冗余的关键步骤。2.2 数据预处理公式图像标准化与LaTeX序列对齐公式识别数据集如IM2LATEX-100K、CROHME提供原始PNG与对应LaTeX源码。预处理需同步处理图像与文本图像端统一缩放至224×896宽高比≈4:1适配公式横向延展特性灰度化后归一化至[0,1]再应用torchvision.transforms.ColorJitter(brightness0.1, contrast0.1)增强光照鲁棒性文本端LaTeX序列需添加sosstart-of-sequence与eosend-of-sequence标记并构建词表vocabulary。关键点在于符号粒度选择若按字符切分如\\, f, r, a, c, {, a, }, {, b, }词表约200若按命令切分如\\frac, {, a, }, {, b, }词表压缩至120以内但需预编译LaTeX命令列表含\sum,\int,\sqrt等127个高频命令。# 示例LaTeX序列向量化PyTorch def latex_to_tensor(latex_str: str, vocab: dict) - torch.Tensor: tokens [sos] latex_str.split() [eos] # 按空格分割命令 indices [vocab.get(t, vocab[unk]) for t in tokens] return torch.tensor(indices, dtypetorch.long) # 构建词表精简版实际需覆盖全部命令 vocab {pad: 0, sos: 1, eos: 2, unk: 3} for i, cmd in enumerate([\\frac, \\int, \\sum, \\sqrt, {, }, a, b, c, x, y, z]): vocab[cmd] i 4此代码将LaTeX字符串转为整数张量pad用于batch内序列长度对齐。注意split()按空格分割是因LaTeX源码中命令间天然有空格如\frac {a} {b}避免将\frac{a}误切为\frac{和a}。2.3 模型搭建ResNet-34编码器 LSTM解码器核心实现编码器采用ResNet-34非ImageNet预训练权重因公式图像与自然图像分布差异大移除最后两层输出特征图送入nn.Conv2d(512, 512, 1)降维解码器用LSTM输入为词嵌入embedding dim512隐藏层维度设为512层数为2。关键设计在于视觉特征注入方式不采用全局平均池化后拼接而是将ResNet输出的[B,512,H,W]特征图沿H、W维度展平为[B,512,H*W]再转置为[B,H*W,512]作为LSTM的初始输入序列——这使解码器每个时间步都能关注不同空间位置的特征模拟人类“视线扫视公式”的过程。# ResNet编码器截断版 class ResNetEncoder(nn.Module): def __init__(self, pretrainedFalse): super().__init__() resnet models.resnet34(pretrainedpretrained) self.conv1 resnet.conv1 self.bn1 resnet.bn1 self.relu resnet.relu self.maxpool resnet.maxpool self.layer1 resnet.layer1 self.layer2 resnet.layer2 self.layer3 resnet.layer3 self.layer4 resnet.layer4 # 输出 [B,512,H//32,W//32] self.proj nn.Conv2d(512, 512, 1) # 降维保持通道数 def forward(self, x): x self.conv1(x) x self.bn1(x) x self.relu(x) x self.maxpool(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) # [B,512,H//32,W//32] x self.proj(x) # [B,512,H//32,W//32] B, C, H, W x.shape x x.view(B, C, -1).permute(0, 2, 1) # [B, H*W, 512] return x # LSTM解码器带注意力机制简化版 class LSTMDecoder(nn.Module): def __init__(self, vocab_size, embed_dim512, hidden_dim512, num_layers2): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.lstm nn.LSTM(embed_dim, hidden_dim, num_layers, batch_firstTrue) self.fc_out nn.Linear(hidden_dim, vocab_size) def forward(self, encoder_out, target_seqNone, teacher_forcing_ratio0.5): # encoder_out: [B, S, 512], SH*W B, S, _ encoder_out.shape if target_seq is not None: # 训练模式teacher forcing tgt_len target_seq.size(1) outputs torch.zeros(B, tgt_len, self.vocab_size).to(encoder_out.device) input_token torch.full((B,), 1, dtypetorch.long) # sos index1 for t in range(tgt_len): embed self.embedding(input_token).unsqueeze(1) # [B,1,512] lstm_out, _ self.lstm(embed, None) # [B,1,512] output self.fc_out(lstm_out.squeeze(1)) # [B,vocab_size] outputs[:, t, :] output # 决定是否用真实标签teacher forcing或预测值 use_teacher torch.rand(1) teacher_forcing_ratio input_token target_seq[:, t] if use_teacher else output.argmax(1) return outputs else: # 推理模式自回归生成 pass参数说明teacher_forcing_ratio0.5平衡训练稳定性与泛化能力hidden_dim512与编码器输出通道一致避免信息瓶颈num_layers2提升时序建模深度但超过2层易引发梯度爆炸需配合梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。3. ResNetTransformer路径视觉特征与符号语义的跨模态对齐3.1 为什么Transformer在此场景优于RNN位置编码解决二维结构建模瓶颈RNN的序列建模本质是单向依赖t时刻仅依赖t-1而公式符号存在双向空间约束a_i的下标i既受a影响也受右侧括号}位置制约。Transformer的Self-Attention机制允许任意两个位置直接交互且通过2D位置编码显式注入坐标信息。具体做法将ResNet输出的特征图[B,512,H,W]视为H×W个视觉token每个token的位置(h,w)映射为二维坐标向量[sin(h), cos(h), sin(w), cos(w)]拼接后经线性层映射为512维加到视觉token上。这比简单展平后用1D位置编码更能保留空间拓扑关系。注意不要直接复用ViT的1D位置编码。公式图像中H高度通常远小于W宽度若用pos_embed nn.Parameter(torch.randn(1, H*W1, 512))会导致垂直方向位置区分度不足。必须分离编码H与W维度。3.2 视觉-语言联合建模Encoder-Decoder架构中的跨模态注意力ResNetTransformer路径采用标准Transformer Encoder-Decoder结构但关键创新在于Decoder的交叉注意力Cross-Attention输入Encoder输入为带2D位置编码的视觉tokenDecoder输入为LaTeX token的嵌入向量。在Decoder每层交叉注意力模块让每个LaTeX token如\\frac动态聚焦于最相关的视觉区域如分数符号所在像素块。这比CNNRNN的“先压缩再解码”更精细——RNN解码器只能看到全局特征摘要而Transformer可定位到\\frac对应图像中分数线的精确位置。# 2D位置编码生成函数 def get_2d_sincos_pos_embed(embed_dim, grid_size_h, grid_size_w, cls_tokenFalse): grid_size_h: 特征图高度如H//32 grid_size_w: 特征图宽度如W//32 返回: [grid_size_h*grid_size_w, embed_dim] 的位置编码 assert embed_dim % 4 0 pos_h torch.arange(grid_size_h, dtypetorch.float32) pos_w torch.arange(grid_size_w, dtypetorch.float32) grid_h, grid_w torch.meshgrid(pos_h, pos_w, indexingij) pos_embed torch.zeros(grid_h.shape[0] * grid_h.shape[1], embed_dim) # 四组正弦余弦sin(h), cos(h), sin(w), cos(w) dim_h embed_dim // 4 dim_w embed_dim // 4 pos_embed[:, 0:dim_h] torch.sin(grid_h.flatten().unsqueeze(1) * torch.pi / grid_size_h) pos_embed[:, dim_h:2*dim_h] torch.cos(grid_h.flatten().unsqueeze(1) * torch.pi / grid_size_h) pos_embed[:, 2*dim_h:3*dim_h] torch.sin(grid_w.flatten().unsqueeze(1) * torch.pi / grid_size_w) pos_embed[:, 3*dim_h:] torch.cos(grid_w.flatten().unsqueeze(1) * torch.pi / grid_size_w) if cls_token: pos_embed torch.cat([torch.zeros([1, embed_dim]), pos_embed], dim0) return pos_embed # 在模型初始化中调用 pos_embed get_2d_sincos_pos_embed(512, H//32, W//32) # [H*W, 512] self.pos_embed nn.Parameter(pos_embed.unsqueeze(0)) # [1, H*W, 512]此代码生成严格按2D网格计算的位置编码torch.pi / grid_size_h确保频率随图像尺寸缩放避免小图像位置编码过于密集。3.3 Transformer Decoder的LaTeX生成策略自回归与束搜索Decoder输出为LaTeX token概率分布训练时用交叉熵损失推理时需自回归生成。区别于RNN的单次预测Transformer Decoder在每步需重新计算所有历史token的Self-Attention计算量较大。实际部署中我们采用束搜索Beam Search并设置beam_width5维护5个最优候选序列每步扩展所有可能token保留累计概率最高的5个。关键参数length_penalty0.6抑制过长序列LaTeX中冗余空格或未闭合括号易导致无限生成early_stoppingTrue在遇到eos时立即终止该分支。# 束搜索核心逻辑简化版 def beam_search_decode(model, encoder_out, vocab, beam_width5, max_len150): B encoder_out.size(0) # 初始化每个样本5个候选起始均为sos candidates [[1]] * B # sos index1 scores torch.zeros(B, beam_width).to(encoder_out.device) # 循环生成最多max_len步 for step in range(max_len): all_probs [] for b in range(B): # 对每个样本的当前候选序列计算下一步概率 current_seqs torch.tensor(candidates[b], dtypetorch.long).to(encoder_out.device) # 调用model.decode_step(encoder_out[b:b1], current_seqs) probs model.decode_step(encoder_out[b:b1], current_seqs) # [1, vocab_size] all_probs.append(probs) # 合并并取top-k probs_stack torch.cat(all_probs, dim0) # [B*beam, vocab_size] topk_probs, topk_indices torch.topk(probs_stack, beam_width, dim1) # 更新candidates与scores此处省略细节需按beam重组 return candidates[0][0] # 返回最优序列decode_step方法需在Decoder中实现输入当前token序列返回下一个token的概率分布。注意必须屏蔽未来位置causal mask确保第t步无法看到t1及之后的token。4. 双模型训练与评估损失函数设计、超参调优与指标解读4.1 损失函数选择Label Smoothing防止过拟合LaTeX语法噪声公式数据集中存在标注噪声同一公式可能有多种LaTeX写法如\frac{a}{b}与{a \over b}纯交叉熵损失会强制模型在歧义处过度自信。采用Label Smoothingε0.1将真实标签概率从1降至0.9其余类别均分0.1使模型输出更平滑。对于CNNRNN路径损失函数为$$\mathcal{L}{\text{seq}} -\sum{t1}^{T} \sum_{k1}^{V} \tilde{y}{t,k} \log p{t,k}$$其中$\tilde{y}_{t,k} (1-\epsilon) \cdot \mathbb{I}(ky_t) \epsilon / V$$y_t$为真实token索引$V$为词表大小。Transformer路径同理但需额外添加Encoder输出的KL散度正则项强制视觉token分布接近均匀避免模型只关注图像中心区域而忽略边缘符号如积分限。4.2 关键超参对比表学习率、Batch Size与优化器选择超参CNNRNN路径ResNetTransformer路径说明学习率3e-4AdamW5e-5AdamWTransformer需更小学习率防止注意力权重震荡Batch Size32GPU显存≥16GB16需梯度累积至等效BS32Transformer内存占用高显存不足时用torch.cuda.amp混合精度Warmup步数10005000Transformer需更长warmup让注意力机制稳定Dropout率0.3LSTM层0.1Attention与FFNRNN更易过拟合需更强正则权重衰减1e-40.05Transformer对权重衰减更敏感过大导致收敛慢提示ResNetTransformer路径务必启用torch.compile(model)PyTorch 2.0可提升训练速度25%以上。编译前需确保模型无动态控制流如if条件分支所有操作为静态图。4.3 评估指标BLEU-4与Edit Distance的适用边界公式识别不用Accuracy字符级匹配因LaTeX中空格、花括号位置不影响渲染结果。主流指标为BLEU-4计算n-gram1~4重叠度权重各0.25。对命令级匹配敏感如\frac与\dfrac视为不同适合评估语法正确性Edit DistanceLevenshtein统计将预测LaTeX转换为真实LaTeX所需的插入、删除、替换操作数。值越低越好但需归一化除以真实序列长度Render Accuracy将预测LaTeX用matplotlib.mathtext渲染为图像与原图SSIM结构相似性0.85才计为正确。这是终极指标但计算开销大仅用于最终验证。# BLEU-4计算使用nltk from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction smooth SmoothingFunction().method4 bleu_score sentence_bleu([true_latex.split()], pred_latex.split(), weights(0.25, 0.25, 0.25, 0.25), smoothing_functionsmooth)注意true_latex.split()按空格分割命令确保与词表构建方式一致SmoothingFunction.method4解决短序列BLEU为0的问题。5. 实战技巧如何用有限算力跑通双模型并快速定位失败原因5.1 显存优化三板斧梯度检查点、混合精度与序列截断在单卡RTX 309024GB上运行ResNetTransformer需显存约18GB。若遇OOM按顺序启用梯度检查点Gradient Checkpointing在Transformer Encoder每层后插入torch.utils.checkpoint.checkpoint牺牲20%训练速度换取50%显存节省混合精度训练AMPscaler torch.cuda.amp.GradScaler()包裹forward与backward自动管理FP16/FP32切换序列截断对LaTeX序列长度120的样本在DataLoader中丢弃或截断target_seq target_seq[:120]因长公式占比5%影响有限。# 梯度检查点示例在Transformer Encoder Layer中 def custom_forward(self, x): return self.norm1(x self._sa_block(self.norm1(x))) # 替换为 def custom_forward(self, x): return torch.utils.checkpoint.checkpoint( lambda x: self.norm1(x self._sa_block(self.norm1(x))), x )5.2 失败诊断清单从日志到可视化的一站式排错当模型Loss不下降或BLEU20%时按此顺序排查Step 1检查数据加载运行next(iter(train_loader))打印images.shape应为[32,1,224,896]与targets[0][:20]前20个token是否含sos与合理命令Step 2验证编码器输出在训练循环中插入print(encoder_out.mean(), encoder_out.std())正常值mean≈0±0.1std≈0.5±0.2。若std≈0说明ResNet未激活检查BN层track_running_statsTrueStep 3注意力热力图可视化提取Decoder最后一层Cross-Attention权重shape:[B,8,H*W,T]8为head数取平均后reshape为[H,W,T]用plt.imshow显示第T个token关注的图像区域。若所有token都聚焦左上角说明位置编码失效Step 4生成序列分析对验证集前5个样本保存pred_latex与true_latex人工比对是否频繁生成unk词表未覆盖、是否重复\\frac注意力陷入循环、是否缺失eos长度惩罚过弱。5.3 快速验证技巧用单样本过拟合测试模型表达能力为确认代码无逻辑错误执行过拟合单样本测试从训练集取1张公式图像与对应LaTeX构造train_loader仅含该样本batch_size1, num_workers0训练100轮。理想情况下CNNRNN路径Loss应在50轮内降至0.1以下BLEU达100%ResNetTransformer路径Loss应在30轮内降至0.05以下且注意力热力图精准覆盖公式符号区域。若未达成问题必在数据预处理如图像未归一化或模型定义如Decoder未mask future positions。此测试可在5分钟内完成是排除环境配置问题的黄金标准。本文还有配套的精品资源点击获取