☰
深度学习基元拆解:全连接层与激活函数的原理、调参与实战
2026/10/17 2:32:49 网站建设 项目流程

很多人学深度学习,最早接触到的就是全连接层(也叫线性层)和激活函数。第一天看觉得挺简单:全连接层不就是矩阵乘一个权重再加个偏置吗?激活函数不就是max(0, x)吗?但真到了自己搭网络、调参、排查训练不收敛的时候才发现,这两个东西才是所有模型表现力的地基。地基没打牢,后面换再多网络结构、加再多技巧,都是在沙子上盖楼。

这篇文章是“深度学习算法学习”系列的第二篇,我把这两个最基础的组件从头到尾掰开讲:它们到底在干什么、为什么必须这么设计、实际搭建的时候有哪些坑、如何排查训练中出问题的地方。不管你是刚入门的新手,还是已经写过一些模型但总觉得没吃透的开发者,这篇都应该对你有帮助。我会给出可以直接照着用的参数和代码写法,也会解释每个选择背后的原因,而不是让你背一堆口诀。

1. 全连接层:一个线性变换的事

1.1 全连接层到底在算什么

全连接层,又叫线性层,它的本质是一个线性变换。假设输入是一个长度为n的向量x,全连接层的权重矩阵W的维度是m×n,偏置b的长度是m,那么输出y就是:

y = W @ x + b

这里的每一个输出神经元,都接收上一层的所有输入,然后把它们加权求和,再加上一个偏置,所以叫“全连接”。这也是为什么在不少框架的文档里,它写的是Fully Connected Layer,或者在工程实现上叫Linear、Dense。

我见过不少初学者会被这个名字搞晕。有人觉得“全连接”是一种很复杂的结构,其实它的核心就是一个矩阵乘法。你要做的就是确定两件事:输入维度n是多少,输出维度m是多少。这两个数字一旦定下来,权重W的维度就是确定的,参数量也就确定了。

举个例子。假设输入是一张28×28的手写数字图片,把它拉平成784维的向量(28×28=784),然后经过一个输出维度为128的全连接层,那么这个层的权重矩阵就是128×784,偏置是128个数字,总共的参数量是128×784+128=100352+128=100480。这个计算方式非常实用,因为训练模型时需要知道显存占用,参数量直接决定模型文件大小和训练耗时。

1.2 权重和偏置的生活化理解

很多人问,为什么要设偏置?权重都学出来了,偏置有什么用?

我用一个简单的生活类比:权重决定的是“看重什么”,偏置决定的是“什么情况下容易触发”。

比如你在判断一个人是否适合做程序员,输入的特征是:数学成绩、英语成绩、沟通能力。权重如果是[0.8, 0.2, -0.1],说明这个模型更看重数学,沟通能力有一点负向影响。但问题是,如果这个人各项成绩都是0分,公式算出来也是0,模型永远输出“不适合”,这明显不合理。偏置就是来修正这种情况的:即使输入全为0,偏置也可以让输出不等于0,保证模型有能力表达“默认偏向某一类”的倾向。

所以偏置的存在,让线性层不再是严格过原点的一条直线,而是一条可以平移的直线(二维情况下)。有了偏置,模型能覆盖的空间更大,表达力也更强。对于一维的情况,y = wx + b这个式子大家应该都很熟,这其实就是最简单的全连接层,只不过n=1、m=1而已。

1.3 输入输出维度:最常踩坑的地方

全连接层的维度匹配,是初学者最容易翻车的地方。因为框架一般不会自动改变你的输入形状,你必须手动把多维张量展平成二维矩阵。

实际操作中,我们通常在输入到全连接层之前,先用一个reshape或flatten操作,把形状变成(batch_size, feature_len)。比如处理一张图像,卷积层输出的特征图形状是(batch, channels, height, width),这时候想接全连接层,就先用flatten把它变成(batch, channelsheightwidth)。如果不做这一步,框架会直接报维度不匹配的错误,而且报错信息常常看得人一头雾水。

我自己有个习惯:在搭建模型前,先用一张纸把每一层的输入输出维度全部列出来,从数据进入模型开始,一步一步写到输出。写完之后再拼代码。这样能减少至少一半的维度错误。别嫌麻烦,这个习惯在日后搭复杂网络时能节约大量时间。

1.4 全连接层适合做什么

全连接层的优点是结构简单、表达能力上限高——理论上,只要层数和宽度足够,任何连续函数都可以被逼近。但它的缺点也很明显:参数量巨大,尤其输入是高维数据时。

