当前位置: 首页 > news >正文

【深度学习-Day 20】PyTorch入门:核心数据结构张量(Tensor)详解与操作

Langchain系列文章目录

01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来

Python系列文章目录

PyTorch系列文章目录

机器学习系列文章目录

深度学习系列文章目录

Java系列文章目录

JavaScript系列文章目录

深度学习系列文章目录

01-【深度学习-Day 1】为什么深度学习是未来?一探究竟AI、ML、DL关系与应用
02-【深度学习-Day 2】图解线性代数:从标量到张量,理解深度学习的数据表示与运算
03-【深度学习-Day 3】搞懂微积分关键:导数、偏导数、链式法则与梯度详解
04-【深度学习-Day 4】掌握深度学习的“概率”视角:基础概念与应用解析
05-【深度学习-Day 5】Python 快速入门:深度学习的“瑞士军刀”实战指南
06-【深度学习-Day 6】掌握 NumPy:ndarray 创建、索引、运算与性能优化指南
07-【深度学习-Day 7】精通Pandas:从Series、DataFrame入门到数据清洗实战
08-【深度学习-Day 8】让数据说话:Python 可视化双雄 Matplotlib 与 Seaborn 教程
09-【深度学习-Day 9】机器学习核心概念入门:监督、无监督与强化学习全解析
10-【深度学习-Day 10】机器学习基石:从零入门线性回归与逻辑回归
11-【深度学习-Day 11】Scikit-learn实战:手把手教你完成鸢尾花分类项目
12-【深度学习-Day 12】从零认识神经网络:感知器原理、实现与局限性深度剖析
13-【深度学习-Day 13】激活函数选型指南:一文搞懂Sigmoid、Tanh、ReLU、Softmax的核心原理与应用场景
14-【深度学习-Day 14】从零搭建你的第一个神经网络:多层感知器(MLP)详解
15-【深度学习-Day 15】告别“盲猜”:一文读懂深度学习损失函数
16-【深度学习-Day 16】梯度下降法 - 如何让模型自动变聪明?
17-【深度学习-Day 17】神经网络的心脏:反向传播算法全解析
18-【深度学习-Day 18】从SGD到Adam:深度学习优化器进阶指南与实战选择
19-【深度学习-Day 19】入门必读:全面解析 TensorFlow 与 PyTorch 的核心差异与选择指南
20-【深度学习-Day 20】PyTorch入门:核心数据结构张量(Tensor)详解与操作


文章目录

  • Langchain系列文章目录
  • Python系列文章目录
  • PyTorch系列文章目录
  • 机器学习系列文章目录
  • 深度学习系列文章目录
  • Java系列文章目录
  • JavaScript系列文章目录
  • 深度学习系列文章目录
  • 前言
  • 一、为什么选择深度学习框架回顾与本系列选择
    • 1.1 框架的核心优势
    • 1.2 本系列框架选择说明
  • 二、初识张量 (Tensor)
    • 2.1 张量:深度学习的基石
    • 2.2 张量与 NumPy 数组的异同
  • 三、PyTorch 张量创建与属性
    • 3.1 创建张量
      • 3.1.1 从现有数据创建
      • 3.1.2 创建特定形状和类型的张量
      • 3.1.3 指定数据类型和设备
    • 3.2 张量属性
  • 四、PyTorch 张量基本操作
    • 4.1 算术运算
    • 4.2 索引与切片
    • 4.3 形状变换
    • 4.4 矩阵运算
    • 4.5 与 NumPy 的无缝转换
  • 五、核心特性:自动求导 (Autograd)
    • 5.1 为什么需要自动求导?
    • 5.2 PyTorch 中的 `requires_grad`
    • 5.3 计算图与梯度计算
    • 5.4 示例:简单函数的梯度计算
    • 5.5 梯度不回传 (`torch.no_grad()`)
  • 六、张量在 GPU 上的运算
    • 6.1 将张量移至 GPU
    • 6.2 检查 GPU 可用性
    • 6.3 GPU 运算的优势与注意事项
  • 七、常见问题与最佳实践
    • 7.1 数据类型不匹配 (`dtype`)
    • 7.2 张量形状不匹配 (`shape`)
    • 7.3 何时使用 `.item()`?
    • 7.4 内存管理提示
  • 八、总结


前言

在前面的学习中(特别是【深度学习-Day 19】),我们探讨了深度学习框架的重要性,以及 TensorFlow 和 PyTorch 这两大主流框架的特点。从本篇文章开始,我们将选择 PyTorch 作为主要的学习和实践框架(当然,很多概念在 TensorFlow 中也是类似的,我们会适时提及共通性),深入探索其核心功能。一切深度学习模型的构建、训练和部署,都离不开其最基础的数据结构——张量 (Tensor)。理解张量及其操作,是踏入深度学习实践大门的第一步,也是至关重要的一步。本文将带你全面认识 PyTorch 中的张量,包括它的创建、属性、常用操作以及核心的自动求导机制。

一、为什么选择深度学习框架回顾与本系列选择

在正式学习张量之前,让我们简要回顾一下为什么深度学习框架如此重要,并明确本系列文章的框架选择。

1.1 框架的核心优势

