spoon无法初始化至少一个步骤_敲黑板!搭建DNN深度神经网络的各种步骤与细节...

aa0abe4d05a714c53cd34d86c9d69f4f.png

相信不少人学过了深度神经网络,但学完之后如果不总结下来的话很多概念仅仅知道个定义,这里给大家总结一篇神经网络的知识点,包含构造神经网络所需的一切组件。

搭建一个神经网络通常需要进行以下步骤:

  1. Weight initialization 权重初始化;
  2. Forward propagation 前向传播计算;
  3. Compute cost 损失计算;
  4. Backward propagation 后向传播计算;
  5. Update parameter 更新权重。

注:本文适合有深度学习基础的读者,目的是为大家总结知识点,从而查漏补缺。如果你看不懂里面提到的概念,Don't worry about it,去学习吴恩达老师的DeepLearning.ai 系列课程的第一门课程,学完回来再看本文就会恍然大悟。

本文的大部分内容都是总结于吴恩达老师Neural Networks and Deep Learning课程的内容,部分概念会附上一些简单的代码,但完整代码不宜公开,否则就违反了coursera的荣誉准则了,建议想实操练习的朋友去报名Ng的课程,物超所值。每一个部分都有非常多的细节,看看你都记得多少。

Weight initialization 权重初始化

神经网络有大量的参数,在训练网络前,必须对参数进行初始化,好的初始化可以避免发生梯度弥散、梯度爆炸的情况,加快网络训练。通常有以下几种初始化方式:

  • zero, random

零初始化普遍认为都不适用于神经网络中,因为如果对所有神经元都初始化为0的话,后向传播回来的梯度全部一致,导致网络无法学习到有用的权重。但有一个特殊情况,就是当用在逻辑回归的时候,零初始化是没什么问题的,因为不存在隐藏层神经元,权重的梯度也仅与特征x有关,更新的时候可以打破这种对称,从而学得很好。random随机初始化也是比较常用的,一般认为比zero初始化要好,但也有导致梯度爆炸梯度弥散的风险。

  • Xavier, he

由于神经网络层数深,每一层的梯度往浅层传播的时候,如果权重数值很大或者很小,经过累乘就会使得梯度变得很大/很小(即爆炸或弥散),如果我们在初始化权重的时候能把权重初始化成只比1大一点点或者小一点点,这样累乘起来的效应就没那么大了。假设我们使用numpy写一个初始化代码,如:

W 

我们只需在代码后面乘以一个小数字,比如0.001,或者使用Xavier initialization方法,乘以一个参数Var :

分母表示本层神经元个数和下一层神经元个数,这是一个大于零小于一的小数,用代码表示:

W 

或者使用he initialization 方法,乘以这个数:

。还有其它一些参数的选择,但一般常用这两个已经获得比较好的效果,详细的分析可以搜一下相关的文章,这里不作赘述。

Forward propagation 前向传播计算

前向传播过程是除了初始化之外最简单的步骤,很多人会有一种错觉,觉得把整个网络结构搭起来了是一件很累很光荣的事,但实际上最麻烦的是数据的预处理及输入(对你没听错,后面再慢慢解释)。keras的作者Francios Chollet也在他的书中说到,神经网络就是一堆高中数学的结合,无非就是线性方程和一些非线性方程的堆叠,去拟合出一个很复杂的函数。只要搭建网络的时候小心一点,注意矩阵的维度,很快就能写好。每一层的前向计算只有两个步骤:linear_forwardactivation_forward

  • linear_forward:
  • activation_forward:

这里activation forward 用到的g(x),是一些非线性函数,例如relu、sigmoid、tanh,一般来说在隐藏层用tanh、relu或者leaky relu能有很好的效果。而sigmoid一般只在分类任务的最后一层激活使用,如果在隐藏层中使用会导致神经元“死亡”、“饱和”的问题。用numpy很简单就能实现(激活函数用relu为例),注意前向过程要输出一个“cache”,这个cache是一个包含激活值Z和A、权重W和偏置b的tuple数据,这些值将会在后向传播中使用。实现了一层之后,剩下的就是使用for循环堆叠几个这样的层了。

def linear_forward(A, W, b):
    Z = np.dot(W, A) + b
    cache = (A, W, b)
    return Z, cache

def relu(Z):
    A = np.maximum(0,Z)
    cache = Z 
    return A, cache

