在本文中,咱们将一步步展示如何从零开始搭建一个简单的三层神经网络。虽然咱们不会深入探讨所有复杂的数学公式,但我会尽量用直观的方式解释清楚。
我假设你对基本的微积分和机器学习的概念,比如分类和正则化,已经有所了解。最好你还熟悉梯度下降等优化技术。不过,即使您对这些内容不太熟悉,本文依然会非常有趣!
那么,为什么要从头开始实现神经网络呢?即使你以后打算使用像PyBrain这样的神经网络库,至少有一次从头实现网络的经历也是极其有价值的。这能帮助您深入理解神经网络的工作原理,对设计有效的模型至关重要。
目录
生成数据集
咱们先从生成一个可用的数据集开始。幸运的是,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 坐标的情况下预测正确的类(男性和女性)。请注意,数据不是线性可分的,我们无法绘制一条直线来分隔这两个类。这意味着线性分类器(如逻辑回归)将无法拟合数据,除非您手动设计适用于给定数据集的非线性特征(例如多项式)。
事实上,这是神经网络的主要优势之一。您无需担心特征工程。神经网络的隐藏层将为您学习特征。
逻辑回归
为了证明这一点,让我们训练一个逻辑回归分类器。它的输入将是 x 值和 y 值,输出是预测类(0 或 1)。为了让我们的生活变得轻松,我们使用了 scikit-learn 中的逻辑回归类。
# 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")
该图显示了我们的逻辑回归分类器学习的决策边界。它使用直线尽可能地分隔数据,但它无法捕获我们数据的“月亮形状”。
训练神经网络
现在让我们构建一个 3 层神经网络,其中包含一个输入层、一个隐藏层和一个输出层。输入层中的节点数由数据的维数 2 决定。同样,输出层中的节点数由我们拥有的类数决定,也是 2。(因为我们只有 2 个类,我们实际上可以只用一个输出节点来预测 0 或 1,但有 2 个可以更轻松地将网络扩展到更多的类)。网络的输入将是 x 和 y 坐标,其输出将是两个概率,一个用于类 0(“女性”),另一个用于类 1(“男性”)。它看起来像这样:
我们可以自由选择隐藏层的节点数量。节点越多,网络能够拟合的函数就越复杂。然而,增加节点数量并非没有代价。首先,更多的节点意味着需要进行更多的计算,无论是进行预测还是学习网络参数。此外,参数数量的增加还可能导致模型更容易过拟合数据。
那么,如何确定隐藏层的大小呢?虽然有一些通用的指导原则和建议,但最终还是要根据具体问题来定,这更像是一门艺术,而非纯粹的科学。稍后我们会探讨隐藏层节点数量对输出的影响。
接下来,我们需要为隐藏层选择一个激活函数。激活函数负责将层的输入转换为输出。非线性激活函数使我们能够拟合非线性假设。常见的激活函数包括tanh、sigmoid函数和ReLU。我们选择使用tanh,因为它在许多情况下表现良好。这些函数的一个优点是,它们的导数可以直接从原始函数值计算得出。
由于我们希望网络输出概率,因此输出层的激活函数将采用softmax。这是一种将原始分数转换为概率的方法。如果你熟悉逻辑回归,可以将softmax视为其在多分类场景下的扩展。
我们的网络如何进行预测
我们的网络使用前向传播进行预测,这只是一堆矩阵乘法和我们上面定义的激活函数的应用。如果 x 是我们网络的二维输入,那么我们计算我们的预测如下:
是我们网络的参数,我们需要从训练数据中学习。您可以将它们视为在网络层之间转换数据的矩阵。查看上面的矩阵乘法,我们可以计算出这些矩阵的维数。如果我们使用 500 个节点作为隐藏层,那么:
现在你明白为什么如果我们增加隐藏层的大小,我们会有更多的参数。
损失函数的作用是衡量模型预测值与真实值之间的差距,而交叉熵损失是分类问题中常用的损失函数。通过梯度下降算法,我们可以找到使损失最小的参数。反向传播算法则帮助我们高效地计算梯度,从而更新参数。
具体来说,损失函数 L(y,y^) 的公式是:
L(y, y^) = - (1/N) * sum(sum(y_n,i * log(y^_n,i)))
虽然这个公式看起来复杂,但它实际上只是对我们的训练示例求和,如果我们的预测类别错误,就会增加损失。当真实标签y和预测y^之间的概率分布差异越大时,损失就越大。通过找到最小化损失的参数,我们可以最大限度地提高训练数据的可能性。
我们可以使用梯度下降来寻找这个最小值。我们将实现最简单的梯度下降版本,即具有固定学习率的批量梯度下降。在实践中,SGD(随机梯度下降)或小批量梯度下降等变体通常表现更好。因此,如果您非常认真,您可能会考虑使用其中之一,并在训练过程中逐渐降低学习率。
梯度下降需要损失函数相对于参数的梯度(导数向量)作为输入:
-
dL/dW1: 损失函数关于W1的梯度
-
dL/db1: 损失函数关于b1的梯度
-
dL/dW2: 损失函数关于W2的梯度
-
dL/db2: 损失函数关于b2的梯度
为了计算这些梯度,我们使用了著名的反向传播算法,它是一种有效计算梯度的方法,从输出开始向后传播。我不会详细介绍反向传播的工作原理,但网络上有很多很好的解释资源(例如这里或这里)。
应用反向传播公式,我们得到以下结果:
-
δ3 = y^ - y
-
δ2 = (1 - tanh^2(z1)) ∘ (δ3 * W2^T)
-
dL/dW2 = a1^T * δ3
-
dL/db2 = δ3
-
dL/dW1 = x^T * δ2
-
dL/db1 = δ2
实现
现在我们已准备好实施。我们首先为梯度下降定义一些有用的变量和参数
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 的神经网络决策边界
耶!这看起来还不错。我们的神经网络能够找到一个成功分离类的决策边界。
改变隐藏图层大小
在上面的示例中,我们选择了隐藏层大小 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()
具有不同隐藏层大小的神经网络决策边界 我们可以看到,一个隐藏的低维层很好地捕捉了我们数据的总体趋势。较高的维度容易出现过拟合。他们正在“记忆”数据,而不是拟合一般形状。如果我们要在一个单独的测试集上评估我们的模型(你应该!),由于更好的泛化,具有较小隐藏层大小的模型可能会表现得更好。我们可以通过更强的正则化来抵消过拟合,但是为隐藏层选择正确的大小是一种更经济的解决方案。