深度学习框架(如 PyTorch, TensorFlow)为我们提供了:

  1. 高效的数值计算库:特别是针对大规模多维数组(即张量)的运算。
  2. 自动求导机制:这是训练神经网络的核心,使我们从手动计算复杂的梯度中解脱出来。
  3. GPU 加速支持:深度学习通常涉及大量计算,GPU 能显著提升训练速度。框架简化了 GPU 的使用。
  4. 预置的神经网络层与工具:方便我们快速搭建和试验不同的模型结构。
  5. 庞大的社区与丰富的资源:遇到问题时更容易找到解决方案和学习资料。

1.2 本系列框架选择说明

正如【深度学习-Day 19】所讨论的,TensorFlow 和 PyTorch 都是非常优秀的框架。

  • TensorFlow (TF):由 Google 开发,拥有强大的生态系统和部署工具 (TensorFlow Extended, TensorFlow Lite, TensorFlow.js)。其 Keras API 以用户友好著称。早期以静态计算图为主,现在也大力支持动态图 (Eager Execution)。
  • PyTorch (PT):由 Facebook AI Research (FAIR) 开发,以其 Pythonic 的风格和动态计算图(“define-by-run”)受到学术界和研究人员的广泛喜爱,易于调试。

在本系列后续的文章中,我们将主要以 PyTorch 为核心进行讲解和代码演示。 主要原因在于:

  • 学习曲线:对于初学者,PyTorch 的动态图机制和更贴近 Python 编程习惯的接口,可能更容易理解和上手。
  • 灵活性:动态图使得模型结构和计算流程的调试更为直观。
  • 社区活跃度:PyTorch 近年来在学术界的增长迅猛,拥有非常活跃的社区。

当然,我们也会在适当的时候提及 TensorFlow 中的对应概念或实现,帮助大家理解两者之间的共性与差异。掌握一个框架后,学习另一个框架的成本会大大降低。

二、初识张量 (Tensor)

张量是深度学习框架中最基本、最核心的数据结构。可以将其理解为一个多维数组。

2.1 张量:深度学习的基石

在数学和物理学中,张量是一个更广义的概念,但在此处,我们可以简单地将其视为数字的容器,这些数字可以是标量、向量、矩阵或更高维度的数组。

  • 0维张量 (标量 Scalar):一个单独的数字,例如 7 7 7
  • 1维张量 (向量 Vector):一列数字,例如 [ 1 , 2 , 3 ] [1, 2, 3] [1,2,3]
  • 2维张量 (矩阵 Matrix):一个数字的表格,例如
    ( 1 2 3 4 5 6 ) \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix} (142536)
  • 3维张量:可以想象成一个立方体的数字块,例如彩色图像数据 (高度 x 宽度 x 通道数)。
  • 更高维张量:例如视频数据 (帧数 x 高度 x 宽度 x 通道数),或者神经网络中批处理的小批量数据 (批量大小 x …)。

在 PyTorch 中,torch.Tensor 是存储和变换数据的主要工具。

2.2 张量与 NumPy 数组的异同

如果你熟悉 Python 中的 NumPy 库,那么理解 PyTorch 张量会容易得多。PyTorch 张量与 NumPy 的 ndarray 非常相似:

  • 相似点

    • 都是多维数组,可以存储数值型数据。
    • 支持类似的索引、切片和数学运算。
    • PyTorch 张量可以与 NumPy 数组高效地相互转换。
  • 主要区别

    1. GPU 加速:PyTorch 张量可以轻松地在 CPU 和 GPU 之间迁移,并利用 GPU 进行并行计算加速,这是 NumPy ndarray 原生不具备的。
    2. 自动求导 (Automatic Differentiation):PyTorch 张量能够追踪其上的所有操作,从而自动计算梯度。这是训练神经网络(依赖反向传播算法)的关键特性。NumPy 本身不具备这个功能。

下图简要对比了两者:

核心功能
Python 生态
CPU Only
CPU/GPU
追踪操作
高效多维数组运算
高效多维数组运算
自动求导 / Autograd
NumPy ndarray
PyTorch Tensor

三、PyTorch 张量创建与属性

现在,让我们动手用 PyTorch 创建一些张量,并了解它们的属性。首先,确保你已经安装了 PyTorch。

import torch
import numpy as np # 稍后用于与 NumPy 交互
print(torch.__version__) # 打印 PyTorch 版本

3.1 创建张量

PyTorch 提供了多种创建张量的方法。

3.1.1 从现有数据创建

最直接的方式是从 Python 列表或 NumPy 数组创建张量。

# 从 Python 列表创建
data_list = [[1, 2], [3, 4]]
tensor_from_list = torch.tensor(data_list)
print("从列表创建:\n", tensor_from_list)# 从 NumPy 数组创建
data_numpy = np.array([[5, 6], [7, 8]])
tensor_from_numpy = torch.from_numpy(data_numpy) # 或者 torch.tensor(data_numpy)
print("从NumPy数组创建 (torch.from_numpy):\n", tensor_from_numpy)# 使用 torch.tensor() 也可以从 NumPy 数组创建,它会复制数据
tensor_from_numpy_copy = torch.tensor(data_numpy)
print("从NumPy数组创建 (torch.tensor):\n", tensor_from_numpy_copy)

