[关闭]
@Duanxx 2016-12-30T02:10:44.000000Z 字数 9365 阅读 3634

BP神经网络

神经网络&深度学习


@author : duanxxnj@163.com
@time: 2016-11-15


之前已经对感知机有了详细的解释,但是感知机只能解决线性可分的二分类问题。如果要做多分类的非线性可分问题,感知机就力不从心了。但是如果能够将感知机机连接在一起,形成一个网络的话,基于这个网络就可以实现非线性的多分类问题,这就是神经网络。

一个神经网络大致就是下面这个形式,下图所示的就是一个是三层的神经网络的示意图,其主要包括输入层(Input layer),隐藏层(Hidden layer),输出层(Output layer)。每一层都可以有多个神经元,神经网络也可以有多个隐藏层。
----------image_1b19n58vn128jjt21aahgp41o0cc.png-61.1kB

对于输入层和输出层而言,其神经元的个数基本上是定的。输入层神经元的个数和输入特征向量的维度一致;输出层的维度和待分类的类别的数据相同。决定神经网络复杂度的主要就是隐藏层的维度。

隐藏层神经元的数目越多,那么神经网络的复杂度就越高,需要的计算量就约到,但是其学习能力也就越强,当然过拟合的几率也就越大。至于隐藏层神经元的数目应该如何去选择,这个涉及模型选择相关的问题,这里并不讨论。

之前说了,神经网络中每个神经元都是一个感知机,对于感知机而言,最重要的就是其激活函数的选择。

这里可以这么理神经网络:每一个圆圈都代表一个激活函数,而每一条带箭头的线段都代表一次乘积,每个线段上都有一个权值;带箭头的线段的输入,是其输入端圆圈的输出,而带箭头线段的输出,是其输入和线段上的权值的乘积。每个圆圈的作用就是将输入到当前神经元的数据做一个求和,然后再对求和的结果使用激活函数,就可以得到神经元的输出。

各层激活函数的选择

对于输入层而言,可以认为输入层没有激活函数,输入层就是将输入的数据原原本本的放到了神经网络之中。

对于隐藏层而言,其激活函数的选择有很多,一般使用的是下面这三种函数:

上面这三个函数的共同特点就是,其求导十分的容易,使用自身的结果就可以得到导数。

这里仅仅考虑使用 函数做代码演示,主要也是因为这个函数在绝大多数场合都很适用。

对于输出层而言,由于需要判断输入的样本属于哪一个类别,所以一般考虑使用 函数, 这个函数在多分类 回归中提到过:对于有 个类别的分类算法而言,将其输出转换成对应的概率。

假设神经网络输出的类别有 个,即需要将输入到神经网络中的样本分类到这 中类别中的某一个去,假设输入样本x,在隐藏层的输出是向量 ,那么输入样本x输入第 类的概率为:

显然, 这里的 代表的是输出层第 个神经元的输入,即

这里 或者说是 其实也可以写成 ,代表的是输出层第 个神经元的输出。

代表的是输出层所有神经元输出的总和。

函数最明显的特点在于:它把每个神经元的输入占当前层所有神经元输入之和的比值,当作该神经元的输出。这使得输出更容易被解释:神经元的输出值越大,则该神经元对应的类别是真实类别的可能性更高。另外, 不仅把神经元输出构造成概率分布,而且还起到了归一化的作用,适用于很多需要进行归一化处理的分类问题。

的导数:
如果

如果

损失函数的选择

一般来说,基于 函数的神经网络,所使用的损失函数(Cost Function)是交叉熵损失函数(Cross-entropy cost function)。

平方损失函数

在之前机器学习的算法中,大部分算法所使用的的损失函数都是SSE,即平方误差和。但是SSE损失函数有一个比较大的缺点,就是这个损失函数在初始化权值和目标权值差的很远的时候,其收敛速度特别慢,只有在权值相对接近最优解的时候,收敛速度才比较的快,其测试代码和结果如下:

代码连接

  1. #-*- coding=utf-8 -*-
  2. """
  3. @file : test_perceptron.py
  4. @author : duanxxnj@163.com
  5. @time : 2016-11-15
  6. """
  7. import matplotlib.pyplot as plt
  8. import numpy as np
  9. from dxxlearn.linear_model.perceptron import Perceptron
  10. # 这里使用单个感知机,单输入做为测试样本
  11. # 主要用于测试初始化权值对平方误差损失函数收敛速率的影响
  12. clf = Perceptron(300, 0.01)
  13. # 初始化权值为w=[2, 2]
  14. cost1 = clf.train(np.array([[1, 1]]), [0], clf.sigmoid, w_in=2)
  15. # 初始化权值为w=[0.6, 0.6]
  16. cost2 = clf.train(np.array([[1, 1]]), [0], clf.sigmoid, w_in=0.6)
  17. plt.plot(cost1, label='init w=[2, 2]', c='b')
  18. plt.plot(cost2, label='init w=[0.6, 0.6]', c='r')
  19. plt.legend()
  20. plt.grid()
  21. plt.show()