比如处理一张224×224的RGB图片,直接拉平就是150528维。如果第一层全连接输出256个神经元,参数量就是150528×256=3852万,光这一层就接近四千万参数。这在很老的一些网络里是真实存在的设计,但现在基本没人这么干了,因为纯全连接处理图像既不高效又容易过拟合,图片的空间局部特征完全被忽略了。

所以今天的实践中,全连接层一般被用在两个位置:一是网络最后当分类头,也就是把前面的特征向量映射到类别得分;二是用在输入是定长向量的场景里,比如推荐系统的特征向量、结构化表格数据、词嵌入向量的拼接等。如果你处理图像、音频、序列,前面一般要有卷积、循环网络或注意力结构来提取特征,最后再接全连接层输出。

2. 激活函数:让模型从直线变成曲线

2.1 为什么没有激活函数就不行

如果网络里只有全连接层,那不管堆多少层,最终等价于一个线性变换。你可以自己推一下:y = W2 @ (W1 @ x + b1) + b2,这个式子展开之后,本质上还是一个矩阵乘一个x再加一个偏移。也就是说,多层线性层的叠加,完全可以合并成一层。这样的模型只能表达线性关系,别说图像识别,连异或这种稍微复杂一点的规律都学不会。

激活函数的作用,就是给网络引入非线性。激活函数本身一般是一个逐元素的操作,输入一个数,输出一个数,但因为它是非线性的,所以每一层的输出不再是输入的线性组合,多层堆叠起来后,模型的表达能力就是真正的“深度”了。

可以这样理解:线性层负责“调整坐标轴”,激活函数负责“折叠空间”。有了折叠,模型才能把线性不可分的两个类别分开。这就像折纸,一张纸本身是平的,但你不停地折,它就能在空间中变出复杂的形态,神经网络就是这么干。

2.2 常见激活函数的数学本质与行为对比

说到激活函数,最常见的几个就是Sigmoid、Tanh、ReLU,以及ReLU的变体Leaky ReLU,还有输出层的Softmax。我把它们整理成一张表,方便对照。

函数公式输出范围导数范围主要优点主要缺点
Sigmoid1 / (1 + exp(-x))(0, 1)(0, 0.25]输出可以当概率;历史最悠久梯度消失严重;输出均值不为0;含有指数运算
Tanh(exp(x) - exp(-x)) / (exp(x) + exp(-x))(-1, 1)(0, 1]零均值,梯度消失比Sigmoid好一些仍存在饱和区梯度消失
ReLUmax(0, x)[0, +∞)正区间为1,负区间为0简单、正区间无梯度消失、计算快负区间会“死亡神经元”;输出均值大于0
Leaky ReLUmax(0.01x, x)(-∞, +∞)正区间1,负区间0.01保留负区间的梯度斜率需要手动设置
Softmaxexp(x_i) / sum(exp(x))(0, 1),各输出和为1与Sigmoid有关输出的确是多分类概率一般不用于隐藏层,仅用于输出层

这张表信息量比较大,我展开说一下最重要的几个点。

先说Sigmoid。它的输出范围在0到1之间,确实很像概率,早年很流行。但是用的时候要注意,在x绝对值比较大的时候,它的导数会变得非常小,最大值也就0.25。多层反向传播的时候,梯度一层一层乘下来,很快就趋近于0了,神经网络几乎学不动。这就是所谓的“梯度消失”。另外Sigmoid的输出均值是0.5,不是0,这会让下一层收到的信号全部是正数,对权重的更新方向有影响,训练时容易出现锯齿状波动。

再说ReLU。ReLU在正区间导数恒为1,所以从全连接层出来的正值信号可以直接传下去,梯度不会迅速衰减。这个特性让深度网络在训练上有了质的飞跃,也让它成了当前隐藏层的主流默认选择。但它有一个致命陷阱:如果某一层的输出在某个神经元上长期是负数,那这个神经元的导数就是0,权重再也更新不了,就永远输出0。这就是所谓的“死亡神经元”。

Leaky ReLU就是针对这个问题做了改良:负数区间不再一刀切为0,而是给一个很小的斜率(通常是0.01或0.2),这样负区间的梯度至少还有一点,神经元不会彻底死掉。

Softmax和上面那些激活函数用途不同,它不放在隐藏层,而是放在分类任务的最后一层。它做的事情是把一组实数转换成一组和为1的正数。比如模型输出[2.0, 1.0, 0.1],经过Softmax之后变成类似[0.65, 0.25, 0.10]的结果,怎么看都是“更像第一类”的意思。多分类任务在配合交叉熵损失使用它时,训练会更加稳定。

