Dataset类
Dataset 的核心职责是定义数据集的边界和单条样本的获取方式。
在 PyTorch 中,自定义数据集通常需要继承 torch.utils.data.Dataset,并且必须实现两个核心的方法:
__len__(self):告诉调用者这个数据集一共有多少个样本。__getitem__(self, index):定义了当给定一个索引index时,如何读取并返回单条数据(通常是一个 tuple,比如(image_tensor, label)) 代码示例
import torch
from torch.utils.data import Dataset
class MyCustomDataset(Dataset):
def __init__(self, data_list, labels):
# 初始化:通常在这里加载文件路径列表、定义图像变换 (transforms) 等
self.data = data_list
self.labels = labels
def __len__(self):
# 返回数据集的总大小
return len(self.data)
def __getitem__(self, index):
# 根据 index 获取单条样本
sample = self.data[index]
label = self.labels[index]
# 在这里可以做实时的预处理,比如将图片转为 Tensor,或者对文本做 Tokenization
sample_tensor = torch.tensor(sample, dtype=torch.float32)
label_tensor = torch.tensor(label, dtype=torch.long)
return sample_tensor, label_tensorDataLoader (数据组装与调度)
DataLoader 的核心职责是高效、按批次(Batch)地把数据喂给模型。
它接收一个实例化好的 Dataset 对象,并把它包装成一个可迭代对象(Iterable)。它的几个核心参数直接决定了训练的效率:
dataset:传入你上面定义好的 Dataset 实例。batch_size:每次从 Dataset 里拿多少条数据拼成一个 Batch(比如 32 或 64)。shuffle:在每个 Epoch 开始前,是否打乱所有数据的索引。训练集(Train)通常设为True,验证集(Val/Test)设为False。num_workers:开启多少个子进程来并行拉取数据。这是打破“GPU 算得太快,CPU 读数据太慢”瓶颈的关键参数。collate_fn(进阶):这是一个非常强大的参数。当DataLoader拿到batch_size个单条样本后,默认情况下它只会用torch.stack把它们简单地堆叠成高维 Tensor。但在处理 VLM(视觉语言模型)或者 NLP 任务时,文本的长度往往不一致,直接堆叠会报错。此时你需要自定义collate_fn来对齐长度(比如用 Padding 填充),或者使用torch.cat进行特定的拼接。
from torch.utils.data import DataLoader
# 1. 实例化 Dataset
my_dataset = MyCustomDataset(data_list=[1, 2, 3, 4, 5, 6], labels=[0, 1, 0, 1, 0, 1])
# 2. 实例化 DataLoader
train_loader = DataLoader(
dataset=my_dataset,
batch_size=2, # 每次拿2条数据
shuffle=True, # 打乱顺序
num_workers=2 # 开启两个进程加速读取
)
# 3. 在训练循环中使用
for batch_idx, (inputs, targets) in enumerate(train_loader):
# DataLoader 自动将单条数据拼成了 Batch Tensor
# inputs.shape 会是 [2, ...]
print(f"Batch {batch_idx}: inputs={inputs}, targets={targets}")TensorBoard
核心功能拆解
1. 标量记录 (Scalars):画出优美的折线图
这是最常用的功能。你可以把每一个 Epoch 或每一个 Step 的 Loss(训练集和验证集)、Accuracy、学习率(Learning Rate)等数值记录下来。TensorBoard 会实时把它们绘制成平滑的交互式折线图。你可以非常直观地看出模型是否收敛、是否过拟合(比如 Train Loss 还在降,但 Val Loss 开始反弹了)。
2. 直方图分布 (Histograms):偷窥模型的“内部血压”
当你进行微调(比如使用 LoRA) 时,这个功能极其关键。你可以把模型某一层的权重(Weights)、偏置(Biases)或者梯度(Gradients)扔进 TensorBoard。
- 排错利器:如果你发现 LoRA 的 矩阵和 矩阵的梯度直方图全聚在 0 附近,或者随着训练数值爆炸到无限大,你就立刻知道遇到了“梯度消失”或“梯度爆炸”,需要赶紧停下来调学习率了。
3. 图像与文本展示 (Images & Text):多模态调试
可以把模型在验证集上生成的图像、或者预测的文本结果直接输出到 TensorBoard 里。这样你就不需要等到训练彻底结束再去翻看生成的文件,训练途中就能“肉眼”评估模型现在的表现有多聪明(或多离谱)。
4. 计算图可视化 (Graphs):理清网络架构
你可以把整个 PyTorch 模型结构导进去,TensorBoard 会帮你画出非常详细的网络拓扑图,方便你检查数据的流向、维度的变化以及各个模块(比如你插入的 Adapter 或 LoRA 层)是否连接正确。
代码层面
在代码层面,使用 TensorBoard 只需要一个核心类:SummaryWriter。
整个流程只有简单的三步:
- 开张:实例化一个
SummaryWriter,告诉它把日志文件存到哪个文件夹(比如runs/experiment_1)。 - 记录:在你的
DataLoader训练循环里,调用writer.add_scalar()或writer.add_image()把数据写进去。 - 关门:训练结束后调用
writer.close()代码示例
import math
from torch.utils.tensorboard import SummaryWriter
# 1. 开张:指定日志存放的文件夹
writer = SummaryWriter(log_dir="runs/simple_math_demo")
print("正在绘制曲线,请稍候...")
# 2. 记录:模拟一个训练循环 (比如 100 个 step)
for step in range(100):
# 假装这是我们算出来的 Loss 和 Accuracy
fake_loss = math.sin(step / 10.0)
fake_accuracy = math.cos(step / 10.0)
# 将数值写入 TensorBoard
# 格式: writer.add_scalar("图表名称", 纵坐标数值, 横坐标数值)
writer.add_scalar("Metrics/Loss", fake_loss, step)
writer.add_scalar("Metrics/Accuracy", fake_accuracy, step)
# 3. 关门:保存并关闭
writer.close()
print("✅ 绘制完成!请用 VS Code 打开 TensorBoard 查看。")Transform库
torchvision.transforms 是一个极其核心且高频使用的库,可以把它看作是输入图片进入神经网络之前的 “加工厂”。
它的主要作用有两个:数据预处理(Preprocessing)(让数据符合模型的输入要求)和数据增强(Data Augmentation)(通过对图像进行随机变换来扩充数据集,防止模型过拟合)。
1. 核心功能分类
transforms 库包含了几十种图像处理方法,通常可以分为以下几类:
- 类型转换: *
transforms.ToTensor(): 将 PIL 图像或 NumPy 数组转换为 PyTorch 的 Tensor,同时会将像素值从 自动缩放到 。这是必不可少的一步。 - 空间/几何变换(主要用于数据增强):
transforms.Resize((h, w)): 调整图像大小。transforms.RandomCrop(size): 随机裁剪图像的一部分。transforms.RandomHorizontalFlip(p=0.5): 以一定的概率(如 50%)水平翻转图像。
- 色彩/亮度变换:
transforms.ColorJitter(brightness, contrast, saturation, hue): 随机改变图像的亮度、对比度、饱和度和色调。
- 数值处理:
transforms.Normalize(mean, std): 对 Tensor 进行标准化处理。计算公式为:。这能加快模型的收敛速度。
2. 代码使用指南
在实际使用中,我们很少单独使用某一个操作,而是用 transforms.Compose 将多个操作串联(打包) 起来,形成一个处理流水线(Pipeline)。
基础代码示例:构建一个 Transform Pipeline
import torch
from torchvision import transforms
from PIL import Image
# 1. 定义变换流水线 (注意顺序很重要)
my_transforms = transforms.Compose([
transforms.Resize((256, 256)), # 第一步:把图片统一缩放成 256x256
transforms.RandomCrop(224), # 第二步:随机裁剪出 224x224 的区域(常用于 ResNet 等模型)
transforms.RandomHorizontalFlip(p=0.5), # 第三步:50% 的概率水平翻转
transforms.ToTensor(), # 第四步:转为 Tensor,并将像素值归一化到 [0, 1]
transforms.Normalize( # 第五步:基于 ImageNet 数据集的均值和方差进行标准化
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
# 2. 读取一张单张图片进行测试
img_path = "your_image.jpg"
try:
img = Image.open(img_path).convert('RGB') # 确保图片是 RGB 格式
# 3. 将图片送入流水线处理
transformed_img = my_transforms(img)
print("原始图片大小:", img.size)
print("处理后 Tensor 的形状:", transformed_img.shape) # 应该是 [3, 224, 224]
except FileNotFoundError:
print("请提供一张真实的图片路径来运行此代码。")在数据集中的实际应用
在训练模型时,我们通常会将定义好的 transforms 作为参数传递给 PyTorch 的 Dataset。通常,我们会为训练集和测试集/验证集准备两套不同的流水线(训练集需要数据增强,测试集只需要标准化)。
from torchvision import datasets
# 训练集:包含数据增强
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
# 测试集:不包含数据增强,只做缩放、中心裁剪和标准化
test_transforms = 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])
])
# 将 transforms 应用于 ImageFolder 数据集
train_dataset = datasets.ImageFolder(root='data/train', transform=train_transforms)
test_dataset = datasets.ImageFolder(root='data/val', transform=test_transforms)注意 顺序极其重要
transforms.ToTensor()必须放在所有需要处理 PIL 图片的操作(如Resize,RandomCrop)之后。transforms.Normalize()必须放在ToTensor()之后,因为它只能处理 Tensor 格式的数据。
torchvision中的数据集使用
使用内置经典数据集 (以 CIFAR-10 为例) torchvision.datasets 内置了丰富的常用学术数据集。
import torchvision from torchvision
import transforms
# 定义预处理流水线
transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ])
# 实例化训练集与测试集
train_dataset = torchvision.datasets.CIFAR10(
root='./data', # 数据集存放的本地路径
train=True, # True 为训练集,False 为测试集
download=True, # 如果本地没有,是否自动从网络下载
transform=transform # 传入定义好的预处理流水线
)- 左边的
transform=是CIFAR10或ImageFolder这个类自带的参数名。 - 右边的
transform是我们自己定义的变量名(也就是前面用transforms.Compose([...])打包好的那一串操作,比如转 Tensor、标准化等)。
神经网络的基本骨架 nn.module by Gemini
在 PyTorch 中,torch.nn.Module 是所有神经网络的基石。无论是简单的全连接层,还是复杂的 ResNet、Transformer,底层全都继承自 nn.Module。
你可以把它想象成一个 “智能乐高底座”:只要你把各种网络层(也是小模块)拼装在这个底座上,它就能自动帮你管理权重参数、处理 GPU 迁移、甚至帮你打包保存模型。
以下是 nn.Module 的基本骨架和核心使用指南:
1. 核心骨架:两大必须重写的方法
构建自己的神经网络时,你必须创建一个继承自 nn.Module 的类,并重写其中的两个核心方法:__init__ 和 forward。
① __init__(self):准备零部件
在这里,你要提前声明网络中需要用到的所有具有可学习参数的层(比如线性层 nn.Linear、卷积层 nn.Conv2d 等)。
- 避坑警告:在
__init__的第一行,必须调用super().__init__()(或者super(YourClassName, self).__init__())。如果不写这一句,PyTorch 内部的参数追踪机制就会崩溃。
② forward(self, x):组装流水线
这里定义了前向传播(Forward Pass) 的具体计算过程。也就是当输入数据 x 进入模型后,它是如何一步步流经你在 __init__ 中定义的那些层的。
- 注意:PyTorch 的自动求导机制(Autograd)会根据你在
forward里写的运算,自动在后台帮你构建反向传播(Backward Pass)的计算图。你绝对不需要手动去写反向传播逻辑。
2. 标准代码模板:搭建一个简单的神经网络
让我们用代码来直观感受一下。以下是一个用于图像分类的简单多层感知机(MLP):
import torch
import torch.nn as nn
import torch.nn.functional as F
# 1. 继承 nn.Module
class SimpleNet(nn.Module):
def __init__(self):
# 2. 必须调用的父类初始化
super().__init__()
# 3. 定义需要的网络层 (准备零部件)
# 输入特征数为 784 (比如 28x28 的展平图像),输出特征数为 128
self.fc1 = nn.Linear(in_features=784, out_features=128)
# 第二个全连接层,输出为 10 (比如 10 分类任务)
self.fc2 = nn.Linear(in_features=128, out_features=10)
# 定义一个 Dropout 层防止过拟合
self.dropout = nn.Dropout(p=0.5)
def forward(self, x):
# 4. 定义数据的前向流动过程 (组装流水线)
# 假设 x 的形状原本是 [batch_size, 1, 28, 28],先展平为 [batch_size, 784]
x = x.view(x.size(0), -1)
# 经过第一层全连接,内部计算为 y = xA^T + b
x = self.fc1(x)
# 经过 ReLU 激活函数 (注意:没有权重的操作通常直接用 F 里的函数)
x = F.relu(x)
# 经过 Dropout
x = self.dropout(x)
# 经过最后一层得到预测结果
x = self.fc2(x)
return x
# 实例化模型
model = SimpleNet()
print(model)3. nn.Module 隐藏的能力
为什么一定要继承 nn.Module?因为它在后台默默帮你做了大量繁琐的工作:
- 自动管理参数 (
model.parameters()): 只要你把层赋值给了self(比如self.fc1 = ...),nn.Module就会自动将这些层里面的权重(Weights)和偏置(Biases)注册为模型的参数。你可以直接把它们喂给优化器:
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)- 一键设备迁移 (
model.to(device)): 只需要一行代码,它就能把模型内部成千上万个参数同时搬到显卡上:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)- 状态切换 (
model.train()与model.eval()): 像 Dropout 和 BatchNorm 这样的层,在训练和测试时的行为是不同的(测试时不能扔掉神经元)。你只需要调用model.train()或model.eval(),nn.Module就会自动通知所有的子模块切换到正确的模式。 - 状态字典 (
model.state_dict()): 它可以一键导出当前模型所有参数的值,极其方便于模型的保存和加载。
4. 进阶:使用 nn.Sequential 简化代码
如果你发现你的 forward 过程就像糖葫芦一样,完全是线性的(一层挨着一层,没有跳跃连接或复杂的逻辑分支),你可以用 nn.Sequential 来大幅度精简代码:
class QuickNet(nn.Module):
def __init__(self):
super().__init__()
# 将多个层打包在一起
self.features = nn.Sequential(
nn.Linear(784, 128),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(128, 10)
)
def forward(self, x):
x = x.view(x.size(0), -1)
# 直接一波流通过所有层
return self.features(x)PyTorch 神经网络基本骨架 —— nn.Module by Claude
1. 什么是 nn.Module?
nn.Module 是 PyTorch 中所有神经网络模型的基类,位于 torch.nn 模块中。
- 所有自定义神经网络都应该继承自
nn.Module - 它封装了参数管理、前向传播、模型保存等核心功能
- 可以嵌套使用,构建复杂的网络结构
import torch
import torch.nn as nn2. 基本使用结构
class MyModel(nn.Module):
def __init__(self):
super().__init__() # 必须调用父类的 __init__
# 在这里定义网络层
self.linear = nn.Linear(3, 1)
def forward(self, x):
# 定义前向传播逻辑
out = self.linear(x)
return out
## 实例化模型
model = MyModel()⚠️ 两个必须实现的部分:
__init__:初始化网络层forward:定义前向传播
3. __init__ 方法
def __init__(self):
super().__init__() # 初始化父类,必须写!
self.conv1 = nn.Conv2d(1, 32, 3)
self.relu = nn.ReLU()
self.fc = nn.Linear(32, 10)super().__init__()激活nn.Module的参数追踪等功能- 将子层赋值给
self.xxx,PyTorch 会自动将其注册为子模块
4. forward 方法
def forward(self, x):
x = self.conv1(x)
x = self.relu(x)
x = self.fc(x)
return x- 定义数据如何流经网络
- 不要手动调用
forward,而是直接像函数一样调用模型:
output = model(input) # ✅ 正确
output = model.forward(input) # ❌ 不推荐5. 常用内置方法
| 方法 | 说明 |
|---|---|
model.parameters() | 返回所有可训练参数的迭代器 |
model.named_parameters() | 返回参数名称和参数的迭代器 |
model.children() | 返回直接子模块 |
model.modules() | 返回所有模块(递归) |
model.state_dict() | 返回模型参数字典(用于保存) |
model.load_state_dict() | 加载参数字典 |
model.train() | 设置为训练模式 |
model.eval() | 设置为评估模式 |
model.to(device) | 将模型移至指定设备(CPU/GPU) |
## 查看参数
for name, param in model.named_parameters():
print(name, param.shape)
## 保存与加载
torch.save(model.state_dict(), 'model.pth')
model.load_state_dict(torch.load('model.pth'))
## 切换设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)6. 模型嵌套(Sequential)
方式一:使用 nn.Sequential
model = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10)
)方式二:在自定义 Module 中嵌套子 Module
class BigModel(nn.Module):
def __init__(self):
super().__init__()
self.block1 = SmallBlock() # SmallBlock 也是 nn.Module 的子类
self.fc = nn.Linear(64, 10)
def forward(self, x):
x = self.block1(x)
x = self.fc(x)
return x7. 训练模式 vs 评估模式
model.train() # 启用 Dropout、BatchNorm 的训练行为
model.eval() # 关闭 Dropout,BatchNorm 使用统计均值在测试/推理时务必调用
model.eval(),并配合torch.no_grad()节省内存:
model.eval()
with torch.no_grad():
output = model(input)8. 完整示例
import torch
import torch.nn as nn
## 定义模型
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(256, 10)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
## 实例化
model = SimpleNet()
print(model)
## 构造输入(batch_size=4, features=784)
x = torch.randn(4, 784)
## 前向传播
output = model(x)
print(output.shape) # torch.Size([4, 10])9. 核心要点总结
nn.Module
├── 继承它来定义自己的模型
├── __init__:注册子层(nn.Linear, nn.Conv2d 等)
├── forward:定义数据流
├── 自动管理参数(parameters)
├── 支持 .to(device) 整体迁移
└── 支持 state_dict 保存与加载
💡 记住:
nn.Module就是神经网络的”容器+管家”,负责把所有层组织起来,并追踪它们的参数。
卷积层详解(PyTorch)
卷积层是用来做什么的?
直觉上理解:卷积层就是一个特征探测器。
想象你在看一张图片,你的眼睛不会一次看整张图,而是会在图上”滑动”,每次关注一个小区域,判断这个区域有没有某种特征(比如边缘、纹理、颜色变化)。卷积层做的事情完全一样。
卷积核(Filter)是什么?
卷积核就是一个小矩阵,比如 3×3 大小,里面的数字是网络自己学习出来的参数。它在图片上从左到右、从上到下滑动,每滑到一个位置就做一次乘法求和,输出一个数字,最终生成一张新的”特征图”。
原图(5×5) 卷积核(3×3) 特征图(3×3)
┌─────────────┐ ┌─────────┐ ┌─────────┐
│ 1 2 3 4 5 │ │ 1 0 -1 │ │ ? ? ? │
│ 1 2 3 4 5 │ * │ 1 0 -1 │ = │ ? ? ? │
│ 1 2 3 4 5 │ │ 1 0 -1 │ │ ? ? ? │
│ 1 2 3 4 5 │ └─────────┘ └─────────┘
│ 1 2 3 4 5 │
└─────────────┘
一个卷积层可以有很多个卷积核,每个核负责探测不同的特征,所以输出会有多张特征图。
PyTorch 代码详解
基本定义
import torch
import torch.nn as nn
# 定义一个卷积层
conv = nn.Conv2d(
in_channels=1, # 输入通道数(灰度图=1,RGB图=3)
out_channels=32, # 输出通道数,即用多少个卷积核
kernel_size=3, # 卷积核大小,3 表示 3×3
stride=1, # 步长:卷积核每次滑动多少格,默认1
padding=0 # 边缘填充:在图片边缘补多少圈0,默认0
)跑一下看看输入输出的形状变化
# 构造一张假的灰度图
# torch.randn 生成随机数,括号里是形状
# (batch_size, channels, height, width)
# batch_size=1 表示一次送入1张图
# channels=1 表示灰度图(1个通道)
# height=28, width=28 表示图片大小 28×28(类似 MNIST 手写数字)
x = torch.randn(1, 1, 28, 28)
print(x.shape) # torch.Size([1, 1, 28, 28])
# 让图片经过卷积层
out = conv(x)
print(out.shape) # torch.Size([1, 32, 26, 26])
# ↑ ↑ ↑
# 32个特征图 26×26(变小了,因为没有padding)为什么从 28×28 变成了 26×26?
因为 3×3 的卷积核在 28×28 的图上滑动,边缘放不下,所以输出尺寸 = 28 - 3 + 1 = 26。
padding 的作用
# 加上 padding=1,在图片边缘补一圈0
conv_with_padding = nn.Conv2d(1, 32, kernel_size=3, padding=1)
out2 = conv_with_padding(x)
print(out2.shape) # torch.Size([1, 32, 28, 28])
# ↑↑ 尺寸保持不变了!加 padding 的目的是让输出特征图和输入尺寸一样,避免图片越卷越小。
stride 的作用
# stride=2,卷积核每次跳2格
conv_stride = nn.Conv2d(1, 32, kernel_size=3, stride=2, padding=1)
out3 = conv_stride(x)
print(out3.shape) # torch.Size([1, 32, 14, 14])
# ↑↑ 尺寸缩小了一半!stride 常用来主动缩小特征图尺寸,作用类似于池化层。
放入完整网络中使用
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
# 卷积层:1通道输入 → 32个特征图,核3×3,padding保持尺寸
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
# 激活函数
self.relu = nn.ReLU()
# 展平层:把 (32, 28, 28) 的特征图拉成一维向量
# 32 × 28 × 28 = 25088 个数字
self.flatten = nn.Flatten()
# 全连接层:25088 → 10(比如分10个类)
self.fc = nn.Linear(32 * 28 * 28, 10)
def forward(self, x):
x = self.conv1(x) # (1,1,28,28) → (1,32,28,28)
x = self.relu(x) # 形状不变,只是把负数变0
x = self.flatten(x) # (1,32,28,28) → (1,25088)
x = self.fc(x) # (1,25088) → (1,10)
return x
model = SimpleCNN()
x = torch.randn(1, 1, 28, 28) # 一张 28×28 灰度图
out = model(x)
print(out.shape) # torch.Size([1, 10])注意 nn.Flatten() 这一步非常重要!卷积层输出的是三维特征图,而全连接层只接受一维向量,所以中间必须展平。
总结一张图
输入图片
(1, 1, 28, 28)
↓
Conv2d(1→32, 3×3, padding=1) # 用32个卷积核探测特征
(1, 32, 28, 28)
↓
ReLU() # 激活,过滤负值
(1, 32, 28, 28)
↓
Flatten() # 展平成一维
(1, 25088)
↓
Linear(25088 → 10) # 全连接,输出分类结果
(1, 10)
卷积层的核心思路就是:用小卷积核在图上滑动 → 提取局部特征 → 多个核提取多种特征 → 后续层再根据这些特征做判断。
池化层详解(PyTorch)
池化层是用来做什么的?
卷积层提取特征后,特征图往往还是很大,计算量很高。池化层的作用是对特征图进行压缩,保留最重要的信息,同时减小尺寸。
它带来三个好处:
- 减少计算量:特征图变小,后续层参数更少
- 防止过拟合:丢弃一些细节,让模型更关注整体特征
- 平移不变性:图片里的猫往左移了几个像素,池化后的结果几乎不变
最大池化 vs 平均池化
|最大池化|平均池化| |---|---|---| |取值方式|窗口内最大值|窗口内平均值| |保留信息|最显著的特征|整体平均信息| |常见用途|卷积网络中间层|网络末尾全局池化| |使用频率|⭐⭐⭐ 更常用|⭐⭐ 特定场景|
最大池化(MaxPooling)
最常用的池化方式。在一个小窗口内取最大值,相当于”这个区域有没有这个特征,有的话有多强”。
特征图(4×4) MaxPool 2×2, stride=2 输出(2×2)
┌──────────────┐ ┌────────┐
│ 1 3 2 4 │ │ 6 4 │
│ 5 6 1 2 │ ──────────────────────────> │ 3 8 │
│ 3 2 7 8 │ └────────┘
│ 1 0 4 6 │
└──────────────┘
左上 2×2 区域:1,3,5,6 → 取最大值 6 右上 2×2 区域:2,4,1,2 → 取最大值 4 左下 2×2 区域:3,2,1,0 → 取最大值 3
PyTorch 代码详解
最大池化 nn.MaxPool2d
import torch
import torch.nn as nn
# 定义最大池化层
pool = nn.MaxPool2d(
kernel_size=2, # 窗口大小 2×2
stride=2 # 步长为2,窗口不重叠(默认等于kernel_size)
)
# 构造输入特征图
# (batch_size=1, channels=32, height=28, width=28)
x = torch.randn(1, 32, 28, 28)
out = pool(x)
print(out.shape) # torch.Size([1, 32, 14, 14])
# ↑↑ 长宽各缩小一半!计算公式:输出尺寸 = (输入尺寸 - kernel_size) / stride + 1 = (28 - 2) / 2 + 1 = 14
平均池化 nn.AvgPool2d
取窗口内所有值的平均值,而不是最大值。
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
x = torch.randn(1, 32, 28, 28)
out = avg_pool(x)
print(out.shape) # torch.Size([1, 32, 14, 14])
# 形状变化和 MaxPool 一样,只是取值方式不同最大池化更常用,因为它保留的是最显著的特征;平均池化则保留整体的平均信息,在一些特定场景(如全局池化)中使用。
全局平均池化 nn.AdaptiveAvgPool2d
不管输入特征图多大,都压缩成指定的固定尺寸,常用于网络末尾代替 Flatten。
# 无论输入多大,都输出 1×1
global_pool = nn.AdaptiveAvgPool2d((1, 1))
x = torch.randn(1, 32, 28, 28)
out = global_pool(x)
print(out.shape) # torch.Size([1, 32, 1, 1])
# 每个通道的整张特征图被压缩成一个数字放入完整网络中使用
class CNNWithPooling(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
# 最大池化,窗口2×2,步长2
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.relu = nn.ReLU()
self.flatten = nn.Flatten()
# 经过一次池化后,28×28 → 14×14
# 所以全连接层输入是 32 × 14 × 14 = 6272
self.fc = nn.Linear(32 * 14 * 14, 10)
def forward(self, x):
x = self.conv1(x) # (1,1,28,28) → (1,32,28,28)
x = self.relu(x) # (1,32,28,28) → (1,32,28,28) 形状不变
x = self.pool(x) # (1,32,28,28) → (1,32,14,14) 尺寸减半
x = self.flatten(x) # (1,32,14,14) → (1,6272)
x = self.fc(x) # (1,6272) → (1,10)
return x
model = CNNWithPooling()
x = torch.randn(1, 1, 28, 28)
out = model(x)
print(out.shape) # torch.Size([1, 10])注意和之前没有池化的版本对比:全连接层的输入从 25088 减少到了 6272,参数量大幅下降!
总结一张图
输入特征图
(1, 32, 28, 28)
↓
MaxPool2d(2×2, stride=2) # 每个 2×2 区域取最大值
(1, 32, 14, 14) # 长宽各缩小一半,通道数不变
↓
Flatten()
(1, 6272)
↓
Linear(6272 → 10)
(1, 10)
💡 记住: 池化层没有任何可学习的参数,它只是一个固定的压缩操作。所有的学习都发生在卷积层里。
非线性激活函数详解(PyTorch)
为什么需要激活函数?
先看没有激活函数的情况:
# 两层全连接网络,没有激活函数
y = W2 * (W1 * x + b1) + b2
# 展开后等价于:
y = (W2*W1) * x + (W2*b1 + b2)
# 本质上还是一个线性变换!无论叠多少层,没有激活函数的神经网络本质上等价于一层,只能拟合线性关系,无法处理复杂问题(比如图像识别、语音识别)。
激活函数引入非线性,让网络有能力拟合任意复杂的函数。
常用激活函数一览
1. ReLU(最常用)
公式:
import torch
import torch.nn as nn
relu = nn.ReLU()
x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
print(relu(x)) # tensor([0., 0., 0., 1., 2.])
# 负数全部变0,正数保持不变输出
↑
| /
| /
| /
| /
─|────/──────→ 输入
| 0
优点:
- 计算极简单,训练速度快
- 有效缓解梯度消失问题(后面会解释)
- 实际效果好,是目前最常用的激活函数
缺点:
- 神经元死亡问题:如果某个神经元的输入一直是负数,它的输出永远是0,梯度也永远是0,这个神经元就再也无法更新参数了,相当于”死掉了”
# inplace 参数(上节课讲过)
nn.ReLU() # inplace=False,默认,安全
nn.ReLU(inplace=True) # 直接修改原变量,省内存但要小心2. Sigmoid
sigmoid = nn.Sigmoid()
x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
print(sigmoid(x))
# tensor([0.1192, 0.2689, 0.5000, 0.7311, 0.8808])
# 所有输出都被压缩到 (0, 1) 之间
优点:
- 输出在 (0,1) 之间,可以解释为概率
- 常用于二分类问题的输出层
缺点(严重):
- 梯度消失:当输入很大或很小时,曲线趋于平坦,梯度接近0,导致靠近输入层的参数几乎无法更新
- 输出不是以0为中心,会影响训练效率
- 含有指数运算,计算较慢
⚠️ 现在几乎不在隐藏层使用,只在需要输出概率的场景(二分类输出层)使用。
3. Tanh
公式:
tanh = nn.Tanh()
x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
print(tanh(x))
# tensor([-0.9640, -0.7616, 0.0000, 0.7616, 0.9640])
# 输出压缩到 (-1, 1) 之间优点:
- 输出以0为中心,比 Sigmoid 收敛更快
- 在 RNN/LSTM 等循环神经网络中仍常用
缺点:
- 同样存在梯度消失问题
- 同样含指数运算,计算较慢
4. LeakyReLU(ReLU 的改进版)
公式:
leaky_relu = nn.LeakyReLU(negative_slope=0.01) # 负数区域有个小斜率
x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
print(leaky_relu(x))
# tensor([-0.0200, -0.0100, 0.0000, 1.0000, 2.0000])
# 负数不再变0,而是乘以一个很小的系数 0.01输出
↑
| /
| /
─|──────/────→ 输入
\ 0
\ (斜率很小,约0.01)
优点:
- 解决了 ReLU 的神经元死亡问题
- 负数区域仍有梯度,神经元不会”死”
5. GELU(现代大模型常用)
gelu = nn.GELU()
x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
print(gelu(x))
# tensor([-0.0455, -0.1587, 0.0000, 0.8413, 1.9545])GELU 是目前 Transformer、BERT、GPT 等大模型中使用的激活函数,效果比 ReLU 更好,但计算稍复杂。
梯度消失问题(重要概念)
训练神经网络靠的是反向传播:误差从输出层往输入层一层一层传回去,每层的参数根据梯度来更新。
Sigmoid 和 Tanh 在输入值很大或很小时,导数(梯度)趋近于0:
Sigmoid 的导数最大只有 0.25
经过10层网络:0.25^10 ≈ 0.000001
梯度经过多层后变得极小,靠近输入层的参数几乎无法更新,网络无法正常训练,这就是梯度消失。
ReLU 的导数在正数区域永远是1,不会出现这个问题,这是它成为主流的核心原因。
在网络中如何选择激活函数?
| 位置 | 推荐激活函数 |
|---|---|
| 隐藏层(CNN、MLP) | ReLU 或 LeakyReLU |
| 隐藏层(Transformer) | GELU |
| 隐藏层(RNN/LSTM) | Tanh |
| 输出层(二分类) | Sigmoid |
| 输出层(多分类) | Softmax(不在 nn.Module 里,用 F.softmax) |
| 输出层(回归) | 不加激活函数 |
完整对比示例
import torch
import torch.nn as nn
x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])
activations = {
'ReLU': nn.ReLU(),
'Sigmoid': nn.Sigmoid(),
'Tanh': nn.Tanh(),
'LeakyReLU': nn.LeakyReLU(0.01),
'GELU': nn.GELU(),
}
for name, fn in activations.items():
print(f"{name:12s}: {fn(x).numpy().round(3)}")输出:
ReLU : [0. 0. 0. 1. 2. ]
Sigmoid : [0.119 0.269 0.5 0.731 0.881]
Tanh : [-0.964 -0.762 0. 0.762 0.964]
LeakyReLU : [-0.02 -0.01 0. 1. 2. ]
GELU : [-0.046 -0.159 0. 0.841 1.955]
总结
激活函数的本质:给网络引入非线性,让它能拟合复杂函数
ReLU → 隐藏层首选,简单高效
LeakyReLU → ReLU 改进版,解决神经元死亡
Sigmoid → 只用于二分类输出层
Tanh → RNN 类网络的隐藏层
GELU → Transformer 类大模型的首选
核心原则:隐藏层用 ReLU 系列,输出层根据任务选择
💡 记住: 激活函数本身没有任何可学习的参数(LeakyReLU 的 slope 是手动设置的超参数),它只是一个固定的数学变换,作用是给网络引入非线性能力。
PyTorch 线性层 & Norm 层详解
一、线性层 nn.Linear
原理
线性层执行仿射变换:
| 参数 | 说明 |
|---|---|
in_features | 输入维度 |
out_features | 输出维度 |
bias | 是否加偏置,默认 True |
权重初始化默认使用 Kaiming Uniform,偏置用 Uniform。
import torch
import torch.nn as nn
# 基本用法
linear = nn.Linear(in_features=128, out_features=64)
x = torch.randn(32, 128) # batch=32, feature=128
y = linear(x) # -> (32, 64)
print(f"Weight shape: {linear.weight.shape}") # (64, 128)
print(f"Bias shape: {linear.bias.shape}") # (64,)
# 多维输入 (序列场景)
x_seq = torch.randn(32, 10, 128) # (batch, seq_len, d_model)
y_seq = linear(x_seq) # -> (32, 10, 64) ✅ 自动广播最后一维二、Normalization 层
归一化层的核心目的:稳定训练、加速收敛、缓解梯度消失/爆炸。
三者的本质区别在于 在哪个维度上做归一化:
输入 shape: (N, C, H, W) 或 (N, T, D)
BatchNorm → 跨 Batch 维度归一化(同一特征通道)
LayerNorm → 跨特征维度归一化(同一样本内部)
RMSNorm → LayerNorm 的简化版,去掉均值中心化
1. Batch Normalization
原理
对每个特征维度,跨 batch 统计均值和方差:
- 训练时:用当前 mini-batch 统计量
- 推理时:用训练期间维护的滑动平均 (
running_mean,running_var) - 有可学习参数 (scale)和 (shift)
适用场景
✅ CV 任务(CNN)、较大 batch size
❌ 小 batch、RNN/Transformer、在线推理不稳定
import torch
import torch.nn as nn
# ---- 2D 输入 (全连接场景) ----
bn1d = nn.BatchNorm1d(num_features=64)
x = torch.randn(32, 64) # (N, C)
out = bn1d(x) # -> (32, 64)
# ---- 4D 输入 (卷积场景) ----
bn2d = nn.BatchNorm2d(num_features=32)
x = torch.randn(8, 32, 224, 224) # (N, C, H, W)
out = bn2d(x) # -> (8, 32, 224, 224)
# 归一化在 (N, H, W) 维度进行,每个通道独立
print(f"gamma (weight): {bn2d.weight.shape}") # (32,)
print(f"beta (bias): {bn2d.bias.shape}") # (32,)
print(f"running_mean: {bn2d.running_mean.shape}") # (32,) 非参数
# ---- 训练 vs 推理模式切换 ----
bn1d.train() # 使用 batch 统计量
bn1d.eval() # 使用 running_mean / running_var
# ---- 手动实现 ----
def batch_norm_manual(x, gamma, beta, eps=1e-5):
# x: (N, C)
mean = x.mean(dim=0, keepdim=True) # (1, C)
var = x.var(dim=0, keepdim=True, unbiased=False)
x_hat = (x - mean) / (var + eps).sqrt()
return gamma * x_hat + beta2. Layer Normalization
原理
对单个样本的所有特征归一化,不依赖 batch:
其中 在 normalized_shape 指定的维度上计算。
适用场景
✅ NLP / Transformer(BERT、GPT 标配)
✅ 小 batch 或 batch=1
✅ 可变长序列
❌ CNN 效果不如 BN
import torch
import torch.nn as nn
# ---- 基本用法 ----
d_model = 512
ln = nn.LayerNorm(normalized_shape=d_model) # 对最后一维归一化
x = torch.randn(32, 10, 512) # (batch, seq_len, d_model)
out = ln(x) # -> (32, 10, 512)
# 对每个 token 的 512 维特征独立归一化
# ---- 多维归一化 ----
ln_2d = nn.LayerNorm(normalized_shape=[10, 512]) # 对最后两维归一化
out_2d = ln_2d(x)
print(f"gamma shape: {ln.weight.shape}") # (512,)
print(f"beta shape: {ln.bias.shape}") # (512,)
# ---- 手动实现 ----
def layer_norm_manual(x, gamma, beta, eps=1e-5):
# x: (..., D)
mean = x.mean(dim=-1, keepdim=True)
var = x.var(dim=-1, keepdim=True, unbiased=False)
x_hat = (x - mean) / (var + eps).sqrt()
return gamma * x_hat + beta
# ---- 验证 ----
x_test = torch.randn(4, 8, 512)
ln_test = nn.LayerNorm(512)
out_official = ln_test(x_test)
out_manual = layer_norm_manual(x_test, ln_test.weight, ln_test.bias)
print(torch.allclose(out_official, out_manual, atol=1e-5)) # True3. RMS Normalization
原理
来自论文 《Root Mean Square Layer Normalization》(2019),用于 LLaMA、Mistral、Qwen 等现代大模型。
去掉了均值中心化,只用 RMS 做缩放:
优点:
- 比 LayerNorm 少约 7~64% 计算量(省去均值计算和减均值)
- 实践中效果与 LayerNorm 相当甚至更好
- 无 bias 参数
import torch
import torch.nn as nn
# ---- PyTorch 2.4+ 官方实现 ----
# rms_norm = nn.RMSNorm(normalized_shape=512) # PyTorch >= 2.4
# ---- 自定义实现(通用) ----
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim)) # gamma,无 beta
def _norm(self, x):
# x: (..., dim)
rms = x.pow(2).mean(dim=-1, keepdim=True).add(self.eps).sqrt()
return x / rms
def forward(self, x: torch.Tensor) -> torch.Tensor:
# 在 float32 下计算以保持精度,再转回原始类型
output = self._norm(x.float()).type_as(x)
return output * self.weight
# ---- 使用示例 ----
rms_norm = RMSNorm(dim=512)
x = torch.randn(32, 10, 512)
out = rms_norm(x)
print(out.shape) # (32, 10, 512)
# ---- 与 LayerNorm 的速度对比 ----
import time
x_bench = torch.randn(64, 512, 4096, device='cpu')
ln = nn.LayerNorm(4096)
rms = RMSNorm(4096)
N = 100
t0 = time.time()
for _ in range(N): ln(x_bench)
print(f"LayerNorm: {(time.time()-t0)*1000/N:.2f} ms")
t0 = time.time()
for _ in range(N): rms(x_bench)
print(f"RMSNorm: {(time.time()-t0)*1000/N:.2f} ms")三、三种 Norm 对比总结
| 特性 | BatchNorm | LayerNorm | RMSNorm |
|---|---|---|---|
| 归一化维度 | 跨 Batch(同特征) | 跨特征(同样本) | 跨特征(同样本) |
| 均值中心化 | ✅ | ✅ | ❌ |
| 可学习参数 | γ, β | γ, β | γ(无β) |
| 依赖 batch size | ✅ 强依赖 | ❌ 无关 | ❌ 无关 |
| 训练/推理不一致 | ✅ 有差异 | ❌ 一致 | ❌ 一致 |
| 计算开销 | 中 | 中 | 低 |
| 主要用途 | CNN / 视觉 | Transformer | 现代 LLM |
| 代表模型 | ResNet, VGG | BERT, GPT-2 | LLaMA, Qwen |
四、在 Transformer Block 中的典型用法
class TransformerBlock(nn.Module):
"""展示 Pre-Norm 结构(现代主流)"""
def __init__(self, d_model=512, nhead=8, use_rms=True):
super().__init__()
Norm = RMSNorm if use_rms else nn.LayerNorm
self.norm1 = Norm(d_model)
self.norm2 = Norm(d_model)
self.attn = nn.MultiheadAttention(d_model, nhead, batch_first=True)
self.ff = nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Linear(d_model * 4, d_model),
)
def forward(self, x):
# Pre-Norm: 先 Norm 再做操作,残差连接
x = x + self.attn(self.norm1(x), self.norm1(x), self.norm1(x))[0]
x = x + self.ff(self.norm2(x))
return x
model = TransformerBlock()
x = torch.randn(2, 16, 512)
print(model(x).shape) # (2, 16, 512)