BACK_TO_BASE
Pytorch与深度学习
Engineering Notebook // Build Log
/
21:54:58
/
NOTEBOOK_ENTRY

Pytorch与深度学习

目录 1. 环境安装 1 环境安装 2. Jupyter Notebook 环境与工作流 2 jupyter notebook 环境与工作流 3. 张量(Tensor)基础 3 张量tensor基础 4. 自动求导(Autograd) 4 自动求导autograd 5. 数据加载(Dataset & DataLoader) 5 数据加载dataset dataloader 6. 构建神经网络 6 构建神经网络 7. 损失函数与优化器 7…

Notebook Time
4 min
Image Frames
1
View Tracks
268
AI
FIELD_GUIDE

FIELD GUIDE

Use the guide rail to jump between sections.

目录

  1. 环境安装
  2. Jupyter Notebook 环境与工作流
  3. 张量(Tensor)基础
  4. 自动求导(Autograd)
  5. 数据加载(Dataset & DataLoader)
  6. 构建神经网络
  7. 损失函数与优化器
  8. 完整训练流程
  9. 计算机视觉实战:CNN 图像分类
  10. 自然语言处理实战:文本情感分类
  11. 综合实战:迁移学习图像分类
  12. 进阶提示与学习资源

1. 环境安装

使用 pip 安装

# 安装 CPU 版本(适合入门学习)
pip install torch torchvision torchaudio

# 如果你有 NVIDIA GPU,安装 CUDA 版本以获得加速(以 CUDA 12.1 为例)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

使用 conda 安装

conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia

验证安装

import torch
print(f"PyTorch 版本: {torch.__version__}")
print(f"CUDA 是否可用: {torch.cuda.is_available()}")

小贴士:如果你刚入门,CPU 版本完全够用。GPU 加速在处理大规模数据和复杂模型时才显得尤为重要。


2. Jupyter Notebook 环境与工作流

为什么用 Jupyter Notebook 学深度学习?

Jupyter Notebook 是深度学习领域最流行的开发工具,几乎所有教程和论文的代码都用它展示。它的核心优势是交互式运行:你可以把代码分成一小块一小块(称为"单元格"),逐块运行、逐步观察结果,非常适合学习和实验。

类比:如果 Python 脚本像写一整篇作文,Jupyter Notebook 就像写草稿纸——你可以一段一段地写,写完一段就检查一下,随时修改。

安装与启动

# 安装经典 Jupyter Notebook
pip install notebook

# 或者安装功能更强大的 JupyterLab(推荐)
pip install jupyterlab

# 启动 Jupyter Notebook
jupyter notebook

# 启动 JupyterLab
jupyter lab

运行启动命令后,浏览器会自动打开一个网页界面。在里面点击 New → Python 3 就可以创建一个新的 Notebook 文件(.ipynb 格式)。

小贴士:如果你使用 VS Code,可以直接安装 Jupyter 扩展,在 VS Code 中打开 .ipynb 文件就能使用,不需要单独启动浏览器。

基本操作

Notebook 由一个个**单元格(Cell)**组成,主要有两种类型:

单元格类型用途示例
Code写和运行 Python 代码import torch
Markdown写文字说明、公式、标题# 这是标题

常用快捷键

掌握这些快捷键可以大幅提高效率:

快捷键功能说明
Shift + Enter运行当前单元格并跳到下一个最常用的快捷键
Ctrl + Enter运行当前单元格(不跳转)想反复运行同一个单元格时用
Esc进入命令模式(单元格边框变蓝)在命令模式下才能用下面的快捷键
Enter进入编辑模式(单元格边框变绿)开始编辑代码/文字
A在上方插入新单元格命令模式下使用(Above)
B在下方插入新单元格命令模式下使用(Below)
DD删除当前单元格命令模式下连按两次 D
M将单元格转为 Markdown命令模式下使用
Y将单元格转为 Code命令模式下使用
Ctrl + S保存养成随手保存的习惯

注意:Jupyter 的单元格执行顺序是你手动点击运行的顺序,不一定是从上到下。这意味着你可以先运行后面的单元格再运行前面的。但要小心:如果变量之间有依赖关系,乱序运行会导致意想不到的错误。建议定期用 Kernel → Restart & Run All 从头到尾重新运行一遍,确保代码逻辑正确。

深度学习工作流:在 Notebook 中可视化

Jupyter 最强大的地方在于可以边训练边可视化。下面介绍几个实用技巧。

技巧 1:内嵌显示图表

# 在 Notebook 的第一个单元格运行这行"魔法命令"
# 它让 matplotlib 的图表直接显示在 Notebook 中,而不是弹出新窗口
%matplotlib inline

import matplotlib.pyplot as plt
import torch

技巧 2:可视化训练损失曲线

训练模型时,损失曲线能帮你直观判断模型是否在学习、是否过拟合。

import matplotlib.pyplot as plt

# 假设你在训练过程中记录了每个 epoch 的损失
train_losses = [2.3, 1.8, 1.2, 0.8, 0.5, 0.35, 0.25, 0.18, 0.15, 0.12]
val_losses =   [2.3, 1.9, 1.4, 1.0, 0.8, 0.75, 0.73, 0.72, 0.73, 0.75]

plt.figure(figsize=(8, 5))
plt.plot(train_losses, label='训练损失', marker='o')
plt.plot(val_losses, label='验证损失', marker='s')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('训练过程中的损失变化')
plt.legend()
plt.grid(True)
plt.show()

# 如果验证损失先降后升(像上面的例子),说明模型开始过拟合了!
# 你可以在验证损失最低的那个 epoch 停止训练(这叫 Early Stopping)

技巧 3:展示数据集中的样本图像

在训练前先看看数据长什么样,确认数据加载和预处理是否正确。

import matplotlib.pyplot as plt
import torchvision
from torchvision import datasets, transforms

transform = transforms.Compose([transforms.ToTensor()])
dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)

