您的位置:首页 > 理论基础 > 计算机网络

深度学习之手撕神经网络代码(基于numpy)

2019-08-30 12:20 1751 查看
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/TeFuirnever/article/details/100136800

声明

1)该文章整理自网上的大牛和机器学习专家无私奉献的资料,具体引用的资料请看参考文献。
2)本文仅供学术交流,非商用。所以每一部分具体的参考资料并没有详细对应。如果某部分不小心侵犯了大家的利益,还望海涵,并联系博主删除。
3)博主才疏学浅,文中如有不当之处,请各位指出,共同进步,谢谢。
4)此属于第一版本,若有错误,还需继续修正与增删。还望大家多多指点。大家都共享一点点,一起为祖国科研的推进添砖加瓦。

文章目录

  • 5、单隐层神经网络的编程实现(基于numpy)
  • 参考文章
  • 0、前言

    为什么准备写这个系列呢?这个问题我思考了好久一阵子,主要是基于两个方面:

    • 第一,希望自己能更加认真透彻地去理解深度学习这门艺术,为着自己的目标去努力;
    • 第二,就是希望能分享更好更棒的东西给那些初学者,让他们少走一些坑。😃

    前一阵子写了一个初学者必看的 大话卷积神经网络CNN(干货满满),反应还不错,算是有了 正反馈,除此之外还上了CSDN首页的今日推荐,nice!!!

    这是成为博客专家之后认真写的系列——深度学习专题,加油!!!

    1、深度学习到底需要什么能力?

    深度学习到底需要什么能力?当你认真了解了之后就会发现,光是理论知识是极其匮乏无力的,和所有学习一样,需要实践,所以 coding 的能力是极其重要的。针对这个问题,目前我有两个计划,一个是针对LeetCode进行刷题,另一个就是追求最朴素的方式——手撕神经网络代码。

    很多人都说深度学习就是个 黑箱子,也就是黑盒艺术,诚然我相信目前确实没法真的说得清,这个我在 深度学习100问之深度学习的本质 中讨论过这个问题,这里就不说了。但我要说的是,如果你已是一名功力纯厚的深度学习工程师,这么做当然没问题。但大多数人应该和我一样,都是走在学习深度学习的路上。一上来就上框架对于你做项目之类的并没有什么特别不妥之处,但对你理解深度学习的黑箱机制是了无裨益的。

    所以在学习深度学习的路上,从最简单的感知机开始写起,一步一步捋清神经网络的结构,以至于激活函数怎么写、采用何种损失函数、前向传播怎么写、后向传播又怎么写,权值如何迭代更新,这些都需要你自己去实现,坚持下去,必然会有不一样的成效。相反,若在一开始就直接调用框架,小的 demo 可以跑起来,糊弄一时,当让你独自解决一个大问题时,你必然会手足无措。

    无论你是在什么书,是看 花书,还是在学习 Adrew NG 的 deeplearningai,或者是在 CS231n 课程,对神经网络的基本理论了如指掌的你,一定很想像自己见过的大佬一样手撕代码,亲手用 python 来实现一个网络模型。下面我们就要开始了,在不借助任何深度学习框架的基础上,利用 python 的科学计算库 numpy 由最初级的感知机开始,从零搭建一个神经网络模型

    2、神经网络的基本概念


    如上图,是一个 单隐藏层神经网络,我们来就此说一下相关的概念:

    1. 输入层

    神经网络中的第一层,它需要输入信号并把输入信号传递到下一层,同时不对输入信号做任何操作,并且没有关联的权重和偏置值。在上图中,对应x1,x2,也就是 input layer

    1. 输出层

    神经网络的最后一层,它接收来自最后一个隐藏层的输入。通过这个层,我们可以知道期望的值和期望的范围。在上图这个网络中,输出层 有1个神经元,输出prediction,也就是 output layer

    1. 隐藏层

    神经网络输入层与输出层之间的网络结构。一个 隐藏层 是垂直排列的神经元的集合(Representation)。在给出的图像中有1个 隐藏层,这个 隐藏层 有4个神经元(节点),它将自己层的输出值传递给输出层(这块比较拗口,每一个层都有自己的输入和输出,这个是值,不是层,传递的大结构是层)。隐藏层 中的每个神经元都与下一层的每一个神经元有连接,因此我们有一个全连接的 隐藏层

    1. 权重与偏置

    权重 表示单元之间连接的强度,也就是w。

    • 权重 决定了输入对输出的影响程度。如果从节点1到节点2的 权重 比较大,意味着神经元1对神经元2的影响比较大。
    • 权重 降低了输入值的重要性,所以当 权重 接近零时意味着更改此输入将几乎不会更改输出,而负 权重 意味着增加此输入会降低输出。

    偏置 是神经元的额外输入,当然还有一种叫法,叫做阈值。通俗来说,阈值的高低代表了意愿的强烈,阈值越低就表示越想去,越高就越不想去。

    在图中明确写出了上面所讲的四个概念,输入层、隐藏层、输出层、权重和偏置。

    1. 激活函数

    这个概念我们上次讲过,大话卷积神经网络CNN(干货满满),激活函数可以认为是神经网络中的非线性引入者,如果没有非线性,那么再多的层数叠加也不过是一个线性函数,可以用足以简单的 y=wx+by=wx+by=wx+b 来表示。如下是一些常用的激活函数

    1. 损失函数

    损失函数(loss function) 定义了拟合结果和真实结果之间的差异,作为优化的目标直接关系模型训练的好坏,所以很多研究工作的内容也集中在损失函数的设计优化上。在应用中,损失函数通常作为学习准则与优化问题相联系,即通过最小化损失函数求解和评估模型,在统计学和机器学习中被用于模型的参数估计(parameteric estimation)。

    1. 前向传播

    前向传播的过程是向神经网络馈送输入值并得到prediction的输出。当我们将输入值提供给神经网络的第一层(即输入层)时,它没有进行任何操作,而第二层网络(即隐藏层)则是从第一层网络(即输入层)中的输出值并进行乘法,加法和激活函数等一系列操作,然后将得到的值传递给下一层网络,在我们这里就是输出层(如果有多层隐藏层的话,就在后面的隐藏层中执行相同的操作),最后在输出层得到一个输出值 。

    如果后期有时间的话,会在深度学习100问中详细写一写前向传播的,可以期待一下。

    1. 反向传播

    BP算法(即反向传播算法)适合于多层神经元网络的一种学习算法,它建立在梯度下降法的基础上。BP网络的输入输出关系实质上是一种映射关系:一个n输入m输出的BP神经网络所完成的功能是从n维欧氏空间向m维欧氏空间中一有限域的连续映射,这一映射具有高度非线性。它的信息处理能力来源于简单非线性函数的多次复合,因此具有很强的函数复现能力。这是BP算法得以应用的基础。

    关于理解反向传播,毛遂自荐一下自己的博客——深度学习100问之深入理解Back Propagation(反向传播)

    这里辨析一下两个概念,因为很多初学者容易被这两个概念绕住,一个是前向传播,一个是反向传播。前向传播简单而言就是计算预测输出 y 的过程,而后向传播则是根据预测值和实际值之间的误差,不断往回推更新权值和偏置的过程。

    3、神经网络最简单的结构单元:感知机

    首先,先来搞一下神经网络最简单的结构单元:感知机,对猫识别问题进行分析。

    感知机就是一个最简单的线性分类模型,不严谨的说,就是一个 y=wx+b。


    如图所示,是一个比较熟悉的分类模型——猫的识别,详细的理论这里就不多赘述了。现在来想一下一个完整的神经网络模型需要什么,这个必须心中有数,其实简单来说,就是上面提到的相关概念分分类,通常情况下是:

    1. 构建网络
    2. 初始化参数
    3. 迭代优化
    4. 计算损失
    5. 反向传播
    6. 更新参数

    4、感知机的编程实现(基于numpy)

    有了上面这个思路和进行分析之后,就可以开始编程实现感知机了。当然了,要先说明一下,我们准备实现的是一个最简单的感知机模型,所以不需要做出特别定义,就如图中所示即可。

    1)激活函数

    首先定义激活函数,激活函数有很多种,上面也给出常用的激活函数,这里就不多说了,直接使用大名鼎鼎的 sigmoid 函数,公式定义如下:

    代码如下:

    import numpy as np
    
    def sigmoid(x):
    return 1 / (1 + np.exp(-x))

    2)参数初始化

    模型参数主要包括权值

    w
    和偏置
    b
    ,这也是神经网络学习过程中需要学习的东西。关于参数初始化成多少这件事,其实就是一个经验问题,我暂时不知道什么相关的理论,如果你有的话,可以评论告诉我。接着继续利用 numpy 对参数进行初始化,代码如下:

    def initilize_with_zeros(dim):
    w = np.zeros((dim, 1))
    b = 0.0
    #assert(w.shape == (dim, 1))
    #assert(isinstance(b, float) or isinstance(b, int))
    return w, b

    接下来就要进入模型的主体部分,这个部分中包括迭代优化、计算损失、反向传播和更新参数这四部分,这也是神经网络训练过程中每一次需要迭代的部分。

    3)前向传播

    前向传播函数的预测值

    y
    为模型从输入到经过激活函数处理后的输出的结果。损失函数采用交叉熵损失,公式如下:

    利用 numpy 定义如下函数:

    def propagate(w, b, X, Y):
    m = X.shape[1]
    A = sigmoid(np.dot(w.T, X) + b)
    cost = -1 / m * np.sum(Y * np.log(A) + (1 - Y) * np.log(1 - A))
    
    dw = np.dot(X, (A - Y).T) / m
    db = np.sum(A - Y) / m
    # assert(dw.shape == w.shape)
    # assert(db.dtype == float)
    cost = np.squeeze(cost)
    # assert(cost.shape == ())
    grads = {'dw': dw,
    'db': db}
    
    return grads, cost

    在上面的前向传播函数中,先是通过激活函数直接表示了感知机输出的预测值

    A
    ,然后通过定义的交叉熵损失函数计算了损失
    cost
    ,最后根据损失函数计算了权值
    w
    和偏置
    b
    的梯度
    dw
    db
    ,将参数梯度结果以字典和损失一起作为函数的输出进行返回,这就是前向传播的编写思路。

    有童鞋不懂

    dw
    db
    为什么那么表示的,下面是相关的推导过程,下面贴上自己的手写版本,字迹太草还请见谅。如果你实在不能懂这一块,记住就行,因为可能有的童鞋数学功底不太好,不过这对于我们这一节并不是特别重要。

    其中的sigmoid求导过程如下:

    至于为什么
    dw
    没有求和号,
    db
    有求和号,个人觉得应该是
    dw
    是矩阵乘法,理解为可以自动求和,而
    db
    是矩阵加法,需要手动求和。

    4)反向传播

    反向传播操作计算每一步的当前损失,然后根据损失对权值进行更新。关于理解反向传播,还是毛遂自荐一下自己的博客——深度学习100问之深入理解Back Propagation(反向传播),和之前一样,还是定义一个函数,代码如下:

    def backward_propagation(
    w, b, X, Y,
    num_iterations,
    learning_rate,
    print_cost=False):
    cost = []
    for i in range(num_iterations):
    grad, cost = propagate(w, b, X, Y)
    
    dw = grad['dw']
    db = grad['db']
    
    w = w - learing_rate * dw
    b = b - learning_rate * db
    if i % 100 == 0:
    cost.append(cost)
    if print_cost and i % 100 == 0:
    print("cost after iteration %i: %f" % (i, cost))
    
    params = {"dw": w,
    "db": b}
    
    grads = {"dw": dw,
    "db": db}
    
    return params, grads, costs

    在上述函数中,先是建立了一个损失列表

    cost
    ,然后将前一步定义的前向传播函数
    propagate(w, b, X, Y)
    放进去执行迭代操作,计算每一步的当前损失和梯度
    grad
    ,利用梯度下降法对权值进行更新,并用字典封装迭代结束时的参数和梯度进行返回
    params

    5)测试函数

    通常一个模型建好之后,还需要对测试数据进行预测,根据预测的结果才能判断模型的好坏,所以也要定义一个预测函数

    predict
    ,将模型的概率输出转化为0 / 1值,1就是是,0就是否,也就是判断结果。

    def predict(w, b, X):
    m = X.shape[1]
    Y_prediction = np.zeros((1, m))
    w = w.reshape(X.shape[0], 1)
    
    A = sigmoid(np.dot(w.T, X) + b)
    for i in range(A.shape[1]):
    if A[:, i] > 0.5:
    Y_prediction[:, i] = 1
    else:
    Y_prediction[:, i] = 0
    
    # assert(Y_prediction.shape == (1, m))
    return Y_prediction

    6)封装函数(选看)

    到这里整个模型算是写完了,下面这个可以选用,但是定义了这么多函数,调用起来太麻烦,所以致力于要写出

    pythonic
    的代码的我们,
    pythonic
    是一种代码风格,当然最常用的代码风格还是
    PEP8
    ,自己看这个博客——杂谈——Python代码写得丑怎么办?autopep8来帮你,肯定想对这些函数进行一下简单的封装,代码如下:

    def model(X_train, Y_train, X_test, Y_test, num_iterations=2000,
    learning_rate=0.5,
    print_cost=False):
    #用零初始化参数
    w, b = initialize_with_zeros(X_train.shape[0])  #梯度下降
    #从字典 "parameters" 中检索参数 w 和 b
    parameters, grads, costs = backward_propagation(
    w, b, X_train, Y_train, num_iterations, learning_rate, print_cost)
    
    w = parameters["w"]
    b = parameters["b"]  # Predict test/train set examples
    Y_prediction_train = predict(w, b, X_train)
    Y_prediction_test = predict(w, b, X_test)  # Print train/test Errors
    print("train accuracy: {} %".format(100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100))
    print("test accuracy: {} %".format(100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100))
    
    d = {"costs": costs,
    "Y_prediction_test": Y_prediction_test,
    "Y_prediction_train": Y_prediction_train,
    "w": w,
    "b": b,
    "learning_rate": learning_rate,
    "num_iterations": num_iterations}
    return d

    这样,一个简易的神经网络——感知机,就用 numpy 写出来了,下面是实现一个单隐层的神经网络。

    5、单隐层神经网络的编程实现(基于numpy)

    我们在第二章简单地讲过什么是单隐层的神经网络,就是只有一个隐藏层,而含单隐层的神经网络是不一样的,结构如下所示:

    下面按照步骤进行问题分析:

    1. 构建网络
    2. 初始化参数
    3. 迭代优化
    4. 计算损失
    5. 反向传播
    6. 更新参数

    1)构建网络

    假设

    X
    为神经网络的输入特征矩阵,
    y
    为标签向量,则网络结构定义如下:

    def layer_sizes(X, Y):
    n_x = X.shape[0] #输入层大小
    n_h = 4 #隐藏层大小
    n_y = Y.shape[0] #输出层大小
    return (n_x, n_h, n_y)

    其中输入层和输出层的大小分别与

    X
    y
    shape
    有关,也就是待定的状态,根据输入输出的要求而有所改变,而隐层的大小可由我们手动指定,这里我们指定隐层的大小为4。

    2)初始化参数

    假设

    W1
    为输入层到隐层的权重、
    b1
    为输入层到隐层的偏置;
    W2
    为隐层到输出层的权重,
    b2
    为隐层到输出层的偏置。初始化参数的函数如下:

    def initialize_parameters(n_x, n_h, n_y):
    W1 = np.random.randn(n_h, n_x)*0.01
    b1 = np.zeros((n_h, 1))
    W2 = np.random.randn(n_y, n_h)*0.01
    b2 = np.zeros((n_y, 1))
    
    #assert (W1.shape == (n_h, n_x))
    #assert (b1.shape == (n_h, 1))
    #assert (W2.shape == (n_y, n_h))
    #assert (b2.shape == (n_y, 1))
    
    parameters = {"W1": W1,
    "b1": b1,
    "W2": W2,
    "b2": b2}
    
    return parameters

    其中对权值的初始化,利用了 numpy 中的生成随机数的模块

    np.random.randn
    ,偏置的初始化则是使用了模块
    np.zero
    。通过设置一个字典进行封装,并返回包含初始化参数之后的结果
    parameters

    3)前向传播

    在完成了前两步之后,就开始执行神经网络的训练过程了,而训练的第一步则是执行前向传播计算。

    假设隐层的激活函数为

    tanh
    函数:

    输出层的激活函数为 sigmoid 函数,前面用过的那个激活函数,前向传播的基本原理公式如下:

    y=wx+by=wx+by=wx+b

    A=g(y)A=g(y)A=g(y)

    则我们的单隐层神经网络的前向传播公式为:

    定义前向传播计算函数,代码如下:

    def forward_propagation(X, parameters):
    #从字典 "parameters" 中检索每个参数
    W1 = parameters['W1']
    b1 = parameters['b1']
    W2 = parameters['W2']
    b2 = parameters['b2']
    #实现正向传播以计算A2(概率)
    Z1 = np.dot(W1, X) + b1
    A1 = np.tanh(Z1)
    Z2 = np.dot(W2, Z1) + b2
    A2 = sigmoid(Z2)
    #assert(A2.shape == (1, X.shape[1]))
    
    cache = {"Z1": Z1,
    "A1": A1,
    "Z2": Z2,
    "A2": A2}
    
    return A2, cache

    从参数初始化结果字典

    cache
    里取到各自的参数,然后执行一次前向传播计算,将前向传播计算的结果保存到
    cache
    这个字典中, 其中
    A2
    为经过
    sigmoid
    激活函数激活后的输出层的结果。

    4)计算损失

    前向传播计算完成后,需要确定以当前参数执行计算后的的输出与标签值之间的损失大小,即损失函数。如果采用损失函数交叉熵损失,则公式如下:

    代码如下:

    def compute_cost(A2, Y, parameters):
    m = Y.shape[1]  #样例的数量
    #计算交叉熵损失
    logprobs = np.multiply(np.log(A2), Y) + np.multiply(np.log(1 - A2), 1 - Y)
    cost = -1 / m * np.sum(logprobs)
    cost = np.squeeze(cost)     #确保损失函数是我们期望的维度
    
    #assert(isinstance(cost, float))
    return cost

    5)反向传播

    当前向传播和当前损失确定之后,就需要继续执行反向传播过程来调整权值了,也就是迭代优化。中间涉及到各个参数的梯度计算,这里给出的是吴恩达深度学习课程中的推导公式,具体如下图所示:

    则根据上述梯度计算公式定义反向传播函数,代码如下:

    def backward_propagation(parameters, cache, X, Y):
    m = X.shape[1]
    #首先,从字典 "parameters" 中检索 W1 和 W2
    W1 = parameters['W1']
    W2 = parameters['W2']
    #还可以从字典 "cache" 中检索 A1 和 A2
    A1 = cache['A1']
    A2 = cache['A2']
    #反向传播:计算dW1, db1, dW2, db2
    dZ2 = A2 - Y
    dW2 = 1 / m * np.dot(dZ2, A1.T)
    db2 = 1 / m * np.sum(dZ2, axis=1, keepdims=True)
    dZ1 = np.dot(W2.T, dZ2) * (1 - np.power(A1, 2))
    dW1 = 1 / m * np.dot(dZ1, X.T)
    db1 = 1 / m * np.sum(dZ1, axis=1, keepdims=True)
    
    grads = {"dW1": dW1,
    "db1": db1,
    "dW2": dW2,
    "db2": db2}
    return grads

    将各参数的求导计算结果

    dW1
    db1
    dW2
    db2
    求出来,然后放入字典
    grad
    进行返回。

    注意:在机器学习中,当所学问题有了具体的形式之后,就会转化成数学问题,也就是机器学习就会形式化为一个 求优化 的问题。不论是 梯度下降法随机梯度下降牛顿法拟牛顿法,抑或是 Adam 之类的高级的优化算法,这些都需要花时间掌握去掌握其数学原理,慢慢积累,先掌握最基本的知识,以实现功能从而带来正反馈,不至于劝退。

    6)权值更新

    迭代计算的最后一步,就是根据反向传播的结果更新权值了,更新公式如下:

    其中,θ\thetaθ 是要更新的参数,α\alphaα 是学习率,JJJ 损失函数。

    由该公式可以定义权值更新函数,代码如下:

    def update_parameters(parameters, grads, learning_rate=1.2):
    #从字典 "parameters" 中检索每个参数
    W1 = parameters['W1']
    b1 = parameters['b1']
    W2 = parameters['W2']
    b2 = parameters['b2']
    #从字典 "grads" 中检索每个梯度
    dW1 = grads['dW1']
    db1 = grads['db1']
    dW2 = grads['dW2']
    db2 = grads['db2']
    #每个参数更新的规则
    W1 -= dW1 * learning_rate
    b1 -= db1 * learning_rate
    W2 -= dW2 * learning_rate
    b2 -= db2 * learning_rate
    
    parameters = {"W1": W1,
    "b1": b1,
    "W2": W2,
    "b2": b2}
    return parameters

    7)封装函数(选看)

    这样,前向传播 - 计算损失 - 反向传播 - 权值更新 的神经网络训练过程就完成了,网络学习的过程其实就是这么一个迭代的过程,从而找到使得损失函数最小的最优解。和前面一样,为了更加 pythonic 一点,可以把各个模块组合起来,定义一个神经网络模型:

    def nn_model(X, Y, n_h, num_iterations=10000, print_cost=False):
    np.random.seed(3)
    n_x = layer_sizes(X, Y)[0]
    n_y = layer_sizes(X, Y)[2]
    #初始化参数,然后检索 W1, b1, W2, b2。输入:“n_x,n_h,n_y”。outputs=“W1, b1, W2, b2,parameters”
    parameters = initialize_parameters(n_x, n_h, n_y)
    W1 = parameters['W1']
    b1 = parameters['b1']
    W2 = parameters['W2']
    b2 = parameters['b2']
    #循环(梯度下降)
    for i in range(0, num_iterations):
    #正向传播。输入:“X,parameters”。输出:“A2,cache”
    A2, cache = forward_propagation(X, parameters)
    #损失函数。输入:“A2,Y,parameters”。输出:“cost”
    cost = compute_cost(A2, Y, parameters)
    #反向传播。输入:“parameters, cache, X, Y”。输出:“grads”
    grads = backward_propagation(parameters, cache, X, Y)
    #梯度下降更新参数。输入:“parameters, grads”
    #输出:“parameters”
    parameters = update_parameters(parameters, grads, learning_rate=1.2)
    #每1000次迭代打印一次损失
    if print_cost and i % 1000 == 0:
    print("Cost after iteration %i: %f" % (i, cost))
    
    return parameters

    这样,一个含单隐层的神经网络,就用 numpy 写出来了。

    参考文章

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: 
    相关文章推荐