2.2 RNN入門

0. 寫在前面

前兩天看到一篇中文文章,整篇文章各種英文口頭語比如「trick」,給讀者造成一個非常大的誤解就是這個貨很牛逼啊。一般來說用英文來掩蓋詞窮是中很聰明的策略,就像初學者看了他的文章依然什麼都不懂。

1. 文本變為向量

實際上我們用RNN經常需要其做的就是自然語言處理,這裡有個大坑,就是文本變為向量的過程。整個自然語言處理在初學者看來都不是那麼有好。有興趣的可以看看TfIdf的文本向量化的方式。這是統計一篇文章中的詞頻,再乘以一個判別係數。我們可以不用那麼麻煩就是建立所有詞的一個表,文章出現那個詞就在這個表相應的詞的位置統計這篇文章詞的詞頻。當然一篇文章不可能出現所有的詞,因此文本向量化是一個稀疏矩陣,這種稀疏性就帶來了處理的難題。我們可以看到,假如所有文章不同詞的個數為20000個,有10000篇文章,那麼這個稀疏矩陣的維度就是20000*10000,20000個詞很多嗎?一點都不多,做NLP這個量級只能算正常。這個維度的矩陣處理可以用貝葉斯演算法,也可以用LDA降個維後處理,但是想用PCA降維你可以試試內存和時間消耗。當然這個代碼sklearn裡面有,直接看文檔就行。這個處理是「詞」級別的。

上面所說的,是傳統的文本處理方法,是沒有前後文的。RNN的一個優勢就是可以順序的輸入文本進行處理。在RNN的處理中我們可以藉助於神經網路強大的表達能力放棄「詞」級別這種粗大的處理方法(可以的,word2vect這個就是處理文本降維的),而採用「字」級別的處理。這種處理方式就比較方便了。最主要的文本向量比較小。比如英文來說處理級別為「字母」級別我們的字典長度為26+2+1,這裡26代表26個字母,2代表空格和句號,1代表blank,也就是空白什麼都沒有。對於中文文本來說字典長度為3500個,這是國家推薦的常用字個數。

3500是什麼意思呢?神經網路是無法處理整形數字的,也就是說我們可以將漢字排排隊從0編碼到3499,但是這是整型我們無法用神經來預測,於是可以用3500個神經元來表示,這3500個神經元的輸出做一個分析,第幾個神經元輸出最大就是那個整型數字。3500對於神經網路來說太大了,參數數量會爆炸,所以在輸入核心處理RNN或CNN的過程中需要經歷的過程就是embedding,這是什麼意思呢?我們將一個漢字可以編碼的形式為3500長度的向量,這個向量的編碼形式可以是onehot的形式(是第幾個漢字向量相應位置就為1,其他位置為0),將這個向量乘以一個降維矩陣,比如一個3500*256維度的矩陣W1,可以看成是一個全鏈接網路,這樣就可以將3500長度變為256長度,所以說這是一個降維的過程。當然也可以乘以一個矩陣W2維度256*3500的矩陣,將神經網路輸出的向量還原為漢字編碼維度用來預測相應字的概率。這個W1和W2都是神經網路可訓練的量。也就是TensorFlow中embedding需要輸入的那個矩陣:

# W為降維矩陣nW = tf.get_variable("embedding", [len(words)+1, rnn_size])n# input_data 為用整型數字表示的句子ninputs = tf.nn.embedding_lookup(W, input_data) n

2. RNN入門

目前來看,實現前饋神經網路是輕鬆寫意加自然的,然而事情到了RNN就並非那麼簡單了。第一API變得非常複雜,第二API變得更加複雜。所以說很多人在學到這裡的時候都會遇到很大的問題。那麼就從最簡單的RNN開始扯吧。

我們都知道用於RNN訓練的是BPTT演算法,這個演算法我們放到數學章節在講,但是這裡的意思是RNN其實也是一個改進的全鏈接網路,但是由於時間步的提出使得訓練過程鏈式求導過程更加麻煩,或者說很長。這個很長帶來一個問題就是難以訓練,非常難以訓練,於是各種名詞就來了,比如minibatch等小技巧。RNN的模型其實看起來更像是一個隱式馬爾科夫鏈,與概率相關的東西看起來都比較麻煩,很多論文都是這麼寫的,比如我們那篇關於CTC非常著名的論文就是利用概率來解釋的,但是神經網路輸出一直都沒有真正的概率一說,其輸出是固定的類似於函數那樣。所謂概率我們通常是利用sigmiod的激活函數來展示0-1之間的輸出當做概率的,或者加入一個softmax當做概率。解釋完這個問題我們來看一下rnn的輸入和輸出:

輸入:

[x_1 ,x_2, cdots , x_T]

輸出:

[y_1, y_2, cdots ,y_T]

注意這裡的輸入和輸出是嚴格的一一對應的,也就說我們輸入一個x就有一個輸出h:

y_t = f(x_t) (2-1)

寫成函數的形式更加合適這種函數表達就需要我們有個輸入就需要一個輸出,並不存在有了一個輸入並沒有輸出的情況存在。有些人說輸出可以是任意長度,這顯然是被某度給荼毒了。實際上有些情況輸出的是空白(文本向量定義的那個blank),這個空白也是我們自己定義的,但是你並不能說沒有輸出。

注意2-1式用到的那個函數,這個函數並沒有什麼不對,但是我們假設其更簡單的形式:

y_t = g(h_t) h_t = f(x_t,h_{t-1}) (2-2)

也就是 h_t 只跟此時的輸入和上一個時刻的輸出有關,這裡h稱之狀態向量,如何模擬這個函數:

cell = tf.nn.rnn_cell.BasicLSTMCell(rnn_size, state_is_tuple=True) n

