
PyTorch是一个开源的深度学习框架由 Facebook 的人工智能研究团队开发广泛应用于计算机视觉CV、自然语言处理NLP等领域。它以灵活性和易用性著称特别适合研究人员和开发者进行快速原型设计和实验。PyTorch 的模型训练通常包括以下步骤数据加载使用Dataset和DataLoader加载和预处理数据。模型定义通过继承torch.nn.Module构建神经网络。损失函数与优化器选择合适的损失函数如交叉熵和优化器如 SGD 或 Adam。训练与验证通过循环迭代训练模型并使用验证集评估性能。保存与加载模型使用torch.save和torch.load保存和恢复模型。深度学习模型CNNCNN卷积神经网络适合用在图像识别和分类中通过一系列的卷积层、池化层和全连接层来处理数据在卷积层中CNN使用一组可学习的滤波器卷积核来扫描输入的图像或信号。每个滤波器都能够检测输入中的特定特征如边缘或颜色斑点。通过这种方式CNN能够捕捉到图像中的局部特征并保持这些特征的空间关系。池化层也称为下采样层则用于降低特征的空间尺寸从而减少参数数量和计算复杂度同时使特征检测更加鲁棒。全连接层则将学习到的高级特征用于分类或其他任务。CNN的结构和工作原理一个典型的CNN包含以下几个主要部分输入层接收原始数据如图像的像素值。卷积层使用多个卷积核提取输入的特征。激活函数如ReLU用于引入非线性使网络能够学习更复杂的特征。池化层降低特征的空间维度减少计算量。全连接层将学习到的特征映射到最终的输出如分类标签。输出层输出网络的最终结果如分类的概率分布。CNN通过这些层的堆叠能够从简单到复杂逐渐提取图像的特征。在训练过程中CNN通过反向传播算法调整卷积核中的权重以最小化预测结果和真实标签之间的差异。训练MNIST❓什么是MNIST全称Modified National Institute of Standards and Technology是机器学习和深度学习领域最经典的入门数据集被称为深度学习的“Hello World”。它包含手写数字0-9的灰度图像广泛用于图像分类算法的训练与测试。模型定义首先定义模型的结构这个结构在训练和推理过程中都是要保持一致的其中包括模型组件的定义和模型流程定义代码如下所示class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 nn.Conv2d(1, 32, 3, 1) self.conv2 nn.Conv2d(32, 64, 3, 1) self.dropout1 nn.Dropout2d(0.25) self.fc1 nn.Linear(9216, 128) self.fc2 nn.Linear(128, 10) def forward(self, x): x self.conv1(x) x F.relu(x) x self.conv2(x) x F.relu(x) x F.max_pool2d(x, 2) x self.dropout1(x) x torch.flatten(x, 1) x self.fc1(x) x F.relu(x) x self.dropout2(x) x self.fc2(x) output F.log_softmax(x, dim1) return output上面的代码中首先定义了模型的组件结构其次forward是模型的流程结构。这是一个经典的“特征提取分类器”双阶段结构分为两个阶段。第一阶段特征提取卷积神经网络负责把像素变成“语义特征”。卷积层 1(Conv1)初级特征提取边缘、线条self.conv1 nn.Conv2d(1, 32, 3, 1)in_channels1 : 输入是灰度图1个通道out_channels32: 派出32个不同的“侦探”滤波器kernel_size3 : 每个侦探拿3x3的放大镜stride1 : 每次移动1个像素输出尺寸计算(28 - 3 1) 26 - 输出 (32, 26, 26)卷积层 2高级特征提取部件、纹理self.conv2 nn.Conv2d(32, 64, 3, 1)in_channels32: 接收上一层的32个特征图out_channels64: 增加到64个侦探组合更复杂的模式输出尺寸计算(26 - 3 1) 24 - 输出 (64, 24, 24)Dropout 层 1针对卷积特征的正则化self.dropout1 nn.Dropout2d(0.25)作用随机“掐断”25%的通道整张特征图,强迫模型不要把赌注押在某几个特定的特征组合上为什么用 Dropout2d防止特征图之间产生共适应Co-adaptationDropout 层 2针对全连接层的正则化self.dropout2 nn.Dropout2d(0.5)作用随机“掐断”50%的神经元因为全连接层参数最多最容易过拟合第二阶段分类器全连接网络负责把特征变成具体的数字类别全连接层 1特征映射与降维self.fc1 nn.Linear(9216, 128)输入维度 9216 的由来关键计算经过 Conv2(64, 24, 24) - MaxPool(2, 2)输出尺寸64个通道 * 12 * 12 9216输出维度 128将高维特征压缩成128维的“数字指纹”全连接层 2最终输出层self.fc2 nn.Linear(128, 10)输入 128接收上层的指纹输出 10对应 0-9 十个数字类别而这些阶段落实到的流程执行中需要用到一些额外的手段如下所示relu激活函数引入非线性筛选有效特征max_pool2d最大池化模型不再关心像素的精确位置只关心“有没有”flatten展平把三维特征图拉直成一维向量为了对接全连接层只能吃一维数据log_softmax转换为对数概率forward函数清晰地展示了数据流(Flow)像素 → 边缘 → 部件 → 抽象特征 → 概率。❓ 我当时真实的困惑为什么卷积核数量是32和64不能是30吗9216这个数字是怎么来的forward 里数据尺寸到底是怎么一步步变化的这几个是非常典型、也非常值得认真回答的问题。首先回答第一个问题可以是 30但几乎没人这么做。原因有三个都是工程现实不是玄学1️⃣ 最重要的是GPU 硬件对齐因为现代 GPU 的运算单元CUDA Core / Tensor Core是按 2 的幂次并行工作的能刚好对齐显存带宽和线程块block如果用30会出现padding / waste计算效率下降训练变慢。2️⃣ 经验性容量设计在第一层提取简单特征边、角、点32个通道足够覆盖常见低级模式第二层组合成复杂结构弧线、圈、部件需要更多表示能力64是实践中验证过的稳定选择3️⃣ 历史惯性LeNet 传统很多教程只是继承这种翻倍增长 的设计而不是重新发明其次是第二个问题9216 不是玄学是尺寸推导是算出来的✅ 已知前提输入图片28 × 28卷积核3 × 3padding0stride1✅ 每一层尺寸变化层操作输出尺寸Input原始图片1 × 28 × 28Conv13×3 卷积无 padding32 × 26 × 26MaxPool2×2 池化32 × 13 × 13Conv23×3 卷积64 × 11 × 11MaxPool2×2 池化64 × 5 × 5⚠️ 注意不同实现可能略有差异但常见版本最后会再接一个卷积或 padding使最终特征图为64 × 12 × 12。那么最终9216 的来源就是64 个通道 × 12 × 12 每个通道的特征图 9216模型训练整体来说训练过程就是把一个 batch 的数据送进模型 → 算损失 → 反向传播 → 更新参数 → 记录日志这是深度学习里“学习”发生的唯一地方PyTorch 的数据是“流式”的不是一次性全塞进去def train(model, device, train_loader, optimizer, epoch, losses): model.train() for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() output model(data) loss F.nll_loss(output, target) loss.backward() optimizer.step() losses.append(loss.item()) if batch_idx % 100 0: print( fEpoch {epoch} f[{batch_idx * len(data)}/{len(train_loader.dataset)} f({100. * batch_idx / len(train_loader):.0f}%)]\t fLoss: {loss.item():.6f} )函数参数表含义如下所示参数是什么model你的 CNN 网络deviceCPU / GPUtrain_loader数据加载器自动分批optimizer优化器Adam / SGDepoch当前是第几轮losses用来记录 loss 的列表首先要做的就是让模型切换到训练模式使用model.train()它告诉模型Dropout层开启随机失活BatchNorm层使用当前batch 的均值/方差如果有GPU那么data, target data.to(device), target.to(device)就是把数据搬到GPU因为PyTorch默认梯度是累加的所以使用optimizer.zero_grad()清空梯度来给新的学习腾地方而模型在这里的作用就是做前向传播就是前面所将的流程Conv1 → ReLU → Conv2 → ReLU → Pool → Dropout → FC → Softmax模型的输出output的形状是(batch_size, 10)计算损失使用loss F.nll_loss(output, target)它衡量的是模型猜的有多离谱其中nll_loss是负对数似然target是真实标签0~9我们肯定是想要最小化损失函数的反向传播Backward Pass是深度学习最核心的一行代码可以自动计算∂Loss∂w然后把梯度存到每个参数的.grad属性里核心是把“总错误”按责任比例分摊回去对比正向传播过程如下Loss ↓ ∂Loss/∂FC ↓ ∂FC/∂Pool ↓ ∂Pool/∂Conv2 ↓ ∂Conv2/∂Conv1 ↓ ∂Conv1/∂W有反向传播就可以自动学习而不用手动调参梯度告诉了方向然后是优化器Optimizer Stepoptimizer.step()负责迈步子读取.grad根据优化规则更新参数其中Adam 做的是θθ−η⋅m√vϵ这是“真正动手改模型”的地方大致的训练流程就是这样那么现在开始实际的训练吧。def main(): # # 1. 设备选择与配置 # # 自动检测是否有可用的 GPUCUDA # 如果有则用 GPU 加速训练否则回退到 CPU device torch.device(cuda if torch.cuda.is_available() else cpu) # # 2. 模型与优化器初始化 # # 实例化我们定义的 CNN 模型 # .to(device) 将模型的所有参数和缓冲区移动到指定设备GPU/CPU model Net().to(device) # 使用 Adam 优化器 # model.parameters() 告诉优化器需要更新哪些参数 # lr0.001 是学习率控制每次参数更新的步长 optimizer optim.Adam(model.parameters(), lr0.001) # # 3. 数据预处理管道 # # transforms.Compose 将多个变换组合成一个流水线 transform transforms.Compose([ # 将 PIL 图像或 numpy.ndarray 转换为 PyTorch Tensor # 同时将像素值从 [0, 255] 缩放到 [0.0, 1.0] transforms.ToTensor(), # 标准化处理Normalization # 使用 MNIST 数据集的官方均值和标准差 # 公式: output (input - mean) / std # 作用: 使数据分布均值为0方差为1加速模型收敛 transforms.Normalize((0.1307,), (0.3081,)) ]) # # 4. 数据集与数据加载器 # # 加载 MNIST 训练数据集 dataset datasets.MNIST( ./data, # 数据存储路径 trainTrue, # 使用训练集而非测试集 downloadTrue, # 如果数据不存在自动从网上下载 transformtransform # 应用上面定义的数据预处理 ) # DataLoader 负责批量加载数据并提供打乱、并行读取等功能 train_loader DataLoader( dataset, # 要加载的数据集 batch_size64, # 每个批次包含 64 个样本 shuffleTrue # 每个 epoch 都打乱数据顺序防止模型记忆顺序 ) # # 5. 训练循环 # # 用于记录每个 batch 的损失值以便后续可视化 losses [] # 训练 10 个 epoch完整遍历数据集 10 次 for epoch in range(1, 11): # 调用我们定义的 train 函数 # 将模型、设备、数据加载器、优化器、当前轮次和损失列表传入 train(model, device, train_loader, optimizer, epoch, losses) # # 6. 保存训练好的模型 # # 保存模型的参数state_dict # 只保存参数而不保存整个模型是推荐做法节省空间且灵活 torch.save(model.state_dict(), mnist_cnn.pt) # # 7. 可视化训练过程 # # 创建一个 8x5 英寸大小的画布 plt.figure(figsize(8, 5)) # 绘制损失曲线 # losses 列表记录了每个 batch 的损失值 plt.plot(losses, labelTraining Loss) # 设置坐标轴标签 plt.xlabel(Batch) # X轴批次索引 plt.ylabel(Loss) # Y轴损失值 # 设置图表标题 plt.title(MNIST CNN Training Loss) # 显示网格线便于观察数值 plt.grid(True) # 显示图例对应 labelTraining Loss plt.legend() # 自动调整子图参数使之填充整个画布 plt.tight_layout() # 显示图形窗口 plt.show()经过训练循环过程后模型的犯错程度在不断的下降如下图所示到这里基本的训练过程就讲差不多了那么一个epoch 里到底发生了什么Epoch 1 ├─ Batch 1: 猜 → 算错 → 改参数 ├─ Batch 2: 猜 → 算错 → 改参数 ├─ ... └─ Batch N: 猜 → 算错 → 改参数 Epoch 2 ├─ 猜得更准一点 ├─ Loss 更小 └─ ... Epoch 10 └─ 基本稳定训练函数的本质就是用损失函数告诉模型“你错在哪再用反向传播告诉它“该怎么改”模型测试我使用了Forward Hook前向钩子方法来达到可视化推理的中间过程如下所示features {} def hook(name): def fn(_, __, out): features[name] out.detach() return fn # 注册钩子 model.conv1.register_forward_hook(get_features(conv1)) model.conv2.register_forward_hook(get_features(conv2)) model.dropout1.register_forward_hook(get_features(dropout1))进行测试时加载之前训练好的模型权重并开启model.eval()同时需要注意模型定义要和训练时保持一致device torch.device(cuda if torch.cuda.is_available() else cpu) model Net().to(device) model.load_state_dict(torch.load(mnist_cnn.pt, map_locationdevice)) model.eval()模型接受的输入需要先预处理一下因为MNIST的图片有一定规范比如尺寸、颜色等# 图片预处理 img_path my_digit.png img Image.open(img_path).convert(L) # ✅ 白底黑字 → 黑底白字MNIST 风格 img ImageOps.invert(img) # resize 到 28×28 img img.resize((28, 28), Image.LANCZOS) # 输入预处理 transform T.Compose([ T.ToTensor(), T.Normalize((0.1307,), (0.3081,)) ]) # 获得模型输入 input_tensor transform(img).unsqueeze(0).to(device)最后就是模型推理代码了# # 推理模式禁用梯度计算 # 作用节省显存、加快计算、关闭 Dropout # with torch.no_grad(): # # 前向传播让模型看这张图片 # output 形状: (1, 10) # 注意这是 log_softmax 输出对数概率 # output model(input_tensor) # # 将对数概率转换回普通概率 # 公式: prob exp(log_prob) # 作用让人类能直观理解0~1 之间 # prob torch.exp(output) # # 找出概率最大的类别预测结果 # argmax(dim1): 在类别维度上找最大值索引 # item(): 把 Tensor 转成 Python 整数 # 结果: 0~9 的数字 # pred prob.argmax(dim1).item() # # 取出该类别的预测置信度 # prob[0, pred]: 第 0 个样本、预测类别上的概率值 # item(): 转成 Python 浮点数 # 结果: 0.0~1.0 之间的置信度 # conf prob[0, pred].item()使用上面提到的钩子函数获取过程特征图以Conv1举例conv1_feat features[conv1].squeeze().cpu() plt.figure(figsize(12, 6)) for i in range(32): plt.subplot(4, 8, i1) plt.imshow(conv1_feat[i], cmapgray) plt.axis(off) plt.suptitle(Conv1: Edge Detectors) plt.show()下面就是推理的中间过程图片有4个分别是conv1,conv2,dropout1以及全连接层的输出如下所示Conv1 输出32 张特征图有的对竖边敏感有的对横边敏感有的对角点敏感Conv2 输出不再是简单边缘开始出现圈、弧线、局部形状Pooling Dropout输出池化在“做减法”精简信息Dropout 在“做干扰”增强韧性