零基礎入門深度學習(7) - 遞歸神經網絡

往期回顧

在前面的文章中,咱們介紹了循環神經網絡,它能夠用來處理包含序列結構的信息。然而,除此以外,信息每每還存在着諸如樹結構、圖結構等更復雜的結構。對於這種複雜的結構,循環神經網絡就無能爲力了。本文介紹一種更爲強大、複雜的神經網絡:遞歸神經網絡 (Recursive Neural Network, RNN),以及它的訓練算法BPTS (Back Propagation Through Structure)。顧名思義,遞歸神經網絡(巧合的是,它的縮寫和循環神經網絡同樣,也是RNN)能夠處理諸如樹、圖這樣的遞歸結構。在文章的最後,咱們將實現一個遞歸神經網絡,並介紹它的幾個應用場景。node

 

遞歸神經網絡是啥

由於神經網絡的輸入層單元個數是固定的,所以必須用循環或者遞歸的方式來處理長度可變的輸入。循環神經網絡實現了前者,經過將長度不定的輸入分割爲等長度的小塊,而後再依次的輸入到網絡中,從而實現了神經網絡對變長輸入的處理。一個典型的例子是,當咱們處理一句話的時候,咱們能夠把一句話看做是詞組成的序列,而後,每次向循環神經網絡輸入一個詞,如此循環直至整句話輸入完畢,循環神經網絡將產生對應的輸出。如此,咱們就能處理任意長度的句子了。入下圖所示:python

然而,有時候把句子看作是詞的序列是不夠的,好比下面這句話『兩個外語學院的學生』:git

上圖顯示了這句話的兩個不一樣的語法解析樹。能夠看出來這句話有歧義,不一樣的語法解析樹則對應了不一樣的意思。一個是『兩個外語學院的/學生』,也就是學生可能有許多,但他們來自於兩所外語學校;另外一個是『兩個/外語學院的學生』,也就是隻有兩個學生,他們是外語學院的。爲了可以讓模型區分出兩個不一樣的意思,咱們的模型必須可以按照樹結構去處理信息,而不是序列,這就是遞歸神經網絡的做用。當面對按照樹/圖結構處理信息更有效的任務時,遞歸神經網絡一般都會得到不錯的結果。github

遞歸神經網絡能夠把一個樹/圖結構信息編碼爲一個向量,也就是把信息映射到一個語義向量空間中。這個語義向量空間知足某類性質,好比語義類似的向量距離更近。也就是說,若是兩句話(儘管內容不一樣)它的意思是類似的,那麼把它們分別編碼後的兩個向量的距離也相近;反之,若是兩句話的意思大相徑庭,那麼編碼後向量的距離則很遠。以下圖所示:算法

從上圖咱們能夠看到,遞歸神經網絡將全部的詞、句都映射到一個2維向量空間中。句子『the country of my birth』和句子『the place where I was born』的意思是很是接近的,因此表示它們的兩個向量在向量空間中的距離很近。另外兩個詞『Germany』和『France』由於表示的都是地點,它們的向量與上面兩句話的向量的距離,就比另外兩個表示時間的詞『Monday』和『Tuesday』的向量的距離近得多。這樣,經過向量的距離,就獲得了一種語義的表示。數組

上圖還顯示了天然語言可組合的性質:詞能夠組成句、句能夠組成段落、段落能夠組成篇章,而更高層的語義取決於底層的語義以及它們的組合方式。遞歸神經網絡是一種表示學習,它能夠將詞、句、段、篇按照他們的語義映射到同一個向量空間中,也就是把可組合(樹/圖結構)的信息表示爲一個個有意義的向量。好比上面這個例子,遞歸神經網絡把句子"the country of my birth"表示爲二維向量[1,5]。有了這個『編碼器』以後,咱們就能夠以這些有意義的向量爲基礎去完成更高級的任務(好比情感分析等)。以下圖所示,遞歸神經網絡在作情感分析時,能夠比較好的處理否認句,這是賽過其餘一些模型的:網絡

在上圖中,藍色表示正面評價,紅色表示負面評價。每一個節點是一個向量,這個向量表達了以它爲根的子樹的情感評價。好比"intelligent humor"是正面評價,而"care about cleverness wit or any other kind of intelligent humor"是中性評價。咱們能夠看到,模型可以正確的處理doesn't的含義,將正面評價轉變爲負面評價。dom