# 展示前 16 张图片
fig, axes = plt.subplots(2, 8, figsize=(12, 3))
for i, ax in enumerate(axes.flat):
    image, label = dataset[i]
    ax.imshow(image.squeeze(), cmap='gray')  # squeeze 去掉通道维度
    ax.set_title(f'标签: {label}')
    ax.axis('off')
plt.tight_layout()
plt.show()

技巧 4:用 tqdm 显示训练进度条

训练大型模型时,进度条能让你知道还要等多久。

# 安装:pip install tqdm
from tqdm.notebook import tqdm  # 注意:在 Notebook 中用 tqdm.notebook 版本

import torch
import torch.nn as nn

# 模拟训练循环中使用进度条
num_epochs = 10
num_batches = 100

for epoch in range(num_epochs):
    loop = tqdm(range(num_batches), desc=f'Epoch {epoch+1}/{num_epochs}')
    for batch_idx in loop:
        # ... 训练代码 ...
        fake_loss = 2.0 / (epoch + 1) + torch.rand(1).item() * 0.1

        # 在进度条上实时显示当前损失
        loop.set_postfix(loss=f'{fake_loss:.4f}')

技巧 5:逐层调试模型——查看中间输出形状

设计 CNN 时经常遇到形状不匹配的问题。在 Notebook 中可以逐层运行来排查:

import torch
import torch.nn as nn

# 创建一个假的输入,模拟一个 batch 的图片
dummy_input = torch.randn(4, 1, 28, 28)  # 4张 1通道 28×28 的图片
print(f"输入形状: {dummy_input.shape}")

# 逐层运行,查看每层输出的形状
conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
out = conv1(dummy_input)
print(f"Conv2d 后: {out.shape}")     # (4, 32, 28, 28)

pool = nn.MaxPool2d(2)
out = pool(out)
print(f"MaxPool 后: {out.shape}")    # (4, 32, 14, 14)

conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
out = conv2(out)
print(f"Conv2d 后: {out.shape}")     # (4, 64, 14, 14)

out = pool(out)
print(f"MaxPool 后: {out.shape}")    # (4, 64, 7, 7)

# 现在你知道展平后的维度是 64 * 7 * 7 = 3136
out_flat = out.view(out.size(0), -1)
print(f"展平后: {out_flat.shape}")   # (4, 3136)

小贴士:这种"逐层运行"的调试方式是 Jupyter 的独特优势。在普通 Python 脚本中,你需要在 forward 方法中加 print 语句然后重新运行整个脚本,而在 Notebook 中你可以直接在单元格里一层层试。


3. 张量(Tensor)基础

什么是张量?

张量(Tensor)是 PyTorch 中最核心的数据结构,你可以把它理解为"升级版的多维数组":

  • 标量(0维张量):一个数字,比如 3.14
  • 向量(1维张量):一行数字,比如 [1, 2, 3]
  • 矩阵(2维张量):一个表格,比如一张灰度图像
  • 3维张量:比如一张彩色图像(高度 × 宽度 × 颜色通道)
  • 4维张量:比如一批彩色图像(批量大小 × 高度 × 宽度 × 通道)

张量和 NumPy 的 ndarray 非常相似,但张量有两个 NumPy 没有的超能力:

  1. 可以在 GPU 上运算,速度快几十倍
  2. 支持自动求导,这是训练神经网络的基础

创建张量

import torch

# 从 Python 列表创建
a = torch.tensor([1, 2, 3])
print(a)  # tensor([1, 2, 3])

# 创建全零/全一张量 —— 常用于初始化
zeros = torch.zeros(2, 3)     # 2行3列的全零矩阵
ones = torch.ones(2, 3)       # 2行3列的全一矩阵

# 创建随机张量 —— 神经网络权重通常用随机值初始化
rand_tensor = torch.randn(2, 3)  # 标准正态分布随机数

# 创建等差序列
seq = torch.arange(0, 10, 2)  # tensor([0, 2, 4, 6, 8])

# 查看张量的形状、数据类型、所在设备
print(f"形状: {rand_tensor.shape}")       # torch.Size([2, 3])
print(f"数据类型: {rand_tensor.dtype}")    # torch.float32
print(f"设备: {rand_tensor.device}")       # cpu

张量运算

x = torch.tensor([1.0, 2.0, 3.0])
y = torch.tensor([4.0, 5.0, 6.0])

# 基础数学运算(逐元素操作)
print(x + y)       # tensor([5., 7., 9.])
print(x * y)       # tensor([ 4., 10., 18.])
print(x ** 2)      # tensor([1., 4., 9.])

# 矩阵乘法 —— 深度学习中最常见的运算
A = torch.randn(2, 3)
B = torch.randn(3, 4)
C = A @ B          # 结果形状为 (2, 4)
# 等价于 C = torch.matmul(A, B)

注意* 是逐元素相乘,@ 才是矩阵乘法,这是初学者常犯的错误。

形状变换

x = torch.arange(12)        # tensor([ 0, 1, 2, ..., 11])
print(x.shape)               # torch.Size([12])

# reshape:改变形状但不改变数据
y = x.reshape(3, 4)          # 变成 3行4列
print(y)

# view:和 reshape 功能类似,但要求内存连续
z = x.view(2, 6)             # 变成 2行6列

# 用 -1 自动推断维度(非常实用!)
w = x.reshape(3, -1)         # -1 处自动计算为 4,结果 3行4列

# squeeze 和 unsqueeze:去掉/添加维度
a = torch.zeros(1, 3, 1)     # 形状 (1, 3, 1)
print(a.squeeze().shape)      # 形状 (3,) —— 去掉所有长度为1的维度
print(a.unsqueeze(0).shape)   # 形状 (1, 1, 3, 1) —— 在第0维添加一个维度

小贴士reshape(-1) 可以把任意形状的张量"拍平"成一维,这在把卷积层输出送入全连接层时非常常用。

GPU 加速

