作者:John Wittenauer
翻译:GreatX
源:Machine Learning Exercises In Python, Part 5
这篇文章是一系列 Andrew Ng 在 Coursera 上的机器学习课程的练习的一部分。这篇文章的原始代码,练习文本,数据文件可从这里获得。
Part 1 简单线性回归(Simple Linear Regression)
Part 2 多元线性回归(Multivariate Linear Regression)
Part 3 逻辑回归(Logistic Regression)
Part 4 多元逻辑回归(Multivariate Logistic Regression)
Part 5 神经网络(Neural Networks)
Part 6 支持向量机(Support Vector Machines)
Part 7 K-均值聚类与主成分分析(K-Means Clustering & PCA)
Part 8 异常检测与推荐(Anomaly Detection & Recommendation)
在第四部分,我们通过将解拓展到多类分类(multi-class classification)上实现了逻辑回归并用手写数字数据集测试了它。仅用逻辑回归我们的分类精度就达到了 97.5%,它确实很好但却达到了我们能实现的线性模型最好的情况。在这篇文章里,我们将再次处理实现数字数据集,不过这次我们用是具有反向传播(backpropagation)1的前馈(feed-forward)神经网络。我们将实现非正则化和正则化两个版本的神经网络代价函数并通过反向传播算法计算梯度。最后,我们会在优化程序中运行该算法并评估该网络在手写数字数据集上的表现。
我要说的是在这练习里的所涉及的数学和代码有点难懂,从零开始实现一个神经网络不适合一个内心弱小的人(not for the faint of heart),对于那些想要冒雨前行的人,我想送一句话给你们——此处有龙2 。我也强烈鼓励读者阅读相应的 Andrew Ng 课程的练习文本。这里有我上传的副本。这个文本包含了我们要实现的所有表达式,有了这些,让我们开始吧!
因为数据集是我们之前用过的,所以我们可以重用上次的代码来导入数据。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.io import loadmat
%matplotlib inline
data = loadmat('data/ex3data1.mat')
data
{'X': array([[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
...,
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.]]),
'__globals__': [],
'__header__': 'MATLAB 5.0 MAT-file, Platform: GLNXA64, Created on: Sun Oct 16 13:09:09 2011',
'__version__': '1.0',
'y': array([[10],
[10],
[10],
...,
[ 9],
[ 9],
[ 9]], dtype=uint8)}
因为以后我们会用到这些(并且是经常用),所以让我们来创建几个有用的变量吧。
X = data['X']
y = data['y']
X.shape, y.shape
((5000L, 400L), (5000L, 1L))
我们也会用到一位有效编码(one-hot encode)3来处理标签。one-hot 编码将类标签 n (共 k 个类)映射到向量,该向量长度为 k ,在下标为 n 处是 1 (hot)其余处为 0 。Scikit-learn 有一个(相关的)内置实用工具,我们可以用在这。
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(sparse=False)
y_onehot = encoder.fit_transform(y)
y_onehot.shape
(5000L, 10L)
y[0], y_onehot[0,:]
(array([10], dtype=uint8),
array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]))
我们将要为这个练习生成的神经网络有一个输入层其单元个数与实例数据相匹配( 400 + 偏置单元),一个有 25 个单元的隐含层(加上偏置就有 26 个单元),一个有10个单元的输出层。第一块我们要实现的是代价函数,用来评估给定网络参数集的损失情况。在练习文本中有源数学函数,看起来非常可怕4,但它真的对分解问题有好处。下面是用来计算代价的函数。
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def forward_propagate(X, theta1, theta2):
m = X.shape[0]
a1 = np.insert(X, 0, values=np.ones(m), axis=1)
z2 = a1 * theta1.T
a2 = np.insert(sigmoid(z2), 0, values=np.ones(m), axis=1)
z3 = a2 * theta2.T
h = sigmoid(z3)
return a1, z2, a2, z3, h
def cost(params, input_size, hidden_size, num_labels, X, y, learning_rate):
m = X.shape[0]
X = np.matrix(X)
y = np.matrix(y)
# reshape the parameter array into parameter matrices for each layer
theta1 = np.matrix(np.reshape(params[:hidden_size * (input_size + 1)], (hidden_size, (input_size + 1))))
theta2 = np.matrix(np.reshape(params[hidden_size * (input_size + 1):], (num_labels, (hidden_size + 1))))
# run the feed-forward pass
a1, z2, a2, z3, h = forward_propagate(X, theta1, theta2)
# compute the cost
J = 0
for i in range(m):
first_term = np.multiply(-y[i,:], np.log(h[i,:]))
second_term = np.multiply((1 - y[i,:]), np.log(1 - h[i,:]))
J += np.sum(first_term - second_term)
J = J / m
return J
我们在之前就用过 sigmoid 函数所以这不是什么新东西。前向传播函数(forward-propagate function)对每个训练实例计算给定参数的假设(hypothesis)函数(也就是说,对于给定网络的当前状态和一系列输入,它能够计算网络中每层的输出)。假设向量(记为 h )(其包含每个类的预测概率)的维度应该和 y 相匹配。最后,代价函数执行前向传播这一步并计算出假设(预测)与实例的真实标签之间的误差。
我们能很快地检验这一步以使我们确信它能像我们期望的那样运行。观察中间步骤的输出也是一个让我们能搞清楚到底发生了什么的方法。
# initial setup
input_size = 400
hidden_size = 25
num_labels = 10
learning_rate = 1
# randomly initialize a parameter array of the size of the full network's parameters
params = (np.random.random(size=hidden_size * (input_size + 1) + num_labels * (hidden_size + 1)) - 0.5) * 0.25
m = X.shape[0]
X = np.matrix(X)
y = np.matrix(y)
# unravel the parameter array into parameter matrices for each layer
theta1 = np.matrix(np.reshape(params[:hidden_size * (input_size + 1)], (hidden_size, (input_size + 1))))
theta2 = np.matrix(np.reshape(params[hidden_size * (input_size + 1):], (num_labels, (hidden_size + 1))))
theta1.shape, theta2.shape
((25L, 401L), (10L, 26L))
a1, z2, a2, z3, h = forward_propagate(X, theta1, theta2)
a1.shape, z2.shape, a2.shape, z3.shape, h.shape
((5000L, 401L), (5000L, 25L), (5000L, 26L), (5000L, 10L), (5000L, 10L))
代价函数,当计算出假设矩阵 h 后,用代价等式计算出 y 和 h 之间的总误差。
cost(params, input_size, hidden_size, num_labels, X, y_onehot, learning_rate)
6.8228086634127862
下一步是将代价函数正则化,通过对代价函数添加惩罚项收缩参数的大小。这个等式长得有点丑,但它能精简到一行代码添加到原始的代价函数中去。只用添加下面一行到 return 语句前一行就行。
J += (float(learning_rate) / (2 * m)) * (np.sum(np.power(theta1[:,1:], 2)) + np.sum(np.power(theta2[:,1:], 2)))
接下来是反向传播(BP)算法。反向传播计算并更新参数以减少在训练数据上该网络的误差。我们首先需要一个函数来计算我们之前创建的 sigmoid 函数的梯度。
def sigmoid_gradient(z):
return np.multiply(sigmoid(z), (1 - sigmoid(z)))
现在,我们已经实现了计算梯度的 BP。因为反向传播所需的计算是代价函数所需的计算超集 / 父集,实际上我们要扩展代价函数使之也能执行反向传播,并返回代价和梯度。如果你想知道为什么我不直接从 backprop 函数内调用现有的代价函数以使设计更加模块化,这是因为 backprop 函数需要使用代价函数内部一些其他变量来计算。下面是完整的实现。我跳过了非正则化的版本直接加上了梯度正则化。
def backprop(params, input_size, hidden_size, num_labels, X, y, learning_rate):
##### this section is identical to the cost function logic we already saw #####
m = X.shape[0]
X = np.matrix(X)
y = np.matrix(y)
# reshape the parameter array into parameter matrices for each layer
theta1 = np.matrix(np.reshape(params[:hidden_size * (input_size + 1)], (hidden_size, (input_size + 1))))
theta2 = np.matrix(np.reshape(params[hidden_size * (input_size + 1):], (num_labels, (hidden_size + 1))))
# run the feed-forward pass
a1, z2, a2, z3, h = forward_propagate(X, theta1, theta2)
# initializations
J = 0
delta1 = np.zeros(theta1.shape) # (25, 401)
delta2 = np.zeros(theta2.shape) # (10, 26)
# compute the cost
for i in range(m):
first_term = np.multiply(-y[i,:], np.log(h[i,:]))
second_term = np.multiply((1 - y[i,:]), np.log(1 - h[i,:]))
J += np.sum(first_term - second_term)
J = J / m
# add the cost regularization term
J += (float(learning_rate) / (2 * m)) * (np.sum(np.power(theta1[:,1:], 2)) + np.sum(np.power(theta2[:,1:], 2)))
##### end of cost function logic, below is the new part #####
# perform backpropagation
for t in range(m):
a1t = a1[t,:] # (1, 401)
z2t = z2[t,:] # (1, 25)
a2t = a2[t,:] # (1, 26)
ht = h[t,:] # (1, 10)
yt = y[t,:] # (1, 10)
d3t = ht - yt # (1, 10)
z2t = np.insert(z2t, 0, values=np.ones(1)) # (1, 26)
d2t = np.multiply((theta2.T * d3t.T).T, sigmoid_gradient(z2t)) # (1, 26)
delta1 = delta1 + (d2t[:,1:]).T * a1t
delta2 = delta2 + d3t.T * a2t
delta1 = delta1 / m
delta2 = delta2 / m
# add the gradient regularization term
delta1[:,1:] = delta1[:,1:] + (theta1[:,1:] * learning_rate) / m
delta2[:,1:] = delta2[:,1:] + (theta2[:,1:] * learning_rate) / m
# unravel the gradient matrices into a single array
grad = np.concatenate((np.ravel(delta1), np.ravel(delta2)))
return J, grad
这里有很多细节,所以让我们试着解开一些。函数的前半部分使数据加上当前参数经过“网络”(前向传播函数)并将得到输出与真实标签进行比较来计算误差。 整个数据集中的总误差表示为 J 。这是我们前面讨论的作为代价函数的部分。
函数剩下的部分本质上是回答“如何调整我的参数,以减少下次通过网络的误差?”这个问题的。它回答这个问题通过计算每层对总误差的贡献和提出“梯度”矩阵(或者,每个参数改变了多少?和在什么方向上?)来适当地调整。
反向传播计算最难的部分(除了理解为什么我们要做所有这些计算)是得到正确矩阵维度(shape),这就是为什么我要对一些计算做注释显示计算结果的 shape。 顺便说一句,当你发现使用 A*B
与 np.multiply(A, B)
感到困惑时,你要记住,你并不是一个人。 简单说,前者是矩阵乘法后者是(Matlab 中的)点乘(除非A或B是标量值,在这种情况下不管用哪个都无关紧要)。 对于这种情况,我希望有一个更简洁的语法(也许有,只是我不知道)。
无论如何,让我们测试它以确保函数返回的是我们期望的。
J, grad = backprop(params, input_size, hidden_size, num_labels, X, y_onehot, learning_rate)
J, grad.shape
(6.8281541822949299, (10285L,))
到现在,我们终于准备训练网络并使用它进行预测了。 这大致类似于之前练习里的多类逻辑回归。
from scipy.optimize import minimize
# minimize the objective function
fmin = minimize(fun=backprop, x0=params, args=(input_size, hidden_size, num_labels, X, y_onehot, learning_rate),
method='TNC', jac=True, options={'maxiter': 250})
fmin
status: 3
success: False
nfev: 250
fun: 0.33900736818312283
x: array([ -8.85740564e-01, 2.57420350e-04, -4.09396202e-04, ...,
1.44634791e+00, 1.68974302e+00, 7.10121593e-01])
message: 'Max. number of function evaluations reach'
jac: array([ -5.11463703e-04, 5.14840700e-08, -8.18792403e-08, ...,
-2.48297749e-04, -3.17870911e-04, -3.31404592e-04])
nit: 21
我们对迭代次数进行了限制,因为目标函数不可能完全收敛。 我们的总代价已经下降到0.5以下,这是一个很好的指标表明该算法是可运行的。让我们使用它找到参数并用它们进行前向传播以获得一些预测。 我们必须 reshape 优化器的输出以匹配我们网络所期望的参数矩阵的 shape,然后再运行前向传播生成对输入数据的假设。
X = np.matrix(X)
theta1 = np.matrix(np.reshape(fmin.x[:hidden_size * (input_size + 1)], (hidden_size, (input_size + 1))))
theta2 = np.matrix(np.reshape(fmin.x[hidden_size * (input_size + 1):], (num_labels, (hidden_size + 1))))
a1, z2, a2, z3, h = forward_propagate(X, theta1, theta2)
y_pred = np.array(np.argmax(h, axis=1) + 1)
y_pred
array([[10],
[10],
[10],
...,
[ 9],
[ 9],
[ 9]], dtype=int64)
最后,我们可以计算一下精确度来看看我们训练的网络有多好。
correct = [1 if a == b else 0 for (a, b) in zip(y_pred, y)]
accuracy = (sum(map(int, correct)) / float(len(correct)))
print 'accuracy = {0}%'.format(accuracy * 100)
accuracy = 99.22%
我们终于完成了!我们已经成功地实现了一个基本的具有反向传播的前馈神经网络,并使用它识别了手写数字。这看起来令人感到惊讶,首先,我们设法做到了这一点而没有实现任何类似于一个网络的类或数据结构。这不是神经网络的全部吗?这是我在参加课程时最大的惊喜之一 —— 所有的这些基本上可以归结为一系列矩阵乘法。事实证明,这是迄今为止解决问题最有效的方式。事实上,如果你观察任何流行的深度学习框架如 Tensorflow5,它们本质上都是线性代数计算的图形。这是一种思考机器学习算法非常有用和实用的方式。
以上就是神经网络的练习。在下一个练习中我们将讨论另一种流行的监督学习算法,支持向量机(support vector machine)。