儘管遞歸神經網絡具備更爲強大的表示能力,可是在實際應用中並不太流行。其中一個主要緣由是,遞歸神經網絡的輸入是樹/圖結構,而這種結構須要花費不少人工去標註。想象一下,若是咱們用循環神經網絡處理句子,那麼咱們能夠直接把句子做爲輸入。然而,若是咱們用遞歸神經網絡處理句子,咱們就必須把每一個句子標註爲語法解析樹的形式,這無疑要花費很是大的精力。不少時候,相對於遞歸神經網絡可以帶來的性能提高,這個投入是不太划算的。python2.7

咱們已經基本瞭解了遞歸神經網絡是作什麼用的,接下來,咱們將探討它的算法細節。ide

 

遞歸神經網絡的前向計算

接下來,咱們詳細介紹一下遞歸神經網絡是如何處理樹/圖結構的信息的。在這裏,咱們以處理樹型信息爲例進行介紹。

遞歸神經網絡的輸入是兩個子節點(也能夠是多個),輸出就是將這兩個子節點編碼後產生的父節點,父節點的維度和每一個子節點是相同的。以下圖所示:

分別是表示兩個子節點的向量,是表示父節點的向量。子節點和父節點組成一個全鏈接神經網絡,也就是子節點的每一個神經元都和父節點的每一個神經元兩兩相連。咱們用矩陣表示這些鏈接上的權重,它的維度將是,其中,表示每一個節點的維度。父節點的計算公式能夠寫成:

 

 

 

在上式中,tanh是激活函數(固然也能夠用其它的激活函數),是偏置項,它也是一個維度爲的向量。若是讀過前面的文章,相信你們已經很是熟悉這些計算了,在此不作過多的解釋了。

而後,咱們把產生的父節點的向量和其餘子節點的向量再次做爲網絡的輸入,再次產生它們的父節點。如此遞歸下去,直至整棵樹處理完畢。最終,咱們將獲得根節點的向量,咱們能夠認爲它是對整棵樹的表示,這樣咱們就實現了把樹映射爲一個向量。在下圖中,咱們使用遞歸神經網絡處理一棵樹,最終獲得的向量,就是對整棵樹的表示:

舉個例子,咱們使用遞歸神將網絡將『兩個外語學校的學生』映射爲一個向量,以下圖所示:

最後獲得的向量就是對整個句子『兩個外語學校的學生』的表示。因爲整個結構是遞歸的,不只僅是根節點,事實上每一個節點都是以其爲根的子樹的表示。好比,在左邊的這棵樹中,向量是短語『外語學院的學生』的表示,而向量是短語『外語學院的』的表示。

式1就是遞歸神經網絡的前向計算算法。它和全鏈接神經網絡的計算沒有什麼區別,只是在輸入的過程當中須要根據輸入的樹結構依次輸入每一個子節點。

須要特別注意的是,遞歸神經網絡的權重和偏置項在全部的節點都是共享的。

 

遞歸神經網絡的訓練

遞歸神經網絡的訓練算法和循環神經網絡相似,二者不一樣之處在於,前者須要將殘差從根節點反向傳播到各個子節點,然後者是將殘差從當前時刻反向傳播到初始時刻

下面,咱們介紹適用於遞歸神經網絡的訓練算法,也就是BPTS算法。

 

偏差項的傳遞

首先,咱們先推導將偏差從父節點傳遞到子節點的公式,以下圖:

定義爲偏差函數E相對於父節點的加權輸入的導數,即:

 

 

 

 

是父節點的加權輸入,則

 

在上述式子裏,都是向量,而是矩陣。爲了看清楚它們的關係,咱們將其展開:

 

 

 

 

在上面的公式中,表示父節點p的第i個份量;表示子節點的第i個份量;表示子節點的第i個份量;表示子節點的第k個份量到父節點p的第i個份量的的權重。根據上面展開後的矩陣乘法形式,咱們不難看出,對於子節點來講,它會影響父節點全部的份量。所以,咱們求偏差函數E對的導數時,必須用到全導數公式,也就是:

 

 

 

 

有了上式,咱們就能夠把它表示爲矩陣形式,從而獲得一個向量化表達:

 

 

 

 

其中,矩陣是從矩陣W中提取部分元素組成的矩陣。其單元爲:

 

 

 

 

上式看上去可能會讓人暈菜,從下圖,咱們能夠直觀的看到究竟是啥。首先咱們把W矩陣拆分爲兩個矩陣,以下圖所示:

顯然,子矩陣分別對應子節點的到父節點權重。則矩陣爲:

 

 

 

 

也就是說,將偏差項反向傳遞到相應子節點的矩陣就是其對應權重矩陣的轉置。

如今,咱們設是子節點的加權輸入,是子節點c的激活函數,則:

 

 

 

 

