news 2026/9/7 6:38:26

神经网络实战:用PyTorch从零实现BP、MLP与CNN

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
神经网络实战:用PyTorch从零实现BP、MLP与CNN

神经网络(Neural Network)这个概念,相信很多读者并不陌生。但真正动手写过网络结构、调过损失函数、跑通过一次训练的人,往往比只会背概念的人少得多。笔者早期学习时也有过这样的阶段:看了很多教程,知道了 CNN 可以用来做图像,RNN 可以用来处理序列,但真的打开 IDE 准备写代码时,却不知道第一行应该写什么。

这篇文章就是围绕“神经网络实操与代码解释”展开的。我会从神经网络的核心概念讲起,然后重点拆解 BP 神经网络、多层感知机(MLP)、卷积神经网络(CNN)的完整代码实现,并逐段解释代码的含义。文章中的代码基于 PyTorch 框架,适合刚入门深度学习、或者想系统理解非论文式代码的读者。读完本文,你应该能独立实现一个简单的图像分类模型,并且知道如何调试训练过程中最常见的问题。

1. 神经网络到底是什么:先建立直观印象

1.1 从“函数拟合”理解神经网络

抛开复杂的生物学比喻,神经网络本质上是一个“万能函数逼近器”。什么意思呢?假设我们有一堆数据,比如房子的面积和价格,我们想找到一个函数 f,使得输入面积 x,输出价格 y 尽可能准确。传统做法可能是线性回归,也就是 y = ax + b。

但现实世界的问题通常没有这么简单。房价还受地段、楼层、朝向、装修程度等多种因素影响,而且这些因素和价格之间往往不是简单的线性关系。神经网络的作用就是通过大量参数的组合,自动学习出一个复杂的非线性函数 f(x),让它的输出尽可能接近真实标签。

一个典型的神经网络由三层组成:

  • 输入层:接收原始特征,比如图片像素、商品价格、文本向量。
  • 隐藏层:对输入进行非线性变换,提取特征。
  • 输出层:输出预测结果,比如分类概率或回归数值。

数据从输入层流向输出层,这个过程叫“前向传播”(Forward Propagation)。而训练过程会让预测值和真实值之间产生一个误差,这个误差会反向从输出层传回输入层,更新每一层的权重参数,这个过程叫“反向传播”(Backpropagation)。

1.2 常用的神经网络家族

神经网络经过多年发展,已经演化出多种结构,每种结构适合不同的问题:

网络类型英文缩写适用场景
前馈神经网络 / 多层感知机FNN / MLP表格数据、回归、分类
卷积神经网络CNN图像识别、目标检测
循环神经网络RNN文本、时间序列
长短期记忆网络LSTM长序列建模,是 RNN 的改进
图神经网络GNN社交网络、分子结构、推荐系统
物理信息神经网络PINN偏微分方程求解、科学计算

这篇文章的重点是前两个:MLP 和 CNN。因为它们最能体现神经网络的基本工作原理,也是后续学习 RNN、Transformer 和大模型的地基。

1.3 为什么必须动手实操

很多初学者陷入“看了很多理论,但写不出代码”的困境。原因在于,神经网络的代码不是背出来的,而是需要理解数据流。同样一个模型,数据维度不对,可能报错;损失函数选错,模型可能不收敛;学习率太大,loss 可能变成 NaN。

实操的意义在于,你会在报错中理解张量形状,会通过观察 loss 变化调整超参数,会逐渐建立“数据 --> 模型 --> 损失 --> 优化器 --> 迭代”这个闭环。本文后续的每一段代码都会解释它做了什么,以及为什么要这么做。

2. 环境准备与版本说明

2.1 安装 Python 和 PyTorch

本文示例代码以 Python 3.9 以上环境为例,深度学习框架使用 PyTorch。PyTorch 的安装细节会根据操作系统、CUDA 版本不同而变化,这里给出通用步骤:

# 建议使用 conda 创建独立环境 conda create -n nn_tutorial python=3.9 conda activate nn_tutorial # 安装 CPU 版本 PyTorch(不依赖显卡) pip install torch torchvision # 如果你有 NVIDIA 显卡,请到 PyTorch 官网 # 选择对应的 CUDA 版本安装命令

