九. 介绍神经网络———反向传播
杜客 Source
译者注:本文智能单元首发,译自斯坦福CS231n课程笔记Backprop Note__,课程教师Andrej Karpathy__授权翻译。本篇教程由杜客翻译完成,堃堃和巩子嘉进行校对修改。译文含公式和代码,建议PC端阅读。
内容列表:
目标:本节将帮助读者对反向传播形成直观而专业的理解。反向传播是利用链式法则递归计算表达式的梯度的方法。理解反向传播过程及其精妙之处,对于理解、实现、设计和调试神经网络非常关键。
问题陈述:这节的核心问题是:给定函数 ,其中是输入数据的向量,需要计算函数关于的梯度,也就是。
目标:之所以关注上述问题,是因为在神经网络中对应的是损失函数(),输入里面包含训练数据和神经网络的权重。举个例子,损失函数可以是SVM的损失函数,输入则包含了训练数据、权重和偏差。注意训练集是给定的(在机器学习中通常都是这样),而权重是可以控制的变量。因此,即使能用反向传播计算输入数据 上的梯度,但在实践为了进行参数更新,通常也只计算参数(比如)的梯度。然而 的梯度有时仍然是有用的:比如将神经网络所做的事情可视化便于直观理解的时候,就能用上。
如果读者之前对于利用链式法则计算偏微分已经很熟练,仍然建议浏览本篇笔记。因为它呈现了一个相对成熟的反向传播视角,在该视角中能看见基于实数值回路的反向传播过程,而对其细节的理解和收获将帮助读者更好地通过本课程。
从简单表达式入手可以为复杂表达式打好符号和规则基础。先考虑一个简单的二元乘法函数。对两个输入变量分别求偏导数还是很简单的:
解释:牢记这些导数的意义:函数变量在某个点周围的极小区域内变化,而导数就是变量变化导致的函数在该方向上的变化率。
注意等号左边的分号和等号右边的分号不同,不是代表分数。相反,这个符号表示操作符被应用于函数,并返回一个不同的函数(导数)。对于上述公式,可以认为值非常小,函数可以被一条直线近似,而导数就是这条直线的斜率。换句话说,每个变量的导数指明了整个表达式对于该变量的值的敏感程度。比如,若,则,的导数。这就说明如果将变量的值变大一点,整个表达式的值就会变小(原因在于负号),而且变小的量是变大的量的三倍。通过重新排列公式可以看到这一点()。同样,因为,可以知道如果将的值增加,那么函数的输出也将增加(原因在于正号),且增加量是。
> 函数关于每个变量的导数指明了整个表达式对于该变量的敏感程度。
如上所述,梯度是偏导数的向量,所以有。即使是梯度实际上是一个向量,仍然通常使用类似"x上的梯度"的术语,而不是使用如"x的偏导数"的正确说法,原因是因为前者说起来简单。
我们也可以对加法操作求导:
这就是说,无论其值如何,的导数均为1。这是有道理的,因为无论增加中任一个的值,函数的值都会增加,并且增加的变化率独立于的具体值(情况和乘法操作不同)。取最大值操作也是常常使用的:
上式是说,如果该变量比另一个变量大,那么梯度是1,反之为0。例如,若,那么max是4,所以函数对于就不敏感。也就是说,在上增加,函数还是输出为4,所以梯度是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],它们告诉我们函数f对于变量[x, y, z]的敏感程度。这是一个最简单的反向传播。一般会使用一个更简洁的表达符号,这样就不用写df了。这就是说,用dq来代替dfdq,且总是假设梯度是关于最终输出的。
这次计算可以被可视化为如下计算线路图像:
————————————————————————————————————————
上图的真实值计算线路展示了计算的视觉化过程。前向传播从输入计算到输出(绿色),反向传播从尾部开始,根据链式法则递归地向前计算梯度(显示为红色),一直到网络的输入端。可以认为,梯度是从计算链路中回流。
————————————————————————————————————————
反向传播是一个优美的局部过程。在整个计算线路图中,每个门单元都会得到一些输入并立即计算两个东西:1. 这个门的输出值,和2.其输出值关于输入值的局部梯度。门单元完成这两件事是完全独立的,它不需要知道计算线路中的其他细节。然而,一旦前向传播完毕,在反向传播的过程中,门单元门将最终获得整个网络的最终输出值在自己的输出值上的梯度。链式法则指出,门单元应该将回传的梯度乘以它对其的输入的局部梯度,从而得到整个网络的输出对该门单元的每个输入值的梯度。
> 这里对于每个输入的乘法操作是基于链式法则的。该操作让一个相对独立的门单元变成复杂计算线路中不可或缺的一部分,这个复杂计算线路可以是神经网络等。
下面通过例子来对这一过程进行理解。加法门收到了输入[-2, 5],计算输出是3。既然这个门是加法操作,那么对于两个输入的局部梯度都是+1。网络的其余部分计算出最终值为-12。在反向传播时将递归地使用链式法则,算到加法门(是乘法门的输入)的时候,知道加法门的输出的梯度是-4。如果网络如果想要输出值更高,那么可以认为它会想要加法门的输出更小一点(因为负号),而且还有一个4的倍数。继续递归并对梯度使用链式法则,加法门拿到梯度,然后把这个梯度分别乘到每个输入值的局部梯度(就是让-4乘以x和y的局部梯度,x和y的局部梯度都是1,所以最终都是-4)。可以看到得到了想要的效果:如果x,y减小(它们的梯度为负),那么加法门的输出值减小,这会让乘法门的输出值增大。
因此,反向传播可以看做是门单元之间在通过梯度信号相互通信,只要让它们的输入沿着梯度方向变化,无论它们自己的输出值在何种程度上升或降低,都是为了让整个网络的输出值更高。
上面介绍的门是相对随意的。任何可微分的函数都可以看做门。可以将多个门组合成一个门,也可以根据需要将一个函数分拆成多个门。现在看看一个表达式:
在后面的课程中可以看到,这个表达式描述了一个含输入x和权重w的2维的神经元,该神经元使用了_sigmoid激活_函数。但是现在只是看做是一个简单的输入为x和w,输出为一个数字的函数。这个函数是由多个门组成的。除了上文介绍的加法门,乘法门,取最大值门,还有下面这4种:
其中,函数使用对输入值进行了常量的平移,将输入值扩大了常量倍。它们是加法和乘法的特例,但是这里将其看做一元门单元,因为确实需要计算常量的梯度。整个计算线路如下:
———————————————————————————————————————
使用sigmoid激活函数的2维神经元的例子。输入是[x0, x1],可学习的权重是[w0, w1, w2]。一会儿会看见,这个神经元对输入数据做点积运算,然后其激活数据被sigmoid函数挤压到0到1之间。
————————————————————————————————————————
在上面的例子中可以看见一个函数操作的长链条,链条上的门都对w和x的点积结果进行操作。该函数被称为sigmoid函数。sigmoid函数关于其输入的求导是可以简化的(使用了在分子上先加后减1的技巧):
可以看到梯度计算简单了很多。举个例子,sigmoid表达式输入为1.0,则在前向传播中计算出输出为0.73。根据上面的公式,局部梯度为(1-0.73)*0.73~=0.2,和之前的计算流程比起来,现在的计算使用一个单独的简单表达式即可。因此,在实际的应用中将这些操作装进一个单独的门单元中将会非常有用。该神经元反向传播的代码实现如下:
w = [2,-3,-3] # 假设一些随机数据和权重
x = [-1, -2]
# 前向传播
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
# 完成!得到输入的梯度
实现提示:分段反向传播。上面的代码展示了在实际操作中,为了使反向传播过程更加简洁,把向前传播分成不同的阶段将是很有帮助的。比如我们创建了一个中间变量dot,它装着w和x的点乘结果。在反向传播的时,就可以(反向地)计算出装着w和x等的梯度的对应的变量(比如ddot,dx和dw)。
本节的要点就是展示反向传播的细节过程,以及前向传播过程中,哪些函数可以被组合成门,从而可以进行简化。知道表达式中哪部分的局部梯度计算比较简洁非常有用,这样他们可以"链"在一起,让代码量更少,效率更高。
看另一个例子。假设有如下函数:
首先要说的是,这个函数完全没用,读者是不会用到它来进行梯度计算的,这里只是用来作为实践反向传播的一个例子,需要强调的是,如果对或进行微分运算,运算结束后会得到一个巨大而复杂的表达式。然而做如此复杂的运算实际上并无必要,因为我们不需要一个明确的函数来计算梯度,只需知道如何使用反向传播计算梯度即可。下面是构建前向传播的代码模式:
x = 3 # 例子数值
y = -4
# 前向传播
sigy = 1.0 / (1 + math.exp(-y)) # 分子中的sigmoi #(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′|┛ 嗷~~,到了表达式的最后,就完成了前向传播。注意在构建代码s时创建了多个中间变量,每个都是比较简单的表达式,它们计算局部梯度的方法是已知的。这样计算反向传播就简单了:我们对前向传播时产生每个变量(sigy, num, sigx, xpy, xpysqr, den, invden)进行回传。我们会有同样数量的变量,但是都以d开头,用来存储对应变量的梯度。注意在反向传播的每一小块中都将包含了表达式的局部梯度,然后根据使用链式法则乘以上游梯度。对于每行代码,我们将指明其对应的是前向传播的哪部分。
# 回传 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在前向传播的表达式中出现多次,那么进行反向传播的时候就要非常小心,使用**+=而不是=**来累计这些变量的梯度(不然就会造成覆写)。这是遵循了在微积分中的_多元链式法则_,该法则指出如果变量在线路中分支走向不同的部分,那么梯度在回传的时候,就应该进行累加。
一个有趣的现象是在多数情况下,反向传播中的梯度可以被很直观地解释。例如神经网络中最常用的加法、乘法和取最大值这三个门单元,它们在反向传播过程中的行为都有非常简单的解释。先看下面这个例子:
——————————————————————————————————————————
一个展示反向传播的例子。加法操作将梯度相等地分发给它的输入。取最大操作将梯度路由给更大的输入。乘法门拿取输入激活数据,对它们进行交换,然后乘以梯度。
——————————————————————————————————————————
从上例可知:
加法门单元把输出的梯度相等地分发给它所有的输入,这一行为与输入值在前向传播时的值无关。这是因为加法操作的局部梯度都是简单的+1,所以所有输入的梯度实际上就等于输出的梯度,因为乘以1.0保持不变。上例中,加法门把梯度2.00不变且相等地路由给了两个输入。
取最大值门单元对梯度做路由。和加法门不同,取最大值门将梯度转给其中一个输入,这个输入是在前向传播中值最大的那个输入。这是因为在取最大值门中,最高值的局部梯度是1.0,其余的是0。上例中,取最大值门将梯度2.00转给了z变量,因为z的值比w高,于是w的梯度保持为0。
乘法门单元相对不容易解释。它的局部梯度就是输入值,但是是相互交换之后的,然后根据链式法则乘以输出值的梯度。上例中,x的梯度是-4.00x2.00=-8.00。
非直观影响及其结果。注意一种比较特殊的情况,如果乘法门单元的其中一个输入非常小,而另一个输入非常大,那么乘法门的操作将会不是那么直观:它将会把大的梯度分配给小的输入,把小的梯度分配给大的输入。在线性分类器中,权重和输入是进行点积,这说明输入数据的大小对于权重梯度的大小有影响。例如,在计算过程中对所有输入数据样本乘以1000,那么权重的梯度将会增大1000倍,这样就必须降低学习率来弥补。这就是为什么数据预处理关系重大,它即使只是有微小变化,也会产生巨大影响。对于梯度在计算线路中是如何流动的有一个直观的理解,可以帮助读者调试网络。
上述内容考虑的都是单个变量情况,但是所有概念都适用于矩阵和向量操作。然而,在操作的时候要注意关注维度和转置操作。
矩阵相乘的梯度:可能最有技巧的操作是矩阵相乘(也适用于矩阵和向量,向量和向量相乘)的乘法操作:
# 前向传播
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)。
使用小而具体的例子:有些读者可能觉得向量化操作的梯度计算比较困难,建议是写出一个很小很明确的向量化例子,在纸上演算梯度,然后对其一般化,得到一个高效的向量化操作形式。
在下节课中,将会开始定义神经网络,而反向传播使我们能高效计算神经网络各个节点关于损失函数的梯度。换句话说,我们现在已经准备好训练神经网络了,本课程最困难的部分已经过去了!ConvNets相比只是向前走了一小步。
反向传播笔记全文翻译完。
十. 介绍神经网络——神经网络(1)
9 months ago
译者注:本文智能单元首发,译自斯坦福CS231n课程笔记Neural Nets notes 1__,课程教师Andrej Karpathy__授权翻译。本篇教程由杜客翻译完成,巩子嘉和堃堃进行校对修改。译文含公式和代码,建议PC端阅读。
内容列表:
在不诉诸大脑的类比的情况下,依然是可以对神经网络算法进行介绍的。在线性分类一节中,在给出图像的情况下,是使用来计算不同视觉类别的评分,其中是一个矩阵,是一个输入列向量,它包含了图像的全部像素数据。在使用数据库CIFAR-10的案例中,是一个[3072x1]的列向量,是一个[10x3072]的矩阵,所以输出的评分是一个包含10个分类评分的向量。
神经网络算法则不同,它的计算公式是。其中的含义是这样的:举个例子来说,它可以是一个[100x3072]的矩阵,其作用是将图像转化为一个100维的过渡向量。函数是非线性的,它会作用到每个元素。这个非线性函数有多种选择,后续将会学到。但这个形式是一个最常用的选择,它就是简单地设置阈值,将所有小于0的值变成0。最终,矩阵的尺寸是[10x100],因此将得到10个数字,这10个数字可以解释为是分类的评分。注意非线性函数在计算上是至关重要的,如果略去这一步,那么两个矩阵将会合二为一,对于分类的评分计算将重新变成关于输入的线性函数。这个非线性函数就是_改变_的关键点。参数将通过随机梯度下降来学习到,他们的梯度在反向传播过程中,通过链式法则来求导计算得出。
一个三层的神经网络可以类比地看做,其中是需要进行学习的参数。中间隐层的尺寸是网络的超参数,后续将学习如何设置它们。现在让我们先从神经元或者网络的角度理解上述计算。
神经网络算法领域最初是被对生物神经系统建模这一目标启发,但随后与其分道扬镳,成为一个工程问题,并在机器学习领域取得良好效果。然而,讨论将还是从对生物系统的一个高层次的简略描述开始,因为神经网络毕竟是从这里得到了启发。
大脑的基本计算单位是神经元(neuron)。人类的神经系统中大约有860亿个神经元,它们被大约10^14-10^15个突触(synapses)连接起来。下面图表的左边展示了一个生物学的神经元,右边展示了一个常用的数学模型。每个神经元都从它的树突获得输入信号,然后沿着它唯一的**轴突(axon)**产生输出信号。轴突在末端会逐渐分枝,通过突触和其他神经元的树突相连。
在神经元的计算模型中,沿着轴突传播的信号(比如)将基于突触的突触强度(比如),与其他神经元的树突进行乘法交互(比如)。其观点是,突触的强度(也就是权重),是可学习的且可以控制一个神经元对于另一个神经元的影响强度(还可以控制影响方向:使其兴奋(正权重)或使其抑制(负权重))。在基本模型中,树突将信号传递到细胞体,信号在细胞体中相加。如果最终之和高于某个阈值,那么神经元将会_激活_,向其轴突输出一个峰值信号。在计算模型中,我们假设峰值信号的准确时间点不重要,是激活信号的频率在交流信息。基于这个_速率编码_的观点,将神经元的激活率建模为激活函数(activation function),它表达了轴突上激活信号的频率。由于历史原因,激活函数常常选择使用sigmoid函数,该函数输入实数值(求和后的信号强度),然后将输入值压缩到0-1之间。在本节后面部分会看到这些激活函数的各种细节。
————————————————————————————————————————
————————————————————————————————————————
一个神经元前向传播的实例代码如下:
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函数。在本节的末尾部分将介绍不同激活函数的细节。
粗糙模型:要注意这个对于生物神经元的建模是非常粗糙的:在实际中,有很多不同类型的神经元,每种都有不同的属性。生物神经元的树突可以进行复杂的非线性计算。突触并不就是一个简单的权重,它们是复杂的非线性动态系统。很多系统中,输出的峰值信号的精确时间点非常重要,说明速率编码的近似是不够全面的。鉴于所有这些已经介绍和更多未介绍的简化,如果你画出人类大脑和神经网络之间的类比,有神经科学背景的人对你的板书起哄也是非常自然的。如果你对此感兴趣,可以看看这份评论__或者最新的另一份__。
神经元模型的前向计算数学公式看起来可能比较眼熟。就像在线性分类器中看到的那样,神经元有能力"喜欢"(激活函数值接近1),或者不喜欢(激活函数值接近0)输入空间中的某些线性区域。因此,只要在神经元的输出端有一个合适的损失函数,就能让单个神经元变成一个线性分类器。
二分类Softmax分类器。举例来说,可以把看做其中一个分类的概率,其他分类的概率为,因为它们加起来必须为1。根据这种理解,可以得到交叉熵损失,这个在线性分一节中已经介绍。然后将它最优化为二分类的Softmax分类器(也就是逻辑回归)。因为sigmoid函数输出限定在0-1之间,所以分类器做出预测的基准是神经元的输出是否大于0.5。
二分类SVM分类器。或者可以在神经元的输出外增加一个最大边界折叶损失(max-margin hinge loss)函数,将其训练成一个二分类的支持向量机。
理解正则化。在SVM/Softmax的例子中,正则化损失从生物学角度可以看做_逐渐遗忘_,因为它的效果是让所有突触权重在参数更新过程中逐渐向着0变化。
> 一个单独的神经元可以用来实现一个二分类分类器,比如二分类的Softmax或者SVM分类器。
每个激活函数(或非线性函数)的输入都是一个数字,然后对其进行某种固定的数学操作。下面是在实践中可能遇到的几种激活函数:
————————————————————————————————————————
左边是Sigmoid非线性函数,将实数压缩到[0,1]之间。右边是tanh函数,将实数压缩到[-1,1]。
————————————————————————————————————————
**Sigmoid。**sigmoid非线性函数的数学公式是,函数图像如上图的左边所示。在前一节中已经提到过,它输入实数值并将其"挤压"到0到1范围内。更具体地说,很大的负数变成0,很大的正数变成1。在历史上,sigmoid函数非常常用,这是因为它对于神经元的激活频率有良好的解释:从完全不激活(0)到在求和后的最大频率处的完全饱和(saturated)的激活(1)。然而现在sigmoid函数已经不太受欢迎,实际很少使用了,这是因为它有两个主要缺点:
**Tanh。**tanh非线性函数图像如上图右边所示。它将实数值压缩到[-1,1]之间。和sigmoid神经元一样,它也存在饱和问题,但是和sigmoid神经元不同的是,它的输出是零中心的。因此,在实际操作中,tanh非线性函数比sigmoid非线性函数更受欢迎。注意tanh神经元是一个简单放大的sigmoid神经元,具体说来就是:。
————————————————————————————————————————
左边是ReLU(校正线性单元:Rectified Linear Unit)激活函数,当时函数值为0。当函数的斜率为1。右边是从 Krizhevsky__等的论文中截取的图表,指明使用ReLU比使用tanh的收敛快6倍。
————————————————————————————————————————
**ReLU。**在近些年ReLU变得非常流行。它的函数公式是。换句话说,这个激活函数就是一个关于0的阈值(如上图左侧)。使用ReLU有以下一些优缺点:
**Leaky ReLU。**Leaky ReLU是为解决"ReLU死亡"问题的尝试。ReLU中当x<0时,函数值为0。而Leaky ReLU则是给出一个很小的负数梯度值,比如0.01。所以其函数公式为其中是一个小的常量。有些研究者的论文指出这个激活函数表现很不错,但是其效果并不是很稳定。Kaiming He等人在2015年发布的论文Delving Deep into Rectifiers__中介绍了一种新方法PReLU,把负区间上的斜率当做每个神经元中的一个参数。然而该激活函数在在不同任务中均有益处的一致性并没有特别清晰。
**Maxout。**一些其他类型的单元被提了出来,它们对于权重和数据的内积结果不再使用函数形式。一个相关的流行选择是Maxout(最近由Goodfellow__等发布)神经元。Maxout是对ReLU和leaky ReLU的一般化归纳,它的函数是:。ReLU和Leaky ReLU都是这个公式的特殊情况(比如ReLU就是当的时候)。这样Maxout神经元就拥有ReLU单元的所有优点(线性操作和不饱和),而没有它的缺点(死亡的ReLU单元)。然而和ReLU对比,它每个神经元的参数数量增加了一倍,这就导致整体参数的数量激增。
以上就是一些常用的神经元及其激活函数。最后需要注意一点:在同一个网络中混合使用不同类型的神经元是非常少见的,虽然没有什么根本性问题来禁止这样做。
一句话:"那么该用那种呢?"用ReLU非线性函数。注意设置好学习率,或许可以监控你的网络中死亡的神经元占的比例。如果单元死亡问题困扰你,就试试Leaky ReLU或者Maxout,不要再用sigmoid了。也可以试试tanh,但是其效果应该不如ReLU或者Maxout。
神经网络笔记1(上)完。
十. 介绍神经网络——神经网络(2)
9 months ago
译者注:本文智能单元首发,译自斯坦福CS231n课程笔记Neural Nets notes 1__,课程教师Andrej Karpathy__授权翻译。本篇教程由杜客翻译完成,李艺颖和堃堃进行校对修改。译文含公式和代码,建议PC端阅读。
内容列表:
将神经网络算法以神经元的形式图形化。神经网络被建模成神经元的集合,神经元之间以无环图的形式进行连接。也就是说,一些神经元的输出是另一些神经元的输入。在网络中是不允许循环的,因为这样会导致前向传播的无限循环。通常神经网络模型中神经元是分层的,而不是像生物神经元一样聚合成大小不一的团状。对于普通神经网络,最普通的层的类型是全连接层(fully-connected layer)。全连接层中的神经元与其前后两层的神经元是完全成对连接的,但是在同一个全连接层内的神经元之间没有连接。下面是两个神经网络的图例,都使用的全连接层:
————————————————————————————————————————
左边是一个2层神经网络,隐层由4个神经元(也可称为单元(unit))组成,输出层由2个神经元组成,输入层是3个神经元。右边是一个3层神经网络,两个含4个神经元的隐层。注意:层与层之间的神经元是全连接的,但是层内的神经元不连接。
————————————————————————————————————————
**命名规则。**当我们说N层神经网络的时候,我们没有把输入层算入。因此,单层的神经网络就是没有隐层的(输入直接映射到输出)。因此,有的研究者会说逻辑回归或者SVM只是单层神经网络的一个特例。研究者们也会使用_人工神经网络(_Artificial Neural Networks _缩写ANN)_或者_多层感知器(_Multi-Layer Perceptrons _缩写__MLP)_来指代神经网络。很多研究者并不喜欢神经网络算法和人类大脑之间的类比,他们更倾向于用_单元(unit)_而不是神经元作为术语。
**输出层。**和神经网络中其他层不同,输出层的神经元一般是不会有激活函数的(或者也可以认为它们有一个线性相等的激活函数)。这是因为最后的输出层大多用于表示分类评分值,因此是任意值的实数,或者某种实数值的目标数(比如在回归中)。
**确定网络尺寸。**用来度量神经网络的尺寸的标准主要有两个:一个是神经元的个数,另一个是参数的个数,用上面图示的两个网络举例:
为了方便对比,现代卷积神经网络能包含约1亿个参数,可由10-20层构成(这就是深度学习)。然而,_有效(effective)_连接的个数因为参数共享的缘故大大增多。在后面的卷积神经网络内容中我们将学习更多。
不断重复的矩阵乘法与激活函数交织。将神经网络组织成层状的一个主要原因,就是这个结构让神经网络算法使用矩阵向量操作变得简单和高效。用上面那个3层神经网络举例,输入是[3x1]的向量。一个层所有连接的强度可以存在一个单独的矩阵中。比如第一个隐层的权重W1是[4x3],所有单元的偏置储存在b1中,尺寸[4x1]。这样,每个神经元的权重都在W1的一个行中,于是矩阵乘法**np.dot(W1, x)**就能计算该层中所有神经元的激活数据。类似的,W2将会是[4x4]矩阵,存储着第二个隐层的连接,W3是[1x4]的矩阵,用于输出层。完整的3层神经网络的前向传播就是简单的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)
在上面的代码中,W1,W2,W3,b1,b2,b3都是网络中可以学习的参数。注意x并不是一个单独的列向量,而可以是一个批量的训练数据(其中每个输入样本将会是x中的一列),所有的样本将会被并行化的高效计算出来。注意神经网络最后一层通常是没有激活函数的(例如,在分类任务中它给出一个实数值的分类评分)。
> 全连接层的前向传播一般就是先进行一个矩阵乘法,然后加上偏置并运用激活函数。
理解具有全连接层的神经网络的一个方式是:可以认为它们定义了一个由一系列函数组成的函数族,网络的权重就是每个函数的参数。如此产生的问题是:该函数族的表达能力如何?存在不能被神经网络表达的函数吗?
现在看来,拥有至少一个隐层的神经网络是一个_通用的近似器_。在研究(例如1989年的论文Approximation by Superpositions of Sigmoidal Function__,或者Michael Nielsen__的这个直观解释。)中已经证明,给出任意连续函数和任意,均存在一个至少含1个隐层的神经网络(并且网络中有合理选择的非线性激活函数,比如sigmoid),对于,使得。换句话说,神经网络可以近似任何连续函数。
既然一个隐层就能近似任何函数,那为什么还要构建更多层来将网络做得更深?答案是:虽然一个2层网络在数学理论上能完美地近似所有连续函数,但在实际操作中效果相对较差。在一个维度上,虽然以为参数向量"指示块之和"函数也是通用的近似器,但是谁也不会建议在机器学习中使用这个函数公式。神经网络在实践中非常好用,是因为它们表达出的函数不仅平滑,而且对于数据的统计特性有很好的拟合。同时,网络通过最优化算法(例如梯度下降)能比较容易地学习到这个函数。类似的,虽然在理论上深层网络(使用了多个隐层)和单层网络的表达能力是一样的,但是就实践经验而言,深度网络效果比单层网络好。
另外,在实践中3层的神经网络会比2层的表现好,然而继续加深(做到4,5,6层)很少有太大帮助。卷积神经网络的情况却不同,在卷积神经网络中,对于一个良好的识别系统来说,深度是一个极端重要的因素(比如数十(以10为量级)个可学习的层)。对于该现象的一种解释观点是:因为图像拥有层次化结构(比如脸是由眼睛等组成,眼睛又是由边缘组成),所以多层处理对于这种数据就有直观意义。
全面的研究内容还很多,近期研究的进展也很多。如果你对此感兴趣,我么推荐你阅读下面文献:
在面对一个具体问题的时候该确定网络结构呢?到底是不用隐层呢?还是一个隐层?两个隐层或更多?每个层的尺寸该多大?
首先,要知道当我们增加层的数量和尺寸时,网络的容量上升了。即神经元们可以合作表达许多复杂函数,所以表达函数的空间增加。例如,如果有一个在二维平面上的二分类问题。我们可以训练3个不同的神经网络,每个网络都只有一个隐层,但是每层的神经元数目不同:
————————————————————————————————————————
更大的神经网络可以表达更复杂的函数。数据是用不同颜色的圆点表示他们的不同类别,决策边界是由训练过的神经网络做出的。你可以在ConvNetsJS demo__上练练手。
————————————————————————————————————————
在上图中,可以看见有更多神经元的神经网络可以表达更复杂的函数。然而这既是优势也是不足,优势是可以分类更复杂的数据,不足是可能造成对训练数据的过拟合。过拟合**(Overfitting)是网络对数据中的噪声有很强的拟合能力,而没有重视数据间(假设)的潜在基本关系。举例来说,有20个神经元隐层的网络拟合了所有的训练数据,但是其代价是把决策边界变成了许多不相连的红绿区域。而有3个神经元的模型的表达能力只能用比较宽泛的方式去分类数据。它将数据看做是两个大块,并把个别在绿色区域内的红色点看做噪声。在实际中,这样可以在测试数据中获得更好的泛****化(generalization)**能力。
基于上面的讨论,看起来如果数据不是足够复杂,则似乎小一点的网络更好,因为可以防止过拟合。然而并非如此,防止神经网络的过拟合有很多方法(L2正则化,dropout和输入噪音等),后面会详细讨论。在实践中,使用这些方法来控制过拟合比减少网络神经元数目要好得多。
不要减少网络神经元数目的主要原因在于小网络更难使用梯度下降等局部方法来进行训练:虽然小型网络的损失函数的局部极小值更少,也比较容易收敛到这些局部极小值,但是这些最小值一般都很差,损失值很高。相反,大网络拥有更多的局部极小值,但就实际损失值来看,这些局部极小值表现更好,损失更小。因为神经网络是非凸的,就很难从数学上研究这些特性。即便如此,还是有一些文章尝试对这些目标函数进行理解,例如The Loss Surfaces of Multilayer Networks__这篇论文。在实际中,你将发现如果训练的是一个小网络,那么最终的损失值将展现出多变性:某些情况下运气好会收敛到一个好的地方,某些情况下就收敛到一个不好的极值。从另一方面来说,如果你训练一个大的网络,你将发现许多不同的解决方法,但是最终损失值的差异将会小很多。这就是说,所有的解决办法都差不多,而且对于随机初始化参数好坏的依赖也会小很多。
重申一下,正则化强度是控制神经网络过拟合的好方法。看下图结果:
————————————————————————————————————————
不同正则化强度的效果:每个神经网络都有20个隐层神经元,但是随着正则化强度增加,它的决策边界变得更加平滑。你可以在ConvNetsJS demo__上练练手。
————————————————————————————————————————
需要记住的是:不应该因为害怕出现过拟合而使用小网络。相反,应该进尽可能使用大网络,然后使用正则化技巧来控制过拟合。
小结如下: