第三周是配工具箱——把深度学习里最常用的那几个组件挨个过一遍:Adam、Embedding、CNN、RNN/LSTM、池化、归一化、Dropout,最后串成一个完整模型。
这周写代码最多,六个脚本 + 一个作业。每个组件单独跑一遍看输出形状,比看图理解快得多。
一、Adam:为什么不用 SGD 了
先说 SGD 的四个毛病:
- 学习率难调(全局固定,大则震荡小则慢)
- 各参数同等对待,忽略梯度尺度差异
- 容易在鞍点/平坦区停滞
- 无动量导致方向抖动
Adam 用三个机制解决:
| 机制 | 作用 |
|---|---|
| 一阶矩(动量) | 累积梯度方向,平滑震荡 |
| 二阶矩(平方) | 按参数梯度量级自动缩放学习率 |
| 偏差修正 | 消除训练初期动量被低估的问题 |
θ ← θ − η · m̂ / (√v̂ + ε)
第二条是关键——不同参数拿到的实际学习率是不一样的。梯度一直很大的参数,步子自动变小;梯度很小的参数,步子自动变大。SGD 做不到这件事。
二、Embedding:把词变成向量
本质就是一张查找表:
E ∈ ℝ^(V×d) V = 词表大小,d = 向量维度(通常 64~512)
输入词 id → 输出对应的 d 维稠密向量
形状变化是这样的:
[batch, seq_len] → Embedding → [batch, seq_len, embed_dim]
初始化怎么选:
- 数据量充足 → 随机初始化,让模型自学
- 数据量少 → 用预训练词向量(如 Word2Vec),加速收敛、迁移泛化
三、CNN:局部感受野 + 权重共享
两个核心思想:
- 局部感受野:每次只看一个小窗口,不看全局
- 权重共享:同一个卷积核在整个序列上滑动,参数量不随输入长度增加
文本用 Conv1d(核在序列长度这一维滑),图像用 Conv2d(核在 H×W 二维滑)。
输出长度公式:
L_out = ⌊(L_in + 2p − k) / s⌋ + 1
out_channels 是卷积核数量,每个核学一种局部模式——比如某个核专门识别「不太好」这种否定搭配。
四、RNN / LSTM:处理长距离依赖
为什么需要 RNN
CNN 的卷积核大小固定,只能看 k 个相邻 token,处理不了长距离依赖。
RNN 的思路是让隐状态当「记忆」,一路往下传:
hₜ = tanh(W_h·hₜ₋₁ + W_x·xₜ + b)
简单 RNN 的致命问题
梯度消失。 序列很长时,早期信息经过多次 tanh 传递后接近 0,模型「忘记」了很早之前的内容。
LSTM 的三个门
| 门 | 控制什么 |
|---|---|
| 遗忘门 | 丢弃多少旧信息 |
| 输入门 | 写入多少新信息 |
| 输出门 | 读出多少作为隐状态 |
关键在于 细胞状态 cₜ 是直接传播的——它的梯度通道比隐状态顺畅得多,所以能支持更长的序列。这是 LSTM 相比 RNN 的本质改进,不是「门多了几个」那么简单。
双向 LSTM 则是从左到右、从右到左各跑一遍再拼接,让每个位置同时看到前后文。
五、池化:降维 + 平移不变性
两大作用:
- 下采样降维:压缩长序列,减少计算量,保留最重要的特征
- 平移不变性:关键特征不管出现在哪个位置都能被捕捉到
两种方式:MaxPool 取窗口最大值(保留最显著特征)、AvgPool 取平均(反映整体分布)。
最实用的是全局最大池化——不限定窗口,直接对整个序列维度取最大值:
[batch, seq_len, hidden] → 全局最大池化 → [batch, hidden]
它把变长序列压成了固定维度向量,后面才能接全连接层。这一步是文本分类模型的标准操作。
六、归一化:BatchNorm vs LayerNorm
要解决的问题
内部协变量偏移——每次参数更新后,各层的输入分布都在漂移,导致训练不稳定、只能用很小的学习率。
归一化把每层输入拉回均值 0、方差 1 附近,加速收敛 + 允许更大学习率。
两者的区别
这个我一开始老记反,后来找到个简单办法:
BatchNorm 按「列」统计 同一特征跨样本 → 图像常用
LayerNorm 按「行」统计 同一样本跨特征 → 文本 / Transformer / BERT 标准选择
NLP 用 LayerNorm 的原因是:文本的 batch 内样本长度不一,按 batch 方向统计不稳定;而按样本自己的特征维度统计就没这个问题。
七、Dropout:训练时随机「装死」
训练时随机让一部分神经元输出置 0,强迫网络不能依赖某几个特定神经元,学习更分散、更鲁棒的特征。
正则化的解释是:相当于同时训练了 2ⁿ 个子网络的集成,防止神经元「共同适应」。
训练和推理的行为不一样,这点很关键:
- 训练时:按概率 p 屏蔽部分节点,并放大补偿其余节点
- 推理时(
model.eval()):全部节点激活,不做屏蔽
忘记调用
model.eval()是新手最常见的 bug 之一——推理结果会莫名其妙地不稳定,因为 Dropout 还在随机丢神经元。
常见摆放与取值:
p = 0.1~0.2 放 Embedding 层后
p = 0.3~0.5 放全连接层前(最常用最有效)
p > 0.5 一般不用,信息损失太多
八、串起来:一个完整模型
课程最后把所有组件串成一条完整链路:
Embedding → 词转向量
↓
RNN → 捕捉序列信息
↓
池化 → 压缩为定长向量
↓
LayerNorm → 归一化
↓
Dropout → 防过拟合
↓
Linear → 维度对齐到类别数
↓
Sigmoid → 输出
↓
Adam 优化器
这周的作业(week3/task/文本位置多分类任务.py)就是用这套结构做一个「判断特定字符出现在第几个位置」的多分类任务。
这周的收获
1. 每个组件都在解决一个具体的问题。
这周最大的收获是想清楚了「为什么需要它」这条线:
CNN 看不了长距离 → RNN 用隐状态传递记忆
RNN 梯度消失 → LSTM 用细胞状态开一条梯度通道
序列变长 → 池化压成定长
训练不稳定 → 归一化
过拟合 → Dropout
学习率难调 → Adam
不是「深度学习有这些组件」,而是「遇到了这些问题,所以有了这些组件」。 按后者理解,记忆负担小很多。
2. 形状变化比公式更值得盯。
我这周每写一个组件都会 print 一遍输入输出的 shape。[batch, seq_len] → [batch, seq_len, embed] → [batch, hidden] → [batch, num_classes] 这条链路走通了,模型结构就不再是抽象的方块图。
3. 训练态和推理态是两套行为。
Dropout 和 BatchNorm 在 train() 和 eval() 下行为不同。这是从「写业务代码」转到「写模型代码」时最容易忽略的一点——同一段代码在两种模式下跑出来的结果本来就该不一样。
下周进入语言模型,从 N-gram 一路讲到 BERT 和 Transformer。