# Week 1:CRNN + CTC ###### tags: `技術研討` ## 與會者 晟瑋、昊中、沛筠、宜昌、育銓、信賢 + CV Team ## Agenda 1. 前言 (LiLi) 2. CNN (LiLi) 3. RNN (昱睿) 4. Transcription (昱睿) 5. Loss (昱睿) 6. github code 的執行流程架構圖 (LiLi) 7. data_manager.py (LiLi) 8. crnn.py (昱睿) ## 論文研讀: An End-to-End Trainable Neural Network for Image-based Sequence Recognition and Its Application to Scene Text Recognition * 論文連結:[link](https://arxiv.org/pdf/1507.05717.pdf) - 前言 - 舊有的演算法都是將訓練和調教分開,但 CRNN 可以做到 end-to-end 的訓練 - DCNN 模型 - 從詞彙當中把每個字元裁切出來 (問題點:要先訓練強字元偵測器) - 把每個詞彙都當成一個分類,改為分類模型 (問題點:預測的詞至少要在訓練集有看過才行) - RNN 模型 - 雖可處理不同長度的詞彙,但要先抽取影像當中的特徵 (e.g., SIFT) ![](https://i.imgur.com/8QI9yFL.jpg) - CRNN 模型 (結合 DCNN + RNN) - 只需針對詞彙標註,不用標註當中的字元 - 不須進行影像預處理 (e.g., 字元定位、字元分割) - 長度無約束、只要對高度進行規一化 - 參數較過往的 DCNN 模型少很多,因此占用的儲存空間較少 - 網路架構 - 卷積層 (Convolutional Layers):提取影像特徵 - [Convolutional Layer、Pooling Layer、Fully Connected Layer 簡要回顧](https://medium.com/jameslearningnote/資料分析-機器學習-第5-1講-卷積神經網絡介紹-convolutional-neural-network-4f8249d65d4f) - [stride & padding 解釋](https://chih-sheng-huang821.medium.com/卷積神經網路-convolutional-neural-network-cnn-卷積計算中的步伐-stride-和填充-padding-94449e638e82) ![](https://i.imgur.com/QwjYNPG.png) - 迴圈層 (Recurrent Layers):預測每一幀的標籤分布 - 上下文資訊有助於訓練 - 透過反向傳播 (back propagation) 可以將誤差差值傳到卷積層,讓迴圈層和卷積層可以共同訓練 - 可以對任意長度的序列進行操作 - 轉錄層 (Transciption Layer):將每一幀的預測變為最終的標籤 - 無詞典轉錄 (lexicon-free) - 基於詞點的轉錄 (lexicon-based) ### Sequence Labeling (RNN 的建構) - [在這裏 RNN 扮演什麼角色?] 講 CNN 產生出來的 feature sequences 轉換成序列性的 label - input: feature maps - output: sequence of labels (e.g.) #### RNN 的特色 * 一個講 RNN 不錯的網站: [link](https://baubibi.medium.com/速記ai課程-深度學習入門-二-954b0e473d7f) * <font color='red'>**[優點]**</font> 使用 RNN 有三個優點 1. 擅長抓語意分析 * 基於圖片的背景資訊比符號資訊還要可靠 * 圖片的特徵可以讓模型分辨出模稜兩可的字,e.g. i & l 2. 能夠將誤差反向傳輸到上一層 (CNN layers),所以才可以 end-to-end 訓練 3. 能夠作用在不定長度的內容 * <font color='blue'>**[缺點]**</font> 因為 RNN 容易 gradient vanishing * 所以用 LSTM 比較會避免這個問題 #### 採用 LSTM * [bi-directional] 但是 LSTM 是有方向性的,他會參考,但也只會參考過去的預測資訊;不過在影像問題可以用**雙向** LSTM 而且是很有幫助的 * 影像資料有個特別之處是個小格 input data 出現的順序跟時間沒有關係,不會因為在分析第 5 格 feature 的時候看不到第 6 格的 feature (至少在現在的應用場景是這樣)。因此,這裡的 LSTM 可以再加一個反過來做的 LSTM;也就是參考比較前面的 feature 然後回推上一個字是甚麼。所以,兩個合起來就會是雙向的 LSTM * [deep] 而且,多個雙向 LSTM 是可以被疊再一起的,就會變成 <font color='red'>**deep bi-directional LSTM**</font>,深層的成效看起來比較好 (從語音的例子是這樣顯示的) * [SPEECH RECOGNITION WITH DEEP RECURRENT NEURAL NETWORKS ](https://arxiv.org/pdf/1303.5778.pdf) #### back-propagation in RNN case * [BPTT] 在 RNN 的例子,要計算誤差時一樣是使用反向傳播法。因為 RNN 本來就是一層一層傳遞,因此計算反向誤差時方向就會是正向計算反向。又因為 RNN 的不同層就好比在不同時間發生,所以這時的反向傳播也常稱為 "back-propagation through time" **(時間性的反向傳播)** * [Map-to-Sequence] 實際上,會製造一個叫做 Map-to-Sequence 的 神經網路層 ,當作 RNN 與 CNN 的橋樑,目的是將 RNN 的 feature sequences 變成 feature map 傳給 CNN #### RNN 先前筆記區 - RNN 變形 (<font color='red'>RNN & LSTM & 雙向 LSTM & 深度雙向 LSTM 在應用上的差異?<<待釐清>></font>) - RNN - LSTM - 雙向LSTM - 在影象的序列中,兩個方向的上下文是相互有用且互補的 - 深雙向LSTM - 在語音識別任務中取得了顯著的效能改進 ### Transcription Layer (轉錄層) - 目的:將 RNN 預測出來的值轉換成 label sequence - 數學上的意思就是找到條件機率最大的那個 label sequence,已知 RNN 預測出來的值 - 實務上這邊有兩種模式 - lexicon-free transcription: 預測值不基於任何辭典 (直接組起來?) - lexicon-based transcription: 從 lexicon 裡面挑出機率最高的那一個 (有點像分類問題?) - 什麼是 lexicon? - [answer] 就是辭典,一個收集很多詞彙的集合 - [e.g.] ```python lexicon = { 'hello world', 'good morning', 'I feel very hungry', ... } ``` #### 2.3.1 Probability of label sequence * CTC Layer * 當我們使用 negative log-likelihood 作為訓練目標的誤差函數,我們只需要圖片的 label sequence 以及 image data 本身,不需要針對上面一個一個字去人工標註 * 怎麼去對應? * 一個 $\mathcal{B}$ 函數,會做兩件事情 1. 消除重複的 label 2. 把空白去除 * [e.g. ] $\mathcal{B}$("--hh-e-l-ll-oo--") = "hello",其中 "-" 為空白的意思 * 想辦法調整參數讓下面這個條件機率最大化 * $p(\textbf{l}|\textbf{y}) = \sum\limits_{\pi: \mathcal{B}(\pi)=1}\,p(\pi|\textbf{y})$ --> Eq. 1 * $\textbf{l}$ = "hello" * $\pi$ 就是經過消除重複、刪除空白後會變成 "hello" 的原始 label sequence,例如 "--hh-e-l-ll-oo--", "--h--e-l-ll-ooo--", ... * $\textbf{y}$ 是一個長度為 $T$ 的向量,每一個分量都是一個機率分佈 * $\textbf{y} = (y_1, y_2, ..., y_T)$ * $p(\pi|y) = \prod\limits_{t=1}^T\,y_{\pi_t}^t$, 其中 $y_{\pi_t}^t$ 是在時間 $t$ 被預測為 $\pi_t$ 的機率 * 在我們的例子,時間 $t$ 意思是第 $t$ 張小格子圖 * $\pi_t$ 就是第 $t$ 個小格子圖被預測的值 * 最後利用 CTC 來最大化 Eq. 1 #### 2.3.2 Lexicon-free transcription * 因為沒有 lexicon,所以就是找每個條件機率所對應到的 $\pi_t$,然後再把這些字組起來,也就是這個算式: $l^* \approx \mathcal{B}({argmax}_\pi\, p(\pi|y))$ #### 2.3.3 Lexicon-based transcription * 因為有 lexicon,所以會用這種算式去極大化 $l^* \approx \mathcal{B}(arg\max\limits_{\textbf{l}\in\mathcal{D}}p(\textbf{l}|\textbf{y}))$ * 其中 $\mathcal{D}$ 就是辭典 (lexicon) * 但是這樣子找 $\textbf{l}^*$ 是非常耗時的,所以會採取 lexicon-free 的方式先找到最一個 label ($\textbf{l}'$) ,然後再用 edit distance metric 去算一個集合,然後再去找這附近最像的 * $l^*=arg\max\limits_{\textbf{l}\in\mathcal{N}_{\delta}(\textbf{l}')}p(\textbf{l}|\textbf{y})$ * 這個集合 $\mathcal{N}_{\delta}(\textbf{l}')$ 就是在找候選字的意思 * 說用 B-K tree 可以找很快,運算複雜度是 $\mathcal{O}(log(|\mathcal{D}|))$ * 然後候選字跟正確答案的字,距離是 edit distance * 新增 * 替換 * 刪除 * [e.g. ] $ED("123", "124") = 1$, $ED("123", "142" = 2)$ ### 整個模型的 Loss 要怎麼計算? * 誤差函數:$\mathcal{O} = -\sum\limits_{I_i, \mathcal{\mathcal{l}_i}\in \chi}log(P(\mathcal{l}_i, \textbf{y}_i))$ $\to$ 想辦法最小化這個值 ($\mathcal{O}$) * 訓練資料在數學上我們這樣表示: $\chi = {(I_i, \mathcal{\mathcal{l}_i})}$,其中 $I_i$ 是影像資料的意思, $\mathcal{l}_i$ 是那個影像的正確答案 * end-to-end * SGD * back-propagation * 用 ADADELTA 用調整不同維度的 learning-rate (這個意思是每個參數的更新速度不同嗎?) * CTC 計算方式:[Connectionist temporal classification: labelling unseg- mented sequence data with recurrent neural networks.](https://www.cs.toronto.edu/~graves/icml_2006.pdf) - 實驗 - Pooling 採用 1×2 大小的矩形池化視窗而不是傳統的正方形,有助於識別一些具有窄形狀的字元,例如i和l - 在 convolutional layer 後加入 batch normalization layer 可讓訓練過程大大加快 ## [code] [Belval/CRNN](https://github.com/Belval/CRNN) ### 程式大致架構 ![](https://i.imgur.com/OCQVh7T.png) ### data_manager.py #### def load_data(): ```python= def load_data(self): """Load all the images in the folder """ print("Loading data") examples = [] count = 0 skipped = 0 for f in os.listdir(self.examples_path): ### ---------------------------------- # 圖檔的 label 是直接寫在檔名上 # 後面帶隨機碼 (e.g., $1,000_a3hf.jpg) # 這樣的做法會有一些限制 # 像是 110/03/09 就沒辦法在檔名上呈現,因為檔名不支援 / ### ---------------------------------- # label 長度會根據 cnn 的 output 而定 ### ---------------------------------- if len(f.split("_")[0]) > self.max_char_count: continue ### ---------------------------------- # resize_image 寫法詳見下方說明 ### ---------------------------------- arr, initial_len = resize_image( imread(os.path.join(self.examples_path, f), mode="L"), self.max_image_width, ) examples.append( ( arr, f.split("_")[0], label_to_array(f.split("_")[0], self.char_vector), ) ) count += 1 return examples, len(examples) ``` #### def resize_image(): (utils.py) 影像在丟進CRNN前,皆會轉為 32(高) x W(寬) 【在此用 W=200 示意】 - 如果影像的寬大於 200,會直接變形為 32 x 200 - 示意圖 ![](https://i.imgur.com/WRjHBql.png) - 範例圖 ![](https://i.imgur.com/qwtmyVQ.png) - 如果影像的高小於 32,會先等比例放大至高等於 32。接著若影像的寬大於 200,就會將圖截斷;反之,會將影像的寬補足至200 (補黑底) - 示意圖 ![](https://i.imgur.com/CAaPCqd.png) - 範例圖 ![](https://i.imgur.com/kA2FZY4.png) ---------------------------------------- ![](https://i.imgur.com/mM5MRLx.png) #### def label_to_array(): (utils.py) ```python= # 將各字元轉成對應的 index def label_to_array(label, char_vector): try: return [char_vector.index(x) for x in label] except Exception as ex: print(label) raise ex ``` #### def generate_all_train_batches(): ```python= def generate_all_train_batches(self): train_batches = [] while not self.current_train_offset + self.batch_size > self.test_offset: old_offset = self.current_train_offset new_offset = self.current_train_offset + self.batch_size self.current_train_offset = new_offset # 根據 batch_size 產出 batch data raw_batch_x, raw_batch_y, raw_batch_la = zip( *self.data[old_offset:new_offset] ) batch_y = np.reshape(np.array(raw_batch_y), (-1)) ### ---------------------------------- # 這邊不需要攤平 # 錯誤寫法: # batch_dt = sparse_tuple_from(np.reshape(np.array(raw_batch_la), (-1))) # 直接將各 label 對應的 index list 餵進 sparse_tuple_from() 即可 # batch_dt 主要會產出 3 個東西: # 1. 每個字元是在第幾張圖中的第幾個位置 # 2. 每個字元是在第幾張圖中,還有在 CHAR_VECTOR 中的 index # 3. (batch_size, batch data中最長的字元數) batch_dt = sparse_tuple_from(raw_batch_la) raw_batch_x = np.swapaxes(raw_batch_x, 1, 2) raw_batch_x = raw_batch_x / 255.0 batch_x = np.reshape( np.array(raw_batch_x), (len(raw_batch_x), self.max_image_width, 32, 1) ) train_batches.append((batch_y, batch_dt, batch_x)) return train_batches ``` ### crnn.py * tensorflow 精神:先畫圖,再實際跑資料 * 先畫圖:在 python 上產生一堆變數,這些變數都是神經網路的框架,在 python 裡面就是 Tensor (這個時候 data 還沒進來喔!) * 實際跑 * 定義 init 然後叫他 run ```python= session = tf.Session() with session.as_default(): output, input, init = nn(a, b, c)# 等一下要跑的網路架構 init.run() ``` * 初始化: tf.Session().as_default() * 餵入資料開始在網路架構執行 ```python= real_cnn_output = session.run([cnn_output], feed_dict={ inputs: train_batches[0][2] } ) ``` #### 看 code 可能會有的問題 * 到底我這樣 train 最多會預測多長的字元? * A: 在這個 code 的例子會是 seq_len = (max_image_width $\div$ 4) $- 1$,原因是 CNN 的網路結構讓最後的 feature map 的形狀是這樣;如果是不同的疊法可能會產生不同的 shape,那麼最大字元數也會不同 * $\div$ <font color='red'>**4**</font> 的意義是,原始圖片的可辨識最小字元至少要 4 個 pixels;再小的話就無法被訓練,當然也無法預測 * CNN 層如何 output 給 RNN 層? * 就是 CNN output 的 feature maps 轉換成 RNN 要 input 的形狀,方式不拘。在我們的例子是 tf.Squeeze,效果如下 ```python= cnn_output #<tf.Tensor 'conv2d_104/Relu:0' shape=(?, 49, 1, 512) dtype=float32> rnn_input = tf.squeeze(cnn_output, [2]) ''' 把位置 2 的 1 給壓縮掉 ''' rnn_input #<tf.Tensor 'Squeeze_2:0' shape=(?, 49, 512) dtype=float32> ``` #### 常見的 tensorflow 指令 ##### 1. 神經元 * tf.placeholder (預設 Tensor 的形狀,給 input data 用的) * tf.Variable (裡面的值會在過程中被更新) * tf.constant (常數項) * tf.nn.ctc_loss (loss 也是神經元之一喔!因為他要被更新) * tf.nn.ctc_beam_search_decoder ##### 2. 網路架構運算法 * tf.layers.conv2d * tf.layers.max_pooling2d * tf.nn.relu * tf.layers.batch_normalization * tf.nn.rnn_cell.BasicLSTMCell * tf.nn.bidirectional_dynamic_rnn ##### 3. 看起好像很可怕,其實就是 numpy 的指令 * tf.matmul * tf.add * tf.concat * tf.reshape * tf.reduce_mean * tf.transpose * tf.Squeeze * ...... ##### 4. 運算指令 * tf.global_variables_initializer * tf.Session * tf.Session().as_default() * tf.Session().run() * tf.train * tf.train.Saver * tf.train.AdamOptimizer ## 提問區 |姓名|問題|解答| | - | - |-| | 昊中 | CRNN模型的input data是不是無法直接輸入多行文字影像?得先手動將文字切成一列一列的?|對,每個 frame 只會預測一個字元,所以實務上沒辦法預測多行文字的影像,要先切好| | 沛筠 | 目前Map-to-Sequence的方式除了Squeeze之外,還有其他的嗎? |其實是有的,只是在這裡的用法是直接合併然後將資料餵給 RNN,當然也可以在 CNN output 之後做一個矩陣相乘然後再給 RNN ;只是還是要符合某些形狀。e.g. 在我們的例子是 24 x 1 x 512 (tf.Squeeze)$\to$ 24 x 512 $\to$ RNN,也可以嘗試這樣 24 x 1 x 512 (tf.matmul)$\to$ 24 x 1024 $\to$ RNN| | Track 2 | 1. 論文裡的 Figure 2. 說到 feature sequence 的每一個 vector 都對應到圖片的其中一區的感受視野 (receptive field),好奇為什麼是一個 feature sequence 對應某幾個 columns 而不是全部的圖 (過程中每個 kernel 不都會掃過整個 feature maps 嗎?)<br>2. 想要再聽一次宜昌版本的 CTC loss 解說,beam search 是什麼?|1. 詳見附錄1<br>2. 宜昌老師說請先看這篇: [answer](https://www.ycc.idv.tw/crnn-ctc.html)(昱睿)| | 信賢 | 1.(講者準備的資料)想詢問resize若有補黑底,不會造成訓練不準嗎?<br>2.(第8頁table 4.)是說CRNN對於真實的圖像反而表現比較好?但清楚的影像反而比較差@@ 不太能理解 ![](https://i.imgur.com/p47KyHD.png)|1. 黑底可以把它想成和白底一樣,都是無意義的背景,所以在萃取feature的時候,黑底的部分就不會是重要資訊<br>2. paper 中並無針對三種資料的訓練集和測試集多做描述,但若以實務來看,乾淨的樂譜的準確度應該要較高才對| |軒彤|我現場版|| |倚任副理|1. SIFT 的 output 怎麼變成 sequence 餵進 RNN?|| - 附錄1 ![](https://i.imgur.com/VQYQ9mC.jpg) ![](https://i.imgur.com/9urAh6s.jpg) - 補充 - CRNN 可以處理任意長度的序列,此外不需要字元分割、scaling 以及 normalization (CRNN中切小圖的CNN的寬是固定的還是會根據整張圖的寬而切大小的小圖?) - scaling (min-max scaling) [(詳細說明)](https://kharshit.github.io/blog/2018/03/23/scaling-vs-normalization) $$ x' = \frac{x-xmin}{xmax-xmin} $$ - normalizationl (符合常態分布) $$ x' = \frac{x-xmean}{xmax-xmin} $$