用Python从头开始实现神经网络-简介

这是一篇原文翻译文章,主要是自己学习的总结 *********

原文链接

代码链接

笔者使用的环境是 jupyter notebook ,编译环境是python3

在这篇文章中,我们将从头开始实现一个简单的3层神经网络。我们不会得到所需的所有数学,但我会尝试直观地解释我们正在做什么。我还将指出您阅读有关详细信息的资源。

在这里,我假设您熟悉基本的微积分和机器学习概念,例如,您知道什么是分类和正则化。理想情况下,您还可以了解梯度下降等优化技术的工作原理。但即使你不熟悉上述任何一篇文章,这篇文章仍然会变得有趣;)

但是为什么要从头开始实施神经网络呢?即使您计划将来使用像PyBrain这样的神经网络库,从头开始实施网络至少一次也是非常有价值的练习。它可以帮助您了解神经网络的工作原理,这对于设计有效模型至关重要。

 

需要注意的一点是,这里的代码示例并不是非常有效。它们意味着易于理解。

 

生成数据集

首先我们需要生成可以使用的数据集,幸运的是,“scikit-learn”有一些有用的数据集生成 器,所以我们不需要自己编写代码,我们将使用make moons函数

首先,插入需要的包:

# Package imports
import matplotlib.pyplot as plt
import numpy as np
import sklearn
import sklearn.datasets
import sklearn.linear_model
import matplotlib

# 显示内联图并更改默认图尺寸
%matplotlib inline
matplotlib.rcParams['figure.figsize'] = (10.0, 8.0)

生成数据集:

# 生成数据集并绘制它
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)

运行结果:

用Python从头开始实现神经网络-简介_第1张图片

关于数据集的解释:

在特定的坐标轴下,我们可以将蓝色和红色的点视为医学中的男性患者和女性患者, x轴和y轴为医学测量。 我们的目标是训练机器分类器在给定x和y坐标的情况下可以预测正确的类(男性还是女性) 请注意数据不是线性可分的,这意味着线性分类器(如Logistic回归)将无法拟合数据, 除非您手工设计适用于给定数据集的非线性要素(如多项式)。 事实上,神经网络的一个主要优势就是你不必担心特征工程,它隐藏在神经网络中并帮你学习特征 。

 

训练logistics回归分类器:

为了证明这一点,让我们训练一个Logistic回归分类器。它的输入是x和y值,输出是预测的类(0或1)。 为了让我们的工作更轻松,我们使用scikit-learn中的Logistic回归类。

