Day 34 GPU训练及类的call方法

今日任务:
  1. CPU性能的查看:看架构代际、核心数、线程数
  2. GPU性能的查看:看显存、看级别、看架构代际
  3. GPU训练的方法:数据和模型移动到GPU device上
  4. 类的call方法:为什么定义前向传播时可以直接写作self.fc1(x)

ps:在训练过程中可以在命令行输入nvida-smi查看显存占用情况

先简单回顾昨天学习的简单神经网络流程

(1)准备数据(2)数据预处理:归一化、张量转换(3)模型框架定义:继承类;定义输出层/隐藏层/输入层;前向传播(3)损失函数和优化器的创建(4)循环训练:确定训练轮数;循环内部过程(forward,loss,backward,step)(5)可视化损失函数,查看收敛情况

CPU和GPU性能查看

【深度学习小常识】CPU(中央处理器)和GPU(图像处理器)的区别

CPU

import wmi
c = wmi.WMI()
processors = c.Win32_Processor()
for processor in processors:
    print(f"CPU 型号: {processor.Name}") 
    print(f"核心数: {processor.NumberOfCores}")
    print(f"线程数: {processor.NumberOfLogicalProcessors}")

GPU

GPU训练

GPU的并行计算能力强,训练神经网络可以提速10-100倍。要想让模型在GPU上训练,需要将模型数据(张量)迁移到GPU设备上,使用的方法是.to(device)
考虑到代码的可移植性和鲁棒性(不是所有人都有GPU),使用如下写法,确定device设备:

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")

模型/数据迁移

