3.1 多层感知机
1.只会画直线可不够:为什么得加隐藏层
上一章我们见识了线性模型的硬伤,它只会画直线,一遇到弯的关系就直接摆烂。不妨把它想成一个只会做一道菜的学徒厨师,不管什么食材进门,他都只端上同一道菜,碰上客人想换个口味就应付不过来了。那要怎样才能让它学会处理弯弯绕绕的关系呢?最直接的办法,就是在输入和输出之间再多加几层中间结构进去,这些中间层就叫隐藏层。加了隐藏层的这种网络,就叫多层感知机。
为什么叫隐藏呢?因为它们夹在输入和输出中间,我们不直接给它们喂原始数据,也不直接读取它们的输出,它们在里头默默干活,从外面看不见,所以叫隐藏层。这就好比小明刷题,外面的人只看到他最后考了多少分,中间他怎么错、怎么改、怎么悟出来的,他们都看不见,但这些隐藏的过程才是让他真正长本事的环节。
我们用上标来区分不同的层。第 层( 表示第几层,从1开始数)的输入记作 ,这一层的权重记作 (还记得吧,到了多层网络这里,权重就从向量升级成矩阵了,因为一层里头有一大堆神经元,每个神经元都自带一串权重),偏置记作 。层内先做一次线性变换,会得到一个中间结果:
这里的 就是第 层线性变换算出来的中间结果,还没有经过激活函数的处理。
要是故事到这就结束了,那多层感知机其实帮不上什么忙。怎么说呢,线性变换这东西有个毛病,你叠再多层,最后还是能合并成一个总的线性变换,绕了一大圈又回到一条直线上,相当于什么也没干。就好比把一个数先乘2再乘3再乘5,看起来折腾了好几步,其实等价于直接乘30,本质上还是一步乘法,变不出弯来。所以光堆线性层是没用的,网络真正缺的,是非线性。这一步,得靠激活函数 来完成。隐藏层的输出写成:
这里的 表示激活函数,它对中间结果 做一次非线性变换,吐出这一层最终的输出 。也就是先做一次线性变换,再套上一层激活函数,这一套组合打下来,网络才算真正拥有了画弯线的能力。
2.激活函数:给网络注入灵魂
激活函数就是夹在每次线性变换后面的那个非线性操作,别看它不起眼,它简直是整个神经网络的灵魂,没它网络就是个花架子。
最常用、也最朴实的一个,叫ReLU,全称是线性整流函数,定义是:
这里的 表示在0和 之间取较大的那个。逻辑直白到家了,输入是正数就原样输出,输入是负数就直接摁成零。比如输入是3就输出3,输入是-2就输出0。不妨把它想成一个特别严的宿舍门卫,正数能进,负数一律拦在外面,不管你是谁。ReLU火起来是有道理的,它算起来飞快,而且只要输入是正数,它的梯度就稳稳是1,深层网络训练起来特别省心,所以现在隐藏层基本默认都用它。
再来看一个老牌的,叫Sigmoid,定义是:
这里的 是自然常数,大概是2.718。Sigmoid的特点是,不管输入多大或者多小,输出都被压到0和1之间,区间是 。这种把任何数都揉进0到1里的脾气,让它特别适合拿来表达概率,或者做一种软乎乎的开关。你想啊,输出0.9就表示这事十有八九能成,输出0.1就表示基本没戏,这跟概率那套说法天然对得上。不过Sigmoid也有个老毛病,当输入的绝对值比较大的时候,它的梯度会变得特别特别小,深层网络里一层层乘下去,梯度就传不动了,这毛病叫梯度消失,后面会专门讲。
光说激活函数有点干,我们拿个经典老问题来感受一下它的威力,这就是著名的异或问题(XOR)。假设平面上散落着两色点,规则是这样的,两个输入一个填1一个填0的时候算红点,两个输入都填0或者都填1的时候算蓝点。这种关系你拿条直线怎么切都切不开,线性模型当场抓瞎。说起来我之前翻过一本讲模式识别的入门书,里面举了个挺贴切的比方,说这种数据就像两家开在同一条街上的奶茶店,一家周一三五开、一家二四六开,你想拿一道笔直的分界线把两家店的客流分开,怎么画都画不准,因为它们本来就缠在一起。
可只要你肯加一个隐藏层、配上激活函数,网络就能把原始坐标扭到一个新空间里,在那个新空间里,红蓝两色点变得能用一刀切开了。激活函数给的那一下非线性,就是让网络拥有这种扭转魔力的关键。
(还有一个叫tanh的,跟Sigmoid是亲戚,输出压到-1和1之间,原理类似,这里就不展开了,感兴趣的可以自己去琢磨一下。)
既然顺嘴提了tanh,干脆就把激活函数这一大家子都拉出来遛遛,省得你以后翻论文的时候一脸懵。
先正式介绍一下tanh,全称叫双曲正切函数,定义是:
这里的 还是那个自然常数2.718。你看这长相,跟Sigmoid确实是亲戚,输出被压到-1和1之间,区间是 。比起Sigmoid,tanh有个明显的优点,它的输出是零中心的,输入是0的时候输出也是0,正负两边大致对称。这一条在数值计算里挺讨喜,能让下一层的输入分布更均衡一些。所以早期的隐藏层经常会选tanh,少有人用Sigmoid。不过它俩犯了同一个老毛病,输入绝对值一大,梯度照样消失,深层网络用起来还是头疼。
ReLU当道之后,大家发现它也有个不高不低的小毛病,输入一旦是负数,梯度就直接是零,那个神经元就彻底罢工再也不更新了,俗称神经元死亡。为了让负数区域也能漏点梯度出来,LeakyReLU应运而生,定义是:
这里的 是一个小小的系数,通常取0.01,意思是负数输入不被直接摁成零,而是乘个很小的系数漏出去。正数部分跟ReLU一模一样,负数部分好歹还留了条活路,梯度不再是零,神经元想死都难。所以LeakyReLU特别适合用来对付那些容易死神经元的任务。
再来说GELU,这个是后来居上的新贵,全称叫高斯误差线性单元,定义稍微有点劝退:
这里的 是标准正态分布的累积分布函数(你只要知道它是个从0平滑涨到1的S形曲线就行,输入越小它越接近0,输入越大它越接近1)。GELU做的事情,可以粗略理解成ReLU的平滑升级版,正数区域大致原样放行,负数区域它就按概率稍微放一点过去,整体曲线比ReLU圆润那么一截。你别看它公式唬人,GELU现在可是个大红人,Transformer里基本都用它,BERT、GPT这些大名鼎鼎的模型,隐藏层激活函数默认就是GELU。
仔细观察你会发现一个规律,深层网络的隐藏层几乎全都被ReLU这一家子占着。原因特别实在,ReLU和它的变种在正数区域梯度稳稳是1,反向传播的时候梯度一层层往回传不会越乘越小,网络摞得再深梯度也还传得动。Sigmoid和tanh那种把输出压进小区间的脾气,深层一摞梯度直接消失给你看。所以但凡你听说某个模型特别深,隐藏层用的大概率就是ReLU这一脉,ReLU本尊、LeakyReLU和GELU三个轮流坐庄,规律就摆在这里。
3.网络到底该多深、该多宽
加了激活函数之后,网络就能学会把简单的小模式,一步步拼成复杂的大模式。这个过程是分级的,浅层先抓住一些最基础的东西(比如一条边、一小块颜色),深层再把这些基础件组合起来(边拼成角,角拼成形状,形状拼成物体)。把一个复杂任务拆成一连串小变换,每一层只干自己那一小摊活,最后整个网络再把这些局部规律攒成任务要的结果,这就是所谓深度表示的价值,也是深度学习里深度两个字的含金量所在。
不妨把它想成读书的层次。刚开始你只会做最基础的求导题,这是浅层。慢慢地你能把求导和积分串起来用,这是中层。到后来你甚至能搞定那些把好几个知识点揉一块的综合大题,这是深层。每多过一遍脑子,你对知识的理解就往更抽象的层面走一步,这跟神经网络一层层往上抽象特征是一个道理。
这里有两个尺寸得分清楚。一个是宽度,指的是一层里头有多少个神经元,宽一点的网络每一层能同时抓住更多种特征。另一个是深度,指的是网络一共摞了多少层变换,深一点的网络能把简单模式往更复杂的抽象里多揉几轮。宽也好深也好,都能让网络变聪明,但代价也实打实,参数蹭蹭往上涨,训练变慢,还更容易把训练集死记硬背下来、在新数据上翻车(没错,又是过拟合那个老熟人)。
所以一个网络的胖瘦高矮,得跟你手头的数据规模、任务的难易程度搭着来,一味地往大里堆并不划算,找到那个最合身的甜点位才是本事。
说到深和宽到底该怎么取舍,就不得不提一个特别重要的结论,叫万能逼近定理。它通俗的意思是,只要隐藏层够宽,只有一个隐藏层的多层感知机就能以任意精度逼近任意连续函数。不管那个目标函数长得多扭曲多离谱,理论上你把隐藏层神经元数量堆到足够多,网络总能学出一个跟它长得足够像的拟合。
你可能会想,既然一个隐藏层理论上就够万能了,那大家干嘛还费劲把网络摞那么深?这就要说到效率。深度网络的优势,体现在它能把复杂的函数拆成简单的小步,每一层只负责一小部分,层层叠加之下反而能用更少的总参数达到同样的效果。我看过一本讲体育训练的书,里面提到一个道理,长跑快的人靠的是每一步都踩得稳当,单凭某几步迈得特别大未必能跑赢。深度网络的道理也差不多,浅而宽的网络得堆成千上万个神经元才勉强逼近的函数,换成深一点的结构,每层少放些神经元就办到了。说穿了,深度带来的好处更多体现在省参数上,它让网络在参数预算有限的情况下,把表达能力用得更划算。现实里大家宁可把网络摞深一点,也不愿意把单层撑到特别宽,毕竟参数是实打实要算的,省一点是一点,训练快显存也省。
4.参数初始化:别让网络一上来就出岔子
讲了这么多层和激活函数的事,还有个细节得补上,那就是参数一开始到底该填什么值。你别小看这步,深层网络要是初始化没弄好,训练还没开始就已经废了。
回想一下前向传播的过程,每一层都在做 这套运算,输入信号一层层往后传。如果权重 的初始值设得太大,信号每过一层就被放大一截,几层下来数值就涨到天文数字,激活函数直接饱和,梯度也没了。反过来要是初始值设得太小,信号每过一层就被削弱一截,传到最后基本归零,网络相当于什么也没学到。深层网络对这件事特别敏感,层数越多,这种尺度滚雪球的效应越明显。
Xavier初始化就是来治这个毛病的,它的核心想法是让每一层输出的方差尽量跟输入的方差持平,信号既不爆炸也不消失,稳稳地往后传。具体做法是把权重初始化成一个均值为0、方差为 的高斯分布,这里的 是这一层输入的神经元个数。Xavier初始化跟Sigmoid和tanh这种对称的激活函数特别搭,早期那些用tanh的网络基本都靠它起家。
后来ReLU横空出世,Xavier初始化就有点不太合身了,因为ReLU直接把一半的负数输入摁成零,相当于信号天然少了一半,方差对不上。He初始化(也叫He正态初始化)专门为ReLU系量身定做,把方差从 调到 ,正好补上ReLU摁掉的那一半,让信号尺度重新稳住。所以你用ReLU、LeakyReLU、GELU的时候,初始化默认就选He,这一套搭配是经过实战检验的标配。
PyTorch里这些初始化都给你封装好了,一行代码就能调用,你只要记住哪个激活函数配哪套初始化就行,不用自己手算方差。
5.反向传播:梯度怎么一层层传回去
前面我们把前向传播(数据从输入流到输出)讲透了,可网络要学习,光前向还不够,还得知道"每个参数该往哪个方向调、调多少",也就是要算损失对每个参数的梯度。这套把梯度从输出层一层层传回输入层的算法,就叫反向传播(backpropagation)。它是整个深度学习的命脉,我们把它从头推一遍。
设网络一共 层,前向传播我们已经有了两套公式: 和 ,最终输出 代入损失函数得到标量损失 。反向传播要算的是 和 ,对每一层。
第一步:定义误差信号 。 为了推导干净,我们对每一层定义一个中间量 ,它表示损失对第 层线性变换输出的梯度。一旦把每一层的 算出来,参数的梯度就能立刻得到。
第二步:参数梯度由 直接给出。 用链式法则,注意 里 的每个元素只影响 的对应分量:
(这里 是列向量乘行向量,正好得到和 同样形状的矩阵。)
第三步: 怎么从后一层传过来。 这是反向传播的核心。由 和链式法则:
其中 是逐元素乘(Hadamard积), 是激活函数的导数逐元素作用。而 可以从后一层的 反推——因为 是通过 影响下一层的:
把这两步合起来,就得到反向传播最关键的反传公式:
整个反向传播的算法就浓缩在这一行里:先算输出层的 (依赖具体损失函数),然后用上面这个公式一层层往前算 ,每算出一层的 就顺手用第二步的公式得到该层的 和 。最后梯度下降更新参数。
一个两层网络的手算例子。 光看公式还是虚,走一遍数字就踏实了。设一个最简网络:输入 (标量),第一层 、、激活用 ReLU,所以 、。第二层 、、激活用恒等(不套激活),所以 、。设损失 。
反向传播:先算输出层 (恒等激活导数为1)。然后 ,。再往前传:,( 时 ReLU 导数为1),所以 。最后 ,。
你可以验算:,对 求导正好是 ,和我们反向传播算出来的一致。这就是反向传播干的事——把一个看似要嵌套求导的吓人问题,拆成每一层只做一次矩阵乘、一次逐元素乘的流水线。PyTorch 的 autograd 就是自动干这件事,但原理你必须懂,否则训练出问题根本无从排查。
今天就先聊到这里,下一章见。
练习
Q1. 多层感知机为什么必须加激活函数?如果全是线性层会怎样?
如果每层只有线性变换、没有激活函数,那么多层线性变换叠起来仍然等价于一个总的线性变换(就像先乘2再乘3再乘5等于直接乘30),网络本质上还是只能画直线,加多少层都白搭。激活函数 给每层注入非线性,网络才真正拥有画弯线、逼近复杂函数的能力,这是隐藏层的灵魂。
Q2. ReLU 和 Sigmoid 各有什么脾气?为什么深层网络的隐藏层基本都用 ReLU 这一家?
ReLU 是 ,正数原样输出、负数摁成零,算得快且正数区域梯度恒为 1。Sigmoid 把输出压到 ,适合表达概率,但输入绝对值一大梯度就特别小。深层网络若用 Sigmoid/tanh,反向传播时梯度一层层乘下去会越来越小(梯度消失),传不动;而 ReLU 系(含 LeakyReLU、GELU)正数区域梯度稳定为 1,摞得再深梯度也能传,所以深层隐藏层基本都被 ReLU 这一脉占着。
Q3. Xavier 初始化和 He 初始化分别配哪种激活函数?为什么 ReLU 要专门用 He?
Xavier 让每层输出方差跟输入方差持平,跟 Sigmoid、tanh 这种对称激活函数搭。He 初始化是给 ReLU 系量身定做的,把方差从 调到 。原因是 ReLU 直接把一半负数输入摁成零、信号天然少了一半,方差会对不上,He 用两倍方差正好补回这一半,让信号尺度重新稳住,避免梯度消失或爆炸。
Q4.(面试题) 请说明反向传播中误差信号 的递推公式,并解释为什么它把嵌套求导变成了流水线。
定义 ,关键递推是 ,而参数梯度 、。也就是说先算输出层 ,然后每往前一层只需做一次矩阵乘 和一次逐元素乘 ,就能得到本层的 并顺手算出参数梯度。本来梯度是层层嵌套的复合求导,反传把它拆成"一次矩阵乘 + 一次逐元素乘"的重复流水线,每个中间梯度只算一次,这就是反向传播高效的根本原因。