# 检查 GPU 是否可用
if torch.cuda.is_available():
    device = torch.device("cuda")
    x_cpu = torch.randn(3, 3)
    x_gpu = x_cpu.to(device)       # 把张量移到 GPU
    print(f"张量在: {x_gpu.device}")  # cuda:0

    # 运算完后移回 CPU(比如要用 NumPy 处理结果时)
    x_back = x_gpu.cpu()

注意:参与同一运算的张量必须在同一个设备上(都在 CPU 或都在同一张 GPU 上),否则会报错。


4. 自动求导(Autograd)

为什么需要自动求导?

训练神经网络的核心思路是:

  1. 用模型做预测
  2. 计算预测和真实值之间的误差(损失)
  3. 计算损失对每个参数的梯度(导数)—— 告诉我们参数该往哪个方向调
  4. 根据梯度更新参数,让损失变小

第 3 步需要求导数。手动推导公式既麻烦又容易出错,而 Autograd 可以自动帮你算梯度,你只需要定义前向计算过程,PyTorch 会自动记录所有运算并反向求导。

基本用法

import torch

# 创建张量,设置 requires_grad=True 告诉 PyTorch:"请追踪这个张量的所有运算"
x = torch.tensor(3.0, requires_grad=True)

# 定义一个计算:y = x² + 2x + 1
y = x ** 2 + 2 * x + 1

# 反向传播:自动计算 dy/dx
y.backward()

# 查看梯度:dy/dx = 2x + 2,当 x=3 时,梯度 = 8
print(x.grad)  # tensor(8.)

计算图是怎么工作的?

当你对设置了 requires_grad=True 的张量进行运算时,PyTorch 会在后台构建一个计算图

调用 y.backward() 后,PyTorch 沿着这个图从输出到输入反向遍历,用链式法则自动计算每个节点的梯度。这就是"反向传播"名字的由来。

多变量求导

x = torch.tensor(1.0, requires_grad=True)
w = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(3.0, requires_grad=True)

# 模拟一个简单的线性模型:y = w * x + b
y = w * x + b

y.backward()

print(f"dy/dx = {x.grad}")  # tensor(2.) → 等于 w 的值
print(f"dy/dw = {w.grad}")  # tensor(1.) → 等于 x 的值
print(f"dy/db = {b.grad}")  # tensor(1.) → 常数项导数为1

梯度清零:一个必须注意的坑

x = torch.tensor(2.0, requires_grad=True)

# 第一次计算
y1 = x ** 2
y1.backward()
print(x.grad)  # tensor(4.)  ✓ 正确

# 第二次计算(不清零的话梯度会累加!)
y2 = x ** 3
y2.backward()
print(x.grad)  # tensor(16.) ✗ 期望是12,但梯度累加变成了 4+12=16

重要:PyTorch 默认会累加梯度,所以在每次反向传播前必须清零梯度。在训练循环中,这就是 optimizer.zero_grad() 的作用。

# 正确做法:手动清零
x.grad.zero_()   # 原地操作,下划线表示原地修改
y3 = x ** 3
y3.backward()
print(x.grad)    # tensor(12.)  ✓ 正确

5. 数据加载(Dataset & DataLoader)

为什么需要 DataLoader?

深度学习训练时,我们不会把所有数据一次性喂给模型(内存/显存装不下),而是分成一小批一小批地喂。这种方式叫做 mini-batch 训练DataLoader 就是帮你自动把数据切分成 batch 的工具。

自定义数据集

from torch.utils.data import Dataset, DataLoader

class MyDataset(Dataset):
    """自定义数据集需要继承 Dataset 并实现两个方法"""

    def __init__(self, data, labels):
        self.data = data
        self.labels = labels

    def __len__(self):
        """返回数据集大小 —— DataLoader 需要知道总共有多少条数据"""
        return len(self.data)

    def __getitem__(self, idx):
        """根据索引返回一条数据 —— DataLoader 通过这个方法取数据"""
        return self.data[idx], self.labels[idx]

# 创建模拟数据:100个样本,每个有5个特征
data = torch.randn(100, 5)
labels = torch.randint(0, 2, (100,))  # 二分类标签(0或1)

dataset = MyDataset(data, labels)

# 创建 DataLoader
loader = DataLoader(
    dataset,
    batch_size=16,    # 每批16个样本
    shuffle=True,     # 每个 epoch 开始时打乱顺序(防止模型"记住"数据顺序)
    num_workers=0     # 数据加载的子进程数,0表示主进程加载
)

# 遍历数据
for batch_data, batch_labels in loader:
    print(f"一个 batch 的数据形状: {batch_data.shape}")   # (16, 5)
    print(f"一个 batch 的标签形状: {batch_labels.shape}")  # (16,)
    break  # 只看第一个 batch

小贴士shuffle=True 非常重要!如果数据是按类别排列的(比如先是所有猫的图片,再是所有狗的图片),不打乱的话模型在一个 batch 里只能看到一种类别,学习效果会很差。

使用 PyTorch 内置数据集

PyTorch 通过 torchvision 提供了很多经典数据集,可以一行代码下载使用:

from torchvision import datasets, transforms

# 定义数据预处理流水线
transform = transforms.Compose([
    transforms.ToTensor(),                # 将图片转为张量,像素值从 [0,255] 缩放到 [0,1]
    transforms.Normalize((0.5,), (0.5,))  # 标准化到 [-1, 1](均值0.5,标准差0.5)
])

# 下载 MNIST 手写数字数据集
train_dataset = datasets.MNIST(
    root='./data',          # 数据存储路径
    train=True,             # 训练集
    download=True,          # 自动下载
    transform=transform     # 应用上面的预处理
)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)

6. 构建神经网络

nn.Module:一切模型的基类

在 PyTorch 中,所有神经网络模型都要继承 nn.Module。你只需要做两件事:

  1. __init__ 中定义网络层(有哪些零件)
  2. forward 中定义前向传播(零件怎么组装)
import torch
import torch.nn as nn