這樣,咱們獲得:

 

 

 

 

若是咱們將不一樣子節點對應的偏差項鏈接成一個向量。那麼,上式能夠寫成:

 

 

 

式2就是將偏差項從父節點傳遞到其子節點的公式。注意,上式中的也是將兩個子節點的加權輸入連在一塊兒的向量。

有了傳遞一層的公式,咱們就不難寫出逐層傳遞的公式。

上圖是在樹型結構中反向傳遞偏差項的全景圖,反覆應用式2,在已知的狀況下,咱們不難算出爲:

 

 

 

 

在上面的公式中,表示取向量屬於節點p的部分。

 

權重梯度的計算

根據加權輸入的計算公式:

 

 

 

 

其中,表示第l層的父節點的加權輸入,表示第l層的子節點。是權重矩陣,是偏置項。將其展開可得:

 

 

 

 

那麼,咱們能夠求得偏差函數在第l層對權重的梯度爲:

 

 

 

 

上式是針對一個權重項的公式,如今須要把它擴展爲對全部的權重項的公式。咱們能夠把上式寫成矩陣的形式(在下面的公式中,m=2n):

 

 

 

式3就是第l層權重項的梯度計算公式。咱們知道,因爲權重是在全部層共享的,因此和循環神經網絡同樣,遞歸神經網絡的最終的權重梯度是各個層權重梯度之和。即:

 

 

 

由於循環神經網絡的證實過程已經在零基礎入門深度學習(4) - 卷積神經網絡一文中給出,所以,遞歸神經網絡『爲何最終梯度是各層梯度之和』的證實就留給讀者自行完成啦。

接下來,咱們求偏置項的梯度計算公式。先計算偏差函數對第l層偏置項的梯度:

 

 

 

 

把上式擴展爲矩陣的形式:

 

 

 

式5是第l層偏置項的梯度,那麼最終的偏置項梯度是各個層偏置項梯度之和,即:

 

 

 

 

權重更新

若是使用梯度降低優化算法,那麼權重更新公式爲:

 

 

 

 

其中,是學習速率常數。把式4帶入到上式,便可完成權重的更新。同理,偏置項的更新公式爲:

 

 

 

 

把式6帶入到上式,便可完成偏置項的更新。

這就是遞歸神經網絡的訓練算法BPTS。因爲咱們有了前面幾篇文章的基礎,相信讀者們理解BPTS算法也會比較容易。

 

遞歸神經網絡的實現

完整代碼請參考GitHub: https://github.com/hanbt/learn_dl/blob/master/recursive.py (python2.7)

如今,咱們實現一個處理樹型結構的遞歸神經網絡。

在文件的開頭,加入以下代碼:

 
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. import numpy as np
  4. from cnn import IdentityActivator

上述四行代碼很是簡單,沒有什麼須要解釋的。IdentityActivator激活函數是在咱們介紹卷積神經網絡時寫的,如今引用一下它。

咱們首先定義一個樹節點結構,這樣,咱們就能夠用它保存卷積神經網絡生成的整棵樹:

 
  1. class TreeNode(object):
  2. def __init__(self, data, children=[], children_data=[]):
  3. self.parent = None
  4. self.children = children
  5. self.children_data = children_data
  6. self.data = data
  7. for child in children:
  8. child.parent = self

接下來,咱們把遞歸神經網絡的實現代碼都放在RecursiveLayer類中,下面是這個類的構造函數:

 
  1. # 遞歸神經網絡實現
  2. class RecursiveLayer(object):
  3. def __init__(self, node_width, child_count,
  4. activator, learning_rate):
  5. '''
  6. 遞歸神經網絡構造函數
  7. node_width: 表示每一個節點的向量的維度
  8. child_count: 每一個父節點有幾個子節點
  9. activator: 激活函數對象
  10. learning_rate: 梯度降低算法學習率
  11. '''
  12. self.node_width = node_width
  13. self.child_count = child_count
  14. self.activator = activator
  15. self.learning_rate = learning_rate
  16. # 權重數組W
  17. self.W = np.random.uniform(-1e-4, 1e-4,
  18. (node_width, node_width * child_count))
  19. # 偏置項b
  20. self.b = np.zeros((node_width, 1))
  21. # 遞歸神經網絡生成的樹的根節點
  22. self.root = None

下面是前向計算的實現:

 
  1. def forward(self, *children):
  2. '''
  3. 前向計算
  4. '''
  5. children_data = self.concatenate(children)
  6. parent_data = self.activator.forward(
  7. np.dot(self.W, children_data) + self.b
  8. )
  9. self.root = TreeNode(parent_data, children
  10. , children_data)