# 训练 logistic 回归分类器
clf = sklearn.linear_model.LogisticRegressionCV()
clf.fit(X, y)
# 帮助绘制决策边界的函数。
# 如果您不完全了解此功能,请不要担心,它只是生成下面的等高线图。
def plot_decision_boundary(pred_func):
    # 设置最小值和最大值
    x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
    y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
    h = 0.01
    # 生成一个点间网格,它们之间的距离为h
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
    # 预测整个gid的函数值
    Z = pred_func(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    # 绘制轮廓和训练示例
    plt.contourf(xx, yy, Z, cmap = plt.cm.Spectral)
    plt.scatter(X[:, 0], X[:, 1], c = y, cmap = plt.cm.Spectral)
# 绘制决策边界
plot_decision_boundary(lambda x: clf.predict(x))
plt.title("Logistic Regression")

运行结果:

用Python从头开始实现神经网络-简介_第2张图片

在这里,该图显示了Logistic回归分类器学习的决策边界。 它使用直线将数据尽可能好地分离,但它无法捕获数据的“月亮形状”。

 

训练一个神经网络:

现在让我们构建一个带有一个输入层,一个隐藏层一个输出层的神经网络,输入节点的数量取决于我们数据的维度,我们确定为2。类似的,输出层的取决于我们需要的类,在这里也是2 因为我们只有2个类实际上只有一个输出节点可以预测0或1, 但是有2个可以让网络更容易扩展到更多的类。网络的输入将是x和y坐标,其输出将是两个概率, 一个用于0级(“女性”),一个用于1级(“男性”)。网络层的结构看起来像这样:

用Python从头开始实现神经网络-简介_第3张图片

我们可以选择隐藏层的维度(节点的个数)。隐藏层的节点个数越多,函数适应的情况就越复杂 但是更高的维度需要付出更多的代价。首先,需要更多的计算来进行预测来学习网络的参数, 大量的参数也有可能导致我们过拟合现象的发生 。

如何选择隐藏层的参数?虽然有一些一般的指导方针和建议,但它总是取决于您的具体问题, 更多的是艺术而不是科学。稍后我们将使用隐藏中的节点数来查看它是如何影响我们的输出的。

我们还需要为隐藏层选择一个激活函数。激活功能将图层的输入转换为其输出。 非线性激活函数允许我们拟合非线性假设。激活函数的常见chocies是tanh,sigmoid函数或ReLU。 我们将使用tanh,它在许多场景中表现都很好。 这些函数的一个很好的属性是它们的派生可以使用原始函数值来计算。 例如,衍生物 tanh x是1- tanh ^ 2 x。这很有用,因为它允许我们计算 tanh x 一次并在以后重新使用它的值来获得导数。

因为我们希望我们的网络输出概率,所以输出层的激活函数将是softmax,这只是将原始分数转换为概率的一种方法。如果您熟悉逻辑函数,您可以将softmax视为对多个类的推广。

我们构建的网络可以预测的原理:

我们的网络使用前向传播进行预测,这只是一堆矩阵乘法和我们在上面定义的激活函数的应用。 如果x是我们网络的二维输入,那么我们计算我们的预测\widehat{y}(也是二维)如下:

z_{1} = x\ast W_{1}+b

a_{1}=tanh(z_{1})

z_{2}=a_{1}\ast W_{2}+b_{2}

a_{2}=\widehat{y}=softmax(z_{2})

z_{i} 是输入层的加权和,a_{i}是输出层i在使用激活函数后的输出 。 W_{1},b_{1},W_{2},b_{2}是网络的参数,这些参数需要从我们的训练集中得出。您可以将它们视为在网络层之间转换数据的矩阵。观察上面的矩阵乘法,我们可以计算出这些矩阵的维数。 如果我们在隐藏层使用500个节点,那么W_{1}\in \mathbb{R}^{2\ast 500},b_{1}\in \mathbb{R}^{ 500},W_{2}\in \mathbb{R}^{500\ast 2},b_{2}\in \mathbb{R}^{ 500}。现在你了解为什么我们增加隐藏层的大小时会有更多参数。

学习参数:

学习网络参数意味着找到(W_{1},b_{1},W_{2},b_{2})参数使得训练数据误差最小,但是我们是如何定义 误差的呢?我们把测量误差的函数称之为损失函数(loss function).softmax的常见选择输出是交叉熵损失,如果我们有N个训练样本和C类,那么我们对真实值y的预测 \widehat{y}由下面的式子表示出来:

L(y,\hat{y})=-\tfrac{1}{n}\sum_{n\in\mathbb{N}} \sum_{i\in\mathbb{C}} y_{n,i}log\hat{y_{n,i}}

公式看起来很复杂,不过其所做的就是将训练样例求和。此外如果我们预测了不正确的类,损失就会增加。 所以预测值 和估计值\widehat{y}差的越多,损失值就会越大。

请牢记我们的目标是寻找到可以使损失最小的参数。我们可以使用梯度下降法来寻找到其最小值。我将实现最流行 的梯度下降法,也称之为具有固定学习速率的批量梯度下降。列如SGD(随机梯度下降)或者小批量梯度下降 之类的变化通常在事件中表现得更好。因此如果你足够严谨你将会想要使用这其中的一种。理想情况下你也希望可以 随着时间的推移而不断减少学习率。

作为一个输入,梯度下降法需要损失函数的梯度(矢量的衍生物),相对的参数是:\frac{\partial L}{\partial W_{1}},\frac{\partial L}{\partial b_{1}},\frac{\partial L}{\partial W_{2}},\frac{\partial L}{\partial b_{2}}, 为了计算出这些参数我们需要使用著名的反向传播算法。这是一个可以有效的从输出计算出梯度的算法。

使用反向传播算法,我们可以得到以下的式子:

\delta _{3}=\hat{y}-y

\delta _{2}=(1-tanh^{2}z_{1})\ast \delta _{3}\ast W_{2}^{T}

\frac{\partial L}{\partial W_{2}} =a_{1}^{T}\ast \delta _{3}

\frac{\partial L}{\partial b_{2}} =\delta _{3}

\frac{\partial L}{\partial W_{1}} =x_{1}^{T}\ast \delta _{2}

\frac{\partial L}{\partial b_{1}} = \delta _{2}

实现:

根据前面已经推演出的公式,现在我们已经准备好实现我们的算法了,我们开始为梯度下降来定义一些有用的变量和参数 :

num_examples = len(X) # 训练集大小
nn_input_dim = 2 # 输入层的维度
nn_output_dim = 2 # 输出层的维度

# 梯度下降法的参数(由手工挑选)
epsilon = 0.01 # 梯度下降法的学习率(learning rate)
reg_lambda = 0.01 # 正则化强度

首先我们实现定义的损失函数。我们使用这些来估计我们的模型做的如何:

# 辅助函数用于评估数据集的总损失
def calculate_loss(model):
    W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
    # 使用前向传播来计算我们的预测
    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)
    # 计算损失
    corect_logprobs = -np.log(probs[range(num_examples), y])
    data_loss = np.sum(corect_logprobs)
    # 为损失添加规范化条款(可选)
    data_loss += reg_lambda/2 * (np.sum(np.square(W1)) + np.sum(np.square(W2)))
    return 1./num_examples * data_loss