class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()  # 必须调用父类的初始化
        # 定义网络层
        self.fc1 = nn.Linear(784, 256)   # 全连接层:784个输入 → 256个输出
        self.fc2 = nn.Linear(256, 128)   # 全连接层:256 → 128
        self.fc3 = nn.Linear(128, 10)    # 输出层:128 → 10(10个类别)
        self.relu = nn.ReLU()            # 激活函数

    def forward(self, x):
        """定义数据在网络中的流动路径"""
        x = self.relu(self.fc1(x))  # 第1层 → 激活
        x = self.relu(self.fc2(x))  # 第2层 → 激活
        x = self.fc3(x)             # 输出层(分类任务最后一层通常不加激活)
        return x

model = SimpleNet()
print(model)

# 查看模型的所有参数数量
total_params = sum(p.numel() for p in model.parameters())
print(f"模型总参数量: {total_params:,}")

各层的作用解释

作用类比
nn.Linear(in, out)全连接层,做线性变换 y = Wx + b类似一个加权投票系统
nn.ReLU()激活函数,引入非线性:负数变0,正数不变类似一个"过滤器",只保留有用的信号
nn.Conv2d()卷积层,提取图像的局部特征类似用放大镜扫描图片的每个区域
nn.Dropout(p)训练时随机"关闭"一部分神经元,防止过拟合类似考试时随机不让一些学生作弊抄答案

为什么需要激活函数? 如果没有激活函数,无论叠多少层线性层,整个网络等价于一个线性变换,无法学习复杂的非线性关系。ReLU 是最常用的激活函数,简单高效。


7. 损失函数与优化器

损失函数:衡量模型预测有多"差"

损失函数计算模型预测值和真实值之间的差距。损失越小,模型越好。

import torch.nn as nn

# 分类任务常用:交叉熵损失
criterion = nn.CrossEntropyLoss()

# 模拟:模型输出3个类别的得分,真实标签为第2类(索引从0开始)
predictions = torch.tensor([[2.0, 1.0, 0.1]])  # 模型认为第0类得分最高
target = torch.tensor([0])                       # 真实标签确实是第0类
loss = criterion(predictions, target)
print(f"损失值: {loss.item():.4f}")  # .item() 将单个张量转为 Python 数字

# 回归任务常用:均方误差损失
mse_loss = nn.MSELoss()
pred = torch.tensor([2.5, 3.0])
true = torch.tensor([3.0, 3.0])
loss = mse_loss(pred, true)
print(f"MSE 损失: {loss.item():.4f}")  # 0.125 = ((2.5-3)² + (3-3)²) / 2

如何选择损失函数? 简单规则:分类任务用 CrossEntropyLoss,回归任务用 MSELoss

优化器:根据梯度更新参数

优化器的工作是:拿到梯度后,决定每个参数具体怎么更新。

import torch.optim as optim

model = SimpleNet()

# SGD:最基础的优化器,lr 是学习率(步长)
optimizer_sgd = optim.SGD(model.parameters(), lr=0.01)

# Adam:最常用的优化器,自动调节每个参数的学习率,通常效果更好
optimizer_adam = optim.Adam(model.parameters(), lr=0.001)

学习率(lr)是什么? 想象你在山谷里找最低点。梯度告诉你"往哪走",学习率决定"每步走多远"。太大会来回震荡甚至越走越远,太小会走得极慢。常用起点:Adam 用 0.001,SGD 用 0.01


8. 完整训练流程

把前面学的所有内容串起来,这是一个完整的训练流程。请仔细阅读每一步的注释

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# ========== 1. 准备数据 ==========
# 生成模拟数据:学习一个简单的分类任务
torch.manual_seed(42)  # 固定随机种子,确保结果可复现
X = torch.randn(1000, 20)          # 1000个样本,每个20个特征
y = (X[:, 0] + X[:, 1] > 0).long() # 简单规则:前两个特征之和>0 为类别1

# 划分训练集和测试集
train_X, test_X = X[:800], X[800:]
train_y, test_y = y[:800], y[800:]

train_loader = DataLoader(
    TensorDataset(train_X, train_y),
    batch_size=32, shuffle=True
)
test_loader = DataLoader(
    TensorDataset(test_X, test_y),
    batch_size=32
)

# ========== 2. 定义模型 ==========
class Classifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(    # Sequential 可以把多层按顺序串起来
            nn.Linear(20, 64),
            nn.ReLU(),
            nn.Dropout(0.2),         # 随机丢弃20%的神经元,防止过拟合
            nn.Linear(64, 32),
            nn.ReLU(),
            nn.Linear(32, 2)         # 输出2个类别的得分
        )

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

model = Classifier()

# ========== 3. 定义损失函数和优化器 ==========
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# ========== 4. 训练循环 ==========
num_epochs = 20  # 整个数据集要过20遍

for epoch in range(num_epochs):
    model.train()  # 切换到训练模式(Dropout 等层会生效)
    total_loss = 0

    for batch_X, batch_y in train_loader:
        # 前向传播:模型做预测
        outputs = model(batch_X)

        # 计算损失
        loss = criterion(outputs, batch_y)

        # 反向传播三步曲(顺序不能错!)
        optimizer.zero_grad()  # ① 清零梯度(否则梯度会累加)
        loss.backward()        # ② 计算梯度
        optimizer.step()       # ③ 更新参数

        total_loss += loss.item()

    # 每5个 epoch 打印一次训练信息
    if (epoch + 1) % 5 == 0:
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {total_loss/len(train_loader):.4f}")

# ========== 5. 评估模型 ==========
model.eval()  # 切换到评估模式(Dropout 等层会关闭)
correct = 0
total = 0

with torch.no_grad():  # 评估时不需要计算梯度,节省内存和计算
    for batch_X, batch_y in test_loader:
        outputs = model(batch_X)
        _, predicted = torch.max(outputs, 1)  # 取得分最高的类别
        total += batch_y.size(0)
        correct += (predicted == batch_y).sum().item()

