1. 开篇

循着第一篇CRNN的路径,我们介绍这个系列的第二篇论文——GRCNN.因为CRNN已经搭建起了一个清晰简洁的文字识别框架,加上受到RCNN(Recurrent Convolution Neural Network)的启发,本文很自然的将CRNN的backbone替换成了RCNN,从而提出了GRCNN的识别方法。代码实现可见:GitHub - chibohe/text_recognition_toolbox: text_recognition_toolbox: The reimplementation of a series of classical scene text recognition papers with Pytorch in a uniform way.

2. 论文解读

2.1 总览

GRCNN是2017年提出的论文,论文的全称是《Gated Recurrent Convolution Neural Network for OCR》。本文的整体架构和CRNN类似,都是分成特征提取层、序列模型和转译层。主要的改进点在特征提取层,具体是在卷积神经网络的前向信息中加入了循环信息,即在高层中能同时保证不同感受野信息的输入,从而能兼顾对不同尺度特征的提取。算法整体架构如下:

2.2 特征提取层

本文的重点就在于特征提取层。首先,我们讲一下模型设计主要考虑的因素。一般来说,针对经典的卷积神经网络,例如ResNet、Inception等等,网络的层数都比较深。网络的层数越深,其感受野就会越大,关于感受野的计算,可以参考。。。而感受野越大的话,就会包括原图上更大范围的视觉特征。那么针对文字识别而言,是不是感受野越大就越好呢,答案是不尽然。在这里我们举论文中的一个例子来说明,例如下面这幅图。当我们准备识别"h"的时候,将感受野适当扩大是有好处的,即图中的红框所示。而当我们继续扩大感受野,达到绿框的范围时,因为包含了"p"的信息,可能就会对识别产生干扰了。由此可见,感受野并非越大越好。

下面我们就具体看看论文作者如何去兼顾不同的感受野。首先我们回顾一下循环神经网络RNN,RNN的核心在于如何计算隐藏状态,一个通用的计算公式如下,其中u(t)是输入,x(t)就是t时刻的隐藏状态,F代表激活函数。

再由下面的公式,我们可以将经典的RNN改造成RCL(Recurrent Convolution Layer),其中"*"代表卷积。

 

接下来具体看看是如何实现上述思路的,

 

上述流程图看起来有些复杂,我们用下面的公式进行阐述。其中G是门控的意思,跟GRU(Gated Recurrent Network)架构非常类似,区别的地方在于这里的t并不是一般意义上的时间步,而更多的类似层的概念,即一层一层的往上累积。 

 

上述生成的G到底有什么用呢。通过下面的公式我们就清晰了,其中G就表示隐藏状态信息流入的强度。当设置成1时,我们选择保留t-1步所有的信息,当设置成0时,我们完全不考虑上一个隐藏状态,而只考虑u(t),即只考虑最开始输入的信息。

 

结合上面的流程图和公式,我们就明白了,u(t)即代表较小感受野的信息,适合提取小目标的特征,而x(t)则代表较大感受野的信息,适合提取大目标的特征。这样就兼顾了不同尺度的特征信息。

2.3 序列模型

这里的序列模型跟CRNN中的序列模型都是LSTM,区别在于,这里使用的是peephole LSTM.说白了就是在计算各个门控的时候,加入了cell state,即c(t).具体公式如下:

2.4 转译层

转译层跟CRNN里的一致,都是使用CTC进行解码,这里就不再赘述了。

3. 代码解读

这里重点讲一下特征提取层的代码,即如何实现GRCL.

class GRCL_unit(nn.Module):
    def __init__(self, output_channel):
        super(GRCL_unit, self).__init__()
        self.wgf_bn = nn.BatchNorm2d(output_channel)
        self.wgr_bn = nn.BatchNorm2d(output_channel)
        self.wf_bn = nn.BatchNorm2d(output_channel)
        self.wx_bn = nn.BatchNorm2d(output_channel)
        self.gx_bn = nn.BatchNorm2d(output_channel)

    def forward(self, wgf, wgx, wf, wx):
      # 这里对应着上述公式中关于G是如何生成的
        G = F.sigmoid(self.wgf_bn(wgf) + self.wgr_bn(wgx))
        wf = self.wf_bn(wf)
        wx = self.wx_bn(wx)
			# x就是隐藏状态,通过与G作element wise乘积,再加入输入即可
        x = F.relu(wf + self.gx_bn(wx * G))

        return x

 然后就是GRCL整体的代码。

class GRCL(nn.Module):
    def __init__(self, input_channel, output_channel, num_iterations, 
                 kernel_size, pad):
        super(GRCL, self).__init__()
        # 这里wgf和wgr和上面公式是一一对应的,另外注意在不同的时刻,权重是共享的,即wgr_x和wg_x都是一样的
        self.wgf_u = nn.Conv2d(input_channel, output_channel, 1, 1, 0, bias=False)
        self.wgr_x = nn.Conv2d(output_channel, output_channel, 1, 1, 0, bias=False)
        self.wf_u = nn.Conv2d(input_channel, output_channel, kernel_size, 1, pad, bias=False)
        self.wg_x = nn.Conv2d(output_channel, output_channel, kernel_size, 1, pad, bias=False)

        self.bn_x = nn.BatchNorm2d(output_channel)

        self.num_iterations = num_iterations
        self.GRCL = [GRCL_unit(output_channel) for _ in range(num_iterations)]
        self.GRCL = nn.Sequential(*self.GRCL)

    def forward(self, inputs):
        self.wgf = self.wgf_u(inputs)
        # t=0时刻的隐藏状态,对应于上述公式的x(0)
        self.wf = self.wf_u(inputs)
        x = F.relu(self.bn_x(self.wf))

        for i in range(self.num_iterations):
            x = self.GRCL[i](self.wgf, self.wgr_x(x), self.wf, self.wg_x(x))

        return x

4. 收尾

GRCNN这种backbone的架构在后续的OCR发展中被提及的次数不多,但其背后的设计思路还是值得学习的。下一篇会开始介绍attention在文字识别里的应用。

 

更多推荐