day 48 模型的可视化与推理
循序渐进,从基础的开始,逐渐加大深度。先回顾之前的内容
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
import time
import matplotlib.pyplot as plt
# 设置gpu 设备
device = torch.device("cuda" 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)
# 将数据转换为tensor
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):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
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) % 200 == 0:losses.append(loss.item())print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')# 打印训练信息if(epoch+1)%100==0:print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}")
time_all=time.time()-start_time
print(f"训练时间:{time_all:.4f}秒")# 可视化损失曲线
plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss Curve')
plt.show()一、模型结构可视化
理解一个深度学习网络最重要的2点:
1、了解损失如何定义的,知道损失从何而来---把抽象的任务通过损失函数量化出来
2、了解参数总量,即知道每一层的设计才能推出--层设计决定参数总量。
为了了解参数的总量,我们需要哦知道层设计,以及每一层数据的数量。下面介绍几个层的可视化工具:
1.1nn.Module自带的方法这是最基础、最简单的方法,直接打印模型对像,他会输出模型的结构,显示模型中各个层的名称和参数信息。# nn.Module的内置功能,返回模型的可训练参数迭代器
for name,param in model.named_parameters():print (f"Parameter name: {name},shape:{param.shape}")可以将模型中带有wight的参数提取出来,并转为numpy数组形式,对其进行统计分布。# 提取权重数据
import numpy as np
weight_data={}
for name,param in model.named_parameters():if 'weight' in name:weight_data[name]=param.detach().cpu().numpy()
# 可视化权重分布
fig,axes=plt.subplots(1,len(weight_data),figsize=(15,5))
fig.suptitle('Weight Distribution')
for i,(name,weights) in enumerate(weight_data.items()):# 展平权重张量为一维数组weights_flat=weights.flatten()# 绘制直方图axes[i].hist(weights_flat,bins=50,alpha=0.7)axes[i].set_title(name)axes[i].set_xlabel('Weight Value')axes[i].set_ylabel('Frequency')axes[i].grid(True,linestyle='--',alpha=0.7)
plt.tight_layout()
plt.subplots_adjust(top=0.85)
plt.show()
#计算并打印每层权重的统计信息
print("\n ======权重统计信息====")
for name,weights in weight_data.items():mean=np.mean(weights)std=np.std(weights)min_val=np.min(weights)max_val=np.max(weights)print(f"name: {name}")print(f"Mean: {np.mean(weights):.4f}")print(f"Std: {np.std(weights):.4f}")print(f"Min: {np.min(weights):.4f}")print(f"Max: {np.max(weights):.4f}")print("="*30)对比fc1和fc2的权重分布,可以发现他们的均值、标准差、最值存在差异,反应了不同层在模型中的作用不同。
权重统计信息可以为超参数调整提供参考。如果发现权重标准差过大导致训练不稳定,尝试调整学习率,使用权重更新更平稳;或者改变权重初始化的方法,使初始化权重分布更合理。如果最小值和最大值在训练后期仍然波动较大,可能需要考虑调整正则化参数,防止过拟合或欠拟合。
1.2 torch summary 库的sunmmary方法
#pip install torchsummary -i https://pypi.tuna.tsinghua.edu.cn/simple
from torchsummary import summary
# 打印模型摘要,可以放置在模型定义后面
summary(model, input_size=(4,))
该方法不显示输入层的尺寸,因为输入的神经网是自己设置的,所以不需要显示输入层的尺寸。
但是在使用该方法时,input_size=(4,) 参数是必需的,因为 PyTorch 需要知道输入数据的形状才能推断模型各层的输出形状和参数数量。这是因为PyTorch 的模型在定义时是动态的,它不会预先知道输入数据的具体形状。nn.Linear(4, 10) 只定义了 “输入维度是 4,输出维度是 10”,但不知道输入的批量大小和其他维度,比如卷积层需要知道输入的通道数、高度、宽度等信息。----并非所有输入数据都是结构化数据因此,要生成模型摘要(如每层的输出形状、参数数量),必须提供一个示例输入形状,让 PyTorch “运行” 一次模型,从而推断出各层的信息。summary 函数的核心逻辑是:
1. 创建一个与 input_size 形状匹配的虚拟输入张量(通常填充零)
2. 将虚拟输入传递给模型,执行一次前向传播(但不计算梯度)
3. 记录每一层的输入和输出形状,以及参数数量
4. 生成可读的摘要报告不同场景下的 `input_size` 示例
| 模型类型 | 输入类型 | `input_size` 示例 | 实际输入形状(batch_size=32) |
|----------------|------------------------|--------------------------|-----------------------------|
| MLP(单样本) | 一维特征向量 | `(4,)` | `(32, 4)` |
| CNN(图像) | 三维图像(C,H,W) | `(3, 224, 224)` | `(32, 3, 224, 224)` |
| RNN(序列) | 二维序列(seq_len, feat) | `(10, 5)` | `(32, 10, 5)` |
二、进度条功能
介绍一下tqdm这个库,非常适合用在循环中观察进度。尤其在深度学习这个训练时循环的场景中,最核心的逻辑代码如下:
1、创建一个进度条对象,并传入总迭代次数。一般用with 语句创建对象,这样对象会在with语句结束后自动关销毁,保证资源释放。with 常见的上下文管理器,这样的使用方式还有用with 打开文件,结束后会自动关闭文件。
2、更新进度条,通过pbar.update()方法更新进度条,一般用在循环中,每次循环结束后调用一次。
3、设置进度条描述信息,通过pbar.set_description()方法设置进度条的描述信息,一般用在循环开始前调用一次。
2.1手动更新
from tqdm import tqdm
import time
#创建一个总步数为10 的进度条
with tqdm(total=10) as pbar:for i in range(10):time.sleep(0.5)pbar.update(1)
from tqdm import tqdm
import time
# 创建进度时添加描述和单位
with tqdm(total=10, desc="进度", unit="步") as pbar:# 进度条这个对象,可以设置描述和单位# desc是描述,在左侧显示# unit是单位,在进度条右侧显示for i in range(10):time.sleep(0.5)pbar.update(1)2.2自动更新
from tqdm import tqdm
import time
# 直接将range(3)传给tdqm,自动生成进度条
# 这个写法有点神奇,直接给这个对象内部传入一个可迭代的对象,生成进度条
for i in tqdm(range(3),desc="处理任务",unit="epoch"):time.sleep(1)
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
import time
import matplotlib.pyplot as plt
from tqdm import tqdm# 设置GPU 设备
deceive=torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 加载数据集
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)
# 转换为Tensor
X_train =torch.FloatTensor(X_train).to(deceive)
X_test =torch.FloatTensor(X_test).to(deceive)
y_train =torch.LongTensor(y_train).to(deceive)
y_test =torch.LongTensor(y_test).to(deceive)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
model=MLP().to(deceive)
# 分类问题使用交叉熵损失函数
criterion=nn.CrossEntropyLoss()
# 优化器
optimizer=optim.Adam(model.parameters(),lr=0.01)
# 训练模型
num_epochs=2000
# 用于存储每个100个epoch的损失
losses=[]
epochs=[]
start_time=time.time()
# 创建tdqm 进度条
with tqdm(total=num_epochs,desc="训练进度",unit="epoch") as pbar:for epoch in range(num_epochs):model.train()optimizer.zero_grad()outputs=model(X_train)loss=criterion(outputs,y_train)loss.backward()optimizer.step()if (epoch+1)%200==0:losses.append(loss.item())epochs.append(epoch+1)# 更新进度条的描述信息pbar.set_postfix({'loss':f'{loss.item():.4f}'})# 每1000个epoch 更新一次进度条if (epoch +1 )%1000==0:pbar.update(1000)# 确保进度条达到100if pbar.n<num_epochs:pbar.update(num_epochs-pbar.n)# 计算剩余的进度并更新
time_all=time.time()-start_time
print(f"训练完成,耗时{time_all:.2f}秒")三、模型的推理
那么现在我们来测试模型。测试这个词在大模型领域叫做推理(inference),意味着把数据输入到训练好的模型的过程。注意 损失和优化器在训练阶段。
# 在测试集上评估模型,此时model 内部已经是训练好的参数了
# 评估模型
model.eval()# 设置模型为评估模型
with torch.no_grad():# torch.no_grad()的作用是禁用梯度计算,可以提高模型推理速度outputs=model(X_test)# 对测试数据进行前向传播,获得预测结果_,predicted=torch.max(outputs,1)# torch.max(outputs, 1)返回每行的最大值和对应的索引#这个函数返回2个值,分别是最大值和对应索引,参数1是在第1维度(行)上找最大值,_ 是Python的约定,表示忽略这个返回值,所以这个写法是找到每一行最大值的下标# 此时outputs是一个tensor,p每一行是一个样本,每一行有3个值,分别是属于3个类别的概率,取最大值的下标就是预测的类别# predicted == y_test判断预测值和真实值是否相等,返回一个tensor,1表示相等,0表示不等,然后求和,再除以y_test.size(0)得到准确率# 因为这个时候数据是tensor,所以需要用item()方法将tensor转化为Python的标量# 之所以不用sklearn的accuracy_score函数,是因为这个函数是在CPU上运行的,需要将数据转移到CPU上,这样会慢一些# size(0)获取第0维的长度,即样本数量correct=(predicted==y_test).sum().item()accuracy=correct/y_test.size(0)print (f"测试集准确率: {accuracy * 100:.2f}%'")
模型的评估模式简单来说就是评估阶段会关闭一些训练相关的操作和策略 ,比如更新参数 正则化等操作,确保模型输出结果的稳定性和一致性。可能有同学好奇,为什么评估模式不关闭梯度计算,推理不是不需要更新参数么?主要还是因为在某些场景下,评估阶段可能需要计算梯度(虽然不更新参数)。例如:计算梯度用于可视化(如 CAM 热力图,主要用于cnn相关)。所以为了避免这种需求不被满足,还是需要手动关闭梯度计算。
@浙大疏锦行