注意torch.tensor() 会复制数据,而 torch.from_numpy() 会共享内存(如果 NumPy 数组在 CPU 上),这意味着修改一方可能会影响另一方。

3.1.2 创建特定形状和类型的张量

PyTorch 也允许你创建具有特定形状和初始值的张量,类似于 NumPy。

# 创建一个未初始化的张量 (值是随机的,取决于内存状态)
empty_tensor = torch.empty(2, 3)
print("未初始化张量 (empty_tensor):\n", empty_tensor)# 创建一个全零张量
zeros_tensor = torch.zeros(2, 3)
print("全零张量 (zeros_tensor):\n", zeros_tensor)# 创建一个全一张量
ones_tensor = torch.ones(2, 3)
print("全一张量 (ones_tensor):\n", ones_tensor)# 创建一个随机张量 (均匀分布在 [0, 1))
rand_tensor = torch.rand(2, 3)
print("随机张量 (rand_tensor, 均匀分布):\n", rand_tensor)# 创建一个随机张量 (标准正态分布,均值为0,方差为1)
randn_tensor = torch.randn(2, 3)
print("随机张量 (randn_tensor, 标准正态分布):\n", randn_tensor)# 创建一个与现有张量形状相同的张量
x_data = torch.tensor([[1,2],[3,4]])
zeros_like_x = torch.zeros_like(x_data) # 形状与 x_data 相同,元素为0
print("zeros_like_x:\n", zeros_like_x)
ones_like_x = torch.ones_like(x_data)   # 形状与 x_data 相同,元素为1
print("ones_like_x:\n", ones_like_x)
rand_like_x = torch.rand_like(x_data)   # 形状与 x_data 相同,元素为随机
print("rand_like_x:\n", rand_like_x)

3.1.3 指定数据类型和设备

创建张量时,可以指定其数据类型 (dtype) 和存储设备 (device)。

# 指定数据类型
float_tensor = torch.tensor([1, 2, 3], dtype=torch.float32)
print("浮点型张量 (float_tensor):", float_tensor, " dtype:", float_tensor.dtype)long_tensor = torch.tensor([1, 2, 3], dtype=torch.long) # 通常用于索引或标签
print("长整型张量 (long_tensor):", long_tensor, " dtype:", long_tensor.dtype)# 指定设备 (CPU 或 GPU)
# 首先检查 CUDA 是否可用
if torch.cuda.is_available():device = torch.device("cuda")          # 使用第一个可用的 CUDA GPUcuda_tensor = torch.ones(2, 2, device=device)print("CUDA 张量 (cuda_tensor):\n", cuda_tensor)print("CUDA 张量设备:", cuda_tensor.device)
else:device = torch.device("cpu")print("CUDA 不可用,使用 CPU。")# 在 CPU 上创建
cpu_tensor = torch.ones(2, 2, device=torch.device("cpu")) # 或者 device='cpu'
print("CPU 张量 (cpu_tensor):\n", cpu_tensor)
print("CPU 张量设备:", cpu_tensor.device)# 也可以使用 .to() 方法在不同设备间移动张量
tensor_on_cpu = torch.randn(2,2)
print("原始张量在 CPU:", tensor_on_cpu.device)
if torch.cuda.is_available():tensor_on_gpu = tensor_on_cpu.to(device) # device 已设为 "cuda"print("移动到 GPU 后:", tensor_on_gpu.device)tensor_back_to_cpu = tensor_on_gpu.to("cpu")print("移回 CPU 后:", tensor_back_to_cpu.device)

3.2 张量属性

张量对象包含一些重要的属性,可以帮助我们了解其特性:

  • tensor.shapetensor.size(): 返回张量的形状 (一个元组)。
  • tensor.dtype: 返回张量中元素的数据类型 (例如 torch.float32, torch.int64)。
  • tensor.device: 返回张量所在的设备 (例如 cpu, cuda:0)。
  • tensor.requires_grad: 一个布尔值,指示该张量是否需要计算梯度。默认为 False,除非显式设置为 True,或者该张量是由一个 requires_grad=True 的张量操作得到的。这是自动求导的关键。
  • tensor.ndimlen(tensor.shape): 返回张量的维度数量。
my_tensor = torch.randn(3, 4, dtype=torch.float32, device="cpu")print("张量:\n", my_tensor)
print("形状 (shape):", my_tensor.shape)
print("形状 (size()):", my_tensor.size())
print("数据类型 (dtype):", my_tensor.dtype)
print("所在设备 (device):", my_tensor.device)
print("是否需要梯度 (requires_grad):", my_tensor.requires_grad) # 默认为 False
print("维度数量 (ndim):", my_tensor.ndim)

四、PyTorch 张量基本操作

PyTorch 张量支持丰富的操作,包括算术运算、索引切片、形状变换、矩阵运算等。这些操作大多与 NumPy 类似。

4.1 算术运算

常见的算术运算都可以按元素 (element-wise) 应用于张量。

