All writing Notes & observations

Deep Learning And Machine Learning Classic Algo

目标:这是一份面向初学者到进阶学习者的系统化 Markdown 学习文档,覆盖机器学习和深度学习中的经典算法,并重点深入讲解 CNN、ResNet、Transformer 的理论、结构设计、训练方式和代码实现。


为什么要学习经典算法

学习机器学习和深度学习时,很多人一开始就会被各种新模型、新论文、新框架吸引。但真正能建立长期能力的,不是“会调库”,而是对经典算法的理解。

经典算法的重要性主要体现在以下几个方面:

  1. 它们构成了现代 AI 的核心思想来源。
  2. 许多新模型本质上都是在经典模型上做改进。
  3. 如果不了解基础原理,后续调参、排错、建模都会很吃力。
  4. 面试、竞赛、项目实践都离不开这些基础。

你可以把整个学习路径理解为:

1
2
3
4
5
6
7
数学基础
  -> 机器学习基本思想
  -> 经典机器学习算法
  -> 神经网络基础
  -> CNN / RNN / Attention
  -> ResNet / Transformer
  -> 更复杂的大模型和应用系统

机器学习与深度学习总览

1. 什么是机器学习

机器学习(Machine Learning, ML)本质上是:让计算机从数据中学习规律,而不是手工编写规则。

传统编程是:

1
规则 + 输入数据 -> 输出结果

机器学习则更像:

1
2
输入数据 + 正确答案 -> 学到规则(模型)
学到的规则 + 新数据 -> 新的预测结果

2. 什么是深度学习

深度学习(Deep Learning, DL)是机器学习的一个分支,它使用多层神经网络自动学习复杂特征表示。

简单理解:

  • 机器学习通常依赖人工设计特征。
  • 深度学习倾向于让模型自己从原始数据中学特征。

例如图像分类任务:

  • 传统机器学习:你可能先人工提取边缘、颜色直方图、纹理等特征,再交给分类器。
  • 深度学习:直接把原始像素输入网络,让网络自己学低层边缘、中层纹理、高层语义。

3. 两者关系

1
2
3
人工智能 AI
└── 机器学习 ML
    └── 深度学习 DL

4. 常见任务类型

监督学习

数据带标签,例如:

  • 房价预测
  • 垃圾邮件分类
  • 图像分类

无监督学习

数据没有标签,例如:

  • 聚类
  • 降维
  • 表征学习

半监督学习

少量有标签数据 + 大量无标签数据。

强化学习

智能体和环境交互,通过奖励信号学习策略。

什么是奖励信号?例如:

  • 玩游戏时得分
  • 机器人完成任务时的成功反馈

机器学习的基础概念

这一部分非常关键。很多人后面学不动,不是因为模型太难,而是因为这些底层概念没有真正弄懂。

1. 数据集、特征、标签

  • 样本(sample):一条数据。
  • 特征(feature):描述样本的信息。
  • 标签(label):样本的目标结果。

例如房价预测:

  • 特征:面积、地段、房龄、楼层。
  • 标签:房价。

2. 训练集、验证集、测试集

  • 训练集:用于训练模型参数。
  • 验证集:用于调参、选择模型。
  • 测试集:用于最终评估泛化性能。

典型划分:

1
2
训练集 : 验证集 : 测试集 = 7 : 1.5 : 1.5
或 8 : 1 : 1

3. 模型、参数、超参数

模型

模型是一个函数:

$$ \hat{y} = f(x) $$

其中:

  • $x$ 是输入
  • $\hat{y}$ 是预测输出
  • $f$ 是模型

参数

参数是在训练中学出来的,例如线性回归的权重、神经网络中的权重矩阵。

超参数

超参数是训练前人为指定的,例如:

  • 学习率
  • batch size
  • 网络层数
  • 正则化系数

4. 损失函数

损失函数衡量模型预测结果和真实结果之间的差距。

回归常见损失

均方误差(MSE):

$$ L = \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2 $$

分类常见损失

交叉熵损失(Cross Entropy):

$$ L = -\sum_i y_i \log \hat{y}_i $$

5. 优化目标

训练模型就是找到一组参数,使损失最小:

$$ \theta^* = \arg\min_{\theta} L(\theta) $$

6. 欠拟合与过拟合

欠拟合

模型太简单,无法学到数据规律。

表现:

  • 训练误差高
  • 测试误差也高

过拟合

模型把训练集记住了,但泛化能力差。

表现:

  • 训练误差很低
  • 测试误差高

7. 偏差与方差

偏差(Bias)

模型假设过强,表达能力不足。

方差(Variance)

模型对训练数据过于敏感。

理想目标是:在偏差和方差之间找到平衡。

8. 常见评估指标

回归

  • MAE(Mean Absolute Error):表示平均绝对误差。
  • MSE(Mean Squared Error):表示均方误差。
  • RMSE(Root Mean Squared Error):表示均方根误差。
  • $R^2$(决定系数):表示模型的拟合优度。

分类

  • Accuracy(准确率):表示分类正确的样本占总样本的比例。
  • Precision(精确率):表示预测为正例的样本中实际为正例的比例。
  • Recall(召回率):表示实际为正例的样本中被正确预测的比例。
  • F1-score(F1分数):表示精确率和召回率的调和平均。
  • ROC-AUC(ROC曲线下面积):表示模型在不同阈值下的分类性能。

对于类别不平衡问题,不能只看 Accuracy。

9. 数据预处理

机器学习中,数据质量往往比模型复杂度更重要。

常见处理包括:

  • 缺失值处理
  • 异常值处理
  • 标准化
  • 归一化
  • 类别编码
  • 特征选择

标准化:把数据变成均值为 0,标准差为 1 的分布

$$ x’ = \frac{x - \mu}{\sigma} $$

归一化:把数据缩放到 [0, 1] 区间

$$ x’ = \frac{x - x_{min}}{x_{max} - x_{min}} $$


经典机器学习算法

这一部分先建立“传统机器学习脑图”,后面再进入神经网络世界。

1. 线性回归

线性回归用于预测连续值。

模型形式:

$$ \hat{y} = w^Tx + b $$

核心思想:

  • 假设输出和输入特征是线性关系。
  • 用最小二乘或梯度下降拟合参数。

优点

  • 简单、可解释。
  • 是很多复杂模型的起点。

缺点

  • 只能表达线性关系。

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import numpy as np
from sklearn.linear_model import LinearRegression

X = np.array([[1], [2], [3], [4], [5]])
y = np.array([2, 4, 6, 8, 10])

model = LinearRegression()
model.fit(X, y)

print("weight:", model.coef_)
print("bias:", model.intercept_)
print("pred:", model.predict([[6]]))

model = LinearRegression() 这行代码创建了一个线性回归模型对象。model.fit(X, y) 这行代码使用训练数据 X 和标签 y 来训练模型,学习出权重和偏置。最后,model.predict([[6]]) 这行代码使用训练好的模型来预测输入 6 的输出。

2. 逻辑回归

虽然名字叫“回归”,但逻辑回归通常用于分类。

它先做线性变换:

$$ z = w^Tx + b $$

再通过 sigmoid 映射为概率:

$$ \sigma(z) = \frac{1}{1 + e^{-z}} $$

输出可以解释为样本属于正类的概率。

优点

  • 简洁、高效、可解释。
  • 对很多结构化数据任务依然有效。

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

clf = LogisticRegression(max_iter=500)
clf.fit(X_train, y_train)

print("test accuracy:", clf.score(X_test, y_test))

X, y = load_iris(return_X_y=True) 这行代码加载了 Iris 数据集,并将特征存储在 X 中,标签存储在 y 中。train_test_split 这行代码将数据集划分为训练集和测试集,其中 test_size=0.2 表示测试集占总数据的 20%。

clf = LogisticRegression(max_iter=500) 创建了一个逻辑回归模型对象,并设置最大迭代次数为 500。clf.fit(X_train, y_train) 使用训练数据来训练模型。最后,clf.score(X_test, y_test) 这行代码评估模型在测试集上的准确率。

补充:iris 数据集是一个经典的多分类数据集,包含 150 个样本,每个样本有 4 个特征(花萼长度、花萼宽度、花瓣长度、花瓣宽度),分为 3 个类别(Setosa、Versicolor、Virginica)。逻辑回归在这个数据集上表现不错,因为它可以通过线性决策边界来区分不同类别。

3. 决策树

决策树通过一系列“如果…那么…”规则进行划分。

例如:

1
2
如果年龄 < 30 且 收入 > 10k,则购买
否则不购买

核心思想

通过选择最优特征进行划分,使每个子节点尽量“纯”。

常见划分指标:

  • 信息增益
  • 信息增益率
  • 基尼指数

优点

  • 可解释性强
  • 不需要特征归一化

缺点

  • 容易过拟合
  • 单棵树泛化能力有限

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

X, y = load_wine(return_X_y=True)   # 加载葡萄酒数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

tree = DecisionTreeClassifier(max_depth=4, random_state=42)
tree.fit(X_train, y_train)

print("accuracy:", tree.score(X_test, y_test))

tree = DecisionTreeClassifier(max_depth=4, random_state=42) 这行代码创建了一个决策树分类器对象,并设置最大深度为 4,以防止过拟合。tree.fit(X_train, y_train) 使用训练数据来训练决策树模型。最后,tree.score(X_test, y_test) 这行代码评估模型在测试集上的准确率。

