cs231n笔记5—反向传播/神经网络

反向传播

问题陈述:这节的核心问题是,给定函数f(x),其中x是输入数据的向量,需要计算函数f关于x的梯度,也就是∇f(x)

反向传播是利用链式法则递归计算表达式的梯度的方法。理解反向传播过程及其精妙之处,对于理解、实现、设计和调试神经网络非常关键

1 简单表达式和理解梯度
从简单表达式入手可以为复杂表达式打好符号和规则基础。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上式是说,如果该变量比另一个变量大,那么梯度是1,反之为0

2 使用链式法则计算复合表达式
在这里插入图片描述
在这里插入图片描述

输入计算到输出(绿色),反向传播从尾部开始,根据链式法则递归地向前计算梯度(显示为红色)
示例代码:

# 设置输入值
x = -2; y = 5; z = -4

# 进行前向传播
q = x + y # q becomes 3
f = q * z # f becomes -12

# 进行反向传播:
# 首先回传到 f = q * z
dfdz = q # df/dz = q, 所以关于z的梯度是3
dfdq = z # df/dq = z, 所以关于q的梯度是-4
# 现在回传到q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1. 这里的乘法是因为链式法则
dfdy = 1.0 * dfdq # dq/dy = 1

最后得到变量的梯度[dfdx,dfdy,dfdz],它们告诉我们函数ff对于变量[x,y,z]的敏感程度。这是一个最简单的反向传播。一般会使用一个更简洁的表达符号,用dq来代替dfdq,且总是假设梯度是关于最终输出的。

3 反向传播的直观理解
反向传播是一个优美的局部过程。在整个计算线路图中,每个门单元都会得到一些输入并立即计算两个东西:这个门的输出值,和其输出值关于输入值的局部梯度。

门单元完成这两件事是完全独立的,它不需要知道计算线路中的其他细节。然而,一旦前向传播完毕,在反向传播的过程中,门单元门将最终获得整个网络的最终输出值在自己的输出值上的梯度。链式法则指出,门单元应该将回传的梯度乘以它对其的输入的局部梯度,从而得到整个网络的输出对该门单元的每个输入值的梯度。
模块化:Sigmoid例子
上面介绍的门是相对随意的。任何可微分的函数都可以看做门。可以将多个门组合成一个门,也可以根据需要将一个函数分拆成多个门。现在看看一个表达式:
在这里插入图片描述
在后面的课程中可以看到,这个表达式描述了一个含输入xx和权重ww的2维的神经元,该神经元使用了sigmoidsigmoid激活函数。但是现在只是看做是一个简单的输入为xx和ww,输出为一个数字的函数。这个函数是由多个门组成的。除了上文介绍的加法门,乘法门,取最大值门,还有下面这4种
在这里插入图片描述
整个计算线路如下:
在这里插入图片描述
使用sigmoidsigmoid激活函数的2维神经元的例子。输入是[x0,x1]],可学习的权重是[w0,w1,w2]。一会儿会看见,这个神经元对输入数据做点积运算,然后其激活数据被sigmoid函数挤压到0到1之间。

在上面的例子中可以看见一个函数操作的长链条,链条上的门都对w和x的点积结果进行操作。该函数被称为sigmoid函数σ(x)。sigmoid函数关于其输入的求导是可以简化的(使用了在分子上先加后减1的技巧):
在这里插入图片描述
可以看到梯度计算简单了很多。举个例子,sigmoidsigmoid表达式输入为1.0,则在前向传播中计算出输出为0.73。根据上面的公式,局部梯度为(1−0.73)∗0.73 =0.2(实际是0.1966) ,和之前的计算流程比起来,现在的计算使用一个单独的简单表达式即可。
在这里插入图片描述
代码:

w = [2,-3,-3] # 假设一些随机数据和权重
x = [-1, -2]

# 前向传播
import math
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid函数

# 对神经元反向传播
ddot = (1 - f) * f # 点积变量的梯度, 使用sigmoid函数求导
dx = [w[0] * ddot, w[1] * ddot] # 回传到x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # 回传到w
# 完成!得到输入的梯度