x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
y = torch.tensor([[5.0, 6.0], [7.0, 8.0]])# 加法
print("x + y:\n", x + y)
print("torch.add(x, y):\n", torch.add(x, y))# 减法
print("x - y:\n", x - y)# 乘法 (element-wise)
print("x * y (element-wise):\n", x * y)
print("torch.mul(x, y):\n", torch.mul(x, y))# 除法
print("x / y:\n", x / y)# 指数
print("torch.exp(x):\n", torch.exp(x))# 许多操作都有一个 inplace 版本,通常以下划线结尾,例如 add_()
# inplace 操作会直接修改原始张量
z = torch.zeros_like(x)
z.add_(x) # z 的值现在和 x 一样了
print("z after z.add_(x):\n", z)
# x.add_(y) # x 的值会被修改
# print("x after x.add_(y):\n", x) # x has been modified

4.2 索引与切片

张量的索引和切片操作与 NumPy ndarray 非常相似。

tensor_slice = torch.arange(10) # tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print("原始张量:", tensor_slice)# 获取单个元素
print("第一个元素:", tensor_slice[0])
print("最后一个元素:", tensor_slice[-1])# 切片
print("前3个元素:", tensor_slice[:3])
print("从索引2到索引5 (不含5):", tensor_slice[2:5])
print("所有元素,步长为2:", tensor_slice[::2])# 多维张量索引
matrix = torch.tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("原始矩阵:\n", matrix)
print("第一行:", matrix[0])         # 等价于 matrix[0, :]
print("第一列:", matrix[:, 0])
print("元素 (1,1):", matrix[1, 1]) # 第二行第二列的元素 (5)
print("子矩阵 (前两行,后两列):\n", matrix[:2, 1:])

4.3 形状变换

改变张量的形状而不改变其数据,是常见的需求。

  • tensor.view(*shape): 返回一个具有新形状的新张量,但与原张量共享数据 (如果可能)。要求新旧张量元素总数一致,且原张量是连续的 (contiguous)。
  • tensor.reshape(*shape): 功能类似 view,但更灵活,不一定共享数据 (如果需要,它会复制数据)。通常更推荐使用 reshape,除非你确定需要共享数据且了解 view 的限制。
  • tensor.squeeze(): 移除所有维度为1的维度。
  • tensor.unsqueeze(dim): 在指定维度 dim 上插入一个维度。
  • tensor.permute(*dims): 按照给定的维度顺序重新排列张量的维度。
original = torch.randn(2, 3, 4) # 形状 (2, 3, 4)
print("Original shape:", original.shape)# view (要求连续内存)
# 元素总数 2*3*4 = 24
view_tensor = original.view(2, 12) # 形状 (2, 12)
print("View tensor shape:", view_tensor.shape)
# view_tensor_fail = original.view(2, 10) # 会报错,元素总数不匹配# reshape (更常用)
reshape_tensor = original.reshape(6, 4) # 形状 (6, 4)
print("Reshape tensor shape:", reshape_tensor.shape)
reshape_auto = original.reshape(3, -1) # -1 表示该维度大小由其他维度自动推断
print("Reshape with -1 shape:", reshape_auto.shape) # (3, 8)# squeeze 和 unsqueeze
s_tensor = torch.randn(1, 3, 1, 2)
print("s_tensor shape:", s_tensor.shape) # torch.Size([1, 3, 1, 2])
squeezed = s_tensor.squeeze()
print("squeezed shape:", squeezed.shape) # torch.Size([3, 2])unsqueezed_dim0 = squeezed.unsqueeze(0)
print("unsqueezed_dim0 shape:", unsqueezed_dim0.shape) # torch.Size([1, 3, 2])
unsqueezed_dim1 = squeezed.unsqueeze(1)
print("unsqueezed_dim1 shape:", unsqueezed_dim1.shape) # torch.Size([3, 1, 2])# permute (维度换位)
# 例如,将 (通道数, 高度, 宽度) 转为 (高度, 宽度, 通道数)
img_tensor = torch.randn(3, 224, 224) # (C, H, W)
permuted_img = img_tensor.permute(1, 2, 0) # (H, W, C)
print("Original img shape:", img_tensor.shape)
print("Permuted img shape:", permuted_img.shape)

关于 viewreshape 的一个重要提示view 要求张量在内存中是“连续的”(contiguous)。如果张量由于某些操作(如 permute 或某些切片)导致不连续,直接调用 view 可能会失败。此时,可以先调用 .contiguous() 方法使其连续,然后再 view,或者直接使用 reshape,后者会自动处理连续性问题(可能通过数据复制)。

4.4 矩阵运算

对于2D张量(矩阵),PyTorch 提供了标准的矩阵运算。

  • torch.matmul(tensor1, tensor2)tensor1 @ tensor2: 矩阵乘法。
  • torch.mm(mat1, mat2): 专门用于2D矩阵乘法,是 matmul 的一个子集。
  • tensor.t(): 转置 (仅限2D张量)。对于更高维张量,使用 permute