版本说明:本文演示使用的是 PyTorch 2.x 版本。如果你的环境中是 1.x 版本,核心 API 基本兼容,但部分新特性可能不支持。建议尽量保持 PyTorch 版本在 2.0 及以上,遇到问题时可先查阅官方文档。

2.2 其他依赖库

除了 PyTorch,还需要以下常见库:

pip install numpy matplotlib scikit-learn
  • numpy:处理张量转换和数组操作。
  • matplotlib:绘制训练曲线,可视化图片。
  • scikit-learn:生成数据集、评估指标。

2.3 开发工具选择

代码编辑器推荐使用 VS Code 或 PyCharm。两者对 Python 的支持都非常成熟,断点调试功能可以帮助你观察每一步的张量形状和数值变化。如果你是初学者,我建议优先用 PyCharm 的调试器,它对新手更友好。

2.4 示例项目结构

为了不让代码堆在同一个文件里,本文采用如下目录结构:

nn_practice/ ├── data/ │ └── (自动下载的数据集) ├── models/ │ └── mlp.py │ └── cnn.py ├── utils/ │ └── visualization.py ├── train_mlp.py └── train_cnn.py

如果你只是跑通代码,也可以先把所有代码写在两个.py文件中,不必强求目录完整。但实际项目中,模块化是必须的,后面第 8 节会展开讲。

3. 神经网络核心代码概念拆解

在写完整代码之前,我们需要先理解五个核心概念。它们是阅读任何深度学习代码的基础。

3.1 张量(Tensor):神经网络里的“数据容器”

张量可以理解为多维数组。0 维张量是标量,1 维张量是向量,2 维张量是矩阵,3 维张量可以理解为多通道图片或序列数据。

PyTorch 中创建张量的方式很简单:

import torch # 创建一个 2x3 的随机张量 x = torch.randn(2, 3) print(x.shape) # torch.Size([2, 3]) # 创建一个全零张量 y = torch.zeros(4, 5) # 从 numpy 数组转换 import numpy as np arr = np.array([[1, 2], [3, 4]]) tensor_from_np = torch.from_numpy(arr) print(tensor_from_np.dtype) # torch.int64

理解张量的 shape(形状)是调试神经网络最重要的技能。后面你会频繁遇到 shape mismatch 的报错,原因就是某个环节的维度没对上。

3.2 激活函数:给网络引入非线性

如果神经网络只有线性变换,那么无论堆多少层,本质上仍然等价于一层线性模型。激活函数的作用就是引入非线性,让网络能够学习复杂的模式。

常用的激活函数:

激活函数公式特点
ReLUmax(0, x)计算简单,缓解梯度消失,最常用
Sigmoid1 / (1 + e^(-x))输出范围 0~1,适合二分类输出层
Tanh(e^x - e^(-x)) / (e^x + e^(-x))输出范围 -1~1,常用于 RNN
Softmax归一化指数多分类输出层,输出概率分布

在 PyTorch 中,这些激活函数都在torch.nn.functional中:

import torch.nn.functional as F x = torch.tensor([-1.0, 0.0, 2.0]) print(F.relu(x)) # tensor([0., 0., 2.]) print(F.sigmoid(x)) # tensor([0.2689, 0.5000, 0.8808])

3.3 损失函数:衡量预测与真实的差距

损失函数告诉模型“你错得有多离谱”。训练的过程就是不断最小化损失函数。

常见的损失函数:

  • 均方误差(MSELoss):用于回归任务。
  • 交叉熵损失(CrossEntropyLoss):用于多分类任务。
  • 二元交叉熵(BCELoss):用于二分类任务。

PyTorch 中的使用方式:

loss_fn = torch.nn.CrossEntropyLoss() # 假设模型输出 3 个类别的得分 pred = torch.tensor([[2.0, 1.0, 0.1]], requires_grad=True) # 真实标签是第 0 类 target = torch.tensor([0]) loss = loss_fn(pred, target) print(loss.item()) # 损失值

