神经网络(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 激活函数:给网络引入非线性
如果神经网络只有线性变换,那么无论堆多少层,本质上仍然等价于一层线性模型。激活函数的作用就是引入非线性,让网络能够学习复杂的模式。
常用的激活函数:
| 激活函数 | 公式 | 特点 |
|---|---|---|
| ReLU | max(0, x) | 计算简单,缓解梯度消失,最常用 |
| Sigmoid | 1 / (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 训练闭环:五步循环
一个标准的训练循环包含五个步骤:
- 前向传播:把数据输入模型,得到预测输出。
- 计算损失:将预测输出与真实标签比较,得到 loss。
- 梯度清零:清空上一次迭代留下的梯度。
- 反向传播:计算损失对每个参数的梯度。
- 更新参数:优化器根据梯度更新模型权重。
对应 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 或时间序列建模,建议按下面的顺序学习:
- 标准 RNN(理解时间步和隐藏状态)。
- LSTM(理解门控机制)。
- GRU(LSTM 的简化版本)。
- Seq2Seq(编码器-解码器结构)。
- 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、数据增强、减小模型容量 |
排查问题建议遵循以下顺序:
- 先检查数据形状。用
print(x.shape)逐步确认每一层输入输出。 - 再检查数值是否合理。打印 loss 初始值,如果一开始就是 0,说明有可能标签和数据没对齐。
- 然后检查优化器状态。确认
optimizer.zero_grad()是否调用,如果忘记清梯度,梯度会累积。 - 最后考虑模型结构。对于新模型,先在一个小批量数据上跑通,再上全量训练。
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 训练模型的标准流程:定义网络结构、加载数据、选择损失函数和优化器、执行训练循环、评估模型效果。这个流程可以复用到几乎所有的深度学习任务中,无论是文本分类、目标检测还是语音识别,骨架都是一样的。
下一步,建议你按这个路线继续深入:
- 动手修改本文的 CNN 结构,增加一层卷积、调整通道数,观察准确率变化。
- 学习数据增强的常见方式,并把它应用到 CIFAR-10 训练中。
- 用 RNN 或 LSTM 做一个简单的中文文本分类任务。
- 学习迁移学习,用预训练的 ResNet 在自定义数据集上微调,这是工程中最常用的技巧。
- 当你的网络越来越深、数据越来越多时,再去深入了解学习率调度、正则化、分布式训练等进阶内容。
神经网络的学习曲线确实有些陡峭,初期会遇到大量报错和概念困惑,但只要你动手把这份代码跑通、改一改、调一调,很多模糊的概念就会自然清晰起来。如果你在实践过程中遇到具体报错,可以先按第 8 节的排查表格对照,或者把错误信息中的 shape 信息打印出来,大多数问题都能快速定位。