1. 文本识别与CRNN基础认知
第一次接触文本识别时,我盯着街边的广告牌突发奇想:计算机到底怎么读懂这些五花八门的文字?后来在车牌识别项目中踩了无数坑才明白,传统OCR需要复杂的字符分割和单独识别,而CRNN这种端到端模型直接把图片变成文字序列,就像教小孩从字母拼读升级到整句阅读。
CRNN(Convolutional Recurrent Neural Network)由三大模块组成:
- CNN部分:我用VGG式结构实测发现,5-7层卷积最适合提取文字特征。比如识别快递单时,3x3小卷积核能保留"收件人"这种细小笔画的细节
- RNN部分:双向LSTM就像两个人正反方向阅读文字,在识别古籍时效果提升特别明显。有次处理倾斜文本,双向网络准确率比单向高了18%
- CTC解码:这个最让我头疼的模块其实是"对齐神器"。记得第一次训练时没加CTC,识别"Hello"输出成了"H-e-l-l-o",CTC直接解决了字符对齐问题
对比传统OCR方案,CRNN有三处明显优势:
- 整行识别无需字符切割,曾经需要200行代码的预处理现在10行搞定
- 动态适应不同长度文本,从商品标签到长篇文章都能处理
- 模型体积小,在树莓派上跑识别速度能达到15fps
2. 数据准备与TFRecord实战
处理过上万张验证码图片后,我总结出文本识别数据集的关键点:字体多样性>数量。用Python生成样本时,这行代码帮了大忙:
from PIL import ImageFont font = ImageFont.truetype(random.choice(fonts_list), size=random.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_list=tf.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输入要求
经过多次实验验证的结构配置:
| 层级 | 卷积核 | 输出通道 | 备注 |
|---|---|---|---|
| conv1 | 3x3 | 64 | 配合ReLU激活 |
| pool1 | 2x2 | - | stride=2 |
| conv2 | 3x3 | 128 | 加入残差连接 |
| pool2 | 2x2 | - | 高度方向不降维 |
特别要注意的是最后一层的设计:
net = slim.conv2d(net, 512, [2,1], stride=[2,1], padding='valid')这个特殊卷积实现了特征图到序列的转换,相当于把高维特征"拍扁"成序列格式。我第一次实现时在这里卡了一周,直到画出特征图尺寸变化才明白原理。
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_units=hidden_size) for _ in range(layers)] bw_cell = [rnn.LSTMCell(num_units=hidden_size) for _ in range(layers)] outputs, _, _ = rnn.stack_bidirectional_dynamic_rnn( fw_cell, bw_cell, inputs, dtype=tf.float32)有个项目识别手写药方,加入双向结构后准确率从72%提升到89%,特别是对连笔字效果显著。
5. CTC损失实战详解
第一次见CTC公式时我直接懵了,后来用这个类比才理解:就像老师批改作文时,会忽略学生重复写的字("今今天天"→"今天")。CTC的核心是计算所有可能对齐路径的概率之和。
训练时要注意三个细节:
- 序列长度必须大于标签长度
- 学习率需要动态调整
- 使用Adadelta优化器比Adam更稳定
完整的训练循环代码框架:
ctc_loss = tf.nn.ctc_loss( labels=labels, inputs=logits, sequence_length=seq_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_dict=feed_dict) if epoch % 10 == 0: print(f"Epoch {epoch}, Loss: {loss_val:.4f}")在身份证识别项目中,CTC将错误率从15%降到3.2%。有个坑要注意:标签中出现的空格符需要特殊处理,我专门加了个字符类别。
6. 模型部署与优化实战
把CRNN部署到移动端时,我总结了几条实用经验:
- 量化模型能使体积缩小4倍:
tf.lite.TFLiteConverter - 使用TensorRT加速推理速度提升3倍
- 输入图片归一化改为[0,1]范围更稳定
性能对比测试数据:
| 设备 | 原始模型 | 优化后 |
|---|---|---|
| PC | 15ms | 8ms |
| 树莓派4B | 320ms | 180ms |
| Android手机 | 85ms | 45ms |
遇到过一个典型问题:部署后识别结果乱码。后来发现是字符映射表没打包进应用,解决方案是:
# 保存字符映射表 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%。