Day33 GPU及call方法

核心知识点梳理

1. GPU 训练核心概念

表格

知识点 说明
设备指定(device) 通过torch.device("cuda" if torch.cuda.is_available() else "cpu")自动选择 GPU/CPU
.to(device)方法 - 张量:返回新的设备上的张量(原张量不变)- 模型:直接修改模型参数的设备(无返回值)
设备一致性要求 模型、输入张量、标签张量必须在同一设备,否则报RuntimeError
常见错误原因 1. 模型在 GPU,张量在 CPU;2. 部分张量迁移设备,部分未迁移;3. 忘记迁移标签张量
2. __call__魔术方法核心概念

表格

知识点 说明
核心作用 让类的实例可以像函数一样被调用(instance()等价于instance.__call__()
内部逻辑 调用实例时自动执行__call__方法,可结合__init__保存的状态实现灵活逻辑
典型应用场景 1. 自定义模型封装(模拟 nn.Module 的调用逻辑);2. 带状态的函数(如计数器)
__init__的区别 __init__在实例化时执行(初始化),__call__在实例调用时执行(核心逻辑)
python 复制代码
pip install torch torchvision
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import TensorDataset, DataLoader

# ==================== 知识点1:GPU训练核心实现 ====================
print("=== 1. GPU训练核心演示 ===")

# 步骤1:检测并指定计算设备
# 自动判断是否有可用GPU,无则用CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"当前使用设备:{device}")
# 打印GPU信息(如有)
if torch.cuda.is_available():
    print(f"GPU名称:{torch.cuda.get_device_name(0)}")
    print(f"GPU数量:{torch.cuda.device_count()}")

# 步骤2:准备示例数据(回归任务)
# 生成模拟数据:y = 2x1 + 3x2 + 4(带噪声)
torch.manual_seed(42)  # 固定随机种子
X = torch.randn(1000, 2)  # 特征:1000样本,2特征
y = 2*X[:,0] + 3*X[:,1] + 4 + 0.1*torch.randn(1000)  # 标签
y = y.unsqueeze(1)  # 转为(1000,1)维度

# 步骤3:数据迁移到指定设备(关键!)
# 注意:张量的.to(device)返回新张量,需重新赋值
X = X.to(device)
y = y.to(device)

# 步骤4:定义简单的线性回归模型
class LinearModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(2, 1)  # 2输入→1输出
    
    def forward(self, x):
        return self.linear(x)

# 步骤5:模型迁移到指定设备(关键!)
model = LinearModel().to(device)  # 模型.to(device)直接修改自身

# 步骤6:定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 步骤7:训练模型(确保所有数据/模型在同一设备)
epochs = 50
model.train()
for epoch in range(epochs):
    # 前向传播
    y_pred = model(X)
    loss = criterion(y_pred, y)
    
    # 反向传播+优化
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    # 打印进度
    if (epoch+1) % 10 == 0:
        print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")

# 验证模型参数(接近2,3,4)
print("\n模型训练后参数:")
for name, param in model.named_parameters():
    print(f"{name}: {param.data.cpu().numpy().squeeze()}")

# ==================== 知识点2:常见GPU错误演示 ====================
print("\n=== 2. 常见GPU错误演示 ===")
# 错误场景1:模型在GPU,张量在CPU
model_gpu = LinearModel().to("cuda" if torch.cuda.is_available() else "cpu")
X_cpu = torch.randn(10, 2)  # 故意留在CPU
try:
    model_gpu(X_cpu)
except RuntimeError as e:
    print(f"错误1(设备不一致):{e.args[0][:100]}...")

# 错误场景2:部分张量迁移,部分未迁移
X_gpu = X_cpu.to(device)
y_cpu = torch.randn(10, 1)  # 标签留在CPU
try:
    y_pred = model_gpu(X_gpu)
    loss = criterion(y_pred, y_cpu)
except RuntimeError as e:
    print(f"错误2(标签设备不一致):{e.args[0][:100]}...")

# 正确做法:所有张量都迁移
X_gpu = X_cpu.to(device)
y_gpu = y_cpu.to(device)
y_pred = model_gpu(X_gpu)
loss = criterion(y_pred, y_gpu)
print("修复后:计算正常,损失值 =", loss.item())

# ==================== 知识点3:__call__方法核心实现 ====================
print("\n=== 3. __call__方法核心演示 ===")

# 示例1:带状态的计数器(基础用法)
class Counter:
    def __init__(self, start=0):
        self.count = start  # 初始化状态
    
    def __call__(self, step=1):
        # 调用实例时执行的逻辑
        self.count += step
        return self.count

# 使用__call__方法
counter = Counter(start=5)
print(f"初始计数:{counter.count}")
print(f"调用counter():{counter()}")  # 等价于counter.__call__()
print(f"调用counter(3):{counter(3)}")
print(f"最终计数:{counter.count}")

# 示例2:模拟nn.Module的__call__逻辑(进阶)
class CustomModel:
    def __init__(self, in_dim, out_dim):
        # 初始化模型参数(模拟nn.Linear)
        self.weight = torch.randn(out_dim, in_dim).to(device)
        self.bias = torch.randn(out_dim).to(device)
        self.in_dim = in_dim
        self.out_dim = out_dim
    
    def forward(self, x):
        # 前向传播逻辑(模拟nn.Module的forward)
        return x @ self.weight.T + self.bias
    
    def __call__(self, x):
        # 调用实例时自动执行:数据校验 + 前向传播
        # 1. 数据设备校验
        if x.device != self.weight.device:
            x = x.to(self.weight.device)
            print(f"自动将输入迁移到{self.weight.device}")
        # 2. 维度校验
        if x.shape[1] != self.in_dim:
            raise ValueError(f"输入维度错误!预期{self.in_dim},实际{x.shape[1]}")
        # 3. 执行前向传播
        return self.forward(x)

# 使用自定义模型(模拟nn.Module的调用方式)
custom_model = CustomModel(in_dim=2, out_dim=1)
test_x = torch.randn(5, 2)  # CPU张量
# 调用实例(自动触发__call__)
output = custom_model(test_x)
print(f"\n自定义模型输出:\n{output.cpu().detach().numpy()}")

# 示例3:结合GPU训练的__call__应用
class Trainer:
    def __init__(self, model, criterion, optimizer, device):
        self.model = model
        self.criterion = criterion
        self.optimizer = optimizer
        self.device = device
        self.loss_history = []
    
    def __call__(self, dataloader, epochs=10):
        # 调用Trainer实例时执行训练逻辑
        self.model.train()
        for epoch in range(epochs):
            total_loss = 0.0
            for X_batch, y_batch in dataloader:
                # 确保批次数据在指定设备
                X_batch = X_batch.to(self.device)
                y_batch = y_batch.to(self.device)
                
                # 训练步骤
                y_pred = self.model(X_batch)
                loss = self.criterion(y_pred, y_batch)
                
                self.optimizer.zero_grad()
                loss.backward()
                self.optimizer.step()
                
                total_loss += loss.item() * X_batch.size(0)
            
            avg_loss = total_loss / len(dataloader.dataset)
            self.loss_history.append(avg_loss)
            if (epoch+1) % 5 == 0:
                print(f"Train Epoch [{epoch+1}/{epochs}], Avg Loss: {avg_loss:.4f}")
        return self.loss_history

# 使用Trainer(实例像函数一样调用)
dataset = TensorDataset(X, y)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)