4 分段反向传播
在这里插入图片描述
首先要说的是,这个函数完全没用,读者是不会用到它来进行梯度计算的,这里只是用来作为实践反向传播的一个例子,需要强调的是,如果对x或y进行微分运算,运算结束后会得到一个巨大而复杂的表达式。然而做如此复杂的运算实际上并无必要,因为我们不需要一个明确的函数来计算梯度,只需知道如何使用反向传播计算梯度即可。下面是构建前向传播的代码模式:

sigy = 1.0 / (1 + math.exp(-y)) # 分子中的sigmoid    			  #(1)
num = x + sigy # 分子                                       	  #(2)
sigx = 1.0 / (1 + math.exp(-x)) # 分母中的sigmoid        		  #(3)
xpy = x + y                                                   	  #(4)
xpysqr = xpy**2                                                   #(5)
den = sigx + xpysqr # 分母                                 		  #(6)
invden = 1.0 / den                                                #(7)
f = num * invden # 搞定!                                		  #(8)


#反向传播,回传,好绕啊,要细心看o
# 回传 f = num * invden
dnum = invden # 分子的梯度                                         #(8)
dinvden = num                                                     #(8)
# 回传 invden = 1.0 / den 
dden = (-1.0 / (den**2)) * dinvden                                #(7)
# 回传 den = sigx + xpysqr
dsigx = (1) * dden                                                #(6)
dxpysqr = (1) * dden                                              #(6)
# 回传 xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr                                        #(5)
# 回传 xpy = x + y
dx = (1) * dxpy                                                   #(4)
dy = (1) * dxpy                                                   #(4)
# 回传 sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below  #(3)
# 回传 num = x + sigy
dx += (1) * dnum                                                  #(2)
dsigy = (1) * dnum                                                #(2)
# 回传 sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy                                 #(1)
# 完成! 嗷~~

需要注意的一些东西:

对前向传播变量进行缓存:在计算反向传播时,前向传播过程中得到的一些中间变量非常有用。在实际操作中,最好代码实现对于这些中间变量的缓存,这样在反向传播的时候也能用上它们。如果这样做过于困难,也可以(但是浪费计算资源)重新计算它们。

在不同分支的梯度要相加:如果变量x,y在前向传播的表达式中出现多次,那么进行反向传播的时候就要非常小心**,使用+=而不是==来累计这些变量的梯度**(不然就会造成覆写)。这是遵循了在微积分中的多元链式法则,该法则指出如果变量在线路中分支走向不同的部分,那么梯度在回传的时候,就应该进行累加

4 回传流中的模式

在这里插入图片描述
加法操作 将梯度相等地分发给它的输入,这一行为与输入值在前向传播时的值无关
取最大操作 将梯度路由给更大的输入,这个输入是在前向传播中值最大的那个输入。这是因为在取最大值门中,最高值的局部梯度是 1.0,其余的是 0。上例中,取最大值门将梯度 2.00 转给了z变量,因为z的值比w高,于是w的梯度保持为 0。
乘法门 拿取输入激活数据,对它们进行交换,然后乘以梯度。上例中,x的梯度是 -4.00 x 2.00=-8.00。

4 用向量化操作计算梯度
上述内容考虑的都是单个变量情况,但是所有概念都适用于矩阵和向量操作。然而,在操作的时候要注意关注维度和转置操作。
矩阵相乘的梯度:可能最有技巧的操作是矩阵相乘(也适用于矩阵和向量,向量和向量相乘)的乘法操作:

# 前向传播
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)

# 假设我们得到了D的梯度
dD = np.random.randn(*D.shape) # 和D一样的尺寸,注意:*表示的是将调用的多个参数放入元组
dW = dD.dot(X.T) #.T就是对矩阵进行转置
dX = W.T.dot(dD)

提示:要分析维度!注意不需要去记忆dW和dX的表达,因为它们很容易通过维度推导出来。例如,权重的梯度dW的尺寸肯定和权重矩阵W的尺寸是一样的,而这又是由X和dD的矩阵乘法决定的(在上面的例子中X和W都是数字不是矩阵)。总有一个方式是能够让维度之间能够对的上的。例如,X的尺寸是 [10x3],dD的尺寸是 [5x3],如果你想要dW和W的尺寸是 [5x10],那就要dD.dot(X.T)

