打卡35天
模型可视化与推理
知识点回顾:
三种不同的模型可视化方法:推荐torchinfo打印summary+权重分布可视化
进度条功能:手动和自动写法,让打印结果更加美观
推理的写法:评估模式
作业:调整模型定义时的超参数,对比下效果。
# nn.Module 的内置功能,直接输出模型结构
print(model)
简单来说,你看到的是一个神经网络模型的“内部结构清单”,就像拆开一台机器后看到的零件列表。咱们用生活中的例子来理解:
假设你要做一个“判断水果类型”的模型(比如区分苹果、香蕉、橘子),输入可能是水果的4个特征(比如重量、颜色、表皮光滑度、大小),输出是3种水果的概率。这个MLP模型的结构就像一条“数据加工流水线”:
第一关(fc1层):有个“4转10的转换器”。它拿到输入的4个特征数据后,会做一些数学计算(比如加权求和+偏移),把这4个数据“变”成10个新的数据。就像把4种原材料加工成10种中间产物。
激活关(relu层):这一步会“过滤掉没用的中间产物”。比如如果某个中间产物算出来是负数(可能代表“没用的信息”),它就会直接变成0(相当于丢弃),只保留正数的部分。这样能让模型更“聪明”,只关注有用的信息。
第二关(fc2层):有个“10转3的转换器”。它拿到前面过滤后的10个数据,再做一次数学计算,最终把它们“浓缩”成3个结果。这3个结果就对应你要判断的3种水果的概率(比如哪个数值大,就更可能是对应的水果)。
所以整体看,这个模型就是通过两层“数据转换器”+一层“过滤无用信息”的步骤,把输入的4个特征,一步步处理成3个最终结果。
# nn.Module 的内置功能,返回模型的可训练参数迭代器
for name, param in model.named_parameters():
print(f"Parameter name: {name}, Shape: {param.shape}")
这些是神经网络模型中各个层的可学习参数(权重和偏置)的信息,用大白话解释就是模型“内部用来做计算的规则”。我们一个一个看:
1. fc1.weight(形状 [10, 4])
这是第一个全连接层(fc1)的“权重矩阵”。
作用:负责把输入的4维数据“转换”成10维数据。
形状含义:10行(对应输出的10个维度),4列(对应输入的4个维度)。可以理解为:每个输出维度(共10个)需要和输入的4个维度“一一配对”计算,所以需要10×4个“小规则”(即权重参数)。
2. fc1.bias(形状 [10])
这是第一个全连接层的“偏置参数”。
作用:给每个输出维度的计算结果加一个“偏移量”(类似数学里的 y = kx + b 中的 b),让模型能拟合更复杂的规律。
形状含义:因为fc1输出是10维,所以需要10个偏置参数(每个输出维度对应1个)。
3. fc2.weight(形状 [3, 10])
这是第二个全连接层(fc2)的“权重矩阵”。
作用:负责把前一层(fc1)输出的10维数据“转换”成最终的3维结果。
形状含义:3行(对应最终输出的3个维度),10列(对应前一层输入的10个维度)。每个输出维度(共3个)需要和前一层的10个维度“配对”计算,所以需要3×10个权重参数。
4. fc2.bias(形状 [3])
这是第二个全连接层的“偏置参数”。
作用:给最终输出的每个维度加一个“偏移量”。
形状含义:因为最终输出是3维,所以需要3个偏置参数(每个输出维度对应1个)。
总结
这些参数是模型在训练过程中自动学习的(比如通过调整这些参数的值,让模型预测更准)。简单说,它们就像模型内部的“计算器”,输入数据经过这些参数的计算后,最终得到你想要的结果(比如分类、回归等)。
# 提取权重数据
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 of Layers')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}:")print(f" 均值: {mean:.6f}")print(f" 标准差: {std:.6f}")print(f" 最小值: {min_val:.6f}")print(f" 最大值: {max_val:.6f}")print("-" * 30)
这里的“每层权重”指的是神经网络中每个可学习层(比如全连接层)内部的“核心参数”,也就是模型通过训练自动调整的“规则数值”。咱们结合你的代码和神经网络的工作原理,用大白话解释:
1. 什么是“权重”?
简单说,权重是神经网络里神经元之间的“连接强度”。就像你做数学题时的“计算公式”——比如输入4个特征(x1, x2, x3, x4),输出10个中间结果(y1~y10),每个y的计算可能是:
y1 = x1*w11 + x2*w12 + x3*w13 + x4*w14
这里的w11、w12、w13、w14就是“权重”(weight),它们决定了输入特征对输出结果的影响程度。
2. “每层权重”具体指什么?
你的代码里,model.named_parameters()会遍历模型所有可训练参数(包括权重和偏置),而if 'weight' in name筛选出了属于“权重”的参数。例如:
如果模型有fc1(第一个全连接层),对应的权重是fc1.weight;
如果有fc2(第二个全连接层),对应的权重是fc2.weight;
这些就是代码中“每层权重”的具体对象。
3. 代码在“提取”和“分析”它们的什么?
你的代码做了两件关键的事:
(1)提取权重数据
把模型中每个层的权重参数(比如fc1.weight和fc2.weight)从PyTorch的张量(Tensor)转成numpy数组,存到weight_data字典里。这样后续可以用numpy和matplotlib分析。
(2)可视化+统计分析
直方图:把每个层的权重值“摊平”成一维数组(比如fc1.weight是10×4的矩阵,摊平后是40个数值),然后统计这些数值的分布(比如大部分权重是0附近的小数?还是有很多大的正数/负数?)。
统计信息:计算每个层权重的均值、标准差、最小/最大值。这些数值能帮你快速判断:
权重是否初始化合理(比如均值是否接近0,标准差是否太小/太大);
训练是否正常(比如训练后权重是否有明显变化,是否出现梯度消失/爆炸)。
总结
“每层权重”就是模型中每个层(比如全连接层)里用于“计算输入→输出”的核心参数。你的代码在“检查”这些参数的分布和统计特征,就像给模型做“体检”——通过它们的数值表现,你可以判断模型是否训练正常、参数初始化是否合理,甚至定位训练中的问题(比如权重全为0可能是初始化错误)。
from torchsummary import summary
# 打印模型摘要,可以放置在模型定义后面
summary(model, input_size=(4,))
这是用 torchsummary 工具打印的模型“详细体检报告”,帮你快速看清模型每一层的“输出尺寸”和“参数数量”。咱们逐行用大白话解释:
第一部分:各层的具体信息(表格)
表格里的具体行:
Linear-1(第一个全连接层)
Output Shape [-1, 10]:不管输入多少个样本(-1 代表批量大小),每个样本经过这层后会变成10个特征的数据。
Param # 50:这层有50个参数。计算方式是:输入4个特征,输出10个特征 → 权重参数是 10×4=40(类似“4转10的转换规则”),再加上10个偏置参数(每个输出特征一个),总共 40+10=50。
ReLU-2(激活函数层)
Output Shape [-1, 10]:激活函数不会改变数据的特征数量,所以输出还是10个特征。
Param # 0:ReLU激活函数(比如 max(0, x))没有需要学习的参数,只是单纯的数学运算,所以参数数为0。
Linear-3(第二个全连接层)
Output Shape [-1, 3]:每个样本经过这层后会变成3个特征的数据(比如对应3分类任务的结果)。
Param # 33:输入是前一层的10个特征,输出3个特征 → 权重参数是 3×10=30,加上3个偏置参数,总共 30+3=33。
第二部分:模型整体统计(表格下方)
Total params: 83:整个模型所有层的参数总和(50+0+33=83)。
Trainable params: 83:所有参数都可以被训练(比如用数据调整这些参数的值)。
Non-trainable params: 0:没有“冻结”的参数(有些模型会固定部分参数不训练,这里没有这种情况)。
第三部分:内存占用估计(最下方)
这部分是模型运行时的内存消耗估算(输入数据、计算过程、参数本身占用的内存)。这里显示 0.00 可能是因为输入数据的尺寸太小(比如输入是4维的小数),实际项目中如果输入数据很大(如图像),这里会显示具体数值。
总结:这个摘要是模型的“透明化报告”,让你一眼看清每一层怎么“加工数据”,以及模型总共需要学多少参数(参数越多,模型“记忆能力”越强,但可能越容易过拟合)。
该方法不显示输入层的尺寸,因为输入的神经网是自己设置的,所以不需要显示输入层的尺寸。但是在使用该方法时,input size=(4,)参数是必需的,因为PyTorch需要知道输入数据的形状才能推断模型各层的输出形状和参数数量。
这是因为PyTorch的模型在定义时是动态的,它不会预先知道输入数据的具体形状。nn.Linear(4,10)只定义了"输入维度是4,输出维度是10”,但不知道输入的批量大小和其他维度,比如卷积层需要知道输入的通道数、高度、宽度等信息。-并非所有输入数据都是结构化数据
因此,要生成模型摘要(如每层的输出形状、参数数量),必须提供一个示例输入形状,让PyTorch"运行”一次模型,从而推断出各层的信息。
summary函数的核心逻辑是:
创建一个与input size形状匹配的虚拟输入张量(通常填充零)
将虚拟输入传递给模型,执行一次前向传播(但不计算梯度)
记录每一层的输入和输出形状,以及参数数量
生成可读的摘要报告
构建神经网络的时候
输入层不需要写:x多少个特征输入层就有多少神经元
隐藏层需要写,从第一个隐藏层可以看出特征的个数
输出层的神经元和任务有关,比如分类任务,输出层有3个神经元,一个对应每个类别
上图只是帮助你理解,和上述架构不同,每条线记录权重w,在每个神经元内计算并且输出Relu(w*x+b)
可以看做,线记录权重,神经元记录偏置和损失函数
可学习参数计算
Linear-1对应self.fc1=nn.Linear(4,10),表明前一层有4个神经元,这一层有10个神经元,每2个神经元之间靠着线相连,所有有4*10个权重参数+10个偏置参数=50个参数
rlu层不涉及可学习参数,可以把它和前一个线性层看成一层,图上也是这个含义
Linear-3层对应代码self.fc2=nn.Linear(10,3),10*3个权重参数+3个偏置=33个参数
总参数83个,占用内存几乎为0
torchinfo是提供比torchsummary更详细的模型摘要信息,包括每层的输入输出形状、参数数量、计算量等。
torchinfo是提供比torchsummary更详细的模型摘要信息,包括每层的输入输出形状、参数数
量、计算量等。
进度条功能:
我们介绍下tgdm这个库,他非常适合用在循环中观察进度。尤其在深度学习这种训练是循环的场景中。他最核心的逻辑如下:创建一个进度条对象,并传入总迭代次数。一般用wth语句创建对象,这样对象会在with语句结束后自动销毁,保证资源释放。wth是常见的上下文管理器,这样的使用方式还有用with打开文件,结束后会自动关闭文件。
更新进度条,通过pbar.update(n)指定每次前进的步数n(适用于非固定步长的循环)。
手动更新:
from tqdm import tqdm # 先导入tqdm库
import time # 用于模拟耗时操作# 创建一个总步数为10的进度条
with tqdm(total=10) as pbar: # pbar是进度条对象的变量名# pbar 是 progress bar(进度条)的缩写,约定俗成的命名习惯。for i in range(10): # 循环10次(对应进度条的10步)time.sleep(0.5) # 模拟每次循环耗时0.5秒pbar.update(1) # 每次循环后,进度条前进1步from tqdm import tqdm
import time# 创建进度条时添加描述(desc)和单位(unit)
with tqdm(total=5, desc="下载文件", unit="个") as pbar:# 进度条这个对象,可以设置描述和单位# desc是描述,在左侧显示# unit是单位,在进度条右侧显示for i in range(5):time.sleep(1)pbar.update(1) # 每次循环进度+1unit参数的核心作用是明确进度条中每个进度单位的含义,使可视化信息更具可读性。在深度学习训练中,常用的单位包括:epoch:训练轮次(遍历整个数据集一次)。
batch:批次(每次梯度更新处理的样本组)。
sample:样本(单个数据点)
自动更新:
from tqdm import tqdm
import time# 直接将range(3)传给tqdm,自动生成进度条
# 这个写法我觉得是有点神奇的,直接可以给这个对象内部传入一个可迭代对象,然后自动生成进度条
for i in tqdm(range(3), desc="处理任务", unit="epoch"):time.sleep(1)for i in tqdm(range(3),desc="处理任务",unit="个")这个写法则不需要在循环中调用update()方法,更加简洁
实际上这2种写法都随意选取,这里都介绍下 # 用tqdm的set_postfix方法在进度条右侧显示实时数据(如当前循环的数值、计算结果等):
from tqdm import tqdm
import timetotal = 0 # 初始化总和
with tqdm(total=10, desc="累加进度") as pbar:for i in range(1, 11):time.sleep(0.3)total += i # 累加1+2+3+...+10pbar.update(1) # 进度+1pbar.set_postfix({"当前总和": total}) # 显示实时总和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 # 导入tqdm库用于进度条显示# 设置GPU设备
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)# 将数据转换为PyTorch张量并移至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)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 # 训练的轮数# 用于存储每100个epoch的损失值和对应的epoch数
losses = []
epochs = []start_time = time.time() # 记录开始时间# 创建tqdm进度条
with tqdm(total=num_epochs, desc="训练进度", unit="epoch") as pbar:# 训练模型for epoch in range(num_epochs):# 前向传播outputs = model(X_train) # 隐式调用forward函数loss = criterion(outputs, y_train)# 反向传播和优化optimizer.zero_grad()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) # 更新进度条# 确保进度条达到100%if pbar.n < num_epochs:pbar.update(num_epochs - pbar.n) # 计算剩余的进度并更新time_all = time.time() - start_time # 计算训练时间
print(f'Training time: {time_all:.2f} seconds')# # 可视化损失曲线
# plt.figure(figsize=(10, 6))
# plt.plot(epochs, losses)
# plt.xlabel('Epoch')
# plt.ylabel('Loss')
# plt.title('Training Loss over Epochs')
# plt.grid(True)
# plt.show()
模型的推理:
之前我们说完了训练模型,那么现在我们来测试模型。测试这个词在大模型领域叫做推理(inference),意味着把数据输入到训练好的模型的过程。
注意损失和优化器在训练阶段。
“注意损失和优化器在训练阶段”是指:在深度学习模型的训练阶段(而非测试/评估阶段),损失函数(Loss Function)和优化器(Optimizer)是核心组件,用于更新模型参数;而在测试/评估阶段不需要这两个组件。
具体解释如下:
1. 损失函数(Loss Function)的作用
损失函数用于衡量模型预测值与真实值之间的误差。在训练阶段,我们需要通过损失函数计算当前模型的“错误程度”(例如分类任务常用交叉熵损失)。这个损失值会作为信号,指导优化器调整模型参数。
2. 优化器(Optimizer)的作用
优化器根据损失函数计算出的误差,通过反向传播(Backward Propagation)更新模型的权重参数(例如SGD、Adam等优化器)。训练的核心目标就是通过优化器不断调整参数,使得损失值逐渐降低,模型性能提升。
3. 为什么测试阶段不需要损失和优化器?
测试/评估阶段的目标是验证模型在未见过数据上的表现(如你提供的代码中的model.eval()和torch.no_grad())。此时模型参数已经固定(通过训练阶段优化完成),只需前向传播得到预测结果(如outputs = model(X_test)),不需要计算损失(因为不关心“误差多大”,只关心“预测是否正确”),也不需要优化器(因为不更新参数)。与你当前代码的关系
提供的代码是测试阶段的评估逻辑,因此没有出现损失函数和优化器的代码。但在训练阶段(例如
for epoch in range(num_epochs)的循环中),一定会包含类似以下逻辑:# 训练阶段示例代码(非修改你的原代码)
model.train() # 开启训练模式
optimizer.zero_grad() # 清空优化器梯度
outputs = model(X_train) # 前向传播
loss = criterion(outputs, y_train) # 计算损失(criterion是损失函数)
loss.backward() # 反向传播计算梯度
optimizer.step() # 优化器更新参数
# 在测试集上评估模型,此时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相关)。所以为了避免这种需求不被满足,还是需要手动关闭梯度计算。
@浙大疏锦行