Get the code: To follow along, all the code is also available as an iPython notebook on Github.
在这篇文章中,我们将从头开始实现一个简单的3层神经网络。我们不会得到所需的所有数学,但我会尝试直观地解释我们正在做什么。我还会指出资源,让您了解详细信息。
在这里,我假设您熟悉基本的微积分和机器学习概念,例如:你知道什么是分类和正规化。理想情况下,您还可以了解梯度下降等优化技术的工作原理。但即使你不熟悉上述任何一篇文章,这篇文章仍然会变得有趣;)
但为什么要从头开始实施神经网络呢?即使您计划将来使用像PyBrain这样的神经网络库,从头开始实施网络至少一次也是非常有价值的练习。它可以帮助您了解神经网络的工作原理,这对于设计有效模型至关重要。
需要注意的一点是,这里的代码示例并不是非常有效。它们意味着易于理解。在即将发表的文章中,我将探讨如何使用Theano编写有效的神经网络实现。 (更新:现已推出)
生成数据集
让我们从生成我们可以使用的数据集开始。幸运的是,scikit-learn有一些有用的数据集生成器,因此我们不需要自己编写代码。我们将使用make_moons函数。
# Generate a dataset and plot it
np.random.seed(0)
X, y = sklearn.datasets.make_moons(200, noise=0.20)
plt.scatter(X[:,0], X[:,1], s=40, c=y, cmap=plt.cm.Spectral)
我们生成的数据集有两个类,绘制为红色和蓝色点。您可以将蓝点视为男性患者,将红点视为女性患者,x轴和y轴为医学测量。
我们的目标是训练机器学习分类器,该分类器在给定x和y坐标的情况下预测正确的类(女性的男性)。请注意,数据不是线性可分的,我们不能绘制分隔两个类的直线。这意味着线性分类器(如Logistic回归)将无法拟合数据,除非您手工设计适用于给定数据集的非线性要素(如多项式)。
事实上,这是神经网络的主要优势之一。您无需担心功能工程。神经网络的隐藏层将为您学习功能。
事实上,这是神经网络的主要优势之一。您无需担心功能工程。神经网络的隐藏层将为您学习功能。
Logistic回归
为了证明这一点,让我们训练一个Logistic回归分类器。它的输入是x和y值,输出是预测的类(0或1)。为了让我们的生活更轻松,我们使用scikit-learn中的Logistic回归类。
# Train the logistic rgeression classifier
clf = sklearn.linear_model.LogisticRegressionCV()
clf.fit(X, y)
# Plot the decision boundary
plot_decision_boundary(lambda x: clf.predict(x))
plt.title("Logistic Regression")
该图显示了Logistic回归分类器学习的决策边界。它使用直线将数据尽可能好地分离,但它无法捕获数据的“月亮形状”。
训练神经网络
现在让我们构建一个3层神经网络,其中包含一个输入层,一个隐藏层和一个输出层。输入层中的节点数由我们的数据的维数确定.2。类似地,输出层中的节点数由我们拥有的类的数量决定,也是2.(因为我们只有2个类实际上只有一个输出节点可以预测为0或1,但是有2个可以让以后更容易将网络扩展到更多的类)。网络的输入将是x和y坐标,其输出将是两个概
率,一个用于0级(“女性”),一个用于1级(“男性”)。它看起来像这样:
我们可以选择隐藏层的维度(节点数)。我们在隐藏层中放置的节点越多,我们能够适应的功能就越复杂。但更高的维度需要付出代价。首先,需要更多的计算来进行预测并学习网络参数。更多参数也意味着我们更容易过度拟合数据。
如何选择隐藏层的大小?虽然有一些一般的指导方针和建议,但它总是取决于您的具体问题,而更多的是艺术而不是科学。稍后我们将使用隐藏中的节点数来查看它是如何影响我们的输出的。
我们还需要为隐藏层选择一个激活函数。激活功能将层的输入转换为其输出。非线性激活函数允许我们拟合非线性假设。激活函数的常见chocies是tanh,sigmoid函数或ReLU。我们将使用tanh,它在许多场景中表现都很好。这些函数的一个很好的属性是它们的派生可以使用原始函数值来计算。例如: tanh x的导数是1- tanh ^ 2 x。这很有用,因为它允许我们计算一次tanh x并稍后重新使用它的值来获得导数。
因为我们希望我们的网络输出概率,所以输出层的激活函数将是softmax,这只是将原始分数转换为概率的一种方式。如果您熟悉逻辑函数,您可以将softmax视为对多个类的推广。
我们的网络如何进行预测
我们的网络使用前向传播进行预测,这只是一堆矩阵乘法和我们在上面定义的激活函数的应用。如果x是我们网络的二维输入,那么我们计算我们的预测(也是二维的),如下所示:
z_i是层i的输入,a_i是应用激活函数后的层i的输出。 W_1,b_1,W_2,b_2是我们网络的参数,我们需要从我们的培训数据中学习。您可以将它们视为在网络层之间转换数据的矩阵。观察上面的矩阵乘法,我们可以计算出这些矩阵的维数。如果我们为隐藏图层使用500个节点,那么。现在,如果我们增加隐藏层的大小,您就会明白为什么我们有更多参数。
学习参数
学习网络参数意味着找到最小化训练数据误差的参数(W_1,b_1,W_2,b_2)。但是我们如何定义错误呢?我们将测量我们的错误的函数称为损失函数。 softmax输出的常见选择是分类交叉熵损失(也称为负对数似然)。如果我们有N个训练样例和C类,那么关于真实标签y的预测的损失由下式给出:
公式看起来很复杂,但它真正做的就是总结我们的训练样例,如果我们预测了不正确的类,就会增加损失。两个概率分布y(正确的标签)和\ hat {y}(我们的预测)越远,我们的损失就越大。通过找到最小化损失的参数,我们最大化了训练数据的可能性。
我们可以使用梯度下降来找到最小值,我将实现最普遍的梯度下降版本,也称为具有固定学习率的批量梯度下降。诸如SGD(随机梯度下降)或小批量梯度下降之类的变化通常在实践中表现更好。因此,如果你是认真的,你会想要使用其中一种,理想情况下你也会随着时间的推移而衰减学习率。
作为输入,梯度下降需要相对于我们的参数的损失函数的梯度(导数的矢量):。为了计算这些梯度,我们使用着名的反向传播算法,这是一种从输出开始有效计算梯度的方法。我不会详细介绍反向传播的工作原理,但是网上有许多优秀的解释(here or here) .
应用反向传播公式,我们发现以下内容(相信我):
实现
现在我们已准备好实施。我们首先为梯度下降定义一些有用的变量和参数:
num_examples = len(X) # training set size
nn_input_dim = 2 # input layer dimensionality
nn_output_dim = 2 # output layer dimensionality
# Gradient descent parameters (I picked these by hand)
epsilon = 0.01 # learning rate for gradient descent
reg_lambda = 0.01 # regularization strength
首先让我们实现上面定义的损失函数。我们用它来评估我们模型的表现:
# Helper function to evaluate the total loss on the dataset
def calculate_loss(model):
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# Forward propagation to calculate our predictions
z1 = X.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
# Calculating the loss
corect_logprobs = -np.log(probs[range(num_examples), y])
data_loss = np.sum(corect_logprobs)
# Add regulatization term to loss (optional)
data_loss += reg_lambda/2 * (np.sum(np.square(W1)) + np.sum(np.square(W2)))
return 1./num_examples * data_loss
我们还实现了一个辅助函数来计算网络的输出。它按照上面的定义进行前向传播,并返回具有最高概率的类。
# Helper function to predict an output (0 or 1)
def predict(model, x):
W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
# Forward propagation
z1 = x.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
return np.argmax(probs, axis=1)
最后,这里有训练神经网络的功能。它使用我们在上面找到的反向传播导数实现批量梯度下降。
# This function learns parameters for the neural network and returns the model.
# - nn_hdim: Number of nodes in the hidden layer
# - num_passes: Number of passes through the training data for gradient descent
# - print_loss: If True, print the loss every 1000 iterations
def build_model(nn_hdim, num_passes=20000, print_loss=False):
# Initialize the parameters to random values. We need to learn these.
np.random.seed(0)
W1 = np.random.randn(nn_input_dim, nn_hdim) / np.sqrt(nn_input_dim)
b1 = np.zeros((1, nn_hdim))
W2 = np.random.randn(nn_hdim, nn_output_dim) / np.sqrt(nn_hdim)
b2 = np.zeros((1, nn_output_dim))
# This is what we return at the end
model = {}
# Gradient descent. For each batch...
for i in xrange(0, num_passes):
# Forward propagation
z1 = X.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
exp_scores = np.exp(z2)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
# Backpropagation
delta3 = probs
delta3[range(num_examples), y] -= 1
dW2 = (a1.T).dot(delta3)
db2 = np.sum(delta3, axis=0, keepdims=True)
delta2 = delta3.dot(W2.T) * (1 - np.power(a1, 2))
dW1 = np.dot(X.T, delta2)
db1 = np.sum(delta2, axis=0)
# Add regularization terms (b1 and b2 don't have regularization terms)
dW2 += reg_lambda * W2
dW1 += reg_lambda * W1
# Gradient descent parameter update
W1 += -epsilon * dW1
b1 += -epsilon * db1
W2 += -epsilon * dW2
b2 += -epsilon * db2
# Assign new parameters to the model
model = { 'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
# Optionally print the loss.
# This is expensive because it uses the whole dataset, so we don't want to do it too often.
if print_loss and i % 1000 == 0:
print "Loss after iteration %i: %f" %(i, calculate_loss(model))
return model
具有大小为3的隐藏层的网络
让我们看看如果我们训练隐藏层大小为3的网络会发生什么。
# Build a model with a 3-dimensional hidden layer
model = build_model(3, print_loss=True)
# Plot the decision boundary
plot_decision_boundary(lambda x: predict(model, x))
plt.title("Decision Boundary for hidden layer size 3")
好极了!这看起来很不错。我们的神经网络能够找到一个成功分离类的决策边界。
改变隐藏的图层大小
在上面的示例中,我们选择了隐藏的图层大小3.让我们现在了解隐藏图层大小的变化如何影响结果。
plt.figure(figsize=(16, 32))
hidden_layer_dimensions = [1, 2, 3, 4, 5, 20, 50]
for i, nn_hdim in enumerate(hidden_layer_dimensions):
plt.subplot(5, 2, i+1)
plt.title('Hidden Layer size %d' % nn_hdim)
model = build_model(nn_hdim)
plot_decision_boundary(lambda x: predict(model, x))
plt.show()
我们可以看到隐藏的低维度层很好地捕捉了我们数据的总体趋势。较高的尺寸易于过度拟合。它们“记忆”数据而不是拟合一般形状。如果我们在一个单独的测试集上评估我们的模型(你应该!),由于更好的泛化,具有较小隐藏层大小的模型可能会表现得更好。我们可以用更强的正则化来抵消过度拟合,但为隐藏层选择正确的尺寸是一种更“经济”的解决方案
练习
以下是您可以尝试更熟悉代码的一些事项:
1、使用minibatch梯度下降(更多信息),而不是批量梯度下降,来训练网络。 Minibatch梯度下降通常在实践中表现更好。
2、我们使用固定学习率进行梯度下降。实现梯度下降学习率的退火计划(更多信息)。
3、我们为隐藏层使用了tanh激活函数。尝试其他激活功能(一些在上面提到)。请注意,更改激活功能还意味着更改反向传播导数。
4、将网络从两个类扩展到三个类。您需要为此生成适当的数据集。
5、将网络扩展到四层。试验图层大小。添加另一个隐藏层意味着您需要调整前向传播和后向传播代码。