4. 随机森林

随机森林是多个决策树的集成。

核心思想:

  1. 对数据进行有放回抽样(Bootstrap)。
  2. 每棵树训练时只看部分特征。
  3. 最终通过投票或平均得到输出。

优点

  • 比单棵树更稳健
  • 抗过拟合能力更强
  • 在结构化数据上表现常常很好

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

X, y = load_breast_cancer(return_X_y=True)  # 加载乳腺癌数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

rf = RandomForestClassifier(n_estimators=200, max_depth=6, random_state=42)
rf.fit(X_train, y_train)

print("accuracy:", rf.score(X_test, y_test))

5. 支持向量机 SVM

SVM 的核心思想是找到一个分类超平面,使两类样本之间的间隔最大。

对于线性可分情况:

$$ w^Tx + b = 0 $$

SVM 希望最大化 margin,也就是两类样本之间的距离。

对于线性不可分情况,可以借助核函数映射到高维空间。

常见核函数:

  • 线性核
  • 多项式核
  • RBF 高斯核

优点

  • 小样本、特征维度高时效果常常不错
  • 理论体系完整

缺点

  • 大规模数据训练较慢
  • 参数和核函数选择较敏感

6. KNN(K Nearest Neighbors)

K 近邻是一种非常直观的算法。

预测时:

  1. 找到离当前样本最近的 K 个训练样本。
  2. 看这些邻居的标签。
  3. 多数投票或平均。

特点

  • 无需显式训练
  • 推理成本高
  • 对距离度量敏感

7. K-Means 聚类

K-Means 是经典无监督聚类算法。

步骤:

  1. 初始化 K 个中心点。
  2. 将每个样本分配到最近中心。
  3. 更新每个簇中心为簇内均值。
  4. 重复直到收敛。

优化目标:

$$ \sum_{i=1}^{K}\sum_{x \in C_i} ||x - \mu_i||^2 $$

缺点

  • 需要事先指定 K
  • 对初始化敏感
  • 假设簇是球形

8. PCA 主成分分析

PCA 用于降维。

核心思想:

  • 找到数据方差最大的方向作为主成分。
  • 用较少维度保留大部分信息。

PCA 的价值:

  • 降低维度
  • 去噪
  • 可视化高维数据

9. 集成学习:Boosting

Boosting 通过串行训练多个弱学习器,不断纠正前一个模型的错误。

典型算法:

  • AdaBoost
  • GBDT
  • XGBoost
  • LightGBM
  • CatBoost

如果你将来做表格数据任务,Boosting 系列模型几乎一定要学。


深度学习基础

要理解 CNN、ResNet、Transformer,必须先真正理解神经网络。

1. 感知机

感知机是最早的神经元模型之一。

形式:

$$ z = w^Tx + b $$

经过激活函数得到输出:

$$ y = \phi(z) $$

如果 $\phi$ 是阶跃函数,就是早期感知机。

2. 多层感知机 MLP

单层线性模型能力有限,因此引入隐藏层:

$$ h = \phi(W_1x + b_1) $$

$$ \hat{y} = W_2h + b_2 $$

如果堆叠更多层,就形成深层神经网络。

3. 激活函数

没有非线性激活函数,多层网络等价于单层线性模型。

Sigmoid

$$ \sigma(x) = \frac{1}{1+e^{-x}} $$

优点:

  • 输出可解释为概率

缺点:

  • 容易梯度消失

Tanh

$$ \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} $$

输出范围为 [-1, 1]。

ReLU

$$ \text{ReLU}(x)=\max(0, x) $$

优点:

  • 计算简单
  • 收敛快
  • 缓解梯度消失

缺点:

  • 可能出现“神经元死亡”

GELU

Transformer 中常见。

比 ReLU 更平滑。

4. 前向传播

前向传播就是输入数据逐层经过线性变换和激活,得到输出。

5. 反向传播

反向传播是深度学习训练的核心。

它通过链式法则计算损失对各层参数的梯度。

核心过程:

  1. 前向传播得到输出和损失。
  2. 从输出层向前一层层计算梯度。
  3. 用优化器根据梯度更新参数。

6. 梯度下降

参数更新公式:

$$ \theta \leftarrow \theta - \eta \nabla_\theta L $$

其中:

  • $\theta$ 是参数
  • $\eta$ 是学习率
  • $\nabla_\theta L$ 是梯度

三种常见形式

  • Batch Gradient Descent:每次用整个训练集计算梯度,更新一次参数。
  • Stochastic Gradient Descent:每次用一个样本计算梯度,更新一次参数。
  • Mini-batch Gradient Descent:每次用一个小批量样本计算梯度,更新一次参数。

实际深度学习中通常使用 mini-batch。

7. 常见优化器

SGD(Stochastic Gradient Descent,随机梯度下降)

最基础的优化器。

Momentum(动量)

加入动量,减少震荡。

Adam(Adaptive Moment Estimation,自适应矩估计)

结合一阶矩和二阶矩估计,训练中最常用。

8. 正则化

为了防止过拟合,常见手段包括:

  • L1 正则化:通过在损失函数中加入权重的绝对值之和,促使模型稀疏化。
  • L2 正则化:通过在损失函数中加入权重的平方和,限制权重的大小。
  • Dropout:在训练过程中随机将一部分神经元的输出设为 0,防止模型过度依赖某些特定的神经元。
  • Data Augmentation(数据增强):通过对训练数据进行变换(如旋转、翻转、裁剪等),增加数据的多样性。
  • Early Stopping(早停):在验证集上的性能不再提升时停止训练,防止模型在训练集上过拟合。

9. Batch Normalization(批归一化)

Batch Normalization 就是在神经网络中间层对特征做标准化,让数据分布更稳定,从而让模型训练更快、更稳。

BN 的作用:

  • 稳定训练
  • 允许更大学习率
  • 一定程度上起到正则化作用

10. 深度学习训练的一般流程

1
2
3
4
5
6
7
8
准备数据
  -> 构建 Dataset / DataLoader
  -> 定义模型
  -> 定义损失函数
  -> 定义优化器
  -> 训练循环
  -> 验证评估
  -> 保存模型

11. 一个最简单的神经网络例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import torch
import torch.nn as nn
import torch.optim as optim

X = torch.tensor([[0.0, 0.0],
                  [0.0, 1.0],
                  [1.0, 0.0],
                  [1.0, 1.0]])
y = torch.tensor([[0.0], [1.0], [1.0], [0.0]])

model = nn.Sequential(
    nn.Linear(2, 8),
    nn.ReLU(),
    nn.Linear(8, 1),
    nn.Sigmoid()
)

criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)

for epoch in range(5000):
    pred = model(X)
    loss = criterion(pred, y)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 500 == 0:
        print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}")

print("Predictions:")
print(model(X).detach())

这个例子实现了一个简单的 XOR 学习任务。它说明:

  • 线性模型无法解决 XOR。
  • 加入隐藏层和非线性激活后,网络能拟合非线性关系。

CNN:卷积神经网络

CNN 是图像任务的基础架构之一。即使在 Transformer 主导的今天,理解 CNN 仍然非常重要,因为它奠定了视觉建模的很多基本思想。

1. 为什么全连接网络不适合图像

假设输入一张 224 x 224 x 3 的彩色图片。

如果直接展平后送入全连接层,每个样本的参数就为:

$$ 224 \times 224 \times 3 = 150528 $$

若隐藏层有 1000 个神经元,参数量大约是:

$$ 150528 \times 1000 $$

这会导致参数量巨大,而且还丢失了图像的空间结构信息。

图像有几个天然特点:

  1. 局部区域往往比全局更重要。
  2. 同一种模式可能出现在不同位置。
  3. 邻近像素之间强相关。

CNN 正是围绕这些特点设计的。

2. CNN 的核心思想

CNN 的三大关键思想:

  1. 局部感受野(Local Receptive Field)
  2. 参数共享(Weight Sharing)
  3. 层次化特征学习(Hierarchical Feature Learning)

局部感受野

卷积核不会一次看整张图片,而是只看局部小区域,比如 3x3、5x5。

这样做的好处:

  • 符合图像局部相关性
  • 降低参数量

参数共享

同一个卷积核会在整张图上滑动。

这意味着:

  • 无论边缘出现在左上角还是右下角,都用同一组参数检测
  • 大幅减少参数量

层次化学习

浅层学到:

  • 边缘
  • 角点
  • 纹理

中层学到:

  • 形状片段
  • 局部结构

深层学到:

  • 物体部件
  • 语义概念

3. 卷积到底在做什么

给定输入特征图 $X$ 和卷积核 $K$,卷积操作可以写作:

$$ Y(i,j) = \sum_m \sum_n X(i+m, j+n)K(m,n) $$

在深度学习框架中,通常实现的是“互相关”而不是严格数学意义上的翻转卷积,但工程上大家都习惯称为卷积。

4. 卷积层的关键超参数

卷积核大小 Kernel Size

常见:

  • 3x3
  • 5x5
  • 7x7

现代网络中 3x3 最常见。

为什么卷积核的大小通常是奇数?因为这样可以有一个中心点,方便对称地处理边界。

步幅 Stride

卷积核每次移动多少格。

  • stride = 1:细致扫描,可以保持空间尺寸。
  • stride = 2:下采样,空间尺寸减半。

Padding

在边缘补 0,避免特征图过快缩小。

若输入高宽为 $H, W$,卷积核大小为 $K$,padding 为 $P$,stride 为 $S$,输出尺寸为:

$$ H_{out} = \left\lfloor \frac{H - K + 2P}{S} \right\rfloor + 1 $$ $$ W_{out} = \left\lfloor \frac{W - K + 2P}{S} \right\rfloor + 1 $$

后面的 +1 是因为卷积核的第一个位置也算一次。

输入通道与输出通道

彩色图像输入通常是 3 通道。

假设:

  • 输入通道数为 C_in
  • 输出通道数为 C_out
  • 卷积核大小为 K x K

参数量为:

$$ C_{out} \times C_{in} \times K \times K + C_{out} $$

5. 一个直观例子

输入:32 x 32 x 3

卷积层:16 个 3x3 卷积核,每个卷积核都会覆盖 3 个通道,stride=1,padding=1

输出:

1
32 x 32 x 16

因为 padding 保持了空间尺寸不变,而输出通道数由卷积核数量决定。

6. 池化层 Pooling

池化用于降低空间分辨率。

常见类型:

  • Max Pooling(最大池化)
  • Average Pooling(平均池化)

Max Pooling

从局部窗口中取最大值。

作用:

  • 降低计算量
  • 增强局部平移不变性
  • 保留最强响应

例如 2x2 最大池化,stride=2,会把宽高减半。

Average Pooling

从局部窗口中取平均值。

作用:

  • 降低计算量
  • 平滑特征图
  • 可能丢失一些细节信息

7. CNN 的典型结构

一个基础 CNN 往往是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
输入
 -> 卷积
 -> 激活函数
 -> 卷积
 -> 激活函数
 -> 池化
 -> 多次重复
 -> 展平
 -> 全连接层
 -> 分类输出

8. 为什么 CNN 适合图像

CNN 对图像的优势来自归纳偏置(inductive bias):

  • 假设图像局部结构重要
  • 假设相同特征可在不同位置复用

这种先验在图像任务中非常合理,因此 CNN 在数据量不算极大的场景下经常训练更稳定。

9. 经典 CNN 家族简史

LeNet

最早成功的卷积网络之一,用于手写数字识别。

LeNet 的结构相对简单,主要由两层卷积和两层全连接组成。虽然在当时取得了不错的效果,但由于计算资源限制和数据集规模较小,LeNet 的影响力有限。

LeNet 的代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
import torch.nn as nn
import torch.nn.functional as F

class LeNet(nn.Module):
    # 定义网络结构,假设输入是 32x32x1 的灰度图像
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5) # 输入通道数为 1(灰度图),输出通道数为 6,卷积核大小为 5x5,得到 28x28x6 的特征图
        self.pool1 = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5) # 输入通道数为 6,输出通道数为 16,卷积核大小为 5x5,得到 14x14x16 的特征图
        self.pool2 = nn.MaxPool2d(2, 2)
        self.conv3 = nn.Conv2d(16, 120, kernel_size=5) # 输入通道数为 16,输出通道数为 120,卷积核大小为 5x5,得到 1x1x120 的特征图
        self.fc1 = nn.Linear(120, 84) # 输入维度为 120,输出维度为 84
        self.fc2 = nn.Linear(84, 10) # 输入维度为 84,输出维度为 10

    def forward(self, x):
        x = self.pool1(F.relu(self.conv1(x)))
        x = self.pool2(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.conv3(x))
        x = F.relu(self.fc1(x))
        return self.fc2(x)

AlexNet

2012 年 ImageNet 大赛中爆火,推动深度学习重新进入主流。

关键贡献:

  • ReLU
  • Dropout
  • GPU 训练

AlexNet 的结构相较于 LeNet 更加深层和复杂,包含了 5 层卷积和 3 层全连接层。AlexNet 的成功标志着深度学习在计算机视觉领域的崛起,极大地推动了后续研究的发展。

AlexNet 的代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch
import torch.nn as nn

class AlexNet(nn.Module):
    # 定义网络结构,假设输入是 3x227x227 的彩色图像
    def __init__(self, num_classes=1000):
        super().__init__()
        # 卷积层部分,用于提取特征
        self.features = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=11, stride=4), # 输入通道数为 3(RGB 图像),输出通道数为 96,卷积核大小为 11x11,步幅为 4,得到 55x55x96 的特征图
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2), # 池化层,卷积核大小为 3x3,步幅为 2,得到 27x27x96 的特征图
            nn.Conv2d(96, 256, kernel_size=5, padding=2), # 输入通道数为 96,输出通道数为 256,卷积核大小为 5x5,填充为 2,得到 27x27x256 的特征图
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2), # 池化层,卷积核大小为 3x3,步幅为 2,得到 13x13x256 的特征图
            nn.Conv2d(256, 384, kernel_size=3, padding=1), # 输入通道数为 256,输出通道数为 384,卷积核大小为 3x3,填充为 1,得到 13x13x384 的特征图
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 384, kernel_size=3, padding=1), # 输入通道数为 384,输出通道数为 384,卷积核大小为 3x3,填充为 1,得到 13x13x384 的特征图
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1), # 输入通道数为 384,输出通道数为 256,卷积核大小为 3x3,填充为 1,得到 13x13x256 的特征图
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2) # 池化层,卷积核大小为 3x3,步幅为 2,得到 6x6x256 的特征图
        )
        # 全连接层部分,用于分类
        self.classifier = nn.Sequential(
            nn.Dropout(),   # 随机丢弃部分神经元,防止过拟合
            nn.Linear(256 * 6 * 6, 4096), # 输入维度根据前面卷积层的输出特征图大小计算得出,这里假设输入图像大小为227x227
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes) # 输出维度等于类别数
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), 256 * 6 * 6)
        x = self.classifier(x)
        return x

VGG(Visual Geometry Group,视觉几何组)

强调使用大量 3x3 卷积堆叠。

优点:

  • 结构规整
  • 思路清晰

缺点:

  • 参数量大

VGG 的结构非常简单,主要由多个 3x3 卷积层和池化层交替组成,最后是几个全连接层。VGG 的成功在于证明了深层网络的有效性,并且通过使用小卷积核来减少参数量。

VGG 的代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import torch
import torch.nn as nn


class VGG16(nn.Module):
    def __init__(self, num_classes=1000):
        super(VGG16, self).__init__()

        # 卷积特征提取部分
        self.features = nn.Sequential(
            # Block 1: 224×224×3 -> 112×112×64
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            # Block 2: 112×112×64 -> 56×56×128
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            # Block 3: 56×56×128 -> 28×28×256
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            # Block 4: 28×28×256 -> 14×14×512
            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            # Block 5: 14×14×512 -> 7×7×512
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )

        # 自适应池化,保证进入全连接层前是 7×7×512
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))

        # 分类器部分
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),

            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),

            nn.Linear(4096, num_classes)
        )

    def forward(self, x):
        # 卷积层提取图像特征
        x = self.features(x)

        # 调整特征图尺寸为 7×7
        x = self.avgpool(x)

        # 展平成一维向量,送入全连接层
        x = torch.flatten(x, start_dim=1)

        # 分类输出
        x = self.classifier(x)
        return x

GoogLeNet / Inception

通过多尺度卷积并行提取特征,提高效率。

10. CNN 中的常见设计技巧

小卷积核堆叠替代大卷积核

两个 3x3 卷积堆叠的感受野接近一个 5x5,但参数更少,还多了非线性。

逐步增加通道数

随着特征图变小,通道数通常会增加,例如:

1
64 -> 128 -> 256 -> 512

使用 BN

卷积后常接:

1
Conv -> BatchNorm -> ReLU

11. CNN 的前向过程示意

假设输入一张猫的图片:

  1. 第一层卷积检测边缘。
  2. 第二层卷积组合边缘形成纹理和简单图形。
  3. 更深层把耳朵、眼睛、脸部局部组合起来。
  4. 最终分类层判断“这是猫”。

12. CNN 的优缺点

优点

  • 对图像结构建模有效
  • 参数共享降低复杂度
  • 在视觉任务中长期有效

缺点

  • 长距离依赖建模不如注意力机制直接
  • 对全局关系的表达相对间接
  • 深层网络训练可能困难

13. 用 PyTorch 从零写一个简单 CNN

下面给出一个适合初学者理解的 CNN 分类模型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import torch
import torch.nn as nn


class SimpleCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),

            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),

            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d((1, 1))
        )

        self.classifier = nn.Linear(128, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x


if __name__ == "__main__":
    model = SimpleCNN(num_classes=10)
    x = torch.randn(8, 1, 28, 28)
    y = model(x)
    print(y.shape)  # [8, 10]

14. 训练 CNN 的示例代码

下面给出一个基于 MNIST 的完整训练例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms


class SimpleCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2),

            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d((1, 1))
        )
        self.fc = nn.Linear(128, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, 1)
        return self.fc(x)


device = "cuda" if torch.cuda.is_available() else "cpu"

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

train_dataset = datasets.MNIST(root="./data", train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root="./data", train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False)

model = SimpleCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)

