从头开始实现神经网络(python)

在本文中,咱们将一步步展示如何从零开始搭建一个简单的三层神经网络。虽然咱们不会深入探讨所有复杂的数学公式,但我会尽量用直观的方式解释清楚。

我假设你对基本的微积分和机器学习的概念,比如分类和正则化,已经有所了解。最好你还熟悉梯度下降等优化技术。不过,即使您对这些内容不太熟悉,本文依然会非常有趣!

那么,为什么要从头开始实现神经网络呢?即使你以后打算使用像PyBrain这样的神经网络库,至少有一次从头实现网络的经历也是极其有价值的。这能帮助您深入理解神经网络的工作原理,对设计有效的模型至关重要。

目录

生成数据集

逻辑回归

训练神经网络

我们的网络如何进行预测

实现

具有大小为 3 的隐藏层的网络

改变隐藏图层大小

生成数据集

咱们先从生成一个可用的数据集开始。幸运的是,scikit-learn提供了一些非常有用的数据集生成器,因此咱们无需自己编写代码。咱们将使用make_moons函数。

# Generate a dataset and plot itnp.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 classifierclf = sklearn.linear_model.LogisticRegressionCV()clf.fit(X, y)
# Plot the decision boundaryplot_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 sizenn_input_dim = 2 # input layer dimensionalitynn_output_dim = 2 # output layer dimensionality
# Gradient descent parameters (I picked these by hand)epsilon = 0.01 # learning rate for gradient descentreg_lambda = 0.01 # regularization strength

首先,让我们实现我们上面定义的损失函数。我们用它来评估我们的模型的表现如何:

# Helper function to evaluate the total loss on the datasetdef 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 iterationsdef 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 layermodel = build_model(3, print_loss=True)
# Plot the decision boundaryplot_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()

图片

具有不同隐藏层大小的神经网络决策边界 我们可以看到,一个隐藏的低维层很好地捕捉了我们数据的总体趋势。较高的维度容易出现过拟合。他们正在“记忆”数据,而不是拟合一般形状。如果我们要在一个单独的测试集上评估我们的模型(你应该!),由于更好的泛化,具有较小隐藏层大小的模型可能会表现得更好。我们可以通过更强的正则化来抵消过拟合,但是为隐藏层选择正确的大小是一种更经济的解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值