确定好设备后,就需要将数据和模型迁移至设备上。需要注意的是,所有需要在GPU/CPU上做计算的核心对象都支持.to(device),如Tensor(张量)子类、nn.Module子类(模型,并且要保证这些对象迁移到同一个设备上。

#张量转换,迁移至GPU
X_train = torch.FloatTensor(X_train).to(device)
X_test = torch.FloatTensor(X_test).to(device)
y_train = torch.LongTensor(y_train).to(device)
y_test = torch.LongTensor(y_test).to(device)

#实例化,迁移至设备
model = MLP().to(device)

完整版

# GPU版本
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
import torch
import torch.nn as nn
import torch.optim as optim

# 设置设备
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print(f'使用的设备:{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) # 划分数据集

# 数据预处理
#归一化
scaler = MinMaxScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
#张量转换,迁移至GPU
X_train = torch.FloatTensor(X_train).to(device)
X_test = torch.FloatTensor(X_test).to(device)
y_train = torch.LongTensor(y_train).to(device)
y_test = torch.LongTensor(y_test).to(device)

# 模型框架定义
class MLP(nn.Module): # 继承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
#实例化,迁移至设备
model = MLP().to(device)

# 损失函数和优化器
criterion = nn.CrossEntropyLoss() # 损失函数
optimiser = optim.SGD(model.parameters(), lr=0.01) # SGD
#optimiser = optim.Adam(model.parameters(),lr=0.001) # Adam

# 循环训练
epoch_num = 20000 # 训练轮数
losses = []
import time
start_time = time.time()
for epoch in range(epoch_num):
    output = model(X_train) # 隐式调用forward方法
    loss = criterion(output,y_train)
    #反向传播
    optimiser.zero_grad() # 清零
    loss.backward() # 反向传播计算梯度
    optimiser.step() # 更新参数
    # 记录loss值
    losses.append(loss.item())

    # 打印
    if (epoch + 1) % 100 == 0: # range是从0开始,所以epoch+1是从当前epoch开始,每100个epoch打印一次
        print(f'Epoch [{epoch+1}/{epoch_num}], Loss: {loss.item():.4f}')

time_all = time.time() - start_time # 计算训练时间
print(f'The training time of CPU: {time_all:.2f} seconds')
# 可视化
import matplotlib.pyplot as plt
plt.plot(list(range(epoch_num)),losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss over Epochs')
plt.show()

Adam优化器:

计算开销

租用云服务器RTX-4090,CPU版本用时4.15s,GPU版本用时9.59s。

对于极小的数据集(如鸢尾花数据),CPU的运行速度快于GPU,主要是因为GPU运行时存在固定的计算开销:

  • 数据传输开销CPU 内存 ↔ GPU 显存之间的数据拷贝。GPU计算前,数据从RAM到VRAM;结果返回时,从GPU到CPU,比如loss.item()(把标量从GPU到CPU,触发同步
  • 核心启动开销:GPU 执行的每个操作(Linear + ReLU + Linear)都涉及到在 GPU 上启动一个“核心”(kernel)——一个在 GPU 众多计算单元上运行的小程序,存在固定开销。
  • 性能浪费:数据量太少时,GPU的很多计算单元都没有被用到,即使用了全批次也没有用到的全部计算单元。

综上,对于小数据集来说,GPU固定的计算开销远大于它在速度上的提升,因此在这个情况下速度反而慢于CPU。

GPU能发挥优势的场景主要集中在大批量数据处理,能发挥并行性优势:

  • 大型数据集: 例如,图像数据集成千上万张图片,每张图片维度很高。
  • 大型模型: 例如,深度卷积网络 (CNNs like ResNet, VGG) 或 Transformer 模型,它们有数百万甚至数十亿的参数,计算量巨大。
  • 合适的批处理大小: 能够充分利用 GPU 并行性的 batch size,不至于还有剩余的计算量没有被 GPU 处理。
  • 复杂的、可并行的运算: 大量的矩阵乘法、卷积等。

优化

对于固定的流程和数据,能够优化的只有数据传输这一步了,考虑到loss.item()每次循环都会进行从GPU到CPU的过程,可能累积大量时间,从这个优化入手:

(1)直接去除,但是无法可视化,用时8.62s。

# GPU-修改版
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
import torch
import torch.nn as nn
import torch.optim as optim

# 设置设备
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print(f'使用的设备:{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) # 划分数据集

# 数据预处理
#归一化
scaler = MinMaxScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
#张量转换,迁移至GPU
X_train = torch.FloatTensor(X_train).to(device)
X_test = torch.FloatTensor(X_test).to(device)
y_train = torch.LongTensor(y_train).to(device)
y_test = torch.LongTensor(y_test).to(device)

# 模型框架定义
class MLP(nn.Module): # 继承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
#实例化,迁移至设备
model = MLP().to(device)

# 损失函数和优化器
criterion = nn.CrossEntropyLoss() # 损失函数
optimiser = optim.SGD(model.parameters(), lr=0.01) # SGD
#optimiser = optim.Adam(model.parameters(),lr=0.001) # Adam

# 循环训练
epoch_num = 20000 # 训练轮数
losses = []
import time
start_time = time.time()
for epoch in range(epoch_num):
    output = model(X_train) # 隐式调用forward方法
    loss = criterion(output,y_train)
    #反向传播
    optimiser.zero_grad() # 清零
    loss.backward() # 反向传播计算梯度
    optimiser.step() # 更新参数
    # 记录loss值
    #losses.append(loss.item())

    # 打印
    if (epoch + 1) % 100 == 0: # range是从0开始,所以epoch+1是从当前epoch开始,每100个epoch打印一次
        print(f'Epoch [{epoch+1}/{epoch_num}], Loss: {loss.item():.4f}')

time_all = time.time() - start_time # 计算训练时间
print(f'The training time of CPU: {time_all:.2f} seconds')
# 可视化

(2)减少次数访问(保留可视化),每隔N步记录一次(间隔200)

# GPU-修改版
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
import torch
import torch.nn as nn
import torch.optim as optim

# 设置设备
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print(f'使用的设备:{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) # 划分数据集

# 数据预处理
#归一化
scaler = MinMaxScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
#张量转换,迁移至GPU
X_train = torch.FloatTensor(X_train).to(device)
X_test = torch.FloatTensor(X_test).to(device)
y_train = torch.LongTensor(y_train).to(device)
y_test = torch.LongTensor(y_test).to(device)

# 模型框架定义
class MLP(nn.Module): # 继承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
#实例化,迁移至设备
model = MLP().to(device)

# 损失函数和优化器
criterion = nn.CrossEntropyLoss() # 损失函数
optimiser = optim.SGD(model.parameters(), lr=0.01) # SGD
#optimiser = optim.Adam(model.parameters(),lr=0.001) # Adam

# 循环训练
epoch_num = 20000 # 训练轮数
losses = []
import time
start_time = time.time()
for epoch in range(epoch_num):
    output = model(X_train) # 隐式调用forward方法
    loss = criterion(output,y_train)
    #反向传播
    optimiser.zero_grad() # 清零
    loss.backward() # 反向传播计算梯度
    optimiser.step() # 更新参数
        
    # 打印
    if (epoch + 1) % 100 == 0: # range是从0开始,所以epoch+1是从当前epoch开始,每100个epoch打印一次
        print(f'Epoch [{epoch+1}/{epoch_num}], Loss: {loss.item():.4f}')
    # 记录loss值
    if (epoch + 1) % 200 == 0:
        losses.append(loss.item()) # item()方法返回一个Python数值,loss是一个标量张量

time_all = time.time() - start_time # 计算训练时间
print(f'The training time of CPU: {time_all:.2f} seconds')
# 可视化
import matplotlib.pyplot as plt
plt.plot(range(len(losses)), losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss over Epochs')
plt.show()

(3)记录间隔和时长的关系

版本时间(SGD)时间(Adam)
CPU4.53 s7.49 s
GPU9.37 s12.27 s
GPU-18.81 s12.59 s
GPU-29.29 s11.01 s
记录间隔(轮)200400500800100012502000
 记录次数(次) 100504025201610
时间9.29 s9.00 s8.96 s8.97 s9.10 s9.13 s8.69 s

总体上,呈现的是记录间隔越大,时间越短的规律。根本原因是loss.item() 触发 GPU-CPU 同步(gpu需要等待cpu完成才能开启下次运算),记录间隔越小 → loss.item() 越频繁 → 同步越多 → GPU 利用率下降 → 总时间增加

但是存在波动,存在其它因素影响(AI分析):

因素说明是否导致波动
.item() 同步开销每次调用阻塞 GPU,耗时约 1~10 μs主导趋势
GPU 调度随机性CUDA kernel 启动顺序、资源竞争等导致微小波动
显存访问模式不同 batch 或 epoch 下缓存命中率不同影响性能
PyTorch 缓存分配器行为首次分配 vs 再利用显存可能引入差异
系统负载变化其他进程占用 GPU/CPU/内存外部干扰

魔术方法__call__

在定义模型的基本框架时,可以注意到,self.fc1 = nn.Linear(4, 10)  此时,是实例化了一个nn.Linear(4, 10)对象,并把这个对象赋值给了MLP的初始化函数中的self.fc1变量。然后, out = self.fc1(x),这个self.fc1这个变量却实现了函数的用法

这是因为nn.Linear继承了nn.Module类,nn.Module类中定义了__call__方法。PyTorch 中几乎所有层(nn.Linear, nn.ReLU, nn.Conv2d 等)都是 nn.Module 的子类,所以它们的实例也都有__call__方法,可以像函数一样调用。

__call__方法:让实例对象像函数一样被调用。不管有没有参数,只要有call方法,那么实例对象就可以像函数一样被使用。

class Adder:
    def __init__(self):
        self.count = 0
    #def __call__(self): # 无参数的call方法,在每次调用实例后自动触发
        #self.count += 1
        return self.count
    def __call__(self, a, b): # 有参数的call方法
        return a + b

add = Adder() # 创建实例对象
result = add(3, 4)  # 看起来像调用函数
print(result)  # 输出: 7
#add_test = Adder()
#print(add_test()) # 若无参数,则自动调用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值