DAY 35 模型可视化与推理

发布于:2025-06-19 ⋅ 阅读:(19) ⋅ 点赞:(0)

@浙大疏锦行https://blog.csdn.net/weixin_45655710知识点回顾:

  1. 三种不同的模型可视化方法:推荐torchinfo打印summary+权重分布可视化
  2. 进度条功能:手动和自动写法,让打印结果更加美观
  3. 推理的写法:评估模式

作业:调整模型定义时的超参数,对比下效果。

这里的“超参数”指的是在模型开始训练之前,由我们(人)手动设置的参数,它们决定了模型的“骨架”和学习方式。与之相对的是模型在训练过程中自己学习的参数(如权重w和偏置b)。

对于这个MLP模型,我们可以调整的结构性超参数主要有:

  1. 隐藏层的宽度:目前隐藏层有 10 个神经元 (nn.Linear(4, 10))。我们可以把它变宽(如20个)或变窄(如5个)。
  2. 隐藏层的深度:目前只有1个隐藏层。我们可以增加更多隐藏层,把模型变得更“深”。
  3. 激活函数:目前用的是ReLU。我们也可以换成其他的激活函数,如Sigmoid, Tanh等(虽然ReLU通常是很好的选择)。
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
import numpy as np
from tqdm import tqdm # 导入tqdm库用于进度条显示

# --- 1. 数据准备 (保持不变) ---
# 设置设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")

# 加载并处理数据
iris = load_iris()
X_data = iris.data
y_data = iris.target
X_train_raw, X_test_raw, y_train, y_test = train_test_split(X_data, y_data, test_size=0.2, random_state=42)

scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train_raw)
X_test_scaled = scaler.transform(X_test_raw)

X_train = torch.FloatTensor(X_train_scaled).to(device)
y_train = torch.LongTensor(y_train).to(device)
X_test = torch.FloatTensor(X_test_scaled).to(device)
y_test = torch.LongTensor(y_test).to(device)


# --- 2. 定义一个更灵活的模型 ---
class MLP(nn.Module):
    # 【修改】初始化函数接收层大小作为参数
    def __init__(self, input_size, hidden_size, output_size):
        super(MLP, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        return out

# --- 3. 将训练、评估、可视化的完整流程封装成一个函数 ---
def run_experiment(hidden_size, learning_rate, num_epochs):
    """
    运行一次完整的训练、评估和可视化流程。
    """
    print(f"\n{'='*20} 实验开始 {'='*20}")
    print(f"参数: hidden_size={hidden_size}, learning_rate={learning_rate}, num_epochs={num_epochs}")
    
    # 实例化模型并移至设备
    model = MLP(input_size=4, hidden_size=hidden_size, output_size=3).to(device)
    print("\n--- 模型结构 ---")
    print(model)

    # 定义损失函数和优化器
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(model.parameters(), lr=learning_rate)
    
    # --- 训练模型 (带tqdm进度条) ---
    start_time = time.time()
    losses = []
    
    # 使用tqdm包装range来自动显示进度条
    for epoch in tqdm(range(num_epochs), desc="训练进度", unit="epoch"):
        outputs = model(X_train)
        loss = criterion(outputs, y_train)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        # 每100轮记录一次损失值并更新进度条后缀
        if (epoch + 1) % 100 == 0:
            losses.append(loss.item())
            # 更新进度条右侧的实时损失显示
            # pbar.set_postfix({'Loss': f'{loss.item():.4f}'}) 
            # 上面的写法在tqdm直接包装可迭代对象时不能直接用,但tqdm会自动显示速率,效果类似
    
    end_time = time.time()
    print(f"训练耗时: {end_time - start_time:.2f} 秒")

    # --- 评估模型 ---
    model.eval()
    with torch.no_grad():
        outputs = model(X_test)
        _, predicted = torch.max(outputs, 1)
        correct = (predicted == y_test).sum().item()
        accuracy = correct / y_test.size(0)
        print(f"测试集准确率: {accuracy * 100:.2f}%")
        
    # --- 权重可视化与统计 ---
    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=(12, 4))
    fig.suptitle(f'权重分布 (hidden_size={hidden_size})')

    for i, (name, weights) in enumerate(weight_data.items()):
        weights_flat = weights.flatten()
        axes[i].hist(weights_flat, bins=30, alpha=0.75)
        axes[i].set_title(name)
        axes[i].set_xlabel('权重值')
        axes[i].set_ylabel('频率')
        axes[i].grid(True, linestyle='--')
    
    plt.tight_layout(rect=[0, 0, 1, 0.96]) # 调整布局为标题留出空间
    plt.show()

    print("\n--- 权重统计信息 ---")
    for name, weights in weight_data.items():
        mean, std = np.mean(weights), np.std(weights)
        min_val, max_val = np.min(weights), np.max(weights)
        print(f"{name}:")
        print(f"  均值: {mean:.4f}, 标准差: {std:.4f}, 最小值: {min_val:.4f}, 最大值: {max_val:.4f}")
    
    print(f"{'='*20} 实验结束 {'='*20}\n")


# --- 4. 运行不同超参数的实验进行对比 ---
if __name__ == "__main__":
    # 实验1:原始参数
    run_experiment(hidden_size=10, learning_rate=0.01, num_epochs=20000)
    
    # 实验2:调整后的参数(例如:增加隐藏层神经元数量,稍微增大学习率)
    run_experiment(hidden_size=32, learning_rate=0.05, num_epochs=20000)

 

 

  • 将模型、训练、评估和可视化封装成一个函数 run_experiment

    • 这样做的好处是代码复用性极高。我们想测试一组新的超参数,只需要再次调用这个函数并传入不同的参数即可,无需复制粘贴大量代码。
    • 函数的参数包括 hidden_size (隐藏层神经元数量), learning_rate (学习率), 和 num_epochs (训练轮数),这些都是最常调整的超参数。
  • 模型定义的灵活性

    • MLP 类的 __init__ 方法被修改为可以接收 input_size, hidden_size, 和 output_size。这使得我们可以通过向 run_experiment 函数传递不同的 hidden_size 来轻松地改变网络结构。
  • 实现了进度条功能

    • run_experiment 函数的训练循环中,我们使用了 for epoch in tqdm(range(num_epochs), ...)
    • 这种“自动更新”的 tqdm 写法非常简洁,它会自动包装 range(num_epochs) 这个可迭代对象,并在每次循环时更新进度条,同时显示迭代速率和预计剩余时间。
  • 实现了权重可视化与统计功能

    • 这部分逻辑被完整地保留在了 run_experiment 函数的末尾。
    • 每次实验结束后,它都会自动绘制出当前模型(fc1.weightfc2.weight)的权重分布直方图,并打印出详细的统计信息(均值、标准差、最值)。
  • 进行了超参数对比

    • if __name__ == "__main__": 部分,我们先后调用了两次 run_experiment
    • 第一次使用和您之前代码基本一致的参数 (hidden_size=10, lr=0.01) 作为基线 (Baseline)
    • 第二次我们调整了参数,将隐藏层扩大到32个神经元,并增大了学习率到0.05,作为对比实验

网站公告

今日签到

点亮在社区的每一天
去签到