ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PyTorch入门实战:从张量操作到Logistic回归模型完整实现

PyTorch入门实战:从张量操作到Logistic回归模型完整实现 1. 项目概述从“Hello, PyTorch!”到第一个可运行的模型如果你刚接触深度学习面对PyTorch官网和一堆教程感觉无从下手那这个实验就是为你量身定做的。它不是一个简单的“安装-打印Hello World”流程而是一个精心设计的、旨在让你亲手触摸到深度学习核心流程的入门实验。核心目标很明确通过完成一个完整的、从数据生成、模型定义、训练到评估的Logistic回归任务来掌握PyTorch最基础、最核心的操作。为什么是Logistic回归因为它结构简单但包含了深度学习模型的所有关键要素张量操作、自动求导、优化器、损失函数。完成它你就打通了使用PyTorch解决任何监督学习问题的“任督二脉”。这个实验通常会要求你手动实现前向传播、反向传播并与PyTorch内置的优化器进行对比从而深刻理解框架背后的自动化机制。对于新手来说这比直接跑通一个复杂的CNN或Transformer更有价值因为你清楚地知道每一行代码在做什么而不是当一个“调包侠”。我见过很多同学卡在第一步环境配置。这里有个心得不要盲目追求最新版本的PyTorchCUDA组合。对于实验1你的首要目标是稳定运行。如果你的显卡是NVIDIA的可以尝试安装CUDA版本的PyTorch来体验GPU加速如果没有显卡或者驱动复杂直接安装CPU版本完全不影响你理解核心概念。在Anaconda中创建一个干净的虚拟环境比如叫pytorch_lab1然后用官网提供的conda命令安装是最稳妥的方式。2. 实验核心目标与前置知识拆解2.1 实验要我们具体做什么这个实验的典型任务书会包含以下几个递进式的模块我们逐一拆解其背后的意图PyTorch张量Tensor基本操作创建、索引、切片、重塑reshape、运算加减乘除、矩阵乘法。这不是为了炫技而是因为张量是PyTorch中数据的唯一载体。图像、文本、语音最终都会被转换成张量。熟练操作张量就像厨师熟悉刀工一样是后续一切的基础。自动求导Autograd机制验证手动实现一个简单函数如 $y 3x^2 2x 1$的求导并与PyTorch的autograd计算结果对比。这一步的目的是让你信任并理解框架的自动微分能力。知道loss.backward()一句代码背后PyTorch帮你计算了所有参数梯度这是你敢于构建复杂模型的底气。Logistic回归模型的手动实现数据准备生成或加载一个简单的二分类数据集如make_moons或make_blobs。模型定义用nn.Linear定义线性层并手动编写sigmoid激活函数。训练循环手动实现梯度下降。包括前向传播计算预测值和损失二元交叉熵损失、手动将梯度清零、反向传播计算梯度、手动更新参数w w - lr * w.grad。这个过程会让你对“训练”有一个肌肉记忆般的理解。使用PyTorch内置模块重构用torch.nn.Sequential组合线性层和nn.Sigmoid用torch.optim.SGD替代手动更新用nn.BCELoss计算损失。对比两次实现你会发现代码变得极其简洁和标准化。这时你就明白了PyTorch的模块化设计如何提升开发效率。结果可视化与评估绘制决策边界计算准确率。将训练过程的损失变化用图表画出来直观感受模型是否在学习损失是否在下降。2.2 你需要提前准备好的“弹药”在开始编码前确保你脑子里有以下几点概念实验时会顺畅很多Python基础列表、循环、函数定义、NumPy的基本操作。PyTorch和NumPy在API设计上有很多相似之处熟悉NumPy会让你学PyTorch事半功倍。线性代数与微积分基础知道矩阵乘法、向量内积、sigmoid函数、导数的链式法则。不需要非常精通但至少要明白Logistic回归的公式 $y \sigma(Wx b)$ 在计算什么以及损失函数对参数求导的意义。Jupyter Notebook或Python脚本环境强烈推荐使用Jupyter Notebook来做这个实验。它可以分段执行代码、即时显示图表和中间变量值非常适合学习和调试。VSCode或PyCharm也都有很好的Jupyter支持。注意很多教程会一上来就教torch.nn和torch.optim但在这个实验里一定要先耐着性子完成手动实现的部分。这个“笨办法”是你未来解决复杂模型调试问题的基石。3. 环境搭建与张量操作实战3.1 创建一个专属的虚拟环境环境混乱是新手最大的噩梦之一。第一步我们建立一个隔离的环境。# 打开你的终端Anaconda Prompt或系统终端 conda create -n pytorch_lab1 python3.9 # 推荐Python 3.8或3.9兼容性最好 conda activate pytorch_lab1接下来安装PyTorch。不要去记复杂的命令直接访问 PyTorch官网 在“Get Started”页面根据自己的操作系统、包管理工具Conda/Pip、CUDA版本有无GPU选择网站会自动生成安装命令。例如对于没有GPU的Windows用户可能得到pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu安装完成后在Jupyter Notebook或Python交互界面中运行以下代码验证import torch print(fPyTorch版本: {torch.__version__}) print(fCUDA是否可用: {torch.cuda.is_available()}) # 如果为True恭喜你GPU环境配置成功 x torch.rand(3, 3) print(f随机张量:\n{x})如果顺利打印出版本和一个3x3的随机矩阵说明环境OK。3.2 张量Tensor深度学习世界的“乐高积木”张量可以看作是多维数组。0维张量是标量1维是向量2维是矩阵3维及以上就是高阶张量例如一张RGB图片是[通道, 高, 宽]的3维张量。核心操作实录import torch # 1. 创建张量 a torch.tensor([1, 2, 3, 4]) # 从列表创建 b torch.zeros(2, 3) # 2行3列的全0张量 c torch.ones_like(b) # 形状和b一样的全1张量 d torch.randn(2, 3) # 标准正态分布随机数 e torch.arange(0, 10, 2) # 类似range [0, 2, 4, 6, 8] # 2. 张量属性 print(f张量d的形状: {d.shape}) # 输出: torch.Size([2, 3]) print(f张量d的数据类型: {d.dtype}) # 默认是torch.float32 print(f张量d的设备: {d.device}) # 在CPU还是GPU上 # 3. 索引与切片 (和NumPy/Python列表几乎一样) t torch.tensor([[1,2,3], [4,5,6], [7,8,9]]) print(t[0]) # 第一行: tensor([1, 2, 3]) print(t[:, 1]) # 第二列: tensor([2, 5, 8]) print(t[1:, :2]) # 第二行及以后前两列: tensor([[4, 5], [7, 8]]) # 4. 重塑Reshape与 视图View # Reshape和View都能改变形状但View要求张量在内存中连续否则会报错。新手用reshape更安全。 original torch.arange(6) # tensor([0, 1, 2, 3, 4, 5]) reshaped original.reshape(2, 3) # 变成2x3矩阵 # 注意reshape返回的可能是新张量也可能是原张量的一个视图共享内存。 # 5. 运算 x torch.tensor([1., 2., 3.]) y torch.tensor([4., 5., 6.]) print(x y) # 逐元素相加: tensor([5., 7., 9.]) print(x * y) # 逐元素相乘: tensor([4., 10., 18.]) print(x y) # 向量内积: tensor(32.) print(torch.matmul(x, y)) # 同上 A torch.randn(2, 3) B torch.randn(3, 2) print(torch.mm(A, B)) # 矩阵乘法输出形状为(2, 2)实操心得reshape和view的区别是初期的一个小坑。如果你对一个张量进行了transpose或permute等操作它在内存中可能就不连续了此时调用view()会报错。保险起见在不确定时使用tensor.reshape(...)或者先调用tensor.contiguous()再view。4. 自动求导Autograd深度解析这是PyTorch的“灵魂”。我们通过一个例子把它扒开看明白。假设我们有函数 $z 3x^2 2y 1$我们想求在点(x2, y3)处z对x和y的偏导数。手动计算$\frac{\partial z}{\partial x} 6x 12$ $\frac{\partial z}{\partial y} 2$。用PyTorch实现import torch # 1. 创建需要求导的张量必须设置 requires_gradTrue x torch.tensor(2.0, requires_gradTrue) y torch.tensor(3.0, requires_gradTrue) # 2. 构建计算图 z 3 * x ** 2 2 * y 1 # 3. 反向传播自动计算所有 requires_gradTrue 的张量的梯度 z.backward() # 4. 查看梯度 print(fdz/dx (在x2处): {x.grad}) # 输出: tensor(12.) print(fdz/dy (在y3处): {y.grad}) # 输出: tensor(2.)发生了什么当我们设置requires_gradTrue时PyTorch开始跟踪在该张量上的所有操作。z 3*x**2 2*y 1这个计算过程被动态地构建成了一个计算图。x和y是叶子节点z是根节点。调用z.backward()时PyTorch从z开始依据链式法则反向遍历计算图计算每个叶子节点的梯度并将结果累积到对应张量的.grad属性中。关键注意事项梯度累积默认情况下backward()计算的梯度会累加到.grad属性中。所以在每次参数更新前必须手动将梯度清零否则梯度会越加越大。这是新手常犯的错误。非标量输出如果z不是一个标量比如是一个向量在调用backward()时需要传入一个与z形状相同的gradient参数作为“权重”。在简单的损失函数中损失loss总是标量所以直接loss.backward()即可。中断跟踪在推理测试阶段我们不需要计算梯度。可以用with torch.no_grad():上下文管理器包裹代码块或者对张量调用.detach()方法这样可以大幅减少内存消耗并加速计算。5. 手动实现Logistic回归理解每一行代码现在我们进入核心环节。我们将用最“原始”的方式实现一个Logistic回归模型来分类一个二维数据集。5.1 生成与准备数据我们使用sklearn的make_moons生成一个非线性可分的二分类数据集这样比线性数据更有挑战性也更能体现模型的能力。import numpy as np import torch from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt # 生成数据 X_np, y_np make_moons(n_samples1000, noise0.1, random_state42) # 可视化数据 plt.figure(figsize(8, 6)) plt.scatter(X_np[y_np0, 0], X_np[y_np0, 1], cblue, labelClass 0, alpha0.6) plt.scatter(X_np[y_np1, 0], X_np[y_np1, 1], cred, labelClass 1, alpha0.6) plt.xlabel(Feature 1) plt.ylabel(Feature 2) plt.legend() plt.title(Moons Dataset) plt.show() # 数据转换与分割 # 将NumPy数组转换为PyTorch张量并调整数据类型 X torch.from_numpy(X_np).float() # 特征需要是float y torch.from_numpy(y_np).float().view(-1, 1) # 标签也需要是float并reshape为列向量 (n_samples, 1) # 分割训练集和测试集 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) print(f训练集大小: {X_train.shape}) print(f测试集大小: {X_test.shape})5.2 定义模型、损失函数和手动训练循环Logistic回归模型是$\hat{y} \sigma(Wx b)$其中$\sigma$是sigmoid函数将输出映射到(0,1)表示正类的概率。# 1. 初始化参数 # 输入特征维度是2输出是1个标量概率 torch.manual_seed(42) # 固定随机种子确保结果可复现 W torch.randn(2, 1, requires_gradTrue) # 权重矩阵形状(2,1) b torch.zeros(1, requires_gradTrue) # 偏置项形状(1,) # 2. 定义sigmoid函数和二元交叉熵损失函数 def sigmoid(z): 手动实现sigmoid激活函数 return 1 / (1 torch.exp(-z)) def binary_cross_entropy(y_hat, y): 手动实现二元交叉熵损失。 y_hat: 预测概率 (0~1) y: 真实标签 (0或1) # 添加一个极小值epsilon防止log(0)出现数学错误 eps 1e-8 loss - (y * torch.log(y_hat eps) (1 - y) * torch.log(1 - y_hat eps)) return loss.mean() # 对批次内所有样本的损失求平均 # 3. 设置超参数 learning_rate 0.1 num_epochs 500 train_losses [] # 记录每轮训练损失 # 4. 手动训练循环 for epoch in range(num_epochs): # ---- 前向传播 ---- # 线性部分: z X_train W b # X_train形状 (n, 2), W形状 (2,1), 结果z形状 (n, 1) z torch.matmul(X_train, W) b y_hat sigmoid(z) # 预测概率形状 (n, 1) # 计算损失 loss binary_cross_entropy(y_hat, y_train) # ---- 反向传播 ---- # 在PyTorch中我们不需要手动推导梯度公式 # 只需要告诉它从loss开始反向传播 loss.backward() # 自动计算W.grad和b.grad # ---- 手动梯度下降更新参数 ---- # 重要必须在更新参数前用.no_grad()上下文管理器暂停梯度跟踪 # 否则对参数的操作如减法也会被记录到计算图中导致错误。 with torch.no_grad(): W - learning_rate * W.grad b - learning_rate * b.grad # ---- 梯度清零至关重要---- # 如果不清零下一次backward()计算的梯度会与当前.grad累加。 W.grad.zero_() b.grad.zero_() # 记录损失 train_losses.append(loss.item()) # 每100轮打印一次 if (epoch 1) % 100 0: print(fEpoch [{epoch1}/{num_epochs}], Loss: {loss.item():.4f}) # 绘制训练损失曲线 plt.plot(train_losses) plt.xlabel(Epoch) plt.ylabel(Training Loss) plt.title(Manual Training Loss Curve) plt.grid(True) plt.show()代码逐行解析与避坑指南参数初始化W和b必须设置requires_gradTrue这样PyTorch才会在计算图中跟踪它们以便计算梯度。损失函数实现注意我们在torch.log()里加了eps一个极小的数如1e-8。这是因为当预测概率y_hat非常接近0或1时log(0)会导致结果为负无穷-inf使计算崩溃。这是一个非常实用的工程细节。with torch.no_grad():这个上下文管理器是关键中的关键。在更新参数W W - lr * W.grad时这个操作本身不应该被记录到计算图中否则下次backward()时会试图对这个更新操作求导这既没必要也会导致错误。no_grad()块内的所有计算都不会被跟踪。grad.zero_()梯度清零。zero_()是原地操作in-place。如果忘记这一步梯度会不断累积导致优化方向错误模型无法收敛。这是手动训练循环里最高频的错误之一。loss.item()loss是一个包含单个元素的张量。用.item()方法可以将其转换为Python标量数字方便记录和打印。5.3 模型评估与决策边界可视化训练完成后我们需要看看模型学得怎么样。# 1. 在测试集上进行预测 with torch.no_grad(): # 测试阶段不需要梯度 z_test torch.matmul(X_test, W) b y_hat_test sigmoid(z_test) # 将概率转换为类别 (0.5为1否则为0) y_pred (y_hat_test 0.5).float() # 2. 计算准确率 accuracy (y_pred y_test).float().mean() print(f手动实现模型在测试集上的准确率: {accuracy.item()*100:.2f}%) # 3. 绘制决策边界 (这是一个非常实用的技巧) def plot_decision_boundary(model, X, y): 绘制模型在二维特征空间中的决策边界 # 设置网格范围 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 h 0.01 # 网格步长 xx, yy np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) # 将网格点转换为PyTorch张量并进行预测 grid_tensor torch.from_numpy(np.c_[xx.ravel(), yy.ravel()]).float() with torch.no_grad(): z_grid torch.matmul(grid_tensor, W) b probs sigmoid(z_grid) Z (probs 0.5).numpy().reshape(xx.shape) # 绘制等高线和散点图 plt.figure(figsize(10, 8)) plt.contourf(xx, yy, Z, alpha0.3, cmapplt.cm.RdBu) scatter plt.scatter(X[:, 0], X[:, 1], cy.view(-1), edgecolorsk, cmapplt.cm.RdBu) plt.xlabel(Feature 1) plt.ylabel(Feature 2) plt.title(Decision Boundary of Manual Logistic Regression) plt.legend(*scatter.legend_elements(), titleClasses) plt.show() # 使用全部数据绘制决策边界 plot_decision_boundary(None, X, y) # 这里传入None因为我们直接使用了全局变量W和b看到那条弯曲的决策边界了吗这就是你的Logistic回归模型学习到的“分界线”。虽然Logistic回归本质是线性分类器决策边界是直线但因为我们使用了原始特征对于moons这样的数据它只能拟合出一条直线效果不会太好。这引出了特征工程和非线性模型如神经网络的重要性。6. 使用PyTorch内置模块重构体验框架的优雅手动实现让我们理解了底层原理。现在我们用PyTorch的nn.Module和optim来重写代码将变得异常简洁和标准。import torch.nn as nn import torch.optim as optim # 1. 定义模型类 (继承自nn.Module) class LogisticRegressionPytorch(nn.Module): def __init__(self, input_dim): super(LogisticRegressionPytorch, self).__init__() # 使用nn.Linear定义线性层它内部已经包含了权重W和偏置b self.linear nn.Linear(input_dim, 1) # 我们不再需要手动定义sigmoid使用nn.Sigmoid()作为激活函数层 def forward(self, x): # 前向传播线性变换 - sigmoid激活 # 等价于手动实现的 z xW b; y_hat sigmoid(z) out self.linear(x) out torch.sigmoid(out) # 使用torch.sigmoid函数 return out # 2. 初始化模型、损失函数和优化器 input_dim 2 model LogisticRegressionPytorch(input_dim) criterion nn.BCELoss() # 二元交叉熵损失内置了数值稳定的实现 optimizer optim.SGD(model.parameters(), lr0.1) # 随机梯度下降优化器 # 3. 训练循环 (对比一下简洁了多少) num_epochs 500 train_losses_pytorch [] for epoch in range(num_epochs): # 前向传播 outputs model(X_train) # 直接调用model.forward(X_train) loss criterion(outputs, y_train) # 反向传播与优化 optimizer.zero_grad() # 梯度清零替代了手动的 W.grad.zero_() 和 b.grad.zero_() loss.backward() # 自动求导 optimizer.step() # 参数更新替代了手动的 W - lr * W.grad train_losses_pytorch.append(loss.item()) if (epoch 1) % 100 0: print(fEpoch [{epoch1}/{num_epochs}], Loss: {loss.item():.4f}) # 4. 评估 model.eval() # 将模型设置为评估模式如果模型有Dropout、BatchNorm层此模式会关闭它们 with torch.no_grad(): test_outputs model(X_test) test_preds (test_outputs 0.5).float() accuracy_pytorch (test_preds y_test).float().mean() print(fPyTorch模块实现模型在测试集上的准确率: {accuracy_pytorch.item()*100:.2f}%) # 5. 对比两种实现的训练损失曲线 plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) plt.plot(train_losses) plt.title(Manual Implementation Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.grid(True) plt.subplot(1, 2, 2) plt.plot(train_losses_pytorch) plt.title(PyTorch Module Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.grid(True) plt.tight_layout() plt.show()核心变化与优势nn.Module所有自定义模型的基类。它帮你管理了所有参数通过model.parameters()可获取并提供了钩子函数让模型结构更清晰。nn.Linear封装了线性变换 $y xA^T b$。你不需要再操心权重W和偏置b的初始化它使用了一种合理的初始化方式如Kaiming初始化。nn.BCELoss()内置的二元交叉熵损失。它内部已经处理了数值稳定性问题比如防止log(0)比我们手动实现的更健壮。optim.SGD优化器。你只需要传入模型参数和学习率它会在optimizer.step()中完成所有参数的更新。优化器还支持动量Momentum、权重衰减L2正则化等高级特性。optimizer.zero_grad()一键清零所有模型参数的梯度比手动清零方便且不易出错。model.eval()这是一个好习惯。虽然我们这个简单模型没有Dropout或BatchNorm但养成在测试时调用eval()训练时调用model.train()的习惯对于使用复杂网络至关重要。7. 实验常见问题与调试技巧实录做完实验你可能会遇到以下问题。这里是我和学生们踩过的坑以及解决方法。7.1 损失不下降Nan或保持不变现象训练一开始损失就是NaN或者在一个很高的值上波动不下降。排查思路学习率过大这是最常见的原因。过大的学习率会导致参数更新步伐太大在损失函数表面“跳跃”无法收敛甚至发散损失变成NaN。解决方案尝试调小学习率例如从0.1调到0.01、0.001。可以先用一个很小的学习率如1e-4测试看损失是否缓慢下降再逐步调大。数据未归一化/标准化如果特征量纲差异巨大如一个特征范围是[0,1]另一个是[1000,10000]梯度更新会不稳定。解决方案对输入特征进行标准化减均值除以标准差或归一化缩放到[0,1]。损失函数实现有误检查手动实现的损失函数特别是对数运算确保没有出现log(0)。解决方案使用PyTorch内置的nn.BCELoss()它经过了数值稳定优化。梯度爆炸在深层网络中更常见但Logistic回归中如果初始化权重过大也可能发生。解决方案使用更小的随机初始化如torch.randn(2,1) * 0.01或使用PyTorch默认的初始化。7.2 准确率始终在50%左右随机猜测现象无论怎么训练模型在二分类任务上的准确率都接近50%和抛硬币没区别。排查思路标签张量形状错误这是超级高频错误y的形状应该是(n_samples, 1)而不是(n_samples,)。如果形状是后者在计算损失criterion(outputs, y)时由于广播机制可能不会报错但计算是完全错误的。解决方案使用y y.view(-1, 1)或y y.reshape(-1, 1)确保标签是列向量。忘记调用zero_grad()梯度累积导致优化方向错误。解决方案在每次loss.backward()之前确认执行了optimizer.zero_grad()。数据本身不可分检查一下你生成或加载的数据两类样本是不是完全混在一起了解决方案可视化你的数据 (plt.scatter)。7.3 运行速度慢现象在CPU上训练几百轮感觉也很慢。排查思路没有利用向量化确保你的操作是面向整个批次的张量运算而不是在Python循环中对单个样本进行计算。我们上面的代码都是向量化的。在循环中频繁将张量转换为NumPy在训练循环内避免tensor.numpy()和torch.from_numpy()的转换这会在CPU和GPU如果有间产生开销。可以考虑GPU加速如果你有NVIDIA GPU且安装了CUDA版本的PyTorch可以将模型和数据移动到GPU上。device torch.device(cuda if torch.cuda.is_available() else cpu) model LogisticRegressionPytorch(input_dim).to(device) X_train, y_train X_train.to(device), y_train.to(device) # 在训练循环中数据已经在device上了7.4 决策边界绘制不出来或不对现象plot_decision_boundary函数报错或画出的图很奇怪。排查思路网格数据转换错误确保np.c_[]操作正确地将xx.ravel()和yy.ravel()组合成了[x1, y1], [x2, y2], ...的格式。模型处于训练模式如果模型有Dropout等层在预测时需要设置为eval()模式。我们的Logistic回归没有但养成好习惯model.eval()。忘记with torch.no_grad()在预测大量网格点时计算梯度会消耗巨量内存且毫无必要。必须用no_grad()包裹预测代码。7.5 实验报告与思考题延伸完成代码后一份好的实验报告还应该包括对结果的思考和扩展。你可以尝试回答以下问题这能让你理解更深比较手动实现和PyTorch模块实现的训练曲线它们的收敛速度和最终损失值有差异吗为什么提示思考权重初始化和损失函数实现的细微差别。学习率的影响将学习率分别设为0.01, 0.1, 1.0观察训练过程。绘制不同学习率下的损失曲线你能得出什么结论尝试不同的优化器将optim.SGD换成optim.Adam保持其他参数不变观察训练速度和收敛效果有何不同特征工程尝试Logistic回归是线性分类器。对于moons数据集你可以尝试添加新的特征例如 $x_1^2$, $x_2^2$, $x_1x_2$再用这个扩展后的特征集进行训练。观察决策边界是否从直线变成了曲线准确率是否提升从二分类到多分类Logistic回归如何用于多分类问题提示搜索“Softmax回归”或“多类Logistic回归”。这个实验虽然基础但它构建了你使用PyTorch进行深度学习的完整心智模型。理解了数据如何流动、梯度如何计算、参数如何更新后面学习卷积神经网络、循环神经网络时你关注的将是网络结构本身的创新而不会再被这些基础流程困扰。
返回列表