print(f"\n测试集准确率: {100 * correct / total:.1f}%")

# ========== 6. 保存和加载模型 ==========
# 保存(只保存参数,推荐方式)
torch.save(model.state_dict(), 'model.pth')

# 加载
loaded_model = Classifier()
loaded_model.load_state_dict(torch.load('model.pth'))
loaded_model.eval()

训练流程总结

┌─────────────────────────────────────────────┐
│  for each epoch:                            │
│    for each batch:                          │
│      ① outputs = model(inputs)  # 前向传播   │
│      ② loss = criterion(outputs, targets)   │
│      ③ optimizer.zero_grad()    # 清零梯度   │
│      ④ loss.backward()          # 反向传播   │
│      ⑤ optimizer.step()         # 更新参数   │
└─────────────────────────────────────────────┘

model.train() vs model.eval() 有什么区别?某些层(如 Dropout、BatchNorm)在训练和推理时行为不同。train() 开启这些层的训练行为,eval() 关闭。忘记切换是一个常见 bug


9. 计算机视觉实战:CNN 图像分类

卷积神经网络(CNN)是什么?

想象你在看一张猫的照片。你不会一次性看整张图,而是关注局部特征:耳朵是尖的、眼睛是圆的、有胡须……CNN 做的事情类似:

  • 卷积层(Conv2d):用一个小窗口(卷积核)在图像上滑动,提取局部特征(边缘、纹理、形状等)
  • 池化层(MaxPool2d):缩小特征图的尺寸,保留最重要的信息,同时减少计算量
  • 全连接层(Linear):把提取到的特征综合起来,做最终分类
输入图像 → [卷积→激活→池化] × N → 展平 → 全连接层 → 分类结果

实战:用 CNN 分类 MNIST 手写数字

MNIST 是一个经典的入门数据集,包含 0-9 的手写数字灰度图像(28×28 像素)。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# ========== 1. 数据准备 ==========
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))  # MNIST 的全局均值和标准差
])

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

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000)

# ========== 2. 定义 CNN 模型 ==========
class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        # 特征提取部分
        self.features = nn.Sequential(
            # 第一个卷积块
            nn.Conv2d(1, 32, kernel_size=3, padding=1),
            # 参数解释:
            #   1 = 输入通道数(灰度图只有1个通道,彩色图是3个)
            #   32 = 输出通道数(即用32个不同的卷积核提取32种特征)
            #   kernel_size=3 = 卷积核大小 3×3
            #   padding=1 = 边缘填充1圈0,保持图像尺寸不变
            nn.ReLU(),
            nn.MaxPool2d(2),   # 2×2 最大池化,图像尺寸减半:28→14

            # 第二个卷积块
            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),   # 图像尺寸再减半:14→7
        )
        # 分类部分
        self.classifier = nn.Sequential(
            nn.Linear(64 * 7 * 7, 128),  # 64个通道 × 7×7 的特征图 = 3136 个特征
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(128, 10)            # 10个数字类别
        )

    def forward(self, x):
        x = self.features(x)       # 卷积特征提取
        x = x.view(x.size(0), -1)  # 展平:(batch, 64, 7, 7) → (batch, 3136)
        x = self.classifier(x)     # 全连接分类
        return x

# ========== 3. 训练 ==========
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = CNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

num_epochs = 5  # MNIST 比较简单,5个 epoch 就够了

for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0

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

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

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

        running_loss += loss.item()

    avg_loss = running_loss / len(train_loader)
    print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")

# ========== 4. 测试 ==========
model.eval()
correct = 0
total = 0

with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print(f"\nMNIST 测试集准确率: {100 * correct / total:.2f}%")
# 通常可以达到 98-99% 的准确率

CNN 中数据形状的变化过程

理解数据形状如何变化是掌握 CNN 的关键:

输入: (batch, 1, 28, 28) ← 64张 28×28 的灰度图
Conv2d(1→32): (batch, 32, 28, 28) ← 32个特征图
MaxPool2d(2): (batch, 32, 14, 14) ← 尺寸减半
Conv2d(32→64): (batch, 64, 14, 14) ← 64个特征图
MaxPool2d(2): (batch, 64, 7, 7) ← 尺寸再减半
展平: (batch, 3136) ← 64×7×7 = 3136
Linear→128: (batch, 128)
Linear→10: (batch, 10) ← 10个类别的得分

10. 自然语言处理实战:文本情感分类

RNN 和 LSTM 是什么?

处理文本和时间序列这类顺序数据时,我们需要模型能"记住前面看过的内容"。

  • RNN(循环神经网络):逐个读取序列中的每个元素,同时维护一个"记忆"(隐藏状态),每读一个新元素就更新记忆
  • LSTM(长短期记忆网络):RNN 的改进版,解决了 RNN "记不住长距离信息"的问题,通过"门控机制"选择性地记住或遗忘信息

类比:RNN 就像你读一本书,每读一页就在脑子里更新对故事的理解。LSTM 则像一个更聪明的读者,知道哪些情节重要要记住,哪些细节可以忘掉。

实战:用 LSTM 进行情感分类

我们用一个简单的例子来演示如何用 LSTM 对文本做正/负情感分类:

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset

# ========== 1. 数据准备(简化版,用模拟数据演示流程) ==========
# 实际项目中你会用真实文本数据,这里用随机数据演示模型结构

vocab_size = 5000   # 词汇表大小(假设有5000个不同的词)
embed_dim = 64      # 词嵌入维度(每个词用64维向量表示)
hidden_dim = 128    # LSTM 隐藏层维度
num_classes = 2     # 正面/负面 两个类别
max_len = 50        # 每条文本最多50个词

class TextDataset(Dataset):
    def __init__(self, num_samples=1000):
        self.texts = torch.randint(0, vocab_size, (num_samples, max_len))
        self.labels = torch.randint(0, num_classes, (num_samples,))

    def __len__(self):
        return len(self.labels)

    def __getitem__(self, idx):
        return self.texts[idx], self.labels[idx]