for epoch in range(5):
    model.train()
    total_loss = 0.0
    correct = 0
    total = 0

    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)

        logits = model(images)
        loss = criterion(logits, labels)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item() * images.size(0)
        pred = logits.argmax(dim=1)
        correct += (pred == labels).sum().item()
        total += labels.size(0)

    train_loss = total_loss / total
    train_acc = correct / total

    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            logits = model(images)
            pred = logits.argmax(dim=1)
            correct += (pred == labels).sum().item()
            total += labels.size(0)

    test_acc = correct / total
    print(f"epoch={epoch+1}, train_loss={train_loss:.4f}, train_acc={train_acc:.4f}, test_acc={test_acc:.4f}")

15. CNN 中常见问题

问题 1:卷积核越大越好吗

不是。卷积核越大,参数越多,计算成本越高。现代网络往往偏向使用小卷积核反复堆叠。

问题 2:池化一定需要吗

不一定。很多现代网络会用 stride convolution 替代部分池化。

问题 3:为什么浅层卷积学边缘

因为边缘本身就是局部模式,而卷积正好擅长检测局部模式。

16. CNN 的学习重点

如果你想真正掌握 CNN,建议一定要搞懂以下几点:

  1. 卷积输出尺寸如何计算。
  2. 卷积层参数量如何计算。
  3. 为什么局部感受野和参数共享适合图像。
  4. 池化层和 stride 的区别。
  5. 为什么深层 CNN 能形成层次化特征。

17. CNN 中的参数量怎么计算

这一点很重要,因为很多初学者能把模型跑起来,但不知道哪里最耗参数、哪里最耗计算。

卷积层参数量

卷积层参数量的计算公式为:

$$ \text{参数量} = C_{out} \times C_{in} \times K \times K + C_{out} $$

假设卷积层配置如下:

  • 输入通道数:C_in = 3
  • 输出通道数:C_out = 64
  • 卷积核大小:3 x 3

那么参数量为:

$$ 64 \times 3 \times 3 \times 3 + 64 = 1792 $$

解释:第一个乘积表示每个输出通道与输入通道的卷积操作参数,第二个加法表示每个输出通道的偏置项。

如果不带 bias,则是:

$$ 64 \times 3 \times 3 \times 3 = 1728 $$

全连接层参数量

全连接层参数量的计算公式为: $$ \text{参数量} = \text{输入维度} \times \text{输出维度} + \text{输出维度} $$

如果全连接层输入维度 1024,输出维度 100,则参数量:

$$ 1024 \times 100 + 100 = 102500 $$

你会发现,全连接层的参数量通常非常大,这也是很多现代 CNN 倾向于减少大型全连接层、改用全局平均池化的原因。

18. CNN 特征图尺寸变化的完整例子

下面我们手动推一次典型图像分类网络的尺寸变化。

输入图像:

1
224 x 224 x 3

经过以下层:

  1. Conv(3 -> 64, kernel=7, stride=2, padding=3)
  2. MaxPool(kernel=3, stride=2, padding=1)
  3. Conv(64 -> 128, kernel=3, stride=1, padding=1)
  4. MaxPool(kernel=2, stride=2)

第一步:7x7 卷积

输出高宽:

$$ \left\lfloor \frac{224 - 7 + 2 \times 3}{2} \right\rfloor + 1 = 112 $$

输出:

1
112 x 112 x 64

第二步:3x3 最大池化

输出高宽:

$$ \left\lfloor \frac{112 - 3 + 2 \times 1}{2} \right\rfloor + 1 = 56 $$

输出:

1
56 x 56 x 64

第三步:3x3 卷积,padding=1,stride=1

输出高宽:

$$ \left\lfloor \frac{56 - 3 + 2 \times 1}{1} \right\rfloor + 1 = 56 $$

空间尺寸不变:

1
56 x 56 x 128

第四步:2x2 池化,stride=2

输出高宽:

$$ \left\lfloor \frac{56 - 2}{2} \right\rfloor + 1 = 28 $$

输出:

1
28 x 28 x 128

这类推导你一定要能手算,因为后面无论是自己搭网络还是读论文,都会频繁遇到。

19. 感受野 Receptive Field

感受野是指某一层某个神经元在原始输入上“能看到”的区域大小。

直觉上:

  • 浅层感受野小,只看局部
  • 深层感受野大,能看更大范围

为什么这很重要?

因为物体识别往往需要更大范围的上下文信息。

卷积网络通过不断堆叠卷积和池化,让感受野逐步增大。

例如连续两个 3x3 卷积,第二层神经元在原图上的感受野其实已经大于 3x3。

这也是“小卷积核堆叠”有效的重要原因之一。

20. 1x1 卷积有什么用

很多初学者看到 1x1 conv 会困惑:这不是只看一个点吗,为什么有用?

1x1 卷积虽然不扩大空间感受野,但它可以:

  1. 在通道维度做线性组合
  2. 改变通道数
  3. 降低参数和计算
  4. 提升网络非线性表达能力

例如输入是 H x W x 256,你可以用 1x1 卷积变成 H x W x 64,起到压缩通道的作用。

21. CNN 中常见增强与正则化

训练视觉模型时,数据增强非常关键。

常见增强:

  • Random Crop(随机裁剪):随机裁剪图像的一部分,增加模型对位置变化的鲁棒性。
  • Horizontal Flip(水平翻转):随机水平翻转图像,增强模型对左右对称的理解。
  • Color Jitter(颜色扰动):随机调整图像的亮度、对比度、饱和度等,增强模型对颜色变化的适应能力。
  • Random Rotation(随机旋转):随机旋转图像,增加模型对角度变化的鲁棒性。
  • Cutout(随机遮挡):随机遮挡图像的一部分,增强模型对局部缺失的鲁棒性。
  • Mixup(混合样本):将两个样本线性组合,生成新的训练样本。
  • CutMix(混合样本并遮挡):结合 Mixup 和 Cutout 的思想,生成混合样本并进行遮挡。

这些增强的目标是提高泛化能力,减少模型对训练样本细节的过拟合。

22. CNN 训练中常见故障排查

现象 1:训练集准确率上不去

可能原因:

  • 学习率过大或过小
  • 模型容量不足
  • 数据预处理错误
  • 标签错位

现象 2:训练集很高,验证集很低

通常是过拟合。

可以尝试:

  • 加强数据增强
  • 使用 Dropout / Weight Decay
  • 降低模型复杂度
  • 增加训练数据

现象 3:loss 变成 NaN

常见原因:

  • 学习率过大
  • 数值不稳定
  • 输入中有异常值
  • softmax / log 实现不当

23. 一个更贴近实战的 CNN 模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import torch
import torch.nn as nn


class ConvBNReLU(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
        super().__init__()
        self.block = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias=False),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        return self.block(x)


class BetterCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stage1 = nn.Sequential(
            ConvBNReLU(3, 64),
            ConvBNReLU(64, 64),
            nn.MaxPool2d(2)
        )
        self.stage2 = nn.Sequential(
            ConvBNReLU(64, 128),
            ConvBNReLU(128, 128),
            nn.MaxPool2d(2)
        )
        self.stage3 = nn.Sequential(
            ConvBNReLU(128, 256),
            ConvBNReLU(256, 256),
            nn.AdaptiveAvgPool2d((1, 1))
        )
        self.classifier = nn.Linear(256, num_classes)

    def forward(self, x):
        x = self.stage1(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = torch.flatten(x, 1)
        return self.classifier(x)

这类模板比最初学时的“卷积 + 池化 + 全连接”更接近常见工程写法。


ResNet:残差网络

ResNet 是深度学习历史上最重要的架构之一。它不仅是一个“更深的 CNN”,更重要的是它提供了一种解决深层网络训练困难的关键思想:残差学习(Residual Learning)。

1. 为什么需要 ResNet

理论上,网络越深,表达能力越强。

但实际中,简单地把网络层数加深,常常会出现两个问题:

  1. 梯度消失 / 梯度爆炸
  2. 退化问题(Degradation Problem)

退化问题是什么

退化问题不是过拟合,而是:

  • 更深的网络训练误差反而更高,不只是验证集,而是训练集的误差也更高了。
  • 说明优化变得困难了

**也就是说,退化问题主要是“深层网络不好优化”,不是泛化能力差。**直觉上,如果多加几层没用,模型至少应该学成恒等映射,不比浅层差。但普通深层网络很难自动学到这种“啥都不做”的行为。

2. 残差学习的核心思想

假设我们本来想学习一个映射:

$$ H(x) $$

ResNet 不直接学习 $H(x)$,而是学习残差:

$$ F(x) = H(x) - x $$

于是原映射变为:

$$ H(x) = F(x) + x $$

这就是跳跃连接(skip connection)或捷径连接(shortcut connection)的来源。

3. 为什么残差更容易学

如果最优映射接近恒等映射,那么:

$$ H(x) \approx x $$

此时只需要让:

$$ F(x) \approx 0 $$

就可以了。

学习“接近 0 的残差”通常比直接拟合复杂映射更容易。

4. 残差块的基本结构

一个基础残差块通常是:

公式写作:

$$ y = F(x, W) + x $$

再经过激活函数:

$$ out = \text{ReLU}(y) $$

5. 如果维度不一致怎么办

如果输入和输出维度不同,比如:

  • 通道数变化
  • 特征图尺寸变化

就不能直接相加。

这时通常使用 1x1 卷积对 shortcut 分支进行投影:

$$ y = F(x, W) + W_s x $$

其中 $W_s$ 是 shortcut 分支的投影矩阵。

6. ResNet 的意义不只是“更深”

ResNet 的价值有三层:

  1. 让超深网络可训练。
  2. 残差连接成为后续许多网络的标准组件。
  3. 它把“信息直接流动”的思想引入主流架构。

今天无论是视觉模型、语音模型还是 Transformer,残差连接都极其常见。

7. Basic Block 与 Bottleneck

Basic Block

常用于 ResNet-18 / ResNet-34。

结构大致是:

1
3x3 Conv -> 3x3 Conv

作用:

  • 两个 3x3 卷积提取特征
  • 适合较浅的网络

优点:

  • 结构简单
  • 计算相对较少

Bottleneck Block

常用于更深的网络,如 ResNet-50 / 101 / 152。

结构为:

1
1x1 Conv -> 3x3 Conv -> 1x1 Conv

作用:

  • 第一个 1x1 降维
  • 中间 3x3 做空间卷积
  • 最后 1x1 升维

优点:

  • 计算更高效
  • 更适合构建很深的网络

8. ResNet 的经典层数

  • ResNet-18

  • ResNet-34

  • ResNet-50

  • ResNet-101

  • ResNet-152

一般来说:

  • 18/34 使用 Basic Block
  • 50/101/152 使用 Bottleneck

二者的区别在于是否使用了 1x1 卷积来调整维度。

9. 残差连接如何帮助梯度传播

如果:

$$ y = F(x) + x $$

那么反向传播时,对输入的梯度中会包含一项来自恒等映射:

$$ \frac{\partial y}{\partial x} = \frac{\partial F(x)}{\partial x} + 1 $$

这个额外的 +1 很关键,因为它给梯度提供了一条更直接的传播路径,缓解深层网络中梯度衰减问题。

10. ResNet 的结构示意

一个 ResNet 大致会是:

11. 为什么是全局平均池化

相较于传统大规模全连接层,全局平均池化(Global Average Pooling)有几个好处:

  • 参数更少
  • 不容易过拟合
  • 更自然地把每个通道解释为一种语义响应

12. ResNet 的代码实现:BasicBlock

下面给出一个适合学习的残差块实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch
import torch.nn as nn


class BasicBlock(nn.Module):
    # 残差块的扩展因子,对于 BasicBlock 来说是 1,因为输出通道数不变
    expansion = 1

    # 初始化残差块
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)

        # 如果输入输出维度不匹配,使用 1x1 卷积进行投影
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),     # 投影 shortcut
                nn.BatchNorm2d(out_channels)    # 规范化 shortcut
            )
        else:
            self.shortcut = nn.Identity()       # 恒等 shortcut

    def forward(self, x):
        identity = self.shortcut(x)

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        out = out + identity
        out = self.relu(out)
        return out

13. 用 BasicBlock 搭一个简化版 ResNet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import torch
import torch.nn as nn


class BasicBlock(nn.Module):
    # 残差块的扩展因子,对于 BasicBlock 来说是 1,因为输出通道数不变
    expansion = 1

    # 初始化残差块
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)

        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )
        else:
            self.shortcut = nn.Identity()

    def forward(self, x):
        identity = self.shortcut(x)
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out = self.relu(out + identity)
        return out


class SmallResNet(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        # 初始卷积层,输入通道数 3,输出通道数 64
        self.stem = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True)
        )

        # 每个 layer 包含 2 个 BasicBlock,通道数逐步增加,空间尺寸逐步减小
        self.layer1 = self._make_layer(64, 64, blocks=2, stride=1)
        # 注意 layer2 开始 stride=2,进行下采样
        self.layer2 = self._make_layer(64, 128, blocks=2, stride=2)
        # 后续 layer3 和 layer4 继续下采样并增加通道数
        self.layer3 = self._make_layer(128, 256, blocks=2, stride=2)
        self.layer4 = self._make_layer(256, 512, blocks=2, stride=2)

        self.pool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512, num_classes)

    def _make_layer(self, in_channels, out_channels, blocks, stride):
        layers = [BasicBlock(in_channels, out_channels, stride=stride)]
        for _ in range(1, blocks):
            layers.append(BasicBlock(out_channels, out_channels, stride=1))
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.stem(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.pool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)
        return x


if __name__ == "__main__":
    model = SmallResNet(num_classes=10)
    x = torch.randn(4, 3, 32, 32)
    y = model(x)
    print(y.shape)  # [4, 10]

14. ResNet 为什么这么成功

ResNet 成功的原因并不只是“层数多”,而是它解决了一个非常根本的问题:深层网络怎么稳定训练。

它的设计带来的结果包括:

  • 深层网络性能提升成为可能
  • 后续 DenseNet、U-Net、Transformer 等都受到启发
  • 残差思想成为现代网络默认组件之一

15. ResNet 和普通 CNN 的区别

普通 CNN:

1
x -> f(x)

ResNet:

1
x -> f(x) + x

表面上只是多了一条跳连,实际上对优化动力学影响非常大。

16. ResNet 的变体

Pre-Activation ResNet(预激活 ResNet)

把 BN 和 ReLU 放到卷积前面。

这样有利于梯度传播,训练更深网络时效果更好。

ResNeXt(ResNet with Grouped Convolutions,分组卷积 ResNet)

引入分组卷积和 cardinality 概念。

Wide ResNet(宽 ResNet)

不是一味加深,而是适当加宽。

17. 什么时候选 ResNet

适合:

  • 图像分类
  • 图像检测/分割 backbone
  • 需要稳健 CNN 基线时

如果你想打好视觉基础,ResNet 必须会。

18. 学 ResNet 时真正要懂的点

问题 简要解释
1. 退化问题和过拟合不是一回事 过拟合是训练集效果好、测试集效果差;退化问题是网络变深后,连训练集效果都变差。也就是说,退化问题主要是“深层网络不好优化”,不是泛化能力差。
2. 为什么残差连接让优化更容易 普通网络要直接学习 (H(x)),ResNet 让网络学习残差 (F(x)=H(x)-x),最后输出 (F(x)+x)。如果某些层不需要改变输入,只要让 (F(x)=0),输出就等于 (x),所以更容易学到恒等映射。
3. 什么时候需要 projection shortcut 当主分支输出和捷径分支输入的尺寸不一致时,需要 projection shortcut。比如通道数从 64 变成 128,或者空间尺寸从 (32 * 32) 变成 (16 * 16),此时不能直接相加,需要用 (1 * 1) 卷积调整维度。
4. BasicBlock 和 Bottleneck 的区别 BasicBlock 通常由两个 (3 * 3) 卷积组成,结构简单,常用于 ResNet-18、ResNet-34;Bottleneck 通常是 (1 * 1 + 3 * 3 + 1 * 1),先降维、再卷积、再升维,常用于 ResNet-50、ResNet-101、ResNet-152。
5. 为什么 ResNet 对后续架构影响巨大 ResNet 证明了“通过跳跃连接可以训练很深的网络”。后来的很多网络,比如 DenseNet、U-Net、Transformer、扩散模型里的 U-Net,也都大量使用类似的跳跃连接思想。

19. 为什么 shortcut 分支通常尽量保持简单

ResNet 的关键思想之一,就是让 shortcut 尽量接近“无障碍的信息通道”。

如果 shortcut 分支过于复杂:

  • 直接信息流被削弱
  • 优化优势会下降

所以当输入输出维度一致时,最好的 shortcut 往往就是:

1
identity

即什么都不做,直接传过去。

20. Bottleneck Block 代码示例

更深的 ResNet 常用 Bottleneck 结构,下面给出一个学习版实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import torch
import torch.nn as nn


class Bottleneck(nn.Module):
    expansion = 4

    def __init__(self, in_channels, mid_channels, stride=1):
        super().__init__()
        out_channels = mid_channels * self.expansion

        self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(mid_channels)

        self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(mid_channels)

        self.conv3 = nn.Conv2d(mid_channels, out_channels, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channels)

        self.relu = nn.ReLU(inplace=True)

        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )
        else:
            self.shortcut = nn.Identity()

    def forward(self, x):
        identity = self.shortcut(x)

        out = self.relu(self.bn1(self.conv1(x)))
        out = self.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))

        out = self.relu(out + identity)
        return out

21. Post-Activation 与 Pre-Activation

经典 ResNet 通常是:

1
Conv -> BN -> ReLU

而 Pre-Activation ResNet 会改成:

1
BN -> ReLU -> Conv

Pre-Activation 的优势在于:

  • 梯度传播更顺畅
  • 深层训练更稳定

这说明 ResNet 不只是“加 shortcut”,它的每个细节都和优化密切相关。

22. ResNet 可以怎样理解成“微分方程视角”

这是一个更进阶但非常有价值的理解方式。

如果每个残差块写成:

$$ x_{l+1} = x_l + F(x_l) $$

它很像离散时间系统中的一步更新。

从这个视角看,深层网络是在不断对表示做“小步修正”,而不是每一层都完全重新生成表示。

这个理解会帮助你未来学习:

  • Neural ODE
  • Continuous-depth models
  • 更一般的残差结构