forward函數接收一系列的樹節點對象做爲輸入,而後,遞歸神經網絡將這些樹節點做爲子節點,並計算它們的父節點。最後,將計算的父節點保存在self.root變量中。

上面用到的concatenate函數,是將各個子節點中的數據拼接成一個長向量,其代碼以下:

 
  1. def concatenate(self, tree_nodes):
  2. '''
  3. 將各個樹節點中的數據拼接成一個長向量
  4. '''
  5. concat = np.zeros((0,1))
  6. for node in tree_nodes:
  7. concat = np.concatenate((concat, node.data))
  8. return concat

下面是反向傳播算法BPTS的實現:

 
  1. def backward(self, parent_delta):
  2. '''
  3. BPTS反向傳播算法
  4. '''
  5. self.calc_delta(parent_delta, self.root)
  6. self.W_grad, self.b_grad = self.calc_gradient(self.root)
  7. def calc_delta(self, parent_delta, parent):
  8. '''
  9. 計算每一個節點的delta
  10. '''
  11. parent.delta = parent_delta
  12. if parent.children:
  13. # 根據式2計算每一個子節點的delta
  14. children_delta = np.dot(self.W.T, parent_delta) * (
  15. self.activator.backward(parent.children_data)
  16. )
  17. # slices = [(子節點編號,子節點delta起始位置,子節點delta結束位置)]
  18. slices = [(i, i * self.node_width,
  19. (i + 1) * self.node_width)
  20. for i in range(self.child_count)]
  21. # 針對每一個子節點,遞歸調用calc_delta函數
  22. for s in slices:
  23. self.calc_delta(children_delta[s[1]:s[2]],
  24. parent.children[s[0]])
  25. def calc_gradient(self, parent):
  26. '''
  27. 計算每一個節點權重的梯度,並將它們求和,獲得最終的梯度
  28. '''
  29. W_grad = np.zeros((self.node_width,
  30. self.node_width * self.child_count))
  31. b_grad = np.zeros((self.node_width, 1))
  32. if not parent.children:
  33. return W_grad, b_grad
  34. parent.W_grad = np.dot(parent.delta, parent.children_data.T)
  35. parent.b_grad = parent.delta
  36. W_grad += parent.W_grad
  37. b_grad += parent.b_grad
  38. for child in parent.children:
  39. W, b = self.calc_gradient(child)
  40. W_grad += W
  41. b_grad += b
  42. return W_grad, b_grad

在上述算法中,calc_delta函數和calc_gradient函數分別計算各個節點的偏差項以及最終的梯度。它們都採用遞歸算法,先序遍歷整個樹,並逐一完成每一個節點的計算。

下面是梯度降低算法的實現(沒有weight decay),這個很是簡單:

 
  1. def update(self):
  2. '''
  3. 使用SGD算法更新權重
  4. '''
  5. self.W -= self.learning_rate * self.W_grad
  6. self.b -= self.learning_rate * self.b_grad

以上就是遞歸神經網絡的實現,總共100行左右,和上一篇文章的LSTM相比簡單多了。

最後,咱們用梯度檢查來驗證程序的正確性:

 
  1. def gradient_check():
  2. '''
  3. 梯度檢查
  4. '''
  5. # 設計一個偏差函數,取全部節點輸出項之和
  6. error_function = lambda o: o.sum()
  7. rnn = RecursiveLayer(2, 2, IdentityActivator(), 1e-3)
  8. # 計算forward值
  9. x, d = data_set()
  10. rnn.forward(x[0], x[1])
  11. rnn.forward(rnn.root, x[2])
  12. # 求取sensitivity map
  13. sensitivity_array = np.ones((rnn.node_width, 1),
  14. dtype=np.float64)
  15. # 計算梯度
  16. rnn.backward(sensitivity_array)
  17. # 檢查梯度
  18. epsilon = 10e-4
  19. for i in range(rnn.W.shape[0]):
  20. for j in range(rnn.W.shape[1]):
  21. rnn.W[i,j] += epsilon
  22. rnn.reset_state()
  23. rnn.forward(x[0], x[1])
  24. rnn.forward(rnn.root, x[2])
  25. err1 = error_function(rnn.root.data)
  26. rnn.W[i,j] -= 2*epsilon
  27. rnn.reset_state()
  28. rnn.forward(x[0], x[1])
  29. rnn.forward(rnn.root, x[2])
  30. err2 = error_function(rnn.root.data)
  31. expect_grad = (err1 - err2) / (2 * epsilon)
  32. rnn.W[i,j] += epsilon
  33. print 'weights(%d,%d): expected - actural %.4e - %.4e' % (
  34. i, j, expect_grad, rnn.W_grad[i,j])
  35. return rnn

