Chainer 使複雜神經網路變的簡單

OReillyData發表於2016-12-28

“邊執行邊定義”的方法使構建深度學習網路變的靈活簡單

Chainer是一個專門為高效研究和開發深度學習演算法而設計的開源框架。 這篇博文會通過一些例子簡要地介紹一下Chainer,同時把它與其他一些框架做比較,比如Caffe、Theano、Torch和Tensorflow。

大多數現有的深度學習框架是在模型訓練之前構建計算圖。 這種方法是相當簡單明瞭的,特別是對於結構固定且分層的神經網路(比如卷積神經網路)的實現。

然而,現在的複雜神經網路(比如迴圈神經網路或隨機神經網路)帶來了新的效能改進和新的應用。雖然現有的框架可以用於實現這些複雜神經網路,但是它們有時需要一些(不優雅的)程式設計技巧,這可能會降低程式碼的開發效率和可維護性。

而Chainer的方法是獨一無二的:即在訓練時“實時”構建計算圖。

這種方法可以讓使用者在每次迭代時或者對每個樣本根據條件更改計算圖。同時也很容易使用標準偵錯程式和分析器來除錯和重構基於Chainer的程式碼,因為Chainer使用純Python和NumPy提供了一個命令式的API。 這為複雜神經網路的實現提供了更大的靈活性,同時又加快了迭代速度,提高了快速實現最新深度學習演算法的能力。

以下我會介紹Chainer是如何工作的,以及使用者可以從中獲得什麼樣的好處。

Chainer 基礎

Chainer 是一個基於Python的獨立的深度學習框架。

不同於其它基於Python介面的框架(比如Theano和TensorFlow),Chainer通過支援相容Numpy的陣列間運算的方式,提供了宣告神經網路的命令式方法。Chainer 還包括一個名為CuPy的基於GPU的數值計算庫。

>>> from chainer import Variable

>>> import numpy as np

Variable 類是把numpy.ndarray陣列包裝在內的計算模組(numpy.ndarray存放在.data中)。

>>> x = Variable(np.asarray([[0, 2],[1, -3]]).astype(np.float32))

>>> print(x.data)

[[ 0.      2.]

[ 1.     -3.]]

使用者可以直接在Variables上定義各種運算和函式(Function的例項)。

>>> y = x ** 2 – x + 1

>>> print(y.data)

[[  1.   3.]

[  1.  13.]]

因為這些新定義的Varriable類知道他們是由什麼類生成的,所以Variable y跟它的父類有一樣的加法運算(.creator)。

>>> print(y.creator)

<chainer.functions.math.basic_math.AddConstant at 0x7f939XXXXX>

利用這種機制,可以通過反向追蹤從最終損失函式到輸入的完整路徑來實現反向計算。完整路徑在執行正向計算的過程中儲存,而不預先定義計算圖。

在chainer.functions類中給出了許多數值運算和啟用函式。 標準神經網路的運算在Chainer類中是通過Link類實現的,比如線性全連線層和卷積層。Link可以看做是與其相應層的學習引數的一個函式(例如權重和偏差引數)。你也可以建立一個包含許多其他Link的Link。這樣的一個link容器被命名為Chain。這允許Chainer可以將神經網路建模成一個包含多個link和多個chain的層次結構。Chainer還支援最新的優化方法、序列化方法以及使用CuPy的由CUDA驅動的更快速計算。

>>> import chainer.functions as F

>>> import chainer.links as L

>>> from chainer import Chain, optimizers, serializers, cuda

>>> import cupy as cp

Chainer的設計:邊執行邊定義

訓練一個神經網路一般需要三個步驟:(1)基於神經網路的定義來構建計算圖;(2)輸入訓練資料並計算損失函式;(3)使用優化器迭代更新引數直到收斂。

通常,深度學習框架在步驟2之前先要完成步驟1。 我們稱這種方法是“先定義再執行”。

640?wx_fmt=jpeg

圖1. 所有圖片由Shohei Hido友情提供

對於複雜神經網路,這種“先定義再執行”的方法簡單直接,但並不是最佳的,因為計算圖必須在訓練前確定。 例如,在實現迴圈神經網路時,使用者不得不利用特殊技巧(比如Theano中的scan()函式),這就會使程式碼變的難以除錯和維護。

與之不同的是,Chainer使用一種“邊執行邊定義”的獨特方法,它將第一步和第二步合併到一個步驟中去。

640?wx_fmt=jpeg

計算圖不是在訓練之前定義的,而是在訓練過程中獲得的。 因為正向計算直接對應於計算圖並且也通過計算圖進行反向傳播,所以可以在每次迭代甚至對於每個樣本的正向計算中對計算圖做各種修改。

舉一個簡單的例子,讓我們看看使用兩層感知器進行MNIST數字分類會發生什麼。

640?wx_fmt=jpeg

