一、从生物神经元到人工感知机
深度学习的名字里带有“神经”二字,是因为它最初的灵感来自生物神经元。一个生物神经元大致是这样工作的:树突接收来自其他神经元的信号,细胞体对这些信号进行整合;当总信号超过某个阈值时,神经元就会被激活,并通过轴突向外发出脉冲。人工感知机就是对这一过程的高度简化。1957 年前后,Frank Rosenblatt 提出了人工感知机模型。它的数学表达非常简单:
$$y = f(W^T x + b)$$
也可以写成:
$$y = f\left(\sum_{i=1}^{n} w_i x_i + b\right)$$
其中:
- $x$ 是输入特征向量。例如一张图片的像素值、一个样本的若干数值特征;
- $W$ 是权重向量,表示每个输入特征的重要程度;
- $b$ 是偏置,可以理解为一个阈值,决定神经元是否容易被激活;
- $f$ 是激活函数,或者说输出函数;
- $y$ 是神经元的输出。
很多初学者第一次看到这个公式时会问:这里的 $f$ 到底是什么?是不是一个 0/1 函数?这个猜测是有道理的。在最早的感知机模型中,$f$ 确实常常是阶跃函数:
$$ f(z)= \begin{cases} 1, & z > 0 \\ 0, & z \le 0 \end{cases} $$
也就是说,从二维来看,如果加权求和的结果大于 0,输出 1;否则输出 0。这看起来就是一个硬性的二分类器。从几何上看,感知机在输入空间中定义了一个超平面:
$$ W^T x + b = 0 $$
这个超平面把数据分成两类:一侧输出 1,另一侧输出 0。在二维空间里,它是一条直线;在三维空间里,它是一个平面;在更高维空间里,它是超平面。因此,感知机本质上是一个线性分类器。
不过,阶跃函数虽然直观,却有一个严重问题:它在 $z=0$ 处不可导。不可导意味着我们无法用微积分中的链式法则去计算梯度,也就很难通过“误差反向传播”来更新参数。所以,在现代神经网络中,感知机公式里的 $f$ 通常不再是阶跃函数,而是更平滑、至少几乎处处可导的函数,例如 Sigmoid、Tanh、ReLU 等。这些函数统称为激活函数。就是说,今天当我们写:
$$ y = f(W^T x + b) $$
时,$f$ 通常指的就是激活函数。
二、为什么需要多层:从线性可分到异或问题
单个感知机能解决的问题是有限的。它只能处理线性可分的问题。所谓线性可分,通俗地说,就是可以用一条直线、一个平面或者一个超平面把不同类别的数据完全分开。例如,如果平面上有两类点,一类在左下方,另一类在右上方,那么我们也许可以用一条直线把它们切开。这种问题对感知机来说不难。
但有些问题不是线性可分的。最经典的例子是异或问题 XOR:假设输入只有两个特征 $x_1$ 和 $x_2$,输出是它们的异或结果:
| $x_1$ | $x_2$ | 输出 | ||:|:| | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 0 |
如果把这四个点画在二维平面上,会发现:输出为 1 的点是 $(0,1)$ 和 $(1,0)$,输出为 0 的点是 $(0,0)$ 和 $(1,1)$。无论你怎么画一条直线,都无法把这两类点完全分开。这就是单层感知机的局限。
历史上,这个问题曾经给神经网络研究带来严重打击。1969 年,Minsky 和 Papert 在《感知机》一书中用严格的数学论证指出,单层感知机无法解决异或问题。这也导致了神经网络研究的第一次寒冬。
解决思路其实很自然:既然一条直线不够,那就用多条直线组合;既然一次线性划分不够,那就多次变换、多次划分。于是,我们把多个神经元组织成多层结构,形成多层感知机,也就是 MLP,Multi-Layer Perceptron。多层网络可以通过多次线性变换和非线性激活,对输入空间进行扭曲、折叠和重新划分。原本线性不可分的数据,在经过若干层变换之后,可能就变成线性可分的了。
三、输入层、隐藏层和输出层
神经网络中经常提到“层”这个概念。一个典型的多层感知机通常包含三类层:
- 输入层;
- 隐藏层;
- 输出层。
输入层负责接收现实世界的数据。例如图片的像素值、文本的数值表示、房价预测中的面积和楼层等特征。输入层是我们可见的,它的维度通常由数据本身决定。
输出层负责给出最终预测结果。例如:
- 房价预测中,输出可能是一个连续数值;
- 猫狗分类中,输出可能是“猫”和“狗”两个类别的概率;
- 手写数字识别中,输出可能是 0 到 9 这 10 个类别的概率。
输出层也是我们可见的,它的维度通常由任务决定。夹在输入层和输出层之间的层,都叫隐藏层。为什么叫“隐藏”?因为这些层的输入和输出并不直接对应现实世界中的某个明确物理量。它们更多是在内部进行特征提取、空间变换和抽象表达。对于使用者来说,它们像是藏在模型内部的中间结构,所以称为隐藏层。
举例来说,一个简单网络可以是:
$$ 输入维度 10 \rightarrow 隐藏层 64 \rightarrow 隐藏层 32 \rightarrow 输出维度 2 $$
这里有两个隐藏层,分别有 64 个节点和 32 个节点。这里的“节点数”可以理解为该层输出向量的维度。如果某一层有 64 个节点,那么这一层的输出就是一个 64 维向量。在数学上,如果第 $l-1$ 层的输出是 $a^{(l-1)}$,第 $l$ 层的输出是 $a^{(l)}$,那么一个线性变换可以写成:
$$ z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)} $$
其中:
- 如果 $a^{(l-1)}$ 是 $d_{l-1}$ 维向量;
- $a^{(l)}$ 是 $d_l$ 维向量;
那么权重矩阵的形状通常是:
$$ W^{(l)} \in \mathbb{R}^{d_l \times d_{l-1}} $$
偏置的形状是:
$$ b^{(l)} \in \mathbb{R}^{d_l} $$
在 PyTorch 中,nn.Linear(in_features, out_features) 就表示这样一个线性变换。它内部维护的权重形状通常是 [out_features, in_features]。
那么,隐藏层的节点数是固定的吗?答案是:模型一旦实例化,结构就固定了;但在设计模型时,隐藏层节点数是可以由我们选择的超参数。输入层维度通常由数据决定,输出层维度通常由任务决定,而隐藏层的层数和每层节点数则有较大的设计空间。
这里有一些常见经验:
- 节点越多,模型容量越大,能拟合更复杂的函数;
- 如果数据量不够大,网络过大容易过拟合,也就是把训练集“背下来了”,但在新数据上表现不好;
- 在很多分类任务中,隐藏层维度会呈现“漏斗形”,例如从 256 到 128,再到 64,最后到类别数;
- 没有万能公式告诉我们某一层必须有多少节点,通常需要结合经验、验证集表现和消融实验来调整。
换句话说,网络结构设计既是一门科学,也包含相当多的工程经验。
四、激活函数:多层网络的灵魂
一开始就提到的激活函数,其实是有其必要性的。有了多层结构之后,一个新的问题出现了:如果我们只是把很多层线性变换堆起来,而不引入任何非线性,会发生什么?假设我们有两个线性层:
$$ h = W_1 x + b_1 $$
$$ y = W_2 h + b_2 $$
把第一个式子代入第二个式子:
$$ y = W_2(W_1 x + b_1) + b_2 $$
展开:
$$ y = W_2 W_1 x + W_2 b_1 + b_2 $$
令:
$$ W’ = W_2 W_1 $$
$$ b’ = W_2 b_1 + b_2 $$
那么:
$$ y = W’ x + b' $$
你会发现,无论中间堆了多少个线性层,只要没有激活函数,最终结果仍然是一个线性变换。也就是说:**没有激活函数的多层神经网络,本质上仍然是一个单层线性模型。**这就是激活函数存在的核心意义:引入非线性。激活函数通常接在线性变换之后。一个神经元的完整计算过程可以写成:
$$ z = W^T x + b $$
$$ a = \sigma(z) $$
其中 $\sigma$ 就是激活函数。
也就是说,激活函数不是游离在网络外面的额外模块,而是每个神经元内部的重要组成部分。
常见的激活函数有以下几种。
1. Sigmoid
Sigmoid 的公式是:
$$ \sigma(x) = \frac{1}{1 + e^{-x}} $$
它把任意实数压缩到 $(0,1)$ 区间内。因此,Sigmoid 常用于输出层表示概率,尤其是在二分类任务中。
不过,Sigmoid 也有缺点。它的导数在输入很大或很小的时候接近 0。如果网络很深,梯度在反向传播过程中可能会越来越小,导致前面的层几乎学不动。这就是著名的梯度消失问题。
2. ReLU
ReLU 是目前隐藏层中最常用的激活函数之一。它的定义非常简单:
$$ \mathrm{ReLU}(x) = \max(0, x) $$
也就是说:
- 当 $x > 0$ 时,输出 $x$;
- 当 $x \le 0$ 时,输出 0。
ReLU 的优点是计算简单,而且在正区间内导数恒为 1,这在一定程度上缓解了梯度消失问题。
很多人会问:ReLU 是分段函数,在 $x=0$ 处有尖角,严格来说不可导,那怎么求导?这是一个很好的问题。ReLU 的导数可以这样写:
$$ \mathrm{ReLU}’(x)= \begin{cases} 1, & x > 0 \\ 0, & x < 0 \end{cases} $$
在 $x=0$ 处,数学上确实不可导。但在工程实现中,我们通常人为规定:
$$ \mathrm{ReLU}’(0) = 0 $$
有时也有其他约定,但这通常不影响训练大局。因为在连续浮点数运算中,某个激活值恰好等于绝对精确的 0 的概率非常小。即使真的遇到了,把这个位置的梯度设为 0,最多意味着该神经元在这一步不更新,对整体训练影响很小。更严格地说,这里使用的是次梯度的思想。ReLU 是凸函数,在 $x=0$ 处存在次梯度区间。深度学习的优化实践允许我们在这种“几乎处处可导”的函数上进行梯度下降。
五、为什么简单的激活函数能拟合复杂的非线性关系
理解了激活函数之后,另一个常见的问题是:ReLU 看起来这么简单,Sigmoid 也不复杂,为什么这些简单的函数组合起来,就能拟合非常复杂的非线性关系?这里背后有一个重要理论:万能近似定理。万能近似定理的一个常见表述是:在适当的条件下,一个具有足够多隐藏层节点的前馈神经网络,可以以任意精度逼近紧致集合上的连续函数。
注意几个关键词:
- “足够多节点”:表达能力够强;
- “适当条件”:对激活函数和网络结构有一定要求;
- “任意精度逼近”:不是说完全相等,而是可以足够接近;
- “连续函数”:这是经典定理适用的重要范围。
为了直观理解,可以把 ReLU 想象成一小段折线。单个 ReLU 只能形成一个简单的拐点。但如果有很多神经元,每个神经元都有自己的权重和偏置,它们就可以产生很多不同的折线段。这些折线段经过后续层的组合、叠加、折叠,就可以拼出非常复杂的曲线或曲面。就像单个像素很简单,但几百万个像素可以组成一张高清图片;单个乐高积木很简单,但足够多的积木可以拼出复杂的形状。因此,神经网络并不是靠某一个激活函数本身多么复杂,而是靠大量简单单元的组合产生复杂表达能力。
当然,万能近似定理主要回答的是“能不能表示”的问题,并没有完全回答:
- 训练能不能找到合适的参数;
- 需要多少数据;
- 模型能不能泛化到新样本;
- 多深、多宽才是最合适的。
这些问题至今仍然是深度学习研究的重要内容。对于初学者,现阶段可以先建立一个直观认识:神经网络通过大量简单函数的组合,获得了表达复杂映射关系的能力。
六、神经网络的数学本质:一个高维复合函数
如果我们暂时抛开生物学比喻,神经网络在数学上到底是什么?本质上,它是一个复杂的高维复合函数。假设输入是 $x$,输出是 $\hat{y}$,网络共有 $L$ 层。第 $l$ 层的计算可以写成:
$$ z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)} $$
$$ a^{(l)} = \sigma^{(l)}(z^{(l)}) $$
其中:
- $a^{(0)} = x$,也就是输入;
- $a^{(l)}$ 是第 $l$ 层的激活值;
- $z^{(l)}$ 是第 $l$ 层线性变换之后、激活之前的中间结果;
- $\sigma^{(l)}$ 是第 $l$ 层的激活函数;
- $a^{(L)} = \hat{y}$,也就是最终输出。
整个网络可以写成:
$$ \hat{y} = f(x;\theta) $$
其中 $\theta$ 是所有可学习参数的集合:
$$ \theta = {W^{(1)}, b^{(1)}, W^{(2)}, b^{(2)}, \dots, W^{(L)}, b^{(L)}} $$
训练神经网络,本质上就是在寻找一组参数 $\theta$,使得模型在训练数据上的表现尽可能好。
假设我们有 $N$ 个训练样本:
$$ (x_1, y_1), (x_2, y_2), \dots, (x_N, y_N) $$
损失函数记为 $\mathcal{L}$,那么训练目标通常可以写成:
$$ \theta^* = \arg\min_{\theta} \frac{1}{N} \sum_{i=1}^{N} \mathcal{L}(f(x_i;\theta), y_i) $$
这叫做经验风险最小化。
如果再加上正则项,还可以写成:
$$ \theta^* = \arg\min_{\theta} \left[ \frac{1}{N} \sum_{i=1}^{N} \mathcal{L}(f(x_i;\theta), y_i)
- \lambda R(\theta) \right] $$
其中 $R(\theta)$ 是正则项,用来抑制过拟合。
从优化角度看,神经网络训练通常是一个高维非凸优化问题。损失函数的地形不是一个简单的碗状曲面,而是充满了局部极小值、鞍点和平坦区域的复杂空间。这也解释了为什么优化器、初始化、学习率调度等工程细节如此重要。
七、损失函数:模型的成绩单
模型做完前向传播之后,会得到一个预测值 $\hat{y}$。接下来我们需要一个标准来衡量预测值和真实值之间差多少。这个标准就是损失函数。损失函数越小,说明模型在当前数据上的表现越好。
1. 回归任务:均方误差 MSE
回归任务预测的是连续值,例如房价、温度、销量等。常用的损失函数是均方误差:
$$ \mathrm{MSE} = \frac{1}{N}\sum_{i=1}^{N}(y_i - \hat{y}_i)^2 $$
其中:
- $y_i$ 是真实值;
- $\hat{y}_i$ 是预测值。
MSE 的特点是误差越大,惩罚越重,因为误差被平方了。例如,预测误差是 2,平方后贡献 4;误差是 10,平方后贡献 100。
2. 分类任务:交叉熵损失
分类任务预测的是离散类别,例如猫或狗、数字 0 到 9。分类任务中常用的损失函数是交叉熵损失。如果模型输出的是一个概率分布 $q$,真实标签用 one-hot 分布 $p$ 表示,那么交叉熵可以写成:
$$ H(p,q) = -\sum_{c} p_c \log q_c $$
其中 $c$ 表示类别。
假设真实类别是猫,那么 $p$ 在猫这个类别上为 1,在狗这个类别上为 0。如果模型预测猫的概率很高,损失就很小;如果模型非常自信地预测成了狗,损失就会很大。交叉熵的直觉可以理解为“惊讶度”:模型对正确答案越不确信,损失越大;模型越确信地犯错,惩罚越重。对于二分类任务,常见的形式是二元交叉熵:
$$ \mathcal{L} = -[y \log \hat{y} + (1-y)\log(1-\hat{y})] $$
对于多分类任务,通常会在输出层使用 Softmax,将输出转化为概率分布,再配合交叉熵损失。
八、优化器:蒙眼下山的策略
有了损失函数,我们就知道模型当前“站”在损失地形上的哪个高度。接下来的目标是下山,也就是降低损失,优化器就是决定我们怎么“下山”。
最核心的思想是梯度下降。
如果损失函数是 $\mathcal{L}(\theta)$,参数是 $\theta$,那么梯度下降的基本更新公式是:
$$ \theta_{\text{new}} = \theta_{\text{old}} - \eta \nabla_{\theta} \mathcal{L} $$
其中:
- $\eta$ 是学习率,也就是步长;
- $\nabla_{\theta} \mathcal{L}$ 是损失函数对参数的梯度;
- 负号表示沿着梯度反方向走,因为梯度指向函数上升最快的方向。
可以把整个过程想象成一个人蒙着眼睛站在山上,他的目标是走到山谷。虽然他看不见全局地形,但他可以感受脚下的坡度,然后沿着最陡的下坡方向迈一步。
1. 批量梯度下降、随机梯度下降和小批量梯度下降
如果每次使用全部训练样本计算梯度,这种方法叫批量梯度下降,Batch Gradient Descent。它的优点是方向稳定,缺点是当数据量很大时,每走一步都要计算所有样本的梯度,代价太高。
如果每次只用一个样本计算梯度,这种方法叫随机梯度下降,Stochastic Gradient Descent,简称 SGD。它的优点是速度快,缺点是梯度方向噪声很大,更新轨迹会来回震荡。
实际训练中最常用的是小批量梯度下降,Mini-batch Gradient Descent。每次随机抽取一小批样本,比如 32、64、128 个样本,计算这一小批数据的平均梯度,然后更新参数。
在深度学习里,我们常说的 SGD 往往也泛指基于小批量的梯度下降。
那么,为什么 SGD 中有“随机”两个字?因为每次参与计算的小批量是从训练集中随机抽取的。不同小批量计算出来的梯度并不完全相同,甚至可能偏离真实的全量梯度。因此,梯度方向带有随机噪声。
有趣的是,这种噪声并不完全是坏事。它有时候可以帮助模型跳出一些尖锐的局部极小值,进入更平坦、泛化能力更好的区域。
2. SGD 的缺点
SGD 虽然简单,但在复杂地形中会遇到一些问题。例如,如果损失函数的地形像一个狭长峡谷,SGD 可能会在峡谷两侧来回震荡,而沿峡谷底部前进的速度很慢。
另外,SGD 对学习率非常敏感:
- 学习率太大,容易震荡甚至发散;
- 学习率太小,训练速度极慢;
- 固定学习率在训练后期可能仍然过大,导致无法稳定收敛。
因此,人们提出了许多改进方法。
3. Adam:目前最常用的优化器之一
Adam 是工业界和科研中都非常常用的优化器。它的名字来自 Adaptive Moment Estimation,可以理解为自适应矩估计。Adam 融合了两种思想:动量和自适应学习率。
先看动量。普通 SGD 每一步只看当前梯度。动量方法则会把过去的梯度方向积累起来,类似于一个滚下山的球具有惯性。如果连续多步梯度都指向同一个方向,动量会放大这个方向,使更新更快;如果梯度方向来回震荡,动量会相互抵消一部分,从而减少震荡。
再看自适应学习率。不同参数对梯度的敏感程度不同,更新频率也不同。Adam 会为每个参数维护不同的学习率尺度。具体来说,Adam 会记录梯度的一阶矩和二阶矩:
$$ m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t $$
$$ v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2 $$
其中:
- $g_t$ 是当前梯度;
- $m_t$ 可以理解为梯度的一阶矩估计,类似均值;
- $v_t$ 可以理解为梯度的二阶矩估计,类似未中心化的方差;
- $\beta_1$ 通常取 0.9;
- $\beta_2$ 通常取 0.999。
为了避免初始化为 0 带来的偏差,Adam 还会做偏差校正:
$$ \hat{m}_t = \frac{m_t}{1-\beta_1^t} $$
$$ \hat{v}_t = \frac{v_t}{1-\beta_2^t} $$
最后参数更新:
$$ \theta_t = \theta_{t-1} - \frac{\eta}{\sqrt{\hat{v}_t}+\epsilon}\hat{m}_t $$
其中 $\epsilon$ 是一个很小的数,用来防止除零。
直观理解:
- 如果某个参数近期梯度很大且方向一致,Adam 会让它快速前进;
- 如果某个参数梯度方向反复横跳,Adam 会适当抑制它;
- 如果某个参数一直梯度较小,Adam 会相对放大它的更新幅度;
- 如果某个参数梯度很大,Adam 会减小其有效学习率,防止冲过头。
因此,Adam 通常比朴素 SGD 更容易调参,也常作为默认优化器。
九、反向传播:让神经网络学会承担责任
现在我们有了模型、损失函数和优化器。但还缺一个关键环节:如何计算损失函数对每一个参数的梯度?也就是说,每个节点的$W$和$b$对于Loss的偏导怎么求解?
神经网络中可能有成千上万、甚至数十亿个参数。如果对每个参数都单独从头求导,计算量会极其庞大。反向传播的伟大之处在于,它用一种非常高效的方式完成了这项工作。
1. 训练的完整闭环
神经网络训练通常包括四步:
- 前向传播:输入数据,逐层计算预测值;
- 计算损失:比较预测值和真实值;
- 反向传播:利用链式法则,从后往前计算每个参数的梯度;
- 参数更新:优化器根据梯度更新参数。
然后不断重复这个过程,直到损失收敛,或者达到预设的训练轮数。
2. 为什么不能直接对每个参数单独求导?
假设网络有上百万个参数。如果我们对每个参数单独求偏导,会产生大量重复计算。例如,某个中间变量可能同时影响后续很多层。如果对每个参数都独立展开整个计算过程,这个公共部分会被反复计算很多次。这在计算机科学中可以看作一个具有大量重复子问题的问题。
反向传播利用了两个核心思想:
- 微积分中的链式法则;
- 计算机科学中的动态规划思想。
前向传播时,我们保留每一层的中间结果。反向传播时,我们从输出层开始,逐层向后传递误差信号。每一层的计算结果都会被复用,避免重复展开整个计算图。这样,反向传播的计算复杂度大致与前向传播处于同一数量级,而不是随参数数量指数爆炸。这就是为什么深层神经网络可以被有效训练。
十、反向传播的数学推导
下面我们用更数学化的语言描述反向传播。
假设第 $l$ 层的计算是:
$$ z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)} $$
$$ a^{(l)} = \sigma^{(l)}(z^{(l)}) $$
其中:
$$ a^{(0)} = x $$
最终输出为:
$$ a^{(L)} = \hat{y} $$
损失函数为:
$$ \mathcal{L} = \ell(a^{(L)}, y) $$
我们定义第 $l$ 层的误差信号:
$$ \delta^{(l)} = \frac{\partial \mathcal{L}}{\partial z^{(l)}} $$
这个 $\delta^{(l)}$ 可以理解为:第 $l$ 层的线性输出 $z^{(l)}$ 对最终损失负有多大责任。
1. 输出层误差信号
首先计算最后一层:
$$ \delta^{(L)} = \frac{\partial \mathcal{L}}{\partial a^{(L)}} \odot \sigma^{(L)\prime}(z^{(L)}) $$
其中 $\odot$ 表示逐元素相乘。
这个公式的含义是:
- 先看最终输出 $a^{(L)}$ 对损失的敏感度;
- 再乘以输出层激活函数的导数;
- 得到损失对线性输出 $z^{(L)}$ 的梯度。
2. 隐藏层误差信号
对于第 $l$ 层,如果已知第 $l+1$ 层的误差信号 $\delta^{(l+1)}$,那么:
$$ \delta^{(l)} = \left( (W^{(l+1)})^T \delta^{(l+1)} \right) \odot \sigma^{(l)\prime}(z^{(l)}) $$
这个公式是反向传播的核心递推公式。它的含义是:下一层的误差通过权重矩阵的转置传回到当前层,然后再乘以当前层激活函数的导数。这就是“反向”的含义:误差从输出层开始,一层一层向前传递。这也体现了动态规划思想:第 $l$ 层的误差不需要从头计算,而是复用第 $l+1$ 层已经算好的误差信号。
3. 参数梯度
有了 $\delta^{(l)}$,参数的梯度就变得很容易计算:
$$ \frac{\partial \mathcal{L}}{\partial W^{(l)}}
\delta^{(l)}(a^{(l-1)})^T $$
$$ \frac{\partial \mathcal{L}}{\partial b^{(l)}}
\delta^{(l)} $$
这两个公式非常直观:
权重的梯度由两部分决定:
- 当前层的误差信号 $\delta^{(l)}$;
- 上一层的激活值 $a^{(l-1)}$。
如果上一层某个神经元激活值很大,而当前层误差也很大,那么连接它们的权重就需要较大幅度更新。如果上一层激活值很小,即使当前误差较大,该权重对当前结果的影响也有限,因此梯度也会较小。反向传播正是通过这种方式,把最终的损失合理地分配给每一层、每一个参数。
十一、PyTorch 实战:训练一个极简 MLP
下面我们用 PyTorch 实现一个极简的多层感知机,让它学习拟合一条直线:
$$ y = 2x + 1 $$
输入是 $x$,输出是 $y$。
虽然这个问题用线性回归就可以解决,但我们故意使用一个带有隐藏层和 ReLU 的小 MLP,以便观察完整的训练闭环。
|
|
这段代码虽然很短,但包含了深度学习训练的核心结构。
1. 模型定义
在 SimpleMLP 中:
nn.Linear(1, 5)表示从 1 维输入映射到 5 维隐藏表示;nn.ReLU()是激活函数;nn.Linear(5, 1)表示从 5 维隐藏表示映射到 1 维输出。
从结构上看:
$$ 1 \rightarrow 5 \rightarrow 1 $$
虽然这个问题本身是线性的,但这个结构足以展示 MLP 的基本形态。
2. 损失函数
这里使用 nn.MSELoss(),因为我们要预测连续数值。
如果是分类任务,通常会换成交叉熵损失,例如:
|
|
或者二分类任务中的:
|
|
具体选择取决于任务类型和输出层设计。
3. 优化器
这里使用 Adam:
|
|
model.parameters() 会把模型中所有可学习参数交给优化器。对于这个小网络,主要是两个 nn.Linear 层中的权重和偏置。
学习率 lr=0.05 在这个玩具问题中是可以的。在实际项目中,学习率通常需要根据模型规模、数据规模和任务类型仔细调整。
4. 训练循环中的三件事
训练循环中最核心的是下面三行:
|
|
更准确地说,顺序通常是:
|
|
它们分别对应:
- 清空旧梯度;
- 计算当前梯度;
- 根据梯度更新参数。
其中 optimizer.zero_grad() 非常重要。
PyTorch 默认会累积梯度。这个设计本身很有用,例如我们可以在多个小批量上累积梯度,再执行一次更新,从而实现更大的等效 batch size。
但如果在每次反向传播之前不清空梯度,那么新的梯度会不断叠加到旧梯度上,参数更新方向会越来越偏离当前批次的真实梯度,训练过程可能变得不稳定,甚至完全发散。
所以,在标准训练循环中,每次调用 loss.backward() 之前,通常都要先调用:
|
|
十二、题外话
神经网络的起落与复兴
神经网络的发展并不是一帆风顺的。它经历过热潮,也经历过寒冬。1943 年,McCulloch 和 Pitts 提出了 M-P 神经元模型,用数学方式描述了神经元之间的阈值逻辑。这可以看作人工神经网络的理论源头。20 世纪 50 年代末,Rosenblatt 提出了感知机。它是第一个能够从数据中学习的模型之一,引发了很大关注。1969 年,Minsky 和 Papert 指出单层感知机无法解决异或问题,并且当时缺乏训练多层网络的有效算法。神经网络研究因此进入低谷。1986 年,Rumelhart、Hinton 和 Williams 等人重新推广了反向传播算法,证明多层网络可以通过梯度下降进行训练。神经网络再次受到关注。20 世纪 90 年代,由于算力有限、数据不足,以及深层网络中常见的梯度消失等问题,神经网络在许多任务上不如 SVM、随机森林等传统机器学习方法。神经网络再次遇冷。2012 年,AlexNet 在 ImageNet 图像识别竞赛中取得了远超传统方法的表现。这被很多人视为深度学习时代的起点。此后,随着 GPU 算力提升、大规模数据集出现、算法不断改进,神经网络迅速发展,并逐渐扩展到今天的大语言模型、多模态生成和科学计算等领域。
神经网络擅长解决什么问题?
传统上,神经网络特别擅长处理非结构化感知数据,例如:
- 图像;
- 语音;
- 视频;
- 文本;
- 时间序列。
这些数据往往具有复杂的局部结构和模式。人类可以很容易看出图片里有一只猫,但很难用传统规则程序穷举所有猫的特征。神经网络的优势在于,它可以从数据中自动学习特征表示。随着 Transformer 等架构的出现,神经网络的应用边界进一步扩大。
今天,神经网络被广泛用于:
- 自然语言处理:机器翻译、文本生成、问答系统、代码生成;
- 计算机视觉:图像分类、目标检测、图像生成;
- 语音处理:语音识别、语音合成;
- 多模态生成:文本生成图像、视频生成、图文理解;
- 科学计算:蛋白质结构预测、天气预测、分子性质预测、流体力学模拟;
- 决策与控制:自动驾驶、机器人控制、游戏智能体。
从抽象角度看,只要一个问题可以表示为:给定输入,寻找合适的输出映射关系并且有足够的数据和算力,神经网络就值得尝试。
神经网络真的简单吗?
学到这里,你可能会有一种感觉:神经网络好像并不复杂。确实,从基本组成来看,它主要由这些东西构成:
- 矩阵乘法;
- 偏置加法;
- 激活函数;
- 损失函数;
- 链式法则;
- 梯度下降。
一个小学生如果会写矩阵乘法和循环,甚至也能写出前向传播的基本代码。但简单背后并不平凡。深度学习仍然有许多深刻的未解难题。
1. 规模效应
当模型参数从几千增加到几十亿、几千亿时,模型能力可能不只是线性增长,而是出现一些质变。例如,大语言模型会表现出一定的推理、规划和代码生成能力。为什么规模会带来这种能力?这个问题至今没有完整的数学解释。学界用简单的“涌现”和“Scaling Law”解释,没有很好的数学解释。不过,也许最近华人数学家邓煜的工作,也就是希尔伯特第六问题,可能与之有所关联。
2. 高维非凸优化
神经网络的损失曲面极其复杂。按照经典优化理论,基于梯度的方法很容易陷入局部最优。但在实践中,SGD 和 Adam 常常能找到足够好的解,甚至还能泛化到未见过的数据。为什么这种看似简单的优化方法在超高维空间中如此有效?这仍然是活跃的研究方向。
3. 表征学习
神经网络不是靠人类手工编写规则,而是从数据中学习表示。隐藏层里到底学到了什么?注意力权重在表达什么?模型为什么会做出某个预测?这些问题并不总是容易回答。可解释性、稳健性和安全性也因此成为深度学习的重要课题。因此,神经网络的魅力恰恰在于:它的规则极其简单,但组合之后却产生了复杂而强大的学习能力。
十三、总结
到这里,我们已经理解了神经网络如何处理连续数值、如何进行函数拟合、如何通过梯度下降学习参数。但现实世界中还有很多数据不是天然的连续数值,例如语言。文字是离散的符号。计算机不能直接把“猫”这个词当作普通数字处理,也不能简单地认为“猫”和“狗”的数值差有什么天然意义。因此,下一步我们需要学习自然语言处理中的一个基础概念:词嵌入。词嵌入的目标,是把离散的词语映射到连续向量空间中,使得语义相近的词在向量空间中也彼此接近。这将成为我们理解 Transformer、大语言模型和现代自然语言处理的重要起点。