23. ResNet 的训练实战建议

如果你要从零训练 ResNet,建议注意:

  1. 图像任务中数据增强通常非常重要。
  2. 使用合适的学习率调度器会明显影响最终效果。
  3. 残差块内部通常不建议随意改动层顺序。
  4. 小数据集上更深网络不一定更好。

24. ResNet 常见误区

误区 1:ResNet 解决了所有梯度问题

不是。它显著缓解了深层训练困难,但并不是万能的。

误区 2:有残差就一定更强

不一定。效果还和数据、任务、宽度、正则化、训练策略有关。

误区 3:ResNet 只是一个图像分类模型

不是。ResNet 更重要的价值是它提供了“残差连接”这一普适思想。


Transformer:注意力与现代深度学习核心架构

如果说 CNN 是视觉时代最重要的结构之一,那么 Transformer 则是现代 AI 大模型时代的核心架构。

它最初用于自然语言处理,但后来扩展到了:

  • 大语言模型
  • 视觉 Transformer
  • 多模态模型
  • 语音模型
  • 蛋白质建模

理解 Transformer,是理解当代 AI 的关键。

1. Transformer 诞生的背景

在 Transformer 之前,序列建模主流是:

  • RNN(循环神经网络)
  • LSTM(长短期记忆网络)
  • GRU(门控循环单元)

这些模型的问题包括:

  1. 难以并行计算
  2. 长距离依赖建模困难
  3. 序列很长时训练效率低

Transformer 的论文标题非常有名:

Attention Is All You Need

它的核心观点是:序列建模可以完全基于注意力机制,不依赖循环结构。

2. 为什么长距离依赖难

在 RNN 中,一个词要影响很远处的另一个词,信息必须经过很多时间步传播,路径很长,容易衰减。

而在自注意力中,任意两个位置可以直接建立联系,路径长度更短。

3. 注意力机制的直觉

注意力可以理解为:

当模型处理某个位置时,它会“有选择地关注”输入中的其他位置。

例如句子:

1
The animal didn't cross the street because it was too tired.

这里 it 指代谁?

模型需要关注句子中前面的词,理解 it 更可能指 animal 而不是 street。

4. Query、Key、Value 是什么

Transformer 中最关键的一步,就是把输入的每个 Token 映射为三组向量:

  • Query(查询)
  • Key(键)
  • Value(值)

可以先用直觉理解:

  • Query:我现在想找什么信息
  • Key:我这里有什么信息标签
  • Value:真正要取出的内容

当某个 Token 的 Query 和其他位置的 Key 更匹配时,就会更多地吸收对应 Token 的 Value。

5. Scaled Dot-Product Attention

注意力公式:

$$ \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V $$

拆开理解:

  1. $QK^T$:计算相似度分数,Q是查询向量,K是键向量,点积越大表示越相关
  2. 除以 $\sqrt{d_k}$:防止维度过大导致数值不稳定,$\sqrt{d_k}$ 是 Key 的维度
  3. softmax:归一化为权重
  4. 乘以 V:按权重加权求和

6. 为什么要除以 $\sqrt{d_k}$

如果向量维度很大,点积数值会变大,softmax 可能进入饱和区,梯度变小。

除以 $\sqrt{d_k}$ 可以让数值范围更稳定。

7. 自注意力 Self-Attention

当 Q、K、V 都来自同一个序列时,就叫自注意力。

这意味着:

  • 一个词在编码自己时,可以参考整个句子中的所有词
  • 一个图像 patch 在编码自己时,可以参考所有其他 patch

8. 多头注意力 Multi-Head Attention

单个注意力头只能从一种角度看关系。

多头注意力做法是:

  1. 把特征分成多个子空间
  2. 每个头独立做注意力
  3. 把所有头结果拼接起来

公式:

$$ \text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) $$

$$ \text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_1, …, \text{head}_h)W^O $$

优点:

  • 不同头可以学不同关系
  • 表达能力更强

9. 位置编码 Positional Encoding

Transformer 本身没有循环,也没有卷积,因此默认不感知顺序。

所以必须注入位置信息。

经典 Transformer 使用正弦余弦位置编码:

$$ PE(pos, 2i) = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) $$

$$ PE(pos, 2i+1) = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) $$

直觉上,这是一种不同频率的周期函数编码,使模型能感知相对和绝对位置信息。

10. Transformer 编码器结构

一个编码器层通常包含两个子层:

  1. Multi-Head Self-Attention
  2. Position-wise Feed Forward Network

且每个子层都配有:

  • 残差连接
  • LayerNorm

结构示意:

11. 前馈网络 FFN

FFN 对每个位置独立地进行非线性变换:

$$ \text{FFN}(x) = W_2 \sigma(W_1 x + b_1) + b_2 $$

它不负责跨位置交互,但负责增强表示能力。

12. Transformer 解码器结构

解码器比编码器多一个模块:

  1. Masked Self-Attention
  2. Cross-Attention
  3. Feed Forward

为什么要 Mask

生成第 t 个 token 时,不能偷看第 t+1 个 token,因此需要因果掩码(causal mask)。

13. Encoder-Only、Decoder-Only、Encoder-Decoder

Encoder-Only

代表:

  • BERT

适合:

  • 文本分类
  • 句子表示
  • 信息抽取

Decoder-Only

代表:

  • GPT 系列

适合:

  • 自回归文本生成

Encoder-Decoder

代表:

  • 原始 Transformer
  • T5

适合:

  • 翻译
  • 摘要
  • 条件生成

14. Transformer 的核心优点

  1. 并行性强
  2. 长距离依赖建模直接
  3. 架构通用性强
  4. 扩展到大规模训练效果非常好

15. Transformer 的核心缺点

最经典的问题是注意力复杂度。

序列长度为 $n$ 时,自注意力的时间和空间复杂度通常是:

$$ O(n^2) $$

当序列很长时,计算和显存开销很大。

这也是后来 Longformer、Performer、FlashAttention 等工作的背景之一。

16. LayerNorm 与 BatchNorm 的区别

Transformer 中通常用 LayerNorm,而不是 BatchNorm。

原因包括:

  • 序列长度可变
  • NLP 中 batch 统计不稳定
  • LayerNorm 对单个样本内部做归一化,更适合序列建模

17. Transformer 的残差结构非常重要

注意一点:Transformer 不是只有注意力。

它的成功来自多个关键组件共同作用:

  • 注意力机制
  • 残差连接
  • LayerNorm
  • FFN
  • 位置编码
  • 大规模数据训练

18. 自注意力的矩阵维度理解

假设:

  • batch size = B
  • 序列长度 = T
  • 隐藏维度 = D

输入张量形状:

1
[B, T, D]

映射到 Q、K、V 后仍是:

1
[B, T, D]

如果分成 H 个头,每个头维度是 d = D / H,则:

1
[B, H, T, d]

注意力分数矩阵:

1
[B, H, T, T]

这就是为什么复杂度和序列长度平方相关。

19. 用 PyTorch 手写一个 Self-Attention

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import math
import torch
import torch.nn as nn


class SelfAttention(nn.Module):
    def __init__(self, d_model):
        super().__init__()
        self.q_proj = nn.Linear(d_model, d_model)
        self.k_proj = nn.Linear(d_model, d_model)
        self.v_proj = nn.Linear(d_model, d_model)
        self.out_proj = nn.Linear(d_model, d_model)

    def forward(self, x, mask=None):
        q = self.q_proj(x)
        k = self.k_proj(x)
        v = self.v_proj(x)

        scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(x.size(-1))

        if mask is not None:
            scores = scores.masked_fill(mask == 0, float("-inf"))

        attn = torch.softmax(scores, dim=-1)
        out = torch.matmul(attn, v)
        return self.out_proj(out)


x = torch.randn(2, 5, 32)
attn = SelfAttention(32)
y = attn(x)
print(y.shape)  # [2, 5, 32]

代码解释:这个实现展示了如何手动编写一个自注意力机制。它包含查询、键和值的线性投影,计算注意力分数,应用 softmax 函数,并最终生成输出。

20. 手写 Multi-Head Attention

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import math
import torch
import torch.nn as nn


class MultiHeadSelfAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        assert d_model % num_heads == 0
        self.d_model = d_model
        self.num_heads = num_heads
        self.head_dim = d_model // num_heads

        self.q_proj = nn.Linear(d_model, d_model)
        self.k_proj = nn.Linear(d_model, d_model)
        self.v_proj = nn.Linear(d_model, d_model)
        self.out_proj = nn.Linear(d_model, d_model)

    def forward(self, x, mask=None):
        B, T, D = x.shape

        q = self.q_proj(x).view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
        k = self.k_proj(x).view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
        v = self.v_proj(x).view(B, T, self.num_heads, self.head_dim).transpose(1, 2)

        scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)

        if mask is not None:
            scores = scores.masked_fill(mask == 0, float("-inf"))

        attn = torch.softmax(scores, dim=-1)
        out = torch.matmul(attn, v)

        out = out.transpose(1, 2).contiguous().view(B, T, D)
        out = self.out_proj(out)
        return out


x = torch.randn(2, 10, 64)
module = MultiHeadSelfAttention(d_model=64, num_heads=8)
y = module(x)
print(y.shape)  # [2, 10, 64]

