从数据到识别:CRNN文本识别实战全流程拆解
1. 文本识别与CRNN基础认知第一次接触文本识别时我盯着街边的广告牌突发奇想计算机到底怎么读懂这些五花八门的文字后来在车牌识别项目中踩了无数坑才明白传统OCR需要复杂的字符分割和单独识别而CRNN这种端到端模型直接把图片变成文字序列就像教小孩从字母拼读升级到整句阅读。CRNNConvolutional Recurrent Neural Network由三大模块组成CNN部分我用VGG式结构实测发现5-7层卷积最适合提取文字特征。比如识别快递单时3x3小卷积核能保留收件人这种细小笔画的细节RNN部分双向LSTM就像两个人正反方向阅读文字在识别古籍时效果提升特别明显。有次处理倾斜文本双向网络准确率比单向高了18%CTC解码这个最让我头疼的模块其实是对齐神器。记得第一次训练时没加CTC识别Hello输出成了H-e-l-l-oCTC直接解决了字符对齐问题对比传统OCR方案CRNN有三处明显优势整行识别无需字符切割曾经需要200行代码的预处理现在10行搞定动态适应不同长度文本从商品标签到长篇文章都能处理模型体积小在树莓派上跑识别速度能达到15fps2. 数据准备与TFRecord实战处理过上万张验证码图片后我总结出文本识别数据集的关键点字体多样性数量。用Python生成样本时这行代码帮了大忙from PIL import ImageFont font ImageFont.truetype(random.choice(fonts_list), sizerandom.randint(24,32))TFRecord转换的避坑指南图片resize要保持宽高比我常用这个公式new_width int(original_width * target_height / original_height)标签处理要特别注意特殊字符有次数据集里的¥符号导致训练崩溃后来加了字符过滤valid_chars set(abcdefghijklmnopqrstuvwxyz0123456789) label .join([c for c in label.lower() if c in valid_chars])完整的数据预处理流程应该是这样的字体渲染建议用20种字体混合背景合成高斯噪声随机颜色块透视变换模拟拍摄角度序列化存储关键代码片段def _bytes_feature(value): return tf.train.Feature(bytes_listtf.train.BytesList(value[value])) feature { image: _bytes_feature(tf.compat.as_bytes(image.tostring())), label: _bytes_feature(label.encode(utf-8)) }3. CNN特征提取的工程细节在搭建CNN时我掉进过几个大坑池化层步长设置不当导致特征图尺寸计算错误忘记加BatchNorm导致收敛极慢最后一层卷积输出通道数不符合RNN输入要求经过多次实验验证的结构配置层级卷积核输出通道备注conv13x364配合ReLU激活pool12x2-stride2conv23x3128加入残差连接pool22x2-高度方向不降维特别要注意的是最后一层的设计net slim.conv2d(net, 512, [2,1], stride[2,1], paddingvalid)这个特殊卷积实现了特征图到序列的转换相当于把高维特征拍扁成序列格式。我第一次实现时在这里卡了一周直到画出特征图尺寸变化才明白原理。4. 双向LSTM的调参技巧RNN部分最容易出现梯度爆炸我的解决方案是初始化使用正交矩阵tf.orthogonal_initializer()加入梯度裁剪tf.clip_by_global_norm(gradients, 5.0)超参数设置经验值LSTM层数2-3层最佳4层以上反而下降隐藏单元数256-512之间超过512容易过拟合dropout率0.3-0.5注意只在训练时启用双向LSTM的实现关键点fw_cell [rnn.LSTMCell(num_unitshidden_size) for _ in range(layers)] bw_cell [rnn.LSTMCell(num_unitshidden_size) for _ in range(layers)] outputs, _, _ rnn.stack_bidirectional_dynamic_rnn( fw_cell, bw_cell, inputs, dtypetf.float32)有个项目识别手写药方加入双向结构后准确率从72%提升到89%特别是对连笔字效果显著。5. CTC损失实战详解第一次见CTC公式时我直接懵了后来用这个类比才理解就像老师批改作文时会忽略学生重复写的字今今天天→今天。CTC的核心是计算所有可能对齐路径的概率之和。训练时要注意三个细节序列长度必须大于标签长度学习率需要动态调整使用Adadelta优化器比Adam更稳定完整的训练循环代码框架ctc_loss tf.nn.ctc_loss( labelslabels, inputslogits, sequence_lengthseq_len) optimizer tf.train.AdadeltaOptimizer(learning_rate).minimize(ctc_loss) with tf.Session() as sess: for epoch in range(100): _, loss_val sess.run([optimizer, ctc_loss], feed_dictfeed_dict) if epoch % 10 0: print(fEpoch {epoch}, Loss: {loss_val:.4f})在身份证识别项目中CTC将错误率从15%降到3.2%。有个坑要注意标签中出现的空格符需要特殊处理我专门加了个字符类别。6. 模型部署与优化实战把CRNN部署到移动端时我总结了几条实用经验量化模型能使体积缩小4倍tf.lite.TFLiteConverter使用TensorRT加速推理速度提升3倍输入图片归一化改为[0,1]范围更稳定性能对比测试数据设备原始模型优化后PC15ms8ms树莓派4B320ms180msAndroid手机85ms45ms遇到过一个典型问题部署后识别结果乱码。后来发现是字符映射表没打包进应用解决方案是# 保存字符映射表 with open(char_map.json, w) as f: json.dump(char_dict, f) # 加载时 char_dict json.load(open(char_map.json))7. 常见问题解决方案训练阶段问题损失不下降检查数据标签是否正确我曾遇到标签文件编码错误导致训练失败输出全是空白降低CTC的blank类别权重过拟合加入Mixup数据增强推理阶段问题倾斜文本识别差加入STN空间变换网络长文本漏识别调整LSTM的sequence_length参数特定字符错误针对性增加训练样本有个电商项目识别商品价格时发现9和g总是混淆。后来在数据增强时专门加入了这两种字符的混合样本错误率下降了60%。