Python-pytorch-高级技巧

PyTorch 高级技巧

🪝 钩子函数(Hooks)

Hooks 让你在不修改 forward() 代码的情况下,在模型的前向/反向传播中插入自定义逻辑。非常适合调试、特征提取、梯度分析和可视化。

前向钩子(Forward Hook)

python 复制代码
# 注册前向钩子
def forward_hook(module, input, output):
    """module: 被 hook 的层
       input: 输入元组
       output: 输出张量"""
    print(f'{module.__class__.__name__}: input shape {input[0].shape}')
    print(f'  → output shape {output.shape}')
    # 可以保存输出用于特征可视化

# 注册到某一层
handle = model.layer3.register_forward_hook(forward_hook)

# 用完记得移除
handle.remove()

反向钩子(Backward Hook)

python 复制代码
# 查看梯度统计
def backward_hook(module, grad_input, grad_output):
    """检查梯度是否正常"""
    for i, g in enumerate(grad_output):
        if g is not None:
            print(f'  grad_output[{i}]: mean={g.mean():.6f}, std={g.std():.6f}, '
                  f'nan={torch.isnan(g).any()}, inf={torch.isinf(g).any()}')

handle = model.fc2.register_full_backward_hook(backward_hook)

参数钩子

python 复制代码
# 监控参数更新
def param_hook(grad):
    return grad * 0.5   # 缩放梯度

model.fc1.weight.register_hook(param_hook)

实战:特征提取

python 复制代码
# 提取中间层特征(不改模型代码)
features = {}
def get_features(name):
    def hook(model, input, output):
        features[name] = output.detach()
    return hook

model.layer1.register_forward_hook(get_features('layer1'))
model.layer4.register_forward_hook(get_features('layer4'))

# 前向传播后
output = model(input_image)
print(features['layer1'].shape)  # 浅层特征
print(features['layer4'].shape)  # 深层特征

# 可视化特征图
import matplotlib.pyplot as plt
feat = features['layer1'][0]  # batch 第一个样本
fig, axes = plt.subplots(4, 8, figsize=(12, 6))
for i, ax in enumerate(axes.flat):
    if i < feat.shape[0]:
        ax.imshow(feat[i].cpu(), cmap='viridis')
    ax.axis('off')

实战:梯度裁剪 + 监控

python 复制代码
def grad_clip_and_log(module, grad_input, grad_output):
    """在反向传播时自动裁剪并记录梯度"""
    for g in grad_output:
        if g is not None:
            torch.nn.utils.clip_grad_norm_(g, max_norm=1.0)
            print(f'Grad norm: {g.norm():.4f}')

handle = model.register_full_backward_hook(grad_clip_and_log)

🔍 性能分析(Profiling)

使用 PyTorch Profiler

python 复制代码
from torch.profiler import profile, record_function, ProfilerActivity

with profile(
    activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
    schedule=torch.profiler.schedule(wait=1, warmup=1, active=3, repeat=1),
    on_trace_ready=torch.profiler.tensorboard_trace_handler('./log/profiler'),
    record_shapes=True,
    profile_memory=True,
    with_stack=True
) as prof:
    for step, (X, y) in enumerate(train_loader):
        with record_function("forward"):
            output = model(X)
            loss = loss_fn(output, y)
        with record_function("backward"):
            loss.backward()
        with record_function("update"):
            optimizer.step()
        prof.step()
        if step >= 5:
            break

# 查看结果
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10))

# 在 TensorBoard 中查看
# tensorboard --logdir=./log/profiler

简单计时

python 复制代码
import time

# CPU 计时
start = time.time()
output = model(input)
print(f'Inference: {time.time() - start:.4f}s')

# GPU 同步计时(更准确)
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)

start.record()
output = model(input)
end.record()
torch.cuda.synchronize()
print(f'GPU Inference: {start.elapsed_time(end):.2f}ms')

单层耗时分析

python 复制代码
# 统计每层的 FLOPs 和参数量
# pip install fvcore
from fvcore.nn import FlopCountAnalysis, parameter_count

input_tensor = torch.randn(1, 3, 224, 224)
flops = FlopCountAnalysis(model, input_tensor)
params = parameter_count(model)

print(f'Total FLOPs: {flops.total() / 1e9:.2f} G')
print(f'Total Params: {params[""] / 1e6:.2f} M')

🔢 模型量化(Quantization)

量化将模型从 FP32 降到 INT8,大幅减少模型体积和推理延迟。

动态量化(最简单)

python 复制代码
# 只量化 Linear 和 LSTM 层的权重(激活保持 FP32)
quantized_model = torch.quantization.quantize_dynamic(
    model,                         # 原始模型
    {nn.Linear, nn.LSTM},          # 要量化的层类型
    dtype=torch.qint8              # 量化目标类型
)