2.3 为什么ReLU是默认选择

我在好多项目里帮人调过参,很多人一上来先把Sigmoid、Tanh、ReLU全部试一遍,然后问我哪个最好。其实实践中隐藏层首选ReLU,这背后是有明确理由的。

首先,它计算简单,激活本身没有指数运算,前向和反向都快,在几十层网络里差别很明显。其次,它正区间的梯度是1,不管层有多深,只要输入为正,梯度就能比较顺畅地传下去。这一点对深层网络的训练至关重要。

但它也不是没有注意事项。ReLU的负区间是0,会让部分神经元的梯度恒为0。如果整个训练过程中某个神经元一直在负区间,那它就被“判了死刑”。为了避免大面积的死亡神经元,一般配合合适的权重初始化,让输入刚好落在正负区间的临界附近。这也就是为什么现在的初始化方法通常推荐Kaiming初始化——它的设计初衷,就是让数据在经过ReLU层之后,输出的方差保持稳定,避免太多神经元一开始就落在负区间。

另外还有一个细节,ReLU的输出均值大于0,不是以0为中心的,这会让下一层收到的全是非负的信号。用一些人话来说,就是信息流总是正的,会造成一些“偏置漂移”。所以你在很多现代网络里会看到:卷积或全连接层后面接ReLU,再接BatchNorm。BatchNorm会把输出重新拉回0均值1方差附近,一定程度上抵消ReLU的偏置漂移问题。

2.4 隐藏层的激活函数选择策略

说一个比较实用的策略,我自己一直这么用。

隐藏层:默认ReLU。如果遇到训练后期神经元大面积死亡,或者收敛速度不够满意,再换成Leaky ReLU或者它的近亲PReLU。不要一上来就用Sigmoid或Tanh,除非你的网络特别浅,或者你有特殊的理由(比如某些时序建模的场景)。

输出层:二分类,一般一个神经元配Sigmoid;多分类,一般用Softmax,配合交叉熵损失函数;回归任务,输出层不加激活函数,直接输出原始数值。这些选择不是随便定的,而是跟损失函数和概率解释紧密绑在一起的,后期如果真的做项目,你会发现换掉这个组合,训练表现往往更差。

激活函数这块,我特别想说的一点是:它不是越新越好,也不是越复杂越好。公式复杂不代表有效,有些花哨的激活函数,效果还不如ReLU稳定。项目里最重要的是可复现、可解释、好调参,而不是一味追新。这个原则在很多地方都适用。

3. 反向传播中的梯度是如何流过这两层的

3.1 反向传播的直觉理解

训练神经网络,本质上是让损失函数变小。怎么变小?每次算出损失之后,要看看每个参数对损失的影响有多大,然后往减少损失的方向调整参数。这就是反向传播。

我见过有人把反向传播想得很玄,其实它就是一个链式法则的问题。设损失为L,某个全连接层的权重W,那么:

∂L / ∂W = ∂L / ∂y * ∂y / ∂W

对于全连接层y = W @ x + b,∂y / ∂W就等于输入x本身,∂y / ∂b等于1。所以反向传播时,权重W的梯度等于“从上层传来的梯度”乘以上层的输入x。偏置b的梯度就等于上层传来的梯度本身。

这层逻辑告诉你一个很重要的结论:权重的更新依赖于输入信号的大小。如果输入x的值太大,梯度就会很大,训练容易震荡;如果输入x的值太小,梯度就很小,网络更新缓慢。这就是为什么我们通常要把输入数据归一化——不是为了让代码显得华丽,而是为了让梯度维持在合理范围。

3.2 梯度消失和梯度爆炸的根源

梯度问题本质上和激活函数、权重初始化的选择有关。

先说梯度消失。最常见的场景就是Sigmoid。它的导数最大只有0.25,如果网络有10层,每层梯度都乘以一个不超过0.25的数,10层之后梯度至少缩小到原来的0.25的10次方,这是什么概念?约等于十万分之一。这么小的梯度,前几层的权重几乎就不会更新了。所以Sigmoid在深层网络里基本被淘汰,这不是偏见,是数学上的必然。

梯度爆炸则往往和权重初始化不当有关。如果初始化的时候权重比较大,或者激活函数没有饱和,每层梯度乘以一个大于1的数,多层叠加下去,梯度就会指数级放大,最后loss变成NaN,训练直接崩溃。正规做法是使用合适的初始化方法,就是专门设计一个让方差在前向和反向传播中保持稳定的初始化策略。它的目标很明确:何不让每一层的输入和输出方差差不多?何不让每一层的梯度方差差不多?这样从头到尾梯度既不会消失也不会爆炸。