trainer = Trainer(model, criterion, optimizer, device)
loss_history = trainer(dataloader, epochs=20)  # 调用__call__方法
print(f"\n训练损失历史(最后5个):{loss_history[-5:]}")

# ==================== 知识点4:综合实战(GPU+__call__) ====================
print("\n=== 4. 综合实战:GPU训练+自定义__call__模型 ===")

# 定义带__call__的完整MLP模型
class MLP(nn.Module):
    def __init__(self, in_dim, hidden_dim, out_dim):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(in_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, out_dim)
        )
        self.to(device)  # 初始化时自动迁移到指定设备
    
    def forward(self, x):
        return self.layers(x)
    
    def __call__(self, x):
        # 重写__call__(增强nn.Module的默认__call__)
        # 1. 自动迁移输入到模型设备
        if x.device != next(self.parameters()).device:
            x = x.to(next(self.parameters()).device)
            print(f"自动迁移输入到{next(self.parameters()).device}")
        # 2. 执行默认的forward逻辑
        return super().__call__(x)

# 初始化MLP并训练
mlp = MLP(in_dim=2, hidden_dim=16, out_dim=1)
mlp_optimizer = optim.Adam(mlp.parameters(), lr=0.001)
mlp_trainer = Trainer(mlp, criterion, mlp_optimizer, device)