3.4 优化器:更新模型参数

模型通过损失函数计算出误差后,需要用优化器更新权重,让误差越来越小。最常用的优化器是 SGD(随机梯度下降)和 Adam。

import torch.optim as optim # 假设 model 是一个神经网络模型 optimizer = optim.Adam(model.parameters(), lr=0.001)

这里lr是学习率,决定了每次参数更新的步长。学习率太大,损失函数会在最优点附近震荡甚至发散;学习率太小,训练速度会非常慢。

3.5 训练闭环:五步循环

一个标准的训练循环包含五个步骤:

  1. 前向传播:把数据输入模型,得到预测输出。
  2. 计算损失:将预测输出与真实标签比较,得到 loss。
  3. 梯度清零:清空上一次迭代留下的梯度。
  4. 反向传播:计算损失对每个参数的梯度。
  5. 更新参数:优化器根据梯度更新模型权重。

对应 PyTorch 代码是:

# 前向传播 output = model(data) # 计算损失 loss = loss_fn(output, target) # 梯度清零 optimizer.zero_grad() # 反向传播 loss.backward() # 更新参数 optimizer.step()

这个五步循环是几乎所有的 PyTorch 训练脚本骨架。后面每一份完整代码都会看到它。

4. 实战一:从零实现 BP 神经网络解决分类问题

4.1 问题定义

我们先从一个最简单的任务开始:二分类。使用 scikit-learn 自带的make_moons数据集,它生成两个半月牙形状的数据点,是测试分类模型的标准数据集。

4.2 数据准备

创建一个generate_data.py文件,代码如下:

# 文件路径:generate_data.py import torch from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split # 生成 1000 个样本,噪声为 0.2 X, y = make_moons(n_samples=1000, noise=0.2, random_state=42) # 转换为 PyTorch 张量,并指定数据类型 X = torch.tensor(X, dtype=torch.float32) y = torch.tensor(y, dtype=torch.long) # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) print(f"训练集大小: {X_train.shape}, 测试集大小: {X_test.shape}")

这里需要注意,make_moons返回的是 NumPy 数组,必须转换为torch.float32类型。PyTorch 默认要求浮点张量是float32,否则会报类型错误。

4.3 定义 BP 神经网络模型

BP 神经网络通常指使用反向传播算法训练的多层前馈网络。下面我们定义一个三层网络:输入层 2 个神经元(对应 x 和 y 坐标),隐藏层 16 个神经元,输出层 2 个神经元(对应两个类别)。

# 文件路径:bp_model.py import torch import torch.nn as nn class BPNetwork(nn.Module): def __init__(self, input_size, hidden_size, num_classes): super(BPNetwork, self).__init__() # 第一层线性变换:输入 -> 隐藏层 self.fc1 = nn.Linear(input_size, hidden_size) # 第二层线性变换:隐藏层 -> 输出层 self.fc2 = nn.Linear(hidden_size, num_classes) # ReLU 激活函数 self.relu = nn.ReLU() def forward(self, x): # 输入 -> 隐藏层 -> ReLU out = self.fc1(x) out = self.relu(out) # 隐藏层 -> 输出层 out = self.fc2(out) return out

逐行解释:

  • nn.Linear(input_size, hidden_size)表示一个全连接层,内部有 weight 和 bias 两个参数。输入是[batch_size, input_size],输出是[batch_size, hidden_size]
  • forward方法定义了数据前向传播的路径。PyTorch 的nn.Module在调用model(x)时会自动执行forward方法。
  • 最后一个线性层后面没有接激活函数,因为交叉熵损失函数CrossEntropyLoss内部已经包含了 Softmax 运算,如果在输出层再手动加 Softmax,会导致数值不稳定。

4.4 训练脚本