3.3 从梯度角度理解ReLU为什么好用

ReLU在正区间的导数恒为1,这就意味着,只要输入为正,反向传播经过这个激活函数时梯度不衰减。相比Sigmoid永远小于1的导数,ReLU对深层次网络的梯度传播友好得多。

但要注意,ReLU的梯度在负区间是0,这意味着一旦某个神经元落入负区间,它对这个batch的贡献就是0,同时梯度也是0。如果网络有大量死亡神经元,相当于模型的实际宽度变小了,表达能力下降,训练可能陷入停滞。这时不仅是换激活函数的问题,还要检查权重初始化和学习率。

我自己在实际项目里遇到过这样的情况:一个模拟项目X的分类模型,前几个epoch loss降得挺正常,之后突然就卡住不动了。查了很久,发现是ReLU导致的神经元大面积死亡,后半段的输出全是0。后来把initialization换成更适合ReLU的方法,并把学习率从0.1降到0.01,问题立刻缓解。这就是“死亡ReLU”的典型表现,处理起来其实并不难,难的是能想到是这个原因。

4. 实操:搭一个两层全连接网络从零开始

4.1 模型结构设计

说了这么多理论,终于到实操环节了。我拿一个手写数字分类任务当例子,这个任务和真实项目里的很多流程是相通的。数据的每个样本是一张28×28的灰度图,我们有10个类别(0到9)。

网络结构设计如下:

输入:784维 第一层:全连接层,输出128维 激活:ReLU 第二层:全连接层,输出10维 输出层:Softmax(与交叉熵损失配合)

参数量计算:

  • 第一层:784×128+128=100480
  • 第二层:128×10+10=1290
  • 总参数量:101770,合约10万参数

这个规模在今天的深度学习里算是非常小的,但在CPU上就能轻松训练,非常适合拿来理解全流程。代码逻辑大概长这样:

class MyModel: def __init__(self): self.w1 = init_kaiming(784, 128) self.b1 = zeros(128) self.w2 = init_kaiming(128, 10) self.b2 = zeros(10) def forward(self, x): x = flatten(x) h = relu(linear(x, self.w1, self.b1)) logits = linear(h, self.w2, self.b2) return logits

注意我这里没有指名任何具体框架,但接口思路是通用的:你只需要把init_kaiming、linear、relu这些函数换成你用的框架对应接口就行,维度不变。

4.2 训练过程中的关键调参点

训练这个模型,我一般会盯几件事:loss是否下降、下降速度是否正常、最终准确率大概是多少。

第一次训练前,可以做一个有意思的验证:随机初始化后,先不训练,直接算一次loss。如果是10分类任务配合交叉熵,初始loss应该约等于ln(10)=2.3026。如果这个数值差得很远,说明初始化或者数据预处理有问题,即使后面能训,也是在错误的前提下做优化。

训练开始后,正常情况下loss会在前几个epoch快速下降。这是因为ReLU让梯度流动顺畅,交叉熵本身也对“分类是否正确”非常敏感,稍微调整就能产生明显的损失变化。再往后,loss下降速度会放缓,这时候别急着加大学习率,先观察是否过拟合了。

我个人的项目习惯是这样的:训练集和验证集的loss同时下降低,说明模型还在正常学习;训练集loss持续下降但验证集loss开始上升,说明已经过拟合了,应该早点停止,或者给模型加一点正则。这里我一般会用早停法,保留验证集表现最好的那一步模型作为最终模型。

4.3 为什么输出层通常不加激活函数

很多初学者会问:输出层不是应该用Softmax吗?为什么我写的代码里输出层只做了一次线性变换,没有显式加激活函数?

原因在于,如果你用Softmax函数 + 交叉熵损失,两者在数学上可以合并求导,框架在计算时会把它们合并,数值上更稳定,计算上也更快。所以常见的做法是,模型最后一层输出的是未经Softmax的原始分数(logits),在计算损失的时候直接喂给交叉熵损失函数,交叉熵内部会自动处理Softmax的数值稳定性问题。只有在推理阶段,如果想输出概率,才显式调用Softmax。

这个细节能避免好多新手踩坑。如果你在输出层手写了Softmax,再用交叉熵损失,你会发现训练时偶尔会出现NaN,这往往不是因为模型的问题,而是Softmax算出来的概率太小或接近0,log之后变成了负无穷。框架内部的融合实现就是为了规避这种数值问题。

4.4 训练过程中的数据预处理