这里是不同的初始 对应的平方损失的收敛速度:
image_1b1iqlfgq5bv1vdqbi3u4org9m.png-88kB

初始权值选择比较好的红色线,其代价随着训练次数增加而快速降低
初始权值选择不太好的蓝色线,其代价在一开始下降得非常缓慢,后来才加快收敛速度

直观上看,初始的误差越大,收敛得越缓慢。但是,根据我们自己的直观感觉,如果初值选择的不好,和最优解有很大的偏离,那么在一开始的时候,其收敛的速度应该是最快的才对。而不应该一开始收敛很慢。

其实,当初始权值选择偏离比较大的时候,收敛速度慢的主要原因在于使用的损失函数:

这里

那么,的导数为:

很显然,的导数中 可以认为是常数,因为这两个值可以直接从训练样本中计算出来,于是乎, 的导导数和 函数的导数成线性关系。

函数和其导数导数如下图所示,显然在刚开始的时候,收敛很慢,随和逐步增加收敛速度的。
image_1b1it1vmo164e18ldnie1i51aco9.png-55.8kB

由于 函数的形式如此,所以,平方误差和损失函数的导数和 函数成线性关系,平方误差和损失函数的收敛在初始偏离很大到时候,收敛非常慢。这就是初始的代价(误差)越大,导致训练越慢的原因。

交叉熵损失函数

交叉熵与熵的关系,如同协方差与方差的关系一样,关于熵,会在其他的文章中详细说明,并不是本文的重点。

二分类交叉熵损失函数:

参数求解过程

基于上面所说的内容,这里将神经网络的参数求解过程概要性的推导一遍。

正向传播

当神经网络需要对样本进行预测的时候,使用的是正向传播算法。其实这个过程仅仅是一些矩阵和激活函数之间的乘法而已。这里假设:
1、 是一个 n 维的输入
2、总的样本个数为
3、是类别向量(对于三个类别而言,类别一:[1, 0, 0];类别二:[0, 1, 0];类别三:[0, 0, 1])
4、是输入经过神经网络之后预测得到的类别向量
5、隐藏层激活函数使用 函数(使用 函数也是一样的)
6、输出层激活函数使用 函数
7、损失函数使用交叉熵损失函数
8、第 层的输入为 ,第 层使用过激活函数后的输出为
9、 是第 层的参数

那么,对于只有一个隐藏层的神经网络而言,其参数分布如下:
image_1b1j85tds10ld17r84l31fv45s6m.png-101kB

前向算法实现:

这里假设输入层有 个神经元,隐藏层有 个神经元,输出层有 个神经元,那么:

反向传播

损失函数是交叉熵损失函数(cross-entropy loss),也叫做negative log likelihood:

然后再对损失函数求导:

