Day 34 训练
Day 34 训练
- CPU 训练
- GPU 训练
- CPU 与 GPU 性能差异分析
- (一)数据传输开销
- (二)核心启动开销
- (三)性能浪费
- 优化策略
- (一)减少数据传输
- (二)调整记录间隔
- PyTorch 中的 call 方法
- `__call__` 方法介绍
- 在 PyTorch 中的应用
- 总结
知识点回归:
-
CPU性能的查看:看架构代际、核心数、线程数
-
GPU性能的查看:看显存、看级别、看架构代际
-
GPU训练的方法:数据和模型移动到GPU device上
-
类的call方法:为什么定义前向传播时可以直接写作self.fc1(x)
要让模型在 GPU 上训练,主要是将模型和数据迁移到 GPU 设备上。
在 PyTorch 里,.to(device) 方法的作用是把张量或者模型转移到指定的计算设备(像 CPU 或者 GPU)上。
- 对于张量(Tensor):调用 .to(device) 之后,会返回一个在新设备上的新张量。-
- 对于模型(nn.Module):调用 .to(device) 会直接对模型进行修改,让其所有参数和缓冲区都移到新设备上。
在进行计算时,所有输入张量和模型必须处于同一个设备。要是它们不在同一设备上,就会引发运行时错误。并非所有 PyTorch 对象都有 .to(device) 方法,只有继承自 torch.nn.Module 的模型以及 torch.Tensor 对象才有此方法。
这个常见错误就是输入张量和模型处于不同的设备。
如何衡量GPU的性能好坏呢?
以RTX 3090 Ti, RTX 3080, RTX 3070 Ti, RTX 3070, RTX 4070等为例
通过“代” 前两位数字代表“代”: 40xx (第40代), 30xx (第30代), 20xx (第20代)。“代”通常指的是其底层的架构 (Architecture)。每一代新架构的发布,通常会带来工艺制程的进步和其他改进。也就是新一代架构的目标是在能效比和绝对性能上超越前一代同型号的产品。
通过级别 后面的数字代表“级别”,
xx90: 通常是该代的消费级旗舰或次旗舰,性能最强,显存最大 (如 RTX 4090, RTX 3090)。
xx80: 高端型号,性能强劲,显存较多 (如 RTX 4080, RTX 3080)。
xx70: 中高端,甜点级,性能和价格平衡较好 (如 RTX 4070, RTX 3070)。
xx60: 主流中端,性价比较高,适合入门或预算有限 (如 RTX 4060, RTX 3060)。
xx50: 入门级,深度学习能力有限。
通过后缀 Ti 通常是同型号的增强版,性能介于原型号和更高一级型号之间 (如 RTX 4070 Ti 强于 RTX 4070,小于4080)。
通过显存容量 VRAM (最重要!!) 他是GPU 自身的独立高速内存,用于存储模型参数、激活值、输入数据批次等。单位通常是 GB(例如 8GB, 12GB, 24GB, 48GB)。如果显存不足,可能无法加载模型,或者被迫使用很小的批量大小,从而影响训练速度和效果
训练阶段:小批量梯度是对真实梯度的一个有噪声的估计。批量越小,梯度的方差越大(噪声越大)。显存小只能够使用小批量梯度。
推理阶段:有些模型本身就非常庞大(例如大型语言模型、高分辨率图像的复杂 CNN 网络)。即使你将批量大小减到 1,模型参数本身占用的显存可能就已经超出了你的 GPU 显存上限。
以下是一篇根据你提供的内容撰写的 blog:
CPU 训练
首先,我们在 CPU 上进行训练。训练前,对数据进行了归一化处理,并将数据划分为训练集和测试集。模型定义了两个全连接层,使用 ReLU 激活函数和交叉熵损失函数,采用随机梯度下降优化器。
训练 20000 个 epoch 后,CPU 训练时长为 2.93 秒。训练过程中,我们每 100 个 epoch 打印一次损失值,清晰地看到损失逐渐下降的趋势。
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
import numpy as npiris = load_iris()
X = iris.data
y = iris.targetX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)from sklearn.preprocessing import MinMaxScaler
scaled = MinMaxScaler()
X_train = scaled.fit_transform(X_train)
X_test = scaled.transform(X_test)X_train = torch.FloatTensor(X_train)
X_test = torch.FloatTensor(X_test)
y_train = torch.LongTensor(y_train)
y_test = torch.LongTensor(y_test)class MLP(nn.Module):def __init__(self):super(MLP, self).__init__()self.fc1 = nn.Linear(4, 10)self.relu = nn.ReLU()self.fc3 = nn.Linear(10, 3)def forward(self, x):x = self.fc1(x)x = self.relu(x)x = self.fc3(x)return xmodel = MLP()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)epochs = 20000
losses = []
import time
start_time = time.time()
for epoch in range(epochs):outputs = model.forward(X_train)loss = criterion(outputs, y_train)losses.append(loss.item())optimizer.zero_grad()loss.backward()optimizer.step()if epoch % 1000 == 0:print(f'Epoch {epoch}, Loss: {loss.item()}')time_all = time.time() - start_time # 计算训练时间
print(f'Training time: {time_all:.2f} seconds')
import matplotlib.pyplot as plt
# 可视化损失曲线
plt.plot(range(epochs), losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss over Epochs')
plt.show()
GPU 训练
接着,我们尝试在 GPU 上进行训练。为了确保公平比较,我们使用相同的模型结构和数据预处理方式。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"Using {device}")
iris = load_iris()
X = iris.data # 特征数据
y = iris.target # 标签数据# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)# 归一化数据
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)# 将数据转换为PyTorch张量并移至GPU
# 分类问题交叉熵损失要求标签为long类型
# 张量具有to(device)方法,可以将张量移动到指定的设备上
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)
class MLP(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)def forward(self, x):out = self.fc1(x)out = self.relu(out)out = self.fc2(out)return out# 实例化模型并移至GPU
# MLP继承nn.Module类,所以也具有to(device)方法
model = MLP().to(device)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)# 训练模型
num_epochs = 20000
losses = []
start_time = time.time()for epoch in range(num_epochs):# 前向传播outputs = model(X_train)loss = criterion(outputs, y_train)# 反向传播和优化optimizer.zero_grad()loss.backward()optimizer.step()# 记录损失值losses.append(loss.item())# 打印训练信息if (epoch + 1) % 100 == 0:print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')time_all = time.time() - start_time
print(f'Training time: {time_all:.2f} seconds')# 可视化损失曲线
plt.plot(range(num_epochs), losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss over Epochs')
plt.show()
CPU 与 GPU 性能差异分析
(一)数据传输开销
GPU 训练需要将数据从 CPU 内存传输到 GPU 显存,训练结束后再将结果传回 CPU。在这个小型任务中,数据传输开销占据了相当大的一部分时间。
(二)核心启动开销
GPU 执行每个操作都需要启动一个 “核心”,这个过程有一个固定的开销。而在这个小型网络和数据集上,核心启动开销在总时间中的占比相对较大。
(三)性能浪费
由于数据量小,GPU 的很多计算单元没有被充分利用,导致其强大的并行计算能力无法发挥。
优化策略
为了优化 GPU 训练性能,我们提出了两种思路:
(一)减少数据传输
我们尝试减少从 GPU 到 CPU 的数据传输次数,例如不打印每个 epoch 的损失值,或者减少打印频率。通过这种方式,可以有效降低数据传输开销。
(二)调整记录间隔
我们还将记录损失值的间隔从每 100 个 epoch 调整为每 200 个 epoch,进一步减少数据传输和记录操作对训练时间的影响。
经过优化后,GPU 训练时间明显缩短,接近 CPU 训练时长。
PyTorch 中的 call 方法
这段内容主要是在介绍 Python 中的 __call__
方法以及它在 PyTorch 框架中的应用,以下是详细解释:
__call__
方法介绍
- 定义:
__call__
是 Python 中的一个特殊魔术方法,当一个类的实例被像函数一样调用时,会触发该方法。它允许类的实例表现得像函数一样,同时还能保留对象的内部状态。 - 示例:
- 不带参数的
__call__
方法:定义了一个Counter
类,在其__call__
方法中,每次调用实例时,内部的count
属性会加 1 并返回当前值。通过counter()
的方式调用实例,可以实现计数功能,且每次调用都会更新内部状态。 - 带参数的
__call__
方法:定义了一个Adder
类,在其__call__
方法中,接受两个参数a
和b
,并返回它们的和。通过adder(3, 5)
的方式调用实例,可以实现加法运算。
- 不带参数的
在 PyTorch 中的应用
- 背景:在 PyTorch 中,
nn.Module
是所有神经网络模块的基类,它定义了__call__
方法。这个方法使得nn.Module
的子类实例(如nn.Linear
、nn.ReLU
等)可以像函数一样被调用。 - 工作原理:当调用
self.fc1(x)
时,实际上是在调用self.fc1.__call__(x)
。而nn.Module
的__call__
方法内部会调用子类的forward
方法(如self.fc1.forward(x)
)。forward
方法是定义模块前向传播逻辑的地方,它接收输入张量并返回输出张量。 - 优势:这种设计使得 PyTorch 的模块可以以统一的方式被调用,同时还能保留模块的内部状态(如参数、梯度等)。此外,使用
__call__
方法还可以触发完整的前向传播流程,包括钩子函数等,这是 PyTorch 的核心设计模式。 - 示例代码中的体现:
- 在
MLP
类中,self.fc1
是一个nn.Linear
对象,self.relu
是一个nn.ReLU
对象。在forward
方法中,通过self.fc1(x)
和self.relu(out)
的方式调用这些对象,实际上是在调用它们的forward
方法,完成前向传播的计算。 nn.Linear
和nn.ReLU
等类都继承自nn.Module
,因此它们都具备了__call__
方法,使得它们可以像函数一样被调用,同时保留了模块的内部状态和前向传播逻辑。
- 在
总结
__call__
方法是 Python 中的一个特殊方法,它允许类的实例像函数一样被调用,并可以保留对象的内部状态。- 在 PyTorch 中,
nn.Module
类定义了__call__
方法,使得其子类实例可以像函数一样被调用,同时触发完整的前向传播流程,包括调用子类的forward
方法。 - 这种设计模式使得 PyTorch 的模块可以以统一的方式被调用,同时保留了模块的内部状态和前向传播逻辑,是 PyTorch 的核心设计之一。
@浙大疏锦行