神经网络

人们将神经网络与大脑和神经学上的其他种类做了大量类比,首先将它看做与大脑无关的一组函数,如之前的线性评分函数f=Wx,将它作为需要优化的函数来使用。我们不用单变换的,如果依旧想在神经网络里用简单的方式,只需将两个变换结合在一起,顶层有一个线性变换和另一个构成两层神经网络,这个0和W间取最大值的非线性函数h将线性层的最大值作为输出。这些非线性计算h很重要,因为如果只是在顶层将线性层合在一起,它们会被简化成一个线性函数,所以首先有线性层,再有非线性计算,继而在顶层再加入另一个线性层,最后可得到一个评分函数s,输出评分向量。
在这里插入图片描述
这里的 h 是W1中每个模板出现的得分,然后 W2 加权所有的中间变量得分,来得到分类的最后得分。W1像之前一样来寻找那样的模板,W2是这些得分的加权。非线性通常出现在h之前,这里h=max(0,W1*x)。
矩阵W2是h里所有向量的权重。

1 神经网络比作神经元

回顾每个计算节点,方法都是类似的,计算图里的节点相互连接,我们需要输入信号x传入神经元,所有的x输入量比如x0、x1、x2等采用比如赋予权重W的方法,叠加汇合到一起,做一些运算,再把所有结果整合起来,得到一个激活函数(activation function),将它应用在神经元的端部,得到值作为输出,输出到相关联的神经元。

神经元将信号传递至相邻神经元的传递信号,通过采用离散尖峰,如果触发非常迅速,然后通过神经元,就会产生一类强信号,我们可以认为是经过激活函数计算后的值,我们将要传递下去的是放电率。
在这里插入图片描述

2 激活函数(Activation Function)就是在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端。在神经元中,输入的 inputs 通过加权,求和后,还被作用了一个函数,这个函数就是激活函数。引入激活函数是为了增加神经网络模型的非线性,使得神经网络可以任意逼近任何非线性函数,这样神经网络就可以应用到众多的非线性模型中。没有激活函数的每层都相当于矩阵相乘。如果不用激励函数,每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合,这种情况就是最原始的感知机(Perceptron)。

3 结构
在这里插入图片描述
输出层,和神经网络中其他层不同,输出层的神经元一般是不会有激活函数的(或者也可以认为它们有一个线性相等的激活函数)。这是因为最后的输出层大多用于表示分类评分值,因此是任意值的实数,或者某种实数值的目标数(比如在回归中)。

4 前向计算

class Neuron(object):
  # ... 
  def forward(inputs):
    """ 假设输入和权重是1-D的numpy数组,偏差是一个数字 """
    cell_body_sum = np.sum(inputs * self.weights) + self.bias
    firing_rate = 1.0 / (1.0 + math.exp(-cell_body_sum)) # sigmoid激活函数
    return firing_rate

把它们按照矩阵或者向量的形式都写出来,用sigmoid函数来做非线性函数f,输入数据x作为一个向量,这样就可以进行第一次矩阵乘法,W1是在前一步,之后用f进行一步非线性计算得到第一个隐藏层h1,接着进行第二次矩阵乘法,可以得到第二个隐藏层h2,最后可以得到输出值,这样就可以写一个3层神经网络的前向传播了

一个3层神经网络的前向传播:

f = lambda x: 1.0/(1.0 + np.exp(-x)) # 激活函数(用的sigmoid)
x = np.random.randn(3, 1) # 含3个数字的随机输入向量(3x1)
h1 = f(np.dot(W1, x) + b1) # 计算第一个隐层的激活数据(4x1)
h2 = f(np.dot(W2, h1) + b2) # 计算第二个隐层的激活数据(4x1)
out = np.dot(W3, h2) + b3 # 神经元输出(1x1)

参考:CS231n课程笔记翻译知乎专栏——智能单元
CS231n深度视觉笔记https://zhuanlan.zhihu.com/p/65271519

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值