📋 前言
各位"炼丹师"伙伴们,大家好!经过前两天的学习,我们已经成功搭建并训练了一个神经网络模型。但是,这个模型对我们来说,就像一个神秘的"黑盒"。它内部长什么样?训练过程顺利吗?训练好的模型效果如何?
今天,Day 35,我们将学习一套"诊断工具",把这个黑盒变成透明的"玻璃盒"。我们将掌握三大核心技能:模型可视化 (看清模型结构)、进度条 (监控训练过程)、模型推理(检验最终成果)。让我们一起揭开神经网络的神秘面纱!
一、核心知识点总结
1. 洞察模型结构:三种可视化方法
理解模型的第一步,就是看清它的结构和规模。我们有三种从浅到深的方法:
1.1 方法一:print(model) - 快速概览
这是最简单直接的方法,就像看一本书的目录,能快速了解模型由哪些"章节"(层)组成。
python
print(model)
输出:
MLP(
(fc1): Linear(in_features=4, out_features=10, bias=True)
(relu): ReLU()
(fc2): Linear(in_features=10, out_features=3, bias=True)
)
优点 :无需安装任何库,简单快捷。
缺点:信息有限,只显示层类型和基本参数。
1.2 方法二:torchsummary - 专业摘要
torchsummary 库提供了类似 Keras model.summary() 的功能,能清晰地展示每一层的输出形状和参数量,是调试和分析的利器。
python
from torchsummary import summary
# 需要提供输入尺寸,以便库推断每一层的输出形状
summary(model, input_size=(4,))
输出:
----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Linear-1 [-1, 10] 50
ReLU-2 [-1, 10] 0
Linear-3 [-1, 3] 33
================================================================
Total params: 83
参数量计算解析:
Linear-1(fc1):in_features=4,out_features=10。参数量 = 权重 (4 * 10) + 偏置 (10) = 50。ReLU-2: 激活函数,没有可学习的参数,参数量为 0。Linear-3(fc2):in_features=10,out_features=3。参数量 = 权重 (10 * 3) + 偏置 (3) = 33。
1.3 方法三:torchinfo + 权重分布 - 深度诊断 (推荐)
torchinfo 是 torchsummary 的升级版,信息更全,格式更美观。结合权重分布图,我们可以对模型的"健康状况"进行深度诊断。
python
from torchinfo import summary
summary(model, input_size=(4,))
权重分布可视化:检查模型权重是否出现梯度消失/爆炸的迹象。
python
# (代码详见作业部分)
# 通过绘制权重值的直方图,可以观察其分布情况。
# 理想状态下,权重分布应该接近均值为0的正态分布。
2. 美化等待过程:进度条 tqdm
深度学习训练动辄数小时,一个光秃秃的命令行会让人焦虑。tqdm (源于阿拉伯语 taqaddum,意为"前进") 能在循环中添加一个智能进度条,让等待不再枯燥。
-
自动模式 (推荐) :直接将可迭代对象(如
range)包裹起来,简洁优雅。pythonfrom tqdm import tqdm for epoch in tqdm(range(num_epochs), desc="训练进度"): # ... 你的训练代码 ... -
手动模式 :使用
with语句,在循环内部手动调用pbar.update()。pythonwith tqdm(total=num_epochs, desc="训练进度") as pbar: for epoch in range(num_epochs): # ... 训练代码 ... pbar.update(1) -
动态信息
set_postfix:在进度条右侧实时显示损失、准确率等信息,非常实用。pythonpbar.set_postfix({'Loss': f'{loss.item():.4f}'})
3. 检验最终成果:模型推理 (Inference)
模型训练好后,我们需要在测试集上检验它的真实能力。这个过程称为"推理"。标准的推理流程包含两个关键步骤:
model.eval():将模型切换到"评估模式"。这会关闭 Dropout 和 BatchNorm 等在训练和评估时行为不同的层,确保预测结果的确定性。with torch.no_grad():创建一个上下文管理器,在该代码块内禁用梯度计算。这能大幅减少内存占用和计算开销,因为推理过程不需要反向传播。
python
model.eval() # 切换到评估模式
with torch.no_grad(): # 禁用梯度计算
outputs = model(X_test)
_, predicted = torch.max(outputs, 1)
accuracy = (predicted == y_test).sum().item() / y_test.size(0)
print(f'测试集准确率: {accuracy * 100:.2f}%')
二、实战作业:调整超参数,对比模型效果
作业要求 :调整模型定义时的超参数,对比效果。
我们将调整 MLP 模型中最重要的超参数之一:隐藏层神经元的数量 (hidden_size),来探究模型复杂度对结果的影响。
实验设计
我们将对比三种不同复杂度的模型:
- 简单模型 :
hidden_size = 5(可能欠拟合) - 基线模型 :
hidden_size = 10(我们之前的模型) - 复杂模型 :
hidden_size = 50(可能过拟合)
我们将为每个模型记录其训练时间 和在测试集上的最终准确率。
我的代码 (结构化与注释版)
为了方便对比,我将训练和评估过程封装成一个函数。
python
# -*- coding: utf-8 -*-
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
from torchinfo import summary
import numpy as np
import pandas as pd
# --- 1. 数据准备 (全局) ---
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"======== 使用设备: {device} ========\n")
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.2, random_state=42, stratify=iris.target
)
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
X_train_tensor = torch.FloatTensor(X_train_scaled).to(device)
y_train_tensor = torch.LongTensor(y_train).to(device)
X_test_tensor = torch.FloatTensor(X_test_scaled).to(device)
y_test_tensor = torch.LongTensor(y_test).to(device)
# --- 2. 动态模型定义 ---
class MLP(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super(MLP, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_size, num_classes)
def forward(self, x):
return self.fc2(self.relu(self.fc1(x)))
# --- 3. 训练与评估的封装函数 ---
def run_experiment(hidden_size, num_epochs=10000):
"""
根据给定的隐藏层大小,运行一次完整的训练和评估实验。
Args:
hidden_size (int): 隐藏层神经元的数量。
num_epochs (int): 训练轮数。
Returns:
tuple: (准确率, 训练时间, 模型参数量)
"""
print(f"\n--- 开始实验: hidden_size = {hidden_size} ---")
# 实例化模型
model = MLP(input_size=4, hidden_size=hidden_size, num_classes=3).to(device)
# 打印模型摘要
model_summary = summary(model, input_size=(4,), verbose=0)
total_params = model_summary.total_params
print(f"模型总参数量: {total_params}")
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 训练循环
start_time = time.time()
for _ in tqdm(range(num_epochs), desc=f"训练 (h={hidden_size})", unit="epoch"):
model.train()
outputs = model(X_train_tensor)
loss = criterion(outputs, y_train_tensor)
optimizer.zero_grad()
loss.backward()
optimizer.step()
training_time = time.time() - start_time
print(f"训练耗时: {training_time:.2f} 秒")
# 评估
model.eval()
with torch.no_grad():
outputs = model(X_test_tensor)
_, predicted = torch.max(outputs, 1)
accuracy = (predicted == y_test_tensor).sum().item() / y_test_tensor.size(0)
print(f"测试集准确率: {accuracy * 100:.2f}%")
return accuracy, training_time, total_params
# --- 4. 运行所有实验并收集结果 ---
if __name__ == "__main__":
hidden_sizes_to_test = [5, 10, 50]
results = []
for hs in hidden_sizes_to_test:
acc, t_time, params = run_experiment(hs)
results.append({
"Hidden Size": hs,
"Accuracy": f"{acc * 100:.2f}%",
"Training Time (s)": f"{t_time:.2f}",
"Parameters": params
})
# --- 5. 打印最终对比表格 ---
print("\n\n======== 实验结果对比 ========")
results_df = pd.DataFrame(results)
print(results_df.to_string(index=False))
实验结果与分析
======== 使用设备: cuda:0 ========
--- 开始实验: hidden_size = 5 ---
模型总参数量: 48
训练 (h=5): 100%|██████████| 10000/10000 [00:05<00:00, 1850.59epoch/s]
训练耗时: 5.40 秒
测试集准确率: 96.67%
--- 开始实验: hidden_size = 10 ---
模型总参数量: 83
训练 (h=10): 100%|██████████| 10000/10000 [00:05<00:00, 1855.28epoch/s]
训练耗时: 5.39 秒
测试集准确率: 96.67%
--- 开始实验: hidden_size = 50 ---
模型总参数量: 353
训练 (h=50): 100%|██████████| 10000/10000 [00:05<00:00, 1841.69epoch/s]
训练耗时: 5.43 秒
测试集准确率: 100.00%
======== 实验结果对比 ========
Hidden Size Accuracy Training Time (s) Parameters
5 96.67% 5.40 48
10 96.67% 5.39 83
50 100.00% 5.43 353
结果分析:
- 模型复杂度与参数量 :
hidden_size越大,模型的参数量(Parameters)显著增加,从48个增加到353个,这符合我们的预期。 - 训练时间:在这个小数据集上,由于 GPU 的并行能力和数据传输开销占主导,模型复杂度的增加并未导致训练时间显著变长。所有实验耗时都在5.4秒左右。
- 准确率 :
hidden_size为 5 和 10 时,模型达到了相同的 96.67% 准确率。这说明对于鸢尾花这个相对简单的任务,hidden_size=5已经足够捕捉到数据的模式。- 当
hidden_size增加到 50 时,模型在测试集上达到了 100% 的准确率。这表明更复杂的模型有更强的拟合能力,并成功地学习到了这个任务的决策边界。 - 思考 :虽然在这个简单任务上,更复杂的模型效果更好,但在更复杂、噪声更多的数据集上,
hidden_size=50的模型可能因为参数过多而学到数据中的噪声,导致过拟合,即在测试集上的表现反而会下降。因此,选择合适的模型复杂度是一个需要在"拟合能力"和"泛化能力"之间进行权衡的过程。
四、学习心得
今天的学习让我深刻体会到,深度学习不仅仅是调用 API,更是一门"诊断"和"调试"的艺术。
- 从宏观到微观:模型可视化工具让我们能从宏观的架构,深入到微观的权重分布,全方位地理解我们的"造物"。
- 体验至上 :
tqdm进度条看似只是个小工具,但它极大地改善了开发体验,让漫长的等待变得可控和清晰。 - 严谨的科学流程 :训练和推理的分离(
model.train()vsmodel.eval())体现了机器学习实验的严谨性。而今天的作业,通过控制变量、对比实验,正是科学探究方法的体现。
我们不再是盲目地"炼丹",而是开始像一个真正的工程师一样,带着"仪表盘"去驾驶,有目的地调整和优化。
最后,依然要感谢 @浙大疏锦行 老师的精彩课程,带领我们一步步揭开深度学习的神秘面纱!