“自然搞懂”深度学习系列(基于Pytorch架构)——03渐入佳境
Forester’s Notebook
“自然搞懂”深度学习(基于Pytorch架构)
文章目录
- Forester's Notebook
- “自然搞懂”深度学习(基于Pytorch架构)
- 第Ⅰ章 初入茅庐
- 第Ⅱ章 小试牛刀
- 第Ⅲ章 渐入佳境
- 一、初识CNN
- 1.1 输入层(Input Layer)
- 1.2 卷积层(Convolution Layer)
- 1.3 池化层(Pooling Layer)
- 1.4 激活层(Activation Layer)
- 1.5 全连接层(Fully Connected Layer,FC Layer)
- 二、深入CNN
- 2.1 Inception Block
- 2.2 简化运算
- 2.3 梯度消失
- 三、实战CNN
- 四、初识RNN
- 4.1 前向传播
- 4.2 反向传播
- 4.3 更新方式
- 4.3.1 逐个样本训练(单样本模式)
- 4.3.2 mini-batch 训练(常用方式)
- 五、深入RNN
- 5.1 多分类问题
- 5.2 双向传播
- 5.3 GRU
- 5.3.1 GRU的提出
- 5.3.2 GRU思想
- 六、实战RNN
第Ⅰ章 初入茅庐
第Ⅰ章 初入茅庐
第Ⅱ章 小试牛刀
第Ⅱ章 小试牛刀
第Ⅲ章 渐入佳境
一、初识CNN

1.1 输入层(Input Layer)
卷积神经网络(Convolutional Neural Networks,CNN) 是一种专门用来处理图片(或具有空间结构数据) 的神经网络。
一般来说,当今图片分为像素图和矢量图,前者由一个个像素方格组成,一个像素格代表[0, 255]的亮度值,于是,一张黑白图片便是一个数字矩阵。
为什么说是黑白图片呢?
因为,RGB 彩色图片有三个通道,分别存红、绿、蓝三种颜色分量。现在,你可以有一双“黄金瞳”,将一张普通彩色图片视为三个矩阵堆叠在一起。

1.2 卷积层(Convolution Layer)
首先,介绍一个关键概念——Kernal(卷积核):也就是小矩阵,用作窗口在大矩阵上滑行,进行计算操作。

接下来,我将操作流程定义为一个**卷积层操作公式**:
卷积层操作=多组运算=B∗多通道运算=B∗(C∗卷积运算)卷积层操作 = 多组运算 = B*多通道运算 = B*(C*卷积运算) 卷积层操作=多组运算=B∗多通道运算=B∗(C∗卷积运算)
其中,B为输出通道数量,C为输入通道数量。
紧接着,我依次解释上述三种运算:
卷积运算:以Kernal为窗口遍历当前输入矩阵——将Kernal内元素与当前输入矩阵窗口内元素对应相乘,再相加得到一个数字,即为输出矩阵的一个对应位置元素,遍历完成即填满输出矩阵。

多通道运算:以C个Kernal对C个通道(即C个输入矩阵)分别进行卷积运算,得到C个输出矩阵,将C个矩阵对应元素相加得到一个新矩阵,即为最终矩阵。

