
1. 赛题背景与整体设计思路拆解1.1 这个比赛到底在做什么街景字符编码识别说白了就是给一张街景照片让模型自动认出图中出现的所有字符序列。这类任务在真实场景里对应的是门牌号识别、路牌识别、快递面单编号提取等需求。比赛提供的数据集是一批街景图片每张图里包含一个或多个字符组成的编号选手需要训练模型输出对应的字符序列。这个任务本质上属于**场景文本识别Scene Text Recognition**的范畴比传统OCR更难的地方在于街景图片中的字符存在光照变化、角度倾斜、遮挡、模糊、背景干扰等问题而且字符长度不固定。我第一次拿到数据的时候随手翻了几十张图发现有些字符被树枝挡住一半有些因为拍摄角度导致严重透视变形还有些图片整体曝光过度字符几乎和背景融为一体。这些真实存在的干扰因素决定了我们不能用简单的模板匹配或者传统图像处理方法来解决。从任务类型来看这是一个不定长字符序列识别问题。输入是任意尺寸的RGB图像输出是变长的字符序列。这跟固定类别的图像分类有本质区别也跟目标检测的输出形式不同。你需要一个能处理序列输出的模型架构。1.2 为什么选择这个技术路线面对字符识别任务常见的技术路线有几条一是先做字符检测再逐个分类二是直接做序列识别端到端输出。我选择的是**CRNNConvolutional Recurrent Neural Network CTCConnectionist Temporal Classification**的方案这也是入门CV比赛最经典、最容易复现的baseline。选择这条路线的原因很实际第一CRNN结构清晰CNN负责提取图像特征RNN负责序列建模CTC负责解决对齐问题每个模块的职责明确调试起来方便定位问题。第二这个方案在多个公开数据集上已经验证过效果社区里有大量可参考的实现遇到问题容易找到解决方案。第三训练成本可控单卡就能跑起来不需要多卡并行或者大规模预训练。相比之下基于检测的方案需要先训练一个字符检测器再训练一个字符分类器两个模型串联会带来误差累积的问题。而且检测框的精度直接影响后续分类效果调试链路更长。对于入门级别的比赛端到端的CRNN方案性价比最高。另一个关键决策是是否使用预训练模型。我试过从零训练和加载预训练权重两种方式实测下来加载在通用图像数据集上预训练过的骨干网络收敛速度明显更快最终精度也更高。原因在于街景字符识别的数据量通常有限从零训练容易过拟合而预训练权重提供了良好的特征提取起点。1.3 整体方案架构整个方案可以拆成四个核心模块数据预处理模块负责读取图片、统一尺寸、归一化、数据增强。这里的关键是要处理不同长宽比的图片同时保证字符不被过度拉伸变形。特征提取模块用CNN骨干网络把输入图像转换成特征序列。我选的是ResNet系列的变体因为它在深层网络中梯度传播更稳定。序列建模模块用双向LSTM对特征序列进行上下文建模捕捉字符之间的依赖关系。比如“1”和“0”相邻时模型能利用上下文信息提高识别准确率。转录模块用CTC解码器把RNN输出的概率序列转换成最终字符序列同时处理重复字符和空白标签。训练时用CTC损失函数推理时用贪心解码或束搜索解码。整个流程串起来就是图片输入→CNN提特征→RNN建序列→CTC出结果。注意CTC有一个关键约束——输入序列长度必须大于等于输出标签长度。这意味着如果你的图片被压缩得太厉害特征序列太短可能无法覆盖所有字符。我在初期就踩过这个坑把图片统一缩到很小尺寸结果长字符序列的样本大量报错。2. 数据处理的细节与实操要点2.1 数据集的读取与标注解析比赛提供的数据通常包含两部分训练集图片和对应的标注文件。标注文件一般是JSON或CSV格式记录了每张图片的文件名和对应的字符编号。我拿到的标注格式是每行一个JSON对象包含“filename”和“label”两个字段。读取标注时需要注意几个细节。第一标注中的字符编号可能包含数字和字母需要统计所有出现过的字符建立字符到索引的映射表。这个映射表决定了模型输出层的维度。第二要检查标注中是否存在空标签或者异常字符这些脏数据如果不处理训练时会导致损失计算异常。第三统计字符长度的分布了解数据集中编号长度的范围这直接影响后续模型设计和CTC解码的参数设置。我实际统计下来字符长度主要集中在4到8位之间最短2位最长12位。这个分布信息很重要因为它决定了CNN输出的特征序列长度至少要到12以上否则CTC无法正确对齐。2.2 图片预处理的关键决策图片预处理是影响最终效果的关键环节我在这上面花了不少时间调试。核心要解决的问题是如何把不同尺寸的原始图片转换成网络可以接受的统一输入。最直接的做法是直接缩放到固定尺寸比如256×64。但这样做有个问题不同长宽比的图片被强行拉伸后字符会变形。比如一张很宽的图片被压到256宽字符会变得又扁又窄特征提取难度增加。我采用的方案是保持长宽比缩放填充。具体操作是先按比例把图片缩放到高度为32像素宽度按原始比例计算如果宽度超过设定最大值比如256就截断或进一步缩放如果宽度不足就在右侧填充灰色像素。这样字符不会变形同时所有图片尺寸统一。另一个重要决策是是否转灰度图。我试过灰度图和RGB两种输入实测下来RGB输入的精度略高一些因为颜色信息在某些场景下有助于区分字符和背景。但灰度图的训练速度更快显存占用更小。如果显存紧张灰度图是更务实的选择。2.3 数据增强的策略与避坑数据增强是提升模型泛化能力的有效手段但字符识别任务的数据增强有其特殊性。不是所有图像增强方法都适用有些甚至会起反作用。我实际使用且验证有效的增强方法包括随机亮度对比度调整模拟不同光照条件幅度控制在±20%以内。幅度太大会导致字符与背景对比度失衡。随机轻微旋转角度控制在±5度以内。旋转角度过大会导致字符倾斜严重CTC对齐困难。随机高斯噪声标准差控制在0.01到0.03之间模拟拍摄噪声。随机遮挡在图片上随机画小矩形块模拟遮挡场景。遮挡面积不超过图片总面积的5%。我试过但效果不好的增强方法水平翻转字符翻转后语义完全改变比如“6”变成“9”这种增强会引入错误标签。大角度旋转超过10度的旋转会让字符排列方向改变CTC无法正确对齐。颜色抖动幅度过大导致部分字符颜色与背景接近模型难以区分。实操心得数据增强的强度需要根据训练轮次动态调整。前期可以用较强的增强帮助模型学习鲁棒特征后期减弱增强让模型精细化拟合。我通常在前30个epoch用完整增强之后逐步降低增强概率。2.4 数据加载器的实现要点数据加载器负责在训练时批量读取图片和标签。这里有几个容易出问题的地方。第一批处理中的序列长度不一致。同一个batch里不同图片的标签长度不同CTC损失函数需要知道每个样本的实际标签长度。解决方案是在collate函数中记录每个样本的标签长度传给损失函数。第二图片尺寸统一后的padding处理。填充的像素值应该与归一化后的背景一致通常用0或者均值。如果填充值选择不当模型可能会把填充区域误认为字符。第三多进程读取的稳定性。使用PyTorch的DataLoader时设置num_workers大于0可以加速数据读取但在某些环境下会出现死锁或者内存泄漏。我一般设置num_workers4并配合pin_memoryTrue加速GPU传输。# 数据加载器核心配置示例 train_loader DataLoader( datasettrain_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue, collate_fncollate_fn # 自定义collate处理变长标签 )3. 模型搭建与训练过程实现3.1 CNN骨干网络的选择与改造骨干网络负责从图像中提取特征。我对比了几种常见选择VGG、ResNet、MobileNet。VGG结构简单但参数量大ResNet梯度传播更好但需要调整输入尺寸MobileNet轻量但精度略低。最终我选择了一个改造版的ResNet-18作为骨干。改造点包括第一把第一层卷积的stride从2改为1这样输出的特征图高度更大保留更多空间信息。第二去掉最后的全局平均池化层和全连接层只保留卷积部分。第三在最后增加一个自适应池化层把特征图高度池化到1宽度保持不变。这样做的原因是CTC需要的是序列特征高度方向压缩到1表示每个时间步对应原图的一个水平条带宽度方向保留表示序列的时间维度。经过计算输入图片高度32像素经过骨干网络后高度变为1宽度从256变为64也就是特征序列长度为64。这个长度足够覆盖最长12位的字符序列。3.2 RNN序列建模的配置细节RNN部分我用了两层双向LSTM隐藏层维度256。双向的设计让每个时间步的输出同时包含左侧和右侧的上下文信息这对字符识别很重要。比如识别一个模糊的“5”时如果它左边是“1”右边是“2”模型可以利用上下文推断出中间更可能是“5”而不是“6”。LSTM的输入维度是CNN输出的通道数ResNet-18是512输出维度是字符类别数加1加1是CTC的空白标签。两层之间加了Dropout概率0.3防止过拟合。有一个细节需要注意RNN的序列长度。如果CNN输出的特征序列长度是64那么RNN就在64个时间步上展开。但实际图片中字符只占一部分宽度其余是背景。CTC的空白标签机制可以处理这种情况模型会在背景区域输出空白标签。3.3 CTC损失函数的计算与调试CTC损失函数是整个训练的核心。它的作用是计算给定输入序列和输出标签之间的对齐概率并对所有可能的对齐路径求和。实际使用中PyTorch提供了CTCLoss接口。调用时需要传入三个关键参数log_probsRNN输出的对数概率形状为T×N×C、targets拼接后的标签序列、input_lengths和target_lengths每个样本的输入和输出长度。我踩过的一个坑是log_probs的维度顺序。PyTorch的CTCLoss默认输入形状是T×N×C时间步×批次×类别而RNN的输出通常是N×T×C。需要先做transpose和log_softmax。如果顺序搞错损失值会异常大或者直接报错。另一个坑是blank标签的索引。CTC要求空白标签的索引为0实际字符从1开始编号。如果搞反了模型永远学不会输出正确字符。# CTC损失计算示例 log_probs F.log_softmax(rnn_output, dim2) # N×T×C log_probs log_probs.transpose(0, 1) # T×N×C loss ctc_loss(log_probs, targets, input_lengths, target_lengths)3.4 训练循环与学习率调度训练循环的伪代码逻辑是前向传播→计算CTC损失→反向传播→更新参数。我用的优化器是Adam初始学习率1e-3配合余弦退火调度。训练过程中我监控三个指标训练损失、验证损失、验证集上的准确率。准确率的计算方式是用贪心解码把模型输出转成字符序列与真实标签逐样本比较完全一致才算正确。实际训练时发现前10个epoch损失下降很快之后进入平台期。这时候降低学习率能帮助模型跳出局部最优。我设置的调度策略是每20个epoch学习率乘以0.5最低降到1e-5。批次大小设为64在单张显卡上训练约50个epoch后验证集准确率能到0.85左右。继续训练到100个epoch准确率提升到0.90附近但再往后提升就很有限了。注意事项CTC训练初期损失值可能很大这是正常的。因为模型一开始输出的概率分布接近均匀分布对齐路径的概率很低。随着训练进行损失会快速下降。如果训练几个epoch后损失仍然不降需要检查标签编码、blank索引、输入长度设置是否正确。4. 常见问题排查与效果优化实录4.1 训练不收敛的排查思路训练不收敛是新手最常遇到的问题。我整理了一套排查流程按优先级从高到低检查排查项检查方法常见问题标签编码打印几个样本的标签索引字符映射表错误索引越界CTC输入长度打印input_lengths和target_lengths输入长度小于目标长度CTC报错学习率观察损失曲线学习率过大导致震荡过小导致下降缓慢数据归一化检查输入像素值范围未归一化或归一化参数错误模型输出检查log_probs是否做了log_softmax忘记做softmax导致损失异常我遇到过一次典型问题损失值一直是NaN。排查后发现是某张图片的标签长度超过了CNN输出的特征序列长度CTC计算时出现除零。解决方案是过滤掉这类异常样本或者在数据预处理时确保所有图片的宽度足够。4.2 验证集准确率上不去的优化方向当训练损失正常下降但验证集准确率停滞时可以从以下几个方向优化数据层面增加数据增强的多样性特别是针对验证集中错误样本的特点做定向增强。比如如果错误集中在模糊图片上就增加高斯模糊增强。模型层面增加RNN的层数或隐藏维度提升序列建模能力。我试过把LSTM从两层加到三层准确率提升了约2个百分点但训练时间增加了30%。解码层面把贪心解码换成束搜索解码。贪心解码每个时间步只取概率最大的字符束搜索保留多个候选路径最终选概率最高的完整序列。实测束搜索宽度设为5时准确率能提升1到2个百分点。后处理层面对模型输出做规则修正。比如统计验证集中常见的混淆对如“0”和“O”、“1”和“I”在解码后根据上下文做替换。4.3 推理速度优化的实操技巧比赛通常对推理时间有要求优化推理速度也是重要环节。我实际用过的加速方法包括降低输入分辨率把图片高度从32降到24宽度从256降到192推理速度提升约40%准确率下降约1.5个百分点。需要根据时间限制权衡。模型量化把模型参数从FP32转为FP16推理速度提升约30%显存占用减半。PyTorch用model.half()一行代码就能实现。批处理推理测试时把多张图片打包成一个batch一起推理比逐张推理快很多。但要注意batch大小不能超过显存限制。去除冗余层如果骨干网络最后几层对精度贡献不大可以裁剪掉。我试过去掉ResNet-18的最后一个stage推理速度提升15%准确率只降了0.3个百分点。4.4 独家避坑经验汇总最后分享几个我在这个比赛中踩过的坑和总结的经验坑一字符映射表不一致。训练时建立的字符到索引映射推理时必须用同一个映射表。我有一次重新训练时改了映射表顺序导致推理结果全部错乱。建议把映射表保存成JSON文件训练和推理都从同一个文件读取。坑二图片读取的通道顺序。OpenCV读取的图片是BGR格式而PyTorch预训练模型通常期望RGB格式。如果不做转换精度会明显下降。我一般统一用PIL读取图片或者在OpenCV读取后做cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。坑三CTC解码时的重复字符处理。CTC解码规则是合并连续相同字符然后去掉空白标签。比如输出序列“11-1-0-0”解码后是“110”而不是“1110”。这个规则需要正确实现否则解码结果会多出重复字符。坑四验证集划分的随机性。如果验证集划分不合理比如验证集中某类字符特别多会导致准确率评估偏差。建议用分层抽样保证验证集中各字符的分布与训练集一致。坑五模型保存与加载的兼容性。保存模型时最好同时保存模型结构和参数或者用state_dict方式保存并在加载时重建相同结构。我有一次只保存了参数加载时模型结构改了一点导致参数对不上。这个比赛作为CV入门项目覆盖了图像分类、序列建模、损失函数设计等多个核心知识点。把这一套流程跑通一遍对理解深度学习项目的完整链路很有帮助。后续如果想进一步提升效果可以尝试引入注意力机制替代CTC或者用Transformer做序列建模这些都是值得探索的方向。