1. 项目概述:从“Hello World”到第一个模型
对于任何想踏入深度学习领域的朋友来说,PyTorch 几乎是一个绕不开的名字。它以其直观的动态计算图和 Python 式的编程风格,成为了学术界和工业界许多研究者和工程师的首选工具。但当你第一次打开 PyTorch 的官方文档,面对海量的 API 和复杂的概念时,难免会感到无从下手。这正是“实验1:PyTorch基本操作实验”存在的意义——它不是一个简单的“安装教程”,而是一个精心设计的、旨在帮你建立核心直觉的“认知脚手架”。
这个实验的目标非常明确:让你亲手“触摸”到深度学习的核心组件,并完成一个从数据到模型预测的完整闭环。它通常包含几个递进的模块:首先是张量(Tensor)这个 PyTorch 乃至整个深度学习世界的基石,你需要学会如何创建、操作和运算它;其次是自动微分(Autograd),这是神经网络能够“学习”的魔法引擎;接着是数据加载与处理,这是连接现实世界与模型算法的桥梁;最后,你会用前面学到的所有知识,亲手搭建并训练一个经典的Logistic 回归模型,解决一个简单的二分类问题。通过这个实验,你不仅能学会 PyTorch 的基本语法,更重要的是,你能理解一个模型从无到有、从随机猜测到有效预测的整个生命周期,为后续学习更复杂的卷积神经网络(CNN)、循环神经网络(RNN)打下坚实的基础。无论你是计算机专业的学生,还是希望转行 AI 的开发者,这个实验都是你深度学习之旅最坚实的第一步。
2. 核心概念与工具准备:搭建你的“炼丹炉”
在开始“炼丹”之前,我们必须准备好“丹炉”和“药材”。对于 PyTorch 实验,这指的是理解核心概念和搭建正确的开发环境。很多人一开始就卡在环境配置上,导致学习热情大减,因此我们先把这部分讲透。
2.1 环境搭建:避坑指南与版本选择
环境配置是实践的第一步,也是最容易劝退新手的一步。我的建议是:优先使用 Anaconda 来管理你的 Python 环境。它能完美解决不同项目间包版本冲突的问题。
安装步骤与要点:
安装 Anaconda:从官网下载并安装适合你操作系统的 Anaconda 版本。安装时务必勾选“Add Anaconda to my PATH environment variable”(添加到环境变量),这能避免后续在命令行中找不到
conda命令的麻烦。创建独立环境:打开终端(Windows 用 Anaconda Prompt,Mac/Linux 用 Terminal),执行以下命令创建一个名为
pytorch_lab的 Python 3.9 环境(3.9 是一个兼容性非常好的版本):conda create -n pytorch_lab python=3.9激活环境:创建成功后,激活该环境:
conda activate pytorch_lab你会看到命令行提示符前面从
(base)变成了(pytorch_lab),这表示你已经在独立的环境中操作了。安装 PyTorch:这是最关键的一步。绝对不要直接用
pip install torch!一定要去 PyTorch 官网 ,利用官网提供的安装命令生成器。你需要根据你的实际情况选择:- PyTorch Build:选择 Stable(稳定版)。
- Your OS:你的操作系统(Windows, Linux, Mac)。
- Package:推荐使用
Conda,因为它能更好地处理 CUDA 相关的依赖。 - Language:Python。
- Compute Platform:这是核心选择。
- 如果你有 NVIDIA 显卡并想使用 GPU 加速,请先确认你的显卡支持 CUDA(通常是较新的游戏卡或专业卡),然后去 NVIDIA 控制面板查看你的 CUDA 版本(比如 11.8, 12.1)。在 PyTorch 官网选择对应的 CUDA 版本(如
CUDA 11.8)。 - 如果你没有 NVIDIA 显卡,或者想先专注于学习语法,强烈建议选择
CPU。这样安装最简单,不会出现任何驱动问题,且对于本实验的数据量来说完全够用。
- 如果你有 NVIDIA 显卡并想使用 GPU 加速,请先确认你的显卡支持 CUDA(通常是较新的游戏卡或专业卡),然后去 NVIDIA 控制面板查看你的 CUDA 版本(比如 11.8, 12.1)。在 PyTorch 官网选择对应的 CUDA 版本(如
假设我们选择 Conda + CPU,官网会生成类似下面的命令:
conda install pytorch torchvision torchaudio cpuonly -c pytorch在你的
(pytorch_lab)环境中执行它即可。
注意:关于 GPU 版本,如果你不确定,就先装 CPU 版本。深度学习前期,算法思想和代码逻辑比运行速度更重要。等你熟悉了基本流程,再迁移到 GPU 环境会非常顺畅。
- 验证安装:安装完成后,在激活的环境中启动 Python 解释器,输入以下代码验证:
如果没有报错,并能打印出一个 3x3 的随机矩阵,恭喜你,环境配置成功!import torch print(torch.__version__) # 打印 PyTorch 版本,如 2.2.0 print(torch.cuda.is_available()) # 如果安装的是CPU版,这里会打印 False;GPU版且驱动正常则为 True x = torch.rand(3, 3) print(x)
2.2 理解张量(Tensor):一切皆是数字的容器
张量是 PyTorch 中最基本的数据结构,你可以把它理解为多维数组。它是承载数据、进行运算的载体。
- 0维张量:就是一个数字,称为标量(Scalar)。
torch.tensor(5.0) - 1维张量:是一个数组,称为向量(Vector)。
torch.tensor([1.0, 2.0, 3.0]) - 2维张量:是一个矩阵(Matrix)。
torch.randn(2, 3)# 生成一个2行3列的随机矩阵 - 3维及以上张量:可以想象成多个矩阵堆叠在一起。例如,一张 RGB 彩色图片可以表示为一个
[通道, 高度, 宽度]即[3, 224, 224]的 3 维张量;一个批次的图片则是[批量大小, 通道, 高度, 宽度]的 4 维张量。
创建张量的常用方式:
import torch # 1. 从Python列表/序列创建 a = torch.tensor([[1, 2, 3], [4, 5, 6]]) print(f"从列表创建: \n{a}, shape: {a.shape}, dtype: {a.dtype}") # 2. 使用内置函数创建特殊张量 zeros = torch.zeros(2, 3) # 全0矩阵 ones = torch.ones(2, 3) # 全1矩阵 eye = torch.eye(3) # 3x3单位矩阵 rand_tensor = torch.rand(2, 3) # 元素在[0,1)均匀分布 randn_tensor = torch.randn(2, 3) # 元素服从标准正态分布(均值0,方差1) # 3. 从NumPy数组创建(无缝衔接) import numpy as np np_array = np.array([[1, 2], [3, 4]]) tensor_from_np = torch.from_numpy(np_array)张量的核心属性:
shape:张量的维度,例如(2, 3)表示 2 行 3 列。dtype:张量的数据类型,如torch.float32,torch.int64。在深度学习中,我们最常用的是torch.float32,因为它是单精度浮点数,在精度和内存占用间取得了良好平衡。device:张量所在的设备,cpu或cuda:0(GPU)。可以通过.to(device)方法在设备间移动张量。
实操心得:刚开始时,多用print(tensor.shape)来查看你操作的张量维度,这是调试深度学习代码最常用、最有效的手段之一。很多错误都源于维度不匹配。
3. 实验核心操作一:张量运算与自动微分
掌握了张量的创建,我们就要让它“动”起来,进行运算。PyTorch 的运算接口设计得非常直观,很多操作与 NumPy 类似。
3.1 张量的基本运算
张量支持所有你熟悉的数学运算,并且通常是逐元素(element-wise)进行的。
x = torch.tensor([1., 2., 3.]) y = torch.tensor([4., 5., 6.]) # 算术运算 print(x + y) # 加法: tensor([5., 7., 9.]) print(x - y) # 减法 print(x * y) # 乘法(逐元素) print(x / y) # 除法(逐元素) print(x ** 2) # 幂运算 # 矩阵乘法(非常重要!) A = torch.randn(2, 3) # 2x3矩阵 B = torch.randn(3, 4) # 3x4矩阵 C = torch.mm(A, B) # 矩阵乘法,结果形状为 (2, 4) # 或者使用 @ 运算符(Python 3.5+) C = A @ B # 广播机制(Broadcasting) # 当两个张量形状不同时,PyTorch会尝试自动扩展维度以进行运算 a = torch.randn(3, 1) # shape: (3, 1) b = torch.randn(1, 4) # shape: (1, 4) c = a + b # a被广播为(3,4),b被广播为(3,4),结果shape: (3,4)3.2 自动微分(Autograd):让模型学会“思考”
这是 PyTorch 的灵魂特性。在机器学习中,我们通过计算损失函数关于模型参数的梯度,并沿着梯度下降的方向更新参数,从而使模型性能提升。PyTorch 的autograd包自动为我们计算这些梯度。
工作原理:
- 当你创建一个张量并设置
requires_grad=True时,PyTorch 会开始跟踪在该张量上的所有操作。 - 当你完成前向计算(从输入到输出)后,调用
.backward()方法。 - PyTorch 会自动计算所有
requires_grad=True的张量的梯度,并将它们累积到各自的.grad属性中。
一个简单的例子:计算 y = x^2 在 x=3 处的导数
# 1. 创建一个需要求导的张量 x = torch.tensor(3., requires_grad=True) # 2. 进行前向计算(构建计算图) y = x ** 2 # 3. 反向传播,计算梯度 y.backward() # 等价于 y.backward(torch.tensor(1.)) # 4. 查看梯度 dy/dx,理论上应为 2*x = 6 print(x.grad) # 输出: tensor(6.)更复杂的例子:复合函数
# 假设我们的函数是 z = (x + y) * (y + 2) x = torch.tensor(1., requires_grad=True) y = torch.tensor(2., requires_grad=True) z = (x + y) * (y + 2) print(f"z = {z}") # z = (1+2)*(2+2) = 12 # 计算z关于x和y的梯度 z.backward() print(f"∂z/∂x = {x.grad}") # 理论值:∂z/∂x = (y+2) = 4 print(f"∂z/∂y = {y.grad}") # 理论值:∂z/∂y = (x+y) + (y+2) = 3 + 4 = 7注意事项:
- 梯度累积:默认情况下,每次调用
.backward(),梯度都会累积(相加)到.grad属性中。在训练循环中,每次计算新批次数据的梯度前,必须将旧梯度清零,否则梯度会越来越大。这是新手常犯的错误。optimizer.zero_grad() # 优化器清零梯度(后续会讲) # 或者手动清零 # x.grad.zero_() # y.grad.zero_() detach()与no_grad():有时我们不需要跟踪某些计算的历史(例如,在评估模型或操作中间数据时),可以使用x.detach()创建一个不需要梯度的新张量,或者用torch.no_grad():上下文管理器包裹代码块,以节省内存和计算资源。with torch.no_grad(): # 在这个块内的所有计算都不会被跟踪 prediction = model(data) # 常用于模型评估或数据预处理
4. 实验核心操作二:数据加载与处理
模型和算法再好,没有数据也是“巧妇难为无米之炊”。PyTorch 提供了torch.utils.data模块来高效地加载和处理数据。
4.1 使用 Dataset 和 DataLoader
Dataset是一个抽象类,代表你的数据集。你需要继承它并实现两个核心方法:
__len__: 返回数据集的大小。__getitem__: 根据索引idx返回一个数据样本和对应的标签。
DataLoader则是一个迭代器,它负责从Dataset中按批次(batch)加载数据,并支持多进程加速、数据打乱(shuffle)等。
示例:创建一个简单的自定义 Dataset假设我们有一个用于二分类的简单数据集,特征X是二维点,标签y是 0 或 1。
import torch from torch.utils.data import Dataset, DataLoader import numpy as np class SimpleDataset(Dataset): """一个简单的二维点二分类数据集""" def __init__(self, num_samples=100): # 生成模拟数据:两类点,分别服从不同的正态分布 np.random.seed(42) # 类别0的数据 X0 = np.random.randn(num_samples//2, 2) + np.array([-2, -2]) # 类别1的数据 X1 = np.random.randn(num_samples//2, 2) + np.array([2, 2]) self.X = torch.from_numpy(np.vstack((X0, X1))).float() # 合并并转为Tensor self.y = torch.from_numpy(np.hstack((np.zeros(num_samples//2), np.ones(num_samples//2)))).long() # 标签 def __len__(self): return len(self.y) def __getitem__(self, idx): # 返回第idx个样本的特征和标签 return self.X[idx], self.y[idx] # 创建数据集和数据加载器 dataset = SimpleDataset(num_samples=200) dataloader = DataLoader(dataset, batch_size=16, shuffle=True, num_workers=0) # num_workers>0可加速,但在Windows下有时有问题 # 遍历数据加载器 for batch_idx, (features, labels) in enumerate(dataloader): print(f"Batch {batch_idx}:") print(f" Features shape: {features.shape}") # torch.Size([16, 2]) print(f" Labels shape: {labels.shape}") # torch.Size([16]) if batch_idx == 1: # 只看前两个批次 break4.2 数据预处理与转换
在实际项目中,数据很少是直接可用的。通常需要进行标准化、归一化、图像增强等操作。PyTorch 提供了torchvision.transforms模块(针对图像)和自定义转换函数。
通用数据预处理流程:
- 读取原始数据。
- 应用转换(Transform):将原始数据(如图像文件路径、文本字符串)转换为模型可接受的张量格式,并进行必要的处理。
- 打包成批次(Batching):由
DataLoader自动完成。
一个自定义转换的例子(对上述 SimpleDataset 的特征进行标准化):
from torchvision import transforms class NormalizeTransform: """自定义转换:对特征进行标准化(减均值,除以标准差)""" def __init__(self, mean, std): self.mean = mean self.std = std def __call__(self, sample): features, label = sample # 假设 features 是一个张量 features_normalized = (features - self.mean) / self.std return features_normalized, label # 计算整个数据集的均值和标准差(在实际中,常用训练集计算,应用到所有集) full_features = dataset.X data_mean = full_features.mean(dim=0) data_std = full_features.std(dim=0) # 我们可以修改 Dataset 的 __getitem__ 来包含转换,或者使用 transforms.Compose # 这里为了演示,我们创建一个新的 Dataset 类 class SimpleDatasetWithTransform(SimpleDataset): def __init__(self, num_samples=100, transform=None): super().__init__(num_samples) self.transform = transform def __getitem__(self, idx): sample = self.X[idx], self.y[idx] if self.transform: sample = self.transform(sample) return sample # 使用带转换的数据集 transform = NormalizeTransform(data_mean, data_std) dataset_transformed = SimpleDatasetWithTransform(transform=transform) dataloader_transformed = DataLoader(dataset_transformed, batch_size=16, shuffle=True) for features, labels in dataloader_transformed: print(f"Transformed features mean: ~{features.mean():.4f}, std: ~{features.std():.4f}") break实操心得:DataLoader的num_workers参数可以设置多进程加载数据,在 Linux/Mac 下能显著提升数据加载速度。但在 Windows 上使用spawn方式创建子进程时,如果主代码不是放在if __name__ == '__main__':块中,可能会报错。一个稳妥的做法是,在 Windows 上先将num_workers设为 0,确保代码逻辑正确后,再尝试调大。
5. 实验核心操作三:构建与训练 Logistic 回归模型
现在,我们将前面所有的知识串联起来,实现一个完整的 Logistic 回归模型。Logistic 回归虽然名字叫“回归”,但它是一个经典的线性二分类模型,是理解神经网络全连接层的绝佳起点。
5.1 模型定义:从公式到 PyTorch 模块
Logistic 回归的数学形式很简单:对于一个输入特征向量x,其预测为正类的概率p为:p = σ(Wx + b)其中,W是权重矩阵,b是偏置项,σ是 Sigmoid 函数,将线性输出映射到 (0, 1) 区间。
在 PyTorch 中,我们通过继承torch.nn.Module来定义模型。
import torch.nn as nn import torch.nn.functional as F class LogisticRegression(nn.Module): """Logistic 回归模型""" def __init__(self, input_dim, output_dim=1): """ 初始化模型层。 Args: input_dim: 输入特征的维度(例如,对于二维点,input_dim=2) output_dim: 输出维度。对于二分类,通常输出一个标量(经过sigmoid后表示概率) """ super(LogisticRegression, self).__init__() # 定义一个线性层:y = xA^T + b # 这里 nn.Linear 实现了 Wx + b self.linear = nn.Linear(in_features=input_dim, out_features=output_dim) def forward(self, x): """ 定义前向传播过程。 Args: x: 输入张量,形状为 [batch_size, input_dim] Returns: outputs: 未经激活的线性输出(logits),形状为 [batch_size, output_dim] """ # 我们返回线性层的输出。Sigmoid激活将在计算损失时由BCEWithLogitsLoss内置,这样数值更稳定。 outputs = self.linear(x) return outputs # 实例化模型 input_dim = 2 # 我们的SimpleDataset特征是二维的 model = LogisticRegression(input_dim=input_dim, output_dim=1) print(model)关键点解析:
nn.Linear层封装了权重W和偏置b,它们是模型需要学习的参数,可以通过model.parameters()访问。- 在
forward方法中,我们没有直接使用torch.sigmoid。这是因为 PyTorch 提供了一个更数值稳定的损失函数nn.BCEWithLogitsLoss(二元交叉熵损失 + Sigmoid),它建议我们将未经过 Sigmoid 的“logits”直接输入损失函数,由损失函数内部处理 Sigmoid 和稳定性计算。这是一种最佳实践。
5.2 训练循环:完整的“学习”过程
训练一个模型需要几个核心组件:模型、损失函数、优化器,以及数据。
import torch.optim as optim # 1. 准备数据(使用之前定义的 SimpleDataset 和 DataLoader) dataset = SimpleDataset(num_samples=200) dataloader = DataLoader(dataset, batch_size=32, shuffle=True) # 2. 初始化模型、损失函数和优化器 model = LogisticRegression(input_dim=2) criterion = nn.BCEWithLogitsLoss() # 二元交叉熵损失(内置Sigmoid) optimizer = optim.SGD(model.parameters(), lr=0.1) # 随机梯度下降优化器,学习率设为0.1 # 3. 训练参数 num_epochs = 50 # 整个数据集遍历50次 # 用于记录训练过程中的损失,方便可视化 train_losses = [] # 4. 训练循环 model.train() # 将模型设置为训练模式(某些层如Dropout、BatchNorm在训练和评估时行为不同) for epoch in range(num_epochs): epoch_loss = 0.0 for batch_idx, (features, labels) in enumerate(dataloader): # 确保标签的维度与模型输出匹配 (从 [batch_size] 变为 [batch_size, 1]) labels = labels.float().unsqueeze(1) # BCEWithLogitsLoss 需要 Float 类型和 [N, 1] 形状 # 前向传播 outputs = model(features) # outputs 是 logits loss = criterion(outputs, labels) # 反向传播与优化 optimizer.zero_grad() # **至关重要:清空上一轮的梯度** loss.backward() # 计算梯度 optimizer.step() # 根据梯度更新参数 # 记录损失 epoch_loss += loss.item() avg_epoch_loss = epoch_loss / len(dataloader) train_losses.append(avg_epoch_loss) if (epoch + 1) % 10 == 0: print(f'Epoch [{epoch+1}/{num_epochs}], Average Loss: {avg_epoch_loss:.4f}') print('训练完成!')代码逐行解读:
model.train():切换模型到训练模式。对于本实验的简单线性模型,这个调用没有实际影响,但对于包含 Dropout、BatchNorm 等层的复杂网络,这是必须的。labels.float().unsqueeze(1):.float():将标签从LongTensor转换为FloatTensor,因为BCEWithLogitsLoss要求如此。.unsqueeze(1):将形状从[batch_size]变为[batch_size, 1],以匹配模型输出outputs的形状[batch_size, 1]。
optimizer.zero_grad():这是新手最容易忘记的一步!PyTorch 的梯度是累积的,如果不清零,下一次loss.backward()时,梯度会与上一次的相加,导致更新方向错误。loss.backward():自动计算损失关于所有requires_grad=True的参数(即模型权重)的梯度。optimizer.step():根据优化器算法(这里是 SGD)和计算出的梯度,更新模型参数。
5.3 模型评估与预测
训练完成后,我们需要评估模型在未见过的数据上的性能,并进行预测。
# 1. 评估模式 model.eval() # 将模型设置为评估模式 # 2. 生成一个测试集(这里我们简单地从原始数据中划分,实际应使用独立数据) test_dataset = SimpleDataset(num_samples=50) test_dataloader = DataLoader(test_dataset, batch_size=50, shuffle=False) # 评估时通常不打乱 # 3. 禁用梯度计算以节省内存和计算 with torch.no_grad(): all_features, all_labels, all_predictions = [], [], [] for features, labels in test_dataloader: outputs = model(features) # 将 logits 通过 sigmoid 转换为概率 probabilities = torch.sigmoid(outputs) # 将概率转换为类别预测(以0.5为阈值) predictions = (probabilities > 0.5).float() all_features.append(features) all_labels.append(labels) all_predictions.append(predictions) # 拼接所有批次的结果 test_features = torch.cat(all_features, dim=0) test_labels = torch.cat(all_labels, dim=0).unsqueeze(1).float() # 同样调整形状和类型 test_predictions = torch.cat(all_predictions, dim=0) # 计算准确率 correct = (test_predictions == test_labels).sum().item() total = test_labels.size(0) accuracy = correct / total print(f'测试集准确率: {accuracy:.2%}') # 4. 进行单个样本预测 sample_point = torch.tensor([[1.5, 1.5]], dtype=torch.float) # 一个二维点 model.eval() with torch.no_grad(): logit = model(sample_point) probability = torch.sigmoid(logit).item() predicted_class = 1 if probability > 0.5 else 0 print(f'输入点 {sample_point.numpy()}') print(f'属于类别1的概率: {probability:.4f}') print(f'预测类别: {predicted_class}')关键点解析:
model.eval():切换模型到评估模式。对于复杂模型,这会关闭 Dropout 层、固定 BatchNorm 层的统计量。with torch.no_grad()::在这个上下文管理器内,所有计算都不会构建计算图,不保存中间变量,可以大幅提升推理速度并节省内存。- 准确率计算:我们以 0.5 为阈值,将 Sigmoid 输出的概率转换为二分类的类别(0 或 1)。
6. 实验扩展与深度思考
完成了基本的训练和评估,你已经跑通了 PyTorch 深度学习的最小工作流。但要让这个实验的价值最大化,我们还需要进行一些扩展和深度思考。
6.1 可视化:让一切变得直观
可视化是理解和调试模型的关键。我们可以绘制损失曲线、决策边界和数据点。
import matplotlib.pyplot as plt import numpy as np # 1. 绘制训练损失曲线 plt.figure(figsize=(10, 4)) plt.subplot(1, 2, 1) plt.plot(range(1, num_epochs+1), train_losses, 'b-', label='Training Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.title('Training Loss Curve') plt.grid(True) plt.legend() # 2. 绘制决策边界和数据点 plt.subplot(1, 2, 2) # 生成网格点用于绘制决策边界 x_min, x_max = dataset.X[:, 0].min() - 1, dataset.X[:, 0].max() + 1 y_min, y_max = dataset.X[:, 1].min() - 1, dataset.X[:, 1].max() + 1 xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1), np.arange(y_min, y_max, 0.1)) # 将网格点转换为Tensor并进行预测 model.eval() with torch.no_grad(): grid_tensor = torch.from_numpy(np.c_[xx.ravel(), yy.ravel()]).float() Z = model(grid_tensor) Z = torch.sigmoid(Z).numpy().reshape(xx.shape) # 绘制等高线(决策边界,概率为0.5的地方) plt.contourf(xx, yy, Z, levels=25, cmap=plt.cm.RdBu, alpha=0.8) plt.contour(xx, yy, Z, levels=[0.5], colors='black', linewidths=2) # 绘制原始数据点 scatter = plt.scatter(dataset.X[:, 0], dataset.X[:, 1], c=dataset.y, cmap=plt.cm.RdBu, edgecolors='k') plt.xlabel('Feature 1') plt.ylabel('Feature 2') plt.title('Decision Boundary and Data Points') plt.colorbar(scatter) plt.tight_layout() plt.show()通过损失曲线,你可以观察模型是否在有效学习(损失是否平稳下降)。通过决策边界图,你可以直观地看到这个线性模型是如何用一条直线(在二维空间)将两类点分开的。
6.2 超参数调优初探
我们之前固定了学习率lr=0.1,批次大小batch_size=32。这些可以调整的参数称为“超参数”。不同的超参数会对训练产生巨大影响。
- 学习率 (Learning Rate, lr):控制参数更新的步长。
- 太大:损失可能震荡甚至发散(NaN)。
- 太小:收敛速度极慢,可能卡在局部最优点。
- 常用策略:可以尝试
0.01, 0.05, 0.1, 0.3,观察损失曲线。也可以使用学习率调度器(如torch.optim.lr_scheduler.StepLR)在训练中动态调整。
- 批次大小 (Batch Size):一次迭代用于更新梯度的样本数。
- 太小(如1,即随机梯度下降SGD):更新噪声大,收敛曲线震荡,但可能有助于跳出局部最优。
- 太大:内存占用高,每次更新更稳定,但可能降低泛化能力。
- 常用值:16, 32, 64, 128。通常设为2的幂次,以利用硬件优化。
- 优化器 (Optimizer):除了 SGD,还有更流行的 Adam 优化器,它自适应地调整每个参数的学习率,通常收敛更快。
optimizer = optim.Adam(model.parameters(), lr=0.01) # 通常Adam使用更小的学习率
一个简单的超参数实验框架:
learning_rates = [0.001, 0.01, 0.1] batch_sizes = [16, 32, 64] results = {} for lr in learning_rates: for bs in batch_sizes: print(f"\n=== 实验: LR={lr}, BS={bs} ===") # 重新初始化模型、数据加载器、优化器 model = LogisticRegression(input_dim=2) dataloader = DataLoader(dataset, batch_size=bs, shuffle=True) optimizer = optim.SGD(model.parameters(), lr=lr) criterion = nn.BCEWithLogitsLoss() # 简化的训练循环(比如只跑10个epoch看初始趋势) losses = [] for epoch in range(10): epoch_loss = 0.0 for features, labels in dataloader: labels = labels.float().unsqueeze(1) optimizer.zero_grad() outputs = model(features) loss = criterion(outputs, labels) loss.backward() optimizer.step() epoch_loss += loss.item() avg_loss = epoch_loss / len(dataloader) losses.append(avg_loss) results[(lr, bs)] = losses[-1] # 记录最终损失 print(f"最终损失: {losses[-1]:.4f}")6.3 常见问题与排查技巧实录
在实际操作中,你一定会遇到各种报错和意外情况。这里记录一些典型问题及其解决方法。
问题1:RuntimeError: mat1 and mat2 shapes cannot be multiplied
- 错误信息:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (axb and cxd) - 原因:这是最经典的维度不匹配错误。发生在
nn.Linear层或torch.mm进行矩阵乘法时。mat1的列数必须等于mat2的行数。 - 排查:
- 立即打印出错时相关张量的
shape。 - 检查你的模型
__init__中定义的input_dim是否与实际输入数据的特征维度一致。 - 检查数据加载环节,
DataLoader返回的features的shape是否为[batch_size, feature_dim]。
- 立即打印出错时相关张量的
- 示例:模型定义为
nn.Linear(10, 1),期望输入[batch_size, 10],但实际输入了[batch_size, 5],就会报错。
问题2:训练损失不下降,准确率始终在50%左右(随机猜测水平)
- 可能原因:
- 学习率过大或过小:尝试调整学习率,观察损失曲线最初几个 epoch 的变化。
- 梯度消失/爆炸:对于深层网络常见,但 Logistic 回归很少见。可以打印梯度的范数检查:
print([p.grad.norm() for p in model.parameters()])。如果梯度接近0或非常大(如1e10),就是问题。 - 数据没有打乱(Shuffle):如果数据是按类别顺序排列的,且没有打乱,每个批次可能只包含一个类别,导致模型无法学习到有效的决策边界。务必确保训练集的
DataLoader设置了shuffle=True。 - 模型容量不足或数据不可分:对于复杂数据,线性模型(Logistic回归)本身就无法很好地区分。可以尝试更复杂的模型(如简单的神经网络)。
- Bug:最常见的是忘记
optimizer.zero_grad(),导致梯度累积,更新方向混乱。
问题3:GPU内存溢出 (CUDA out of memory)
- 场景:当你将模型和数据转移到 GPU(
.cuda()或.to(‘cuda’))后运行时报错。 - 解决方法:
- 减小批次大小 (Batch Size):这是最直接有效的方法。
- 使用梯度累积 (Gradient Accumulation):如果因为内存限制无法使用大的 batch size,可以模拟大 batch 的效果。每 N 个小 batch 计算一次梯度,但只在前 N-1 次调用
loss.backward()后不执行optimizer.step(),在第 N 次再更新参数并清零梯度。 - 检查是否有不必要的大张量保留在内存中:确保在验证/测试时使用
with torch.no_grad():。
问题4:验证/测试时准确率远低于训练准确率
- 可能原因:过拟合 (Overfitting)。模型在训练集上表现太好,记住了噪声,而无法泛化到新数据。
- 应对策略:
- 获取更多训练数据。
- 使用正则化:如为优化器添加权重衰减(L2正则化):
optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)。 - 简化模型(降低模型容量)。
- 早停 (Early Stopping):监控验证集损失,当连续多个 epoch 验证损失不再下降时,停止训练。
调试技巧:
- 打印、打印、再打印:在关键步骤(如模型
forward开始、损失计算后)打印张量的shape、dtype、device以及关键值(如loss.item())。 - 使用小数据集调试:先用一个极小的数据集(比如10个样本)跑通整个流程,确保代码逻辑正确,再扩展到全量数据。
- 可视化中间结果:像我们之前绘制决策边界一样,可视化模型的学习过程,能提供代码无法直接给出的直觉。
7. 从实验到项目:下一步的方向
完成这个基本实验后,你已经掌握了 PyTorch 最核心的流程。但这仅仅是开始。要真正掌握深度学习,你需要沿着以下几个方向深入:
- 深入理论:理解 Logistic 回归背后的最大似然估计、交叉熵损失函数的推导。这能让你在遇到新问题时,知道如何设计或调整损失函数。
- 挑战更复杂的数据集:尝试在经典数据集上应用 Logistic 回归,如 MNIST(手写数字识别,需先将其视为10个二分类问题或使用扩展的多分类版本——Softmax 回归)、Iris(鸢尾花数据集)。
- 迈向神经网络:Logistic 回归可以看作一个没有隐藏层的神经网络。下一步自然是为模型添加隐藏层,构建一个多层感知机(MLP),并使用 ReLU 等激活函数引入非线性。
- 掌握现代网络架构:学习使用
torchvision.models中预定义的经典模型(如 ResNet, VGG),并在自己的数据上进行微调(Fine-tuning),这是解决实际计算机视觉问题的强大工具。 - 探索其他模态:除了结构化数据,尝试处理图像(使用 CNN)、序列数据(使用 RNN, LSTM, Transformer)、图数据(使用 GNN)。
- 工程化实践:学习使用 TensorBoard 或 Weights & Biases 进行实验跟踪和可视化;学习如何将训练好的模型保存(
torch.save)和加载(torch.load);学习如何编写更模块化、可复用的代码。
这个“实验1”就像你学习编程时写的第一个“Hello World”程序。它简单,但包含了所有核心要素。反复练习,理解每一行代码背后的含义,并尝试修改它、打破它、修复它,你就能建立起对 PyTorch 和深度学习最坚实、最直观的理解。记住,在深度学习的实践中,亲手调试一个错误的收获,往往比顺利运行十段代码更大。