一、单通道图片的规范写法
1. 规范写法
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pyplot as plt import numpy as np # 设置中文字体支持 plt.rcParams["font.family"] = ["SimHei"] plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 # 1. 数据预处理 transform = transforms.Compose([ transforms.ToTensor(), # 转换为张量并归一化到[0,1] transforms.Normalize((0.1307,), (0.3081,)) # MNIST数据集的均值和标准差 ]) # 2. 加载MNIST数据集 train_dataset = datasets.MNIST( root='./data', train=True, download=True, transform=transform ) test_dataset = datasets.MNIST( root='./data', train=False, transform=transform ) # 3. 创建数据加载器 batch_size = 64 # 每批处理64个样本 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) # 4. 定义模型、损失函数和优化器 class MLP(nn.Module): def __init__(self): super(MLP, self).__init__() self.flatten = nn.Flatten() # 将28x28的图像展平为784维向量 self.layer1 = nn.Linear(784, 128) # 第一层:784个输入,128个神经元 self.relu = nn.ReLU() # 激活函数 self.layer2 = nn.Linear(128, 10) # 第二层:128个输入,10个输出(对应10个数字类别) def forward(self, x): x = self.flatten(x) # 展平图像 x = self.layer1(x) # 第一层线性变换 x = self.relu(x) # 应用ReLU激活函数 x = self.layer2(x) # 第二层线性变换,输出logits return x # 检查GPU是否可用 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 初始化模型 model = MLP() model = model.to(device) # 将模型移至GPU(如果可用) criterion = nn.CrossEntropyLoss() # 交叉熵损失函数,适用于多分类问题 optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam优化器 # 5. 训练模型(记录每个 iteration 的损失) def train(model, train_loader, test_loader, criterion, optimizer, device, epochs): model.train() # 设置为训练模式 # 新增:记录每个 iteration 的损失 all_iter_losses = [] # 存储所有 batch 的损失 iter_indices = [] # 存储 iteration 序号(从1开始) for epoch in range(epochs): running_loss = 0.0 correct = 0 total = 0 for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) # 移至GPU(如果可用) optimizer.zero_grad() # 梯度清零 output = model(data) # 前向传播 loss = criterion(output, target) # 计算损失 loss.backward() # 反向传播 optimizer.step() # 更新参数 # 记录当前 iteration 的损失(注意:这里直接使用单 batch 损失,而非累加平均) iter_loss = loss.item() all_iter_losses.append(iter_loss) iter_indices.append(epoch * len(train_loader) + batch_idx + 1) # iteration 序号从1开始 # 统计准确率和损失(原逻辑保留,用于 epoch 级统计) running_loss += iter_loss _, predicted = output.max(1) total += target.size(0) correct += predicted.eq(target).sum().item() # 每100个批次打印一次训练信息(可选:同时打印单 batch 损失) if (batch_idx + 1) % 100 == 0: print(f'Epoch: {epoch+1}/{epochs} | Batch: {batch_idx+1}/{len(train_loader)} ' f'| 单Batch损失: {iter_loss:.4f} | 累计平均损失: {running_loss/(batch_idx+1):.4f}') # 原 epoch 级逻辑(测试、打印 epoch 结果)不变 epoch_train_loss = running_loss / len(train_loader) epoch_train_acc = 100. * correct / total epoch_test_loss, epoch_test_acc = test(model, test_loader, criterion, device) print(f'Epoch {epoch+1}/{epochs} 完成 | 训练准确率: {epoch_train_acc:.2f}% | 测试准确率: {epoch_test_acc:.2f}%') # 绘制所有 iteration 的损失曲线 plot_iter_losses(all_iter_losses, iter_indices) # 保留原 epoch 级曲线(可选) # plot_metrics(train_losses, test_losses, train_accuracies, test_accuracies, epochs) return epoch_test_acc # 返回最终测试准确率先不写早停策略,因为规范的早停策略需要用到验证集,一般还需要划分测试集
1. 划分数据集:训练集(用于训练)、验证集(用于早停和调参)、测试集(用于最终报告性能)。
2. 在训练过程中,使用验证集触发早停。
3. 训练结束后,仅用测试集运行一次测试函数,得到最终准确率。
测试函数和绘图函数均被封装在了train函数中,但是test和绘图函数在定义train函数之后,这是因为在 Python 中,函数定义的顺序不影响调用,只要在调用前已经完成定义即可。
# 6. 测试模型 def test(model, test_loader, criterion, device): model.eval() # 设置为评估模式 test_loss = 0 correct = 0 total = 0 with torch.no_grad(): # 不计算梯度,节省内存和计算资源 for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) test_loss += criterion(output, target).item() _, predicted = output.max(1) total += target.size(0) correct += predicted.eq(target).sum().item() avg_loss = test_loss / len(test_loader) accuracy = 100. * correct / total return avg_loss, accuracy # 返回损失和准确率 # 7.绘制每个 iteration 的损失曲线 def plot_iter_losses(losses, indices): plt.figure(figsize=(10, 4)) plt.plot(indices, losses, 'b-', alpha=0.7, label='Iteration Loss') plt.xlabel('Iteration(Batch序号)') plt.ylabel('损失值') plt.title('每个 Iteration 的训练损失') plt.legend() plt.grid(True) plt.tight_layout() plt.show() # 8. 执行训练和测试(设置 epochs=2 验证效果) epochs = 2 print("开始训练模型...") final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, device, epochs) print(f"训练完成!最终测试准确率: {final_accuracy:.2f}%")开始训练模型... Epoch: 1/2 | Batch: 100/938 | 单Batch损失: 0.3583 | 累计平均损失: 0.6321 Epoch: 1/2 | Batch: 200/938 | 单Batch损失: 0.2035 | 累计平均损失: 0.4776 Epoch: 1/2 | Batch: 300/938 | 单Batch损失: 0.3044 | 累计平均损失: 0.4053 Epoch: 1/2 | Batch: 400/938 | 单Batch损失: 0.1427 | 累计平均损失: 0.3669 Epoch: 1/2 | Batch: 500/938 | 单Batch损失: 0.1742 | 累计平均损失: 0.3321 Epoch: 1/2 | Batch: 600/938 | 单Batch损失: 0.3089 | 累计平均损失: 0.3104 Epoch: 1/2 | Batch: 700/938 | 单Batch损失: 0.0456 | 累计平均损失: 0.2921 Epoch: 1/2 | Batch: 800/938 | 单Batch损失: 0.1008 | 累计平均损失: 0.2763 Epoch: 1/2 | Batch: 900/938 | 单Batch损失: 0.3017 | 累计平均损失: 0.2629 Epoch 1/2 完成 | 训练准确率: 92.43% | 测试准确率: 95.90% Epoch: 2/2 | Batch: 100/938 | 单Batch损失: 0.1727 | 累计平均损失: 0.1358 Epoch: 2/2 | Batch: 200/938 | 单Batch损失: 0.1767 | 累计平均损失: 0.1291 Epoch: 2/2 | Batch: 300/938 | 单Batch损失: 0.1239 | 累计平均损失: 0.1283 Epoch: 2/2 | Batch: 400/938 | 单Batch损失: 0.2098 | 累计平均损失: 0.1233 Epoch: 2/2 | Batch: 500/938 | 单Batch损失: 0.0214 | 累计平均损失: 0.1206 Epoch: 2/2 | Batch: 600/938 | 单Batch损失: 0.0557 | 累计平均损失: 0.1190 Epoch: 2/2 | Batch: 700/938 | 单Batch损失: 0.0964 | 累计平均损失: 0.1169 Epoch: 2/2 | Batch: 800/938 | 单Batch损失: 0.1627 | 累计平均损失: 0.1152 Epoch: 2/2 | Batch: 900/938 | 单Batch损失: 0.0743 | 累计平均损失: 0.1138 Epoch 2/2 完成 | 训练准确率: 96.64% | 测试准确率: 96.88%在 PyTorch 中处理张量(Tensor)时,以下是关于展平(Flatten)、维度调整(如 view/reshape)等操作的关键点,这些操作通常不会影响第一个维度(即批量维度 batch_size):
2. 图像任务中的张量形状
输入张量的形状通常为:(batch_size, channels, height, width)例如:(batch_size, 3, 28, 28)其中,batch_size代表一次输入的样本数量。
3. NLP 任务中的张量形状
输入张量的形状可能为:(batch_size, sequence_length)此时,batch_size同样是第一个维度。
3.1 Flatten 操作
- 功能:将张量展平为一维数组,但保留批量维度。
- 示例:
- 输入形状:
(batch_size, 3, 28, 28)(图像数据) - Flatten 后形状:
(batch_size, 3×28×28) = (batch_size, 2352) - 说明:第一个维度
batch_size不变,后面的所有维度被展平为一个维度。
- 输入形状:
3.2 view/reshape 操作
- 功能:调整张量维度,但必须显式保留或指定批量维度。
- 示例:
- 输入形状:
(batch_size, 3, 28, 28) - 调整为:
(batch_size, -1) - 结果:展平为两个维度,保留
batch_size,第二个维度自动计算为3×28×28=2352
- 输入形状:
总结
- 批量维度不变性:无论进行 flatten、view 还是 reshape 操作,第一个维度
batch_size通常保持不变。 - 动态维度指定:使用
-1让 PyTorch 自动计算该维度的大小,但需确保其他维度的指定合理,避免形状不匹配错误。
二、彩色图片的规范写法
彩色的通道也是在第一步被直接展平,其他代码一致
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pyplot as plt import numpy as np # 设置中文字体支持 plt.rcParams["font.family"] = ["SimHei"] plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 # 1. 数据预处理 transform = transforms.Compose([ transforms.ToTensor(), # 转换为张量 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 标准化处理 ]) # 2. 加载CIFAR-10数据集(修改root为压缩包所在文件夹) train_dataset = datasets.CIFAR10( root=r"D:\PythonStudy", # 压缩包所在的文件夹路径(关键!不是压缩包本身) train=True, download=True, # 检测到文件夹内有压缩包时,仅解压不下载 transform=transform ) test_dataset = datasets.CIFAR10( root=r"D:\PythonStudy", # 同训练集的root路径 train=False, transform=transform ) # 3. 创建数据加载器 batch_size = 64 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) # 4. 定义MLP模型(适应CIFAR-10的输入尺寸) class MLP(nn.Module): def __init__(self): super(MLP, self).__init__() self.flatten = nn.Flatten() # 将3x32x32的图像展平为3072维向量 self.layer1 = nn.Linear(3072, 512) # 第一层:3072个输入,512个神经元 self.relu1 = nn.ReLU() self.dropout1 = nn.Dropout(0.2) # 添加Dropout防止过拟合 self.layer2 = nn.Linear(512, 256) # 第二层:512个输入,256个神经元 self.relu2 = nn.ReLU() self.dropout2 = nn.Dropout(0.2) self.layer3 = nn.Linear(256, 10) # 输出层:10个类别 def forward(self, x): # 第一步:将输入图像展平为一维向量 x = self.flatten(x) # 输入尺寸: [batch_size, 3, 32, 32] → [batch_size, 3072] # 第一层全连接 + 激活 + Dropout x = self.layer1(x) # 线性变换: [batch_size, 3072] → [batch_size, 512] x = self.relu1(x) # 应用ReLU激活函数 x = self.dropout1(x) # 训练时随机丢弃部分神经元输出 # 第二层全连接 + 激活 + Dropout x = self.layer2(x) # 线性变换: [batch_size, 512] → [batch_size, 256] x = self.relu2(x) # 应用ReLU激活函数 x = self.dropout2(x) # 训练时随机丢弃部分神经元输出 # 第三层(输出层)全连接 x = self.layer3(x) # 线性变换: [batch_size, 256] → [batch_size, 10] return x # 返回未经过Softmax的logits # 检查GPU是否可用 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 初始化模型 model = MLP() model = model.to(device) # 将模型移至GPU(如果可用) criterion = nn.CrossEntropyLoss() # 交叉熵损失函数 optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam优化器 # 5. 训练模型(记录每个 iteration 的损失) def train(model, train_loader, test_loader, criterion, optimizer, device, epochs): model.train() # 设置为训练模式 # 记录每个 iteration 的损失 all_iter_losses = [] # 存储所有 batch 的损失 iter_indices = [] # 存储 iteration 序号 for epoch in range(epochs): running_loss = 0.0 correct = 0 total = 0 for batch_idx, (data, target) in enumerate(train_loader): data, target = data.to(device), target.to(device) # 移至GPU optimizer.zero_grad() # 梯度清零 output = model(data) # 前向传播 loss = criterion(output, target) # 计算损失 loss.backward() # 反向传播 optimizer.step() # 更新参数 # 记录当前 iteration 的损失 iter_loss = loss.item() all_iter_losses.append(iter_loss) iter_indices.append(epoch * len(train_loader) + batch_idx + 1) # 统计准确率和损失 running_loss += iter_loss _, predicted = output.max(1) total += target.size(0) correct += predicted.eq(target).sum().item() # 每100个批次打印一次训练信息 if (batch_idx + 1) % 100 == 0: print(f'Epoch: {epoch+1}/{epochs} | Batch: {batch_idx+1}/{len(train_loader)} ' f'| 单Batch损失: {iter_loss:.4f} | 累计平均损失: {running_loss/(batch_idx+1):.4f}') # 计算当前epoch的平均训练损失和准确率 epoch_train_loss = running_loss / len(train_loader) epoch_train_acc = 100. * correct / total # 测试阶段 model.eval() # 设置为评估模式 test_loss = 0 correct_test = 0 total_test = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) test_loss += criterion(output, target).item() _, predicted = output.max(1) total_test += target.size(0) correct_test += predicted.eq(target).sum().item() epoch_test_loss = test_loss / len(test_loader) epoch_test_acc = 100. * correct_test / total_test print(f'Epoch {epoch+1}/{epochs} 完成 | 训练准确率: {epoch_train_acc:.2f}% | 测试准确率: {epoch_test_acc:.2f}%') # 绘制所有 iteration 的损失曲线 plot_iter_losses(all_iter_losses, iter_indices) return epoch_test_acc # 返回最终测试准确率 # 6. 绘制每个 iteration 的损失曲线 def plot_iter_losses(losses, indices): plt.figure(figsize=(10, 4)) plt.plot(indices, losses, 'b-', alpha=0.7, label='Iteration Loss') plt.xlabel('Iteration(Batch序号)') plt.ylabel('损失值') plt.title('每个 Iteration 的训练损失') plt.legend() plt.grid(True) plt.tight_layout() plt.show() # 7. 执行训练和测试 epochs = 20 # 增加训练轮次以获得更好效果 print("开始训练模型...") final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, device, epochs) print(f"训练完成!最终测试准确率: {final_accuracy:.2f}%")Files already downloaded and verifiedh 开始训练模型... Epoch: 1/20 | Batch: 100/782 | 单Batch损失: 1.7633 | 累计平均损失: 1.8848 Epoch: 1/20 | Batch: 200/782 | 单Batch损失: 1.9317 | 累计平均损失: 1.8323 Epoch: 1/20 | Batch: 300/782 | 单Batch损失: 1.9207 | 累计平均损失: 1.7920 Epoch: 1/20 | Batch: 400/782 | 单Batch损失: 1.7856 | 累计平均损失: 1.7630 Epoch: 1/20 | Batch: 500/782 | 单Batch损失: 1.5690 | 累计平均损失: 1.7400 Epoch: 1/20 | Batch: 600/782 | 单Batch损失: 1.7203 | 累计平均损失: 1.7281 Epoch: 1/20 | Batch: 700/782 | 单Batch损失: 1.7999 | 累计平均损失: 1.7134 Epoch 1/20 完成 | 训练准确率: 39.27% | 测试准确率: 45.65% Epoch: 2/20 | Batch: 100/782 | 单Batch损失: 1.5004 | 累计平均损失: 1.4784 Epoch: 2/20 | Batch: 200/782 | 单Batch损失: 1.3700 | 累计平均损失: 1.4817 Epoch: 2/20 | Batch: 300/782 | 单Batch损失: 1.5466 | 累计平均损失: 1.4726 Epoch: 2/20 | Batch: 400/782 | 单Batch损失: 1.5477 | 累计平均损失: 1.4704 Epoch: 2/20 | Batch: 500/782 | 单Batch损失: 1.4483 | 累计平均损失: 1.4638 Epoch: 2/20 | Batch: 600/782 | 单Batch损失: 1.3341 | 累计平均损失: 1.4603 Epoch: 2/20 | Batch: 700/782 | 单Batch损失: 1.4533 | 累计平均损失: 1.4589 Epoch 2/20 完成 | 训练准确率: 48.61% | 测试准确率: 49.78% Epoch: 3/20 | Batch: 100/782 | 单Batch损失: 1.3884 | 累计平均损失: 1.3452 Epoch: 3/20 | Batch: 200/782 | 单Batch损失: 1.3260 | 累计平均损失: 1.3438 Epoch: 3/20 | Batch: 300/782 | 单Batch损失: 1.4021 | 累计平均损失: 1.3449 Epoch: 3/20 | Batch: 400/782 | 单Batch损失: 1.4087 | 累计平均损失: 1.3447 Epoch: 3/20 | Batch: 500/782 | 单Batch损失: 1.5543 | 累计平均损失: 1.3442 Epoch: 3/20 | Batch: 600/782 | 单Batch损失: 1.3573 | 累计平均损失: 1.3468 Epoch: 3/20 | Batch: 700/782 | 单Batch损失: 1.2851 | 累计平均损失: 1.3443 ... Epoch: 20/20 | Batch: 500/782 | 单Batch损失: 0.4452 | 累计平均损失: 0.3734 Epoch: 20/20 | Batch: 600/782 | 单Batch损失: 0.1809 | 累计平均损失: 0.3755 Epoch: 20/20 | Batch: 700/782 | 单Batch损失: 0.6064 | 累计平均损失: 0.3807 Epoch 20/20 完成 | 训练准确率: 86.41% | 测试准确率: 52.56%由于深度mlp的参数过多,为了避免过拟合在这里引入了dropout这个操作,他可以在训练阶段随机丢弃一些神经元,避免过拟合情况。dropout的取值也是超参数。
在测试阶段,由于开启了eval模式,会自动关闭dropout。
# 7. 执行训练和测试 epochs = 20 # 增加训练轮次以获得更好效果 print("开始训练模型...") final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, device, epochs) print(f"训练完成!最终测试准确率: {final_accuracy:.2f}%")三、测试与训练的核心逻辑
1. 训练逻辑
train()函数是核心,整体是「多轮(epoch)迭代 + 每轮分批(batch)训练」:
单轮训练(1 个 epoch)的流程
遍历训练集的每个batch → 数据移到GPU(如果有)→ 梯度清零 → 前向传播算预测值 → 算损失 → 反向传播算梯度 → 优化器更新参数 → 记录损失/准确率- 梯度清零:
optimizer.zero_grad(),避免上一个 batch 的梯度累积; - 前向传播:
output = model(data),模型根据输入算预测结果; - 损失计算:
loss = criterion(output, target),用交叉熵损失衡量 “预测值和真实标签的差距”; - 反向传播:
loss.backward(),从损失出发,反向计算每个参数的梯度(告诉模型 “该怎么调整参数”); - 参数更新:
optimizer.step(),Adam 优化器根据梯度调整模型参数,让损失变小。
额外记录:
- 把每个 batch(iteration)的损失都存下来,最后画曲线,能直观看到 “每个批次训练后损失的变化”(比只看 epoch 平均损失更细)。
2. 测试逻辑
test()函数是评估模型效果,核心区别:
model.eval():模型切换到评估模式(不会更新参数,也会关闭 Dropout 等训练特有的层);with torch.no_grad():关闭梯度计算(节省内存、加快速度,因为测试不需要反向传播);- 遍历测试集所有 batch,计算平均损失和准确率,衡量模型 “泛化能力”(能不能识别没见过的图片)。
3、关键细节补充
- 设备适配:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu"),自动用 GPU(如果有),训练速度会快很多; - 优化器选择:Adam 是目前最常用的优化器,比 SGD 收敛更快,学习率设 0.001 是 MNIST 任务的常规值;
- epochs=2:只训练 2 轮,是为了快速验证效果(实际要训练更多轮,比如 10 轮,准确率会更高)。
总结
整个过程的核心是 “通过反向传播和优化器,让模型的预测结果越来越接近真实标签”,而测试则是验证模型是否真的学会了 “识别数字”,而非 “记住训练集”。
勇闯python的第41天@浙大疏锦行