Dataset类

Dataset 的核心职责是定义数据集的边界和单条样本的获取方式

在 PyTorch 中,自定义数据集通常需要继承 torch.utils.data.Dataset,并且必须实现两个核心的方法:

  1. __len__(self):告诉调用者这个数据集一共有多少个样本。
  2. __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_tensor

DataLoader (数据组装与调度)

DataLoader 的核心职责是高效、按批次(Batch)地把数据喂给模型

它接收一个实例化好的 Dataset 对象,并把它包装成一个可迭代对象(Iterable)。它的几个核心参数直接决定了训练的效率:

  1. dataset:传入你上面定义好的 Dataset 实例。
  2. batch_size:每次从 Dataset 里拿多少条数据拼成一个 Batch(比如 32 或 64)。
  3. shuffle:在每个 Epoch 开始前,是否打乱所有数据的索引。训练集(Train)通常设为 True,验证集(Val/Test)设为 False
  4. num_workers:开启多少个子进程来并行拉取数据。这是打破“GPU 算得太快,CPU 读数据太慢”瓶颈的关键参数。
  5. 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

整个流程只有简单的三步:

  1. 开张:实例化一个 SummaryWriter,告诉它把日志文件存到哪个文件夹(比如 runs/experiment_1)。
  2. 记录:在你的 DataLoader 训练循环里,调用 writer.add_scalar()writer.add_image() 把数据写进去。
  3. 关门:训练结束后调用 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=CIFAR10ImageFolder 这个类自带的参数名
  • 右边的 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 nn

2. 基本使用结构

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()

⚠️ 两个必须实现的部分:

  1. __init__:初始化网络层
  2. 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 x

7. 训练模式 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 + beta

2. 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))  # True

3. 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 对比总结

特性BatchNormLayerNormRMSNorm
归一化维度跨 Batch(同特征)跨特征(同样本)跨特征(同样本)
均值中心化
可学习参数γ, βγ, βγ(无β)
依赖 batch size✅ 强依赖❌ 无关❌ 无关
训练/推理不一致✅ 有差异❌ 一致❌ 一致
计算开销
主要用途CNN / 视觉Transformer现代 LLM
代表模型ResNet, VGGBERT, GPT-2LLaMA, 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)