2017/10/23

深度學習引發之深度偏頭痛




若「機器學習」或「AI」這類的課程能在高中或大一當個基礎前導學科來上,相信學生們比較不會有自己所學是否有用的疑慮了!相反的,若您像筆者一樣回過頭來學習「Deep Learning」之類的深層神經網路,有可能會責怪自己為何當初線性代數、微積分、向量梯度最佳化方法等等沒能把他學好呢?而這些原本以為找不到出口的方法與數學工具,在此它們都被充分地運用上了!



秒懂感知器(Perceptron
假設我們發明一個科普兒童玩具,該裝置包含了兩個閘刀式開關「輸入」(ON會點亮LED燈,反之OFF燈滅)與一個「神經元」以旋鈕式可變電阻連接到兩個輸入端與一個偏權值,其中當電阻調整至最大時(標示為0)相當於斷路,而電阻調整至最小時(標示為1)相當於導通。此時給小朋友一個任務:給定一OR閘的輸入/出特性,試著去調整神經元鍵值的大小(旋鈕式可變電阻)並根據四種可能的輸入(亮燈與否)與該特定的輸出將神經網路訓練成OR閘。




相信不用多久,小朋友就能調整出一個設定值使得該神經網路具有OR閘的運算能力,雖然這個系統可能有無限多種解。同理,該神經元可以被訓練成NOR閘、AND閘與NAND閘。沒錯,小朋友在調整這些神經元鍵值最後找到解的過程就好比如下圖中找到一條線,使得該線能成功將輸出分成兩類(亮燈與否),因此任何一條線都是一組解。而其中偏權值使該線不通過座標原點。


若回到60年前「感知器(Perceptron」被提出來的年代,以當時的時空背景揣摩,或許可以感受到為何線性代數大師們會對其嗤之以鼻吧?假設x為二維座標空間所張成之向量,則w∙x=0,表向量w=[1 1]與向量x=[x1 x2]正交(內積為0),w為其法向量。所有符合w∙x=0.5的解,在x座標空間的展開,為法向量為w且通過點(0.5, 0.5)之直線。



對輸出值套用所謂的活化函數(activate function,例如sigmoid),即可以定義出正/負類別。



注意,此時若沒有sigmoid函數(activate function)對y轉換,我們找到的只是一條線,無法判定類別。wx愈像則其內積為正,反之其內積為負,透過sigmoid我們可以將輸出判定為正類或是負類別,因此該系統與線性規劃是等價的但仍屬於窮舉。



多元感知器可以比擬成:由多個前級神經元電訊號的加總來決定是否激勵下一級神經元的處理過程,如下圖。神經網路理論的發明若是如下圖中由下往上類推的過程,你可能會覺得它並沒甚麼了不起。但若我們說它是從上到下的思考推論過程(說個漂亮的故事),媒體或許就會覺得它很聰明很厲害(把它給神話了)。

(Source: Wikipedia)



此外,眼尖的同學應該很快發現單層的感知器根本無法分類像XOR這種樣本分隔兩地的問題。



這類問題須要層疊感知器來處理(增加hidden layer),相當於多個線性規劃的聯集可以解凸邊形(convex)的問題。因此,透過更深的層疊感知器裏所當然可以處理更複雜的問題,例如樣本間彼此分隔多區域(disjoint convex)的分類問題。



但該系統仍屬於窮舉,因此,在20幾年前的時空背景下被具有非常漂亮數學基底的SVMSupport Vector Machine打趴是不覺得意外的。
  


梯度法Gradient Descent
顧名思義,透過微分取得曲面的斜率並往該方向逐步找到區域最佳解,否則神經網路的學習過程與方式將跟隨機的窮舉法沒甚麼兩樣。

相信很多人在撰寫類神經網路程式碼時會跟筆者一樣,剛開始可能有點不太適應,因為許多運算需要跳脫原本「迴圈計算」的思維模式並進入「向量處理」思維模式的腦。以三維曲面f(x0,x1)=x0^2+x1^2為例,下面Python程式碼可以把它畫出來。

# f(x0,x1)=x0**2+x1**2
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D

x0=np.arange(-3,3.5,0.5)
x1=np.arange(-3,3.5,0.5)
X,Y=np.meshgrid(x0,x1)
Z=X**2+Y**2

ax=plt.axes(projection='3d')
#ax.plot_surface(X,Y,Z,cmap='coolwarm')
#ax.plot_wireframe(X,Y,Z)
ax.plot_trisurf(X.flatten(),Y.flatten(),Z.flatten())

plt.xlabel('x0')
plt.ylabel('x1')
plt.show()




對某參數取偏微分的程式碼很簡單,對某一參數的微量變化取均值,其餘變數以定值帶入求斜率(向量梯度)。



改用向量的寫法如下:

# f(x0,x1)=x0**2+x1**2
def function(V):
    return (V[0]**2+V[1]**2)

def numerical_gradient(f, V):
    h=1e-4
    grad=np.zeros_like(V)
    grad[0]=(f([V[0]+h,V[1]])-f([V[0]-h,V[1]]))/(2*h) # gradient x0
    grad[1]=(f([V[0],V[1]+h])-f([V[0],V[1]-h]))/(2*h) # gradient x1
    return grad

def gradient_descent(f, init_v, lr=0.01, step_num=20):
    v=init_v
    hist=[]
    for i in range(step_num):
        hist.append(v.copy())
        grad=numerical_gradient(f, v)
        v-= lr*grad
    return v,np.array(hist)


之後,我們對該曲面某個範圍內的所有格子點做偏微分以計算其梯度。

# gradient
x0=np.arange(-3,3.5,0.5)
x1=np.arange(-3,3.5,0.5)
X,Y=np.meshgrid(x0,x1)
X=X.flatten()
Y=Y.flatten()

grad=numerical_gradient(function,[X,Y])
plt.quiver(X,Y,-grad[0],-grad[1],angles="xy",color="#666666")
plt.xlabel('x0')
plt.ylabel('x1')
plt.grid()



下圖顯示以梯度法逼近曲面區域最低點(微分為零)的收斂過程:



# grdient descent
init_v=np.array([-3.0,2.7])
r,hist=gradient_descent(function,init_v,lr=0.2)
plt.plot(hist[:,0],hist[:,1],'--o')
plt.show()




更深度的學習
到此,眼尖的同學應該會發現:雖然梯度法給了尋求最佳化過程一個指標與方向性,但仍然沒有比窮舉高明多少?它相當的沒有效率,直到「倒傳遞(backpropagation」理論被提出來。若讀者有興趣可以更深入的參考「史丹佛CS231n」的網路學習課程,我們將主動重新拾起課本,看看甚麼是「chain-rule」?原來「微積分」這麼實用且有趣!