機器學習算法 原理、實現與實踐 —— 感知機與梯度下降
一、前言
1,什么是神經網絡?
人工神經網絡(ANN)又稱神經網絡(NN),它是一種受生物學啟發而產生的一種模擬人腦的學習系統。它通過相互連結的結點構成一個復雜的網絡結構,每一個結點都具有多個輸入和一個輸出,並且該結點與其他結點以一個權重因子相連在一起。通俗來說,神經網絡是一種學習器,給它一組輸入,它會得到一組輸出,神經網絡里的結點相互連結決定了輸入的數據在里面經過怎樣的計算。我們可以通過大量的輸入,讓神經網絡調整它自身的連接情況從而總是能夠得到我們預期的輸出。
2,神經網絡能干什么嗎?
神經網絡對於逼近實數值、離散值或向量值的目標函數提供了一種健壯性很強的方法,現在已經成功應用到很多領域,例如視覺場景分析、手寫字符識別、語音識別、人臉識別等。它可以適用在任何實例是“屬性-值”$Feature \to Value$的情況中,需要學習的目標函數是定義在可以用向量描述的實例上,向量由預先定義的特征組成,比如字符識別,那么特征可以是圖像中的每個像素亮度值。
現代計算機識別的過程通常都可以描述為這樣一個過程:對象 $\to$特征$\to$類別,理論上都可以用神經網絡來解決。
本篇文章將是人工神經網絡這個主題的第一篇文章,主要從最簡單的神經網絡結構入手,介紹一些基本算法原理。后面會陸續涉及到多層神經網絡的原理及其C++實現,同時隨着深度學習的提出,卷積神經網絡也漸漸走進人們的視野,它在圖像識別上表現非常不錯,這個主題也將在后面的文章中有全面的介紹。
二、感知器
一個感知器的結構圖如下:
稍后我們會知道感知器實際上是神經網絡結構中的一個神經元,那么一個感知器就夠成了最簡單的神經網絡系統(雖然還算不上是網絡)。感知器是以一組實數向量作為輸入,計算這些輸入的線性組合,如果結果大於某個閾值就輸出1,否則就輸出-1。如果我們用公式表達,則假設輸入為$x_1$到$x_n$,那么感知器可以表示為一個函數:
$$ o(x)=\left\{
\begin{aligned}
1&\ if \ \ \ w_0+x_1w_1+\cdots+x_nw_n>0 \\
-1&\ otherwise
\end{aligned}
\right.
$$
我們把$-w_0$看作是上面提到的閾值,$w_1$至$w_n$為一組權值。$o(x)$就是將輸入向量按一組權重進行線性加權求和后做的一個符號函數。
我們可以把感知器看成$n$維實例空間(點空間)中的超平面的決策面,平面一側的所有實例輸出1,對另一側的實例輸出-1,這個決策超平面的方程是$\vec{w} \cdot \vec{x}=0$。
針對於我們在數據分類的應用時,就是將我們提供的所有樣本數據分為2類,對於其中一類樣本,感知器總是輸出1,而另一類總是輸出-1。但是對於任意樣本總能找出這個超平面嗎,或者是說是找出一組這樣的權值向量嗎?答案顯然是否定的,只有線性可分的空間可以找到超平面,或者說可以找出一組權值。
那么我們怎么利用感知器呢?或者說我們的目標是什么?
我們希望找到一組這樣的權值,對於我們輸入的每一組向量,總是能夠得到一個我們期望的值。但是上面的感知器功能顯然不夠,它只能得到2個結果,即1和-1。
在實際的模式分類的應用中,樣本空間往往並不是線性的,即使是2維數據的集合也可能不是線性可分的,比如下面這張圖:
而用這樣的感知器結點來構建神經網絡顯然是不行的,因為線性單元連結在一起得到的仍然是線性單元,我們需要的是一種非線性映射,於是就產生了激活函數這個概念。激活函數是一種非線性函數同時是可微函數(可以求導數),為什么要可微呢,因為我們需要知道權重是怎么影響最終輸出的,我們要根據輸出來調節那些權重向量,也就是后面講到的梯度下降法則。
激活函數有很多種,關於激活函數的種類這里不准備介紹太多,只要知道我們選用的是S型激活函數,它將整個一維空間映射到[0,1]或[-1,1]。下面是S型$sigmoid$函數和它的導數:
$$
f(x)=\frac{1}{1+e^{-\alpha x}}(0<f(x)<1)
$$
$$
f'(x)=\frac{\alpha e^{-\alpha x}}{(1+e^{-\alpha x})^2}=\alpha f(x)[1-f(x)]
$$
經過這樣的非線性映射,我們的感知器(現在應該叫SIMGOID單元)就變成了下面這種結構:
上面結構中$w_0$我們習慣稱它為偏置,相當於我們多了一個$x_0=1$的輸入。
對於上面這種結構,我們可以有如下結論:
1)對於任意一組輸入和一個我們預想的在[0,1]之間的輸出,我們總可以找到一組$\vec{w}$使得。
2)對於很多組這樣的輸入樣本,我們可以通過不斷的調整權值,來讓它們的輸出接近於我們預想的輸出。
下面我們該考慮,如何求得這樣的一組權值向量。
三、反向傳播算法
我們需要在向量空間中搜索最合適的權值向量,但是我們不能盲目的搜索,需要有一定的規則指導我們的搜索,那么梯度下降就是很有用的方法。首先我們來定義輸出誤差,即對於任意一組權值向量,那它得到的輸出和我們預想的輸出之間的誤差值。
定義誤差的方法很多,不同的誤差計算方法可以得到不同的權值更新法則,這里我們先用這樣的定義:
$$
E(\vec{w})=\frac{1}{2}\sum_{d \in D}(t_d-o_d)^2
$$
上面公式中$D$代表了所有的輸入實例,或者說是樣本,$d$代表了一個樣本實例,$o_d$表示感知器的輸出,$t_d$代表我們預想的輸出。
這樣,我們的目標就明確了,就是想找到一組權值讓這個誤差的值最小,顯然我們用誤差對權值求導將是一個很好的選擇,導數的意義是提供了一個方向,沿着這個方向改變權值,將會讓總的誤差變大,更形象的叫它為梯度。
$$
\nabla E(w_i)=\frac{\partial E}{\partial w}=\frac{1}{2}\frac{\partial \sum_{d \in D}(t_d-o_d)^2}{\partial w_i}=\frac{1}{2}\sum_{d \in D}\frac{\partial (t_d-o_d)^2}{\partial w_i}
$$
既然梯度確定了E最陡峭的上升的方向,那么梯度下降的訓練法則是:
$$
\vec{w_i} \gets \vec{w_i}+\Delta \vec{w_i},其中\Delta \vec{w_i}=-\eta \frac{\partial E}{\partial w_i}
$$
梯度下降是一種重要最優化算法,但在應用它的時候通常會有兩個問題:
1)有時收斂過程可能非常慢;
2)如果誤差曲面上有多個局極小值,那么不能保證這個過程會找到全局最小值。
為了解決上面的問題,實際中我們應用的是梯度下降的一種變體被稱為隨機梯度下降。上面公式中的誤差是針對於所有訓練樣本而得到的,而隨機梯度下降的思想是根據每個單獨的訓練樣本來更新權值,這樣我們上面的梯度公式就變成了:
$$
\frac{\partial E}{\partial w_i}=\frac{1}{2}\frac{\partial (t-o)^2}{\partial w_i}=-(t-o)\frac{\partial o}{\partial w_i}
$$
經過推導后,我們就可以得到最終的權值更新的公式:
$$
w_i=w_i+\Delta w_i \\
\delta=(t-o)o(1-o) \\
\Delta w_i=\eta \delta x_i
$$
有了上面權重的更新公式后,我們就可以通過輸入大量的實例樣本,來根據我們預期的結果不斷地調整權值,從而最終得到一組權值使得我們的SIGMOID能夠對一個新的樣本輸入得到正確的或無限接近的結果。
四、實例說明
上面我們已經介紹了經過基本的感知器,我們構造了一種SIGMOID單元,可以對“輸入向量-值”這種模式的數據進行目標函數的逼近,但是這畢竟只是單個神經元,它逼近不了太復雜的映射關系,我們需要構造一個多層的神經網絡結構來解決更一般的學習與分類問題。
下面我們通過一個簡單的逼近實例來說明單個SIMGOID單元的工作原理。
首先,我們假設我們的輸入是一個4維的向量$x=[x_1,x_2,x_3,x_4]$,其中$x_i$的值為0或者1。為了簡單其見,我們只設計了下面4種樣本。
$$
x_1=[1,0 , 0,0]\\
x_2=[0,1 , 0,0]\\
x_3=[0,0 , 1,0]\\
x_4=[0,0 , 0,1]\\
$$
對於這4類樣本,我們希望它們得到4種不同的結果以說明它們屬於哪一種,也就是我們的目標輸出是一個標號,像下面這樣:
$$
x_1 \to 1 \\
x_2 \to 2 \\
x_3 \to 3 \\
x_4 \to 4 \\
$$
上面的樣本只是4種,我們可以讓每一種樣本重復來構建大量的樣本實例。比如實際采集到的樣本可能會有所浮動,比如與$x_1$同類的樣本可能采集到的數據是這樣的$x=[0.993,0.002 , 0.0012,-0.019]$,所以我們可以用很小的隨機數來模擬大量的樣本輸入。
因為我們的SIMGOID單元輸出值只可能是$[0,1]$,所以我們可以將我們的類別標號歸一化為$[1,2,3,4]/4$,下面我們用C++來模擬這一過程。
1,樣本獲取:
1 void SampleNN::getSamplesData() 2 { 3 const int iterations = 15000; // 15000個樣本 4 for (int i = 0; i < iterations; i++) 5 { 6 int index = i % 4; 7 vector<double> dvect(4, 0); 8 dvect[index] = 1; 9 for (size_t i = 0; i != dvect.size(); i++) 10 { 11 dvect[i] += (5e-3*rand() / RAND_MAX - 2.5e-3); 12 } 13 inputData.push_back(dvect); 14 } 15 }
2,用[0,0.05]之間的隨機值初始化權重。
1 void SampleNN::intialWgt() 2 { 3 // 4個連結和一個偏置w0 4 for (int i = 0; i != 5; i++) 5 { 6 weight.push_back(0.05*rand()/RAND_MAX); 7 } 8 }
3,向前計算
1 void SampleNN::cmtForward(const vector<double>& inVect) 2 { 3 double dsum = weight[4];//先把偏置加上 4 for (size_t i = 0; i != inVect.size(); i++) 5 { 6 dsum += (inVect[i] * weight[i]); 7 } 8 actual_output = 1 / (1 + exp(-1*dsum)); 9 }
4,更新權重
1 void SampleNN::updataWgt(const vector<double>& inVect,const double true_output) 2 { 3 double learnRate = 0.05; // 權重更新參數 4 for (size_t i = 0; i != weight.size() - 1; i++) 5 { 6 weight[i] += (learnRate*(true_output - actual_output)*actual_output*(1 - actual_output)*inVect[i]); 7 } 8 // w0單獨計算 9 weight[4] += (learnRate*(true_output - actual_output)*actual_output*(1 - actual_output)*1); 10 }
下面是經過15000次迭代后得到的結果:
從上面結果可以看出,輸出的值基本收斂於0.25、0.5、0.75與0.96,說明已經可以用來分類的了。
五、結束語
經過上面的討論,單個神經元的功能及其原理應該可以清楚的了解,那么下一步,我們將用這些單個的神經元(SIGMOID單元)相互連結組成一個網狀結構形成神經網絡,並用來做更一些有意義的識別,這些內容將在下一篇文章中詳細描述。