def linear_activation_forward(A_prev, W, b):
    Z, linear_cache = linear_forward(A_prev, W, b)
    A, activation_cache = sigmoid(Z)
    cache = (linear_cache, activation_cache)
    return A, cache

Compute cost 损失计算

神经网络需要依靠损失值来产生梯度,从而更新权重。而损失计算要针对你的任务来选择损失函数,多分类问题可以使用categorical_crossentropy_loss,回归问题可以使用MSE loss等等,有些更复杂的任务可以使用多种损失函数的组合,这里就以二分类的对数损失函数来作为例子,损失函数公式为:

为了在代码中的书写方便,这里的模型输出

我们使用AL代替,指的是最后一层的激活值
。numpy代码示例:
cost = -1/m * np.sum(Y * np.log(AL) + (1-Y) * np.log(1-AL))

基本上常见的损失函数的代码实现都很简单,但到了CNN里面一些自定义的损失函数实现起来会相当麻烦。

Backward propagation 后向传播计算

我们使用上一步得到的损失值cost来计算梯度,把梯度传播到前面的层当中去,用于权重更新。看到这里的时候有人可能会问:“啊?这一步都要自己实现吗?tensorflow、pytorch那些不是都已经自动计算了吗?”“额,你喜欢就好。”现有的框架的确全部都已经帮你写好了,简单到可以用几行代码 import tensorflow as tf,loss = tf.loss........,optimizer=.......就能把一个网络搭建起来运行,但是你永远都不会知道这几行代码的背后都发生了什么,当出问题的时候,你也不知道到底是在哪里出的问题。而且,假如换了一个框架例如mxnet,它背后的求梯度的实现又与tensorflow不一样,那你怎么办呢?所以只有当你用numpy把所有过程写出来,神经网络的各项细节就会了如指掌,一切的框架都是基于这样的原理,只是实现的方式不一样,当你明白了背后所有的原理,什么框架都不是问题。

神经网络首先要求出损失值cost对于最后一层(第L层)激活AL的梯度,用符号表示为:

,以上面的cost为例,用numpy可以表示成:
dAL = - (np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))

有个小细节,我这里没有乘以1/m,看看下面的公式思考下为什么。

6525a9cad377b3b6cc9e37faebaeda38.png
图来自吴恩达Deeplearining第一课第四周作业示例图

对于每一层来说,首先要接收从后一层计算得到的

,再来计算本层的
以及前一层的梯度
,那么各个参数使用链式法则可得():

1/m又在公式5、6中出现了,为什么?

细节:*号代表element-wise product,其它一律代表矩阵乘法,思考一下为什么会有两种乘法。

我们先写出激活函数的求导公式,也就是上面公式中的

,后面会用到。
def sigmoid_backward(dA, cache):
    Z = cache
    s = 1/(1+np.exp(-Z))
    dZ = dA * s * (1-s)
    return dZ

def relu_backward(dA, cache):
    Z = cache
    dZ = np.array(dA, copy=True)
    dZ[Z <= 0] = 0
    return dZ

现在来计算权重更新所需的dW、db,还有要传到前面一层的梯度

def linear_backward(dZ, cache):
    A_prev, W, b = cache
    m = A_prev.shape[1]

    dW = 1/m * np.dot(dZ, A_prev.T)
    db = 1/m * np.sum(dZ, axis=1, keepdims=True)
    dA_prev = np.dot(W.T, dZ)
    
    return dA_prev, dW, db

def linear_activation_backward(dA, cache, activation):
    linear_cache, activation_cache = cache

    if activation == "relu":
        dZ = relu_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)

    elif activation == "sigmoid":
        dZ = sigmoid_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)

    return dA_prev, dW, db

忘记激活函数或求导公式的同学,这里有一些有用的链接,好好练习下吧:

深度学习之激活函数表 - CSDN博客​blog.csdn.net
0cb4247c792d2e28e53d0802bb6a643b.png
多元复合函数求导法则-维基百科​zh.wikipedia.org

Update parameters 更新权重

在上一步求出所有权重的梯度后,接下来就是更新权重。最常用的梯度下降法就是mini-batch gradient descent,公式如下,α为学习率:

还有很多优化算法,上面的SGD是收敛速度最慢的方法,这里挑选出三个常用的算法:

  • SGD with Momentum(SGDM)