多组运算:以B组Kernal(每组C个Kernal)进行相应多通道运算得到B个最终矩阵,即为B个特征图(Feature maps),也称为有B个通道。
Think-Help
操作中关键的参数主要有4个:输出通道数B,输入通道数C,矩阵高H,矩阵宽W。
从公式或图片亦或定义中不难看出:B决定多通道运算次数,最终想要多少个通道(多少个矩阵),就做多少次多通道运算;C决定卷积运算次数,给定输入有多少个通道,就必须做多少次卷积运算。
而矩阵的高和宽(H和W)分为卷积前后,主要取决于Kernal的大小。
设输入矩阵为A * A,Kernal为a * a,输出矩阵为X * X。(因矩阵通常为方阵,故如此假设)关系如下:
X=A−(a//2)∗2X = A-(a//2)*2 X=A−(a//2)∗2
其中,//为整除符号。不难理解:输出矩阵X上下左右都要减去Kernal边长的一半。
1.3 池化层(Pooling Layer)
池化层的主要目的是对特征图进行下采样(subsampling),也就是“缩小尺寸、保留重要信息”。
池化操作是对一个局部区域取代表值,有两种操作方式:
| 类型 | 计算方法 | 含义 |
|---|---|---|
| 最大池化 Max Pooling | 取区域内最大值 | 提取最强特征(常用) |
| 平均池化 Average Pooling | 取区域内平均值 | 平滑特征(早期网络使用) |
其中,最大池化操作如下图:

Think-Help
值得注意:池化仅改变矩阵尺寸(即H和W),不改变通道数。
同时补充两个十分重要的参数:padding(填充)和stride(步幅),虽然在池化层补充,但这两种操作在卷积层和池化层都可以做。
padding
在卷积层我们提到:输出矩阵会根据Kernal大小减少不同程度的边长,也就是损失了边缘信息。如果我们不想损失这些信息,如何做呢?
自然地,在边缘补充一圈0,即输入矩阵上下左右都多了一行或一列。
如下图,本来5 * 5的输入矩阵边长要减去2【2*(3//2)】变为3 * 3,但是将其扩充一圈变为7 * 7,这样,输出矩阵仍为5 * 5。
stride
表示卷积核或池化窗口 每次移动的步长,stride=2的情况如下图:
注:池化的主要目的是“压缩尺寸”,通常窗口之间不重叠,所以通常让步长等于窗口大小最自然。
1.4 激活层(Activation Layer)
处理图像数据,加入非线性特征也是至关重要的。
ReLU(Rectified Linear Unit)是目前 CNN 中最常用的激活函数:
f(x)=max(0,x)f(x)=max(0,x) f(x)=max(0,x)
原因:计算简单、收敛更快、不容易梯度消失。
1.5 全连接层(Fully Connected Layer,FC Layer)
“卷积层是特征提取器,全连接层是分类器。”
保留特征图(Feature maps)形式,是没有办法判断最终类别滴,仍然需要“展平”(Flatten),将其变为一维向量,便于建立类别映射(如下),计算输出概率值。
yj=f(∑iwijxi+bj)y_j = f\left( \sum_i w_{ij} x_i + b_j \right) yj=f(i∑wijxi+bj)
二、深入CNN
2.1 Inception Block
在学习完卷积神经网络常见的层次结构后,我们同样需要将其“组装”起来,以往我们学习到的神经网络设计模式都是线性,也就是将层次顺序链接起来,但实际应用中,往往需要功能更为复杂的设计模式,如下图(出自论文 《Going Deeper with Convolutions》,即 GoogLeNet,这是 Google 在 2014 年提出的经典结构)。
其中我们勾画出一个多种层次路线混合的模块(称为Inception 模块),不同的是,这些层次路线是并行的而非串行。
Inception 模块是一种特殊的卷积块(block),它的核心思想是:
“给定多种选择让网络自己决定使用哪种卷积核尺寸来提取特征,而不是我们人为设定一个固定的卷积核。”
Think-Help
为什么这么设计?
传统 CNN(比如 LeNet、AlexNet)在每层卷积中,只使用固定大小的卷积核(例如全是 3×3)。
但问题是:
- 小卷积核(1×1、3×3)→ 适合捕捉局部特征;
- 大卷积核(5×5)→ 能看见更全局的模式;
- 池化 → 帮助降维并提取主要特征。
Google 团队就想:“既然不同大小的卷积核提取的信息不同,那我为什么不在一层里全部用上,然后再让网络自己学到该重视哪种特征?”
于是 Inception 结构就诞生了。
将层次并行,不同层次路线得出的结果最后如何整合呢?
按照通道(Channels)维度拼接结果,注意另外两个维度(H和W)自然必须一致。相当于把不同尺度的信息融合在一起,得到一个更强的综合特征表示。
2.2 简化运算
观察上面图片,你会发现有许多1 * 1的卷积层(Kernal尺寸为1 * 1),代入卷积层运算思考一下,这有必要吗?
似乎在提取特征方面没什么用,输出矩阵的尺寸没变,输出通道取决于你想要多少通道。那为什么还用呢?
简化运算!时间复杂度过高一直是CNN的痛点,如下图使用1 * 1的Kernal会极大减少运算量(参照1.2自己算一遍哦)。
本质上就是中间进行一次通道缩小。
2.3 梯度消失
设计好模式,在训练和测试时我们可能会遇到这样的情况:
可能有两个问题:过拟合【test error > train error & 56-layer > 20-layer】 和 梯度消失【56-layer > 20-layer,56-layer出现梯度消失导致模型无法得到有效训练】。
过拟合暂不多说,需要减少模型layer、增大惩罚项等操作;CNN的重点在于梯度消失,由于模型层数过多很可能导致梯度消失。
Think-Help
梯度消失和鞍点问题的区别
我们在第Ⅰ章提到过,神经网络的棘手问题是鞍点问题,为应对这个问题提出了SGD(随机梯度下降)和Mini-Batch(小批量梯度下降)。
而我们现在谈的是梯度消失,二者区别在于:
- 鞍点问题指的是在反向传播过程中可能会偶然遇到**
一个梯度**接近于0;- 而梯度消失问题指的是在反向传播过程中**
多个梯度**(小于1)相乘,导致越往前传播梯度越小,最终几乎为 0,使早期层无法有效更新。二者区别要尤其搞清,为应对梯度消失问题我们提出了几种方法(第1种前面1.4提到过,本节我们主要看第二种):
- 使用ReLU等非饱和激活函数(避免Sigmoid、tanh饱和区间造成梯度趋近0);
- 引入残差结构(Residual Connections),如ResNet,使梯度能直接跨层传播。
- 优化权重初始化方法(如Xavier或He初始化);
- 采用批量归一化(Batch Normalization) 来稳定梯度分布。
引入残差结构ResNet,如下图所示:
先看左侧传统网络(Plain net):输入x经过两层带权重的线性变换和非线性激活后,得到输出 H(x)。
再看右侧残差网络(Residual net,简称ResNet):输入x一方面进入普通的卷积层(两层 Weight Layer 加 ReLU),生成F(x);另一方面,被直接跳跃连接到输出端。
ResNet提出了一种新思想:让网络学习“残差”而不是直接学习映射。残差定义为:
F(x)=H(x)−xF(x)=H(x)−x F(x)=H(x)−x
这里的“残差”不同于损失函数中的残差,因为它是映射函数输出减去输入而非真实y值,事实上我们也没法减去真实y值,ResNet在中间层而非输出层。
映射函数便为:
H(x)=F(x)+xH(x)=F(x)+x H(x)=F(x)+x
这样做为什么可以解决梯度消失问题?
很容易理解——ResNet结构中求梯度时绝对值永远大于等于1:
∂H(x)∂x=∂F(x)∂x+1\frac{\partial H(x)}{\partial x}=\frac{\partial F(x)}{\partial x} +1 ∂x∂H(x)=∂x∂F(x)+1
这样,反向传播时再也不怕梯度消失啦!
三、实战CNN
待补充
四、初识RNN
循环神经网络(Recurrent Neural Network,RNN) 是一种专门处理序列数据的神经网络。
其最大的特点/优势就是**“记忆”**——能够结合之前的信息进行思考。
如下图所示,一组序列按照时间步依次输入RNN Cell,而每个RNN Cell都需要结合序列输入x和先前信息h进行下一步输出。
Think-Help
RNN的特殊样本
**在RNN中,一个样本(sequence)是一个时间序列。**序列按照时间步有多个元素,可能是一句话中的一个单词,可能是股价序列中的某一天:
x1,x2,…,xTx_1,x_2,…,x_T x1,x2,…,xT
每个时间步输入一个 x_t,RNN 输出一个 h_t。如:如果你在做语言模型,序列是词序列 [x1=“我”,x2=“喜欢”,x3=“学习”]。这里 x1,x2,x3 是不同时刻的输入(每个可能是词向量)。
4.1 前向传播
具体的前向传播计算图及公式如下:
Think-Help
RNN的特殊权重
RNN 模型本身就只有一组参数(权重矩阵):
Wxh:输入→隐藏Whh:隐藏→隐藏Why:隐藏→输出W_{xh}:输入 → 隐藏\\ W_{hh}:隐藏 → 隐藏\\ W_{hy}:隐藏 → 输出 Wxh:输入→隐藏Whh:隐藏→隐藏Why:隐藏→输出
具体来说,同一样本中,不同时间步共享同一组参数(每个RNN Cell内部的参数都相同);不同样本间也共享参数。简单来说,整个模型就一组参数。
关于每个Cell中的参数类型,由任务类型决定。
多对多:对于每一步都要输出的任务,Cell中三个参数都有,如时序预测(股价每天都预测下一天)。
多对一:对于只需要最后一步输出的任务,前面的Cell中只有W_xh和W_hh,如情感分类(整句话 → 一个情绪标签)。
4.2 反向传播
RNN中的反向传播被称作BPTT(Backpropagation Through Time)。
***初步理解:***相对于BP,BPTT中损失对参数的梯度求解时多考虑了“时间”因素。
∂L∂Whh=∑t=1T∂L∂ht⋅∂ht∂Whh\frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^{T} \frac{\partial L}{\partial h_t} \cdot \frac{\partial h_t}{\partial W_{hh}} ∂Whh∂L=t=1∑T∂ht∂L⋅∂Whh∂ht
这部分还是看实例更好理解(完整实例计算过程Xueyouing/Study-Naturally):
如果该序列时间步长是3呢?
那就需要再加上一个损失函数对W_hh的导数:
∂L∂Whh=∂L∂h3⋅∂h3∂h2⋅∂h2∂h1⋅∂h1∂Whh\frac{\partial L}{\partial W_{hh}} \; = \frac{\partial L}{\partial h_3} \cdot \frac{\partial h_3}{\partial h_2} \cdot \frac{\partial h_2}{\partial h_1} \cdot \frac{\partial h_1}{\partial W_{hh}} ∂Whh∂L=∂h3∂L⋅∂h2∂h3⋅∂h1∂h2⋅∂Whh∂h1
***直观上理解:***从1到N所有时间步长的RNN Cell都用到了W_hh,然后输出了相应的h_t,一步步传递,才得出最终输出值及损失值,最终要更新这个参数,不得求出每个Cell中的梯度再求和。
4.3 更新方式
4.3.1 逐个样本训练(单样本模式)
最原始的训练方式是:每次取一个完整序列——RNN 在时间上展开——做完前向、反向传播——更新参数——再取下一个样本。
流程伪代码:
for sample in dataset:h = 0for x_t in sample:h = RNNCell(x_t, h)loss = compute_loss(h, target)loss.backward()optimizer.step()
该方法效率太低,无法利用 GPU 并行。
4.3.2 mini-batch 训练(常用方式)
现代训练都是 mini-batch 模式——我们把多个样本(序列)组成一个 batch,一起前向传播、一起反向传播、再一起更新参数**【每个batch更新一次参数】**。
假设 batch_size=2,每个序列长度=3:
| 时间步 | 样本1输入 | 样本2输入 |
|---|---|---|
| t=1 | x₁₁ | x₂₁ |
| t=2 | x₁₂ | x₂₂ |
| t=3 | x₁₃ | x₂₃ |
训练时:
- 前向:同时处理两个序列,每个时间步共享参数;
- 反向:梯度沿时间轴回传,并在 batch 维度求平均;
- 更新:优化器对共享权重更新一次。
伪代码:
for batch in data_loader:optimizer.zero_grad()outputs, hidden = model(batch_inputs)loss = criterion(outputs, targets)loss.backward() # 所有样本 + 所有时间步的梯度累积optimizer.step() # 更新一次参数
五、深入RNN
5.1 多分类问题
将RNN用于多分类问题:
和之前将全连接层构建的线性回归模型转为多分类模型思路一致:在其输出后加入交叉熵损失函数(CrossEntropyLoss),转为输出各类别概率值。
5.2 双向传播
对于一个序列(以一句话为例),传统做法是根据语句正向传播信息得到结果,语义的逆向是否也存在有价值的信息呢?
RNN还可以进行双向传播(序列正反方向各走一遍):
该隐藏层输出为:
hidden=[hNf,hNb]hidden=[h_N^f,h_N^b] hidden=[hNf,hNb]
5.3 GRU
5.3.1 GRU的提出
对于RNN,BPTT时很容易导致梯度消失或爆炸,使模型难以记住长序列信息。
Think-Help
RNN的特殊问题
我们先回顾一下梯度消失和爆炸两种情况:
在所有神经网络中,反向传播的梯度更新都要用链式法则:
∂L∂W=∂L∂hn⋅∂hn∂hn−1⋅∂hn−1∂hn−2⋅…⋅∂h1∂W\frac{\partial L}{\partial W} = \frac{\partial L}{\partial h_n} \cdot \frac{\partial h_n}{\partial h_{n-1}} \cdot \frac{\partial h_{n-1}}{\partial h_{n-2}} \cdot \ldots \cdot \frac{\partial h_1}{\partial W} ∂W∂L=∂hn∂L⋅∂hn−1∂hn⋅∂hn−2∂hn−1⋅…⋅∂W∂h1
如果每一步的梯度范数 > 1 → 会越来越大(梯度爆炸);
如果每一步的梯度范数 < 1 → 会越来越小(梯度消失)。注:先前我们并未提过梯度爆炸,因梯度消失更常见且更难解决。
而RNN相对MLP(全连接层)、CNN(卷积)神经网络更易梯度消失或爆炸,为什么?
RNN 的“时间展开”导致乘法次数极多
RNN的BPTT中,序列长度基本等价于层数。同一个权重 W_hh 会被在所有时间步重复使用。
例如一个长度为 100 的序列,RNN 展开后就是 100 层的“共享权重网络”:
ht=f(Whhht−1+Wxhxt)h_t = f(W_{hh}h_{t-1} + W_{xh}x_t) ht=f(Whhht−1+Wxhxt)
反向传播时梯度链路是:
∂L∂Whh∝∏t=1T∂ht∂ht−1\frac{\partial L}{\partial W_{hh}} \propto \prod_{t=1}^{T} \frac{\partial h_t}{\partial h_{t-1}} ∂Whh∂L∝t=1∏T∂ht−1∂ht
这里的乘法次数 T(时间步长度)通常比 CNN 或 MLP 的层数大得多,比如 50、100、甚至几百。RNN 的循环结构容易形成“指数放大/衰减”
记得4.1我们曾说过:整个RNN只有/共享一组参数。这意味着每次传播都会乘上同一个权重矩阵 W_hh。
假设 W_hh 的最大特征值为 λ,那么梯度大致会按 ∣λ∣^T 变化。
- 如果 ∣λ∣<1,梯度趋于 0 → 梯度消失
- 如果 ∣λ∣>1,梯度呈指数增长 → 梯度爆炸
因为这种循环乘法在时间维度上持续发生,所以即使权重稍有不合适,也可能导致数值不稳定;而其它网络∣λ∣可能大于1可能小于1,会有所抵消。
5.3.2 GRU思想
于是人们提出了改进版 —— GRU(门控循环单元)【Gated Recurrent Unit = 有门的循环单元】
首先给定完整公式:
zt=σ(Wzxt+Uzht−1)更新门rt=σ(Wrxt+Urht−1)重置门h~t=tanh(Whxt+Uh(rt⊙ht−1))候选新状态ht=(1−zt)⊙ht−1+zt⊙h~t最终输出\begin{aligned} z_t &= \sigma(W_z x_t + U_z h_{t-1}) && \text{更新门} \\ r_t &= \sigma(W_r x_t + U_r h_{t-1}) && \text{重置门} \\ \tilde{h}_t &= \tanh(W_h x_t + U_h (r_t \odot h_{t-1})) && \text{候选新状态} \\ h_t &= (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t && \text{最终输出} \end{aligned} ztrth~tht=σ(Wzxt+Uzht−1)=σ(Wrxt+Urht−1)=tanh(Whxt+Uh(rt⊙ht−1))=(1−zt)⊙ht−1+zt⊙h~t更新门重置门候选新状态最终输出
接下来,我们据此讲解其思路:
由前两个公式不难看出,更新门和重置门的公式格式一致:新输入x和上一步隐藏层输出(后面将其叫做“先前信息”)分别乘以相应权重。
所以要从二者用途(也就是后两个公式)来区分不同:
- 候选新状态:重置门越大,先前信息占比越大。
- 最终输出:更新门越大,新状态占比越大。
于是,这两个“门”对应的作用可以这样说:
- 重置门(reset gate) — 决定“要不要遗忘过去”
- 更新门(update gate) — 决定“要不要更新记忆”
六、实战RNN
首先需要了解RNN常见参数:
| 参数名称 | 含义 | 作用 |
|---|---|---|
| input_size | 每个时间步输入向量的维度 | 例如:如果每个时间步输入一个词向量,则为词向量长度 |
| hidden_size | 隐藏层神经元数量(隐藏状态维度) | 控制网络的“记忆容量”与建模能力 |
| num_layers | RNN 堆叠的层数 | 决定网络深度(多层RNN输出上层输入) |
| output_size | 输出层神经元数量 | 取决于任务类型:分类、预测、回归等 |
| seq_len | 序列长度(时间步数量) | 决定每次输入序列的时间跨度 |
| batch_size | 每次训练输入的样本数 | 控制并行训练规模 |
| dropout | 随机丢弃比例 | 防止过拟合(在层与层之间使用) |
| bidirectional | 是否为双向RNN | True表示同时考虑正向和反向序列信息 |
Embedding(将元素转为向量)
面对不等长序列,需要填充0,如下图所示:
而这些 0 是不必要进行计算的,于是引入一种更高效的方式——PackedSequence:
-
本文由Forester原创撰写,无偿分享,若发现侵犯版权、转卖倒卖等行为,追究其责任。
-
为保证文体美观,各小节完整代码及PDF笔记已上传至GitHub:Xueyouing/Study-Naturally。
-
欢迎关注,未完待续!