train_dataset = TextDataset(800)
test_dataset = TextDataset(200)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=32)

# ========== 2. 定义 LSTM 模型 ==========
class LSTMClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        # 词嵌入层:把每个词的整数编号转换为一个密集向量
        # 类比:给每个词一张"身份卡",卡上记录了这个词的各种语义信息
        self.embedding = nn.Embedding(vocab_size, embed_dim)

        # LSTM 层
        self.lstm = nn.LSTM(
            input_size=embed_dim,    # 输入维度 = 词嵌入维度
            hidden_size=hidden_dim,  # 隐藏状态维度
            num_layers=2,            # 堆叠2层 LSTM
            batch_first=True,        # 输入形状为 (batch, seq_len, features)
            dropout=0.3,             # 层间 Dropout
            bidirectional=True       # 双向 LSTM:同时从前往后和从后往前读
        )

        # 分类头
        # 双向 LSTM 的输出维度是 hidden_dim × 2
        self.fc = nn.Linear(hidden_dim * 2, num_classes)
        self.dropout = nn.Dropout(0.5)

    def forward(self, x):
        # x 形状: (batch, max_len) —— 每条文本是一串词的编号
        embedded = self.embedding(x)            # (batch, max_len, embed_dim)
        lstm_out, (hidden, cell) = self.lstm(embedded)
        # lstm_out: 每个时间步的输出
        # hidden: 最后一个时间步的隐藏状态

        # 取最后一个时间步的输出做分类
        # 双向 LSTM:拼接前向和后向的最后隐藏状态
        hidden_cat = torch.cat((hidden[-2], hidden[-1]), dim=1)
        output = self.fc(self.dropout(hidden_cat))
        return output

# ========== 3. 训练 ==========
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = LSTMClassifier().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

for epoch in range(10):
    model.train()
    total_loss = 0
    for texts, labels in train_loader:
        texts, labels = texts.to(device), labels.to(device)

        outputs = model(texts)
        loss = criterion(outputs, labels)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    if (epoch + 1) % 2 == 0:
        print(f"Epoch [{epoch+1}/10], Loss: {total_loss/len(train_loader):.4f}")

# ========== 4. 评估 ==========
model.eval()
correct = 0
total = 0
with torch.no_grad():
    for texts, labels in test_loader:
        texts, labels = texts.to(device), labels.to(device)
        outputs = model(texts)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print(f"\n测试集准确率: {100 * correct / total:.1f}%")

关于模拟数据:上面使用随机数据是为了演示模型结构和训练流程。在真实项目中,你需要用 torchtext 或自己编写预处理代码来处理真实文本(分词、构建词汇表、填充序列等)。用真实数据训练后,模型才能学到有意义的模式。

Transformer 简介

Transformer 是当前 NLP 领域的主流架构(GPT、BERT 等都基于它)。它的核心创新是自注意力机制(Self-Attention)

  • 传统 RNN:必须按顺序处理序列,前面的词处理完才能处理后面的
  • Transformer:可以同时关注序列中的所有位置,像是一目十行

自注意力的直觉理解:对于"我喜欢这个苹果,因为它很甜"这句话,模型在处理"它"的时候需要知道"它"指的是"苹果"。自注意力机制让模型能自动学习这种词与词之间的关联。

# PyTorch 内置了 Transformer 组件,可以直接使用
class TransformerClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.pos_encoding = nn.Embedding(max_len, embed_dim)  # 位置编码

        # Transformer 编码器层
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=embed_dim,    # 模型维度
            nhead=4,              # 多头注意力的头数(并行关注不同类型的关联)
            dim_feedforward=256,  # 前馈网络的隐藏层维度
            dropout=0.1,
            batch_first=True
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=2)
        self.fc = nn.Linear(embed_dim, num_classes)

    def forward(self, x):
        seq_len = x.size(1)
        positions = torch.arange(seq_len, device=x.device).unsqueeze(0)

        # 词嵌入 + 位置编码(Transformer 本身没有位置感知能力,需要额外注入位置信息)
        x = self.embedding(x) + self.pos_encoding(positions)

        x = self.transformer(x)         # (batch, seq_len, embed_dim)
        x = x.mean(dim=1)               # 对所有位置取平均,得到句子表示
        x = self.fc(x)
        return x

小贴士:初学者建议先掌握 LSTM,再学习 Transformer。Transformer 虽然更强大,但概念更复杂。理解 LSTM 的序列建模思想有助于你更好地理解 Transformer。


11. 综合实战:迁移学习图像分类

前面我们学习了张量、自动求导、数据加载、神经网络构建、CNN、RNN 等所有核心概念。现在,让我们用一个完整的迁移学习项目把所有知识串联起来。

什么是迁移学习?

迁移学习的核心思想是:不要从零开始训练,而是站在巨人的肩膀上

类比:假设你已经学会了骑自行车,现在要学骑摩托车。你不需要重新学"保持平衡"这个技能,只需要在已有基础上学习油门和离合就行了。迁移学习也是一样——我们拿一个已经在海量数据(如 ImageNet,120万张图片)上训练好的模型,把它学到的"通用视觉能力"(识别边缘、纹理、形状等)迁移到我们自己的小数据集任务上。

迁移学习的优势

  • 数据需求少:自己只有几百张图片也能训练出不错的模型
  • 训练速度快:大部分参数已经训练好了,只需要微调
  • 效果更好:预训练模型学到的特征比从零开始更有价值

项目目标

使用预训练的 ResNet18 模型,对一个小型图像数据集进行分类。我们将走完一个真实项目的完整流程

数据准备 → 数据增强 → 加载预训练模型 → 修改分类层 → 冻结参数
→ 训练 → 验证 → 可视化损失曲线 → 评估 → 保存模型

完整代码

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset, random_split
from torchvision import transforms, models
import matplotlib.pyplot as plt
from PIL import Image
import os