相对于SGD,学习率乘以的不再是梯度,而是前面所产生的多个梯度的指数加权平均值。

  • RMSprop

RMSprop并不像SGDM那样用学习率直接乘以一个梯度加权平均值,它的梯度将会除以一个值,以此来抑制或者增大梯度值。

因为在计算累积梯度时使用了微分平方加权数,如果某一时刻微分平方的值突然变得很大,分母项就会变得更大,更新的梯度项就会变小,直观理解是“抑制”了这次更新造成的震荡。相反,某时刻的微分平方值比较小(更新幅度小),分母项变小,更新的梯度项就会变大,加大了更新的幅度。

  • Adam

Adam是SGDM与RMSprop的结合,与RMSprop的区别在于更新项的分子,RMSprop为dw或者db,Adam则是用了momentum项,如上面的

Momentum项:

均方根项:

更新权重:

附上一个更详细的介绍文章:深度学习优化算法解析(Momentum, RMSProp, Adam)

Training pipeline 训练步骤

上面介绍完所有的组件了,现在把这些组件全部结合起来,我们用伪代码来表示整个训练过程:

X,Y = load_data() # 导入训练数据
parameters = initialize_parameters() # 初始化所有权重
for _ in range(num_epochs): # 循环训练多轮
    AL, cache = linear_activation_forward(X, parameters) # 前向传播计算激活
    cost = compute_cost(AL, Y) # 计算损失
    grads = linear_activation_backward(AL, cost, cache) # 后向传播计算梯度
    parameters = update_parameters(parameters, grads, learning_rate) # 更新权重

以上就是神经网络训练的步骤,其实目前所有的深度学习框架都是遵循着以上的流程进行模型训练,这里以tensorflow的伪代码作为例子:

x_train, y_train = load_data()
X = tf.placeholder(......) # 定义数据集占位符
Y = tf.placeholder(......)

W = tf.Variable(......) # 定义变量
b = tf.Variable(......)

init = tf.global_variables_initializer() # 定义初始化变量的方式

Z = tf.add(tf.matmul(W, X), b) # 定义计算图

loss = tf.losses...... # 定义损失函数,Z和Y作为参数

optimizer = tf.train.AdamOptimizer().minimize(loss) # 定义优化器

with tf.Session as sess:
    sess.run(init) # 开始执行变量初始化
    for _ in range(num_epochs): # 循环训练多轮
        _ , cost = sess.run([optimizer, cost], feed_dict={X:x_train, Y:y_train }) # 执行训练

从上面tensorflow的伪代码可以看到,训练流程跟上面纯numpy训练代码的流程是基本一致的(除了后向传播过程不用重新写),只不过tensorflow习惯先定义好所有操作,例如初始化操作、计算图和优化目标,最后再建立一个session来执行所有的操作。万变不离其宗,换成其它深度学习框架也一样,所以当你懂得底层原理后,all you have to do is find the API.

Summary 总结

上面提到的所有概念,全部都是基!础!知!识!,要想成为一个“合格”的算法工程师或者研究员,这些概念是必须精通的(所以面试官也会经常从上面这些知识点来考你哦)。但对于零基础入门者来说,这种自下而上的学习方法可能会比较打击信心,所以很多人也是看着网上一些什么《十分钟使用tensorflow搭建神经网络》《三天tensorflow从入门到精通》的文章来写一些demo,跑一些简单数据来学习使用框架。笔者觉得这并没有什么毛病,因为大家都这样过来的,早期早点出成果可以增强学习兴趣与信心。但入门之后,建议大家还是多巩固基础,努力去了解背后的原理,这样你的技术水平才能上一个新的台阶。

对了,笔者前面挖了一个坑,编程最麻烦的是数据的预处理及输入,相信很多工程师都有这种感受,本文篇幅有限暂时不在本文详细讨论,可到本人的知乎keras专栏去了解一些有关数据预处理及输入的方式,感受一下。

Justin ho:图片数据集太少?看我七十二变,Keras Image Data Augmentation 各参数详解​zhuanlan.zhihu.com
a132deacfd189d95fe1b9f74db8c8490.png
Justin ho:FancyKeras-数据的输入(花式)​zhuanlan.zhihu.com
fdb4492aaffdbeb41164fd42446ef834.png

本文已收录于本人的个人网站,欢迎浏览收藏:https://ijst.me/wp

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值