# 文件路径:train_bp.py import torch import torch.nn as nn import torch.optim as optim from generate_data import X_train, X_test, y_train, y_test from bp_model import BPNetwork # 超参数设置 input_size = 2 hidden_size = 16 num_classes = 2 learning_rate = 0.01 num_epochs = 200 batch_size = 32 # 创建模型、损失函数、优化器 model = BPNetwork(input_size, hidden_size, num_classes) loss_fn = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=learning_rate) # 将数据组合成数据集 train_dataset = torch.utils.data.TensorDataset(X_train, y_train) train_loader = torch.utils.data.DataLoader( dataset=train_dataset, batch_size=batch_size, shuffle=True ) # 训练循环 for epoch in range(num_epochs): total_loss = 0.0 for batch_x, batch_y in train_loader: # 前向传播 outputs = model(batch_x) loss = loss_fn(outputs, batch_y) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() # 每 20 个 epoch 打印一次损失 if (epoch + 1) % 20 == 0: avg_loss = total_loss / len(train_loader) print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}") # 测试模型精度 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}%")

4.5 运行与预期结果

运行训练脚本:

python train_bp.py

正常情况下,你会看到类似如下的输出:

Epoch [20/200], Loss: 0.4351 Epoch [40/200], Loss: 0.2743 Epoch [60/200], Loss: 0.2087 Epoch [80/200], Loss: 0.1572 Epoch [100/200], Loss: 0.1225 Epoch [120/200], Loss: 0.1004 Epoch [140/200], Loss: 0.0863 Epoch [160/200], Loss: 0.0771 Epoch [180/200], Loss: 0.0712 Epoch [200/200], Loss: 0.0665 测试集准确率: 88.50%

从损失变化可以看出,模型在逐步收敛。如果你运行结果准确率在 85% 到 95% 之间,都是正常范围,因为数据噪声和随机初始化会导致一定波动。

这段代码中有一个细节值得留意:测试时必须用model.eval()切换模型模式,并用torch.no_grad()包裹推理过程。前者会关闭 Dropout 和 BatchNorm 的训练行为,后者会停止梯度追踪,节省内存并加速计算。

4.6 可视化决策边界

为了更直观地理解模型学到了什么,我们可以绘制决策边界:

# 文件路径:plot_decision_boundary.py import numpy as np import matplotlib.pyplot as plt import torch from bp_model import BPNetwork def plot_decision_boundary(model, X, y): model.eval() # 生成网格点 x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5 y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5 xx, yy = np.meshgrid( np.arange(x_min, x_max, 0.02), np.arange(y_min, y_max, 0.02) ) grid = torch.tensor(np.c_[xx.ravel(), yy.ravel()], dtype=torch.float32) with torch.no_grad(): outputs = model(grid) _, preds = torch.max(outputs, 1) preds = preds.reshape(xx.shape).numpy() plt.contourf(xx, yy, preds, alpha=0.7, cmap='coolwarm') plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', cmap='coolwarm') plt.title('BP Neural Network Decision Boundary') plt.show()

从图中可以看到,模型学习到了一条非线性边界,把两个月牙形数据大致分开。这正是神经网络相对于线性模型的优势所在。

5. 实战二:PyTorch 实现 MLP 进行手写数字识别(MNIST)

MNIST 是深度学习领域的 “Hello World” 数据集,包含 0-9 的手写数字灰度图片,每张图片尺寸为 28x28 像素。用 MLP 做 MNIST 分类,是理解神经网络处理图像数据的第一步。

5.1 下载和预处理数据

# 文件路径:prepare_mnist.py import torchvision import torchvision.transforms as transforms # 定义数据预处理:转成张量并归一化 transform = transforms.Compose([ transforms.ToTensor(), # 将 PIL 图片转成 Tensor,并缩放到 [0, 1] transforms.Normalize((0.1307,), (0.3081,)) # MNIST 数据集的均值和标准差 ]) # 下载训练集 train_dataset = torchvision.datasets.MNIST( root='./data', train=True, transform=transform, download=True ) # 下载测试集 test_dataset = torchvision.datasets.MNIST( root='./data', train=False, transform=transform, download=True ) print(f"训练集样本数: {len(train_dataset)}") print(f"测试集样本数: {len(test_dataset)}")

