这里先尽量把最基础的东西串起来,目标不是把所有模型都讲完,而是先弄清楚:深度学习到底在学什么,模型是怎么训练起来的,以及平时写代码时到底在做什么。
深度学习在做什么
如果只看表面,深度学习好像就是“堆很多层 + 喂很多数据 + 跑很久”,但它真正做的事情本质上还是在做一个函数拟合:
$$ f_{\theta}(x) \approx y $$
其中:
- $x$ 是输入,例如图片、文本、语音、时序数据
- $y$ 是我们想预测的目标,例如类别、数值、下一个 token
- $\theta$ 是模型参数,也就是网络中的权重和偏置
和传统机器学习相比,深度学习最大的特点不只是模型更大,而是特征提取和任务求解被放在了同一个可训练系统里。
例如图像分类中,传统方法可能需要手工提边缘、纹理、角点等特征,再送给分类器;深度学习则倾向于直接从原始输入出发,让网络自己去学“什么特征有用”。
所以很多时候可以把深度学习理解成:
- 用一个参数化很强的函数去表示任务
- 用数据去约束这个函数
- 用优化算法不断调整参数,让模型输出越来越接近目标
神经网络的基本单位
感知机
神经网络里最基础的计算单元可以写成:
$$ z = w^T x + b $$
然后再接一个非线性激活函数:
$$ y = \sigma(z) $$
其中:
- $x$ 是输入向量
- $w$ 是权重
- $b$ 是偏置
- $\sigma$ 是激活函数
如果没有激活函数,那么无论堆多少层线性层,最后都等价于一层线性变换,这样表达能力会非常弱。因此非线性是神经网络能够拟合复杂关系的关键。
常见激活函数
Sigmoid
$$ \sigma(x) = \frac{1}{1 + e^{-x}} $$
输出范围是 $(0, 1)$,早期用得很多,常用于二分类输出概率。但它有一个比较明显的问题,就是输入绝对值过大时梯度会变得很小,容易出现梯度消失。
Tanh
$$ \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} $$
输出范围是 $(-1, 1)$,相比 sigmoid 以 $0$ 为中心,优化时有时会更自然一些,但同样可能出现梯度消失。
ReLU
$$ \text{ReLU}(x) = max(0, x) $$
这个函数非常简单,但在实践里特别常用。原因也很直接:
- 计算便宜
- 正区间梯度稳定
- 在深层网络中通常比 sigmoid 更容易训练
当然 ReLU 也有缺点,比如当神经元长期落在负半轴时,梯度恒为 $0$,可能出现“神经元死亡”。
Leaky ReLU
$$ \text{LeakyReLU}(x)= \begin{cases} x, & x > 0 \\ \alpha x, & x \le 0 \end{cases} $$
可以理解为给负半轴留一点斜率,缓解 ReLU 的“死掉”问题。
神经网络为什么能工作
把一个神经元看成一次“线性变换 + 非线性变换”,多层网络就是不断重复这个过程。
例如一个两层全连接网络可以写成:
$$ h = \sigma(W_1x + b_1) $$
$$ \hat y = W_2h + b_2 $$
这里 $h$ 可以理解为中间表示,也就是模型自动学出来的特征。层数更深时,本质上是不断把原始输入映射成更适合当前任务的表示。
如果说得更直白一点,前面的层一般在学“局部和基础的模式”,后面的层则更偏向“更抽象、更任务相关的语义”。
训练流程
深度学习最核心的部分其实就是训练流程。大部分代码归根结底都在围绕下面这几步转:
- 前向传播
- 计算损失
- 反向传播
- 参数更新
前向传播
前向传播就是把输入一路送进网络,得到预测值。
例如分类任务里:
$$ \hat y = f_{\theta}(x) $$
这一步本身没有“学习”,只是根据当前参数计算输出。
损失函数
模型输出之后,需要一个标准来衡量“这次预测得有多差”,这个标准就是损失函数。
训练的目标通常可以写成:
$$ \min_{\theta} \mathcal{L}(f_{\theta}(x), y) $$
也就是让预测值和真实值之间的差距尽可能小。
均方误差
回归问题中常见:
$$ \mathcal{L}{MSE} = \frac{1}{n}\sum{i=1}^{n}(\hat y_i - y_i)^2 $$
误差越大,惩罚越重。
交叉熵
分类问题中更常见。二分类时常写成:
$$ \mathcal{L}_{CE} = -[y\log p + (1-y)\log(1-p)] $$
其中 $p$ 是模型预测为正类的概率。它的直觉意义是:如果模型对错误答案很自信,那么惩罚会很重。
梯度下降
有了损失函数之后,接下来要做的就是调整参数,让损失下降。
最基础的更新公式是:
$$ \theta \leftarrow \theta - \eta \nabla_{\theta}\mathcal{L} $$
其中:
- $\eta$ 是学习率
- $\nabla_{\theta}\mathcal{L}$ 是损失对参数的梯度
梯度告诉我们“当前参数往哪个方向改,损失会下降得最快”。所以训练的过程可以理解为:不断沿着损失下降的方向走。
反向传播
反向传播并不神秘,本质上就是链式法则在计算图上的系统应用。
如果有复合函数:
$$ L = L(y), \quad y = f(z), \quad z = g(x) $$
那么:
$$ \frac{\partial L}{\partial x}= \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial z} \cdot \frac{\partial z}{\partial x} $$
神经网络是很多层复合函数叠起来的,因此完全可以从输出层开始,一层层把梯度传回去。
PyTorch 这类框架帮我们把这件事自动化了,所以平时看到的:
| |
本质上就是在自动求整张计算图上的梯度。
常见网络结构
全连接层
也叫 Linear 层或者 Dense 层,本质上就是:
$$ y = Wx + b $$
这是最基础也最通用的一层,适合处理已经被整理成向量的特征。
优点是表达直接,缺点也很明显:
- 参数量容易很大
- 不利用输入的局部结构
因此在图像、语音等高维结构化数据上,一般不会只靠全连接层。
卷积神经网络
卷积层的核心思想是用一个局部感受野在输入上滑动,提取局部模式。
它适合图像任务的原因主要有两个:
- 图像的局部区域通常有强相关性
- 同一种模式可能会在不同位置重复出现
卷积核共享参数,所以相比直接全连接,参数量会小很多,同时也更符合图像的空间结构。
如果说全连接是在“看整张图后直接做判断”,那卷积更像是“先看局部纹理,再逐步组合成更高层语义”。
池化层
池化层一般用于降采样,例如 max pooling。
它的作用通常包括:
- 降低特征图尺寸
- 减少计算量
- 提升一定程度的平移鲁棒性
不过现在很多网络也会直接用步长卷积替代池化。
循环神经网络
RNN 适合处理序列数据,它会把上一步的隐藏状态传给下一步,因此天然适合建模“前后有关联”的数据。
最基本的形式可以写成:
$$ h_t = \phi(W_xx_t + W_hh_{t-1} + b) $$
它的问题在于长序列训练时容易梯度消失或爆炸,所以后来又有了 LSTM、GRU 这种带门控的结构。
LSTM / GRU
它们的核心想法不是“变得更复杂”,而是通过门控机制让模型自己决定:
- 什么该记住
- 什么该遗忘
- 什么该输出
因此在时序任务、NLP 早期模型里用得很多。
Attention 和 Transformer
Attention 的核心思想是:当前 token 在编码时,不一定只依赖邻近位置,而是可以直接去关注序列中和自己相关的其他位置。
自注意力的公式一般写成:
$$ \text{Attention}(Q, K, V)=\text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V $$
其中:
- $Q$ 是 query
- $K$ 是 key
- $V$ 是 value
Transformer 可以理解为“以 attention 为核心搭起来的一整套结构”,它非常适合并行训练,这也是它后来在 NLP、CV、多模态里全面铺开的重要原因之一。
训练时经常遇到的问题
欠拟合与过拟合
欠拟合
模型太简单,或者训练不够,导致训练集上都学不好。
常见表现:
- 训练误差高
- 验证误差也高
过拟合
模型把训练集记得太细了,泛化能力差。
常见表现:
- 训练误差很低
- 验证误差明显更高
这两者是训练时最常见的判断依据之一。
数据集划分
通常会把数据分成:
- 训练集:用来更新参数
- 验证集:用来调超参数和观察泛化
- 测试集:最后做一次相对客观的评估
测试集最好不要参与调参,不然最后的结果会越来越像“对测试集做了适配”。
学习率
学习率太小,训练会很慢;学习率太大,又可能直接震荡甚至发散。
它是最重要的超参数之一。很多时候模型训不起来,不一定是结构有问题,而是学习率没设对。
一个很常见的经验是:
- 先找一个能稳定下降的学习率
- 再考虑调度器逐步衰减
Batch、Epoch、Iteration
- Batch:一次送进模型的样本数
- Iteration:一次参数更新
- Epoch:整个训练集被完整看过一轮
如果训练集大小为 $N$,batch size 为 $B$,那么一个 epoch 大约有 $\frac{N}{B}$ 次 iteration。
初始化
如果初始化过大,可能导致激活值爆炸;如果初始化过小,又可能导致信号在深层网络里迅速衰减。
因此才会有 Xavier 初始化、Kaiming 初始化这类更适合不同激活函数的初始化方式。
归一化
输入数据做归一化通常很重要,例如图像常见的做法是减均值、除标准差。
网络内部常见的归一化层有:
- BatchNorm
- LayerNorm
它们的目的不完全一样,但都能在不同程度上帮助训练更稳定。
正则化
为了缓解过拟合,常见手段有:
- L2 正则化
- Dropout
- 数据增强
- 提前停止(early stopping)
其中 dropout 可以理解为在训练时随机“屏蔽掉”一部分神经元,避免网络过度依赖某些局部路径。
优化器
最基本的是 SGD:
$$ \theta \leftarrow \theta - \eta \nabla_{\theta}\mathcal{L} $$
但实际训练中更常见的是带动量的 SGD、Adam、AdamW 等。
SGD
优点是简单直接,有时泛化也很好;缺点是对学习率比较敏感,调参不一定轻松。
Adam
Adam 会同时利用梯度的一阶矩和二阶矩信息,自适应地调整不同参数的学习率,因此通常更容易训起来。
很多入门任务直接上 Adam 往往就能得到一个还不错的结果。
不过“更容易优化”不一定等于“最终泛化一定更好”,具体还是得看任务。
一个最小的 PyTorch 训练例子
下面给一个非常小的二分类例子,用纯 torch 生成二维数据并训练一个简单的 MLP。这个例子主要是为了把训练流程串起来。
| |
这段代码里最关键的几步分别是:
- 定义模型
- 定义损失函数
- 定义优化器
- 前向传播得到
logits - 用
loss.backward()反向传播 - 用
optimizer.step()更新参数
如果是分类任务,需要特别注意最后一层和损失函数是否匹配。
例如:
- 多分类常见组合是
Linear + CrossEntropyLoss - 二分类常见组合是
Linear + BCEWithLogitsLoss
像 BCEWithLogitsLoss 这种接口已经把 sigmoid 合进去了,所以训练时不要手动再加一层 sigmoid 再送进去,不然数值上反而不太好。
平时写模型代码时到底在关注什么
如果只是为了把程序跑起来,深度学习代码很容易写成“面向 API 编程”,但真正训练模型的时候,通常要一直盯着下面这些东西:
loss 有没有正常下降
如果 loss 完全不动,优先检查:
- 学习率是否合适
- 标签是否错位
- 输出层和损失函数是否匹配
- 数据预处理是否有问题
训练集和验证集是否分离
如果训练效果很好,但验证效果很差,多半是过拟合,或者数据分布不一致。
梯度是否异常
有时 loss 是 nan,或者训练突然炸掉,可能是:
- 学习率过大
- 数值溢出
- 梯度爆炸
- 输入尺度不稳定
这时可以考虑梯度裁剪、归一化输入、减小学习率等方法。
数据往往比模型更重要
很多时候并不是模型结构不够“高级”,而是:
- 数据量不够
- 标签质量不高
- 特征分布有偏
- 训练目标定义不清楚
这个问题在实际项目里比“模型选哪一篇 paper 的结构”更常见。