mat1 = torch.randn(2, 3)
mat2 = torch.randn(3, 4)
vec = torch.randn(3)# 矩阵乘法
result_mm = torch.mm(mat1, mat2)
print("torch.mm(mat1, mat2) shape:", result_mm.shape) # (2, 4)result_matmul = mat1 @ mat2 # Python 3.5+
print("mat1 @ mat2 shape:", result_matmul.shape) # (2, 4)# matmul 更通用,可以处理更高维度的广播
# (b, n, m) @ (b, m, p) -> (b, n, p)
# (n, m) @ (m) -> (n) (矩阵向量乘法)
mat_x_vec = mat1 @ vec
print("mat1 @ vec shape:", mat_x_vec.shape) # (2)# 转置
mat = torch.tensor([[1,2,3],[4,5,6]])
print("Original matrix:\n", mat)
print("Transposed matrix:\n", mat.t())

4.5 与 NumPy 的无缝转换

PyTorch 张量与 NumPy 数组之间的转换非常方便。

  • tensor.numpy(): 将 CPU 上的 PyTorch 张量转换为 NumPy 数组。共享内存
  • torch.from_numpy(ndarray): 将 NumPy 数组转换为 PyTorch 张量。共享内存
# Tensor to NumPy
cpu_tensor = torch.ones(5)
numpy_array = cpu_tensor.numpy()
print("PyTorch Tensor:", cpu_tensor)
print("NumPy array:", numpy_array)# 修改一方会影响另一方 (因为共享内存)
cpu_tensor.add_(1)
print("PyTorch Tensor after add_():", cpu_tensor)
print("NumPy array after Tensor modification:", numpy_array) # 也变了# NumPy to Tensor
a = np.ones(5)
torch_tensor = torch.from_numpy(a)
print("NumPy array:", a)
print("PyTorch Tensor:", torch_tensor)np.add(a, 1, out=a) # 修改 NumPy 数组
print("NumPy array after modification:", a)
print("PyTorch Tensor after NumPy modification:", torch_tensor) # 也变了# 注意:如果张量在 GPU 上,需要先将其移到 CPU 才能转换为 NumPy 数组
if torch.cuda.is_available():gpu_tensor = torch.ones(3, device="cuda")# numpy_from_gpu = gpu_tensor.numpy() # 会报错numpy_from_gpu = gpu_tensor.cpu().numpy()print("NumPy from GPU tensor:", numpy_from_gpu)

这种内存共享机制在 CPU 上可以实现高效的数据交换,但也要小心意外的副作用。如果想获得一个副本,可以使用 tensor.clone().numpy()torch.tensor(numpy_array)

五、核心特性:自动求导 (Autograd)

自动求导是 PyTorch 等深度学习框架的核心功能,它使得我们能够自动计算损失函数相对于模型参数的梯度,从而进行反向传播和参数优化。

5.1 为什么需要自动求导?

回想一下训练神经网络的过程(如【深度学习-Day 16、17】):

  1. 前向传播:输入数据通过网络计算得到输出和损失。
  2. 计算梯度:损失函数对网络中所有可训练参数(权重、偏置)求偏导数。
  3. 反向传播:利用链式法则,从后向前高效计算这些梯度。
  4. 参数更新:根据梯度和优化算法(如梯度下降)更新参数。

手动计算复杂神经网络的梯度几乎是不可能的。自动求导系统 (如 PyTorch 的 Autograd) 为我们完成了第2步和第3步。

5.2 PyTorch 中的 requires_grad

要让 PyTorch 追踪对某个张量的操作并计算其梯度,需要将其 requires_grad 属性设置为 True

# 创建一个需要梯度的张量
x = torch.ones(2, 2, requires_grad=True)
print("x:\n", x)
print("x.requires_grad:", x.requires_grad)y = x + 2
print("y:\n", y)
# y 是由一个 requires_grad=True 的张量 x 操作得到的,所以 y 也会自动 requires_grad=True
print("y.requires_grad:", y.requires_grad)
# y 还会有一个 grad_fn 属性,指向创建它的函数 (这里是 AddBackward0)
print("y.grad_fn:", y.grad_fn)z = y * y * 3
out = z.mean()
print("z:\n", z)
print("out:", out)
print("out.requires_grad:", out.requires_grad)
print("out.grad_fn:", out.grad_fn) # MeanBackward0

如果一个张量是通过运算从其他 requires_grad=True 的张量得到的,那么它默认也会是 requires_grad=True,并且会有一个 grad_fn 属性,该属性引用了一个创建该张量的函数(例如,加法操作是 AddBackward0,乘法是 MulBackward0 等)。这个 grad_fn 是构建反向传播计算图的关键。

5.3 计算图与梯度计算

PyTorch 使用动态计算图。当你对 requires_grad=True 的张量执行操作时,PyTorch 会在后台构建一个有向无环图 (DAG),记录数据(张量)和所有操作(函数)。叶子节点是输入张量,根节点是输出张量。

当我们在某个标量输出(通常是损失函数值)上调用 .backward() 方法时,Autograd 会:

  1. 从该标量开始,沿着计算图反向传播。
  2. 计算图中所有 requires_grad=True 的叶子节点张量相对于该标量的梯度。
  3. 梯度会累积到相应张量的 .grad 属性中。
# 继续上面的例子
# out 是一个标量
out.backward() # 执行反向传播# 现在 x.grad 包含了 d(out)/dx 的梯度
print("Gradient of out w.r.t. x (x.grad):\n", x.grad)