代码解释:这个实现展示了如何手动编写一个多头自注意力机制。它首先将输入投影到多个头的查询、键和值空间,然后计算每个头的注意力分数,最后将所有头的输出拼接并通过线性层生成最终输出。

21. 手写一个 Transformer Encoder Block

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import torch
import torch.nn as nn


class TransformerEncoderBlock(nn.Module):
    def __init__(self, d_model, num_heads, mlp_ratio=4, dropout=0.1):
        super().__init__()
        self.norm1 = nn.LayerNorm(d_model)
        self.attn = nn.MultiheadAttention(d_model, num_heads, dropout=dropout, batch_first=True)
        self.norm2 = nn.LayerNorm(d_model)
        self.mlp = nn.Sequential(
            nn.Linear(d_model, d_model * mlp_ratio),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(d_model * mlp_ratio, d_model),
            nn.Dropout(dropout)
        )

    def forward(self, x, attn_mask=None):
        attn_input = self.norm1(x)
        attn_out, _ = self.attn(attn_input, attn_input, attn_input, attn_mask=attn_mask)
        x = x + attn_out

        mlp_input = self.norm2(x)
        x = x + self.mlp(mlp_input)
        return x


x = torch.randn(4, 16, 128)
block = TransformerEncoderBlock(d_model=128, num_heads=8)
y = block(x)
print(y.shape)  # [4, 16, 128]

代码解释:这个实现展示了如何手动编写一个 Transformer 编码器块。它包含一个多头自注意力层和一个前馈网络(MLP),每个子层都配有 LayerNorm 和残差连接。

22. 用 Transformer 做文本分类的简化示例

下面示例只展示结构,不包含完整 tokenizer 流程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import torch
import torch.nn as nn


class PositionalEncoding(nn.Module):
    def __init__(self, max_len, d_model):
        super().__init__()
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1).float()
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-torch.log(torch.tensor(10000.0)) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        self.register_buffer("pe", pe.unsqueeze(0))

    def forward(self, x):
        return x + self.pe[:, :x.size(1)]


class TextTransformerClassifier(nn.Module):
    def __init__(self, vocab_size, d_model, num_heads, num_layers, num_classes, max_len=128):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.pos_encoding = PositionalEncoding(max_len=max_len, d_model=d_model)

        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model,
            nhead=num_heads,
            dim_feedforward=d_model * 4,
            dropout=0.1,
            activation="gelu",
            batch_first=True
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        self.classifier = nn.Linear(d_model, num_classes)

    def forward(self, input_ids):
        x = self.embedding(input_ids)
        x = self.pos_encoding(x)
        x = self.encoder(x)
        cls_rep = x[:, 0]
        logits = self.classifier(cls_rep)
        return logits


model = TextTransformerClassifier(
    vocab_size=30000,
    d_model=128,
    num_heads=8,
    num_layers=2,
    num_classes=4,
    max_len=128
)

input_ids = torch.randint(0, 30000, (8, 128))
logits = model(input_ids)
print(logits.shape)  # [8, 4]

代码解释:这个实现展示了如何使用 Transformer 架构构建一个文本分类模型。它包括一个嵌入层、位置编码、多个 Transformer 编码器层,以及一个线性分类器。输入是 token id 序列,输出是类别 logits。

23. Vision Transformer 和 CNN 的关系

Transformer 后来被引入视觉领域,形成 Vision Transformer(ViT)。

做法是:

  1. 把图像切成 patch
  2. 每个 patch 拉平后映射为 token embedding
  3. 加位置编码
  4. 输入 Transformer Encoder

这说明 Transformer 其实可以看作一种更通用的 “ token 关系建模器 ”。

24. Transformer 为什么能成为大模型基础

核心原因包括:

  1. 易并行
  2. 可扩展
  3. 模块化强
  4. 在大数据大算力下表现优异
  5. 自回归训练目标简单有效

25. Transformer 中最容易混淆的点

混淆 1:注意力是不是“权重越大越好”

不是。注意力权重只是表示当前上下文下的重要性分配,不是绝对因果解释。

混淆 2:Transformer 只有注意力吗

不是。FFN、残差、Norm、位置编码都很关键。

混淆 3:多头就是多个模型吗

不是。它们共享同一层框架,只是在不同子空间建模。

26. 学 Transformer 时必须真正掌握的点

  1. Q、K、V 的意义。
  2. 注意力公式每一步的含义。
  3. 为什么复杂度是 $O(n^2)$。
  4. 位置编码为什么必要。
  5. Encoder-only、Decoder-only、Encoder-decoder 的区别。
  6. 为什么残差和 LayerNorm 对训练很重要。

27. 用一个极小例子理解注意力计算

这一节非常重要,因为很多人会背公式,但不知道公式到底在做什么。

假设一句话只有 3 个 token,经过线性映射后,某个头上的:

1
2
3
Q = [q1, q2, q3]
K = [k1, k2, k3]
V = [v1, v2, v3]

当我们计算第一个位置的输出时,本质是:

  1. 计算 q1 和所有 k 的相似度
  2. 得到 3 个分数
  3. 经过 softmax 变成 3 个权重
  4. 用这 3 个权重对 v1, v2, v3 加权求和

也就是说,第一个位置最终得到的是“整句信息的加权汇总”,而不是只保留自己。

这就是自注意力比普通逐位置线性层强很多的根本原因。

28. 为什么多头注意力比单头更强

你可以把不同头理解为不同“观察视角”。

例如对一句话:

  • 某个头可能关注语法依赖
  • 某个头可能关注指代关系
  • 某个头可能关注局部搭配

虽然这是直觉解释,不是严格一一对应,但它说明了多头机制的价值:

它允许模型在不同子空间并行地学习不同关系模式。

29. 因果掩码 Causal Mask

在语言生成中,位置 t 只能看 <= t 的 token,不能看未来。

因果掩码通常是一个上三角屏蔽矩阵。

例如序列长度为 4:

1
2
3
4
1 0 0 0
1 1 0 0
1 1 1 0
1 1 1 1

这里 1 表示可见,0 表示不可见。

30. 生成因果掩码的代码

1
2
3
4
5
6
7
8
9
import torch


def build_causal_mask(seq_len):
    mask = torch.tril(torch.ones(seq_len, seq_len))
    return mask


print(build_causal_mask(4))

如果你用 nn.MultiheadAttention 或手写 attention,都要理解这个 mask 的含义。

31. Decoder-Only Transformer 为什么适合大语言模型

像 GPT 这样的模型通常使用 Decoder-Only 结构。

原因包括:

  1. 自回归目标简单明确
  2. 训练和推理逻辑统一
  3. 更适合 next-token prediction

训练目标通常是:

给定前面的 token,预测下一个 token。

形式上可写成:

$$ P(x_1, x_2, …, x_T) = \prod_{t=1}^{T} P(x_t | x_{<t}) $$

这也是大语言模型最核心的概率分解方式。

32. Transformer 中的 embedding 在做什么

原始 token 是离散 id,不能直接送进网络。

Embedding 层做的是:

  • 把 token id 映射成稠密向量
  • 让语义相近的 token 在向量空间中更接近

输入到 Transformer 的第一步通常是:

1
token ids -> token embeddings -> 加位置编码

33. FFN 为什么不能去掉

有些初学者会觉得 Transformer 最重要的是 attention,于是忽略 FFN。

但 FFN 非常关键,因为:

  1. 注意力主要负责跨位置的信息交换
  2. FFN 负责对每个位置进行更强的非线性变换

没有 FFN,模型表达能力会明显下降。

34. Transformer 的训练为什么通常依赖大数据

Transformer 归纳偏置相对弱,不像 CNN 那样天然利用局部性和平移结构。

这意味着:

  • 如果数据少,Transformer 不一定占优
  • 如果数据和算力足够大,Transformer 的上限往往很高

这是它和 CNN 很重要的风格差异之一。

35. 手写一个带因果 mask 的自注意力

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import math
import torch
import torch.nn as nn


class CausalSelfAttention(nn.Module):
    def __init__(self, d_model):
        super().__init__()
        self.q_proj = nn.Linear(d_model, d_model)
        self.k_proj = nn.Linear(d_model, d_model)
        self.v_proj = nn.Linear(d_model, d_model)
        self.out_proj = nn.Linear(d_model, d_model)

    def forward(self, x):
        B, T, D = x.shape
        q = self.q_proj(x)
        k = self.k_proj(x)
        v = self.v_proj(x)

        scores = q @ k.transpose(-2, -1) / math.sqrt(D)
        mask = torch.tril(torch.ones(T, T, device=x.device))
        scores = scores.masked_fill(mask == 0, float("-inf"))
        attn = torch.softmax(scores, dim=-1)
        out = attn @ v
        return self.out_proj(out)

这个实现已经非常接近 Decoder-Only 模型里的核心逻辑了。

36. Transformer 常见训练问题

问题 1:显存占用过大

原因通常包括:

  • 序列长度太长
  • batch size 太大
  • 模型维度或层数太大

问题 2:训练很慢

需要检查:

  • 数据加载是否成为瓶颈
  • 是否使用混合精度
  • 序列长度是否过长

问题 3:模型学不会

可能原因:

  • 学习率设置不合理
  • mask 用错
  • 位置编码缺失
  • 标签对齐错位

