【神經網絡與深度學習】chainer邊運行邊定義的方法使構建深度學習網絡變的靈活簡單

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

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

然而,如今的複雜神經網絡(好比循環神經網絡或隨機神經網絡)帶來了新的性能改進和新的應用。雖然現有的框架能夠用於實現這些複雜神經網絡,可是它們有時須要一些(不優雅的)編程技巧,這可能會下降代碼的開發效率和可維護性。node

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

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

如下我會介紹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。 咱們稱這種方法是「先定義再運行」。

Slide3_crop-68a589aa5d30ca7b11db288d94d4140d

圖1. 全部圖片由Shohei Hido友情提供

對於複雜神經網絡,這種「先定義再運行」的方法簡單直接,但並非最佳的,由於計算圖必須在訓練前肯定。 例如,在實現循環神經網絡時,用戶不得不利用特殊技巧(好比Theano中的scan()函數),這就會使代碼變的難以調試和維護。

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

Slide2_crop-f8c57262872d5939211f0ae599a05afb

計算圖不是在訓練以前定義的,而是在訓練過程當中得到的。 由於正向計算直接對應於計算圖而且也經過計算圖進行反向傳播,因此能夠在每次迭代甚至對於每一個樣本的正向計算中對計算圖作各類修改。

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

Slide1_crop-d61871466db07cbe5af71ee63ca4df7a

下面是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作爲損失函數):

graph-d9d85235dc0720a8d8abcf07d6522b0b

這裏的關鍵點是神經網絡是直接用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能夠爲更多基於深度學習的前沿研究和產品作出貢獻!

相關文章
相關標籤/搜索