说实话,我第一次跑通那个简单的神经网路模型时,屏幕上的 loss: 0.12 比中彩票还让人兴奋。但在那之前,我经历了整整两周的“报错地狱”。如果你现在正盯着满屏红色的 traceback 发呆,或者看着教程里的代码跑出来全是 NaN,别慌,你不是一个人。这篇东西不是教科书,是我踩过的坑、吐过的槽,最后怎么把代码跑通的真实记录。我会尽量把道理讲得连我家那个高二的小侄子都能听明白,毕竟,能教得简单,才是真的懂。
第一章:环境配置——那个“找不到模块”的噩梦
1.1 虚拟环境:你的救命稻草
很多新手上来就 pip install tensorflow 或者 pip install pytorch,装在全局的 Python 环境里。结果呢?过两个月想换个项目,发现版本冲突,整个 Python 环境崩了,只能重装系统。
为什么一定要用虚拟环境?
想象一下,你的 Python 环境是一个大厨房。全局环境就是大家共用的厨房,有人放了辣油(TensorFlow 2.4),有人放了芥末(PyTorch 1.9),你想做一个清淡的菜(PyTorch 2.0),结果发现全是辣油味,根本没法做。
虚拟环境就是给你自己切出一个专属的小厨房,关上门,谁也别想进。
实操步骤(以 Python 3.10 为例):
# 1. 创建虚拟环境(名字叫 venv_dl)
python -m venv venv_dl
# 2. 激活虚拟环境
# Windows:
venv_dl\Scripts\activate
# macOS/Linux:
source venv_dl/bin/activate
# 激活后,你的命令行前面会出现 (venv_dl) 字样,这就是成功了
# 3. 升级 pip(非常重要,避免旧版 pip 装包出问题)
pip install --upgrade pip
# 4. 安装 PyTorch(以 CPU 版本为例,GPU 用户请去官网选 CUDA 版本)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
# 或者 GPU 版本(NVIDIA 显卡用户):
# pip install torch torchvision torchaudio
# 5. 安装常用库
pip install numpy pandas matplotlib scikit-learn
常见坑:
- 装完发现版本不对:比如你要 CUDA 11.8,结果装了 CUDA 12.1 的版本,运行时直接炸。解决方案:去 PyTorch 官网 复制对应的安装命令,不要自己瞎猜。
- IDE 找不到解释器:在 VS Code 或 PyCharm 里,手动指定解释器路径。VS Code 右下角点一下,选
venv_dl/bin/python(macOS/Linux)或venv_dl\Scripts\python.exe(Windows)。
1.2 设备选择:CPU vs GPU
新手最容易纠结这个问题:“我有没有必要买显卡?”
现实情况:
- 学习阶段:CPU 完全够用。MNIST 手写数字识别、Fashion-MNIST 这些经典小数据集,在 CPU 上跑几分钟就能完事。别被网上那些“GPU 才是正统”的说法吓到。
- 真正训练大模型:比如用 ImageNet 数据集训练 ResNet,或者搞大语言模型,CPU 会慢到让你怀疑人生。这时候,GPU 是必须的。
如何检查你的 GPU 是否可用?
import torch
print(f"PyTorch 版本: {torch.__version__}")
print(f"CUDA 是否可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"GPU 数量: {torch.cuda.device_count()}")
print(f"当前 GPU 名称: {torch.cuda.get_device_name(0)}")
如果输出 CUDA 是否可用: False,而你又确信自己装了 GPU 版本,那通常是 CUDA 版本不匹配,回去检查安装命令。
给小朋友打的比方: CPU 像一个老教授,一根筋,一次只能算一道题,但很稳。GPU 像一千个小学生在操场上同时算题,虽然小,但人多力量大。如果你算的题特别简单(比如加减乘除),老教授反而更快;如果你要算一千万道同样的题,那就得靠小学生团队。
第二章:数据加载——别让你的数据“饿死”模型
2.1 为什么数据比模型重要?
业界有句话:“垃圾进,垃圾出”(Garbage In, Garbage Out)。你花一周时间调参,结果发现数据标注错了,前面全白干。
新手常见错误:
- 数据没归一化:像素值是 0-255,直接扔进网络,梯度会爆炸。
- 训练集和测试集混在一起:这是作弊!模型会记住测试集的答案,上线后性能暴跌。
- 数据形状搞错:PyTorch 期望
(batch_size, channels, height, width),但你可能传了(height, width, channels)。
2.2 实战:加载 MNIST 数据集
MNIST 是“Hello World”级别的数据集,包含 7 万张 28x28 的手写数字图片(0-9)。
import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np
# 1. 定义数据预处理
# 将 PIL 图像或 numpy 数组转换为 Tensor,并归一化到 [0, 1]
transform = transforms.Compose([
transforms.ToTensor(), # 0-255 -> 0.0-1.0
transforms.Normalize((0.5,), (0.5,)) # 归一化到 [-1, 1],方便模型学习
])
# 2. 下载并加载训练集和测试集
# download=True 会自动从网上下载,如果网络不好可以提前下载好
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)
# 3. 数据加载器:把数据打成小批量(Batch)
# batch_size=64 表示每次给模型喂 64 张图片
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)
# 4. 看看数据长什么样
def show_image(sample):
img, label = sample
# img 形状是 (1, 28, 28),转为 (28, 28) 才能用 matplotlib 显示
plt.imshow(img.squeeze(), cmap='gray')
plt.title(f'Label: {label}')
plt.axis('off')
plt.show()
# 取一个样本看看
sample = next(iter(train_loader))
images, labels = sample
show_image((images[0], labels[0]))
print(f"训练集大小: {len(train_dataset)}")
print(f"测试集大小: {len(test_dataset)}")
print(f"图片形状: {images[0].shape}") # 应该是 torch.Size([1, 28, 28])
关键点解释:
shuffle=True:打乱数据顺序,防止模型记住数据的排列规律。Normalize((0.5,), (0.5,)):将数据从 [0, 1] 映射到 [-1, 1]。中心在 0,对称分布,对梯度下降更友好。DataLoader:它像一个流水线工人,自动帮你把数据分批、打乱、多线程加载。
2.3 常见报错及解决
报错 1: FileNotFoundError: [Errno 2] No such file or directory: './data/MNIST/raw/train-images-idx3-ubyte.gz'
原因:下载中途断网,文件损坏或未下载完整。
解决:
# 删除损坏的文件,重新运行 download=True
rm -rf ./data/MNIST
或者手动从 MNIST 官网 下载四个文件,放入 ./data/MNIST/raw/ 目录。
报错 2: RuntimeError: The size of tensor a (28) must match the size of tensor b (4) at non-singleton dimension 0
原因:标签(label)和预测结果(output)的形状不匹配。通常是因为模型输出层单元数不对,或者没有正确展开标签。
解决:检查模型最后一层的 out_features 是否为 10(因为 MNIST 有 10 类),并确保使用 nn.CrossEntropyLoss() 时,标签是长整型(torch.long),而不是浮点型。
# 标签应该是 long 类型
labels = labels.long()
第三章:搭建模型——从线性回归到神经网络
3.1 最简单的模型:线性回归
在讲复杂网络前,我们先回归本质。假设你要预测房价,特征只有“面积”。这其实就是一个线性方程:\(y = wx + b\)。
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 准备伪数据:x 是面积,y 是房价
x_train = torch.tensor([[1.0], [2.0], [3.0], [4.0]], dtype=torch.float32)
y_train = torch.tensor([[2.0], [4.0], [6.0], [8.0]], dtype=torch.float32) # 假设房价 = 面积 * 2
# 2. 定义模型:一个线性层
model = nn.Linear(1, 1) # 输入维度 1,输出维度 1
# 3. 定义损失函数:均方误差(MSE)
criterion = nn.MSELoss()
# 4. 定义优化器:随机梯度下降(SGD)
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 5. 训练循环
for epoch in range(100):
# 前向传播
outputs = model(x_train)
loss = criterion(outputs, y_train)
# 反向传播(计算梯度)
optimizer.zero_grad() # 清空上一次的梯度
loss.backward() # 计算当前梯度
# 更新权重
optimizer.step()
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch+1}/100], Loss: {loss.item():.4f}')
# 6. 测试
x_test = torch.tensor([[5.0]], dtype=torch.float32)
prediction = model(x_test)
print(f'Predicted price for area 5.0: {prediction.item():.2f}') # 应该接近 10.0
给小朋友的解释: 模型就像一个猜测者,他每次猜完,你就告诉他“错了,差这么多”,他记录下来,下次调整一下猜测的幅度。100 次之后,他就能猜得很准了。这个过程叫“训练”。
3.2 进阶:感知机(MLP)处理 MNIST
线性模型太简单了,处理不了复杂的图像。我们需要加入“非线性激活函数”,让模型能学更复杂的规律。
import torch
import torch.nn as nn
import torch.optim as optim
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
# 展平层:把 28x28 的图片压成 784 个特征
self.flatten = nn.Flatten()
# 第一个全连接层:784 -> 128
self.fc1 = nn.Linear(784, 128)
# 激活函数:ReLU(修正线性单元,最简单的非线性)
self.relu = nn.ReLU()
# 第二个全连接层:128 -> 10(10 个数字类别)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.flatten(x) # [batch, 1, 28, 28] -> [batch, 784]
x = self.relu(self.fc1(x)) # 128 维向量
x = self.fc2(x) # 10 维向量( logits )
return x
# 实例化模型
model = SimpleNN()
print(model)
# 定义损失函数和优化器
# CrossEntropyLoss 内部包含了 Softmax,所以输出不需要再转概率
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam 比 SGD 收敛更快
关键点:
nn.Flatten():神经网络只接受一维向量,所以要把二维图片“压扁”。nn.ReLU():把负数变成 0,正数不变。它让模型能学“如果像素某处很黑,那可能是数字 8 的上半部分”这种复杂规则。nn.CrossEntropyLoss():专门用于多分类问题,输入是未归一化的 logits,内部会自动做 softmax。
3.3 训练循环:标准的“四步走”
# 训练 5 个 epoch(遍历整个训练集 5 次)
num_epochs = 5
for epoch in range(num_epochs):
model.train() # 切换到训练模式(dropout 等层会生效)
total_loss = 0
correct = 0
total = 0
for batch_idx, (data, targets) in enumerate(train_loader):
# 1. 前向传播
outputs = model(data)
loss = criterion(outputs, targets)
# 2. 反向传播
optimizer.zero_grad() # 清空梯度
loss.backward() # 计算梯度
# 3. 更新参数
optimizer.step() # 走一步
# 4. 统计准确率(可选)
_, predicted = torch.max(outputs.data, 1)
total += targets.size(0)
correct += (predicted == targets).sum().item()
total_loss += loss.item()
if batch_idx % 100 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], Batch [{batch_idx}/{len(train_loader)}], Loss: {loss.item():.4f}')
# 打印每个 epoch 的准确率
print(f'Epoch [{epoch+1}/{num_epochs}] Accuracy: {100 * correct / total:.2f}%')
给小朋友的解释: 训练就像备考。
- 前向传播:模拟考试,把答案写出来。
- 损失计算:对答案,看错多少。
- 反向传播:分析错题原因,哪里没学好。
- 参数更新:针对性复习,下次改正。
- Epoch:全套试卷做一遍。做 5 遍,基本就能及格了。
3.4 测试与评估
model.eval() # 切换到评估模式(dropout 等层会关闭)
test_loss = 0
correct = 0
with torch.no_grad(): # 测试时不需要计算梯度,节省内存
for data, targets in test_loader:
outputs = model(data)
loss = criterion(outputs, targets)
_, predicted = torch.max(outputs.data, 1)
correct += (predicted == targets).sum().item()
test_loss += loss.item() * data.size(0)
test_loss /= len(test_dataset)
accuracy = 100 * correct / len(test_dataset)
print(f'Test Loss: {test_loss:.4f}, Accuracy: {accuracy:.2f}%')
注意:
torch.no_grad():测试时不需要反向传播,加上这个可以节省一半内存,加快速度。model.eval():有些层(如 Dropout、BatchNorm)在训练和测试时行为不同,必须调用这个。
第四章:经典坑点大全——这些错我替你犯了
坑 1:形状不匹配(Shape Mismatch)
这是新手报错最多的原因。PyTorch 要求形状严格匹配。
常见错误:
- 输入图片是
(batch, 1, 28, 28),但模型期望(batch, 28, 28)。 - 标签是
(batch,),但模型输出是(batch, 10),计算损失时形状不一致。
解决方法: 永远打印形状!
print(f"Input shape: {data.shape}")
print(f"Target shape: {targets.shape}")
print(f"Output shape: {output.shape}")
通过打印,你会发现哪里出了问题。比如,如果输出是 (64, 1) 而不是 (64, 10),那肯定是最后一层的 out_features 写错了。
坑 2:梯度爆炸/消失(Gradient Exploding/Vanishing)
现象: 训练刚开始,loss 变成 NaN 或 Inf;或者训练很久,loss 几乎不下降。
原因: 层数太深,梯度在反向传播时连乘,要么变得巨大,要么变得微小。
解决方法:
- 使用 ReLU 激活函数:避免使用 Sigmoid/Tanh,它们在两端导数接近 0。
- 权重初始化:PyTorch 默认使用 Kaiming 初始化,对 ReLU 友好。如果想手动控制: “`python for m in model.modules(): if isinstance(m, nn.Linear): nn.init.kaiming_normal
