@浙大疏锦行
📘 Day 34 实战作业:GPU 加速与 Python 魔法方法
1. 作业综述
核心目标:
- GPU 训练:掌握 PyTorch 的
.to(device)机制,将模型和数据迁移到显卡上训练,并对比 CPU 与 GPU 的速度差异。 - 性能分析:理解为什么在小数据集上 GPU 反而比 CPU 慢(数据传输开销 vs 计算开销)。
- 魔法方法:深入理解
__call__方法,明白为什么我们用model(x)而不是model.forward(x)。
涉及知识点:
- Device Management:
torch.device,.to(device),torch.cuda.is_available(). - Performance Tuning: 减少 CPU-GPU 通信频率 (减少
.item()调用)。 - Python Magic Methods:
__init__vs__call__.
场景类比:
- CPU: 像是一个全能的博士(核心少但强大),算简单的加减法(小数据)非常快。
- GPU: 像是一万个小学生(核心多但简单),算复杂的矩阵乘法(大数据)非常快。
- 数据传输: 把题目从博士办公室(内存)搬到小学生教室(显存),是需要花时间的。如果题目太少,搬运的时间比计算的时间还长。
步骤 1:环境检测与设备选择
场景描述:
在开始训练前,我们需要先确定电脑上是否有 N 卡(NVIDIA GPU)。
代码应该具有通用性:有显卡就用显卡,没显卡就用 CPU。
任务:
- 使用
torch.cuda.is_available()检测环境。 - 定义
device对象。 - 打印当前使用的设备名称。
importtorchimporttorch.nnasnnimporttorch.optimasoptimfromsklearn.datasetsimportload_irisfromsklearn.model_selectionimporttrain_test_splitfromsklearn.preprocessingimportMinMaxScalerimporttimeimportmatplotlib.pyplotasplt# --- 1. 设备检测 ---# 这是 PyTorch 的标准写法,保证代码在任何机器上都能跑device=torch.device("cuda:0"iftorch.cuda.is_available()else"cpu")print(f"🚀 当前运行设备:{device}")ifdevice.type=='cuda':print(f" 显卡型号:{torch.cuda.get_device_name(0)}")print(f" 显存信息:{torch.cuda.get_device_properties(0).total_memory/1024**3:.2f}GB")else:print(" ⚠️ 未检测到 GPU,将使用 CPU 进行训练。")🚀 当前运行设备: cuda:0 显卡型号: NVIDIA GeForce RTX 3050 Laptop GPU 显存信息: 4.00 GB步骤 2:数据与模型的“搬家”
核心逻辑:
PyTorch 默认将所有数据和模型创建在CPU上。
要使用 GPU,必须显式地调用.to(device)方法将它们“搬运”到显存中。
- 注意:输入数据 (
X)、标签 (y) 和 模型 (model) 必须在同一个设备上,否则会报错。
任务:
- 准备 Iris 数据。
- 将 Tensor 移动到 GPU。
- 实例化 MLP 模型并移动到 GPU。
# --- 数据准备 ---iris=load_iris()X,y=iris.data,iris.target X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.2,random_state=42)# 归一化scaler=MinMaxScaler()X_train=scaler.fit_transform(X_train)X_test=scaler.transform(X_test)# --- 关键:将数据移动到 Device ---# .to(device) 会返回一个新的 Tensor(如果是 GPU,则在显存中)X_train=torch.FloatTensor(X_train).to(device)y_train=torch.LongTensor(y_train).to(device)# 测试集也要移动,否则预测时会报错X_test=torch.FloatTensor(X_test).to(device)y_test=torch.LongTensor(y_test).to(device)print(f"数据设备检测:{X_train.device}")# --- 模型定义 ---classMLP(nn.Module):def__init__(self):super(MLP,self).__init__()self.fc1=nn.Linear(4,10)self.relu=nn.ReLU()self.fc2=nn.Linear(10,3)defforward(self,x):out=self.fc1(x)out=self.relu(out)out=self.fc2(out)returnout# --- 关键:将模型移动到 Device ---# 这一步会将模型的权重矩阵(Weights)和偏置(Biases)全部搬到显存model=MLP().to(device)数据设备检测: cuda:0步骤 3:性能陷阱与优化
思考题:
如果你直接运行训练,可能会发现 GPU 居然比 CPU 慢!
原因:
- 数据量太小:Iris 数据集太小,GPU 启动核心(Kernel Launch)的开销比计算本身还大。
- 通信频繁:如果在循环中频繁使用
.item()或打印 Loss,会强制 CPU 等待 GPU 计算完成并把数据传回来(Sync),打断了 GPU 的流水线。
优化策略:
- 减少
loss.item()的调用频率(例如每 200 轮记录一次)。
任务:
编写训练循环,测量时间,并尝试减少记录频率来优化速度。
# 定义损失和优化器criterion=nn.CrossEntropyLoss()optimizer=optim.SGD(model.parameters(),lr=0.01)num_epochs=20000losses=[]print(f"\n⏱️ 开始训练 (Total Epochs:{num_epochs})...")start_time=time.time()forepochinrange(num_epochs):# 1. 前向传播outputs=model(X_train)loss=criterion(outputs,y_train)# 2. 反向传播optimizer.zero_grad()loss.backward()optimizer.step()# --- 性能优化关键点 ---# 不要每个 epoch 都调用 loss.item(),这会触发 GPU->CPU 的同步if(epoch+1)%200==0:# 只有在需要打印/记录时,才从 GPU 取回数据curr_loss=loss.item()losses.append(curr_loss)if(epoch+1)%5000==0:print(f'Epoch [{epoch+1}/{num_epochs}], Loss:{curr_loss:.4f}')end_time=time.time()print(f"✅ 训练完成!耗时:{end_time-start_time:.4f}秒")# 可视化plt.plot(range(0,num_epochs,200),losses)plt.title("Training Loss")plt.show()⏱️ 开始训练 (Total Epochs: 20000)... Epoch [5000/20000], Loss: 0.1506 Epoch [10000/20000], Loss: 0.0841 Epoch [15000/20000], Loss: 0.0675 Epoch [20000/20000], Loss: 0.0606 ✅ 训练完成!耗时: 18.5502 秒步骤 4:解密 Python 的魔法方法__call__
核心疑问:
为什么我们训练时写的是outputs = model(X_train),而不是outputs = model.forward(X_train)?
- 这得益于 Python 的
__call__机制。 - 当一个对象被当作函数调用时(即后面加括号
()),Python 会自动去执行它的__call__方法。
PyTorch 的设计:
nn.Module定义了__call__。- 在
__call__内部,它不仅调用了你写的forward(),还负责处理Hooks (钩子)等底层逻辑。 - 结论:永远直接调用
model(x),不要显式调用forward(x)。
任务:
编写一个简单的Counter类,实现__call__方法,模拟这种行为。
# --- 自定义一个可调用的类 --- class Counter: def __init__(self): self.count = 0 print("1. 对象已初始化 (__init__)") # 只要实现了这个方法,对象就能像函数一样被调用 def __call__(self, name): self.count += 1 print(f"2. 对象被调用了 (__call__) -> Hello, {name}! (第 {self.count} 次)") return self.count print("\n--- 测试 __call__ ---") # 1. 实例化 c = Counter() # 2. 像函数一样调用对象 # 这实际上是在执行 c.__call__("PyTorch") num = c("PyTorch") c("GPU") c("Deep Learning") print(f"最终计数: {c.count}") # 3. 验证 PyTorch 模型 print("\n--- 验证 PyTorch 模型 ---") print(f"model 是否可调用? {callable(model)}") # model(X) 等价于 model.__call__(X) -> 内部调用 forward(X)--- 测试 __call__ --- 1. 对象已初始化 (__init__) 2. 对象被调用了 (__call__) -> Hello, PyTorch! (第 1 次) 2. 对象被调用了 (__call__) -> Hello, GPU! (第 2 次) 2. 对象被调用了 (__call__) -> Hello, Deep Learning! (第 3 次) 最终计数: 3 --- 验证 PyTorch 模型 --- model 是否可调用? True🎓 Day 34 总结:硬核内功
今天我们跨越了两个维度的障碍:
- 硬件维度:学会了如何驾驭 GPU。虽然在 Iris 这种小数据上 GPU 甚至更慢,但当你面对 ResNet 处理 ImageNet 图片时,GPU 将比 CPU 快 10-100 倍。记住核心口诀:模型和数据,统统
.to(device)。 - 软件维度:理解了
__call__魔法。这是 PyTorch 优雅设计的基石,也是 Python 面向对象编程的高级技巧。
Next Level:
现在我们有了更快的计算设备(GPU),也有了模型。接下来,我们需要解决**“怎么保存模型”和“怎么加载别人训练好的模型”**的问题。明天见!