【深度学习-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)为我们提供了:
- 高效的数值计算库:特别是针对大规模多维数组(即张量)的运算。
- 自动求导机制:这是训练神经网络的核心,使我们从手动计算复杂的梯度中解脱出来。
- GPU 加速支持:深度学习通常涉及大量计算,GPU 能显著提升训练速度。框架简化了 GPU 的使用。
- 预置的神经网络层与工具:方便我们快速搭建和试验不同的模型结构。
- 庞大的社区与丰富的资源:遇到问题时更容易找到解决方案和学习资料。
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 数组高效地相互转换。
-
主要区别:
- GPU 加速:PyTorch 张量可以轻松地在 CPU 和 GPU 之间迁移,并利用 GPU 进行并行计算加速,这是 NumPy
ndarray
原生不具备的。 - 自动求导 (Automatic Differentiation):PyTorch 张量能够追踪其上的所有操作,从而自动计算梯度。这是训练神经网络(依赖反向传播算法)的关键特性。NumPy 本身不具备这个功能。
- GPU 加速:PyTorch 张量可以轻松地在 CPU 和 GPU 之间迁移,并利用 GPU 进行并行计算加速,这是 NumPy
下图简要对比了两者:
三、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.shape
或tensor.size()
: 返回张量的形状 (一个元组)。tensor.dtype
: 返回张量中元素的数据类型 (例如torch.float32
,torch.int64
)。tensor.device
: 返回张量所在的设备 (例如cpu
,cuda:0
)。tensor.requires_grad
: 一个布尔值,指示该张量是否需要计算梯度。默认为False
,除非显式设置为True
,或者该张量是由一个requires_grad=True
的张量操作得到的。这是自动求导的关键。tensor.ndim
或len(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)
关于 view
和 reshape
的一个重要提示: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】):
- 前向传播:输入数据通过网络计算得到输出和损失。
- 计算梯度:损失函数对网络中所有可训练参数(权重、偏置)求偏导数。
- 反向传播:利用链式法则,从后向前高效计算这些梯度。
- 参数更新:根据梯度和优化算法(如梯度下降)更新参数。
手动计算复杂神经网络的梯度几乎是不可能的。自动求导系统 (如 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 会:
- 从该标量开始,沿着计算图反向传播。
- 计算图中所有
requires_grad=True
的叶子节点张量相对于该标量的梯度。 - 梯度会累积到相应张量的
.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=41∑zij=41(27×4)=27
现在计算梯度 ∂ o u t ∂ x i j \frac{\partial out}{\partial x_{ij}} ∂xij∂out:
∂ o u t ∂ z k l = 1 4 \frac{\partial out}{\partial z_{kl}} = \frac{1}{4} ∂zkl∂out=41
∂ z k l ∂ y k l = 6 y k l \frac{\partial z_{kl}}{\partial y_{kl}} = 6y_{kl} ∂ykl∂zkl=6ykl
∂ y k l ∂ x i j = 1 \frac{\partial y_{kl}}{\partial x_{ij}} = 1 ∂xij∂ykl=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} ∂xij∂out=∂zij∂out∂yij∂zij∂xij∂yij=41⋅6yij⋅1=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 ∂xij∂out=23⋅3=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
设为None
或0
。
# 梯度累积示例
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=(ypred−ytrue)2。我们想计算 ∂ L ∂ w \frac{\partial L}{\partial w} ∂w∂L 和 ∂ L ∂ b \frac{\partial L}{\partial b} ∂b∂L。
# 假设的输入和真实值
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)。掌握张量是进行深度学习实践的基础。核心要点回顾:
- 张量的本质:多维数组,是深度学习中数据和参数的基本表示。
- 与 NumPy 的关系:PyTorch 张量与 NumPy 数组高度相似,但增加了 GPU 加速和自动求导的关键特性。两者可以高效转换,并在 CPU 上共享内存。
- 创建张量:可以通过 Python 列表、NumPy 数组创建,也可以创建特定形状(全零、全一、随机等)的张量,并能指定数据类型 (
dtype
) 和设备 (device
)。 - 张量属性:
shape
,dtype
,device
,requires_grad
是理解和调试张量时常用的属性。 - 常用操作:包括算术运算、索引切片、形状变换 (
view
,reshape
,squeeze
,unsqueeze
,permute
) 和矩阵运算 (matmul
,mm
,t
)。 - 自动求导 (Autograd):PyTorch 的核心。通过设置
requires_grad=True
,PyTorch 会构建计算图,并在标量输出上调用.backward()
时自动计算梯度,结果存储在.grad
属性中。梯度是累积的,需要注意清零。 - GPU 运算:使用
.to(device)
或.cuda()
可以将张量和模型移至 GPU 进行加速。运算要求所有相关张量在同一设备。 - 最佳实践:注意数据类型和形状匹配,合理使用
.item()
,并了解基本的内存管理技巧和torch.no_grad()
的使用场景。
通过本文的学习,你应该对 PyTorch 张量有了坚实的理解。在接下来的文章中,我们将基于这些张量操作,开始学习如何使用 PyTorch 构建和训练神经网络模型。敬请期待【深度学习-Day 21】!