
PyTorch
用 PyTorch 从张量到训练一个神经网络
从安装 PyTorch 开始,依次掌握张量的创建、索引、切片与变形,广播与矩阵乘法,GPU 加速,再用 Dataset/DataLoader 组织数据,完整训练并评估一个两层神经网络。每一步都给出可直接运行的代码与输出说明。
PyTorch 是一个开源的深度学习框架,核心特点是动态计算图:网络结构在运行时构建,因此可以在代码里直接修改模型行为,调试时也能逐行观察张量的变化。它适合两类读者:一是刚接触深度学习、想亲手把「张量 → 数据管道 → 训练循环 → 评估」这条链路走通的人;二是已经会用别的框架、想快速迁移到 PyTorch 的开发者。本文按这条链路组织,每一节都给出可直接运行的代码。如果你更想先确认环境与依赖是否就绪,可以先看 PyTorch 工具页,再回到本文动手。
准备工作
Python 环境与安装
PyTorch 通过 pip 安装,最基础的两个包是 torch 与 torchvision:
pip install torch torchvision
如果你的机器上有支持 CUDA 的 GPU,需要安装与显卡驱动匹配的 CUDA 版本。素材给出的示例命令是:
pip install torch torchvision torchaudio cudatoolkit=11.3
这里的 cudatoolkit=11.3 只是一个示例版本号,实际应安装与你本机驱动、操作系统相匹配的版本。具体可用的版本组合、安装命令生成方式以及各版本的生命周期,请以 PyTorch 官网当前信息为准,不要照抄本文的版本号。
验证安装
安装完成后,用下面这段代码确认 PyTorch 能正常导入,并判断当前环境是否可用 GPU:
import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')
这段判断逻辑在后面的 GPU 加速一节还会用到。它把设备选择收敛到一个变量里,后续创建张量时统一传入 device,代码就能在有 GPU 和没有 GPU 的机器上通用。
前置知识
阅读本文需要会写基本的 Python 函数与类,理解「输入 → 输出」的函数映射即可。神经网络部分的数学只用到矩阵乘法与梯度下降的直观概念,不要求推导。
操作步骤
第一步:创建张量
张量(tensor)是多维数组,是 PyTorch 里一切数据的基本结构。可以用 Python 列表直接构造,也可以用内置的工厂函数生成特定形状与初始值的张量:
import torch
tensor_1d = torch.tensor([1, 2, 3])
print("1D Tensor (Vector):")
print(tensor_1d)
print()
tensor_2d = torch.tensor([[1, 2], [3, 4]])
print("2D Tensor (Matrix):")
print(tensor_2d)
print()
random_tensor = torch.rand(2, 3)
print("Random Tensor (2x3):")
print(random_tensor)
print()
zeros_tensor = torch.zeros(2, 3)
print("Zeros Tensor (2x3):")
print(zeros_tensor)
print() 3)
print("Ones Tensor (2x3):")
print(ones_tensor)
几个函数的区别值得记牢:torch.tensor() 从已有数据构造,数据类型由数据推断;torch.rand() 生成指定形状的随机张量;torch.zeros() 与 torch.ones() 分别生成全 0 与全 1 张量。后三个函数只接收形状参数,不接收数据。
第二步:索引、切片与变形
拿到张量之后,最常见的三类操作是取单个元素、取子区域、改变形状。下面用一个 3×2 的张量演示:
import torch
tensor = torch.tensor([[1, 2], [3, 4], [5, 6]])
element = tensor[1, 0]
print(f"Indexed Element (Row 1, Column 0): {element}")
slice_tensor = tensor[:2, :]
print(f"Sliced Tensor (First two rows): \n{slice_tensor}")
reshaped_tensor = tensor.view(2, 3)
print(f"Reshaped Tensor (2x3): \n{reshaped_tensor}")
- 索引:
tensor[1, 0]取第 1 行第 0 列的元素。注意 PyTorch 的行列下标从 0 开始。 - 切片:
tensor[:2, :]取前两行的全部列,得到一个新的 2×2 张量。 - 变形:
view(2, 3)把 3×2 的张量重排成 2×3,元素总数不变、数值不变,只是维度组织方式变了。
变形操作要求元素总数前后一致,否则会报错。这一点在把图像数据展平送进全连接层时尤其容易踩到。
第三步:广播与矩阵乘法
广播(broadcasting)让形状不同的张量也能做逐元素运算:较小的张量会在缺失的维度上自动扩展。矩阵乘法则是神经网络里最核心的计算,用 torch.matmul 完成:
import torch
tensor_a = torch.tensor([[1, 2, 3], [4, 5, 6]])
tensor_b = torch.tensor([[10, 20, 30]])
broadcasted_result = tensor_a + tensor_b
print(f"Broadcasted Addition Result: \n{broadcasted_result}")
matrix_multiplication_result = torch.matmul(tensor_a, tensor_a.T)
print(f"Matrix Multiplication Result (tensor_a * tensor_a^T): \n{matrix_multiplication_result}")
tensor_a 的形状是 2×3,tensor_b 的形状是 1×3。相加时 tensor_b 被扩展成 2×3,两行分别加上 [10, 20, 30]。矩阵乘法里 tensor_a.T 是转置,形状变为 3×2,因此 2×3 乘 3×2 得到 2×2 的结果。理解形状如何变化,是排查神经网络维度报错的关键。
第四步:把计算放到 GPU 上
深度学习涉及大量矩阵运算,把张量搬到 GPU 上通常能显著缩短训练时间。做法是在创建张量时传入 device 参数:
import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')
tensor_size = (10000, 10000)
a = torch.randn(tensor_size, device=device)
b = torch.randn(tensor_size, device=device)
c = a + b
print("Result shape (moved to CPU for printing):", c.cpu().shape)
print("Current GPU memory usage:")
print(f"Allocated: {torch.cuda.memory_allocated(device) / (1024 ** 2):.2f} MB")
print(f"Cached: {torch.cuda.memory_reserved(device) / (1024 ** 2):.2f} MB")
这里有两个实用细节。第一,打印 GPU 上的张量前先调用 .cpu(),把结果搬回内存,避免在部分环境下打印出错。第二,torch.cuda.memory_allocated() 返回当前实际被张量占用的显存,torch.cuda.memory_reserved() 返回缓存分配器保留的显存,两者都以字节为单位,除以 1024 ** 2 换算成 MB。显存不足是训练大模型时最常见的报错来源,这两个数值是排查的起点。
如果 torch.cuda.is_available() 返回 False,上面的代码会自动回退到 CPU,逻辑仍然正确,只是速度慢。这也是把设备选择写成变量的好处。
第五步:定义神经网络类
PyTorch 的模型都继承自 torch.nn.Module,需要实现两个部分:在 __init__ 里声明各层,在 forward 里定义数据如何流经这些层。下面是一个含一个隐藏层的网络,输入 2 维、隐藏层 4 维、输出 1 维:
import torch
import torch.nn as nn
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.fc1 = nn.Linear(2, 4)
self.fc2 = nn.Linear(4, 1)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x
nn.Linear(2, 4) 表示输入特征数为 2、输出特征数为 4 的全连接层,内部自带权重与偏置。forward 里先过第一层,再套 ReLU 激活函数引入非线性,最后过第二层得到输出。没有激活函数的话,两层线性变换叠加等价于一层线性变换,网络就失去了拟合非线性关系的能力。
第六步:准备训练数据
为了能直观判断模型是否学到了东西,这里用 XOR(异或)逻辑门作为数据集:输入是两个二进制位,输出是它们的异或结果。XOR 是非线性可分的,正好能检验隐藏层是否起作用:
X_train = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
y_train = torch.tensor([[0.0], [1.0], [1.0], [0.0]])
注意输入与标签都写成浮点数,并且标签的形状是 4×1 而不是长度 4 的一维张量,这样才能和模型输出 (4, 1) 的形状对齐。
第七步:实例化模型、损失函数与优化器
模型、损失函数、优化器是训练的三件套。这里用均方误差作为损失,用随机梯度下降(SGD)更新权重,学习率设为 0.1:
import torch.optim as optim
model = SimpleNN()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
model.parameters() 把模型里所有需要训练的张量交给优化器,优化器据此知道该更新谁。lr 是学习率,控制每次更新的步长。
第八步:编写训练循环
训练循环是 PyTorch 里最固定的骨架,顺序不能乱:前向传播算输出 → 算损失 → 清空上一步的梯度 → 反向传播算梯度 → 优化器更新参数。
for epoch in range(100):
model.train()
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}')
几个容易出错的点:optimizer.zero_grad() 必须放在 loss.backward() 之前,因为 PyTorch 默认会累加梯度,不清零就会把上一轮的梯度叠加进来;loss.item() 把单元素张量取出为 Python 浮点数,方便格式化打印;model.train() 把模型切到训练模式,对含 Dropout、BatchNorm 的模型这一步是必须的。每 10 个 epoch 打印一次损失,可以观察损失是否在下降。
第九步:评估模型
训练结束后要评估模型在新数据上的表现。评估时切到 model.eval(),并用 torch.no_grad() 关闭梯度计算,既省内存也更快:
model.eval()
with torch.no_grad():
test_data = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
predictions = model(test_data)
print(f'Predictions: \n{predictions}')
输出是四个连续值,分别对应四组输入的预测。因为损失用的是均方误差,输出不会被压到 0 或 1,而是接近目标值的小数。判断模型是否学会 XOR,看的是「接近 0 的输入预测值小、接近 1 的输入预测值大」这个趋势是否成立。
一个完整示例
把上面的步骤串起来,下面是一份从头到尾可运行的完整脚本。它包含设备选择、模型定义、数据准备、训练与评估:
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 选择设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')
# 2. 定义模型
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.fc1 = nn.Linear(2, 4)
self.fc2 = nn.Linear(4, 1)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x
# 3. 准备数据
X_train = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]], device=device)
y_train = torch.tensor([[0.0], [1.0], [1.0], [0.0]], device=device)
# 4. 实例化模型、损失函数、优化器
model = SimpleNN().to(device)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
# 5. 训练
for epoch in range(100):
model.train()
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. 评估
model.eval()
with torch.no_grad():
predictions = model(X_train)
print(f'Predictions: \n{predictions}')
与前面分步代码相比,这份脚本多了两处 device 处理:数据张量在创建时指定 device,模型用 .to(device) 搬到同一设备。PyTorch 要求参与同一次运算的张量位于同一设备上,模型在 GPU 而数据在 CPU 会直接报错,这是初学者最常见的错误之一。
用 Dataset 与 DataLoader 组织数据
上面的例子把全部数据一次性喂给模型。真实任务的数据量往往很大,需要分批、打乱地迭代,这正是 Dataset 与 DataLoader 的职责。
自定义 Dataset
继承 torch.utils.data.Dataset,实现三个方法:__init__ 负责加载数据,__len__ 返回样本总数,__getitem__ 按索引返回单个样本:
import torch
from torch.utils.data import Dataset, DataLoader
class MyDataset(Dataset):
def __init__(self):
self.data = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
self.labels = torch.tensor([0, 1, 0])
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx], self.labels[idx]
dataset = MyDataset()
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
for batch in dataloader:
print("Batch Data:", batch[0])
print("Batch Labels:", batch[1])
batch_size=2 表示每批取 2 个样本,shuffle=True 表示每个 epoch 开始前打乱顺序。打乱能避免模型学到样本排列带来的虚假规律。由于样本总数是 3、批大小是 2,最后一个批次只会有 1 个样本,这是正常现象。
用 DataLoader 驱动训练
把 DataLoader 接进训练循环,只需把「一次算全部数据」换成「逐批计算」:
for epoch in range(2):
for inputs, labels in dataloader:
outputs = inputs + 1
print(f"Epoch {epoch + 1}, Inputs: {inputs}, Labels: {labels}, Outputs: {outputs}")
这段代码用 inputs + 1 代替真实的前向传播,目的是展示批次迭代的结构。实际训练时,把 outputs = inputs + 1 换成 outputs = model(inputs),再补上损失计算、梯度清零、反向传播与参数更新四步即可。分批处理的好处是计算效率更高,在 GPU 等硬件加速器上尤其明显。
图像数据增强
如果任务是图像分类,torchvision.transforms 提供了常用的随机变换,比如随机水平翻转、转成张量。这些变换能增加数据的多样性,帮助模型在未见过的数据上泛化得更好:
import torchvision.transforms as transforms
from PIL import Image
image = Image.open('example.jpg') # 替换为你自己的图片文件
transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.ToTensor()
])
augmented_image = transform(image)
print("Augmented Image Shape:", augmented_image.shape)
transforms.Compose 把多个变换按顺序串起来:先随机水平翻转,再转成张量。ToTensor() 会把像素值缩放到 0 到 1 之间,并把通道维度放到最前面,因此打印出的形状是「通道 × 高 × 宽」。运行前需要准备一张真实存在的图片,并把文件名替换掉。
进阶模型方向
掌握上面的流程后,可以按任务类型换用不同的网络层,训练循环的骨架基本不变。
卷积神经网络(CNN)
处理图像时用 torch.nn.Conv2d 做卷积、配合池化层降采样。在卷积层之后接 torch.nn.BatchNorm2d 做批归一化,可以稳定训练过程、加快收敛。批归一化对卷积层的输出做归一化,是图像模型里的常见配置。
循环神经网络(RNN)
处理序列数据时用 torch.nn.LSTM 或 torch.nn.GRU。这两类模块在 PyTorch 里可以直接调用,适合文本、时间序列等有先后顺序的任务。它们同样继承自 nn.Module,可以像 nn.Linear 一样嵌进自定义模型里。
注意事项
- 梯度必须清零:PyTorch 默认累加梯度,每轮训练前调用
optimizer.zero_grad(),否则参数更新会基于错误的梯度。 - 设备必须一致:模型与输入张量要在同一设备上。用
.to(device)统一搬运,不要一部分在 CPU、一部分在 GPU。 - 训练与评估模式要切换:训练时
model.train(),评估时model.eval(),并在评估时用torch.no_grad()关闭梯度。 - 形状要对齐:
view要求元素总数不变;模型输出与标签的形状要匹配,否则损失计算会报错或广播出错误结果。 - 显存有限:GPU 显存是硬约束,张量过大或批大小过大会触发显存不足。用
torch.cuda.memory_allocated()与torch.cuda.memory_reserved()观察占用情况。 - 版本信息会变:CUDA 版本、安装命令、各版本支持情况随发布更新,安装前请以 PyTorch 官网当前信息为准,不要直接照搬本文示例中的版本号。
- 损失值不是准确率:均方误差输出的是连续值,判断分类效果需要自己设定阈值或换用合适的损失函数,不能只看损失数字。