用 C 語言實現神經網路需要幾步?
本文來自作者滄夜在GitChat上精彩分享「用 C 語言實現神經網路需要幾步?」
【不要錯過文末彩蛋】
編輯 彥祖
一、寫在前面的話
本文主要講講神經網路的數學基礎,並將神經網路中浮誇的概念用合理的順序整理一下。
應該具備的數學基礎說多不多:基本上熟悉導數、線代、概率,那麼大部分內容就可以看懂了,然而再進行深入學習的話又需要了解一些微分流形的東西,這個東西也是函數導數所衍生的概念,想想也不是很複雜。
但好事者給起了很多複雜的名字,比如 PCA 比如 Adam,這無形中也增加了學習成本。以至於很多人學習過程的最大感受就是:我終其一生似乎都在名詞的海洋上漂泊。
很多年前在學習數據挖掘演算法的時候老師就說過:這些演算法你們要完全學會得需要幾年的時間。幾年時間大約也是人生中最有學習興趣的幾年。
現在各種渠道充斥著《28天學會XXXX》的就顯得有些高調的過分了,28天學習大概只夠學習一門簡單的編程語言比如 Python,想學會演算法之上的內容除非你數學很好,是極好的那種,這種人應該是極少的。
所以學習神經網路也是一個體力活,需要不斷的分析實踐。後面我會在 GitChat 上分享用 TensorFlow 來實現神經網路的專題,如果大家感興趣,請看文末。
二、從幾何開始
從幾何開始大概是學習神經網路最合適的切入點,大部分神經網路的目標就在於構建一個超曲面,而幾何中的張量衍生出了矩陣概念,最後作為幾何基礎的函數和導數是分析神經網路的基礎。
首先來看下空間函數:
上面這個函數是定義於N維空間中的函數,這個N維空間數學符號為
,其中被稱為空間坐標,到這裡就是空間幾何的入門了,但接下來我們並不能分析神經網路。在分析之前還需要一個概念就是梯度:
這裡的梯度就是對每個坐標分量求偏導數,這是最簡單的梯度的形式也是歐式空間中的梯度,在其他坐標空間之下如球坐標、柱坐標等,梯度作為一個向量顯然應當保持空間不變性,這種空間不變性指的是不管在何種坐標系之下梯度方向和大小都不會發生變化。
為保持這種不變性梯度分量的數值上就會改變。這種數值上的變化可以用鏈式求導法則求得:
假設空間坐標變換有函數關係將上面的鏈式求導寫成矩陣形式:
這裡為了保持在空間坐標變換下的不變數所乘以的矩陣A就稱之為張量,這個張量是有具體表達的:
所以張量(Tensor)在幾何中就定義為不隨空間變換變化而變化的量,到此為止不再進行更深入的探討。
可以看到 Tensor 與常說的矩陣(Matrix)是有區別的,這種區別使得張量可以說是矩陣的子集。
同時也可以理解為什麼在一些梯度迭代演算法中需要對梯度進行一定的變換形成所謂的共軛梯度演算法,這是因為按笛卡爾空間梯度計算的話梯度可能空間已經發生了扭曲之後,使得梯度方向並非最優方向。
上面說到了矩陣,其可以看成一系列數值、函數的有序集合,用矩陣表示公式是方便的,比如我們的線性方程組:
坐標的仿射變換也可以用矩陣的形式表示:
張量中點乘是一種縮並運算,這種縮並代表著求和運算,但是總寫求和符號是非常麻煩的,很多數學書籍中將其寫成愛因斯坦約定求和的方式:
在乘法計算中相同指標代表求和可以省略求和符號,這在數學分析中是非常有力的工具。
再回到上面的仿射變換,仿射變換代表了空間的旋轉拉伸變換:
再補充一些群的概念
深入學習機器學習不可避免的會涉及到流形和群,所以這裡再補充一些概念。
上面說到,梯度是向量,這種向量的變換方式是與張量A做運算,還有另一種向量其變換方式與梯度向量不同比如速度:
這裡用到了另一種簡寫:
這樣在坐標變換x=>z的情況,依然用鏈式求導法則:
上面就用到了約定求和,可以將其寫成矩陣形式:
空間中向量長度是一個很重要的概念:
這種定義方式很熟悉,在計算機科學中常將其稱為二範數。
上面講過這只是笛卡爾空間中的長度,真實空間長度還需要乘以一個二階張量:
具體形式的推導有興趣的可以自己推導一下,利用鏈式求導法則就可以。
這裡要補充的是,如果變換保持G的形式不變,則將變換稱之為度量之下的一個運動。
我們常用的一個相似度標準有一個閔可夫斯基距離:
涉及到了閔可夫斯基空間的內容,數學中其特點就是向量長度定義為:
這與計算機科學中的閔可夫斯基距離是有區別的,閔可夫斯基空間中是狹義相對論空間。
三、張量和矩陣
前面已經多次用到了張量和矩陣的概念了,所以也能理解為什矩陣中會有一些特徵向量之類的名詞,這是因為矩陣和張量空間的聯繫所致。空間中的向量可以用幾個向量疊加的形式比如:
這裡的是坐標基向量,坐標基可以看成是長度為1的有序實數對,是相應分量上的長度。坐標變換也就是可以看成是對坐標基的變換。對於矩陣可以看成是幾個向量的集合:
這些向量v可以用k個向量表示其中,此時就可以對向量數量進行了壓縮,這也是數據壓縮中所訴求的:用更少的數據存儲更多內容。
下面講講如何進行這種數據的壓縮:
首先的就是一個矩陣的特徵值分解問題,我們可以將一個方陣變為三個矩陣相乘:
其中代表一個對角矩陣。對於的情況,這裡就是矩陣的秩,也就是對角矩陣中有多少個非0值,可以用少於N個向量來表示矩陣A中所包含的所有向量,所以這些向量就稱為特徵向量。
但是對於非方陣情況下,顯然無法進行特徵值分解,這就需要對矩陣進行部分變換:
對A進行常規的特徵值分解:
由此找到了n個正交基
若將正交基用B進行變換,
將向量進行相乘:
化簡過程用到了特徵值的性質:
可以看到,經過線性變換B之後,向量仍然是正交的。
向量的個數與矩陣A的秩相同。將進行標準化:
將上面式子寫成:
再寫成矩陣的形式:
所以
因此任意矩陣都可以寫成三個矩陣相乘的形式。這就是矩陣的奇異值分解(SVD),其中特徵值的大小就代表不同特徵向量的權重,對於小的權重可省略。
所以 SVD 可以方便的進行數據壓縮,但是從另一方面來說,所謂數據壓縮就是去除線性相關性比較高的向量,那麼如何度量這種相關性呢,一種方法就是將相關矩陣對角化:
比如數據矩陣X,假設均值為0,那麼相關矩陣就可以寫成:
由此,可以對X乘以即可使得矩陣對角化,這是PCA方法。
四、函數分析
很多機器學習的訓練過程中都需要建立一個靠譜的函數,稱之為代價函數,這個代價函數也可以稱之為泛函,泛函存在的目的就在於將複雜的數學問題簡化為求解函數最小值的問題,舉一個最優化問題的例子,假設存在一個解線性方程的問題:
上面方程是難於直接求解的(這個問題還是有其他解法的),因此將其轉化為x的函數:
對於二維向量x其泛函是一個空間二次曲面,而泛函極小值,也就是梯度為0的時候:
就對應於方程的解,當然神經網路中泛函常用空間距離或者熵就可以了,這裡並沒有具體涉及到泛函建立的過程。泛函分析還可以用於證明求解高斯分布是最大熵分布。
對於一般多維函數,可以對其進行 Taylor 展開:
上面是多維函數的展開的數學形式,其中 H 就是 Hessian 矩陣。為了尋找函數的最小值,只要對其不斷的在負梯度方向上尋找就可以了:
這就是常說的最速下降法,在最小值附近還有一個特徵是函數改變數最小,這對 Tayler 展開兩邊對 dx 求偏導數就可以得到:
所以這又是一種梯度迭代方法,稱之為牛頓法,還有一種,可以預估函數的線性梯度,並在計算中將其減去:
此時就可以將函數g的目標定位增長為0,此時對其求x的偏導:
這就成為高斯牛頓法
最後來看看多層神經網路的情況,用下降法做基本思想,這裡將代價函數定義為空間距離:
目標就是求解該函數的梯度:
由於多層神經網路是分層的所以求解偏導數時將其用上標表示不同層的權值,來觀察任意相鄰兩層之間的關係:
二者矩陣之間的關係如下:
上面的導數是從最後一層的向第一層遞推傳遞的,因此這裡稱為反向傳播演算法。
這裡理論部分講的差不多了,還需要有個問題,就是在計算的時候實際上所形成的最優化曲面是一個隨著樣本不斷變化的曲面:
這也就是說實際上梯度是帶有一定的隨機性,為了減少這種隨機性,更好的預測梯度的方向,在訓練過程中可以一次加入多個樣本,這個樣本的大小稱為為BATCHSIZE,這種學習方式稱為批學習,批學習是容易進行並行化的,而一個個去輸入樣本的話稱之為在線學習,這兩種學習的學習曲線有所不同。
五、實現
其實到上面應該就可以結束了,但是提到了BP演算法實現,這裡為了方便用了Python,先來看看其結果:
顯然多層網路可以解決抑或問題。
"""@author: Cangye@hotmail.com"""import numpy as npclass BPAlg(): def sigmoid(self,x): """ Define active function sigomid """ return 1/(1+np.exp(-x)) def d_sigmiod(self,x): """ Define df/dx """ return np.exp(-x)/(1+np.exp(-x))**2 def __init__(self,shape): """ Initialize weights """ self.shape=shape self.layer=len(shape) self.W = [] self.b = [] self.e = [] self.y = [] self.dW = [] self.v = [] self.db = [] self.d_sigmoid_v = [] for itrn in range(self.layer-1): self.W.append(np.random.random([shape[itrn], shape[itrn+1]])) self.dW.append(np.random.random([shape[itrn], shape[itrn+1]])) self.b.append(np.random.random([shape[itrn+1]])) self.db.append(np.random.random([shape[itrn+1]])) for itr in shape: self.e.append(np.random.random([itr])) self.y.append(np.random.random([itr])) self.v.append(np.random.random([itr])) self.d_sigmoid_v.append(np.ones([itr])) def forward(self, data): """ forward propagation """ self.y[0][:] = data temp_y = data for itrn in range(self.layer-1): temp_v = np.dot(temp_y, self.W[itrn]) temp_vb = np.add(temp_v, self.b[itrn]) temp_y = self.sigmoid(temp_vb) self.y[itrn+1][:] = temp_y self.d_sigmoid_v[itrn+1][:] = self.d_sigmiod(temp_vb) return self.y[-1] def back_forward(self, dest): """ back propagation """ self.e[self.layer-1] = dest-self.y[self.layer-1] temp_delta = self.e[self.layer-1]*self.d_sigmoid_v[self.layer-1] temp_delta = np.reshape(temp_delta,[-1,1]) self.dW[self.layer-2][:] = np.dot(np.reshape(self.y[self.layer-2],[-1,1]),np.transpose(temp_delta)) self.db[self.layer-2][:] = np.transpose(temp_delta) for itrn in range(self.layer-2, 0, -1): sigma_temp_delta = np.dot(self.W[itrn],temp_delta) temp_delta = sigma_temp_delta*np.reshape(self.d_sigmoid_v[itrn],[-1,1]) self.dW[itrn-1][:] = np.dot(np.reshape(self.y[itrn-1], [-1,1]), np.transpose(temp_delta)) self.db[itrn-1][:] = np.transpose(temp_delta) def data_feed(self, data, dest, eta): NDT = len(data) for itrn in range(NDT): self.forward(data[itrn]) self.back_forward(dest[itrn]) for itrn in range(self.layer-1): self.W[itrn][:] = self.W[itrn] + eta*self.dW[itrn] self.b[itrn][:] = self.b[itrn] + eta*self.db[itrn]
彩蛋
重磅 Chat
《一場 Chat 讓你搞清 BAT 程序員的技術職級》
分享人:
勝洪宇,一線互聯網公司前端技術組長,掘金簽約作者,前端博客博主,所講課程幫助超過20萬前端小夥伴學習。
Chat簡介:
很多程序員嚮往進入 BAT 這樣的大型互聯網公司,但是又不知道他們如何評定技術職級。
阿里集團薪資職級如何劃分?讓你快速得到馬雲的青睞。
在百度明白這些,你將快速晉陞。
騰訊職級里的小秘密,這樣工作你會更強。
一場 Chat 讓你搞清 BAT 的技術評價體系,為您進入超級互聯網公司指明技術方向,時刻做好準備!如果您希望您的技術團隊也像這些互聯網巨頭一樣強大,本場 Chat 我將幫您馬上模仿建立有效的技術職級體系。
想要免費參與本場 Chat ?
※比特幣和區塊鏈基礎
※webpack 從入門到工程實踐
※背後那點事兒、職場 PPT 設計
※Web 安全 PHP 代碼審查之常規漏洞
TAG:謝工的GitChat |