作者:云时之间
来源:知乎
链接:https://zhuanlan.zhihu.com/p/142269888
编辑:王萌
上次的一篇文章说了下DenseNet,这一篇文章来说一下CRNN+CTC的识别原理以及实现过程。这篇文章原理部分主要参考于白裳老师的“一文读懂CRNN+CTC文字识别”,其中的CTC原理的讲解部分是我见过最清晰易懂的,值得好好读一下。
一:OCR识别流程
通常我们做文本识别主要做上图中的三件事,检测的方法比较多,这里说一下识别,之前我们传统的识别过程是将检测到的数字划分为单字符(投影法等),然后扔到CNN里去分类,
但是深度学习的发展,出现了end2end的识别方式,简单的来说就是我们不需要将检测到的数字划分为单字符,无论你检测的数字长度多长,尺寸多宽,直接从头到尾给你识别出来。
比如上图的银行卡号,传统的方式要分割成
这样单个字符,现在直接从左到右识别完整,这样一来,我们就将单个字符的识别问题转化为序列的识别问题。
现在端到端的识别主要有两种比较流行的方式,以银行卡OCR识别为例:
CRNN+CTC,CNN+Seq2Seq+Attention是比较流行的方式,CRNN用的会更广泛些,因为Attention机制限制会比较大些,而这两者最主要的区别也就在这,两者都抛弃了softmax,而CRNN用了CTC来最后文本对齐,而CNN用了Attention机制,这也是端到端的难点所在:如何处理不定长序列对齐问题
二:CRNN+CTC结构
CRNN(卷积循环神经网络),顾名思义就是CNN+RNN的组合,论文中也提到,模型既有CNN强大的提取特征的能力,又有与RNN相同的性质,能够产生一系列序列化标签。
整个CRNN分为了三个部分:
①:卷积层:提取特征(代码输入322561)
②:循环层:使用深层双向RNN,预测从卷积层获取的特征序列的标签(真实值)分布(64*512)
③:转录层:使用CTC,代替softmax,训练样本无需对齐。
这篇文章的难点在于:
①:使用深度双层RNN
②:使用CTC(CTC原理极其难懂)
三:CRNN代码
CRNN算法输入10032归一化高度的词条图像,基于7层CNN(普遍使用VGG16)提取特征图,把特征图按列切分(Map-to-Sequence),每一列的512维特征,输入到两层各256单元的双向LSTM进行分类。在训练过程中,通过CTC损失函数的指导,实现字符位置与类标的近似软对齐。
以我现在使用的代码为例:
我输入的图像为:32
2561,W=256,经过CNN后,W=W/4,此时的W变为64,此时输入RNN的图像为164*512,此时的T=(W/4)=64,D=512,这里的T可以认为是RNN最大时间长度 ,依照本文代码就是有64个时间时间输入,且每个输入的列向量有512.
CNN代码:7层VGG
#CNN part
inputs = Input(shape=(picture_height, picture_width, 1), name='pic_inputs') # H×W×1 32*256*1
x = Conv2D(64, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_1')(inputs) # 32*256*64
x = BatchNormalization(name="BN_1")(x)
x = Activation("relu", name="relu_1")(x)
x = MaxPooling2D(pool_size=(2,2), strides=2, padding='valid', name='maxpl_1')(x) # 16*128*64
x = Conv2D(128, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_2')(x) # 16*128*128
x = BatchNormalization(name="BN_2")(x)
x = Activation("relu", name="relu_2")(x)
x = MaxPooling2D(pool_size=(2,2), strides=2, padding='valid', name='maxpl_2')(x) # 8*64*128
#卷积两次以后池化
x = Conv2D(256, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_3')(x) # 8*64*256
x = BatchNormalization(name="BN_3")(x)
x = Activation("relu", name="relu_3")(x)
x = Conv2D(256, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_4')(x) # 8*64*256
x = BatchNormalization(name="BN_4")(x)
x = Activation("relu", name="relu_4")(x)
x = MaxPooling2D(pool_size=(2,1), strides=(2,1), name='maxpl_3')(x) # 4*64*256
# 卷积两次以后池化
x = Conv2D(512, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_5')(x) # 4*64*512
x = BatchNormalization(axis=-1, name='BN_5')(x)
x = Activation("relu", name='relu_5')(x)
x = Conv2D(512, (3,3), strides=(1,1), padding="same", kernel_initializer=initializer, use_bias=True, name='conv2d_6')(x) # 4*64*512
x = BatchNormalization(axis=-1, name='BN_6')(x)
x = Activation("relu", name='relu_6')(x)
x = MaxPooling2D(pool_size=(2,1), strides=(2,1), name='maxpl_4')(x) # 2*64*512
#卷积一层
x = Conv2D(512, (2,2), strides=(1,1), padding='same', activation='relu', kernel_initializer=initializer, use_bias=True, name='conv2d_7')(x) # 2*64*512
x = BatchNormalization(name="BN_7")(x)
x = Activation("relu", name="relu_7")(x)
conv_otput = MaxPooling2D(pool_size=(2, 1), name="conv_output")(x) # 1*64*512
# Map2Sequence part
x = Permute((2, 3, 1), name='permute')(conv_otput) # 64*512*1
rnn_input = TimeDistributed(Flatten(), name='for_flatten_by_time')(x) # 64*512
RNN代码:双向LSTM
# RNN part,双向LSTM
y = Bidirectional(LSTM(256, kernel_initializer=initializer, return_sequences=True), merge_mode='sum', name='LSTM_1')(rnn_input) # 64*512
y = BatchNormalization(name='BN_8')(y)
y = Bidirectional(LSTM(256, kernel_initializer=initializer, return_sequences=True), name='LSTM_2')(y) # 64*512
这里用到了keras中的Bidirectional函数构建双向LSTM,这里要说一下深层BLSTM,
首先我们在输入层之上。套上一层双向LSTM层。相比RNN,能够更有效地处理句子中单词间的长距离影响。而双向LSTM就是在隐层同一时候有一个正向LSTM和反向LSTM,正向LSTM捕获了上文的特征信息,而反向LSTM捕获了下文的特征信息,这样相对单向LSTM来说能够捕获很多其它的特征信息。所以通常情况下双向LSTM表现比单向LSTM或者单向RNN要好。上图输入层之上的那个BLSTM层就是这个第一层双向LSTM层神经网络。
我们能够把神经网络的深度不断拓展,就是在第一层BLSTM基础上。再叠加一层BLSTM,叠加方法就是把每一个输入相应的BLSTM层的输出作为下一层BLSTM神经网络层相应节点的输入,由于两者序列长度是一一相应的,所以非常好叠加这两层神经网络。假设你觉得有必要,全然能够如此不断叠加更深一层的BLSTM来构造多层深度的BLSTM神经网络。
三:CTC
CTC的推导部分在白裳的文章中,贴上链接:
https://zhuanlan.zhihu.com/p/43534801
这里我谈一下我的理解:
看CTC的训练过程,CTC在这个阶段其实不关心对齐,这一点从ctc_loss的表达式可看出
CTC在训练时更多的考虑是将可能映射(去重、去空)出的标签包含的路径的概率之和来最大化(CTC假设每个时间片的输出是相互独立的,则路径的后验概率是每个时间片概率的累积),那么在输出时根据给定输入搜索概率最大的路径时就更可能搜索出能映射到正确结果的路径。且搜索时考虑了“多对一”的情况,进一步增加了解码出正确结果的可能性。所以我理解的CTC其实并不在意是否学习好了对齐这个过程,对齐只是寻找结果的一个手段,而CTC只在乎是结果,CTC是可以不需要对齐而能解码得到正确结果的方法。至少CTC在训练时不是对齐,但CTC在解码时,特别是搜索解码时,参与解码的部分合法路径可能是“比较整齐的界限分明的多对一对齐”。
CTC代码实现方式:
这里用的keras,keras中ctc_batch_cost函数可以实现CTC:
这里输入:args = (y_true, y_pred, pred_length, label_length)
y_true, y_pred分别是预测的标签和真实的标签
shape分别是(batch_size,max_label_length)和(batch_size, time_steps, num_categories)
perd_length, label_length分别是保存了每一个样本所对应的预测标签长度和真实标签长度
shape分别是(batch_size, 1)和(batch_size, 1)
输出:batch_cost 每一个样本所对应的loss shape是(batch_size, 1)
def ctc_loss_layer(args):
y_true, y_pred, pred_length, label_length = args
batch_cost = K.ctc_batch_cost(y_true, y_pred, pred_length, label_length)
return batch_cost
四:实现效果
训练速度,迭代速度还是可以的,实际测试:
在图片分辨率较为清晰且卡面不花里胡哨的情况下识别准确度以及很高,但是遇到一些定制卡,效果就差强人意,还需要标注数据,多训练,不然没办法。
PS:这段时间标注了几千张银行卡照片,等项目过了之后有条件公布出来,深度学习离不开数据,理解万岁~
来源:oschina
链接:https://my.oschina.net/u/4330613/blog/4946703