让我们手动验证一下:
x = ( 1 1 1 1 ) x = \begin{pmatrix} 1 & 1 \\ 1 & 1 \end{pmatrix} x=(1111)
y = x + 2 = ( 3 3 3 3 ) y = x + 2 = \begin{pmatrix} 3 & 3 \\ 3 & 3 \end{pmatrix} y=x+2=(3333)
z = 3 y 2 = 3 ( 9 9 9 9 ) = ( 27 27 27 27 ) z = 3y^2 = 3 \begin{pmatrix} 9 & 9 \\ 9 & 9 \end{pmatrix} = \begin{pmatrix} 27 & 27 \\ 27 & 27 \end{pmatrix} z=3y2=3(9999)=(27272727)
o u t = 1 4 ∑ z i j = 1 4 ( 27 × 4 ) = 27 out = \frac{1}{4} \sum z_{ij} = \frac{1}{4} (27 \times 4) = 27 out=41zij=41(27×4)=27

现在计算梯度 ∂ o u t ∂ x i j \frac{\partial out}{\partial x_{ij}} xijout:
∂ o u t ∂ z k l = 1 4 \frac{\partial out}{\partial z_{kl}} = \frac{1}{4} zklout=41
∂ z k l ∂ y k l = 6 y k l \frac{\partial z_{kl}}{\partial y_{kl}} = 6y_{kl} yklzkl=6ykl
∂ y k l ∂ x i j = 1 \frac{\partial y_{kl}}{\partial x_{ij}} = 1 xijykl=1 (如果 k , l = i , j k,l = i,j k,l=i,j), 0 0 0 (其他)

所以, ∂ o u t ∂ x i j = ∂ o u t ∂ z i j ∂ z i j ∂ y i j ∂ y i j ∂ x i j = 1 4 ⋅ 6 y i j ⋅ 1 = 3 2 y i j \frac{\partial out}{\partial x_{ij}} = \frac{\partial out}{\partial z_{ij}} \frac{\partial z_{ij}}{\partial y_{ij}} \frac{\partial y_{ij}}{\partial x_{ij}} = \frac{1}{4} \cdot 6y_{ij} \cdot 1 = \frac{3}{2} y_{ij} xijout=zijoutyijzijxijyij=416yij1=23yij
由于 y i j = 3 y_{ij} = 3 yij=3 对所有 i , j i,j i,j,所以 ∂ o u t ∂ x i j = 3 2 ⋅ 3 = 4.5 \frac{\partial out}{\partial x_{ij}} = \frac{3}{2} \cdot 3 = 4.5 xijout=233=4.5
所以 x . g r a d x.grad x.grad 应该是 ( 4.5 4.5 4.5 4.5 ) \begin{pmatrix} 4.5 & 4.5 \\ 4.5 & 4.5 \end{pmatrix} (4.54.54.54.5),这与 PyTorch 计算的结果一致。

重要注意事项

  • .backward() 只能对标量输出调用。如果输出是张量,需要先对其进行聚合操作(如 .sum().mean())得到标量,或者在 .backward() 中提供一个与输出张量形状相同的 gradient 参数(表示上游梯度)。
  • 梯度是累积的 (accumulated)。这意味着如果你多次调用 .backward() 而不清除之前的梯度,新的梯度会加到 .grad 属性上。在训练循环中,通常在每次迭代计算新梯度之前,需要使用 optimizer.zero_grad() 或手动将参数的 .grad 设为 None0
# 梯度累积示例
x = torch.ones(1, requires_grad=True)
y = x * 2
y.backward() # dy/dx = 2. x.grad is now 2.
print("x.grad after first backward:", x.grad)z = x * 3
z.backward() # dz/dx = 3. x.grad is now 2 + 3 = 5.
print("x.grad after second backward (accumulated):", x.grad)# 清除梯度
x.grad.zero_() # In-place zeroing
# 或者 x.grad = None
w = x * 4
w.backward() # dw/dx = 4. x.grad is now 4.
print("x.grad after zeroing and third backward:", x.grad)

5.4 示例:简单函数的梯度计算

让我们再看一个简单的线性回归中的例子,假设我们有一个预测 y p r e d = w x + b y_{pred} = wx + b ypred=wx+b,损失 L = ( y p r e d − y t r u e ) 2 L = (y_{pred} - y_{true})^2 L=(ypredytrue)2。我们想计算 ∂ L ∂ w \frac{\partial L}{\partial w} wL ∂ L ∂ b \frac{\partial L}{\partial b} bL

# 假设的输入和真实值
x_val = torch.tensor(2.0)
y_true = torch.tensor(7.0)# 模型参数 (需要梯度)
w = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)print(f"Initial w: {w.item()}, b: {b.item()}")# 前向传播
y_pred = w * x_val + b
print(f"Prediction y_pred: {y_pred.item()}")# 计算损失 (标量)
loss = (y_pred - y_true)**2
print(f"Loss: {loss.item()}")# 反向传播计算梯度
loss.backward()# 查看梯度
print(f"Gradient dL/dw: {w.grad.item()}")
print(f"Gradient dL/db: {b.grad.item()}")

5.5 梯度不回传 (torch.no_grad())