BP神经网络代码实现

  1. #-*- coding=utf-8 -*-
  2. """
  3. @file : nn_classfication_tests.py
  4. @author : duanxxnj@163.com
  5. @time : 2016/11/15 14:30
  6. """
  7. import numpy as np
  8. from sklearn import datasets, linear_model
  9. import matplotlib.pyplot as plt
  10. class Config:
  11. nn_input_dim = 2 # input layer dimensionality
  12. nn_output_dim = 2 # output layer dimensionality
  13. # Gradient descent parameters (I picked these by hand)
  14. epsilon = 0.01 # learning rate for gradient descent
  15. reg_lambda = 0.01 # regularization strength
  16. def generate_data():
  17. np.random.seed(0)
  18. X, y = datasets.make_moons(200, noise=0.20)
  19. return X, y
  20. def visualize(X, y, model):
  21. # plt.scatter(X[:, 0], X[:, 1], s=40, c=y, cmap=plt.cm.Spectral)
  22. # plt.show()
  23. plot_decision_boundary(lambda x:predict(model,x), X, y)
  24. plt.title("Logistic Regression")
  25. def plot_decision_boundary(pred_func, X, y):
  26. # Set min and max values and give it some padding
  27. x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
  28. y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
  29. h = 0.01
  30. # Generate a grid of points with distance h between them
  31. xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
  32. # Predict the function value for the whole gid
  33. Z = pred_func(np.c_[xx.ravel(), yy.ravel()])
  34. Z = Z.reshape(xx.shape)
  35. # Plot the contour and training examples
  36. plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
  37. plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Spectral)
  38. plt.show()
  39. # Helper function to evaluate the total loss on the dataset
  40. def calculate_loss(model, X, y):
  41. num_examples = len(X) # training set size
  42. W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
  43. # Forward propagation to calculate our predictions
  44. z1 = X.dot(W1) + b1
  45. a1 = np.tanh(z1)
  46. z2 = a1.dot(W2) + b2
  47. exp_scores = np.exp(z2)
  48. probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
  49. # Calculating the loss
  50. corect_logprobs = -np.log(probs[range(num_examples), y])
  51. data_loss = np.sum(corect_logprobs)
  52. # Add regulatization term to loss (optional)
  53. data_loss += Config.reg_lambda / 2 * (np.sum(np.square(W1)) + np.sum(np.square(W2)))
  54. return 1. / num_examples * data_loss
  55. def predict(model, x):
  56. W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
  57. # Forward propagation
  58. z1 = x.dot(W1) + b1
  59. a1 = np.tanh(z1)
  60. z2 = a1.dot(W2) + b2
  61. exp_scores = np.exp(z2)
  62. probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
  63. return np.argmax(probs, axis=1)
  64. # This function learns parameters for the neural network and returns the model.
  65. # - nn_hdim: Number of nodes in the hidden layer
  66. # - num_passes: Number of passes through the training data for gradient descent
  67. # - print_loss: If True, print the loss every 1000 iterations
  68. def build_model(X, y, nn_hdim, num_passes=20000, print_loss=False):
  69. # Initialize the parameters to random values. We need to learn these.
  70. num_examples = len(X)
  71. np.random.seed(0)
  72. W1 = np.random.randn(Config.nn_input_dim, nn_hdim) / np.sqrt(Config.nn_input_dim)
  73. b1 = np.zeros((1, nn_hdim))
  74. W2 = np.random.randn(nn_hdim, Config.nn_output_dim) / np.sqrt(nn_hdim)
  75. b2 = np.zeros((1, Config.nn_output_dim))
  76. # This is what we return at the end
  77. model = {}
  78. # Gradient descent. For each batch...
  79. for i in range(0, num_passes):
  80. # Forward propagation
  81. z1 = X.dot(W1) + b1
  82. a1 = np.tanh(z1)
  83. z2 = a1.dot(W2) + b2
  84. exp_scores = np.exp(z2)
  85. probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
  86. # Backpropagation
  87. delta3 = probs
  88. delta3[range(num_examples), y] -= 1
  89. dW2 = (a1.T).dot(delta3)
  90. db2 = np.sum(delta3, axis=0, keepdims=True)
  91. delta2 = delta3.dot(W2.T) * (1 - np.power(a1, 2))
  92. dW1 = np.dot(X.T, delta2)
  93. db1 = np.sum(delta2, axis=0)
  94. # Add regularization terms (b1 and b2 don't have regularization terms)
  95. dW2 += Config.reg_lambda * W2
  96. dW1 += Config.reg_lambda * W1
  97. # Gradient descent parameter update
  98. W1 += -Config.epsilon * dW1
  99. b1 += -Config.epsilon * db1
  100. W2 += -Config.epsilon * dW2
  101. b2 += -Config.epsilon * db2
  102. # Assign new parameters to the model
  103. model = {'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
  104. # Optionally print the loss.
  105. # This is expensive because it uses the whole dataset, so we don't want to do it too often.
  106. if print_loss and i % 1000 == 0:
  107. print("Loss after iteration %i: %f" % (i, calculate_loss(model, X, y)))
  108. return model
  109. def classify(X, y):
  110. # clf = linear_model.LogisticRegressionCV()
  111. # clf.fit(X, y)
  112. # return clf
  113. pass
  114. def main():
  115. X, y = generate_data()
  116. model = build_model(X, y, 3, print_loss=True)
  117. visualize(X, y, model)
  118. if __name__ == "__main__":
  119. main()

这里有一点需要说明,就是隐藏层的神经元的个数基本上代表的是该神经网络的复杂度。复杂度越高,神经网络的分类能力越强,同时其过拟合的可能性也就越大:

image_1b1jb3um9p3el3n1s621n851nmf1g.png-301.7kB
image_1b1jb4hrv1urm1kmv8bd1aq8kh21t.png-244kB

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注