# 适用于 LSTM、Transformer(对 CNN 效果一般)

静态量化(更高效)

python 复制代码
# 1. 设置量化配置
model.qconfig = torch.ao.quantization.get_default_qconfig('x86')
# 或 'fbgemm'(x86 CPU),'qnnpack'(ARM CPU)

# 2. 插入观察器
model_prepared = torch.ao.quantization.prepare(model)

# 3. 用校准数据运行(收集激活分布)
with torch.no_grad():
    for X, _ in calibration_loader:
        model_prepared(X)

# 4. 转换为量化模型
model_quantized = torch.ao.quantization.convert(model_prepared)

PyTorch 2.0 torch.compile 量化(推荐)

python 复制代码
# 更简单:torch.compile 内置量化优化
model = torch.compile(model, mode='reduce-overhead')
# 或
from torch.ao.quantization import quantize_pt2e
model = quantize_pt2e(model, ...)   # 当前实验性

🔌 自定义算子(Custom C++/CUDA Extension)

Python 层面(torch.autograd.Function)

python 复制代码
class MyReLU(torch.autograd.Function):
    @staticmethod
    def forward(ctx, input):
        """前向传播"""
        ctx.save_for_backward(input)   # 保存用于反向传播
        return input.clamp(min=0)

    @staticmethod
    def backward(ctx, grad_output):
        """反向传播"""
        input, = ctx.saved_tensors
        grad_input = grad_output.clone()
        grad_input[input < 0] = 0
        return grad_input

# 使用
relu_custom = MyReLU.apply
x = torch.randn(5, requires_grad=True)
y = relu_custom(x)

示例: GELU 自定义实现

python 复制代码
class GELU(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x):
        ctx.save_for_backward(x)
        return 0.5 * x * (1 + torch.tanh(
            (2 / torch.pi) ** 0.5 * (x + 0.044715 * x ** 3)))

    @staticmethod
    def backward(ctx, grad_output):
        x, = ctx.saved_tensors
        # GELU 的导数
        cdf = 0.5 * (1 + torch.erf(x / (2 ** 0.5)))
        pdf = torch.exp(-0.5 * x ** 2) / ((2 * torch.pi) ** 0.5)
        return grad_output * (cdf + x * pdf)

gelu = GELU.apply

📜 TorchScript --- 序列化与跨语言部署

Tracing(追踪)vs Scripting(脚本)

python 复制代码
# Tracing: 喂入样例输入,记录执行路径(不支持控制流)
class SimpleModel(nn.Module):
    def forward(self, x):
        return x * 2 + 1

model = SimpleModel()
example = torch.randn(1, 3)
traced = torch.jit.trace(model, example)

# Scripting: 分析 Python 代码(支持控制流)
@torch.jit.script
def my_loop(x):
    for i in range(10):
        x = x + i
    return x

class ModelWithControl(nn.Module):
    def forward(self, x, n):
        if n > 0:             # ← 控制流
            x = x * 2
        return x

scripted = torch.jit.script(ModelWithControl())

保存与加载

python 复制代码
# 保存
traced.save('model_traced.pt')

# 加载(无需 Python 代码!)
model = torch.jit.load('model_traced.pt')

# 在 C++ 中使用
# module = torch::jit::load("model_traced.pt");

🏗️ 参数共享与绑定

python 复制代码
# 让两个层共享参数(权重绑定)
class SharedModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.encoder = nn.Linear(100, 50)
        self.decoder = nn.Linear(50, 100)

        # 绑定 decoder 权重 = encoder 权重的转置
        self.decoder.weight = self.encoder.weight  # 共享参数!

model = SharedModel()
# encoder.weight 和 decoder.weight 是同一个张量

🧪 torch.inference_mode() vs torch.no_grad()

python 复制代码
# torch.no_grad(): 禁用梯度计算,但仍追踪 view/reshape
with torch.no_grad():
    y = model(x)

# torch.inference_mode(): 更激进,完全禁用 autograd(推荐推理用)
with torch.inference_mode():
    y = model(x)

# 区别: inference_mode 更快、更省内存,但不能同时和 autograd 混用

📐 动态 Padding 批处理

python 复制代码
# 处理变长输入的高效方案
from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pad_packed_sequence

# 变长序列
seqs = [torch.randn(3, 128), torch.randn(5, 128), torch.randn(2, 128)]
lengths = torch.tensor([3, 5, 2])

# Padding
padded = pad_sequence(seqs, batch_first=True)  # (3, 5, 128)

# 打包(跳过 padding 的计算)
packed = pack_padded_sequence(padded, lengths, batch_first=True, enforce_sorted=False)

# RNN 前向传播
output_packed, (h, c) = lstm(packed)

# 解包
output, _ = pad_packed_sequence(output_packed, batch_first=True)