transforms.ToTensor()会把形状为(H, W)的 PIL 图像转换成形状为(C, H, W)的张量,其中 C=1 表示灰度通道。归一化操作让像素值从 [0, 1] 分布调整为均值为 0、标准差为 1 的分布,这有助于加快模型收敛。

5.2 定义 MLP 模型

MLP 处理 MNIST 时,需要先将 28x28 的二维图像展平成一维向量,然后输入全连接网络。

# 文件路径:models/mlp.py import torch.nn as nn class MLP(nn.Module): def __init__(self, input_size=784, hidden_size=128, num_classes=10): super(MLP, self).__init__() self.flatten = nn.Flatten() # 将 28x28 展平成 784 self.fc1 = nn.Linear(input_size, hidden_size) self.relu = nn.ReLU() self.fc2 = nn.Linear(hidden_size, hidden_size) self.relu2 = nn.ReLU() self.fc3 = nn.Linear(hidden_size, num_classes) def forward(self, x): x = self.flatten(x) x = self.fc1(x) x = self.relu(x) x = self.fc2(x) x = self.relu2(x) x = self.fc3(x) return x

这里加入了两个隐藏层,网络比前面的 BP 模型更深。原则上,增加隐藏层可以让模型学习更复杂的特征,但也更容易过拟合,需要配合 Dropout 或正则化手段。

5.3 训练与评估

# 文件路径:train_mlp.py import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from prepare_mnist import train_dataset, test_dataset from models.mlp import MLP # 超参数 batch_size = 64 learning_rate = 0.001 num_epochs = 5 # 数据加载器 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) # 模型初始化 model = MLP() loss_fn = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=learning_rate) # 训练 for epoch in range(num_epochs): model.train() running_loss = 0.0 for images, labels in train_loader: # images 形状: [64, 1, 28, 28] outputs = model(images) loss = loss_fn(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss / len(train_loader):.4f}") # 测试 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: outputs = model(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f"测试集准确率: {100 * correct / total:.2f}%")

运行这段代码,5 个 epoch 后测试集准确率通常可以达到 97% 以上。这个成绩在深度学习时代不算高,但已经能说明 MLP 有能力从原始像素中学习到分类规律。

5.4 可视化预测结果

训练完成后,随机选取测试集中的样本,绘制预测结果:

import matplotlib.pyplot as plt import numpy as np # 取一个 batch 的测试数据 dataiter = iter(test_loader) images, labels = next(dataiter) # 取前 8 张 images = images[:8] labels = labels[:8] outputs = model(images) _, predicted = torch.max(outputs, 1) # 绘制图片 fig, axes = plt.subplots(2, 4, figsize=(10, 5)) for i, ax in enumerate(axes.flat): ax.imshow(images[i].squeeze(), cmap='gray') ax.set_title(f"True: {labels[i].item()}, Pred: {predicted[i].item()}") ax.axis('off') plt.show()

通过图片可以直观判断模型在哪些样本上犯了错。通常容易混淆的数字是 4 和 9、3 和 8,因为它们的笔画结构比较接近。

6. 实战三:CNN 卷积神经网络实现 CIFAR-10 图像分类

MLP 在 MNIST 上表现不错,但用同样的结构去处理更复杂的彩色图片(如 CIFAR-10),准确率会大幅下降。原因在于,MLP 将图片展平成向量后,完全丢失了像素之间的空间位置关系。CNN 正是为了解决这个问题而设计的。

6.1 CNN 的核心思想

CNN 包含两个关键操作:

  • 卷积(Convolution):用一个小窗口(卷积核)在图像上滑动,提取局部特征,比如边缘、纹理、颜色变化。
  • 池化(Pooling):对特征图进行下采样,减少计算量,同时增强模型的平移不变性。

一个简单的 CNN 结构通常为:

输入图片 -> [卷积层 -> 激活函数 -> 池化层] -> ... -> 全连接层 -> 输出

6.2 加载 CIFAR-10 数据集

CIFAR-10 包含 10 个类别的彩色图片,每张图片尺寸为 32x32x3。加载方式与 MNIST 类似,但要注意归一化参数不同。

