一.概述
常用文字識別算法主要有兩個框架:
- CNN+RNN+CTC(CRNN+CTC)
- CNN+Seq2Seq+Attention
本文介紹第一種方法。
CRNN是一種卷積循環神經網絡結構,用於解決基於圖像的序列識別問題,特別是場景文字識別問題。
文章認爲文字識別是對序列的預測方法,所以採用了對序列預測的RNN網絡。通過CNN將圖片的特徵提取出來後採用RNN對序列進行預測,最後通過一個CTC的翻譯層得到最終結果。說白了就是CNN+RNN+CTC的結構。
CRNN 全稱爲 Convolutional Recurrent Neural Network,主要用於端到端地對不定長的文本序列進行識別,不用先對單個文字進行切割,而是將文本識別轉化爲時序依賴的序列學習問題,就是基於圖像的序列識別。
二.CRNN網絡結構
整個CRNN網絡結構包含三部分,從下到上依次爲:
- CNN(卷積層),使用深度CNN,對輸入圖像提取特徵,得到特徵圖;
- RNN(循環層),使用雙向RNN(BLSTM)對特徵序列進行預測,對序列中的每個特徵向量進行學習,並輸出預測標籤(真實值)分佈;
- CTC loss(轉錄層),使用 CTC 損失,把從循環層獲取的一系列標籤分佈轉換成最終的標籤序列。
端到端OCR的難點在哪兒呢?在於怎麼處理不定長序列對齊問題!CRNN OCR其實是借用了語音識別中解決不定長語音序列的思路。與語音識別問題類似,OCR可建模爲時序依賴的詞彙或者短語識別問題。基於聯結時序分類(Connectionist Temporal Classification, CTC)訓練RNN的算法,在語音識別領域顯著超過傳統語音識別算法。一些學者嘗試把CTC損失函數借鑑到OCR識別中,CRNN 就是其中代表性算法。CRNN算法輸入100*32歸一化高度的詞條圖像,基於7層CNN(普遍使用VGG16)提取特徵圖,把特徵圖按列切分(Map-to-Sequence),每一列的512維特徵,輸入到兩層各256單元的雙向LSTM進行分類。在訓練過程中,通過CTC損失函數的指導,實現字符位置與類標的近似軟對齊。
CRNN借鑑了語音識別中的LSTM+CTC的建模方法,不同點是輸入進LSTM的特徵,從語音領域的聲學特徵(MFCC等),替換爲CNN網絡提取的圖像特徵向量。CRNN算法最大的貢獻,是把CNN做圖像特徵工程的潛力與LSTM做序列化識別的潛力,進行結合。它既提取了魯棒特徵,又通過序列識別避免了傳統算法中難度極高的單字符切分與單字符識別,同時序列化識別也嵌入時序依賴(隱含利用語料)。在訓練階段,CRNN將訓練圖像統一縮放100×32(w × h);在測試階段,針對字符拉伸導致識別率降低的問題,CRNN保持輸入圖像尺寸比例,但是圖像高度還是必須統一爲32個像素,卷積特徵圖的尺寸動態決定LSTM時序長度。這裏舉個例子
現在輸入有個圖像,爲了將特徵輸入到Recurrent Layers,做如下處理:
- 首先會將圖像縮放到 32×W×1 大小
- 然後經過CNN後變爲 1×(W/4)× 512
- 接着針對LSTM,設置 T=(W/4) , D=512 ,即可將特徵輸入LSTM。
- LSTM有256個隱藏節點,經過LSTM後變爲長度爲T × nclass的向量,再經過softmax處理,列向量每個元素代表對應的字符預測概率,最後再將這個T的預測結果去冗餘合併成一個完整識別結果即可。
1.CNN
卷積層的結構圖:
這裏有一個很精彩的改動,一共有四個最大池化層,但是最後兩個池化層的窗口尺寸由 2x2 改爲 1x2,也就是圖片的高度減半了四次(除以),而寬度則只減半了兩次(除以 ),這是因爲文本圖像多數都是高較小而寬較長,所以其feature map也是這種高小寬長的矩形形狀,如果使用1×2的池化窗口可以儘量保證不丟失在寬度方向的信息,更適合英文字母識別(比如區分i和l)。
CRNN 還引入了BatchNormalization模塊,加速模型收斂,縮短訓練過程。
輸入圖像爲灰度圖像(單通道);高度爲32,這是固定的,圖片通過 CNN 後,高度就變爲1,這點很重要;寬度爲160,寬度也可以爲其他的值,但需要統一,所以輸入CNN的數據尺寸爲 (channel, height, width)=(1, 32, 160)。
CNN的輸出尺寸爲 (512, 1, 40)。即 CNN 最後得到512個特徵圖,每個特徵圖的高度爲1,寬度爲40。
注意:最後的卷積層是一個2*2,s=1,p=0的卷積,此時也是相當於將feature map放縮爲原來的1/2,所以整個CNN層將圖像的h放縮爲原來的,所以最後CNN輸出的featuremap的高度爲1。
assert imgH % 16 == 0, 'imgH has to be a multiple of 16'
在程序中,圖像的h必須爲16的整數倍。
assert h == 1, "the height of conv must be 1"
前向傳播時,CNN得到的featuremap的h必須爲1。
最後CNN得到的featuremap尺度爲512x1x16
2.Map-to-Sequence
我們是不能直接把 CNN 得到的特徵圖送入 RNN 進行訓練的,需要進行一些調整,根據特徵圖提取 RNN 需要的特徵向量序列。
現在需要從 CNN 模型產生的特徵圖中提取特徵向量序列,每一個特徵向量(如上圖中的一個紅色框)在特徵圖上按列從左到右生成,每一列包含512維特徵,這意味着第 i 個特徵向量是所有的特徵圖第 i 列像素的連接,這些特徵向量就構成一個序列。
由於卷積層,最大池化層和激活函數在局部區域上執行,因此它們是平移不變的。因此,特徵圖的每列(即一個特徵向量)對應於原始圖像的一個矩形區域(稱爲感受野),並且這些矩形區域與特徵圖上從左到右的相應列具有相同的順序。特徵序列中的每個向量關聯一個感受野。
具體來講,這40個序列向量,分別以stride=4,與原圖相對應,用來對原圖的相關區域進行分類。
這些特徵向量序列就作爲循環層的輸入,每個特徵向量作爲 RNN 在一個時間步(time step)的輸入。
3.RNN
因爲 RNN 有梯度消失的問題,不能獲取更多上下文信息,所以 CRNN 中使用的是 LSTM,LSTM 的特殊設計允許它捕獲長距離依賴。
LSTM 是單向的,它只使用過去的信息。然而,在基於圖像的序列中,兩個方向的上下文是相互有用且互補的。將兩個LSTM,一個向前和一個向後組合到一個雙向LSTM中。此外,可以堆疊多層雙向LSTM,深層結構允許比淺層抽象更高層次的抽象。
這裏採用的是兩層各256單元的雙向 LSTM 網絡:
通過上面一步,我們得到了40個特徵向量,每個特徵向量長度爲512,在 LSTM 中一個時間步就傳入一個特徵向量進行分類,這裏一共有40個時間步。
我們知道一個特徵向量就相當於原圖中的一個小矩形區域,RNN 的目標就是預測這個矩形區域爲哪個字符,即根據輸入的特徵向量,進行預測,得到所有字符的softmax概率分佈,這是一個長度爲字符類別數的向量,作爲CTC層的輸入。
因爲每個時間步都會有一個輸入特徵向量 ,輸出一個所有字符的概率分佈 ,所以輸出爲 40 個長度爲字符類別數的向量構成的後驗概率矩陣。
如下圖所示:
然後將這個後驗概率矩陣傳入轉錄層。
該部分源碼如下:
self.rnn = nn.Sequential( BidirectionalLSTM(512, nh, nh), BidirectionalLSTM(nh, nh, nclass))
然後參數設置如下:
nh=256 nclass = len(opt.alphabet) + 1 nc = 1
其中雙向LSTM的實現如下:
class BidirectionalLSTM(nn.Module): def __init__(self, nIn, nHidden, nOut): super(BidirectionalLSTM, self).__init__() self.rnn = nn.LSTM(nIn, nHidden, bidirectional=True) self.embedding = nn.Linear(nHidden * 2, nOut) def forward(self, input): recurrent, _ = self.rnn(input) T, b, h = recurrent.size() t_rec = recurrent.view(T * b, h) output = self.embedding(t_rec) # [T * b, nOut] output = output.view(T, b, -1) return output
所以第一次LSTM得到的output=[40*256,256],然後view成output=[40,256,256]
第二次LSTM得到的結果是output=[40*256,nclass],然後view成output=[40,256,nclass]
4.CTC loss
這算是 CRNN 最難的地方,這一層爲轉錄層,轉錄是將 RNN 對每個特徵向量所做的預測轉換成標籤序列的過程。數學上,轉錄是根據每幀預測找到具有最高概率組合的標籤序列。
端到端OCR識別的難點在於怎麼處理不定長序列對齊的問題!OCR可建模爲時序依賴的文本圖像問題,然後使用CTC(Connectionist Temporal Classification, CTC)的損失函數來對 CNN 和 RNN 進行端到端的聯合訓練。
4.1序列合併機制
我們現在要將 RNN 輸出的序列翻譯成最終的識別結果,RNN進行時序分類時,不可避免地會出現很多冗餘信息,比如一個字母被連續識別兩次,這就需要一套去冗餘機制。
比如我們要識別上面這個文本,其中 RNN 中有 5 個時間步,理想情況下 t0, t1, t2 時刻都應映射爲“a”,t3, t4 時刻都應映射爲“b”,然後將這些字符序列連接起來得到“aaabb”,我們再將連續重複的字符合併成一個,那麼最終結果爲“ab”。
這似乎是個比較好的方法,但是存在一個問題,如果是book,hello之類的詞,合併連續字符後就會得到 bok 和 helo,這顯然不行,所以 CTC 有一個blank機制來解決這個問題。
我們以“-”符號代表blank,RNN 輸出序列時,在文本標籤中的重複的字符之間插入一個“-”,比如輸出序列爲“bbooo-ookk”,則最後將被映射爲“book”,即有blank字符隔開的話,連續相同字符就不進行合併。
即對字符序列先刪除連續重複字符,然後從路徑中刪除所有“-”字符,這個稱爲解碼過程,而編碼則是由神經網絡來實現。引入blank機制,我們就可以很好地解決重複字符的問題。
相同的文本標籤可以有多個不同的字符對齊組合,例如,“aa-b”和“aabb”以及“-abb”都代表相同的文本(“ab”),但是與圖像的對齊方式不同。更總結地說,一個文本標籤存在一條或多條的路徑。
4.2訓練階段
在訓練階段,我們需要根據這些概率分佈向量和相應的文本標籤得到損失函數,從而訓練神經網路模型,下面來看看如何得到損失函數的。
如上圖,對於最簡單的時序爲 2 的字符識別,有兩個時間步長(t0,t1)和三個可能的字符爲“a”,“b”和“-”,我們得到兩個概率分佈向量,如果採取最大概率路徑解碼的方法,則“--”的概率最大,即真實字符爲空的概率爲0.6*0.6=0.36。
但是爲字符“a”的情況有多種對齊組合,“aa”, “a-“和“-a”都是代表“a”,所以,輸出“a”的概率應該爲三種之和:
0.4 * 0.4 + 0.4 * 0.6 + 0.6 * 0.4 = 0.16 + 0.24 + 0.24 = 0.64
所以“a”的概率比空“”的概率高!如果標籤文本爲“a”,則通過計算圖像中爲“a”的所有可能的對齊組合(或者路徑)的分數之和來計算損失函數。
所以對於 RNN 給定輸入概率分佈矩陣爲,T 是序列長度,最後映射爲標籤文本 的總概率爲:
其中 代表從序列到序列的映射函數 B 變換後是文本 l 的所有路徑集合,而 則是其中的一條路徑。每條路徑的概率爲各個時間步中對應字符的分數的乘積。
類似普通的分類,CTC的損失函數O定義爲負的最大似然,爲了計算方便,對似然取對數。
我們就是需要訓練網絡使得這個概率值最大化,類似於普通的分類,CTC的損失函數定義爲概率的負最大似然函數,爲了計算方便,對似然函數取對數。
通過對損失函數的計算,就可以對之前的神經網絡進行反向傳播,神經網絡的參數根據所使用的優化器進行更新,從而找到最可能的像素區域對應的字符。
這種通過映射變換和所有可能路徑概率之和的方式使得 CTC 不需要對原始的輸入字符序列進行準確的切分。
4.3測試階段
在測試階段,過程與訓練階段有所不同,我們用訓練好的神經網絡來識別新的文本圖像。這時候我們事先不知道任何文本,如果我們像上面一樣將每種可能文本的所有路徑計算出來,對於很長的時間步和很長的字符序列來說,這個計算量是非常龐大的,這不是一個可行的方案。
我們知道 RNN 在每一個時間步的輸出爲所有字符類別的概率分佈,即一個包含每個字符分數的向量,我們取其中最大概率的字符作爲該時間步的輸出字符,然後將所有時間步得到一個字符進行拼接得到一個序列路徑,即最大概率路徑,再根據上面介紹的合併序列方法得到最終的預測文本結果。
在輸出階段經過 CTC 的翻譯,即將網絡學習到的序列特徵信息轉化爲最終的識別文本,就可以對整個文本圖像進行識別。
比如上面這個圖,有5個時間步,字符類別有“a”, “b” and “-” (blank),對於每個時間步的概率分佈,我們都取分數最大的字符,所以得到序列路徑“aaa-b”,先移除相鄰重複的字符得到“a-b”,然後去除blank字符得到最終結果:“ab”。
4.4總結
預測過程中,先使用標準的CNN網絡提取文本圖像的特徵,再利用BLSTM將特徵向量進行融合以提取字符序列的上下文特徵,然後得到每列特徵的概率分佈,最後通過轉錄層(CTC)進行預測得到文本序列。
利用BLSTM和CTC學習到文本圖像中的上下文關係,從而有效提升文本識別準確率,使得模型更加魯棒。
在訓練階段,CRNN 將訓練圖像統一縮放爲160×32(w × h);在測試階段,針對字符拉伸會導致識別率降低的問題,CRNN 保持輸入圖像尺寸比例,但是圖像高度還是必須統一爲32個像素,卷積特徵圖的尺寸動態決定 LSTM 的時序長度(時間步長)。
五.補充說明
5.1RCNN的編碼
假設有26個英文字母要識別,那麼種類數=27(還有一個空白blank字符)
假設CNN輸出以50個序列爲基準(讀者這裏看不懂就去看RNN識別手寫數字識別),序列太大訓練不準,識別結果會漏字母。序列太小訓練不準,識別會多字母。
5.2CTC詳解
如下圖,爲了便於讀者理解,簡化了RNN的結構,只有單向的一層LSTM,把聲學建模單元選擇爲字母{a-z},並對建模單元字符集做了擴展,且定義了從輸出層到最終label序列的多對一映射函數,使得RNN輸出層能映射到最終的label序列。
所以,如果要計算𝒑(𝒛│𝒙),可以累加其對應的全部輸出序列(也即映射到最終label的“路徑”)的概率即可,如下圖。
如下圖,基於RNN條件獨立假設,即可得到CTC Loss函數的定義:
假定選擇單層LSTM爲RNN結構,則最終的模型結構如下圖:
由於直接暴力計算 𝒑(𝒛│𝒙)的複雜度非常高,作者借鑑HMM的Forward-Backward算法思路,利用動態規劃算法求解。
如下圖,爲了更形象表示問題的搜索空間,用X軸表示時間序列, Y軸表示輸出序列,並把輸出序列做標準化處理,輸出序列中間和頭尾都加上blank,用l表示最終標籤,l’表示擴展後的形式,則由2|l| + 1 = 2|l’|,比如:l=apple => l’=_a_p_p_l_e_
圖中並不是所有的路徑都是合法路徑,所有的合法路徑需要遵循一些約束,如下圖:
所以,依據以上約束規則,遍歷所有映射爲“apple”的合法路徑,最終時序T=8,標籤labeling=“apple”的全部路徑如下圖:
接下來,如何計算這些路徑的概率總和?暴力遍歷?分而治之?作者借鑑HMM的Forward-Backward算法思路,利用動態規劃算法求解,可以將路徑集合分爲前向和後向兩部分,如下圖所示:
通過動態規劃求解出前向概率之後,可以用前向概率來計算CTC Loss函數,如下圖:
類似地方式,我們可以定義反向概率,並用反向概率來計算CTC Loss函數,如下圖:
去掉箭頭方向,把前向概率和後向概率結合起來也可以計算CTC Loss函數,這對於後面CTC Loss函數求導計算是十分重要的一步,如下圖所示:
總結一下,根據前向概率計算CTC Loss函數,得到以下結論:
根據後向概率計算CTC Loss函數,得到以下結論:
根據任意時刻的前向概率和後向概率計算CTC Loss函數,得到以下結論:
至此,我們已經得到CTC Loss的有效計算方法,接下來對其進行求導
如下圖飄紅部分是CTC Loss函數求導的核心部分:
CTC Loss函數相對於RNN輸出層元素的求導過程如下圖所示:
至此,完成了CTC Loss在訓練過程中的講解。
CTC Loss在測試過程中的解碼方法主要有如下兩種:
- CTC Prefix Search Decoding
- CTC Beam Search Decoding