有时,我们不希望某些操作被 Autograd 追踪,例如在模型评估(推理)阶段,或者在更新模型参数时(优化器内部通常会做这个)。可以使用 with torch.no_grad(): 上下文管理器来临时禁用梯度计算。

x = torch.tensor([1.0], requires_grad=True)
print("x.requires_grad:", x.requires_grad)with torch.no_grad():y = x * 2print("Inside no_grad context, y.requires_grad:", y.requires_grad) # Falsez = x * 3 # 离开 no_grad 上下文
print("Outside no_grad context, z.requires_grad:", z.requires_grad) # True, 因为 x 仍然 requires_grad# 另一个用途:当你只想修改一个张量的值,而不希望这个修改被梯度追踪
# (例如,手动更新某些参数或统计数据)
a = torch.randn(2,2, requires_grad=True)
print("a requires_grad:", a.requires_grad)
a[0,0] = 100.0 # 这个操作会被追踪
print("a.grad_fn for modification:", a.grad_fn) # _CopySlices# 如果不想追踪修改操作
b = torch.randn(2,2, requires_grad=True)
print("b requires_grad:", b.requires_grad)
with torch.no_grad():b[0,0] = 100.0
print("b.grad_fn after no_grad modification:", b.grad_fn) # None

此外,张量还有一个 .detach() 方法,它会创建一个与原张量共享数据但不参与梯度计算的新张量。

六、张量在 GPU 上的运算

深度学习模型通常包含大量计算,利用 GPU 可以显著加速训练过程。PyTorch 使得在 GPU 上进行张量运算非常方便。

6.1 将张量移至 GPU

如前所述,可以使用 .to(device) 方法或特定快捷方式如 .cuda() 将张量从 CPU 移动到 GPU,或在不同 GPU 设备间移动。

# 检查 CUDA (NVIDIA GPU) 是否可用
if torch.cuda.is_available():print("CUDA is available! Using GPU.")device = torch.device("cuda")
else:print("CUDA not available. Using CPU.")device = torch.device("cpu")# 创建张量并移至指定设备
x_cpu = torch.randn(3, 3)
x_gpu = x_cpu.to(device)print("x_cpu device:", x_cpu.device)
print("x_gpu device:", x_gpu.device)# 也可以直接在 GPU 上创建张量
if torch.cuda.is_available():y_gpu = torch.randn(2, 2, device=device) # 或者 device='cuda'print("y_gpu device:", y_gpu.device)# 将 GPU 张量移回 CPUy_cpu = y_gpu.cpu() # 等价于 y_gpu.to('cpu')print("y_cpu device:", y_cpu.device)# 模型参数也需要移动到 GPU
# class MyModel(nn.Module):
#     def __init__(self):
#         super().__init__()
#         self.linear = nn.Linear(10,1)
# my_model = MyModel().to(device) # 模型的所有参数都会被移动到 device

重要:要进行运算的张量必须在同一个设备上。尝试对一个 CPU 张量和一个 GPU 张量直接进行运算会导致错误。

# 错误示例:不同设备上的张量运算
# a_cpu = torch.rand(2,2, device='cpu')
# b_gpu = torch.rand(2,2, device=device) # 假设 device 是 'cuda'
# try:
#    c_result = a_cpu + b_gpu
# except RuntimeError as e:
#    print(f"Error: {e}") # 会报 "Expected all tensors to be on the same device"

6.2 检查 GPU 可用性

在代码中动态检查 GPU 可用性是一个好习惯:
torch.cuda.is_available(): 返回布尔值。
torch.cuda.device_count(): 返回可用 GPU 的数量。
torch.cuda.current_device(): 返回当前选定 GPU 的索引。
torch.cuda.get_device_name(0): 返回第0个 GPU 的名称。

6.3 GPU 运算的优势与注意事项

  • 优势:对于大规模并行计算(如矩阵乘法、卷积),GPU 比 CPU 快得多。
  • 注意事项
    • 数据传输开销:将数据从 CPU 内存复制到 GPU 显存(反之亦然)是需要时间的。如果计算量本身很小,这个开销可能会抵消 GPU 的计算优势。因此,应尽量减少不必要的数据来回拷贝。
    • 显存限制:GPU 显存通常比 CPU 内存小。需要注意模型大小和批量大小,避免显存溢出 (CUDA out of memory)。

七、常见问题与最佳实践

在使用 PyTorch 张量时,可能会遇到一些常见问题。

7.1 数据类型不匹配 (dtype)

进行运算时,参与运算的张量通常需要有相同的数据类型,否则可能报错或得到意外结果。
例如 RuntimeError: expected scalar type Float but got Double

解决:使用 .float(), .long(), .double(), .to(dtype=...) 等方法进行类型转换。

tensor_float = torch.tensor([1.0, 2.0], dtype=torch.float32)
tensor_double = torch.tensor([3.0, 4.0], dtype=torch.float64)# print(tensor_float + tensor_double) # 可能会报错或行为不确定,取决于 PyTorch 版本和操作# 显式转换
result = tensor_float + tensor_double.float() # 将 double 转为 float
print("Result after dtype conversion:", result, result.dtype)

7.2 张量形状不匹配 (shape)

