从头开始实现一个神经网络

从头开始实现一个神经网络

翻译自Implementing a Neural Network from Scratch in Python – An Introduction

在这篇文章中,我们实现一个简单的三层神经网络。我们不会推导出所有需要的数学运算,但我会尽力直观地解释我们正在做什么。同时,我也会在一些细节上贴上源代码。

假设你有基础的微积分和机器学习的概念,比如,分类和回归。如果了解过最优化方法更好,比如梯度下降等。但如果上面的你都没听过或者是还不熟悉,也没关系,希望这边文章可以激发你的兴趣。

即使有了诸多实现神经网络的工具库,但是自己动手从头实现一次神经网络还是很有必要的。这可以帮助你更好地理解神经网络的架构和原理和设计高效的模型。

那我们就开始吧。

生成数据集

我们先生成数据集方便我们测试模型。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)

data

生成数据集分为两个类,分别是红色和蓝色。你可以把红色看为正例,把蓝色看为反例。每个点对应的/(x,y)/值就是/X/ 特征值。

我们的目的是: 在给定/(x, y)/情况下, 训练一个机器学习分类器来预测正确的类别(正或者负). 上面的数据不是线性可分的,我们无法用一条直线完全正确划分。这意味着线性分类器(逻辑回归等),无法拟合这种数据,除非你手工添加非线性的特征值(比如多项式),等特征工程。

事实上,神经网络的一个主要优点是,你不需要考虑做特征工程。神经网络的隐藏层会帮你学习这些特征。

逻辑回归

为了验证我们刚才讨论的,我们来训练一个线性回归分类器看看效果。可以轻易的从/sklearn/中导出我们想要的模型。

# 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 function

我们看到逻辑回归模型生成的决策边界,它使用直线将数据分离得尽可能好,但它无法捕捉到我们数据的“月亮形状”。

训练神经网络

输入层,隐藏层,输出层共同组成一个神经网络。输入层的节点个数取决于我们数据的维度,在这里是2。同样的,输出层的节点个数取决于我们类的个数,在这里也是2 。(我们只有两个类,所以我们可以只输出一个值0或者1,但以两个输出的格式,可以方便以后类别的扩展)。输入会是/x/和/y/,输出会是两个概率,一个是类别为正例的概率,另一个则是反例的概率。

nerual networks

我们可以选择隐藏层的维度,如果隐藏层的节点越多,其拟合数据的能力就越强。但是更高的维度会带来一些代价,比如,我们需要更高的训练模型的参数。再者,一个庞大复杂的模型更容易发生过拟合。

隐藏层节点少,模型的拟合能力就差,节点多的话,又会发生过拟合。那么如何选择隐藏层的节点个数呢?虽然有一些一般的指导方针和建议,但总是取决于具体问题,而且更多的是艺术而不是科学。稍后我们将使用隐藏的节点数量来看看它如何影响我们的输出。

我们需要给我们的隐藏层选择一个激活函数。激活函数将层的输入转化为输出。一个非线性激活函数允许我们拟合非线性的假设。一般激活函数有以下几种 tanhsigmoid functionReLUs。我们会用tanh,其在许多方面表现得非常好。这些激活函数有一个很好的性质是它们的导数值可以用原函数值计算。比如,/tanh x/的导数是/1-tanh2x/这很有用,因为当我们知道/tanh x/时,我们就可以计算出导数值了。

因为我们想要我们网络输出的是概率,我们就需要使用softmax 这个激活函数。它可以很简单的将原始数值转化为概率。如果你熟悉/sigmoid/函数,你可以把/softmax/看为对多分类的归一化。

神经网络如何预测

网络利用前向传播计算预测值,其实只是矩阵的相乘和激活函数的应用而已。计算预测值过程如下所示:

compute

/zi/表示第/i/层的输入,/ai/表示经过激活函数后,第/i/层的输出。/W1,b1,W2,b2/表示网络中的一些参数,这些参数就需要从训练的数据中学习。您可以将它们视为矩阵,在网络层之间转换数据。看矩阵相乘之前,我们先搞清楚这些矩阵的维度。如果我们的隐藏层有500个节点,那么就有param 现在我们可以清楚地看到参数的维度。可以看到当我们增加隐藏层的节点大小时,参数就会变多。

学习参数

学习参数的意思是找到合适的参数(/W1,b1,W2,b2/),使得可以最小化模型在训练数据上的错误预测。那我们如何定义错误呢?我们把衡量错误的函数称作loss function 损失函数。softmax输出的一个常见选择是分类交叉熵损失(也称为负对数似然值)。假设我们有/N/个训练样本和/C/个类别,预测值为/~y/,真实值为/y/,这是我们的损失函数是

loss

这个函数看起来有点复杂,其实他做的只是对所有训练样本求和,且如果有错误分类,那么损失值则会增加而已。当两个概率分布/y(correct labels) and ~y(predictions )/相差越远,则损失值最大。通过找到最小化损失值的数值,使得模型最大化对训练数据的似然。

我们可以运用梯度下降来找到最小值。这一次我会用最普通的梯度下降版本来实践我们的网络,也称作批梯度下降。还有其他的改版,比如SGD(stochastic gradient descent)或者是小批梯度下降,通常在实践中表现更好。
所以如果你认为效果很重要的话可以使用这两个其中一个,顺便也注意下学习率随着时间衰减学习率

梯度下降需要损失函数和参数的梯度(向量或者导数)。为了计算这些梯度值,我们会用到很出名的反向传播算法,是一个从输出值开始,高效的计算梯度值的方法。我不会详细讨论后向传播是如何工作的,但在网络中有很多优秀的解释(这里这里)。

应用反向传播的公式后,我们可以得出以下结论(相信我):

formula

实践

现在我们已经准备好了,我们先定义一些有用的变量和参数

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的网络

让我们看看这个网络的表现怎么样

# 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 la

result

耶,这看起来很不错。我们神经网络成功地找到一条决策边界来划分数据。

验证隐藏层的大小

上面的例子,我们选了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()

diff

我们可以看到低维度的隐藏层很好地捕捉了我们数据的总体趋势,更高维度容易过度拟合。如果我们要在一个单独的测试集上评估我们的模型,具有较小隐藏层的模型由于更好的泛化可能会更好。我们可以通过更强的正则化约束来抵消过度拟合,但为隐藏层选择正确的大小是一种更经济的解决方案。

练习

这里有一些练习可以让你更好地理解:

  1. 用小批梯度下降代替批梯度下降来训练网络。小批梯度下降在实践中往往表现得更好。
  2. 我们对梯度下降使用了固定的学习率,实践渐变下降学习率的退火时间表更多信息
  3. 我们为隐藏层使用了/tanh/激活函数。尝试其他激活函数(上面提到了一些)。请注意,改变激活函数也意味着改变反向传播中的导数
  4. 将网络可分类数由/2/扩展到/3/,你应该重新生成一个适合的数据集。
  5. 将网络扩展到四层。添加一个隐藏层意味着您将需要调整正向传播以及反向传播代码。

所有代码都放在GitHub上的iPython notebook,请在评论中留下问题或反馈