# ================================================================
# 第1步:准备数据集
# ================================================================
# 这里我们用模拟数据来演示完整流程
# 在真实项目中,你可以替换为自己的图片文件夹

class SimulatedFlowerDataset(Dataset):
    """
    模拟一个花卉分类数据集(5个类别:玫瑰、向日葵、郁金香、雏菊、蒲公英)。
    在真实项目中,你会从文件夹加载图片,这里用随机张量模拟。
    """
    def __init__(self, num_samples=500, num_classes=5, transform=None):
        self.num_samples = num_samples
        self.num_classes = num_classes
        self.transform = transform
        self.class_names = ['玫瑰', '向日葵', '郁金香', '雏菊', '蒲公英']
        self.labels = torch.randint(0, num_classes, (num_samples,))

    def __len__(self):
        return self.num_samples

    def __getitem__(self, idx):
        # 模拟一张 224×224 的 RGB 图片(ResNet 要求输入 224×224)
        image = torch.randn(3, 224, 224)
        label = self.labels[idx]
        return image, label

# ================================================================
# 第2步:数据增强与加载
# ================================================================
# 数据增强可以有效防止过拟合,让模型从有限数据中学到更多
# 在真实项目中应用这些 transforms 到加载的图片上

train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),       # 随机裁剪并缩放到 224×224
    transforms.RandomHorizontalFlip(),        # 50%概率水平翻转
    transforms.RandomRotation(15),            # 随机旋转 ±15度
    transforms.ColorJitter(                   # 随机调整亮度、对比度、饱和度
        brightness=0.2, contrast=0.2, saturation=0.2
    ),
    transforms.ToTensor(),
    transforms.Normalize(                     # ImageNet 的均值和标准差
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

val_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

# 由于我们用的是模拟数据(已经是张量),这里直接创建数据集
# 真实项目中使用 datasets.ImageFolder(root='path/to/data', transform=train_transform)
full_dataset = SimulatedFlowerDataset(num_samples=500, num_classes=5)

# 划分训练集(80%)和验证集(20%)—— 回忆第5节学过的内容
train_size = int(0.8 * len(full_dataset))
val_size = len(full_dataset) - train_size
train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size])

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=0)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=0)

print(f"训练集: {len(train_dataset)} 张图片")
print(f"验证集: {len(val_dataset)} 张图片")

# ================================================================
# 第3步:加载预训练模型并修改
# ================================================================
# 加载在 ImageNet 上预训练好的 ResNet18
# weights='IMAGENET1K_V1' 表示使用 ImageNet 预训练权重
model = models.resnet18(weights='IMAGENET1K_V1')

# 查看原始模型的最后一层
print(f"原始最后一层: {model.fc}")
# 输出: Linear(in_features=512, out_features=1000, bias=True)
# ImageNet 有 1000 个类别,但我们只有 5 个

# 【关键操作】替换最后的全连接层,输出改为我们的类别数
num_classes = 5
model.fc = nn.Sequential(
    nn.Dropout(0.3),                          # 添加 Dropout 防止过拟合
    nn.Linear(model.fc.in_features, num_classes)  # 512 → 5
)
print(f"修改后最后一层: {model.fc}")

# ================================================================
# 第4步:冻结预训练层的参数
# ================================================================
# "冻结"意味着这些参数在训练时不会更新
# 我们只训练最后替换的分类层,这大大加速了训练

# 先冻结所有参数
for param in model.parameters():
    param.requires_grad = False

# 再解冻最后的分类层(让它可以被训练)
for param in model.fc.parameters():
    param.requires_grad = True

# 统计可训练参数量
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"总参数量: {total_params:,}")
print(f"可训练参数量: {trainable_params:,}")
print(f"冻结参数量: {total_params - trainable_params:,}")
# 你会看到只有很少的参数需要训练,这就是迁移学习快的原因

# ================================================================
# 第5步:定义损失函数和优化器
# ================================================================
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)

criterion = nn.CrossEntropyLoss()

# 只优化需要训练的参数(即 requires_grad=True 的参数)
optimizer = optim.Adam(
    filter(lambda p: p.requires_grad, model.parameters()),
    lr=0.001
)

# 学习率调度器:每 5 个 epoch 将学习率乘以 0.1,逐步减小步长
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)

# ================================================================
# 第6步:训练与验证循环
# ================================================================
num_epochs = 15
train_losses = []    # 记录每个 epoch 的训练损失
val_losses = []      # 记录每个 epoch 的验证损失
train_accs = []      # 记录训练准确率
val_accs = []        # 记录验证准确率
best_val_acc = 0.0   # 记录最佳验证准确率

for epoch in range(num_epochs):
    # ---------- 训练阶段 ----------
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0

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

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

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

        running_loss += loss.item()
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    train_loss = running_loss / len(train_loader)
    train_acc = 100 * correct / total
    train_losses.append(train_loss)
    train_accs.append(train_acc)

    # ---------- 验证阶段 ----------
    model.eval()
    val_loss = 0.0
    correct = 0
    total = 0

    with torch.no_grad():
        for images, labels in val_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            loss = criterion(outputs, labels)
            val_loss += loss.item()
            _, predicted = torch.max(outputs, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    val_loss = val_loss / len(val_loader)
    val_acc = 100 * correct / total
    val_losses.append(val_loss)
    val_accs.append(val_acc)

    # 更新学习率
    scheduler.step()

    # 保存验证集上表现最好的模型
    if val_acc > best_val_acc:
        best_val_acc = val_acc
        torch.save(model.state_dict(), 'best_flower_model.pth')

    print(f"Epoch [{epoch+1}/{num_epochs}] "
          f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.1f}% | "
          f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.1f}%")

print(f"\n最佳验证准确率: {best_val_acc:.1f}%")

