選自automating the future機器之心編譯參與:Jane W、吳攀
在這篇文章中,作者們使用 Elixir 程式語言創建一個標準的 3x3 深度學習神經網絡。希望讀者能通過閱讀本文而對先進的遺傳編程(genetic programming)和 Elixir 中新的人工智慧技術有更深的理解。
開篇語
研究深度學習如從事巫術般瘋狂。人們必須花費相當多的時間理解技術,同時在創造真正自動化的東西時考慮其優點和缺點,還要在半夜醒來時擔心自動化將對我們的社會造成多大的顛覆。
創建你的第一個神經網絡是比較簡單的,在過程中你可以看到如何用少量的代碼來自動完成一項給定的任務。
在這篇文章中,我們將使用 Elixir 程式語言創建一個標準的 3x3 深度學習神經網絡。希望讀者能通過閱讀本文而對先進的遺傳編程(genetic programming)和 Elixir 中新的人工智慧技術有更深的理解。
用途
現在的大數據需要未來的自主深度學習系統。為了掌握這些系統工作的原理,我們將構建一個標準的神經網絡來學習一個小的問題集。
我發現用 Elixir 從頭開始設計和構建這些類型的系統時,有三件事是有幫助的。讀者可以參考以下連結作為輔助:
1. 對 Elixir 和 Erlang OTP 生態系統的基本了解:http://elixir-lang.org/getting-started/introduction.html
2.Numerix(一種基於 Elixir 的機器學習庫,由 Safwan Kamarrudin 編寫):https://github.com/safwank/Numerix
3.Matrix(用於計算矩陣的有用的庫,由 Tom Krauss 編寫):https://github.com/twist-vector/elixir-matrix
還有其它的 Elixir 包(如 Tensor)允許 Elixir 開發者做出一些複雜的東西,但這裡我們將只介紹 Matrix 和 Numerix。
範式
正如前面討論的,未來的神經網絡自動化解決問題的方式與傳統訓練的編程模型解決問題的方式有很大的不同。這些系統通過樣本進行學習。訓練者提出所期望的目標或系統實現的目標,並且給系統提供訓練樣本,直到系統學習如何達到想要的目標(target)。
如果通過人類語言溝通,計算機很難理解生活的細微差別,以及我們想要和需要什麼。然而,如果機器通過數字和浮點整數表示實際的數據和問題,那麼奇蹟般地,它們可以開始解決問題並理解人類!
對於新的軟體系統,由過程/對象(procedural/object)驅動的編程現在開始轉變為由統計數學/數據(statistical-mathematical/data)驅動的方法。這種新的模式轉變是至關重要的,以鑑別讀者是否計劃在技術行業保持相關性。自從 2011 年 Jeopardy 節目上 WATSON 的到來,老的編程和解決問題的方式已經死了。擁抱這種數據驅動的統計模式的公司將最終領導整個技術行業。
並沒有明確的程序指示 WATSON 在那晚的節目上做什麼。事實上,WATSON 甚至不知道它會得到什麼問題。它所擁有的只是數據、學習算法和多層神經網絡。
我們在這裡將構建的神經網絡不會像 WATSON 那樣複雜,但能夠說明多層網絡的概念以及工作的原理。
問題
假設我們有一個數字序列,它可以表示特定問題空間中的任何東西。讓我們假設這個數字序列是 1、0 和 0。這會組成一個看起來像 [1,0,0] 的列表(list)。
但是,這個列表是一個問題。我們想要實現一個都是 1 的序列。該列表表示為 [1,1,1]。這個數字列表可以當作我們的目標。
我們的問題空間,簡而言之,列在下表中:
我們將輸入與希望得到的目標一起列出。
我們希望系統能夠區分輸入數據與目標數據,因此我們還需要一個隨機數據集,以便與目標進行比較。該隨機數據集被稱為訓練集。神經網絡用這個訓練集來學習如何得到預測結果。加入新增的訓練集數據,我們的圖表變成下圖:
設計
在計算機中表示神經網絡的最佳方式是通過矩陣。矩陣是線性代數的有用工具,它允許我們對數字向量進行操作。
從圖表中可以看出,有 3 列和 3 行。此圖表可表示一個 3x3 的矩陣!
主體的神經網絡模型表示為線性代數矩陣列表。數組(array)中的每個元素可以被認為是一個節點/神經元。每個神經元負責計算和生成輸出,輸出又會影響整個神經網絡系統。
多層神經網絡分為三部分。第一部分稱為輸入層。第二層稱為隱藏層。最後一部分稱為輸出層。非常複雜的網絡有多個隱藏層,但是對於本文的例子,我們只有一個隱藏層。
當我們從左到右顯示數據流時,我們神經網絡的圖片看起來像這樣:
數據流從左到右IL 代表輸入層(Input layer),HL 代表隱藏層(hidden layer),OL 代表輸出層(output layer)每個神經元在網絡內表示。我們建立一個 3x3 的網絡,因此有 9 個神經元。
如果我們將圖表變成這樣的架構,它的可視化看起來如下:
I= 輸入,h 表示隱藏,而 o 表示輸出。每個數字對應於上面數據圖表上列出的數字。
這是我們希望神經網絡做的。我們需要它來計算輸入,並將其變成我們想要的輸出!
代碼
現在我們要做的第一件事是創建 Elixir 項目。我決定叫它「DEEPNET」。我們想要一個 Supervisor 讓這個項目能更自動化啟動,所以我們使用命令:
mix new deepnet --sup
這條命令創建了一個帶有 supervisor 的 Elixir 項目。
加載相關包
接下來要做的是加載所需的相關包。我上面提到了 Numerix 和 Matrix,我也加載了 sfmt,以確保我們的隨機權重確實是隨機的。
開始應用
我們需要一種方法讓 Supervisor 自動啟動我們的神經網絡。因為想要一個 3x3 的架構,我們需要創建 9 個神經元。這意味著每層都需要有 3 個神經元。這些神經元可以在啟動時創建。我們編寫一個啟動函數來告訴 Supervisor 這樣做。
應用的 Supervisor 引用了 Deepnet.Network 模塊(我們將在下面介紹)。我們還需要設定神經網絡在每一層創建的節點數。
創建網絡
我們在這裡引用一個 create 函數。這將有助於 Supervisor 下面的工作。create 函數將處理這些數字列表。因為這些數字代表層中的神經元。將初始狀態(state)存儲在 Elixir 代理(agent)中可能比較明智。
每個參數對應於層中的多個節點。第 4 個參數是學習速率(learning rate),默認為 1.0。這將在後面進一步闡釋。
初始化隨機權重
如果你一直在學習 ATF,你可能會記得所有神經元都需要與其相關的權重。我們希望權重儘可能隨機。這種隨機化使我們相信我們在訓練期間已經收斂到正確的解決方案。神經網絡的訓練重點是找到適合於當前特定問題的適當權重。對於每個神經元,我們需要一個函數來創建 9 個不同的權重。我們的計算也需要考慮偏差(bias)。這個函數如下:
首先,我們利用 sfmt 來播種我們的時間戳以幫助確保我們獲得隨機權重。接下來我們為輸入生成隨機權重。但是,我們不想止於此。這會有助於增加新的偏差以便真正平衡我們的權重,隨意從一個 0.5 的矩陣減去我們的輸入權重將能給我們一個很好的隨機化的權重分類以開始。最終,我們使用這些初始化的權重對網絡進行了更新。我們的 9 個權重應該看起來像這樣:
網絡中所有神經元所對應的隨機權重。它的優點在於有一個負權重和正權重的組合
現在我們馬上能夠得到權重了。讓我們先看看矩陣的大小。
現在我們有 3x3 的初始化權重結構。我們的整個神經網絡現在看起來像這樣
我們的神經網絡被表示為 Elixir Struct。除錯誤率和目標(我們將在下一步探索)之外,我們已經設置好所有的數值。
計算誤差
神經網絡需要不斷地反饋預測性能。這個反饋是通過我們所說的錯誤率(error rate)收集的。有不同的計算錯誤率的方法,其方法完全取決於個人。在本文中,我們將使用 MSE,即均方誤差(Mean Squared Error)。
神經網絡在訓練期間的工作是不斷的將其輸出與訓練期間給出的目標輸出進行比較。我們需要一種方法來計算並存儲神經網絡的錯誤,以便我們監控訓練的效果。這個函數的形式很明確:
我們取得神經網絡的最終輸出和初始輸入。然後將整個網絡的最終輸出與目標進行比較,以便我們可以計算均方誤差。
我們引用了 List.flatten/1 函數,旨在將我們的多維列表改為單個列表,使計算更加容易。
最後,我們通過使用神經網絡新的錯誤率來更新代理。
調整網絡權重
在上面函數的最後一行你可能已經注意到對 Deepnet.Network.adjust_weights/2 函數的引用。這個步驟很重要。在訓練期間,如果神經網絡發現與訓練的目標發生偏離,它需要一種方法來改進。剛才我提到了學習速率常數。這就是改進神經網絡要用到的。讓我們來探索 Deepnet.Network.adjust_weights/2 函數:
用 adjust_weights 函數接受圖層的輸出和初始輸入接下來要做的是計算 delta(增量)。這裡我們得到目標與輸出的差異,並計算該結果的點積。這兒需要單獨的計算,即 delta(或小變化)計算。接下處理梯度(gradient),這是我們能做的使我們更接近最終目標的最小幅變化。因為我們可以處理許多輸出,我們將並行(parallel)映射(map)所有輸出的梯度計算。梯度計算定義為:輸出 x delta x learning_rate(學習速率)這個學習速率可以是從 1.0 到 3.0 的任何值。也有人用其它的值域。它取決於神經網絡的創建者以及需要多快的學習進度。在我們的例子中,我將使用 1.0,因為這個問題並不重要。最後,取當前的權重,並從梯度中減去它們得到新權重。然後為網絡更新新權重。
計算神經元輸出
你可能想知道如何產生連續傳入函數的輸出。我們需要一種方法來計算每個神經元的輸出。這裡有一篇博客來幫助你複習神經元如何計算其輸出(http://www.automatingthefuture.com/blog/2016/10/2/the-power-of-activating-the-artificial-neurons)。
對於我們的特定問題集,我們將使用 sigmoid 函數作為我們的激活函數(activation function)。記住,神經元內部的數據信號經過 3 個階段。第一階段是計算輸入和權重的總和或點積。下一階段是使用激活函數。最後一個階段是加入偏差。我們已經在初始化權重時計入了偏差,所以我們的函數不需要有那個部分。那麼我們所有要做的事是階段 1 和 2。
這裡我們取輸入和權重並做並行映射。每個權重對應一個輸入,所以在 Elixir 中我們可以將它們壓縮成元組(tuple)。元組的第一個元素是輸入,第二個元素是權重。對於每個輸入和權重,我們計算:點積(dot_product)/總和 summation。接下來,我們使用 Numerix.Special.logistic/1 函數,它本質上是另一個名稱的 sigmoid 函數。
因為我們需要將每一個計算中結果作為一個列表,我們然後打包結果以形成適當形式的輸出。
我們現在可以計算、神經元的輸出。但是,在完成之前還需要一個步驟。我們需要一種將數據從一層移動到下一層的方法。將數據從上一層移動到下一層的這個過程被稱為前饋(feed-forward)。由於我們將數據從輸入層前饋到隱藏層,然後將隱藏層的輸出傳送到輸出層,因此我們基本上將數據前饋兩次。幸運的是,我們可以很容易的通過 Elixir 的模式匹配做到這一點。
第一個前饋僅接受輸入列表,並計算輸入層與隱藏層相連接的輸出。然後將該結果傳遞到第二個前饋函數。
第二個前饋函數接收前一層的輸出以及前一層的舊權重和原始輸入。然後計算最終輸出。至此整個神經網絡的計算結束。在這裡,通過計算網絡中的錯誤率,我們可以看到得到的結果有多好。
過程
學習是一個重複的過程。如果我們的網絡沒有得到正確的解決方案,它必須再次重複整個過程,直到得到正確結果。每次神經網絡將對其自身進行小幅調整,直到達到其最終目標。可以認為這個過程是一個巨大的學習循環。
每次循環完成並且網絡再次啟動以便最小化錯誤時,我們將該過程稱為反向傳播(back propagation)。這是因為誤差通過網絡傳播以進行重新調整。這就是現代系統與傳統系統的區別。傳統系統不得不等待人類來解決存在的錯誤。現代系統想要最大限度地減少它們的錯誤率,並力求完美從而減輕工程師的維護負擔。從解決問題的角度,希望你開始看到這種好處!
訓練自動化
對於神經網絡,自動化訓練過程總是一個好辦法。有時候,對特定問題集的訓練可能需要幾個小時甚至幾天。手動執行這個過程是不明智的,所以我們將編寫一個函數來處理這個過程。
首先,我們取輸入列表和目標列表。然後,我們將輸入和目標都轉換成二維列表。然後我們用目標更新我們的代理,使它不再為零。最後,我們開始前饋過程。
學習自動化
如前所述,學習過程是一個循環。Elixir 是一種功能語言,這使得我們能夠使用函數來處理循環。在我們的循環中,我們需要收集輸入和目標,並將其傳入到網絡中。網絡訓練數據並計算錯誤率。我們希望得到的錯誤率最小。因此我希望網絡訓練的誤差率低於 0.02。如果發現其錯誤率高於 0.02,那麼它必須繼續訓練。這就是學習過程。神經網絡必須經歷重複循環,直到學習的任務幾乎沒有錯誤。我們可以通過模式匹配(pattern matching)實現:
第一個學習函數接收網絡的錯誤率、用戶數據和 epoch。epoch 是神經網絡迭代一次的周期。你可以認為一個 epoch 就是一個網絡的時間長。此函數僅在我們的錯誤率高於 0.02 時被調用。調用該函數將向系統表明它需要更多的訓練。每次訓練循環後 epoch 的數值增加 1。如果錯誤率小於 0.02,則提取錯誤率並傳入最終的學習函數。如果不是,我們稱它為當前的學習函數。第二個學習函數採用相同的參數,但它被當作停止函數(stopping function)。當訓練完成並且達到可以接受的錯誤率時使用此函數。它表明我們的系統已經完全訓練了數據集,並準備好進行測試。
最後我們需要做的是為我們的用戶輸入和目標創建數據結構(data struct)。然後,這個信息將傳遞給一個能夠啟動整個過程的學習函數。
我們原始的數據表定義為一個結構。
現在我們通過最終函數來啟動整個過程:
這裡我們初始化隨機權重,並將我們的用戶數據和我們想要的目標傳遞給網絡。接下來,我們通過傳入錯誤率、用戶數據和我們網絡的 epoch(第一次啟動的初始值為 0)來調用我們的學習函數。
現在,我們的網絡已經完全建成了。當我們啟動程序時會發生什麼?
成了!我們可以看到用了 13 個 epoch 來完成訓練。我們的網絡終於達到我們的目標列表 [1,1,1],並且有低於 0.02 的錯誤率!這非常令人吃驚。
結論
你可能會想,這有什麼意義?如何在實際中使用它?機器學習的重要性對於下一個技術時代是至關重要的,它允許工程師處理大量的數據,同時訓練系統來得到洞察或預測結果,並解決我們可能無法得到線索的問題。正如我們剛才見到的,我們可以看到這些系統如何擅長最小化錯誤,這在現實中是無價的。
神經網絡的美麗之處在於,我們可以用不同的方式構建它們,在我們的軟體系統中創建類似人類的智能。在本文中,我們沒有介紹所有的算法和這些網絡架構不同的方式。未來的自動化目標是繼續為 Elixir 社區提供如何使用神經網絡解決各種各樣的問題的精彩例子。
現在我們知道如何設計一個基本的多層神經網絡,我們可以應用到一些真正自動化軟體系統的優秀的案例項目,以便學習和解決我們未來遇到的不同類型的問題。我將相關 Deepnet 代碼放在了 GitHub 上,有興趣可以查閱 https://github.com/TheQuengineer/deepnet。你可以 fork、實驗和更改代碼。這個帖子可以作為 Elixir 社區的一個例子,作為從頭開始學習設計深度學習網絡的一種方式!