3.1 多层感知机

1.只会画直线可不够:为什么得加隐藏层

上一章我们见识了线性模型的硬伤,它只会画直线,一遇到弯的关系就直接摆烂。不妨把它想成一个只会做一道菜的学徒厨师,不管什么食材进门,他都只端上同一道菜,碰上客人想换个口味就应付不过来了。那要怎样才能让它学会处理弯弯绕绕的关系呢?最直接的办法,就是在输入和输出之间再多加几层中间结构进去,这些中间层就叫隐藏层。加了隐藏层的这种网络,就叫多层感知机。

为什么叫隐藏呢?因为它们夹在输入和输出中间,我们不直接给它们喂原始数据,也不直接读取它们的输出,它们在里头默默干活,从外面看不见,所以叫隐藏层。这就好比小明刷题,外面的人只看到他最后考了多少分,中间他怎么错、怎么改、怎么悟出来的,他们都看不见,但这些隐藏的过程才是让他真正长本事的环节。

我们用上标来区分不同的层。第 ll 层(ll 表示第几层,从1开始数)的输入记作 h(l1)h^{(l-1)},这一层的权重记作 W(l)W^{(l)}(还记得吧,到了多层网络这里,权重就从向量升级成矩阵了,因为一层里头有一大堆神经元,每个神经元都自带一串权重),偏置记作 b(l)b^{(l)}。层内先做一次线性变换,会得到一个中间结果:

z(l)=W(l)h(l1)+b(l)z^{(l)}=W^{(l)}h^{(l-1)}+b^{(l)}

这里的 z(l)z^{(l)} 就是第 ll 层线性变换算出来的中间结果,还没有经过激活函数的处理。

要是故事到这就结束了,那多层感知机其实帮不上什么忙。怎么说呢,线性变换这东西有个毛病,你叠再多层,最后还是能合并成一个总的线性变换,绕了一大圈又回到一条直线上,相当于什么也没干。就好比把一个数先乘2再乘3再乘5,看起来折腾了好几步,其实等价于直接乘30,本质上还是一步乘法,变不出弯来。所以光堆线性层是没用的,网络真正缺的,是非线性。这一步,得靠激活函数 σ\sigma 来完成。隐藏层的输出写成:

h(l)=σ(z(l))h^{(l)}=\sigma(z^{(l)})

这里的 σ\sigma 表示激活函数,它对中间结果 z(l)z^{(l)} 做一次非线性变换,吐出这一层最终的输出 h(l)h^{(l)}。也就是先做一次线性变换,再套上一层激活函数,这一套组合打下来,网络才算真正拥有了画弯线的能力。

2.激活函数:给网络注入灵魂

激活函数就是夹在每次线性变换后面的那个非线性操作,别看它不起眼,它简直是整个神经网络的灵魂,没它网络就是个花架子。

最常用、也最朴实的一个,叫ReLU,全称是线性整流函数,定义是:

ReLU(z)=max(0,z)\mathrm{ReLU}(z)=\max(0,z)

这里的 max(0,z)\max(0,z) 表示在0和 zz 之间取较大的那个。逻辑直白到家了,输入是正数就原样输出,输入是负数就直接摁成零。比如输入是3就输出3,输入是-2就输出0。不妨把它想成一个特别严的宿舍门卫,正数能进,负数一律拦在外面,不管你是谁。ReLU火起来是有道理的,它算起来飞快,而且只要输入是正数,它的梯度就稳稳是1,深层网络训练起来特别省心,所以现在隐藏层基本默认都用它。

再来看一个老牌的,叫Sigmoid,定义是:

Sigmoid(z)=11+ez\mathrm{Sigmoid}(z)=\frac{1}{1+e^{-z}}

