从零到一跑通第一个训练脚本

完整的训练循环、损失函数/优化器选择、训练模式切换、梯度累积等核心内容。

[toc]

训练流程全解析


🔄 完整训练循环

for epoch in range(num_epochs):
    # ===== 训练阶段 =====
    model.train()
    for batch_x, batch_y in train_loader:
        # 1. 前向传播
        outputs = model(batch_x)
        
        # 2. 计算损失
        loss = criterion(outputs, batch_y)
        
        # 3. 反向传播
        optimizer.zero_grad()  # 清除旧梯度
        loss.backward()        # 计算梯度
        
        # 4. 更新参数
        optimizer.step()       # 参数更新
    
    # ===== 验证阶段 =====
    model.eval()
    with torch.no_grad():
        for batch_x, batch_y in val_loader:
            outputs = model(batch_x)
            val_loss = criterion(outputs, batch_y)

1️⃣ 完整训练脚本 (模板)

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from tqdm import tqdm
import numpy as np
 
def train_one_epoch(model, dataloader, criterion, optimizer, device):
    """训练一个 epoch"""
    model.train()
    total_loss = 0
    correct = 0
    total = 0
    
    pbar = tqdm(dataloader, desc="Training")
    for batch_x, batch_y in pbar:
        batch_x, batch_y = batch_x.to(device), batch_y.to(device)
        
        # 前向传播
        outputs = model(batch_x)
        loss = criterion(outputs, batch_y)
        
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        # 统计
        total_loss += loss.item()
        _, predicted = outputs.max(1)
        total += batch_y.size(0)
        correct += predicted.eq(batch_y).sum().item()
        
        pbar.set_postfix({
            'loss': f'{loss.item():.4f}',
            'acc': f'{100.*correct/total:.2f}%'
        })
    
    return total_loss / len(dataloader), 100. * correct / total
 
 
def validate(model, dataloader, criterion, device):
    """验证一个 epoch"""
    model.eval()
    total_loss = 0
    correct = 0
    total = 0
    
    with torch.no_grad():
        for batch_x, batch_y in tqdm(dataloader, desc="Validating"):
            batch_x, batch_y = batch_x.to(device), batch_y.to(device)
            
            outputs = model(batch_x)
            loss = criterion(outputs, batch_y)
            
            total_loss += loss.item()
            _, predicted = outputs.max(1)
            total += batch_y.size(0)
            correct += predicted.eq(batch_y).sum().item()
    
    return total_loss / len(dataloader), 100. * correct / total
 
 
def main():
    # 超参数
    BATCH_SIZE = 64
    EPOCHS = 50
    LEARNING_RATE = 1e-3
    DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    
    # 准备数据(以 MNIST 为例)
    from torchvision import datasets, transforms
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])
    
    train_dataset = datasets.MNIST(root='./data', train=True, 
                                   download=True, transform=transform)
    val_dataset = datasets.MNIST(root='./data', train=False, 
                                  download=True, transform=transform)
    
    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, 
                              shuffle=True, num_workers=4)
    val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, 
                            shuffle=False, num_workers=4)
    
    # 创建模型
    from model import SimpleCNN
    model = SimpleCNN(num_classes=10).to(DEVICE)
    
    # 损失函数和优化器
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
        optimizer, mode='min', factor=0.5, patience=3
    )
    
    # 训练循环
    best_acc = 0
    for epoch in range(1, EPOCHS + 1):
        print(f"\nEpoch {epoch}/{EPOCHS}")
        
        train_loss, train_acc = train_one_epoch(
            model, train_loader, criterion, optimizer, DEVICE
        )
        val_loss, val_acc = validate(
            model, val_loader, criterion, DEVICE
        )
        
        scheduler.step(val_loss)
        
        print(f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.2f}%")
        print(f"Val   Loss: {val_loss:.4f} | Val   Acc: {val_acc:.2f}%")
        
        # 保存最佳模型
        if val_acc > best_acc:
            best_acc = val_acc
            torch.save(model.state_dict(), 'best_model.pth')
            print(f"✅ 新最佳模型保存! Acc: {best_acc:.2f}%")
 
if __name__ == "__main__":
    main()

2️⃣ 核心组件的选择策略

损失函数

# 分类任务
criterion = nn.CrossEntropyLoss()           # 多分类 (最常用)
criterion = nn.BCEWithLogitsLoss()          # 多标签分类
criterion = nn.BCELoss()                    # 二分类 (配合 sigmoid)
 
# 回归任务
criterion = nn.MSELoss()                    # 均方误差
criterion = nn.L1Loss()                     # 平均绝对误差
 
# 特殊任务
criterion = nn.KLDivLoss()                  # 蒸馏/分布匹配
criterion = nn.CTCLoss()                    # 语音/OCR

优化器

# 新手推荐
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
 
# 进阶
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
optimizer = torch.optim.RMSprop(model.parameters(), lr=1e-3)

学习率调度器

# 常用调度器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, mode='min', factor=0.5, patience=5
)
 
# 预热 + 余弦退火 (训练 LLM 常见)
scheduler = torch.optim.lr_scheduler.LambdaLR(
    optimizer, 
    lr_lambda=lambda epoch: min(epoch / warmup_epochs, 1.0) 
                           if epoch < warmup_epochs 
                           else 0.5 * (1 + cos(pi * epoch / total_epochs))
)

3️⃣ 训练模式 vs 评估模式

model.train()
# BatchNorm 使用当前 batch 统计量
# Dropout 生效,随机丢弃神经元
 
model.eval()
# BatchNorm 使用训练集积累的统计量
# Dropout **关闭**,全部神经元参与

⚠️ 忘记切换 eval 模式是新手最常见的 bug!


4️⃣ 梯度累积 (大模型必备技巧)

当显存不够,又想要大 batch size 时:

accumulation_steps = 4  # 等效 batch size = 32 * 4 = 128
optimizer.zero_grad()
 
for i, (inputs, labels) in enumerate(train_loader):
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    loss = loss / accumulation_steps  # 归一化
    loss.backward()
    
    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

🔗 下一篇

超参数调优