全连接网络对数据范围很敏感。前面说过,权重梯度正比于输入,如果输入像素值全部是0到255之间的数,梯度整体会比0到1之间的输入大很多,学习率稍微设错,训练就容易震荡。

我处理图像数据时,习惯先把像素除以255,让范围落在0到1之间,再按通道计算均值和标准差做标准化。结构化表格数据就做Z-score标准化,让每个特征的均值接近0、方差接近1。这一步看起来不起眼,但在全连接层居多的小模型里,数据范围对收敛速度的影响很明显。

顺便提醒一句,测试和推理的时候也要用训练集算出来的归一化参数去处理,不能重新对测试数据单独算一套均值方差。很多人忘了这个问题,训练表现不错,一到线上就崩,一半的概率是预处理不一致。

5. 常见问题与排查技巧实录

5.1 训练不收敛的排查思路

我把平时排查训练问题的心得整理成了一张速查表,每一行都是我踩过或帮人踩过的坑。

现象大概率原因排查/解决方向
loss完全不下降,准确率等于随机学习率过小或数据未归一化调大学习率观察;检查数据范围
loss从一开始就是NaN学习率过大;数据里有NaN;输出层数值不稳定降低学习率;检查数据;确认Softmax与损失函数的组合
训练集loss持续降,验证集loss回升过拟合早停;增加正则;减小模型宽度
训练到一半loss卡住不动ReLU死亡神经元;学习率过低;陷入局部平坦区换激活函数;重新初始化;调整学习率策略
训练正常但推理时效果差预处理不一致;模型没有切到推理模式用同一套归一化参数;确认推理阶段不计算dropout等操作
输出全是同一个类别数据类别不平衡;最后一层偏置初始化问题检查数据分布;调整损失函数权重或初始化策略

这些是我在实际调试中遇到频率最高的问题,排查路径也相对固定。只要逻辑清晰,大部分问题都能在半小时内定位。

5.2 判读报错信息的小技巧

框架报错信息经常又长又吓人,但实际上大部分维度错误都写在第一行和最后一行。遇到报错,不要慌,先看两个信息:期望的shape和实际拿到的shape。如果你正准备把全连接层的输入从四维特征图变成二维矩阵,报错几乎总是因为忘了flatten。

我自己有一个习惯:报错信息先复制到编辑器里,把无关行删掉,只保留“Expected size”和“Got size”这两行。95%的问题光看这两行就能解决。剩下的才需要去查框架源码,但那已经是另一个层面的事情了。

5.3 我个人的项目习惯

最后分享几个我从实际项目里沉淀下来的习惯,做小模型调参特别有用。

第一个,每次改参数之前先记一个“基线结果”。比如当前模型loss是0.35、准确率94%,我把这些数字写下来再改参数。没有基线,你永远不知道某个改动到底是变好了还是变坏了。

第二个,全连接网络里,第一层的宽度不要一开始就加得很大。我见过有人上来就用1024、2048个神经元的,结果是训练慢、显存高、还容易过拟合。先从128或256开始,确认整个流程没问题,再逐步加宽,这样定位问题更快。

第三个,保存模型的时候连预处理参数一起保存,或者写一个统一的预处理函数,训练和推理都调同一个函数。这能杜绝非常多“训练正常、部署翻车”的问题。

第四个,不要迷信“换个更高级的激活函数模型就会变好”。绝大多数情况下,用ReLU、训练充分、数据干净,就已经能拿到90分的效果。剩下的10分往往出在数据质量、特征工程和学习率策略上,而不是网络结构本身。

6. 一些小建议

全连接层和激活函数是深度学习的两个基石,它们本身不复杂,但如果你真的理解透了,后续学卷积、循环网络、注意力机制都会顺畅很多。因为这些结构本质上也离不开线性变换和非线性激活的组合,只不过它们在线性变换的部分加入了局部连接、参数共享等额外约束。

我自己回头看,第一次把这两块内容弄明白,其实是在一遍又一遍复现简单模型的过程中,而不是在看书或看教程的时候。我建议你也动手搭个小模型,用最简单的数据集把训练、验证、预测整个流程跑通,再故意制造几个错误观察现象,比如把激活函数换成Sigmoid、把数据不归一化、把学习率设成0.5,亲眼看看会发生什么。这种“故意弄坏再修好”的过程,比看任何文章都更能加深记忆。

一次实践的价值,往往胜过十遍阅读。希望这篇拆解能帮你在全连接层和激活函数上真正打通,后面我们再继续深入更复杂的网络结构时,你会发现自己心里已经有了一个很稳固的框架。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询