【python】手敲一个神经网络

前言

学习任何一种算法的最好方法都是自己手敲一遍,所以这里翻译一篇英文的深度学习入门的博文,里面很清晰地介绍了深度学习,而且实现了一个简单的三层神经网络。写在这里记录一下自己的学习过程和思考,也可以给各位读者做一个参考。

原文地址:http://www.wildml.com/2015/09/implementing-a-neural-network-from-scratch/

生成数据集

首先我们需要生成一个可以拿来玩的数据集,这里调用了 sklearn 库里面的 make_moon 函数直接生成。值得一体的是,因为 datasets 是 sklearn 的一个子库,而 python 的机制使 import sklearn 后并不会一起将所有子库一起 import 进来,所以这里需要写 from sklear import datasets 才可以正确运行。

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

这里用到的 scatter 函数是用来画散点图的,第一个参数是 x 轴,第二个参数是 y 轴,s 是 size 大小,c 是 color 颜色,而 cmap 是用来设置颜色渐变的,这里 Spectral 可以为两个标签生成不同的颜色。

最终 plt.show() 出来的图片长这个样子,这就是我们这次要用神经网络预测的数据集了。可以看到因为调用了 make_moon 函数,生成的数据集都是月亮型的。
数据集

这些数据分为两类,每个点的标签由 x 和 y 共同确定,且并不是线性可分的(不能画一条直线将两类数据分开)。不能线性可分意味着像逻辑斯蒂回归这样的线性分类器就不能用了,当然你也可以使用其他的手段改善这些线性的分类器,但这些内容不在这篇博文的讨论范围之内。

其实这也是神经网络的优势之一,即不需要人为的来做 featur engineering。隐藏层会替我们完成这一工作。

这里我们先来试一下LR的效果,这里直接调用 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))

最终效果可想而知。
LR

训练一个神经网络

现在我们来构建一个三层的神经网络来完成这个分类任务。三层中有一个二维的输入层,一个二维的输出层,以及一个隐藏层。输入层的维度是2是因为每个样本是二维的;这里我们将输出层的维度也设为2,这样方便将来做其他的的拓展。对于目前的这个任务,结果输出的分别是两个标签的概率,即一个是0的概率,一个是1的概率。整个结构大概长下面整个样子。

三层神经网络

我们可以自由选择隐藏层的维数,维数越多我们最终得到的函数就可以越复杂,但是要求的计算同样也会增加,而且最重要的是很可能会造成过拟合。那么如何选择隐藏层的大小呢?这主要取决于你当前要处理的问题,而且这其实更像是一个感性的主观认知。

另外我们还需要给隐藏层一个激活函数,从而将这层输入转化成输出。一个非线性的激活函数允许我们解决非线性的问题。常见的选择有tanh、sigmoid函数以及ReLUs,我们在这个任务中选择tanh。以上这些激活函数都有一个很重要的特征,那就是他们的求导都可以通过本身的值来计算。举例来说,tanh(x) 的求导是1-tabh2(x),所以我们计算出tanh(x)后就可以直接计算出它在此处求导的值。

因为我们希望这个神经网络能够输出概率,所以输出层的激活函数应当是归一化的。这是一种可以直接将得分转化为概率的方法。如果你很熟悉逻辑斯蒂函数,那么你可以将归一化函数看作是它在多种类别时的一种推广。

神经网络如何预测

我们的网络通过前向传递来进行预测,所谓前向传递其实就是一层层通过矩阵乘法和激活函数来传递数据。在当前的例子中,每一层参数的计算如下:

公式1
zi 是 i 层的输入,ai 是 i 层在应用激活函数之后的输出。W1、b1、W2、b2 是这个网络的参数,即是我们通过训练希望能够学到的内容。我们可以从上边的式子中看出各个参数的维度。加入我们的隐藏层有500个node,那么 W1 和 W2 就是一个2 * 500的矩阵,而 b1 则是一个一个1 * 500的矩阵,这样你就能理解为什么我说隐藏层越大我们所需的参数就越多了吧。

学习参数

所谓“学习”的过程其实就是找到一组参数(W1,b1,W2,b2)使得最终在训练集上预测的error最小。那么如何定义这个error呢?我们将其称为损失(loss),由损失函数来计算,这也是需要我们来指定的部分。对于归一化的输出结果,一个最常见的损失函数是绝对交叉熵损失(categorical cross-entropy loss ),又称负对数似然(negative log likelihood),即如果我们有 N 个训练样本,最终要分为 C 类,那么对于预测结果 y ^ \hat{y} y^ 最终的 loss 应为
loss
这个公式做的只是将所有训练样本的loss加在一起而已。y 和 y ^ \hat{y} y^ 差得越远,这个 loss 越大。
然后我们通过梯度下降法来最小化这个值。梯度下降法所需要的梯度可以由后向传递算法来实现(backpropagation algorithm),而具体求梯度的过程这里就不再赘述了,最终的结果如下:
梯度

实现

现在我们来开始实现,首先是定义变量。

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

# 梯度下降参数
epsilon = 0.01  # 学习率
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)
    # 计算loss,其中y中存储的是正确结果概率的index
    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

我们还要实现一个predict函数来帮我实现最后的预测过程。它通过前向传递,利用激活函数,最终返回概率较高的标签。

# 真正的预测函数 (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)

最后是最重要的 build_model 函数,用来训练神经网络得到参数的。

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 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)

        # 逆向传播更新参数,其中y中存储的是正确结果概率的index,我们希望正确结果的概率是1,所以和1作差
        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

        # 更新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

当隐藏层有三个节点时

下面让我们来看看隐藏层有三个节点时的分类效果

# 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')
plt.show()

三个节点
可以看出分类效果要比线性的逻辑斯蒂回归好多了。

调整隐藏层的大小

接下来我们让隐藏层分别取不同的值,以此来帮我们理解隐藏层节点个数的意义。

不同节点个数
可以看出当隐藏层的节点个数较少的时候,模型可以很好地适应我们的数据;但是当节点数过多的时候,就会出现过拟合的情况。所谓过拟合,其实就是指我们希望模型做的,是找到数据背后的规律,而不是单纯的记忆数据。
这里因为当前的分类问题较为简单,所以只需要一个具有三个节点的隐藏层就够了。

结语

可以看出,所谓神经网络,其实本质还是一个优化模型。但是就是这样一个简单的模型,就已经能够解决很多问题了;而在这之上衍生出去的RNN等一系列模型,能够进一步解决更多更复杂的问题。这些更进阶的东西,就等以后有机会再继续学习。
另外,作者将所有的代码都放在Github上了,有兴趣的读者可以下载下来跑跑看:https://github.com/dennybritz/nn-from-scratch

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值