很多操作(如元素级乘法、矩阵乘法)对张量的形状有要求。形状不匹配是常见的错误来源。
例如 RuntimeError: The size of tensor a (X) must match the size of tensor b (Y) at non-singleton dimension Z

解决

  • 仔细检查参与运算的张量的 shape
  • 使用 reshape, view, squeeze, unsqueeze, permute 等调整形状。
  • 理解并利用广播机制 (Broadcasting),它允许在某些条件下对不同形状的张量进行运算。

7.3 何时使用 .item()?

当张量只包含一个元素(标量)时,可以使用 .item() 方法将其转换为标准的 Python 数字。这对于从损失值或评估指标中获取 Python 数字非常有用。

scalar_tensor = torch.tensor(3.14159)
py_number = scalar_tensor.item()
print("Scalar Tensor:", scalar_tensor)
print("Python Number:", py_number, type(py_number))# non_scalar_tensor = torch.tensor([1.0, 2.0])
# py_number_fail = non_scalar_tensor.item() # 会报错,因为不是标量

7.4 内存管理提示

  • 显式删除:对于不再需要的大的张量,可以使用 del tensor_name 来解除引用,Python 的垃圾回收机制后续会回收内存。
  • GPU 缓存:PyTorch 有一个 GPU 内存缓存分配器。有时,即使你 del 了一个 GPU 张量,nvidia-smi 可能仍显示显存被占用。可以使用 torch.cuda.empty_cache() 来尝试释放未被占用的缓存块,但这不会释放当前仍被引用的张量所占用的显存。这个操作本身比较耗时,不应频繁调用。
  • torch.no_grad():在推理或不需要梯度的计算中使用,可以节省内存,因为不需要存储中间结果用于反向传播。

八、总结

本文详细介绍了 PyTorch 框架中的核心数据结构——张量 (Tensor)。掌握张量是进行深度学习实践的基础。核心要点回顾:

  1. 张量的本质:多维数组,是深度学习中数据和参数的基本表示。
  2. 与 NumPy 的关系:PyTorch 张量与 NumPy 数组高度相似,但增加了 GPU 加速和自动求导的关键特性。两者可以高效转换,并在 CPU 上共享内存。
  3. 创建张量:可以通过 Python 列表、NumPy 数组创建,也可以创建特定形状(全零、全一、随机等)的张量,并能指定数据类型 (dtype) 和设备 (device)。
  4. 张量属性shape, dtype, device, requires_grad 是理解和调试张量时常用的属性。
  5. 常用操作:包括算术运算、索引切片、形状变换 (view, reshape, squeeze, unsqueeze, permute) 和矩阵运算 (matmul, mm, t)。
  6. 自动求导 (Autograd):PyTorch 的核心。通过设置 requires_grad=True,PyTorch 会构建计算图,并在标量输出上调用 .backward() 时自动计算梯度,结果存储在 .grad 属性中。梯度是累积的,需要注意清零。
  7. GPU 运算:使用 .to(device).cuda() 可以将张量和模型移至 GPU 进行加速。运算要求所有相关张量在同一设备。
  8. 最佳实践:注意数据类型和形状匹配,合理使用 .item(),并了解基本的内存管理技巧和 torch.no_grad() 的使用场景。

通过本文的学习,你应该对 PyTorch 张量有了坚实的理解。在接下来的文章中,我们将基于这些张量操作,开始学习如何使用 PyTorch 构建和训练神经网络模型。敬请期待【深度学习-Day 21】!


相关文章:

  • 【教学类-36-10】20250531蝴蝶图案描边,最适合大小(一页1图1图、2图图案不同、2图图案相同对称)
  • 【计算机CPU架构】ARM架构简介
  • YOLOv10改进|爆改模型|涨点|在颈部网络添加结合部分卷积PConv和SDI融合方法的PSDI特征融合层(附代码+修改教程)
  • 如何打包conda环境从一台电脑到另外一台电脑
  • C语言 — 动态内存管理
  • 鸿蒙HarmonyOS (React Native)的实战教程
  • 【NLP 78、手搓Transformer模型结构】
  • leetcode刷题日记——二叉树的右视图
  • 使用Python绘制节日祝福——以端午节和儿童节为例
  • 嵌入式编译工具链熟悉与游戏移植
  • Fragment事务commit与commitNow区别
  • atapi!IdeReadWrite函数分析中.txt
  • LeeCode 98. 验证二叉搜索树
  • LearnOpenGL-笔记-其十二
  • oscp练习PG Monster靶机复现
  • C# 如何获取当前成员函数的函数名
  • Kerberos面试内容整理-Kerberos 与 LDAP/Active Directory 的集成
  • C++哈希表:unordered系列容器详解
  • 2.5/Q2,Charls最新文章解读
  • 动态规划-376.摆动序列-力扣(LeetCode)
  • 潍坊疫情最新消息今天又增加9人/绍兴百度推广优化排名
  • 改善网站建设/太原seo软件
  • 南京建设网站公司哪家好/网站怎么优化到首页
  • 商务网站的主要存在形式/商丘seo教程
  • 化妆培训网站开发/it培训机构口碑排名
  • 做淘宝的网站/长沙seo优化推荐