# 文件路径:prepare_cifar10.py import torchvision import torchvision.transforms as transforms transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ]) train_dataset = torchvision.datasets.CIFAR10( root='./data', train=True, download=True, transform=transform ) test_dataset = torchvision.datasets.CIFAR10( root='./data', train=False, download=True, transform=transform ) classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

这里的三组均值 (0.4914, 0.4822, 0.4465) 和三组标准差 (0.2470, 0.2435, 0.2616) 是 CIFAR-10 在 RGB 三个通道上预计算好的全局统计值。

6.3 定义 CNN 模型

# 文件路径:models/cnn.py import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super(SimpleCNN, self).__init__() # 卷积块 1: 3 -> 32 个特征图 self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm2d(32) self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # 卷积块 2: 32 -> 64 个特征图 self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(64) self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) # 全连接分类层 self.fc1 = nn.Linear(64 * 8 * 8, 512) self.fc2 = nn.Linear(512, num_classes) self.relu = nn.ReLU() def forward(self, x): # 第一层卷积 x = self.pool1(self.relu(self.bn1(self.conv1(x)))) # 第二层卷积 x = self.pool2(self.relu(self.bn2(self.conv2(x)))) # 展平 x = x.view(x.size(0), -1) # 全连接层 x = self.relu(self.fc1(x)) x = self.fc2(x) return x

有几个地方需要解释:

  • 输入图片x的形状是[batch_size, 3, 32, 32],第一个卷积层输出形状为[batch_size, 32, 32, 32]
  • MaxPool2d(kernel_size=2, stride=2)会把图片宽高各缩小一半:32 -> 16 -> 8。
  • 经过两次池化后,特征图尺寸是 8x8,通道数为 64,所以全连接层的输入维度是64 * 8 * 8 = 4096
  • BatchNorm2d的作用是对每个通道的特征做归一化,可以加速训练并提高稳定性,尤其适合较深的网络。

6.4 训练 CNN

# 文件路径:train_cnn.py import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from prepare_cifar10 import train_dataset, test_dataset from models.cnn import SimpleCNN batch_size = 64 learning_rate = 0.001 num_epochs = 20 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Using device: {device}") model = SimpleCNN().to(device) loss_fn = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=learning_rate) for epoch in range(num_epochs): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = loss_fn(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss / len(train_loader):.4f}") # 测试 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f"测试集准确率: {100 * correct / total:.2f}%")

注意这里增加了device判断,如果环境中有 NVIDIA GPU,代码会自动在 GPU 上训练;否则退回 CPU。20 个 epoch 后,测试准确率大约在 70% 到 75% 之间。这个结果已经明显优于用 MLP 处理 CIFAR-10 的水平。

6.5 为什么 CNN 比 MLP 更适合图像任务

核心原因是“参数共享”和“局部感受野”。卷积核在整张图片上滑动,同一组参数被反复使用,大幅减少了参数量。一个 3x3 的卷积核只有 9 个权重,但能提取整张图片的局部特征。而全连接层每个输入像素都要与下一层每个神经元连接,参数量随图片尺寸增大呈平方增长。

如果你想进一步提高 CIFAR-10 准确率,可以尝试:

  • 增加卷积层数和通道数。
  • 加入 Dropout 防止过拟合。
  • 使用数据增强(随机裁剪、水平翻转、颜色扰动)。
  • 改用预训练的 ResNet 或 VGG 模型做迁移学习。

7. 拓展:RNN、LSTM 与 GNN 的核心思路

7.1 循环神经网络(RNN)与 LSTM

RNN 适合处理序列数据,例如文本、语音、股票价格。它的核心是“隐藏状态”:每个时间步的输入会结合前一个时间步的隐藏状态,共同计算当前输出。标准循环神经网络的隐藏状态更新公式可以简写为:

h_t = tanh(W_ih * x_t + b_ih + W_hh * h_{t-1} + b_hh)

其中x_t是当前时间步输入,h_{t-1}是上一个时间步的隐藏状态。

