
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…
FIELD_GUIDE
FIELD GUIDE
Use the guide rail to jump between sections.
目录
- 环境安装
- Jupyter Notebook 环境与工作流
- 张量(Tensor)基础
- 自动求导(Autograd)
- 数据加载(Dataset & DataLoader)
- 构建神经网络
- 损失函数与优化器
- 完整训练流程
- 计算机视觉实战:CNN 图像分类
- 自然语言处理实战:文本情感分类
- 综合实战:迁移学习图像分类
- 进阶提示与学习资源
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 没有的超能力:
- 可以在 GPU 上运算,速度快几十倍
- 支持自动求导,这是训练神经网络的基础
创建张量
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)
为什么需要自动求导?
训练神经网络的核心思路是:
- 用模型做预测
- 计算预测和真实值之间的误差(损失)
- 计算损失对每个参数的梯度(导数)—— 告诉我们参数该往哪个方向调
- 根据梯度更新参数,让损失变小
第 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。你只需要做两件事:
- 在
__init__中定义网络层(有哪些零件) - 在
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 = 3136Linear→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.Module | ResNet 模型结构、修改分类层 | 第 6 节 |
| 损失函数与优化器 | CrossEntropyLoss、Adam、分层学习率 | 第 7 节 |
| 训练循环 | 完整的训练 + 验证循环、模型保存/加载 | 第 8 节 |
| CNN | ResNet 本身就是一个深层 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
推荐学习路线
- 入门阶段:熟悉张量操作 → 理解自动求导 → 跑通第一个训练循环
- 基础阶段:掌握 CNN → 做一个图像分类项目 → 理解过拟合与正则化
- 进阶阶段:学习 RNN/LSTM → 学习 Transformer → 尝试用预训练模型(如 BERT)
- 实战阶段:参加 Kaggle 竞赛 → 复现论文 → 做自己的项目
推荐资源
- 官方教程:PyTorch Tutorials — 最权威的学习材料
- 动手学深度学习:d2l.ai — 李沐团队的经典教材,理论与代码并重
- PyTorch 官方文档:pytorch.org/docs — API 查阅必备
- 3Blue1Brown 神经网络系列:YouTube 上的可视化讲解,帮助建立直觉
- CS231n(斯坦福):计算机视觉方向的经典课程
- Hugging Face:huggingface.co — NLP 预训练模型的宝库