# 调用trainer(__call__)训练MLP
mlp_loss = mlp_trainer(dataloader, epochs=15)
print(f"MLP训练完成,最终损失:{mlp_loss[-1]:.4f}")

# 验证MLP的__call__自动设备迁移
test_x_cpu = torch.randn(10, 2)  # CPU张量
test_y_pred = mlp(test_x_cpu)  # 自动迁移到GPU
print(f"MLP输出设备:{test_y_pred.device}")
print(f"MLP输出形状:{test_y_pred.shape}")

代码关键部分解释

1. GPU 训练核心
  • 设备指定torch.device("cuda" if torch.cuda.is_available() else "cpu") 是跨平台兼容的标准写法;
  • 张量迁移X = X.to(device) 必须重新赋值(因为张量的.to()返回新对象);
  • 模型迁移model = LinearModel().to(device) 直接修改模型参数的设备(无需重新赋值);
  • 错误避免 :所有参与计算的张量(输入、标签、中间张量)必须和模型在同一设备,可通过x.device检查。
2. __call__方法核心
  • 基础计数器Counter类通过__call__实现调用时计数,保留self.count状态;
  • 模拟 nn.ModuleCustomModel__call__包含数据校验 + 设备自动迁移,模拟 PyTorch 模型的调用逻辑;
  • Trainer 类 :将训练逻辑封装到__call__,让训练器实例像函数一样被调用(trainer(dataloader));
  • 重写 nn.Module 的__call__MLP类重写__call__,增强默认逻辑(自动设备迁移)。

总结

  1. GPU 训练关键

    • .to(device)统一模型和所有张量的设备,张量需重新赋值,模型直接调用;
    • 设备不一致是最常见错误,可通过x.device检查,或在__call__中自动迁移;
    • 跨平台开发时务必用torch.cuda.is_available()判断 GPU 是否可用。
  2. __call__方法核心:

    • 让实例像函数一样调用,同时保留内部状态(如模型参数、计数);
    • 可增强 PyTorch 模型的默认行为(如自动设备迁移、数据校验);
    • 适合封装训练逻辑、自定义模型等场景,让代码更简洁。
  3. 综合技巧:

    • 将设备迁移逻辑封装到__call__,可避免重复的.to(device)代码;
    • 训练器类结合__call__,让训练流程更简洁(trainer(dataloader));
    • 所有涉及 GPU 的代码需做好兼容性(CPU/GPU 自动切换)。

@浙大疏锦行

相关推荐
格林威1 小时前
Baumer相机金属粉末铺粉均匀性评估:用于增材制造过程监控的 7 个实用技巧,附 OpenCV+Halcon 实战代码!
人工智能·opencv·视觉检测·制造·工业相机·智能相机·堡盟相机
rainbow7242441 小时前
学AI的完整花费清单:从入门到进阶的投入预算
人工智能
清水白石0081 小时前
装饰器模式 vs Python 装饰器:同名背后的深度解析与实战融合
数据库·python·装饰器模式
ZPC82101 小时前
window 下使用docker
人工智能·python·算法·机器人
子午1 小时前
【岩石种类识别系统】Python+深度学习+人工智能+算法模型+图像识别+TensorFlow+2026计算机毕设项目
人工智能·python·深度学习
格林威1 小时前
Baumer相机镜面反射区域遮蔽重建:恢复缺失纹理的 6 个关键技术,附 OpenCV+Halcon 实战代码!
人工智能·opencv·计算机视觉·视觉检测·工业相机·智能相机·堡盟相机
iFeng的小屋1 小时前
【2026最新xhs爬虫】用Python批量爬取关键词笔记,异步下载高清图片!
笔记·爬虫·python
okclouderx1 小时前
【Easy-Vibe】【task4】给原型加上 AI 能力
人工智能·trae·ai ide·vibe coding·easy vibe