37. GPT、BERT、ViT 和 Transformer 的关系

这一节很适合帮助你建立整体地图。

GPT

  • 属于 Decoder-Only Transformer
  • 重点是自回归生成

BERT

  • 属于 Encoder-Only Transformer
  • 重点是双向上下文表示学习

ViT

  • 把图像 patch 当作 token
  • 属于 Transformer 在视觉领域的应用

所以你可以把 Transformer 看成“母架构”,不同任务是在它的不同变体上发展出来的。

38. 学 Transformer 时最容易掉进的坑

  1. 只会背 QKV,不会解释为什么这样设计。
  2. 只会调用框架 API,不会看张量维度。
  3. 知道有位置编码,但不知道没有它会发生什么。
  4. 把注意力图当成绝对解释,而不是上下文相关权重。

39. 一个最小 Decoder-Only 模型骨架

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import torch
import torch.nn as nn


class TinyGPT(nn.Module):
    def __init__(self, vocab_size, d_model, num_heads, num_layers, max_len):
        super().__init__()
        self.token_emb = nn.Embedding(vocab_size, d_model)
        self.pos_emb = nn.Embedding(max_len, d_model)
        self.blocks = nn.ModuleList([
            nn.TransformerEncoderLayer(
                d_model=d_model,
                nhead=num_heads,
                dim_feedforward=d_model * 4,
                batch_first=True,
                activation="gelu"
            )
            for _ in range(num_layers)
        ])
        self.norm = nn.LayerNorm(d_model)
        self.lm_head = nn.Linear(d_model, vocab_size)

    def forward(self, input_ids):
        B, T = input_ids.shape
        pos = torch.arange(T, device=input_ids.device).unsqueeze(0)
        x = self.token_emb(input_ids) + self.pos_emb(pos)

        causal_mask = torch.triu(torch.ones(T, T, device=input_ids.device), diagonal=1).bool()
        for block in self.blocks:
            x = block(x, src_mask=causal_mask)

        x = self.norm(x)
        logits = self.lm_head(x)
        return logits

严格来说,nn.TransformerEncoderLayer 不是为 GPT 专门设计的最优实现,但这个骨架足以帮助你理解大语言模型的基础计算图。


三者对比:CNN、ResNet、Transformer

1. 关系概览

  • CNN 是一大类卷积网络。
  • ResNet 是一种带残差连接的深层 CNN。
  • Transformer 是基于注意力机制的架构。

所以:

1
2
ResNet 属于 CNN 家族
Transformer 不属于 CNN,它是另一条主线

2. 特征建模方式对比

CNN

  • 擅长局部模式
  • 利用卷积核逐步扩大感受野

ResNet

  • 本质仍是 CNN
  • 但通过残差连接解决深层训练问题

Transformer

  • 通过注意力直接建模任意位置间关系
  • 更强调全局依赖

3. 归纳偏置对比

CNN / ResNet

强归纳偏置:

  • 局部性
  • 平移等变性

因此在中小数据规模下往往更容易训练。

Transformer

归纳偏置更弱,更依赖数据量和预训练。

4. 复杂度对比

CNN

  • 对图像大小扩展通常较稳定
  • 局部卷积计算高效

Transformer

  • 自注意力随序列长度平方增长
  • 在长序列上成本高

5. 使用建议

如果你是学习者,建议顺序是:

  1. 先学基础 CNN
  2. 再学 ResNet
  3. 最后深入 Transformer

这是因为:

  • CNN 让你理解视觉神经网络的基本语言
  • ResNet 让你理解深层网络优化
  • Transformer 让你进入现代大模型核心

完整训练模板

这一节给你一个相对通用的 PyTorch 训练模板,你可以把前面的 CNN、ResNet、Transformer 模型替换进去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset


def train_one_epoch(model, loader, criterion, optimizer, device):
    model.train()
    total_loss = 0.0
    total_correct = 0
    total_samples = 0

    for inputs, labels in loader:
        inputs = inputs.to(device)
        labels = labels.to(device)

        logits = model(inputs)
        loss = criterion(logits, labels)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item() * inputs.size(0)
        preds = logits.argmax(dim=1)
        total_correct += (preds == labels).sum().item()
        total_samples += labels.size(0)

    return total_loss / total_samples, total_correct / total_samples


@torch.no_grad()
def evaluate(model, loader, criterion, device):
    model.eval()
    total_loss = 0.0
    total_correct = 0
    total_samples = 0

    for inputs, labels in loader:
        inputs = inputs.to(device)
        labels = labels.to(device)

        logits = model(inputs)
        loss = criterion(logits, labels)

        total_loss += loss.item() * inputs.size(0)
        preds = logits.argmax(dim=1)
        total_correct += (preds == labels).sum().item()
        total_samples += labels.size(0)

    return total_loss / total_samples, total_correct / total_samples


def main():
    device = "cuda" if torch.cuda.is_available() else "cpu"

    X_train = torch.randn(1000, 1, 28, 28)
    y_train = torch.randint(0, 10, (1000,))
    X_val = torch.randn(200, 1, 28, 28)
    y_val = torch.randint(0, 10, (200,))

    train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=64, shuffle=True)
    val_loader = DataLoader(TensorDataset(X_val, y_val), batch_size=64)

    model = nn.Sequential(
        nn.Conv2d(1, 16, kernel_size=3, padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2),
        nn.Conv2d(16, 32, kernel_size=3, padding=1),
        nn.ReLU(),
        nn.AdaptiveAvgPool2d((1, 1)),
        nn.Flatten(),
        nn.Linear(32, 10)
    ).to(device)

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=1e-3)

    for epoch in range(10):
        train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device)
        val_loss, val_acc = evaluate(model, val_loader, criterion, device)
        print(
            f"epoch={epoch+1} "
            f"train_loss={train_loss:.4f} train_acc={train_acc:.4f} "
            f"val_loss={val_loss:.4f} val_acc={val_acc:.4f}"
        )


if __name__ == "__main__":
    main()

训练时你应该重点观察什么

  1. 训练损失是否下降。
  2. 验证集性能是否同步提升。
  3. 是否出现明显过拟合。
  4. 学习率是否合适。
  5. 数据预处理是否正确。

常见调参方向

如果模型效果不好,可以按这个顺序排查:

  1. 数据是否正确
  2. 标签是否正确
  3. loss 是否正确
  4. 学习率是否太大或太小
  5. 模型容量是否合适
  6. 是否需要正则化与数据增强

推荐学习路线

你如果想真正学会,而不是“看过”,建议按下面路线推进。

阶段 1:基础准备

先补这些:

  • Python 基础
  • Numpy
  • 线性代数
  • 概率统计
  • 微积分基础

至少要理解:

  • 向量、矩阵乘法
  • 导数、偏导数、链式法则
  • 期望、方差
  • 概率分布基本概念

阶段 2:机器学习基础

建议顺序:

  1. 线性回归
  2. 逻辑回归
  3. 决策树
  4. 随机森林
  5. SVM
  6. 聚类和降维

学习目标:

  • 知道每个算法解决什么问题
  • 知道优缺点
  • 能用 scikit-learn 跑通
  • 能解释结果

阶段 3:神经网络基础

重点学:

  • 感知机
  • MLP
  • 前向传播
  • 反向传播
  • 梯度下降
  • 激活函数
  • 损失函数
  • 优化器

阶段 4:CNN

这里要真正练习:

  • 手算输出尺寸
  • 写简单 CNN
  • 跑 MNIST / CIFAR-10
  • 观察特征图变化

阶段 5:ResNet

这里重点不是“会调用 torchvision”,而是:

  • 明白为什么深层网络难训练
  • 明白残差连接本质
  • 自己实现 BasicBlock

阶段 6:Transformer

这里建议分三层学:

第一层:概念

  • 注意力是什么
  • QKV 是什么
  • 为什么能建模长距离关系

第二层:公式

  • 手推 attention 公式
  • 画出维度变化

第三层:实现

  • 手写 self-attention
  • 手写 encoder block
  • 用现成模块跑文本分类

阶段 7:项目驱动

做几个小项目:

  • MNIST 手写数字分类
  • CIFAR-10 图像分类
  • IMDB 情感分类
  • 简单机器翻译或文本生成

项目能让你发现自己哪些地方“以为懂了,其实没懂”。


附录:你可以立刻开始做的练习

练习 1:手算卷积输出尺寸

输入:64 x 64

卷积参数:

  • kernel = 3
  • stride = 2
  • padding = 1

请你自己算输出尺寸。

练习 2:实现一个两层 CNN

要求:

  • 输入 1 x 28 x 28
  • 输出 10 类
  • 至少包含两层卷积

练习 3:自己实现一个残差块

要求:

  • 当输入输出维度不一致时,加入 1x1 shortcut

练习 4:手写 attention

不要调用 nn.MultiheadAttention,自己完成:

  • QKV 映射
  • 分数矩阵
  • softmax
  • 加权求和

练习 5:写出下面问题的答案

  1. 为什么 CNN 的参数比全连接网络少?
  2. 为什么 ResNet 可以训练更深?
  3. 为什么 Transformer 需要位置编码?
  4. 为什么自注意力复杂度和序列长度平方相关?

如果这些问题你都能讲清楚,并且能手写简化代码,那么你的基础已经相当扎实。