我们还实现了一个辅助函数来计算网络的输出,它按照上面的定义来进行前向传播,并且返回具有最高概率的类:

# 利用辅助函数来预测输出(0 或 1)
def predict(model, x):
    W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
    # 前向传播:
    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)

最后,这里要实现训练神经网络的参数,这里使用我们在反向传播中的导数来实现批量梯度下降 :

# 该函数学习神经网络的参数并返回模型。
# - nn_hdim: 隐藏层中节点的个数
# - num_passes: 通过梯度下降的训练数据的次数
# - print_loss: 如果正确的话,每1000次迭代打印损失
def build_model(nn_hdim, num_passes=20000, print_loss = False):
    
    # 将参数初始化为随机值。 我们需要学习这些。
    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))
    
    # 这里是最后我们的返回值
    model = {}
    
    # 对于每次计算,使用梯度下降法
    for i in range(0, num_passes):
        
        # 前向传播
        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)
        
        # 反向传播
        delta3 = probs
        delta3[range(num_examples), y] -= 1
        dW2 = (a1.T).dot(delta3)
        db2 = np.sum(delta3, axis=0, keepdims=True)
        #delta2 = (1 - np.power(a1, 2)*delta3.dot(W2.T))
        delta2 = (1 - np.power(a1, 2))*delta3.dot(W2.T)
        #delta2 = delta3.dot(W2.T) * (1 - np.power(a1, 2))
        dW1 = np.dot(X.T, delta2)
        db1 = np.sum(delta2, axis=0)
        
        # 添加正则化项(b1和b2没有正则化项)
        dW2 += reg_lambda * W2
        dW1 += reg_lambda * W1
        
        # 梯度下降的参数更新
        W1 += -epsilon * dW1
        b1 += -epsilon * db1
        W2 += -epsilon * dW2
        b2 += -epsilon * db2        
        
        # 为模型分配新的参数
        model = {'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}
        
        # 可以选择打印损失
        # 因为会使用到整个数据集,因此代价很高,我们并不想经常使用到它
        if print_loss and i% 1000 == 0:
            print("Loss after iteration %i: %f" %(i, calculate_loss(model)))
    return model

设置一个具有三个隐藏层的网络:

现在,让我们看一下如果我们训练一个有三个隐藏层的网络将会发生什么 :

# 构建一个由三个隐藏层的模型
model = build_model(3, print_loss=True)

# 绘制决策边界
plot_decision_boundary(lambda x: predict(model, x))
plt.title("Decision Boundary for hidden layer size 3")

运行结果:

用Python从头开始实现神经网络-简介_第4张图片

嗯,太好了!看起来很不错。我们的神经网络能够找到一个成功分离类的决策边界。

那如果改变隐藏层的大小会怎样?

改变隐藏的图层大小: 从上面的示例中,我们选择的隐藏层大小是3,现在让我们实时隐藏层大小的变化将会对结果造成什么样的影响。:

plt.figure(figsize=(16, 32))
hidden_layer_dimensions = [1, 2, 3, 4, 5, 20, 50, 100, 500, 1000]
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()

(这里需要5分钟左右的时间来运行出结果,如果想要快点出结果,那就少输入几个隐藏层的维度)

运行结果:

用Python从头开始实现神经网络-简介_第5张图片

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

 

好了,全文翻译就到这里了,感谢各位看官的耐心浏览,感觉不错的点个赞吧~~~

你可能感兴趣的:(用Python从头开始实现神经网络-简介)