作者:chen_h
微信号 & QQ:862251340
微信公众号:coderpai
简书地址:https://www.jianshu.com/p/1fe8ab3da28c
这篇教程是翻译Peter Roelants写的神经网络教程,作者已经授权翻译,这是原文。
该教程将介绍如何入门神经网络,一共包含五部分。你可以在以下链接找到完整内容。
这部分教程将介绍三部分:
在先前的教程中,我们已经使用学习了一个非常简单的神经网络:一个输入数据,一个隐藏神经元和一个输出结果。在这篇教程中,我们将描述一个稍微复杂一点的神经网络:包括一个二维的输入数据,三维的隐藏神经元和二维的输出结果,并且利用softmax函数来做最后的分类。在之前的网络中,我们都没有添加偏差项,但在这个网络模型中我们加入偏差项。网络模型如下:
我们先导入教程需要使用的软件包。
import numpy as np
import sklearn.datasets
import matplotlib.pyplot as plt
from matplotlib.colors import colorConverter, ListedColormap
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
在这个教程中,我们的分类目标还是二分类:红色类别(t=1)和蓝色类别(t=0)。红色样本是一个环形分布,位于环形中间的是蓝色样本。我们利用scikit-learn的make_circles
的方法得到数据集。
这是一个二维的数据集,而且不是线性分割。如果我们利用教程2中的方法,那么我们不能正确将它们分类,因为教程2中的方法只能学习出线性分割的样本数据。但我们增加一个隐藏层,就能学习这个非线性分割了。
我们有N
个输入数据,每个数据有两个特征,那么我们可以得到矩阵X
如下:
其中,xij
表示第i
个样本的第j
个特征值。
经过softmax函数之后,该模型输出的最终结果T
为:
其中,当且仅当第i
个样本属于类别j
时,tij=1
。因此,我们定义蓝色样本的标记是T = [0 1]
,红色样本的标记是T = [1 0]
。
# Generate the dataset
X, t = sklearn.datasets.make_circles(n_samples=100, shuffle=False, factor=0.3, noise=0.1)
T = np.zeros((100,2)) # Define target matrix
T[t==1,1] = 1
T[t==0,0] = 1
# Separate the red and blue points for plotting
x_red = X[t==0]
x_blue = X[t==1]
print('shape of X: {}'.format(X.shape))
print('shape of T: {}'.format(T.shape))
shape of X: (100, 2)
shape of T: (100, 2)
# Plot both classes on the x1, x2 plane
plt.plot(x_red[:,0], x_red[:,1], 'ro', label='class red')
plt.plot(x_blue[:,0], x_blue[:,1], 'bo', label='class blue')
plt.grid()
plt.legend(loc=1)
plt.xlabel('$x_1$', fontsize=15)
plt.ylabel('$x_2$', fontsize=15)
plt.axis([-1.5, 1.5, -1.5, 1.5])
plt.title('red vs blue classes in the input space')
plt.show()
将二维的输入样本X
转换成三维的输入层H
,我们需要使用链接矩阵Wh
(Whij
表示第i
个输入特征和第j
个隐藏层神经元相连的权重)和偏差向量bh
:
之后计算结果如下:
其中,σ
是一个Logistic函数,H
是一个N*3
的输出矩阵。
整个计算流过程如下图所示。每个输入特征xij
和权重参数whj1
、whj2
、whj3
相乘。最后相乘的每行k(xij*whjk)
结果进行累加得到zik
,之后经过Logistic函数σ
进行非线性激活得到hik
。注意,偏差项Bh
可以被整合在链接矩阵Wh
中,只需要通过在输入参数xi
中增加一个+1
项。
在代码中,Bh
和Wh
使用bh
和Wh
表示。hidden_activations(X, Wh, bh)
函数实现了隐藏层的激活输出。
在计算输出层的激活结果之前,我们需要先定义3*2
的链接矩阵Wo
(Woij
表示隐藏层第i
个神经元和输出层第j
个输出单元的链接权重)和2*1
的偏差项矩阵bo
:
之后计算结果如下:
其中,ς
表示softmax函数。Y
表示最后输出的n*2
的矩阵,Zod
表示矩阵Zo
的第d
列,Wod
表示矩阵Wo
的第d
列,bod
表示向量bo
的第d
个元素。
在代码中,bo
和Wo
用变量bo
和Wo
来表示。output_activations(H, Wo, bo)
函数实现了输出层的激活结果。
# Define the logistic function
def logistic(z):
return 1 / (1 + np.exp(-z))
# Define the softmax function
def softmax(z):
return np.exp(z) / np.sum(np.exp(z), axis=1, keepdims=True)
# Function to compute the hidden activations
def hidden_activations(X, Wh, bh):
return logistic(X.dot(Wh) + bh)
# Define output layer feedforward
def output_activations(H, Wo, bo):
return softmax(H.dot(Wo) + bo)
# Define the neural network function
def nn(X, Wh, bh, Wo, bo):
return output_activations(hidden_activations(X, Wh, bh), Wo, bo)
# Define the neural network prediction function that only returns
# 1 or 0 depending on the predicted class
def nn_predict(X, Wh, bh, Wo, bo):
return np.around(nn(X, Wh, bh, Wo, bo))
最后一层的输出层使用的是softmax函数,该函数的交叉熵损失函数在这篇教程中已经有了很详细的描述。如果需要对N
个样本进行C
个分类,那么它的损失函数ξ
是:
损失函数的误差梯度δo
可以非常方便得到:
其中,Zo(Zo=H⋅Wo+bo)
是一个n*2
的矩阵,T
是一个n*2
的目标矩阵,Y
是一个经过模型得到的n*2
的输出矩阵。因此,δo
也是一个n*2
的矩阵。
在代码中,δo
用Eo
表示,error_output(Y, T)
函数实现了该方法。
对于N
个样本,对输出层的梯度δwoj
是通过∂ξ/∂woj
计算的,具体计算如下:
其中,woj
表示Wo
的第j
行,即是一个1*2
的向量。因此,我们可以将上式改写成一个矩阵操作,即:
最后梯度的结果是一个3*2
的Jacobian矩阵,如下:
在代码中,JWo
表示上面的Jwo
。gradient_weight_out(H, Eo)
函数实现了上面的操作。
对于偏差项bo
可以采用相同的方式进行更新。对于批处理的N
个样本,对输出层的梯度∂ξ/∂bo
的计算如下:
最后梯度的结果是一个2*1
的Jacobian矩阵,如下:
在代码中,Jbo
表示上面的Jbo
。gradient_bias_out(Eo)
函数实现了上面的操作。
# Define the cost function
def cost(Y, T):
return - np.multiply(T, np.log(Y)).sum()
# Define the error function at the output
def error_output(Y, T):
return Y - T
# Define the gradient function for the weight parameters at the output layer
def gradient_weight_out(H, Eo):
return H.T.dot(Eo)
# Define the gradient function for the bias parameters at the output layer
def gradient_bias_out(Eo):
return np.sum(Eo, axis=0, keepdims=True)
在隐藏层上面的误差函数的梯度δh
可以被定义如下:
其中,Zh
是一个n*3
的输入到Logistic函数中的输入矩阵,即:
其中,δh
也是一个N*3
的矩阵。
接下来,对于每个样本i
和每个神经元j
,我们计算误差梯度δhij
的导数。通过前面一层的误差反馈,通过BP算法我们可以计算如下:
其中,woj
表示Wo
的第j
行,它是一个1*2
的向量。δoi
也是一个1*2
的向量。因此,维度是N*3
的误差矩阵δh
可以被如下计算:
其中,∘
表示逐点乘积
在代码中,Eh
表示上面的δh
。error_hidden(H, Wo, Eo)
函数实现了上面的函数。
在N
个样本中,隐藏层的梯度∂ξ/∂whj
可以被如下计算:
其中,whj
表示Wh
的第j
行,它是一个1*3
的向量。我们可以将上面的式子写成矩阵相乘的形式如下:
梯度最后的结果是一个2*3
的Jacobian矩阵,如下:
在代码中,JWh
表示Jwh
。gradient_weight_hidden(X, Eh)
函数实现了上面的操作。
偏差项bh
可以按照相同的方式进行更新操作。在N
个样本上面,梯度∂ξ/∂bh
可以如下计算:
最后的梯度结果是一个1*3
的Jacobian矩阵,如下:
在代码中,Jbh
表示Jbh
。gradient_bias_hidden(Eh)
函数实现了上面的方法。
# Define the error function at the hidden layer
def error_hidden(H, Wo, Eo):
# H * (1-H) * (E . Wo^T)
return np.multiply(np.multiply(H,(1 - H)), Eo.dot(Wo.T))
# Define the gradient function for the weight parameters at the hidden layer
def gradient_weight_hidden(X, Eh):
return X.T.dot(Eh)
# Define the gradient function for the bias parameters at the output layer
def gradient_bias_hidden(Eh):
return np.sum(Eh, axis=0, keepdims=True)
在编程计算反向传播梯度时,很容易产生错误。这就是为什么一直推荐在你的模型中一定要进行梯度检查。梯度检查是通过对于每一个参数进行梯度数值计算进行的,即检查这个数值与通过反向传播的梯度进行比较计算。
假设对于参数θi
,我们计算它的数值梯度∂ξ/∂θi
如下:
其中,f
是一个神经网络的方程,X
是输入数据,θ
表示所有参数的集合。ϵ
是一个很小的值,用来对参数θi
进行评估。
对于每个参数的数值梯度应该接近于反向传播梯度的参数。
# Initialize weights and biases
init_var = 1
# Initialize hidden layer parameters
bh = np.random.randn(1, 3) * init_var
Wh = np.random.randn(2, 3) * init_var
# Initialize output layer parameters
bo = np.random.randn(1, 2) * init_var
Wo = np.random.randn(3, 2) * init_var
# Compute the gradients by backpropagation
# Compute the activations of the layers
H = hidden_activations(X, Wh, bh)
Y = output_activations(H, Wo, bo)
# Compute the gradients of the output layer
Eo = error_output(Y, T)
JWo = gradient_weight_out(H, Eo)
Jbo = gradient_bias_out(Eo)
# Compute the gradients of the hidden layer
Eh = error_hidden(H, Wo, Eo)
JWh = gradient_weight_hidden(X, Eh)
Jbh = gradient_bias_hidden(Eh)
# Combine all parameter matrices in a list
params = [Wh, bh, Wo, bo]
# Combine all parameter gradients in a list
grad_params = [JWh, Jbh, JWo, Jbo]
# Set the small change to compute the numerical gradient
eps = 0.0001
# Check each parameter matrix
for p_idx in range(len(params)):
# Check each parameter in each parameter matrix
for row in range(params[p_idx].shape[0]):
for col in range(params[p_idx].shape[1]):
# Copy the parameter matrix and change the current parameter slightly
p_matrix_min = params[p_idx].copy()
p_matrix_min[row,col] -= eps
p_matrix_plus = params[p_idx].copy()
p_matrix_plus[row,col] += eps
# Copy the parameter list, and change the updated parameter matrix
params_min = params[:]
params_min[p_idx] = p_matrix_min
params_plus = params[:]
params_plus[p_idx] = p_matrix_plus
# Compute the numerical gradient
grad_num = (cost(nn(X, *params_plus), T)-cost(nn(X, *params_min), T))/(2*eps)
# Raise error if the numerical grade is not close to the backprop gradient
if not np.isclose(grad_num, grad_params[p_idx][row,col]):
raise ValueError('Numerical gradient of {:.6f} is not close to the backpropagation gradient of {:.6f}!'.format(float(grad_num), float(grad_params[p_idx][row,col])))
print('No gradient errors found')
No gradient errors found
在前面几个例子中,我们使用最简单的梯度下降算法,根据损失函数来优化参数,因为这些损失函数都是凸函数。但是在多层神经网络,参数量非常巨大并且激活函数是非线性函数时,我们的损失函数极不可能是一个凸函数。那么此时,简单的梯度下降算法不是最好的方法去找到一个全局的最小值,因为这个方法是一个局部优化的方法,最后将收敛在一个局部最小值。
为了解决这个例子中的问题,我们使用一种梯度下降算法的改进版,叫做动量方法。这种动量方法,你可以想象成一只球从损失函数的表面从高处落下。在下降的过程中,这个球的速度会增加,但是当它上坡时,它的速度会下降,这一个过程可以用下面的数学公式进行描述:
其中,V(i)
表示参数第i
次迭代时的速度。θ(i)
表示参数在第i
次迭代时的值。∂ξ/∂θ(i)
表示参数在第i
次迭代时的梯度。λ
表示速度根据阻力减小的值,μ
表示学习率。这个数学描述公式可以被可视化为如下图:
速度VWh
,Vbh
,VWo
和Vbo
对应于参数Wh
,bh
,Wo
和bo
,在代码中,这些参数用VWh
,Vbh
,VWo
和Vbo
表示,并且被存储在一个列表Vs
中。update_velocity(X, T, ls_of_params, Vs, momentum_term, learning_rate)
函数实现了上面的方法。update_params(ls_of_params, Vs)
函数实现了速度的更新方法。
# Define the update function to update the network parameters over 1 iteration
def backprop_gradients(X, T, Wh, bh, Wo, bo):
# Compute the output of the network
# Compute the activations of the layers
H = hidden_activations(X, Wh, bh)
Y = output_activations(H, Wo, bo)
# Compute the gradients of the output layer
Eo = error_output(Y, T)
JWo = gradient_weight_out(H, Eo)
Jbo = gradient_bias_out(Eo)
# Compute the gradients of the hidden layer
Eh = error_hidden(H, Wo, Eo)
JWh = gradient_weight_hidden(X, Eh)
Jbh = gradient_bias_hidden(Eh)
return [JWh, Jbh, JWo, Jbo]
def update_velocity(X, T, ls_of_params, Vs, momentum_term, learning_rate):
# ls_of_params = [Wh, bh, Wo, bo]
# Js = [JWh, Jbh, JWo, Jbo]
Js = backprop_gradients(X, T, *ls_of_params)
return [momentum_term * V - learning_rate * J for V,J in zip(Vs, Js)]
def update_params(ls_of_params, Vs):
# ls_of_params = [Wh, bh, Wo, bo]
# Vs = [VWh, Vbh, VWo, Vbo]
return [P + V for P,V in zip(ls_of_params, Vs)]
# Run backpropagation
# Initialize weights and biases
init_var = 0.1
# Initialize hidden layer parameters
bh = np.random.randn(1, 3) * init_var
Wh = np.random.randn(2, 3) * init_var
# Initialize output layer parameters
bo = np.random.randn(1, 2) * init_var
Wo = np.random.randn(3, 2) * init_var
# Parameters are already initilized randomly with the gradient checking
# Set the learning rate
learning_rate = 0.02
momentum_term = 0.9
# define the velocities Vs = [VWh, Vbh, VWo, Vbo]
Vs = [np.zeros_like(M) for M in [Wh, bh, Wo, bo]]
# Start the gradient descent updates and plot the iterations
nb_of_iterations = 300 # number of gradient descent updates
lr_update = learning_rate / nb_of_iterations # learning rate update rule
ls_costs = [cost(nn(X, Wh, bh, Wo, bo), T)] # list of cost over the iterations
for i in range(nb_of_iterations):
# Update the velocities and the parameters
Vs = update_velocity(X, T, [Wh, bh, Wo, bo], Vs, momentum_term, learning_rate)
Wh, bh, Wo, bo = update_params([Wh, bh, Wo, bo], Vs)
ls_costs.append(cost(nn(X, Wh, bh, Wo, bo), T))
# Plot the cost over the iterations
plt.plot(ls_costs, 'b-')
plt.xlabel('iteration')
plt.ylabel('$\\xi$', fontsize=15)
plt.title('Decrease of cost over backprop iteration')
plt.grid()
plt.show()
在下图中,我们利用输入数据X
和目标结果T
,利用动量方法对BP算法进行更新得到的分类边界。我们利用红色和蓝色去区分输入数据的分类域。从图中,我们可以看出,我们把所有的样本都正确分类了。
# 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 = 200
xs1 = np.linspace(-2, 2, num=nb_of_xs)
xs2 = np.linspace(-2, 2, num=nb_of_xs)
xx, yy = np.meshgrid(xs1, xs2) # 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):
pred = nn_predict(np.asmatrix([xx[i,j], yy[i,j]]), Wh, bh, Wo, bo)
classification_plane[i,j] = pred[0,0]
# Create a color map to show the classification colors of each grid point
cmap = ListedColormap([
colorConverter.to_rgba('b', alpha=0.30),
colorConverter.to_rgba('r', alpha=0.30)])
# Plot the classification plane with decision boundary and input samples
plt.contourf(xx, yy, classification_plane, cmap=cmap)
# Plot both classes on the x1, x2 plane
plt.plot(x_red[:,0], x_red[:,1], 'ro', label='class red')
plt.plot(x_blue[:,0], x_blue[:,1], 'bo', label='class blue')
plt.grid()
plt.legend(loc=1)
plt.xlabel('$x_1$', fontsize=15)
plt.ylabel('$x_2$', fontsize=15)
plt.axis([-1.5, 1.5, -1.5, 1.5])
plt.title('red vs blue classification boundary')
plt.show()
存在两个理由去解释为什么这个神经网络可以将这个非线性的数据进行分类。第一,因为隐藏层中使用了非线性的Logistic函数来帮助数据分类。第二,隐藏层使用了三个维度,即我们将数据从低维映射到了高维数据。下面的图绘制除了在隐藏层中的三维数据分类。
# Plot the projection of the input onto the hidden layer
# Define the projections of the blue and red classes
H_blue = hidden_activations(x_blue, Wh, bh)
H_red = hidden_activations(x_red, Wh, bh)
# Plot the error surface
fig = plt.figure()
ax = Axes3D(fig)
ax.plot(np.ravel(H_blue[:,0]), np.ravel(H_blue[:,1]), np.ravel(H_blue[:,2]), 'bo')
ax.plot(np.ravel(H_red[:,0]), np.ravel(H_red[:,1]), np.ravel(H_red[:,2]), 'ro')
ax.set_xlabel('$h_1$', fontsize=15)
ax.set_ylabel('$h_2$', fontsize=15)
ax.set_zlabel('$h_3$', fontsize=15)
ax.view_init(elev=10, azim=-40)
plt.title('Projection of the input X onto the hidden layer H')
plt.grid()
plt.show()
完整代码,点击这里
作者:chen_h
微信号 & QQ:862251340
简书地址:https://www.jianshu.com/p/1fe8ab3da28c
CoderPai 是一个专注于算法实战的平台,从基础的算法到人工智能算法都有设计。如果你对算法实战感兴趣,请快快关注我们吧。加入AI实战微信群,AI实战QQ群,ACM算法微信群,ACM算法QQ群。长按或者扫描如下二维码,关注 “CoderPai” 微信号(coderpai)