下面是Chainer中兩層感知器的實現程式碼:

# 2-layer Multi-Layer Perceptron (MLP)

# 兩層的多層感知器(MLP)

class MLP(Chain):

def __init__(self):

super(MLP, self).__init__(

l1=L.Linear(784, 100),  # From 784-dimensional input to hidden unit with 100 nodes

# 從784維輸入向量到100個節點的隱藏單元

l2=L.Linear(100, 10),  # From hidden unit with 100 nodes to output unit with 10 nodes  (10 classes)

# 從100個節點的隱藏單元到10個節點的輸出單元(10個類)

)

# Forward computation

# 正向計算

def __call__(self, x):

h1 = F.tanh(self.l1(x))     # Forward from x to h1 through activation with tanh function

# 用tanh啟用函式,從輸入x正向算出h1

y = self.l2(h1)                 # Forward from h1to y

# 從h1正向計算出y

return y

在建構函式(__init__)中,我們分別定義了從輸入單元到隱藏單元,和從隱藏單元到輸出單元的兩個線性變換。要注意的是,這時並沒有定義這些變換之間的連線,這意味著計算圖沒有生成,更不用說固定它了。

跟“先定義後執行”方法不同的是,它們之間的連線關係會在後面的正向計算中通過定義層之間的啟用函式(F.tanh)來定義。一旦對MNIST上的小批量訓練資料集(784維)完成了正向計算,就可以通過從最終節點(損失函式的輸出)回溯到輸入來實時獲得下面的計算圖(注意這裡使用SoftmaxCrossEntropy做為損失函式):

640?wx_fmt=jpeg

這裡的關鍵點是神經網路是直接用Python來定義的而不是領域特定語言,因此使用者可以在每次迭代(正向計算)中對神經網路進行更改。

這種神經網路的命令性宣告允許使用者使用標準的Python語法進行網路分支計算,而不用研究任何領域特定語言(DSL)。這跟TensorFlow、 Theano使用的符號方法以及Caffe和CNTK依賴的文字DSL相比是一個優勢。

此外,可以使用標準偵錯程式和分析器來查詢錯誤、重構程式碼以及調整超引數。 另一方面,儘管Torch和MXNet也允許使用者使用神經網路的命令性建模,但是他們仍然使用“先定義後執行”的方法來構建計算圖物件,因此除錯時需要特別小心。

實現複雜的神經網路

上面只是一個簡單且固定的神經網路的例子。 接下來,讓我們看看如何在Chainer中實現複雜的神經網路。

迴圈神經網路是一種以序列為輸入的神經網路,因此它經常用於自然語言處理中,例如序列到序列的翻譯和問答系統。 它不僅根據來自輸入序列的每個元組而且還基於其前序狀態,來更新內部狀態,因此它把元組序列間的依賴關係也考慮進去了。

由於迴圈神經網路的計算圖包含前序時間和當前時間之間的有向邊,所以其構造方法和反向傳播方法不同於那些固定神經網路的方法(例如卷積神經網路)。在當前實踐中,這種迴圈計算圖在每次模型更新時,通過一個稱為“時間截斷反向傳播”的方法被轉化為有向無環圖。

下面的示例的目標任務是預測給定句子的下一個詞。訓練好的神經網路可以產生語法上正確的詞而不是隨機的詞,即使整個句子對人類來說沒有什麼意義。以下程式碼展示了包含一個迴圈隱藏單元的簡單迴圈神經網路:

# Definition of simple recurrent neural network

# 定義簡單的迴圈神經網路

class SimpleRNN(Chain):

def __init__(self, n_vocab, n_nodes):

super(SimpleRNN, self).__init__(

embed=L.EmbedID(n_vocab, n_nodes),  # word embedding

#嵌入詞

x2h=L.Linear(n_nodes, n_nodes),  # the first linear layer

#第一線性層

h2h=L.Linear(n_nodes, n_nodes),  # the second linear layer

#第二線性層

h2y=L.Linear(n_nodes, n_vocab),  # the feed-forward output layer

)

#前饋輸出層

self.h_internal=None # recurrent state

#迴圈狀態

def forward_one_step(self, x, h):

x = F.tanh(self.embed(x))

if h is None: # branching in network

#網路的分支擴充套件

h = F.tanh(self.x2h(x))

else:

h = F.tanh(self.x2h(x) + self.h2h(h))

y = self.h2y(h)

return y, h

def __call__(self, x):

# given the current word ID, predict the next word ID.

#給定當前詞的ID,預測下一個詞的ID

y, h = self.forward_one_step(x, self.h_internal)

self.h_internal = h # update internal state

#更新內部狀態

return y

在建構函式中以及在多層感知器中,只定義層的型別和大小。 根據輸入詞和當前狀態引數,forward_one_step()方法返回輸出詞和新狀態。 在正向計算(__call__)的每個步驟中,呼叫forward_one_step()方法,並用新狀態更新隱藏迴圈層的狀態。