下面是梯度檢查的結果,徹底正確,OH YEAH!

 

遞歸神經網絡的應用

 

天然語言和天然場景解析

在天然語言處理任務中,若是咱們可以實現一個解析器,將天然語言解析爲語法樹,那麼毫無疑問,這將大大提高咱們對天然語言的處理能力。解析器以下所示:

能夠看出,遞歸神經網絡可以完成句子的語法分析,併產生一個語法解析樹。

除了天然語言以外,天然場景也具備可組合的性質。所以,咱們能夠用相似的模型完成天然場景的解析,以下圖所示:

兩種不一樣的場景,能夠用相同的遞歸神經網絡模型來實現。咱們以第一個場景,天然語言解析爲例。

咱們但願將一句話逐字輸入到神經網絡中,而後,神經網絡返回一個解析好的樹。爲了作到這一點,咱們須要給神經網絡再加上一層,負責打分。分數越高,說明兩個子節點結合更加緊密,分數越低,說明兩個子節點結合更鬆散。以下圖所示:

一旦這個打分函數訓練好了(也就是矩陣U的各項值變爲合適的值),咱們就能夠利用貪心算法來實現句子的解析。第一步,咱們先將詞按照順序兩兩輸入神經網絡,獲得第一組打分:

咱們發現,如今分數最高的是第一組,The cat,說明它們的結合是最緊密的。這樣,咱們能夠先將它們組合爲一個節點。而後,再次兩兩計算相鄰子節點的打分:

如今,分數最高的是最後一組,the mat。因而,咱們將它們組合爲一個節點,再兩兩計算相鄰節點的打分:

這時,咱們發現最高的分數是on the mat,把它們組合爲一個節點,繼續兩兩計算相鄰節點的打分......最終,咱們就可以獲得整個解析樹:

如今,咱們困惑這樣牛逼的打分函數score是怎樣訓練出來的呢?咱們須要定義一個目標函數。這裏,咱們使用Max-Margin目標函數。它的定義以下:

 

 

 

 

在上式中,分別表示第i個訓練樣本的輸入和標籤,注意這裏的標籤是一棵解析樹。就是打分函數s對第i個訓練樣本的打分。由於訓練樣本的標籤確定是正確的,咱們但願s對它的打分越高越好,也就是越大越好。是全部可能的解析樹的集合,而則是對某個可能的解析樹的打分。是對錯誤的懲罰。也就是說,若是某個解析樹和標籤是同樣的,那麼爲0,若是網絡的輸出錯的越離譜,那麼懲罰項的值就越高。表示全部樹裏面最高得分。在這裏,懲罰項至關於Margin,也就是咱們雖然但願打分函數s對正確的樹打分比對錯誤的樹打分高,但也不要高過Margin的值。咱們優化,使目標函數取最小值,即:

 

 

 

 

下面是懲罰函數的定義:

 

 

 

 

上式中,N(y)是樹y節點的集合;subTree(d)是以d爲節點的子樹。上式的含義是,若是以d爲節點的子樹沒有出如今標籤中,那麼函數值+1。最終,懲罰函數的值,是樹y中沒有出如今樹中的子樹的個數,再乘上一個係數k。其實也就是關於兩棵樹差別的一個度量。

是對一個樣本最終的打分,它是對樹y每一個節點打分的總和。

 

 

 

 

具體細節,讀者能夠查閱『參考資料3』的論文。

 

小結

咱們在系列文章中已經介紹的全鏈接神經網絡、卷積神經網絡、循環神經網絡和遞歸神經網絡,在訓練時都使用了監督學習(Supervised Learning)做爲訓練方法。在監督學習中,每一個訓練樣本既包括輸入特徵,也包括標記,即樣本。然而,不少狀況下,咱們沒法得到形如的樣本,這時,咱們就不能採用監督學習的方法。在接下來的幾篇文章中,咱們重點介紹另一種學習方法:加強學習(Reinforcement Learning)。在瞭解加強學習的主要算法以後,咱們還將介紹著名的圍棋軟件AlphaGo,它是一個把監督學習和加強學習進行完美結合的案例。

 

參考資料

  1. CS224d: Deep Learning for Natural Language Processing
  2. Learning Task-Dependent Distributed Representations by Back Propagation Through Structure
  3. Parsing Natural Scenes and Natural Language with Recursive Neural Networks
  4. 轉載自:https://zybuluo.com/hanbingtao/note/626300
相關文章
相關標籤/搜索