但标准 RNN 存在梯度消失问题,难以捕捉长距离依赖。LSTM(长短期记忆网络)通过引入输入门、遗忘门和输出门来解决这个问题,使得信息可以在网络中长时间传递。

如果后续你想学习 NLP 或时间序列建模,建议按下面的顺序学习:

  1. 标准 RNN(理解时间步和隐藏状态)。
  2. LSTM(理解门控机制)。
  3. GRU(LSTM 的简化版本)。
  4. Seq2Seq(编码器-解码器结构)。
  5. Transformer(自注意力机制,当前大模型的基础)。

7.2 图神经网络(GNN)

GNN 处理的是图结构数据,比如社交网络、分子结构、知识图谱。传统神经网络假设输入是规则的网格或序列,而图数据中的节点连接关系是不规则的。GNN 的核心思想是“消息传递”:每个节点聚合邻居节点的信息,更新自己的特征表示。

如果你对 GNN 感兴趣,可以从图卷积网络(GCN)开始。GCN 的核心代码通常只需要几十行,但理解它需要先掌握图的邻接矩阵、度矩阵和拉普拉斯矩阵这几个概念。

7.3 物理信息神经网络(PINN)

PINN 是一种将物理方程嵌入神经网络的方法。它将偏微分方程的残差作为损失函数的一部分,让神经网络既拟合数据,又满足物理规律。这个方向在科学计算领域很热门,但更适合有一定数学和物理基础的读者,不建议作为入门首选。

8. 常见问题与排查思路

在训练神经网络时,你会遇到各种报错和诡异现象。下面整理了我认为出现频率最高的问题:

问题现象常见原因解决思路
Expected 4D input, got 3D卷积层输入缺少 batch 维度检查输入形状,确保是 [B, C, H, W]
mat1 and mat2 shapes cannot be multiplied全连接层输入维度计算错误打印展平后的形状,调整nn.Linear输入维度
loss.item()报错损失是张量,需要先转换为 Python 数值确保loss没有要求梯度,或用.item()
训练 loss 始终不下降学习率太大或太小尝试调整学习率,例如 0.01、0.001、0.0001
loss 变成 NaN学习率过大、数据包含 NaN、梯度爆炸减小学习率,检查数据,考虑梯度裁剪
准确率始终在 10%(多分类)模型输出维度与类别数不一致检查最后一层输出大小是否等于类别数
训练和测试准确率差距大过拟合增加 Dropout、数据增强、减小模型容量

排查问题建议遵循以下顺序:

  1. 先检查数据形状。用print(x.shape)逐步确认每一层输入输出。
  2. 再检查数值是否合理。打印 loss 初始值,如果一开始就是 0,说明有可能标签和数据没对齐。
  3. 然后检查优化器状态。确认optimizer.zero_grad()是否调用,如果忘记清梯度,梯度会累积。
  4. 最后考虑模型结构。对于新模型,先在一个小批量数据上跑通,再上全量训练。

9. 最佳实践与工程建议

9.1 数据处理

训练神经网络前,务必先做数据检查。随机的、未清洗的数据会直接导致模型效果差。建议在训练之前单独写一个脚本来可视化数据样本和标签,确认数据没有错位、方向颠倒等问题。另外,数据增强要在训练集上做,测试集只做 resize 和归一化,不能做随机增强。

9.2 模型设计

从简单的模型开始,逐步增加复杂度。一个好的做法是:先搭建一个极小的模型(比如单层 MLP),在当前数据集上跑通流程;再增加层数或换用复杂结构。这样定位问题会容易得多。如果模型不收敛,不要急着加更多层,先减少数据量看能否过拟合到少数样本,这能快速验证模型的表达能力。

9.3 实验管理

建议把实验配置(学习率、批量大小、epoch 数、模型结构等)写在配置文件中,或者用类似argparse的方式管理。每一轮实验记录以下信息:

  • 当前 commit 对应的代码版本。
  • 超参数组合。
  • 训练 loss 曲线和测试精度。
  • 随机种子。

固定随机种子是保证实验可复现的重要手段:

import random import numpy as np import torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42)

