PyTorch作为当前最受欢迎的深度学习框架之一,以其动态计算图和Pythonic的编程风格赢得了大量开发者的青睐。这个教程专为刚接触PyTorch的新手设计,将带你从零开始构建并训练第一个神经网络模型。不同于官方文档的抽象说明,我会以一个实际的图像分类任务为例,展示完整的开发流程和常见陷阱。
我在2017年第一次接触PyTorch时,曾被其灵活的调试特性所吸引,但同时也踩过不少初学者常见的坑。本教程将分享这些经验教训,帮助你避开那些让新手头疼的问题。我们将使用经典的MNIST手写数字数据集,因为它足够简单又能体现核心概念。
推荐使用Anaconda创建独立的Python环境:
bash复制conda create -n pytorch_env python=3.8
conda activate pytorch_env
pip install torch torchvision
验证安装是否成功:
python复制import torch
print(torch.__version__) # 应显示如1.12.1版本号
print(torch.cuda.is_available()) # 检查GPU支持
注意:如果使用GPU加速,需额外安装对应CUDA版本的PyTorch。Windows用户建议通过官网提供的安装命令直接获取预编译版本。
使用torchvision加载MNIST数据集:
python复制from torchvision import datasets, transforms
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_data = datasets.MNIST(
root='data',
train=True,
download=True,
transform=transform
)
test_data = datasets.MNIST(
root='data',
train=False,
transform=transform
)
数据加载器配置技巧:
python复制from torch.utils.data import DataLoader
batch_size = 64
train_loader = DataLoader(
train_data,
batch_size=batch_size,
shuffle=True, # 训练集必须打乱
num_workers=4 # 多进程加载加速
)
test_loader = DataLoader(
test_data,
batch_size=batch_size,
shuffle=False # 测试集无需打乱
)
实操心得:num_workers设置应根据CPU核心数调整,通常为物理核心数的1-2倍。设置过高可能导致内存溢出。
我们构建一个包含两个隐藏层的全连接网络:
python复制import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(28*28, 512) # 输入层到隐藏层1
self.fc2 = nn.Linear(512, 256) # 隐藏层1到隐藏层2
self.fc3 = nn.Linear(256, 10) # 隐藏层2到输出层
def forward(self, x):
x = x.view(-1, 28*28) # 展平图像
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x) # 最后一层不激活
return F.log_softmax(x, dim=1)
关键设计考量:
python复制def weights_init(m):
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
nn.init.constant_(m.bias, 0.1)
model = Net()
model.apply(weights_init)
Xavier初始化能根据每层神经元数量自动调整初始权重范围,相比随机初始化能更快收敛。我在实际项目中发现,配合偏置的小常数初始化(0.1)能有效避免死亡神经元问题。
python复制from torch import optim
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = nn.NLLLoss()
epochs = 10
for epoch in range(epochs):
model.train()
train_loss = 0
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
train_loss += loss.item()
if batch_idx % 100 == 0:
print(f'Train Epoch: {epoch} [{batch_idx*len(data)}/{len(train_loader.dataset)}]'
f'\tLoss: {loss.item():.6f}')
# 每个epoch结束后验证
model.eval()
test_loss = 0
correct = 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()
pred = output.argmax(dim=1)
correct += pred.eq(target).sum().item()
test_loss /= len(test_loader)
accuracy = 100. * correct / len(test_loader.dataset)
print(f'\nTest set: Average loss: {test_loss:.4f}, '
f'Accuracy: {correct}/{len(test_loader.dataset)} ({accuracy:.2f}%)\n')
避坑指南:务必在训练前后调用model.train()和model.eval(),这对Dropout和BatchNorm层的行为有决定性影响。我曾因忘记切换模式导致验证结果异常波动。
安装matplotlib后添加记录代码:
python复制import matplotlib.pyplot as plt
train_losses = []
test_losses = []
test_accuracies = []
# 在训练循环中追加记录
train_losses.append(train_loss/len(train_loader))
test_losses.append(test_loss)
test_accuracies.append(accuracy)
# 训练结束后绘制
plt.figure(figsize=(12,4))
plt.subplot(1,2,1)
plt.plot(train_losses, label='Train')
plt.plot(test_losses, label='Test')
plt.legend()
plt.subplot(1,2,2)
plt.plot(test_accuracies)
plt.title('Test Accuracy')
plt.show()
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练损失不下降 | 学习率过低/网络结构不合理 | 增大学习率或加深网络 |
| 测试准确率远低于训练集 | 过拟合 | 添加Dropout层/L2正则化 |
| 损失值出现NaN | 学习率过高/数据未归一化 | 降低学习率检查数据预处理 |
| GPU利用率低 | Batch Size太小/数据加载慢 | 增大batch_size或优化DataLoader |
python复制scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)
# 每个epoch结束后调用 scheduler.step()
python复制self.dropout = nn.Dropout(0.5) # 在__init__中添加
x = self.dropout(F.relu(self.fc2(x))) # 在forward中使用
python复制optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)
保存完整模型:
python复制torch.save(model, 'mnist_model.pt')
loaded_model = torch.load('mnist_model.pt')
推荐保存状态字典(更安全):
python复制torch.save(model.state_dict(), 'mnist_state_dict.pt')
new_model = Net()
new_model.load_state_dict(torch.load('mnist_state_dict.pt'))
python复制from PIL import Image
import numpy as np
def predict_image(img_path):
img = Image.open(img_path).convert('L')
img = transform(img).unsqueeze(0).to(device)
with torch.no_grad():
output = model(img)
_, predicted = torch.max(output, 1)
return predicted.item()
# 示例:预测test集中的第一张图片
sample_img = test_data[0][0].numpy().reshape(28,28)
plt.imshow(sample_img, cmap='gray')
print(f"Predicted: {predict_image('sample.png')}")
部署提示:生产环境建议使用TorchScript导出模型,能脱离Python环境运行。使用torch.jit.trace或torch.jit.script转换模型后保存。
完成基础模型后,可以从以下方向深入:
我在实际项目中发现,PyTorch的hook机制对于调试复杂网络非常有用。例如注册forward_hook可以实时监控各层输出分布,这对诊断梯度消失/爆炸问题特别有效。