# ================================================================
# 第7步:可视化训练曲线(在 Jupyter Notebook 中运行效果最佳)
# ================================================================
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# 损失曲线
ax1.plot(range(1, num_epochs+1), train_losses, 'b-o', label='训练损失')
ax1.plot(range(1, num_epochs+1), val_losses, 'r-s', label='验证损失')
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.set_title('损失变化曲线')
ax1.legend()
ax1.grid(True)

# 准确率曲线
ax2.plot(range(1, num_epochs+1), train_accs, 'b-o', label='训练准确率')
ax2.plot(range(1, num_epochs+1), val_accs, 'r-s', label='验证准确率')
ax2.set_xlabel('Epoch')
ax2.set_ylabel('Accuracy (%)')
ax2.set_title('准确率变化曲线')
ax2.legend()
ax2.grid(True)

plt.tight_layout()
plt.savefig('training_curves.png', dpi=150)  # 保存图片
plt.show()

# ================================================================
# 第8步:加载最佳模型并进行最终评估
# ================================================================
model.load_state_dict(torch.load('best_flower_model.pth'))
model.eval()

correct = 0
total = 0
class_correct = [0] * num_classes
class_total = [0] * num_classes

with torch.no_grad():
    for images, labels in val_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

        for i in range(labels.size(0)):
            label = labels[i].item()
            class_total[label] += 1
            if predicted[i] == labels[i]:
                class_correct[label] += 1

print(f"总体准确率: {100 * correct / total:.1f}%\n")
print("各类别准确率:")
class_names = ['玫瑰', '向日葵', '郁金香', '雏菊', '蒲公英']
for i in range(num_classes):
    if class_total[i] > 0:
        print(f"  {class_names[i]}: {100 * class_correct[i] / class_total[i]:.1f}% "
              f"({class_correct[i]}/{class_total[i]})")

进阶:解冻更多层进行微调

当最后一层训练稳定后,可以解冻更多层进一步提升效果:

# 解冻 ResNet 的最后几个卷积块(layer4)
for param in model.layer4.parameters():
    param.requires_grad = True

# 使用更小的学习率——预训练层不需要大幅更新
optimizer = optim.Adam([
    {'params': model.layer4.parameters(), 'lr': 1e-4},   # 预训练层用小学习率
    {'params': model.fc.parameters(), 'lr': 1e-3}         # 新层用正常学习率
])

# 然后继续训练...

小贴士:分层学习率是迁移学习的常见技巧。预训练层已经学到了好的特征,只需要微调,所以用更小的学习率;而新加的分类层需要从头学习,用正常的学习率。

在真实项目中使用 ImageFolder

上面用的是模拟数据。在真实项目中,你只需要把图片按以下结构组织好文件夹:

data/
├── train/
│   ├── 玫瑰/
│   │   ├── rose_001.jpg
│   │   ├── rose_002.jpg
│   │   └── ...
│   ├── 向日葵/
│   │   └── ...
│   └── ...
└── val/
    ├── 玫瑰/
    │   └── ...
    └── ...

然后只需一行代码就能加载:

from torchvision import datasets

train_dataset = datasets.ImageFolder('data/train', transform=train_transform)
val_dataset = datasets.ImageFolder('data/val', transform=val_transform)
print(f"类别: {train_dataset.classes}")  # 自动读取文件夹名作为类别名

这个项目串联了哪些知识点?

回顾一下,这个迁移学习项目中我们用到了前面学过的每个核心概念:

知识点在项目中的应用对应章节
张量操作数据的形状变换、设备转移第 3 节
自动求导loss.backward() 计算梯度第 4 节
Dataset & DataLoader数据集定义、批量加载、划分训练/验证集第 5 节
nn.ModuleResNet 模型结构、修改分类层第 6 节
损失函数与优化器CrossEntropyLoss、Adam、分层学习率第 7 节
训练循环完整的训练 + 验证循环、模型保存/加载第 8 节
CNNResNet 本身就是一个深层 CNN第 9 节
Jupyter 可视化matplotlib 绘制训练曲线第 2 节

12. 进阶提示与学习资源

常见调试技巧

1. 形状不匹配错误

这是最常见的错误。调试方法:在 forward 中打印每一步的张量形状。

def forward(self, x):
    print(f"输入: {x.shape}")
    x = self.conv1(x)
    print(f"conv1 后: {x.shape}")
    # ... 逐层打印,定位问题

2. 损失不下降?检查清单:

  • 学习率是否合适?(试试 0.001、0.0001、0.01)
  • 数据标签是否正确?
  • 是否忘记调用 optimizer.zero_grad()
  • 模型是否在 train() 模式?
  • 数据是否做了适当的归一化/标准化?

3. GPU 内存不足(CUDA out of memory)

  • 减小 batch_size
  • 减少模型参数量
  • 使用 torch.cuda.empty_cache() 释放缓存
  • 确保评估时用了 torch.no_grad()

4. 结果不可复现?

# 在代码开头设置所有随机种子
torch.manual_seed(42)
torch.cuda.manual_seed_all(42)
import random
random.seed(42)
import numpy as np
np.random.seed(42)
torch.backends.cudnn.deterministic = True

推荐学习路线

  1. 入门阶段:熟悉张量操作 → 理解自动求导 → 跑通第一个训练循环
  2. 基础阶段:掌握 CNN → 做一个图像分类项目 → 理解过拟合与正则化
  3. 进阶阶段:学习 RNN/LSTM → 学习 Transformer → 尝试用预训练模型(如 BERT)
  4. 实战阶段:参加 Kaggle 竞赛 → 复现论文 → 做自己的项目

推荐资源

  • 官方教程PyTorch Tutorials — 最权威的学习材料
  • 动手学深度学习d2l.ai — 李沐团队的经典教材,理论与代码并重
  • PyTorch 官方文档pytorch.org/docs — API 查阅必备
  • 3Blue1Brown 神经网络系列:YouTube 上的可视化讲解,帮助建立直觉
  • CS231n(斯坦福):计算机视觉方向的经典课程
  • Hugging Facehuggingface.co — NLP 预训练模型的宝库