使用流行的文字資料集Penn Treebank(PTB),我們訓練了一個模型用來從可能的詞彙中預測下一個詞。 然後使用加權取樣方法來讓這個訓練好的模型預測後續詞彙。

“If you build it,” => “would a outlawed a out a tumor a colonial a”

“If you build it, they” => ” a passed a president a glad a senate a billion”

“If you build it, they will” => ” for a billing a jerome a contracting a surgical a”

“If you build it, they will come” => “a interviewed a invites a boren a illustrated a pinnacle”

這個模型已經學會了(並能產生)許多重複的“a”和一個名詞對或“a”和一個形容詞的詞對。 這意味著“a”是最可能的詞之一,並且名詞或形容詞傾向於跟在“a”之後。

對人類而言,結果看起來幾乎相同。即使使用不同的輸入,結果都是語法錯誤和毫無意義的。 然而,這些推測確實是基於資料集的真實句子,通過訓練詞的型別和詞之間的關係得到的。

因為在SimpleRNN模型中缺乏表達性,造成了上述不可避免的結果。但這裡的重點是:使用者可以實現像SimpleRNN一樣的各類迴圈神經網路。

作為比較,在使用了現成的叫做“長短時記憶網路”的迴圈神經網路模型後,生成的文字從語法上看就比較正確了。

“If you build it,” => “pension say computer ira <EOS> a week ago the japanese”

“If you buildt it, they” => “were jointly expecting but too well put the <unknown> to”

“If you build it, they will” => “see the <unknown> level that would arrive in a relevant”

“If you build it, they will come” => “to teachers without an mess <EOS> but he says store”

由於流行的RNN元件(如LSTM和門控迴圈單元(GRU))已經在大多數框架中被實現了,因此使用者不需要關心它們的底層實現。 儘管如此,如果要對它們做重大的改動或者建立一個全新的演算法和元件,Chainer跟其他框架相比就有更大的靈活性。

隨機變化的神經網路

同樣的,使用Chainer實現隨機變化的神經網路是非常容易的。
以下是實現隨機神經網路ResNet的模擬程式碼。 在__call__函式中,以概率p投擲一個不均勻的硬幣,並根據有沒有單元f來改變正向路徑。這在每批訓練集的每次迭代時完成,並且計算圖每次都不同,它們是在計算完損失函式之後相應地用反向傳播演算法更新的。

# Mock code of Stochastic ResNet in Chainer

# Chainer中的隨機神經網路ResNet的模擬程式碼

class StochasticResNet(Chain):

def __init__(self, prob, size, **kwargs):

super(StochasticResNet, self).__init__(size, **kwargs)

self.p = prob # Survival probabilities

#生存概率

def __call__ (self, h):

for i in range(self.size):

b = numpy.random.binomial(1, self.p[i])

c = self.f[i](h) + h if b == 1 else h

h = F.relu(c)

return h

總結

除了上述內容,Chainer還有許多功能可以幫助使用者容易高效地實現他們自己的神經網路。

CuPy是包含在Chainer中的為GPU使用的 NumPy等效陣列後端。它支援獨立於CPU / GPU的編碼,就像基於NumPy的運算一樣。訓練迴圈神經網路和處理資料集可以用Trainer抽象化,這使得使用者不用每次都編寫這樣的常規程式碼,他們可以專注於編寫創新的演算法。儘管可擴充套件性和效能不是Chainer的主要關注點,但它通過充分利用NVIDIA的CUDA和cuDNN仍然可以與其他框架相競爭(可參考公開的基準測試結果)。

Chainer已經在許多學術論文中被使用,包括計算機視覺、語音處理、自然語言處理和機器人等領域。此外,Chainer在許多行業中越來越受歡迎,因為它有利於新產品和服務的研發。豐田汽車、松下FANUC公司廣泛使用Chainer,並且已經和Preferred Networks公司的Chainer的開發團隊合作展示了一些案例。

歡迎有興趣的讀者訪問Chainer網站以瞭解更多詳情。我希望Chainer可以為更多基於深度學習的前沿研究和產品做出貢獻!

作者介紹


Shohei Hido

Shohei Hido是Preferred Networks公司的首席研究官,Preferred Networks是Preferred集團的分公司。目前Shohei負責Deep Intelligence in Motion(一個在物聯網應用中使用深度學習的軟體平臺)。在此之前,Shohei是Preferred 集團的Jubatus(Jubatus專案是一個用於實時流式的機器學習的開源軟體框架)專案的負責人,並在東京的IBM 研究中心工作了6年,擔任機器學習及其行業應用的專業研究員。 Shohei擁有東京大學資訊情報學碩士學位。

640?wx_fmt=png

相關文章