这里的 ee 是自然常数,大概是2.718。Sigmoid的特点是,不管输入多大或者多小,输出都被压到0和1之间,区间是 (0,1)(0,1)。这种把任何数都揉进0到1里的脾气,让它特别适合拿来表达概率,或者做一种软乎乎的开关。你想啊,输出0.9就表示这事十有八九能成,输出0.1就表示基本没戏,这跟概率那套说法天然对得上。不过Sigmoid也有个老毛病,当输入的绝对值比较大的时候,它的梯度会变得特别特别小,深层网络里一层层乘下去,梯度就传不动了,这毛病叫梯度消失,后面会专门讲。

光说激活函数有点干,我们拿个经典老问题来感受一下它的威力,这就是著名的异或问题(XOR)。假设平面上散落着两色点,规则是这样的,两个输入一个填1一个填0的时候算红点,两个输入都填0或者都填1的时候算蓝点。这种关系你拿条直线怎么切都切不开,线性模型当场抓瞎。说起来我之前翻过一本讲模式识别的入门书,里面举了个挺贴切的比方,说这种数据就像两家开在同一条街上的奶茶店,一家周一三五开、一家二四六开,你想拿一道笔直的分界线把两家店的客流分开,怎么画都画不准,因为它们本来就缠在一起。

可只要你肯加一个隐藏层、配上激活函数,网络就能把原始坐标扭到一个新空间里,在那个新空间里,红蓝两色点变得能用一刀切开了。激活函数给的那一下非线性,就是让网络拥有这种扭转魔力的关键。

(还有一个叫tanh的,跟Sigmoid是亲戚,输出压到-1和1之间,原理类似,这里就不展开了,感兴趣的可以自己去琢磨一下。)

既然顺嘴提了tanh,干脆就把激活函数这一大家子都拉出来遛遛,省得你以后翻论文的时候一脸懵。

先正式介绍一下tanh,全称叫双曲正切函数,定义是:

tanh(z)=ezezez+ez\mathrm{tanh}(z)=\frac{e^z-e^{-z}}{e^z+e^{-z}}

这里的 ee 还是那个自然常数2.718。你看这长相,跟Sigmoid确实是亲戚,输出被压到-1和1之间,区间是 (1,1)(-1,1)。比起Sigmoid,tanh有个明显的优点,它的输出是零中心的,输入是0的时候输出也是0,正负两边大致对称。这一条在数值计算里挺讨喜,能让下一层的输入分布更均衡一些。所以早期的隐藏层经常会选tanh,少有人用Sigmoid。不过它俩犯了同一个老毛病,输入绝对值一大,梯度照样消失,深层网络用起来还是头疼。

ReLU当道之后,大家发现它也有个不高不低的小毛病,输入一旦是负数,梯度就直接是零,那个神经元就彻底罢工再也不更新了,俗称神经元死亡。为了让负数区域也能漏点梯度出来,LeakyReLU应运而生,定义是:

LeakyReLU(z)={z,z>0αz,z0\mathrm{LeakyReLU}(z)=\begin{cases}z, & z>0 \\ \alpha z, & z\le 0\end{cases}

这里的 α\alpha 是一个小小的系数,通常取0.01,意思是负数输入不被直接摁成零,而是乘个很小的系数漏出去。正数部分跟ReLU一模一样,负数部分好歹还留了条活路,梯度不再是零,神经元想死都难。所以LeakyReLU特别适合用来对付那些容易死神经元的任务。

再来说GELU,这个是后来居上的新贵,全称叫高斯误差线性单元,定义稍微有点劝退:

GELU(z)=zΦ(z)\mathrm{GELU}(z)=z\cdot\Phi(z)

这里的 Φ(z)\Phi(z) 是标准正态分布的累积分布函数(你只要知道它是个从0平滑涨到1的S形曲线就行,输入越小它越接近0,输入越大它越接近1)。GELU做的事情,可以粗略理解成ReLU的平滑升级版,正数区域大致原样放行,负数区域它就按概率稍微放一点过去,整体曲线比ReLU圆润那么一截。你别看它公式唬人,GELU现在可是个大红人,Transformer里基本都用它,BERT、GPT这些大名鼎鼎的模型,隐藏层激活函数默认就是GELU。

仔细观察你会发现一个规律,深层网络的隐藏层几乎全都被ReLU这一家子占着。原因特别实在,ReLU和它的变种在正数区域梯度稳稳是1,反向传播的时候梯度一层层往回传不会越乘越小,网络摞得再深梯度也还传得动。Sigmoid和tanh那种把输出压进小区间的脾气,深层一摞梯度直接消失给你看。所以但凡你听说某个模型特别深,隐藏层用的大概率就是ReLU这一脉,ReLU本尊、LeakyReLU和GELU三个轮流坐庄,规律就摆在这里。

3.网络到底该多深、该多宽

加了激活函数之后,网络就能学会把简单的小模式,一步步拼成复杂的大模式。这个过程是分级的,浅层先抓住一些最基础的东西(比如一条边、一小块颜色),深层再把这些基础件组合起来(边拼成角,角拼成形状,形状拼成物体)。把一个复杂任务拆成一连串小变换,每一层只干自己那一小摊活,最后整个网络再把这些局部规律攒成任务要的结果,这就是所谓深度表示的价值,也是深度学习里深度两个字的含金量所在。

不妨把它想成读书的层次。刚开始你只会做最基础的求导题,这是浅层。慢慢地你能把求导和积分串起来用,这是中层。到后来你甚至能搞定那些把好几个知识点揉一块的综合大题,这是深层。每多过一遍脑子,你对知识的理解就往更抽象的层面走一步,这跟神经网络一层层往上抽象特征是一个道理。

这里有两个尺寸得分清楚。一个是宽度,指的是一层里头有多少个神经元,宽一点的网络每一层能同时抓住更多种特征。另一个是深度,指的是网络一共摞了多少层变换,深一点的网络能把简单模式往更复杂的抽象里多揉几轮。宽也好深也好,都能让网络变聪明,但代价也实打实,参数蹭蹭往上涨,训练变慢,还更容易把训练集死记硬背下来、在新数据上翻车(没错,又是过拟合那个老熟人)。

所以一个网络的胖瘦高矮,得跟你手头的数据规模、任务的难易程度搭着来,一味地往大里堆并不划算,找到那个最合身的甜点位才是本事。

说到深和宽到底该怎么取舍,就不得不提一个特别重要的结论,叫万能逼近定理。它通俗的意思是,只要隐藏层够宽,只有一个隐藏层的多层感知机就能以任意精度逼近任意连续函数。不管那个目标函数长得多扭曲多离谱,理论上你把隐藏层神经元数量堆到足够多,网络总能学出一个跟它长得足够像的拟合。

你可能会想,既然一个隐藏层理论上就够万能了,那大家干嘛还费劲把网络摞那么深?这就要说到效率。深度网络的优势,体现在它能把复杂的函数拆成简单的小步,每一层只负责一小部分,层层叠加之下反而能用更少的总参数达到同样的效果。我看过一本讲体育训练的书,里面提到一个道理,长跑快的人靠的是每一步都踩得稳当,单凭某几步迈得特别大未必能跑赢。深度网络的道理也差不多,浅而宽的网络得堆成千上万个神经元才勉强逼近的函数,换成深一点的结构,每层少放些神经元就办到了。说穿了,深度带来的好处更多体现在省参数上,它让网络在参数预算有限的情况下,把表达能力用得更划算。现实里大家宁可把网络摞深一点,也不愿意把单层撑到特别宽,毕竟参数是实打实要算的,省一点是一点,训练快显存也省。

4.参数初始化:别让网络一上来就出岔子

讲了这么多层和激活函数的事,还有个细节得补上,那就是参数一开始到底该填什么值。你别小看这步,深层网络要是初始化没弄好,训练还没开始就已经废了。

回想一下前向传播的过程,每一层都在做 z(l)=W(l)h(l1)+b(l)z^{(l)}=W^{(l)}h^{(l-1)}+b^{(l)} 这套运算,输入信号一层层往后传。如果权重 W(l)W^{(l)} 的初始值设得太大,信号每过一层就被放大一截,几层下来数值就涨到天文数字,激活函数直接饱和,梯度也没了。反过来要是初始值设得太小,信号每过一层就被削弱一截,传到最后基本归零,网络相当于什么也没学到。深层网络对这件事特别敏感,层数越多,这种尺度滚雪球的效应越明显。

Xavier初始化就是来治这个毛病的,它的核心想法是让每一层输出的方差尽量跟输入的方差持平,信号既不爆炸也不消失,稳稳地往后传。具体做法是把权重初始化成一个均值为0、方差为 1n\frac{1}{n} 的高斯分布,这里的 nn 是这一层输入的神经元个数。Xavier初始化跟Sigmoid和tanh这种对称的激活函数特别搭,早期那些用tanh的网络基本都靠它起家。

后来ReLU横空出世,Xavier初始化就有点不太合身了,因为ReLU直接把一半的负数输入摁成零,相当于信号天然少了一半,方差对不上。He初始化(也叫He正态初始化)专门为ReLU系量身定做,把方差从 1n\frac{1}{n} 调到 2n\frac{2}{n},正好补上ReLU摁掉的那一半,让信号尺度重新稳住。所以你用ReLU、LeakyReLU、GELU的时候,初始化默认就选He,这一套搭配是经过实战检验的标配。

PyTorch里这些初始化都给你封装好了,一行代码就能调用,你只要记住哪个激活函数配哪套初始化就行,不用自己手算方差。

5.反向传播:梯度怎么一层层传回去

前面我们把前向传播(数据从输入流到输出)讲透了,可网络要学习,光前向还不够,还得知道"每个参数该往哪个方向调、调多少",也就是要算损失对每个参数的梯度。这套把梯度从输出层一层层传回输入层的算法,就叫反向传播(backpropagation)。它是整个深度学习的命脉,我们把它从头推一遍。

设网络一共 LL 层,前向传播我们已经有了两套公式:z(l)=W(l)h(l1)+b(l)z^{(l)}=W^{(l)}h^{(l-1)}+b^{(l)}h(l)=σ(z(l))h^{(l)}=\sigma(z^{(l)}),最终输出 h(L)h^{(L)} 代入损失函数得到标量损失 JJ。反向传播要算的是 JW(l)\frac{\partial J}{\partial W^{(l)}}Jb(l)\frac{\partial J}{\partial b^{(l)}},对每一层。

第一步:定义误差信号 δ(l)\delta^{(l)} 为了推导干净,我们对每一层定义一个中间量 δ(l)=Jz(l)\delta^{(l)}=\frac{\partial J}{\partial z^{(l)}},它表示损失对第 ll 层线性变换输出的梯度。一旦把每一层的 δ(l)\delta^{(l)} 算出来,参数的梯度就能立刻得到。

第二步:参数梯度由 δ(l)\delta^{(l)} 直接给出。 用链式法则,注意 z(l)=W(l)h(l1)+b(l)z^{(l)}=W^{(l)}h^{(l-1)}+b^{(l)}W(l)W^{(l)} 的每个元素只影响 z(l)z^{(l)} 的对应分量:

JW(l)=δ(l)(h(l1)),Jb(l)=δ(l)\frac{\partial J}{\partial W^{(l)}}=\delta^{(l)}(h^{(l-1)})^\top,\qquad\frac{\partial J}{\partial b^{(l)}}=\delta^{(l)}

(这里 δ(l)(h(l1))\delta^{(l)}(h^{(l-1)})^\top 是列向量乘行向量,正好得到和 W(l)W^{(l)} 同样形状的矩阵。)

第三步:δ(l)\delta^{(l)} 怎么从后一层传过来。 这是反向传播的核心。由 h(l)=σ(z(l))h^{(l)}=\sigma(z^{(l)}) 和链式法则:

δ(l)=Jz(l)=Jh(l)σ(z(l))\delta^{(l)}=\frac{\partial J}{\partial z^{(l)}}=\frac{\partial J}{\partial h^{(l)}}\odot\sigma'(z^{(l)})

其中 \odot 是逐元素乘(Hadamard积),σ(z(l))\sigma'(z^{(l)}) 是激活函数的导数逐元素作用。而 Jh(l)\frac{\partial J}{\partial h^{(l)}} 可以从后一层的 δ(l+1)\delta^{(l+1)} 反推——因为 h(l)h^{(l)} 是通过 z(l+1)=W(l+1)h(l)+b(l+1)z^{(l+1)}=W^{(l+1)}h^{(l)}+b^{(l+1)} 影响下一层的:

Jh(l)=(W(l+1))δ(l+1)\frac{\partial J}{\partial h^{(l)}}=(W^{(l+1)})^\top\delta^{(l+1)}

把这两步合起来,就得到反向传播最关键的反传公式:

δ(l)=(W(l+1))δ(l+1)σ(z(l))\boxed{\delta^{(l)}=(W^{(l+1)})^\top\delta^{(l+1)}\odot\sigma'(z^{(l)})}

整个反向传播的算法就浓缩在这一行里:先算输出层的 δ(L)=Jh(L)σ(z(L))\delta^{(L)}=\frac{\partial J}{\partial h^{(L)}}\odot\sigma'(z^{(L)})(依赖具体损失函数),然后用上面这个公式一层层往前算 δ(L1),δ(L2),,δ(1)\delta^{(L-1)},\delta^{(L-2)},\ldots,\delta^{(1)},每算出一层的 δ(l)\delta^{(l)} 就顺手用第二步的公式得到该层的 JW(l)\frac{\partial J}{\partial W^{(l)}}Jb(l)\frac{\partial J}{\partial b^{(l)}}。最后梯度下降更新参数。

一个两层网络的手算例子。 光看公式还是虚,走一遍数字就踏实了。设一个最简网络:输入 x=h(0)=1x=h^{(0)}=1(标量),第一层 W(1)=2W^{(1)}=2b(1)=0b^{(1)}=0、激活用 ReLU,所以 z(1)=2z^{(1)}=2h(1)=ReLU(2)=2h^{(1)}=\mathrm{ReLU}(2)=2。第二层 W(2)=3W^{(2)}=3b(2)=1b^{(2)}=1、激活用恒等(不套激活),所以 z(2)=3×2+1=7z^{(2)}=3\times2+1=7h(2)=7h^{(2)}=7。设损失 J=12(h(2)10)2=12(710)2=4.5J=\frac{1}{2}(h^{(2)}-10)^2=\frac{1}{2}(7-10)^2=4.5

反向传播:先算输出层 δ(2)=Jz(2)=h(2)10=710=3\delta^{(2)}=\frac{\partial J}{\partial z^{(2)}}=h^{(2)}-10=7-10=-3(恒等激活导数为1)。然后 JW(2)=δ(2)h(1)=3×2=6\frac{\partial J}{\partial W^{(2)}}=\delta^{(2)}h^{(1)}=-3\times2=-6Jb(2)=δ(2)=3\frac{\partial J}{\partial b^{(2)}}=\delta^{(2)}=-3。再往前传:Jh(1)=W(2)δ(2)=3×(3)=9\frac{\partial J}{\partial h^{(1)}}=W^{(2)}\delta^{(2)}=3\times(-3)=-9σ(z(1))=ReLU(2)=1\sigma'(z^{(1)})=\mathrm{ReLU}'(2)=1z(1)>0z^{(1)}>0 时 ReLU 导数为1),所以 δ(1)=9×1=9\delta^{(1)}=-9\times1=-9。最后 JW(1)=δ(1)h(0)=9×1=9\frac{\partial J}{\partial W^{(1)}}=\delta^{(1)}h^{(0)}=-9\times1=-9Jb(1)=δ(1)=9\frac{\partial J}{\partial b^{(1)}}=\delta^{(1)}=-9

你可以验算:J=12(3ReLU(2x)+110)2J=\frac{1}{2}(3\cdot\mathrm{ReLU}(2x)+1-10)^2,对 W(1)=2W^{(1)}=2 求导正好是 9-9,和我们反向传播算出来的一致。这就是反向传播干的事——把一个看似要嵌套求导的吓人问题,拆成每一层只做一次矩阵乘、一次逐元素乘的流水线。PyTorch 的 autograd 就是自动干这件事,但原理你必须懂,否则训练出问题根本无从排查。

今天就先聊到这里,下一章见。

练习

Q1. 多层感知机为什么必须加激活函数?如果全是线性层会怎样?

如果每层只有线性变换、没有激活函数,那么多层线性变换叠起来仍然等价于一个总的线性变换(就像先乘2再乘3再乘5等于直接乘30),网络本质上还是只能画直线,加多少层都白搭。激活函数 σ\sigma 给每层注入非线性,网络才真正拥有画弯线、逼近复杂函数的能力,这是隐藏层的灵魂。

Q2. ReLU 和 Sigmoid 各有什么脾气?为什么深层网络的隐藏层基本都用 ReLU 这一家?

ReLU 是 max(0,z)\max(0,z),正数原样输出、负数摁成零,算得快且正数区域梯度恒为 1。Sigmoid 把输出压到 (0,1)(0,1),适合表达概率,但输入绝对值一大梯度就特别小。深层网络若用 Sigmoid/tanh,反向传播时梯度一层层乘下去会越来越小(梯度消失),传不动;而 ReLU 系(含 LeakyReLU、GELU)正数区域梯度稳定为 1,摞得再深梯度也能传,所以深层隐藏层基本都被 ReLU 这一脉占着。

Q3. Xavier 初始化和 He 初始化分别配哪种激活函数?为什么 ReLU 要专门用 He?

Xavier 让每层输出方差跟输入方差持平,跟 Sigmoid、tanh 这种对称激活函数搭。He 初始化是给 ReLU 系量身定做的,把方差从 1n\frac{1}{n} 调到 2n\frac{2}{n}。原因是 ReLU 直接把一半负数输入摁成零、信号天然少了一半,方差会对不上,He 用两倍方差正好补回这一半,让信号尺度重新稳住,避免梯度消失或爆炸。

Q4.(面试题) 请说明反向传播中误差信号 δ(l)\delta^{(l)} 的递推公式,并解释为什么它把嵌套求导变成了流水线。

定义 δ(l)=Jz(l)\delta^{(l)}=\frac{\partial J}{\partial z^{(l)}},关键递推是 δ(l)=(W(l+1))δ(l+1)σ(z(l))\delta^{(l)}=(W^{(l+1)})^\top\delta^{(l+1)}\odot\sigma'(z^{(l)}),而参数梯度 JW(l)=δ(l)(h(l1))\frac{\partial J}{\partial W^{(l)}}=\delta^{(l)}(h^{(l-1)})^\topJb(l)=δ(l)\frac{\partial J}{\partial b^{(l)}}=\delta^{(l)}。也就是说先算输出层 δ(L)\delta^{(L)},然后每往前一层只需做一次矩阵乘 (W(l+1))δ(l+1)(W^{(l+1)})^\top\delta^{(l+1)} 和一次逐元素乘 σ\odot\sigma',就能得到本层的 δ\delta 并顺手算出参数梯度。本来梯度是层层嵌套的复合求导,反传把它拆成"一次矩阵乘 + 一次逐元素乘"的重复流水线,每个中间梯度只算一次,这就是反向传播高效的根本原因。

相关标签
深度学习多层感知机激活函数