PyTorch基础
一、简介
PyTorch是由Meta(原 Facebook)人工智能团队开发的开源深度学习框架,主要用于构建和训练神经网络,支持动态计算图。自 2016 年发布以来迅速成为学术界和工业界的主流工具。
早期使用TensorFlow(由google团队开发),但逐步被PyTorch替代。TensorFlow虽然性能更好,但灵活性较低(大部分功能内部已经强行设定)。
PyTorch的核心是张量(Tensor),一种类似Numpy数组的多维数组,其可以在GPU上加速计算。PyTorch提供了自动求导系统简化了神经网络的训练过程。此外,PyTorch还提供了丰富的库和工具,如TorchVision、Torch Text等,方便用户处理图像、文本等数据。
主要特性
-
动态计算图(Dynamic Computation Graph):不同于TensorFlow(1.x)提供的静态计算图框架,PyTorch提供了动态计算图。
- 在运行时构建(根据链式法则跟踪正向传播的每一个操作,方便于自动求导)
- 调试友好:可像普通 Python 代码一样使用
print()、pdb等工具 - 可以根据需要随时修改,其灵活性使得PyTorch在处理复杂模型和调试时更加方便。
- 支持条件分支、循环、递归等复杂控制流
-
自动求导(Autograd):PyTorch的自动求导系统可以自动计算张量的梯度。用户只需定义前向传播过程,PyTorch自动处理反向传播和梯度更新,大大简化了神经网络的实现过程。而自动求导实际依赖于动态计算图。
-
GPU加速:相比NumPy只能在CPU进行,PyTorch利用NVIDIA的CUDA技术极大提高了模型训练速度
-
丰富的库和工具:
- TorchVison:图像数据集、模型、转换工具
- TorchText:已被Transformer替代
- TorchAudio:音频处理
- TorchServe:模型部署服务
-
底层支撑:已成为 Transformer、BERT、GPT、ViT 等现代大模型的标准实现平台
应用领域
视觉领域:RestNET、VGG、YOLO等,以PyTorch实现
自然语言处理:Transformer、BERT、GPT等基于PyTorch开发
强化学习
优势
- 灵活性:
- 易用性:
- 社区支持:大量教程、示例代码、解决方案
- 系统集成:与Python生态无缝集成(如Numpy、Pandas、Matplotlib等)
Tensor(张量)
创建张量
import torch
import numpy as np
# 兼容python
t1 = torch.tensor([[1],[2]]) # 支持列表创建
t2 = torch.tensor((1,2)) # 支持元组创建
t1.tolist() # tensor转为list【仅限cpu设备】
# 兼容Numpy
arr = np.array([1, 2, 3])
t3 = torch.from_numpy(arr) # 接受ndarray,共享内存【仅限cpu设备】
t4 = torch.tensor(arr) # 深拷贝(不共享内存)
arr = t3.numpy() # tensor转为ndarray,共享内存【仅限cpu设备】
数据类型与设备
import torch
# 指定数据类型dtype
t = torch.tensor([1,2], dtype=torch.int16) # 指定数据类型创建
t.type(torch.int16) # 修改数据类型
# 指定设备device(存储位置)
t1 = torch.tensor([1,2]) # 默认存储在"cpu"上
torch.tensor([1,2]).device # 查询存储设备
t2 = torch.tensor([1,2], device="cuda") # 指定存储设备创建
torch.tensor([1,2], device="cuda:0") # 指定存储显卡设备创建,不同设备/显卡内存不共享,无法跨设备操作
print(torch.cuda.device_count()) # 查询GPU数量
print(torch.cuda.get_device_name(0)) # 查询第0块GPU型号
# 提高代码健壮性:检查是否支持"gpu"
device = "cuda" if torch.cuda.is_available() else "cpu" # 支持字符串
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 支持device对象
# 拷贝至指定设备【通常tesnor体积较大,不建议设备迁移】
t1 = t1.to(device)
t1 = t1.cuda() # 拷贝至cuda上
t1 = t1.cpu() # 拷贝至cpu上
# 张量之间必须在相同设备上才能进行运算
try:
print(t1 + t2) # t1,t2存储在不同设备上
except RuntimeError as e:
print(e) # 报错
张量的属性/查询方法
.shape/.size() # 张量形状
.dtype/.type() # 数据类型
.device # 存储设备(CPU/GPU)
.nidm/.dim() # 维度
.numel() # 元素数量
.is_cuda # 是否在cuda上
.requires_grad # 是否需要计算梯度
.grad # 梯度值(反向传播后填充)
张量运算
t1=torch.tensor([[2, 3]])
t2=torch.tensor([[4, 5]])
# 元素级运算
print(t1*t2)
print(torch.mul(t1, t2))
# 矩阵乘法toch.mm()
print(torch.mm(t1.T, t2))
张量操作
# 继承numpy的函数方法
tensor = torch.linspace(0, 10, 100)
tensor = torch.randn(3, 4)
# 切片操作
print(tensor[0],tensor[0, 0]) # 访问指定元素,原内存
print(tensor[:] is tensor) # 实际上是拷贝
print(tensor[:, 0]) # 访问切片,原内存
# 维度重排
print(tensor.transpose(0, 1)) # .transpose(dim0, dim1):指定两个axis交换位置,无所谓顺序
print(tensor.permute(1, 0, 2)) # .permute(*dims):指定**所有**dims顺序进行重排
print(tensor.t()) # .t():用于二维张量转置,大于二维将报错
print(tensor.T) # .T:全维度转置((a,b,c)->(c,b,a))
# 张量内存的连续性
print(tensor.is_contiguous()) # .is_contiguous():检查是否连续
print(tensor.transpose(0, 1).is_contiguous()) # 转置后内存不连续
print(tensor.t().is_contiguous()) # 同上
print(tensor.permute(1, 0, 2).is_contiguous()) # 重拍后内存不连续
print(tensor.transpose(0, 1).contiguous())
# 张量拷贝(没有.copy(),而是.clone())
print(tensor.clone())
张量与Numpy互操作
# tensor -> ndarray
t = torch.randn(2,3)
arr = t.numpy() # 与tensor共享内存
# t.item()获取标量值【仅限1个元素的张量】
t1 = torch.tensor([[2]]) # 仅一个元素的张量可正常提取
t2 = torch.tensor([[2,3]]) # 超过一个元素就会报错
print(t1.item())
try:
print(t2.item())
except RuntimeError as e:
print(e)
# ndarray -> tensor
t = torch.from_numpy(arr) # 与ndarray共享内存
t = torch.tensor(arr) # 拷贝ndarray的数据(通常数据体积较大,拷贝将额外占用内存,不建议使用)
张量变形操作
tensor = torch.randn(2,3,1,2,1)
# 指定shape变形
print(tensor.view(2, 6)) # .view():总是返回尝试返回视图,不复制数据,要求张量在内存是连续的,否则出错
print(tensor.reshape(2, 6)) # .reshape():优先返回视图,若张量不连续,返回新的张量(不要求内存连续)
# 增加维度为1的维度
print(tensor.unsqueeze(0).shape)
print(tensor.unsqueeze(1).shape)
# 删除维度为1的轴
print(tensor.squeeze(0).shape)
print(tensor.squeeze(2).shape)
print(tensor.squeeze().shape)
张量的“就地操作”(in-place operation)
PyTorch命名约定:在函数名末尾加 _(如 zero_(), relu_(), add_() )表示就地操作
tensor.func_() 相当于 tensor = tensor.func()
注意事项:
-
破坏计算图:
requires_grad=True的张量就地操作时,可能无法正确计算梯度x = torch.tensor([1.0], requires_grad=True) y = x ** 2 y.backward(retain_graph=True) print(x.grad) y.sigmoid_() # RuntimeError: a leaf Variable that requires grad is being used in an in-place operation. y.backward() print(x.grad)注意:虽然
torch.sigmoid可以直接对其计算,实际上在神经网络中并非使用torch.sigmoid构建激活函数,而是调用import torch.nn.functional as F中F.sigmoid() -
多变量共享内存
使用建议:
- 仅在明确需要节省内存且确认无梯度依赖时,才使用就地操作(如推理阶段、初始化、数据预处理等)。
- 训练过程中尽量避免就地操作,以防 autograd 报错或梯度错误。
计算图
构成
PyTorch的计算图由节点(Node)和边(Edge)构成:
- 节点:表示张量(Tensor)或操作(Function):
x = torch.tensor(3.0, requires_grad=True)中x是叶子节点y = x**2中的平方操作是函数节点
- 边:用箭头表示节点之间的关系(输入 → 输出):
x的边指向y,表示x为输入,y为输出
手动实现自动梯度计算
def manual_gradient(x, w, b, y_true):
"""手动计算简单线性回归梯度。
"""
y_pred = w * x + b
loss = (y_true - y_pred) ** 2
# loss 对 (y_true - y_pred)求梯度。
dloss = 2 * (y_true - y_pred)
# loss 对 w 求梯度。
dw = dloss * -1 * x
# loss 对 b 求梯度。
db = dloss * -1 * 1
return dw, db
def auto_gradient(x, w, b, y_true):
"""自动计算简单线性回归梯度。
"""
y_pred = w * x + b
loss = (y_true - y_pred) ** 2
# 反向传播,自动计算梯度,保存在张量的grad属性中。
loss.backward()
return w.grad, b.grad
x = torch.tensor(2.0)
# requires_grad:该张量是否在反向传播时,计算梯度,默认为False。
# 如果需要计算梯度(requires_grad值为True),则该张量,包括后续对张量进行的一切操作(计算),
# 都会被计算图所跟踪(放到计算图当中)。
w = torch.tensor(1.0, requires_grad=True)
b = torch.tensor(0.5, requires_grad=True)
y_true = torch.tensor(5.0)
print(x.requires_grad, w.requires_grad)
print(manual_gradient(x, w, b, y_true))
print(auto_gradient(x, w, b, y_true))
import torch
x1 = torch.tensor(1.0, requires_grad=True)
x2 = torch.tensor(2.0, requires_grad=True)
w1 = torch.tensor(1.0, requires_grad=True)
w2 = torch.tensor(2.0, requires_grad=True)
w3 = torch.tensor(3.0, requires_grad=True)
x3 = w1 * x1
x4 = w1 * x2
x5 = w2 * x3 + w3 * x4
L = 1 - x5
L.backward()
print(w1.grad)
# 自动求导(Autograd)
x = torch.tensor(2.0, requires_grad=True)
y = x ** 2
z = y + 3
z.backward() # 计算 dz/dx
print(x.grad) # 输出: tensor(4.0),因为 dz/dx = 2*x = 4
x = torch.tensor(3.0, requires_grad=True)
loss1 = x ** 2
# 第一次计算梯度,梯度存储在.grad属性中。
loss1.backward()
print(x.grad)
loss2 = x ** 3
# 第二次计算梯度,注意,新的梯度不会覆盖原始的梯度,而是累积。
# retain_graph:是否保留计算图,默认为False。
loss2.backward(retain_graph=True)
print(x.grad)
# 如果我们期望的是,新的梯度覆盖原始的梯度,而不是对原始梯度的累加,
# 我们需要将之前的梯度值清零。
x.grad.zero_()
loss2.backward()
print(x.grad)
梯度累积
影响
原因
- 显存限制:当GPU显存不足以处理较大批量时,梯度累积允许我们使用小批量进行累积等效使用更大的批量进行训练
工作原理
我们希望处理的批量大小为N,但我们只能处理小批量大小为M(M<N),则我们可通过累积 K=N/M 个小批量的梯度
禁用梯度追踪
当张量设置 requires_grad = True 时PyTorch会记录对其所有操作构建计算图,并在反向传播时通过链式法则计算梯度。这个过程会:
- 占用额外内存(存储中间结果,如激活值、操作历史);
- 增加计算开销(记录操作、分配图节点)。
未必所有阶段都需要求梯度,因此通过禁用梯度可节省内存和计算资源:
| 场景 | 是否需要梯度? | 是否应使用 no_grad? |
|---|---|---|
| 训练阶段(forward + backward) | ✅ 需要 | ❌ 不要 |
| 验证/测试/推理 | ❌ 不需要 | ✅ 必须 |
| 冻结部分网络 | ❌(对冻结部分) | ✅(或设 requires_grad=False) |
| 生成无梯度的目标(如 EMA、伪标签) | ❌ | ✅ |
| 特征提取、可视化、调试 | ❌ | ✅ |
停用方法:
tensor.requires_grad = Falsewith torch.no_grad()或@torch.no_grad():函数体内的张量禁用梯度追踪。右者更整洁
def func():
with torch.no_grad():
函数体
@torch.no_grad()
def func():
函数体
torch.set_grad_enabled(False):全局禁用梯度追踪tensor_new = tensor.detach():重新创建张量,并设置requires_grad = False,tensor_new与tensor共享内存
二、PyTorch代码实现回归任务
构建数据集
import numpy as np
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
# 定义数据集
def generate_data(n_samples, n_features, noise=0.1,random_state=42, batch_size=32):
X, y = make_regression(n_samples=n_samples, n_features=n_features, noise=noise, random_state=random_state)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=random_state)
return get_DataLoader(X_train, y_train,batch_size), get_DataLoader(X_test, y_test, batch_size,shuffle=False)
def get_DataLoader(X, y, batch_size=32, shuffle=True):
X_tensor = torch.from_numpy(X).float()
y_tensor = torch.from_numpy(y).float()
dataset = TensorDataset(X_tensor, y_tensor)
return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)
# 生成数据集(可迭代对象DataLoader)
train_loader, test_loader = generate_data(10, 5, random_state=0)
# 获取可迭代对象的迭代器。通过迭代次就可以遍历元素。
obj = iter(train_loader)
# 通过next方法,获取迭代次的下一个元素(批次)。
print(next(obj))
# 在实际当中,我们可以通过for循环更加方便的获取元素。for循环会使用try-except自动停止迭代。
for X_batch, y_batch in train_loader:
print(X_batch.shape, y_batch.shape)
定义模型类
class LinearRegressionModel(nn.Module):
def __init__(self, in_features, out_features=1):
super().__init__()
self.linear = nn.Linear(in_features, out_features) # 全连接层
def forward(self, x):
'''实际nn.Module类中定义了__call__方法,函数调用时自动进行self.forward()'''
return self.linear(x) # nn.Module.linear 提供了 y = x@W.T + b
解释
继承 nn.Module 的意义:
-
nn.Module重写了__call__,函数式调用实例化对象返回self.forward()结果class MyModel(nn.Module): def forward(self): return print(55) a = MyModel() print(a() is a.forward()) # 返回True,并先print(55)两次 -
存储中间
-
提供了
.parameters()、.to()、.cuda()等参数控制能力,使手动训练时代码量更少; -
提供了
.state_dict()获取模型状态字典,可用于保存 / 加载模型;torch.save(model.state_dict(), 'ckpt.pth')model.load_state_dict(torch.load('ckpt.pth'))- 这些 API 只认
nn.Module生成的层级化字典。
训练模型
train_loader, test_loader = generate_data(1000, 5, 0)
model = LinearRegressionModel(5)
criterion = nn.MSELoss() # 定义损失函数
optimizer = torch.optim.SGD(model.parameters(), lr=0.01) # 定义优化器:构建参数更新框架,参数 -= lr * grad
for epoch in range(epochs:=30):
# 训练模式:开启Dropout和BatchNorm,用于评估模式
model.train()
train_loss = 0.0
for X_batch, y_batch in train_loader:
# 第一步:前向传播
y_pred = model(X_batch)
# 第二步:计算损失
loss = criterion(y_pred.flatten(), y_batch)
# 第三步:反向传播
optimizer.zero_grad() # 需要梯度清零
loss.backward() # 根据loss损失函数,对模型中所有的参数进行求梯度,将梯度存储在参数的.grad中
optimizer.step() # 执行优化器的梯度更新
train_loss+=loss.item() # 累加每个batch的损失值;loss取出元素避免构建计算团
train_loss/=len(train_loader)
# 评估模式:关闭Dropout和BatchNorm,开启无梯度环境
model.eval()
test_loss = torch.zeros(1)
with torch.no_grad():
for X_batch, y_batch in test_loader:
y_pred = model(X_batch)
loss = criterion(y_pred.flatten(), y_batch)
test_loss.add_(loss.item())
test_loss/=len(test_loader)
print(f'Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.4f}, Test Loss: {test_loss:.4f}')
model.eval()
with torch.no_grad():
X_new = torch.randn(5,5)
y_pred = model(X_new)
print(y_pred)
模型参数
for p in model.parameters():
print(p)
print("-"*20)
for name, p in model.named_parameters():
print(name, p)
print("-"*20)
print(model.state_dict())
print("-"*20)
# 通过访问中间层的成员属性获取参数
print(model.linear.weight)
print(model.linear.bias)
print("-"*20)
print(model.linear.weight.data)
print(model.linear.weight.data.requires_grad)
print(model.linear.bias.data)
三、PyTorch代码实现二分类任务
构建数据集
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
def generate_data(n_samples, n_features, random_state=None):
X, y = make_classification(n_samples, n_features, n_classes=2, random_state=random_state)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=random_state)
train_loader = get_dataloader(X_train, y_train)
test_loader = get_dataloader(X_test, y_test)
return train_loader, test_loader
def get_dataloader(X, y, batch_size=32, shuffle=True):
X_tensor = torch.from_numpy(X).float()
y_tensor = torch.from_numpy(y).float()
dataset = TensorDataset(X_tensor, y_tensor)
return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)
定义模型类
class BinaryModel(nn.Module):
"""用于二分类的神经网络类。
"""
def __init__(self, in_features, out_features=1):
super().__init__()
self.linear = nn.Linear(in_features, out_features)
def forward(self, x):
x = self.linear(x)
# 将输出(logits值)转换为概率值。
# x = F.sigmoid(x)
return x
解释
训练模型
n_samples = 1000
n_features = 5
learning_rate = 0.1
epochs = 30
train_loader, test_loader = generate_data(n_samples, n_features, 0)
model = BinaryModel(n_features)
# 定义损失函数。二分类使用二元交叉熵损失函数。
# BCELoss与BCEWithLogitsLoss都是用于二分类的损失函数,区别在于:
# 前者的输入为概率,后者的输入为logits。
# 因此,后者相当于是Sigmoid + BCELoss。PyTorch官方推荐时候BCEWithLogitsLoss,
# 因为将两个操作合并到一个层,更具有数值稳定性。
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
# 训练与评估模型。
for epoch in range(epochs):
model.train()
train_loss = 0.0
train_correct = 0
for X_batch, y_batch in train_loader:
output = model(X_batch).flatten()
loss = criterion(output, y_batch)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
# 计算正确率。如果使用BCEWithLogitsLoss,则模型输出为logits,使用0为阈值。
# 如果使用BCELoss,则模型输出为概率值,使用0.5为阈值。
y_pred = (output > 0).float()
train_correct += (y_pred == y_batch).sum().item()
train_loss /= len(train_loader)
train_accuracy = train_correct / len(train_loader.dataset)
model.eval()
test_loss = 0.0
test_correct = 0
with torch.no_grad():
for X_batch, y_batch in test_loader:
output = model(X_batch).flatten()
loss = criterion(output, y_batch)
test_loss += loss.item()
y_pred = (output > 0).float()
test_correct += (y_pred == y_batch).sum().item()
test_loss /= len(test_loader)
test_accuracy = test_correct / len(test_loader.dataset)
# 打印每个epoch训练集与验证集的损失与正确率。
print(f"Epoch {epoch + 1}/{epochs},"
f"Train Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.4f} "
f"Test Loss: {test_loss:.4f}, Test Accuracy: {test_accuracy:.4f}"
)
解释:
- 损失函数使用:
nn.BCEWithLogitsLoss而不是nn.BCELoss的原因:- 因为
BCELoss输入需是概率(0~1),而Sigmoid在极端值下梯度接近 0,容易导致梯度消失; BCEWithLogitsLoss使用 log-sum-exp 技巧,数值更稳定。- PyTorch 官方推荐使用
BCEWithLogitsLoss
- 因为
四、PyTorch代码实现多分类任务
构建数据集
定义模型类
训练模型
五、模型的保存与恢复
模型的保存
方式一:保存模型的状态字典(State Dictionary)
-
优点:
- 仅保存模型参数,不保存模型结构
- 存储体积更小,加载更快
-
缺点:
- 需手动定义模型结构
-
注意
方式二:保存整个模型
- 优点:
- 缺点:
保存的模型文件:
-
PyTorch模型文件扩展名:
.pth或.pt -
设备兼容性考虑:
- 如果保存的是状态字典,兼容性好,不担心设备环境问题
- 如果保存的是模型结构,要注意是否模型
保存代码
模型的恢复
六、加载数据集
from torch.utils.data import DataLoader, TensorDataset
X_tensor = torch.from_numpy(X).float()
y_tensor = torch.from_numpy(y).float()
dataset = TenosrDataset(X_tensor, y_tensor)
DataLoader(dataset)
hugging face Datasets专用于加载模型数据。mnist。
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
from datasets import load_dataset
from torchvision import transforms
from transformers import Trainer, TrainingArguments
dataset = load_dataset("minst")
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307, ),(0.3081, ))
])
# 对数据集重命名
dataset = dataset.rename_column("label", 'labels')
dataset = dataset.map(
preprocess_function,
batch_size = 1000,
num_proc = 4,
remove_columns = ['image'],
fn_kwargs = {
"transform":transform
}
)
七、
Trainer训练模型【减少代码重复性工作】【transformers中的Trainer】
未学到:学习率的预热、什么的累积?
数据预处理
参数初始化:
需要随机初始化,若全部初始化相同,实际上神经网络退化成单神经元网络,
定义模型:
class MinistModle(nn.Moudule):
def __init__(self,)
def forward(self,)
训练模型
优化策略
Batch Normalization(BN)
意义:意图解决内部协变量偏移问题,主要动态平滑优化 landscape
- 内部协变量偏移(Internal Covariate Shift, ICS):
- 定义:在深度网络训练过程中,由于前层参数不断更新,导致后层的输入分布持续发生变化,导致优化路径曲折、收敛缓慢,这种现象称为。
- 影响:
- 每一层都在“追赶移动的目标”(chasing a moving target)
- 在深度网络中,每一层的输出都是前若干层参数的复合函数。任何一层的微小更新,都会通过链式法则影响所有后续层的输入分布。这种分布变化不是数据本身的分布漂移(如 domain shift),而是由模型自身参数更新引起的内部动态变化,故称“内部协变量偏移”。
- 优化路径曲折,收敛缓慢
- 梯度下降假设损失函数在局部是平滑的,且当前梯度方向指向极小值。
- 但若每一步更新后,后续层的输入分布剧烈变化,相当于损失函数本身在不断变形。
- 结果:当前计算的梯度可能在下一步已不准确,导致优化轨迹震荡、步长受限。
- 需要更小的学习率以维持稳定
- 大学习率会引发前层参数大幅更新 → 后层输入分布剧变 → 梯度爆炸或发散。
- 为维持稳定,不得不使用较小学习率,牺牲训练速度。
- 对参数初始化高度敏感
- 若初始权重导致某层输出分布极端(如全为0或饱和),后续层难以有效学习。
- 而由于“漂移”效应,错误的初始化更难被后续优化纠正。
- 训练收敛缓慢甚至失败
- 例如 sigmoid 函数在输入绝对值 > 4 时梯度接近 0。若某层输出因前层更新而突然偏移到 [5, 10] 区间,则激活后梯度几乎为零,导致梯度消失,反向信号中断。
- 每一层都在“追赶移动的目标”(chasing a moving target)
- 后续研究(如 Santurkar et al., NeurIPS 2018)指出,BN 的主要增益可能并非直接来自 ICS 的缓解,而是通过平滑损失(smoothing the loss landscape)使梯度方向更一致、Hessian 谱条件数更小,从而显著改善优化动态。但 BN 的工程价值已被广泛验证。
- landscape 优化
- landscape,也称 loss landscape,损失曲面,指损失函数 \(\mathcal L(\theta)\) 在参数空间 \(\theta \in \mathbb R^d\) 上的几何形状。
- 优化目标:找到损失曲面上的全局最低点,即全局最优解;或一个足够深的局部极小值(实用解)
- 曲率(Curvature):由 Hessian 矩阵 \(\mathbf H = \nabla^2 \mathcal L(\theta)\) 描述。曲率大 → 梯度方向易震荡;曲率变化剧烈 → 难以选择合适学习率
- 梯度一致性:不同位置的梯度方向是否指向同一极小值。方向一致 → 快速收敛;方向混乱 → 路径曲折
- 局部最小【鞍点】:实际上损失视图通常不是平滑的,而是由多个凹陷构成的局部最小点构成
- 条件数(Condition Number):Hessian 最大特征值 / 最小特征值。条件数大 → 损失曲面“狭长”,梯度下降呈锯齿状,收敛慢
- 平滑化(smoothing):指通过某种机制(如 BN、权重衰减、特定激活函数等)使 loss landscape 的几何结构变得更“规则”、“缓和”,具体表现为:
- 减小 Hessian 的谱范数(最大特征值)→ 曲率更温和;
- 降低 loss landscape 的局部剧烈波动;
- 使梯度方向在邻域内更一致;
- 改善 Hessian 的条件数(即让曲面更接近“圆形碗”而非“狭长峡谷”)。
- L-Lipschitz 连续:在优化理论中,若损失函数 \(\mathcal L(\theta)\) 是 L-smooth 的,意味着其梯度是 Lipschitz 连续 的,这等价于 Hessian 的谱范数有界:\(\|\mathbf H(\theta)\|_2 \leq L\) ,其中,L 越小 → 函数越“平滑” → 梯度变化越缓慢 → 优化越容易。
数学模型:设模型训练批量训练的一个 mini-batch \(B=\{\mathbf x_1,\mathbf x_2,\dots,\mathbf x_m\},\mathbf x_i\in\mathbb R^d\) ,BN对每个特征维度 \(j\in\{1,2,\dots,d\}\)(注意:BN 通常在通道维度上独立进行)执行标准化 + 仿射变换操作:
-
标准化:\(\hat x_i^{(j)} = \frac{x_i^{(j)}-\mu_B^{(j)}}{\sqrt{\sigma_B^{2(j)}+\epsilon}}\\\) ,其中:
- \(\mu_B^{(j)} = \frac 1m \sum_{i=1}^m x_i^{(j)}\\\)
- \(\sigma_B^{2(j)} = \frac 1m \sum_{i=1}^m (x_i^{(j)} - \mu_B^{(j)})^2\\\)
- \(\epsilon\) 为数值稳定项,通常取极小的正数(如 \(10^{-5}\) ),防止分母(标准差)为零
-
仿射变换:\(\mathbf{BN_{\gamma,\beta}}(x_i^{(j)}) = \gamma^{(j)} \hat x_i^{(j)} + \beta^{(j)}\) ,其中 \(\gamma\) 和 \(\beta\) 为可学习参数
- 若 \(\gamma = \sqrt{\sigma^2},\beta = \mu\) ,则 BN 退化为恒等映射——不会降低模型容量。
特性:
-
优势
- 解耦参数尺度与方向:仿射参数 γ,β 允许网络学习尺度,而标准化部分控制分布形状;
- 限制激活值的尺度,缓解深度网路的梯度消失问题:
- 随着网络的层数加深,每一层的输入分布会发生变化,导致训练不稳定【受激活函数影响,激活函数对分布敏感】
- BN对每层输入数据去中心归一化处理,拉向近似标准正态分布(\(\mathcal N(0,1)\)),使得每层网络输入数据的均值和方差限制在一定范围内,减少ICS的影响,使得激活前将输入维持在非饱和区,缓解梯度消失问题【激活函数如sigmoid、tanh等容易出现梯度消失的问题】
- 有效减小局部 Lipschitz 常数,实现“动态平滑”,从而允许更大的学习率,提高训练速度
- 因为BN稳定了每一层网格的输入分布,网格参数的更新更稳定,平滑 landscape 优化,使优化路径更平滑,高学习率下不易发散,因此可使用更高的学习率进行训练【优化路径不稳定时,高学习率容易产生震荡,训练效率低或失败,通常为此采用更低的学习率】,更高的学习率使得训练速度更快
- 引入batch噪声,提高泛化能力:从batch的均值和标准差引入了小批量样本的噪声【更宽的局部最小】,起到了一定的正则化效果,提高了模型的鲁棒性;
- 减少对初始参数的依赖
- 传统深度网络对 Xavier/He 初始化高度依赖;BN使得每层输入数据分布相对稳定,BN消除了层间尺度爆炸/消失,降低了网格对参数初始化的敏感性,增强了模型的鲁棒性
- 即使使用了不同的参数初始化方式,BN仍能使网络快速收敛到较好的解
- 提高sigmoid等饱和激活函数在深层网络中的兼容性
- 饱和激活函数使输出限制在一定范围内,容易陷入饱和区,BN使得其在深层神经网络中发挥更好的作用
-
注意事项:
- Batch Size敏感:Batch Size不能过小,引入噪声过大,需控制当前批量均值和标准差与全局的偏移量;batch size 较大时,正则化效果微弱
- 训练模式、评估模式、推理模式存在行为差异
- 训练时:使用 batch-level 的均值和方差
- 评估时:使用固定均值和方差
- 推理时:使用指数移动平均(EMA)累积的全局统计量
-
其他归一化方法:
- GroupNorm:在 channel 维度分组后归一化,不依赖 batch,因此可特别地处理小批量任务
- LayerNorm(适用于 RNN/Transformer):对单个样本的所有特征归一化
- WeightNorm:对权重向量归一化
- SyncBatchNorm(多卡同步统计量)
- InstanceNorm:对单个样本的每个通道归一化
-
其他平滑 landscape 的方法:但 BN 是最通用、最有效、计算开销最小的平滑化手段之一。
- Weight Decay(L2 正则):抑制参数过大,间接平滑;
- Gradient Clipping:防止梯度爆炸,稳定更新;
- Smooth Activations(如 Swish, GELU):比 ReLU 更平滑,减少非线性突变;
- Learning Rate Warmup:初期小学习率让 landscape “预热”平滑;
- Second-order Methods(如 K-FAC):显式利用曲率信息调整更新方向。
-
不适合场景:
- 序列建模(RNN/Transformer):序列长度可变,batch内样本长度不一,统计量难对齐;BN 难以处理变长序列,通常用 LayerNorm;
- 生成模型(GAN):batch 内样本相互影响,BN 可能导致生成样本多样性下降;
- 在线学习/小样本:无法获得可靠 batch 统计量
Dropout随机失活
介绍:随机使神经元失活。一种正则化技术,主要用于防止模型过拟合,提高模型的泛化能力。
意义:在训练过程中,以一定概率随机“关闭”一部分神经元,迫使网络不依赖于任何单个神经元或特定神经元组合,从而提升泛化能力。
-
共适应
- 在标准训练中,某些神经元可能学会“搭便车”——仅在特定其他神经元激活时才有效;
- 网络可能发展出高度耦合的特征检测器,对训练数据拟合极好,但对新样本脆弱;
- 这种过度特化的协作模式即为“共适应”,是过拟合的重要来源
-
Dropout 通过引入训练时的随机扰动,打破这种依赖,鼓励每个神经元学习鲁棒、独立、可泛化的特征。
数学模型:设某一层的激活输出为向量 \(\mathbf a \in \mathbb R^n\) 。Dropout 操作如下
-
采样掩码(mask):按概率 \(p\) 随机失活神经元
\[\mathbf m \sim \text{Bernoulli}(p)^n \]其中:
- \(\mathbf m\in \{0, 1\}^n\) 为随机二值向量
- \(p \in [0,1]\) 是保留概率(keep probability)【通常 \(p\) 取 [0.5, 0.9] 】,即每个神经元以概率 \(p\) 被保留,以概率 \(1-p\) 被置零。【在PyTorch中 \(p\) 为失活率,此处 \(p\) 采用论文中的保留率】
-
应用掩码并缩放(scale):对保留神经元进行缩放(放大)
\[\tilde{\mathbf a} = \frac 1p \cdot \mathbf a \odot \mathbf m \]其中:
- 训练时期望:\(\mathbb E[\tilde{\mathbf a}]=\mathbf a\) ,而推理时无 dropout;因此缩放是必要的,否则激活值比训练时大 ( 1/p ) 倍,导致分布偏移。
模型解释:
- 模型视角
- 每次前向传播相当于从原始网络中随机采样一个子网络(subnetwork)
- 总共有 \(2^n\) 种可能的子网络(n 为神经元数)
- 训练过程等价于同时训练大量共享权重的子网络
- 推理时,相当于对这些子网络的预测进行加权平均(因权重共享,实际是隐式平均)
- 正则化视角
- 对线性模型,Dropout 的期望损失可等价于在权重上施加自适应 L2 正则项:\(\mathcal{L}_{\text{dropout}} \approx \mathcal{L} + \lambda \sum_{i,j} w_{ij}^2 a_j^2\) ,其中正则强度依赖于输入激活 \(a_j\),具有数据依赖性。
- 在非线性网络中,Dropout 引入的正则化比 L2 更复杂,能抑制高阶交互。
- 信息瓶颈视角(现代观点)
- Dropout 限制了每层可传递的信息量,迫使网络压缩有用信息,丢弃冗余或噪声特征
- 这与信息瓶颈理论(Information Bottleneck)一致:好的表示应在压缩与预测之间取得平衡
特性
- 优势
- 强效防过拟合:尤其在小数据集、大模型(如 MLP、小型 CNN)上效果显著
- 调参工作轻,实现简单,计算开销低(仅 mask + scale)
- 提升模型鲁棒性:对输入噪声、对抗扰动有一定抵抗力(因不依赖特定路径)
- 隐式集成效应
- 注意事项
- 失活率 \(p\) 的调参工作
- 全连接层常用 \(p = 0.5\) ;CNN 中常用 \(p = 0.8\)
- p 过小 → 正则化效果微弱;p 过大 → 信息丢失过多,导致欠拟合
- BN与dropout矛盾,叠加使用会导致过度正则
- RNN/Transformer通常不在隐藏状态上用 Dropout(会破坏时序依赖),而用于 embedding 或 FFN 层;
- 只在训练模式工作,在推理模式不工作;若推理模式下启用,导致输出随机波动
- 失活率 \(p\) 的调参工作
- 其他神经元失活方法
- Spatial Dropout: 在 CNN 中按通道整块失活(而非单个像素),防止通道间共适应,适用于图像
- DropPath(Stochastic Depth):在 ResNet 中随机跳过整个残差块,适用于训练极深网络(如 ViT、EfficientNet)
- Variational Dropout: 为每个权重/神经元学习独立的失活率,适用于贝叶斯神经网络、自动剪枝
- Targeted Dropout:优先失活重要性低的神经元,适用于模型压缩、剪枝预处理
- 调试建议
- 监控训练/验证 loss gap:若 gap 大,可尝试增加 Dropout
- 消融实验:关闭 Dropout 后若验证性能显著下降,则说明有效
- 可视化激活:使用 Grad-CAM 等工具观察是否注意力更分散(理想情况)
浙公网安备 33010602011771号