cell就是我們定義的函數f和g的集合,當然我們可以將h繼續輸入下一層RNN網路之中,形成多層神經網路,這裡TensorFlow也是封裝好了函數:

cell = tf.nn.rnn_cell.MultiRNNCell([cell] * num_layers, state_is_tuple=True) n

cell依然是我們的函數。

其輸入有兩個第一個就是狀態向量,第二個就是第t時刻的輸出:

state = cell.zero_state(batch_size, tf.float64)noutputs = []nfor time_step in range(10):n #新版本的TensorFlow之中已經不用定義變數復用了n if time_step > 0: tf.get_variable_scope().reuse_variables()n (cell_output, state) = cell(indata[:, time_step, :], state)n outputs.append(cell_output)n

可以看到,對於函數輸入為indata,其是一個三維的向量,第一個維度代表batchsize,第二個維度是時間步,第三個維度就是我們embedding降維後的特徵(time_major=False),另外一個就是這個時刻的狀態向量h,這個狀態是要循環進行輸入的,函數返回值有兩個,第一個就是y輸出,第二個就是本時刻的狀態。本次例子將狀態定義為0,但是其他的一些模型中這個狀態向量還有其他作用。對了這個過程可以用一個函數將每個時間步展開:

outputs, last_state = tf.nn.dynamic_rnn(cell, indata, n initial_state=state, time_major=False)n

最後的outputs就是所輸出的一系列y。

3. 第一個例子詩句生成

學習重點:

  1. 生成任意長度文本是咋回事
  2. 概率從何而來

這個例子算是RNN最適合入門的一個實例,這裡來看一看是如何搭建文本的:

首先我們需要一個字典,將出現的詞進行一個編號,這算是向量化的第一步。

# 統計每個字出現次數 nall_words = [] nfor centence in articles: n all_words += [word for word in centence] n#統計詞頻ncounter = collections.Counter(all_words) ncount_pairs = sorted(counter.items(), key=lambda x: -x[1]) nwords, _ = zip(*count_pairs) n# 每個字映射為一個數字ID nword_num_map = dict(zip(words, range(len(words)))) n

這是一個建立字元字典的過程,英文文本的那個太簡單,所以這裡用中文來說明,因為中文字元較多,所以做了個排序以剔除一些不常用的詞。

對於一個詩句來說,其向量化後可能就是這種形式:

[314, 3199, 367, 1556, 26, 179, 680, 0, 3199, 41, 506, 40, 151, 4, 98, 1]

注意這裡有個0和1分別代表逗號和句號,這是需要輸入RNN模型之中的,由於rnn的目的是要根據前面幾個詞預測接下來可能輸出的字是什麼因此,需要對數據進行如下的處理:

x=[314, 3199, 367, 1556, 26, 179, 680, 0, 3199, 41, 506, 40, 151, 4, 98]

d=[3199, 367, 1556, 26, 179, 680, 0, 3199, 41, 506, 40, 151, 4, 98, 1]

注意輸入樣本對為(x, d),x是輸入,標籤是d,我們的目的是預測下一個字,因此x與d之間只相差一個字。雖然很多公式中都喜歡用展開的形式表達RNN,但是我們要知道,每一個時間步所對應的輸入和輸出都是用的同一套權值。所以這種方式來安排樣本是合理的。

那麼接下來我們需要做的,就是對x的輸出y與d進行損失函數的計算,這個損失函數為序列loss:

loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example([logits], [d], [tf.ones_like(targets, dtype=tf.float32)], len(words)) nloss = tf.reduce_mean(loss) n

上面說到我們在計算的過程中是無法直接處理整型數字的,但是我們輸入的標籤d就是整型變數,所以這裡我們就可以理解TensorFlow幫助我們完成了多少後台的工作。注意這裡的logits是對於神經網路的輸出加入了一個全鏈接層,其目的是將RNN網路的輸出升維為字典的長度用以預測某個單詞的概率,這是一個embedding的反過程,但是這裡沒有用到embedding權值矩陣的逆。

outputs, last_state = tf.nn.dynamic_rnn(cell, x, initial_state=initial_state, scope=rnnlm) ny = tf.reshape(outputs,[-1, rnn_size]) n#這個就是上面說的乘以一個全鏈接層 nlogits = tf.matmul(output, softmax_w) + softmax_b n

整個網路的搭建十分簡單。下面來看一個看如何的獲取一句話:

首先我們需要的是給定整個句子的第一個字,比如「人」,接下來將人這個字連同初始狀態輸入。那麼輸出是關於3500個詞的概率,我們可以根據這個概率來選擇下一個詞是什麼。同時輸出的還有狀態向量。那麼接下來我們將通過概率所選擇的這個詞在輸入RNN之中,連同輸入的還有上面獲取的那個狀態。如此又出現了第二個字。

循環往複,我們就獲取了一句完整的輸出。注意此時句號的作用就體現出來的,我們在循環的過程中如果不制止會一直輸出一個無限長的詩句。但是這裡我們遇到句號就終止,由此就獲得了一個一定長度的詩句。來看下寫的藏頭詩:

好的,這篇文章先到這裡,下一個篇中聊一聊RNN的模型與CNN模型在深度方向上進行的改進,比如ResNet比如Attention。當然還有ctc-loss。


推薦閱讀:

2.1 TensorFlow實踐-入門與數字識別示例解析(1)
MobileNet教程(2):用TensorFlow做一個安卓圖像分類App
TensorFlow 聊天機器人開源項目評測第一期:DeepQA
cs20si: tensorflow for research 學習筆記1
Quo Vadis, Action Recognition? A New Model and the Kinetics

TAG:TensorFlow | 神经网络 | RNN |