🗑️ 自定义 nn.Module 的高级特性

python 复制代码
class AdvancedModule(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(10, 10)

        # 非参数缓冲区(随模型移动,但不参与训练)
        self.register_buffer('running_mean', torch.zeros(10))

        # 常量(不会被 state_dict 保存)
        self.register_buffer('const', torch.tensor([1.0]), persistent=False)

    def extra_repr(self):
        """自定义 print(model) 的输出"""
        return 'special_config=42'

    def train(self, mode=True):
        """自定义训练/评估模式切换"""
        super().train(mode)
        print(f'Switched to {"train" if mode else "eval"} mode')

🧩 nn.ModuleListnn.ModuleDict

python 复制代码
# ModuleList: 按索引访问(类似 Python list)
layers = nn.ModuleList([
    nn.Linear(10, 20) for _ in range(5)
])
x = layers[0](x)    # 可以索引访问
x = layers[1](x)

# ModuleDict: 按键名访问(类似 Python dict)
blocks = nn.ModuleDict({
    'encoder': nn.Linear(10, 5),
    'decoder': nn.Linear(5, 10),
})
x = blocks['encoder'](x)

# 关键: 它们的参数会被正确注册,optimizer 和 .to() 都会生效
# 而普通的 Python list/dict 里的 Module 不会自动注册!

🎛️ 可微分数据增强

python 复制代码
# kornia: 在 GPU 上可微分的数据增强
# pip install kornia
import kornia.augmentation as K

augs = nn.Sequential(
    K.RandomHorizontalFlip(p=0.5),
    K.RandomRotation(degrees=15.0),
    K.ColorJitter(0.2, 0.2, 0.2, 0.1),
)

# 直接在训练循环中 GPU 上执行(比 torchvision transforms 快得多)
x_aug = augs(x)  # x 在 GPU 上,结果也在 GPU 上

🔧 调试技巧

python 复制代码
# 1. 检测 NaN/Inf
if torch.isnan(loss):
    print('NaN detected!')
    break
if torch.isinf(loss):
    print('Inf detected!')
    break

# 2. 异常检测(开发时开启,会明显变慢)
torch.autograd.set_detect_anomaly(True)

# 3. 打印所有参数的梯度范数
total_norm = 0.0
for p in model.parameters():
    if p.grad is not None:
        total_norm += p.grad.data.norm(2).item() ** 2
print(f'Gradient norm: {total_norm ** 0.5:.4f}')

# 4. 设置断点调试反向传播
with torch.autograd.profiler.emit_nvtx():
    loss.backward()  # 可在 Nsight Systems 中查看

# 5. 查看每层输出的统计
for name, module in model.named_modules():
    def hook(name):
        def fn(module, input, output):
            if isinstance(output, torch.Tensor):
                print(f'{name}: {output.mean():.4f} ± {output.std():.4f}')
        return fn
    module.register_forward_hook(hook(name))

📝 速查表

需求 代码
前向钩子 module.register_forward_hook(fn)
反向钩子 module.register_full_backward_hook(fn)
推理模式 with torch.inference_mode():
Profiler with profile(...) as prof:
动态量化 torch.quantization.quantize_dynamic(model, ...)
自定义算子 class MyFunc(torch.autograd.Function):
Tracing torch.jit.trace(model, example)
Scripting torch.jit.script(model)
注册 buffer self.register_buffer('name', tensor)
ModuleList nn.ModuleList([...])
NaN 检测 torch.isnan(tensor)
异常检测 torch.autograd.set_detect_anomaly(True)
参数共享 直接赋值 .weight = other.weight

\[pytorch-总览\|← 返回总览\]

相关推荐
用户8356290780511 小时前
如何使用 Python 将 PowerPoint 转换为 PDF 文档
后端·python
lpfasd1231 小时前
python webview打包版卡死、开发版正常
开发语言·python
用户8356290780511 小时前
如何使用 Python 为 PowerPoint 幻灯片添加切换效果
后端·python
weixin_461408581 小时前
npm npx yarn
开发语言·前端·javascript
菜冻鱼1 小时前
Python-pytorch-模型保存与加载
开发语言·人工智能·pytorch·python·深度学习·机器学习
骇客野人1 小时前
Java SSO 统一认证方案
java·开发语言
程序员cxuan2 小时前
Claude Code :如何最大化你的 Session 价值
人工智能·后端·程序员
亿牛云爬虫专家2 小时前
爬虫调度系统设计:用 Celery 实现按媒体权重动态调整的抓取频率控制
爬虫·python·隧道代理·舆情采集·媒体权重·频率控制·ip自动轮换
MatrixOrigin2 小时前
MatrixOne Git4Data 技术详解(十一)·大模型篇:SFT 数据 curation——可审计、可复现的数据清洗
人工智能·矩阵起源·数据底座·git4data