9.4 模型保存与加载

训练结束后,保存模型参数:

# 保存 torch.save(model.state_dict(), 'model_weights.pth') # 加载 model = SimpleCNN() model.load_state_dict(torch.load('model_weights.pth')) model.eval()

这里有两个需要注意的点:一是保存state_dict()而不是整个 model 对象,这样可以避免代码结构变化后的兼容问题;二是加载权重前要先创建同结构的模型实例,模型结构必须与保存时一致。

9.5 注意安全边界

如果你在真实业务中使用神经网络,比如用户画像、风控模型、医疗辅助诊断,请务必保持谨慎:

  • 不要使用未经脱敏的真实个人信息作为训练数据。
  • 模型上线前需要充分评估误差率,特别是错误分类可能带来严重后果的场景。
  • 模型不能作为唯一决策依据,尤其是涉及健康、财务和法律的问题。
  • 尽量使用数据隔离,确保训练数据与在线推理数据完全分离。

9.6 性能优化

如果训练速度过慢,可以参考以下优化方向:

  • 使用 GPU:只要条件允许,尽量把模型和数据搬到 GPU 上。
  • 增大 batch size:但需要同步调整学习率。
  • 使用混合精度训练:PyTorch 自带torch.cuda.amp,可以减少显存占用并加速训练。
  • 减少数据加载瓶颈:使用num_workers参数并预取数据。

10. 总结与下一步学习建议

这篇文章从神经网络的基本概念出发,逐步讲解了 BP 神经网络、MLP 和 CNN 的完整代码实现。你如果跟着本文的代码敲了一遍,应该已经掌握了 PyTorch 训练模型的标准流程:定义网络结构、加载数据、选择损失函数和优化器、执行训练循环、评估模型效果。这个流程可以复用到几乎所有的深度学习任务中,无论是文本分类、目标检测还是语音识别,骨架都是一样的。

下一步,建议你按这个路线继续深入:

  1. 动手修改本文的 CNN 结构,增加一层卷积、调整通道数,观察准确率变化。
  2. 学习数据增强的常见方式,并把它应用到 CIFAR-10 训练中。
  3. 用 RNN 或 LSTM 做一个简单的中文文本分类任务。
  4. 学习迁移学习,用预训练的 ResNet 在自定义数据集上微调,这是工程中最常用的技巧。
  5. 当你的网络越来越深、数据越来越多时,再去深入了解学习率调度、正则化、分布式训练等进阶内容。

神经网络的学习曲线确实有些陡峭,初期会遇到大量报错和概念困惑,但只要你动手把这份代码跑通、改一改、调一调,很多模糊的概念就会自然清晰起来。如果你在实践过程中遇到具体报错,可以先按第 8 节的排查表格对照,或者把错误信息中的 shape 信息打印出来,大多数问题都能快速定位。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 6:38:21

稳压器怎么选?技术流派、容量计算与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 6:37:12

Whisper+LLM本地搭建语音转文字清理工作流

先说一下我自己的使用背景。最近在整理录音笔记和口述草稿时,频繁在“语音转文字 → 内容清理”这两步之间来回折腾。腾讯会议导出的转写稿口语词太多,在线工具又总担心隐私问题,尤其涉及未公开方案和客户信息时,根本不敢往外传。…

作者头像 李华
网站建设 2026/9/7 6:36:00

FPGA上实现100G UDP传输:开源协议栈移植与上板测试全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 6:35:01

用Cocos Creator 3.8制作3D合成大西瓜:物理引擎与TypeScript实战

这次我们来看一个非常典型的小游戏项目:用Cocos Creator 3.8制作 3D 版合成大西瓜。玩法大家都很熟,点击或触摸投放水果,相同等级的水果碰到一起就合并成下一等级,一路合成到“大西瓜”。区别在于这次不是 2D 平面逻辑&#xff0c…

作者头像 李华
网站建设 2026/9/7 6:34:49

MCU芯片开发实战指南:从启动流程到电源设计的全面解读

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 6:33:24

深度网格投影与360立体渲染:UE5 VR性能与分发实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华