文字識別領域經(jīng)典論文回顧第一期:CRNN
共 6166字,需瀏覽 13分鐘
·
2022-02-09 17:36
1. 開篇
在文字識別經(jīng)典論文回顧這個系列里,我會介紹從深度學習興起后,文字識別領域一系列經(jīng)典的論文。這些論文的挑選標準主要有兩方面,一是是否具有足夠的啟發(fā)性,對解決文字識別領域的問題是否具有足夠的推進作用。二是論文的算法是否簡潔且統(tǒng)一,便于我們自己去復現(xiàn)。基于以上兩點,我的介紹也自然分為兩個方面,一方面是論文本身的解讀,二是代碼的解讀。對于所有將要介紹論文,我都會用一個統(tǒng)一的代碼框架進行復現(xiàn),代碼地址為:https://github.com/chibohe/text_recognition_toolbox
2. 論文解讀
2.1 總覽
CRNN是2015年提出的論文,論文的全稱是《An End-to-End Trainable Neural Network for Image-based Sequence Recognition and Its Application to Scene Text Recognition》.顧名思義,針對文字識別,CRNN一方面提出了一個端到端的網(wǎng)絡,另一方面則將文字識別問題轉(zhuǎn)換成了序列識別問題??傮w而言,CRNN的主要貢獻有以下4點:
- 提出了一個可端到端訓練的網(wǎng)絡,由特征提取層(feature extraction)、序列模型(sequence modeling)、轉(zhuǎn)譯層(transcription)三部分組成;
- 將文字識別問題轉(zhuǎn)化成序列識別問題,可處理任意長度的文本;
- 在無需詞典進行后處理修正的情況下,識別效果依然表現(xiàn)良好;
- 框架簡單,模型可以足夠小。
根據(jù)第一點,論文的主體框架為CNN+BiLSTM+CTC, 整體架構詳情見下圖:
接下來,我們對CRNN的各個組成部分進行逐一的拆解。
2.2 特征提取層
在特征提取層中,論文采用了CNN將原圖轉(zhuǎn)換成一系列的特征圖,這些特征圖保留了原始圖片的視覺特征信息。具體而言,論文中采用的CNN是類似VGG的架構,所有的卷積操作均采用3x3的卷積核,并且卷積核的數(shù)量會逐漸從最開始的64個,逐漸雙倍遞增至512.其中需要重點注意的是池化操作,在一般的maxpooling操作中,當kernel_size=(2, 2), stride=(2, 2)時,特征圖的高度和寬度會縮減至原先的二分之一。而在論文的第三個maxpooling和第四個maxpooling中,采用的尺寸是kernel_size=(2, 1), 即高度縮減為原先的二分之一,而寬度只會減一。這樣的操作是為了保留水平方向的信息,便于去處理長文本的識別。最后我們具體來看一下經(jīng)過CNN后,圖片尺寸的變化。在這里我們將一個張量表示為(B, C, H, W),其中B是批量處理的圖片數(shù)量,C是通道個數(shù),H是高度,W是寬度。假設原圖是1通道的灰度圖,高為32,寬為128,即(1, 1, 32, 128)。在經(jīng)過特征提取層之后,尺寸變?yōu)?1, 512, 1, 31). 具體示意圖如下,相當于feature sequence長度為31,而每個的通道數(shù)為512維。
2.3 序列模型
在序列模型中,我們的一個基本假設是處理的文本都是水平單向的。之所以提這一點,是因為后來文字識別領域有一大問題是處理彎曲文本的識別,也就是非水平的文本,而這恰恰是CRNN不熟悉的范圍。為什么要加一個序列模型呢,是因為在特征提取層之后,我們得到了一系列的特征向量,這些特征向量代表的都是圖片的視覺信息,而其中的語義尚未被挖掘。所以增加序列模型的目的,在于提取其中的語義關聯(lián)。論文采用的具體序列模型是雙向的LSTM,采用雙向是因為針對一段文本的某個字符,它不僅跟處于它左邊的字符有關聯(lián),跟它右邊的字符一般也會有一定的聯(lián)系。論文一共堆疊了兩個雙向的LSTM,經(jīng)過序列模型之后,張量的尺寸由(1, 512, 1, 31)變成了(1, 31, 512).其中LSTM隱藏層的維度是256,雙向的話就得乘2了,也就是512.具體示意圖如下:
2.4 轉(zhuǎn)譯層
轉(zhuǎn)譯層是將文字識別轉(zhuǎn)化成序列識別的關鍵所在。在一般深度學習的網(wǎng)絡結構中,輸入和輸出都是固定的。那么就文字識別來說,按照傳統(tǒng)的思路,為了將圖片里某個字符的位置和標注的label對齊,我們得標注每個字符在圖片中的具體位置。而CTC的提出就是為了解決這個問題的,當采用CTC loss之后,舉例來說,我們只用將上述圖片里的文本標記成"STATE",而不用標記出"S"具體在圖片中的哪個位置。CTC是計算最大概率的搜索路徑,具體數(shù)學推導可以參考:https://distill.pub/2017/ctc/
3. 代碼解讀
代碼解讀同樣分成三塊,每一塊都可以和上面的論文解讀進行對照。同時在一些需要注意的地方,我也給出了注釋。整體代碼參考:https://github.com/chibohe/text_recognition_toolbox/blob/main/networks/CRNN.py
3.1 特征提取層代碼
class BackBone(nn.Module):
def __init__(self, inplanes):
super(BackBone, self).__init__()
self.inplanes = inplanes
self.feature_extractor = nn.Sequential(
nn.Conv2d(self.inplanes, 64, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.ReLU(inplace=True),
# 第三個maxpooling,注意此處maxpooling的尺寸,可以和論文解讀進行對照
nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1)),
nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
# # 第四個maxpooling,同第三個一樣
nn.MaxPool2d(kernel_size=(2, 1), stride=(2, 1)),
nn.Conv2d(512, 512, kernel_size=(2, 2), stride=1, padding=0),
nn.ReLU(inplace=True)
)
?
def forward(self, inputs):
return self.feature_extractor(inputs)
?
# 這里一般會有一個Reshape layer,對照于論文中的Map-to-Sequence layer,
# 作用是將張量的高度變成1
class ReshapeLayer(nn.Module):
def __init__(self):
super(ReshapeLayer, self).__init__()
?
def forward(self, inputs):
B, C, H, W = inputs.size()
inputs = inputs.reshape(B, C, H * W)
inputs = inputs.permute(0, 2, 1)
return inputs3.2 序列模型代碼
class SequenceLayer(nn.Module):
def __init__(self, num_inputs, num_hiddens):
super(SequenceLayer, self).__init__()
self.num_inputs = num_inputs
self.num_hiddens = num_hiddens
self.rnn_1 = nn.LSTM(self.num_inputs, self.num_hiddens, bidirectional=True, batch_first=True)
self.rnn_2 = nn.LSTM(self.num_hiddens, self.num_hiddens, bidirectional=True, batch_first=True)
self.linear = nn.Linear(self.num_hiddens * 2, self.num_hiddens)
?
def forward(self, inputs):
self.rnn_1.flatten_parameters()
recurrent, _ = self.rnn_1(inputs)
inputs = self.linear(recurrent)
self.rnn_2.flatten_parameters()
recurrent, _ = self.rnn_2(inputs)
outputs = self.linear(recurrent)
return outputs3.2 轉(zhuǎn)譯層代碼
class CTCLoss(nn.Module):
def __init__(self, params, reduction='mean'):
super().__init__()
blank_idx = params.blank_idx
self.loss_func = torch.nn.CTCLoss(blank=blank_idx, reduction=reduction, zero_infinity=True)
?
def forward(self, pred, args):
batch_size = pred.size(0)
label, label_length = args['targets'], args['targets_lengths']
pred = pred.log_softmax(2)
pred = pred.permute(1, 0, 2)
preds_lengths = torch.tensor([pred.size(0)] * batch_size, dtype=torch.long)
loss = self.loss_func(pred, label.cuda(), preds_lengths.cuda(), label_length.cuda())
return loss
# 最后將上述結構匯總起來
class CRNN(nn.Module):
def __init__(self, flags):
super(CRNN, self).__init__()
self.inplanes = 1 if flags.Global.image_shape[0] == 1 else 3
self.num_inputs = flags.SeqRNN.input_size
self.num_hiddens = flags.SeqRNN.hidden_size
self.converter = CTCLabelConverter(flags)
self.num_classes = self.converter.char_num
?
self.feature_extractor = BackBone(self.inplanes)
self.reshape_layer = ReshapeLayer()
self.sequence_layer = SequenceLayer(self.num_inputs, self.num_hiddens)
self.linear_layer = nn.Linear(self.num_hiddens, self.num_classes)
?
def forward(self, inputs):
x = self.feature_extractor(inputs)
x = self.reshape_layer(x)
x = self.sequence_layer(x)
outputs = self.linear_layer(x)
?
return outputs4. 收尾
經(jīng)過上述的論文和代碼解讀,可以看出來,CRNN是一個結構十分清晰的算法。從理論上來講,算法的每一個組成部分都交代的很清楚。從實現(xiàn)層面來講,復現(xiàn)起來只用了不到100行代碼。另外從實際工作經(jīng)驗上來講,雖然CRNN提出已經(jīng)5年有余,但是針對一般文檔類的數(shù)據(jù),它仍然是最有效的算法之一。所以以它作為整個系列的開篇,是再合適不過的了。下一篇我會沿著CNN+LSTM+CTC的路線,介紹算法GRCNN,敬請期待。
