神經網路入門之隱藏層設計
隱藏層
這部分教程將介紹三部分:
- 隱藏層設計
- 非線性啟用函式
- BP演算法
在前面幾個教程中,我們已經介紹了一些很簡單的教程,就是單一的迴歸模型或者分類模型。在這個教程中,我們也將設計一個二分類神經網路模型,其中輸入資料是一個維度,隱藏層只有一個神經元,並且使用非線性函式作為啟用函式,模型結構能用圖表示為:
我們先匯入教程需要使用的軟體包。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import colorConverter, ListedColormap
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
定義資料集
在這篇教程中,我們將輸入資料x
分類成兩個類別,用藍色表示t
= 1
,用紅色表示t = 0
。其中,紅色分類樣本是一個多峰分佈,被藍色分類樣本包圍。這些資料都是一維的,但是資料之間的間隔並不是線性的分割。這些資料特性將在下圖中表示出來。
這個二分類模型不會完全準確的分類處理啊,因為我們在其中加入了一個神經元,並且採用的是非線性函式。
# Define and generate the samples
nb_of_samples_per_class = 20 # The number of sample in each class
blue_mean = [0] # The mean of the blue class
red_left_mean = [-2] # The mean of the red class
red_right_mean = [2] # The mean of the red class
std_dev = 0.5 # standard deviation of both classes
# Generate samples from both classes
x_blue = np.random.randn(nb_of_samples_per_class, 1) * std_dev + blue_mean
x_red_left = np.random.randn(nb_of_samples_per_class/2, 1) * std_dev + red_left_mean
x_red_right = np.random.randn(nb_of_samples_per_class/2, 1) * std_dev + red_right_mean
# Merge samples in set of input variables x, and corresponding set of
# output variables t
x = np.vstack((x_blue, x_red_left, x_red_right))
t = np.vstack((np.ones((x_blue.shape[0],1)),
np.zeros((x_red_left.shape[0],1)),
np.zeros((x_red_right.shape[0], 1))))
# Plot samples from both classes as lines on a 1D space
plt.figure(figsize=(8,0.5))
plt.xlim(-3,3)
plt.ylim(-1,1)
# Plot samples
plt.plot(x_blue, np.zeros_like(x_blue), 'b|', ms = 30)
plt.plot(x_red_left, np.zeros_like(x_red_left), 'r|', ms = 30)
plt.plot(x_red_right, np.zeros_like(x_red_right), 'r|', ms = 30)
plt.gca().axes.get_yaxis().set_visible(False)
plt.title('Input samples from the blue and red class')
plt.xlabel('$x$', fontsize=15)
plt.show()
輸入樣本
非線性啟用函式
在這裡,我們使用的非線性轉換函式是Gaussian radial
basis function (RBF)。除了徑向基函式網路,RBF函式在神經網路中不經常被作為啟用函式。比較常見的啟用函式是sigmoid函式。但我們根據設計的輸入資料x
,在這裡RBF函式能很好地將藍色樣本資料從紅色樣本資料中分類出來,下圖畫出了RBF函式的影象。RBF函式給定義為:
RBF函式
RBF函式的導數為定義為:
RBF函式的導數
# Define the rbf function
def rbf(z):
return np.exp(-z**2)
# Plot the rbf function
z = np.linspace(-6,6,100)
plt.plot(z, rbf(z), 'b-')
plt.xlabel('$z$', fontsize=15)
plt.ylabel('$e^{-z^2}$', fontsize=15)
plt.title('RBF function')
plt.grid()
plt.show()
RBF函式視覺化
BP演算法
在訓練模型的時候,我們使用BP演算法來進行模型優化,這是一種很典型的優化演算法。BP演算法的每次迭代分為兩步:
- 正向傳播去計算神經網路的輸出。
- 利用神經網路得出的結果和真實結果之間的誤差進行反向傳播來更新神經網路的引數。
1. 正向傳播
在計算正向傳播中,輸入資料被一層一層的計算,最後從模型中得出輸出結果。
計算隱藏層的啟用函式
隱藏層h
經啟用函式之後,輸出結果為:
啟用函式
其中,wh
是權重引數。hidden_activations(x,
wh)
函式實現了該功能。
計算輸出結果的啟用函式
神經網路的最後一層的輸出,是將隱藏層的輸出h
作為資料引數,並且利用Logistic
函式來作為啟用函式。
輸出結果的啟用函式
其中,w0
是輸出層的權重,output_activations(h,
w0)
函式實現了該功能。我們在公式中添加了一個偏差項-1
,因為如果不新增偏差項,那麼Logistic函式只能學到一個經過原點的分類面。因為,隱藏層中的RBF函式的輸入值得範圍是從零到正無窮,那麼如果我們不在輸出層加上偏差項的話,模型不可能學出有用的分類結果,因為沒有樣本的值將小於0
,從而歸為決策樹的左邊。因此,我們增加了一個截距,即偏差項。正常情況下,偏差項也和權重引數一樣,需要被訓練,但是由於這個例子中的模型非常簡單,所以我們就用一個常數來作為偏差項。
# Define the logistic function
def logistic(z):
return 1 / (1 + np.exp(-z))
# Function to compute the hidden activations
def hidden_activations(x, wh):
return rbf(x * wh)
# Define output layer feedforward
def output_activations(h , wo):
return logistic(h * wo - 1)
# Define the neural network function
def nn(x, wh, wo):
return output_activations(hidden_activations(x, wh), wo)
# Define the neural network prediction function that only returns
# 1 or 0 depending on the predicted class
def nn_predict(x, wh, wo):
return np.around(nn(x, wh, wo))
2. 反向傳播
在反向傳播過程中,我們需要先計算出神經網路的輸出與真實值之間的誤差。這個誤差會一層一層的反向傳播去更新神經網路中的各個權重。
在每一層中,使用梯度下降演算法按照負梯度方向對每個引數進行更新。
引數wh
和wo
利用w(k+1)=w(k)−Δw(k+1)
更新,其中Δw=μ∗∂ξ/∂w
,μ
是學習率,∂ξ/∂w
是損失函式ξ
對引數w
的梯度。
計算損失函式
在這個模型中,損失函式ξ
與交叉熵損失函式一樣,具體解釋在這裡:
損失函式
損失函式對於引數wh
和wo
的表示如下圖所示。從圖中,我們發現誤差面不是一個凸函式,而且沿著wh
= 0
這一軸,引數wh
將是損失函式的一個對映。
從圖中發現,沿著wh = 0
,從wo
> 0
開始,損失函式有一個非常陡峭的梯度,並且我們要按照圖形的下邊緣進行梯度下降。如果學習率取得過大,那麼在梯度更新的時候,可能跳過最小值,從一邊的梯度方向跳到另一邊的梯度方向。因為梯度的方向太陡峭了,每次對引數的更新跨度將會非常大。因此,在開始的時候我們需要將學習率取一個比較小的值。
# Define the cost function
def cost(y, t):
return - np.sum(np.multiply(t, np.log(y)) + np.multiply((1-t), np.log(1-y)))
# Define a function to calculate the cost for a given set of parameters
def cost_for_param(x, wh, wo, t):
return cost(nn(x, wh, wo) , t)
# Plot the cost in function of the weights
# Define a vector of weights for which we want to plot the cost
nb_of_ws = 200 # compute the cost nb_of_ws times in each dimension
wsh = np.linspace(-10, 10, num=nb_of_ws) # hidden weights
wso = np.linspace(-10, 10, num=nb_of_ws) # output weights
ws_x, ws_y = np.meshgrid(wsh, wso) # generate grid
cost_ws = np.zeros((nb_of_ws, nb_of_ws)) # initialize cost matrix
# Fill the cost matrix for each combination of weights
for i in range(nb_of_ws):
for j in range(nb_of_ws):
cost_ws[i,j] = cost(nn(x, ws_x[i,j], ws_y[i,j]) , t)
# Plot the cost function surface
fig = plt.figure()
ax = Axes3D(fig)
# plot the surface
surf = ax.plot_surface(ws_x, ws_y, cost_ws, linewidth=0, cmap=cm.pink)
ax.view_init(elev=60, azim=-30)
cbar = fig.colorbar(surf)
ax.set_xlabel('$w_h$', fontsize=15)
ax.set_ylabel('$w_o$', fontsize=15)
ax.set_zlabel('$\\xi$', fontsize=15)
cbar.ax.set_ylabel('$\\xi$', fontsize=15)
plt.title('Cost function surface')
plt.grid()
plt.show()
損失函式誤差表面
輸出層更新
∂ξi/∂wo
是每個樣本i
的輸出梯度,參照第二部分教程的方法,我們可以得出相應的推導公式:
梯度推導
其中,zoi=hi∗wo
,hi
是樣本i
經過啟用函式之後輸出的值,∂ξi/∂zoi=δoi
是輸出層誤差的求導。
gradient_output(y, t)
函式實現了δo
,gradient_weight_out(h,
grad_output)
函式實現了∂ξ/∂wo
。
隱藏層更新
∂ξi/∂wh
是每個樣本i
在影藏層的梯度,具體計算如下:
隱藏層梯度推導
其中,
∂ξi/∂zhi=δhi
表示誤差對於隱藏層輸入的梯度。這個誤差也可以解釋為,zhi
對於最後誤差的貢獻。那麼,接下來我們定義一下這個誤差梯度δhi
:
誤差梯度
又應為∂zhi/∂wh=xi
,那麼我們能計算最後的值為:
梯度計算
在批處理中,對每個對應引數的梯度進行累加,就是最後的梯度。
gradient_hidden(wo, grad_output)
函式實現了δh
。gradient_weight_hidden(x, zh, h, grad_hidden)
函式實現了∂ξ/∂wh
。backprop_update(x, t, wh, wo, learning_rate)
函式實現了BP演算法的每次迭代過程。
# Define the error function
def gradient_output(y, t):
return y - t
# Define the gradient function for the weight parameter at the output layer
def gradient_weight_out(h, grad_output):
return h * grad_output
# Define the gradient function for the hidden layer
def gradient_hidden(wo, grad_output):
return wo * grad_output
# Define the gradient function for the weight parameter at the hidden layer
def gradient_weight_hidden(x, zh, h, grad_hidden):
return x * -2 * zh * h * grad_hidden
# Define the update function to update the network parameters over 1 iteration
def backprop_update(x, t, wh, wo, learning_rate):
# Compute the output of the network
# This can be done with y = nn(x, wh, wo), but we need the intermediate
# h and zh for the weight updates.
zh = x * wh
h = rbf(zh) # hidden_activations(x, wh)
y = output_activations(h, wo)
# Compute the gradient at the output
grad_output = gradient_output(y, t)
# Get the delta for wo
d_wo = learning_rate * gradient_weight_out(h, grad_output)
# Compute the gradient at the hidden layer
grad_hidden = gradient_hidden(wo, grad_output)
# Get the delta for wh
d_wh = learning_rate * gradient_weight_hidden(x, zh, h, grad_hidden)
# return the update parameters
return (wh-d_wh.sum(), wo-d_wo.sum())
BP演算法更新
下面的程式碼,我們模擬了一個50次的迴圈。白色的點表示,引數wh
和wo
在誤差面上面的第k
次迭代。
在更新過程中,我們不斷的線性減小學習率。這是為了在更新到最後的時候,學習率能是0。這樣能保證最後的引數更新不會在最小值附近徘徊。
# Run backpropagation
# Set the initial weight parameter
wh = 2
wo = -5
# Set the learning rate
learning_rate = 0.2
# Start the gradient descent updates and plot the iterations
nb_of_iterations = 50 # number of gradient descent updates
lr_update = learning_rate / nb_of_iterations # learning rate update rule
w_cost_iter = [(wh, wo, cost_for_param(x, wh, wo, t))] # List to store the weight values over the iterations
for i in range(nb_of_iterations):
learning_rate -= lr_update # decrease the learning rate
# Update the weights via backpropagation
wh, wo = backprop_update(x, t, wh, wo, learning_rate)
w_cost_iter.append((wh, wo, cost_for_param(x, wh, wo, t))) # Store the values for plotting
# Print the final cost
print('final cost is {:.2f} for weights wh: {:.2f} and wo: {:.2f}'.format(cost_for_param(x, wh, wo, t), wh, wo))
在我們的機器上面,最後輸出的結果是:
final cost is 10.81 for weights wh: 1.20 and wo: 5.56
但由於引數初始化的不同,可能在你的機器上面執行會有不同的結果。
# Plot the weight updates on the error surface
# Plot the error surface
fig = plt.figure()
ax = Axes3D(fig)
surf = ax.plot_surface(ws_x, ws_y, cost_ws, linewidth=0, cmap=cm.pink)
ax.view_init(elev=60, azim=-30)
cbar = fig.colorbar(surf)
cbar.ax.set_ylabel('$\\xi$', fontsize=15)
# Plot the updates
for i in range(1, len(w_cost_iter)):
wh1, wo1, c1 = w_cost_iter[i-1]
wh2, wo2, c2 = w_cost_iter[i]
# Plot the weight-cost value and the line that represents the update
ax.plot([wh1], [wo1], [c1], 'w+') # Plot the weight cost value
ax.plot([wh1, wh2], [wo1, wo2], [c1, c2], 'w-')
# Plot the last weights
wh1, wo1, c1 = w_cost_iter[len(w_cost_iter)-1]
ax.plot([wh1], [wo1], c1, 'w+')
# Shoz figure
ax.set_xlabel('$w_h$', fontsize=15)
ax.set_ylabel('$w_o$', fontsize=15)
ax.set_zlabel('$\\xi$', fontsize=15)
plt.title('Gradient descent updates on cost surface')
plt.grid()
plt.show()
Gradient descent updates on cost surface
分類結果的視覺化
下面的程式碼可視化了最後的分類結果。在輸入空間域裡面,藍色和紅色代表了最後的分類顏色。從圖中,我們發現所有的樣本都被正確分類了。
# Plot the resulting decision boundary
# Generate a grid over the input space to plot the color of the
# classification at that grid point
nb_of_xs = 100
xs = np.linspace(-3, 3, num=nb_of_xs)
ys = np.linspace(-1, 1, num=nb_of_xs)
xx, yy = np.meshgrid(xs, ys) # create the grid
# Initialize and fill the classification plane
classification_plane = np.zeros((nb_of_xs, nb_of_xs))
for i in range(nb_of_xs):
for j in range(nb_of_xs):
classification_plane[i,j] = nn_predict(xx[i,j], wh, wo)
# Create a color map to show the classification colors of each grid point
cmap = ListedColormap([
colorConverter.to_rgba('r', alpha=0.25),
colorConverter.to_rgba('b', alpha=0.25)])
# Plot the classification plane with decision boundary and input samples
plt.figure(figsize=(8,0.5))
plt.contourf(xx, yy, classification_plane, cmap=cmap)
plt.xlim(-3,3)
plt.ylim(-1,1)
# Plot samples from both classes as lines on a 1D space
plt.plot(x_blue, np.zeros_like(x_blue), 'b|', ms = 30)
plt.plot(x_red_left, np.zeros_like(x_red_left), 'r|', ms = 30)
plt.plot(x_red_right, np.zeros_like(x_red_right), 'r|', ms = 30)
plt.gca().axes.get_yaxis().set_visible(False)
plt.title('Input samples and their classification')
plt.xlabel('x')
plt.show()
分類結果
輸入域的轉換
為什麼神經網路模型能利用最後的線性Logistic實現非線性的分類呢?關鍵原因是隱藏層的非線性RBF函式。RBF轉換函式可以將靠近原點的樣本(藍色分類)的輸出值大於0,而遠離原點的樣本(紅色樣本)的輸出值接近0。如下圖所示,紅色樣本的位置都在左邊接近0的位置,藍色樣本的位置在遠離0的位置。這個結果就是使用線性Logistic分類的。
同時注意,我們使用的高斯函式的峰值偏移量是0,也就是說,高斯函式產生的值是一個關於原點分佈的資料。
# Plot projected samples from both classes as lines on a 1D space
plt.figure(figsize=(8,0.5))
plt.xlim(-0.01,1)
plt.ylim(-1,1)
# Plot projected samples
plt.plot(hidden_activations(x_blue, wh), np.zeros_like(x_blue), 'b|', ms = 30)
plt.plot(hidden_activations(x_red_left, wh), np.zeros_like(x_red_left), 'r|', ms = 30)
plt.plot(hidden_activations(x_red_right, wh), np.zeros_like(x_red_right), 'r|', ms = 30)
plt.gca().axes.get_yaxis().set_visible(False)
plt.title('Projection of the input samples by the hidden layer.')
plt.xlabel('h')
plt.show()
RBF函式產生圖