PyTorch 2.x 深度学习专题【左扬精讲】—— 张量(Tensor):从入门到精通的完全指南
PyTorch 2.x 深度学习专题【左扬精讲】—— 张量(Tensor):从入门到精通的完全指南
张量(Tensor)是 PyTorch 和深度学习的基石。
无论是构建神经网络、处理图像数据、还是训练大语言模型,一切都从张量开始。
很多初学者对张量感到困惑,是因为没有从根本上理解它的本质。
本文将用最通俗易懂的方式,带你彻底搞懂 PyTorch 张量的所有核心概念,包括:NumPy 与 PyTorch Tensor 的区别、四元身份、创建方式、索引切片、广播机制、自动求导、设备迁移、内存布局、类型转换、与 NumPy 互转,以及从各种文件加载张量的方法。
这是一篇面向零基础的超详细教程,学完本文,你将对 PyTorch 张量有全面而深入的理解。
PyTorch 2.x Tensor NumPy 深度学习 张量 自动求导 GPU
学习重点提示
- 必须掌握:NumPy ndarray 与 PyTorch Tensor 的核心区别
- 必须掌握:张量的四元身份:shape / dtype / device / layout
- 必须掌握:张量的各种创建方式
- 必须掌握:索引与切片、广播机制
- 必须掌握:自动求导机制(requires_grad、grad_fn、backward)
- 必须掌握:设备迁移(CPU/GPU/MPS)
- 建议掌握:内存布局(stride、contiguous、memory_format)
- 建议掌握:从各种文件加载张量的方法
目 录
第一章 从认识张量开始
1.1 什么是张量?(What)
张量(Tensor)是 PyTorch 中用于存储和计算数据的基本数据结构。你可以把它理解为多维数组的数学概念在计算机中的实现。
在数学中:
- 标量(Scalar):0 维张量,一个单独的数,如 3.14
- 向量(Vector):1 维张量,一维数组,如 [1, 2, 3, 4]
- 矩阵(Matrix):2 维张量,二维数组,如一张灰度图像
- 张量(Tensor):3 维及以上的数组,如彩色图像(RGB 三通道)、视频(多帧图像)
Why — 为什么深度学习需要张量?
神经网络本质上是对数据进行一系列数学运算(矩阵乘法、卷积等)。张量提供了:
- 统一的数据容器:无论是一维文本、二维图像、还是三维视频,都可以用张量表示
- 高效的计算:张量运算可以在 CPU、GPU、甚至 TPU 上高效执行
- 自动求导:PyTorch 的张量支持自动微分,这是神经网络训练的基础
1.2 张量的直观理解
让我们用日常生活中的例子来理解张量的维度:
标量 (0D): 42 # 一个温度值
向量 (1D): [1, 2, 3, 4, 5] # 一天的气温变化
矩阵 (2D): [[1, 2, 3], # 一周的天气数据
[4, 5, 6],
[7, 8, 9]]
3D张量: [[[1,2], [3,4]], # 一栋楼的温度传感器
[[5,6], [7,8]]]
4D张量: [[[[1,2], [3,4]], # 一个小区所有房间的温度
[[5,6], [7,8]]]]
深度学习中的张量
- 一维张量:文本的词向量、模型的参数向量
- 二维张量:Excel 表格数据、词袋模型的特征向量集合
- 三维张量:时间序列数据(如股票价格)、一句话的词向量序列
- 四维张量:图像数据(batch × channel × height × width)
- 五维张量:视频数据(batch × channel × frames × height × width)
1.3 快速体验:创建你的第一个张量
import torch
# 创建一个简单的 1D 张量(类似 Python 列表)
t1 = torch.tensor([1, 2, 3, 4, 5])
print(f"1D 张量: {t1}")
print(f"形状: {t1.shape}, 数据类型: {t1.dtype}")
# 创建一个 2D 张量(类似 Excel 表格)
t2 = torch.tensor([[1, 2, 3],
[4, 5, 6]])
print(f"\n2D 张量:\n{t2}")
print(f"形状: {t2.shape}")
# 创建一个 3D 张量(类似魔方)
t3 = torch.tensor([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]])
print(f"\n3D 张量:\n{t3}")
print(f"形状: {t3.shape}")
第二章 NumPy ndarray vs PyTorch Tensor 深度对比
2.1 它们是什么?
NumPy ndarray 和 PyTorch Tensor 本质上都是多维数组,它们解决的问题是类似的,但在设计目标和使用场景上有显著差异。
What — 两者分别是什么?
NumPy ndarray 是 Python 科学计算的基础数据结构,由 NumPy 库提供,专门用于高效的数组运算,不支持 GPU 加速。
PyTorch Tensor 是 PyTorch 框架的核心数据结构,不仅支持高效的数组运算,还原生支持 GPU 加速和自动求导。
2.2 核心差异对比
| 对比项 | NumPy ndarray | PyTorch Tensor |
|---|---|---|
| GPU 加速 | 不支持,需要额外库(如 CuPy) | 原生支持,一行代码切换设备 |
| 自动求导 | 不支持,需要手动实现 | 原生支持,神经网络训练必备 |
| 梯度追踪 | 无 | 有(requires_grad 属性) |
| 设备切换 | 只能在 CPU 上运行 | CPU/GPU/MPS 任意切换 |
| 计算图 | 无 | 有(动态计算图) |
| 深度学习框架 | 通用科学计算 | 专为神经网络设计 |
| 社区生态 | 成熟的科学计算生态 | PyTorch 深度学习生态 |
2.3 代码层面的对比
创建数组/张量
import numpy as np
import torch
# NumPy:创建 ndarray
np_array = np.array([[1, 2, 3], [4, 5, 6]])
print(f"NumPy ndarray: type={type(np_array)}, shape={np_array.shape}")
# PyTorch:创建 Tensor
pt_tensor = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(f"PyTorch Tensor: type={type(pt_tensor)}, shape={pt_tensor.shape}")
基本运算对比
import numpy as np
import torch
# NumPy 运算
np_a = np.array([1, 2, 3])
np_b = np.array([4, 5, 6])
np_result = np_a + np_b
print(f"NumPy: {np_a} + {np_b} = {np_result}")
# PyTorch 运算
pt_a = torch.tensor([1, 2, 3])
pt_b = torch.tensor([4, 5, 6])
pt_result = pt_a + pt_b
print(f"PyTorch: {pt_a} + {pt_b} = {pt_result}")
GPU 运算对比
import numpy as np
import torch
# NumPy:只能 CPU
np_arr = np.random.randn(1000, 1000)
# np_arr = np_arr @ np_arr # 纯 CPU 计算
# PyTorch:轻松切换 GPU
if torch.cuda.is_available():
pt_tensor = torch.randn(1000, 1000).cuda()
pt_result = pt_tensor @ pt_tensor # GPU 并行计算
print(f"GPU 上计算,设备: {pt_result.device}")
elif torch.backends.mps.is_available():
pt_tensor = torch.randn(1000, 1000).mps()
pt_result = pt_tensor @ pt_tensor # Apple M 系列 GPU
print(f"MPS 上计算,设备: {pt_result.device}")
自动求导对比
import numpy as np
import torch
# NumPy:不支持自动求导
np_a = np.array([1.0, 2.0, 3.0])
np_b = np.array([4.0, 5.0, 6.0])
# 如果要计算梯度,需要手动实现反向传播
# PyTorch:原生支持自动求导
pt_a = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
pt_b = torch.tensor([4.0, 5.0, 6.0], requires_grad=True)
pt_c = pt_a * pt_b # 前向传播
pt_loss = pt_c.sum() # 计算损失
pt_loss.backward() # 反向传播
print(f"pt_a 的梯度: {pt_a.grad}")
2.4 什么时候用哪个?
选择指南
- 使用 NumPy 的场景:
- 数据预处理(Excel、CSV 数据分析)
- 非深度学习的科学计算
- 快速的原型验证(不考虑 GPU)
- 与传统机器学习算法结合(Scikit-learn)
- 使用 PyTorch Tensor 的场景:
- 深度学习模型开发
- 需要 GPU 加速的计算
- 神经网络训练和微调
- 需要自动求导的优化问题
最佳实践
在实际项目中,通常的流程是:使用 NumPy 进行数据加载和预处理,然后转换为 PyTorch Tensor 进行模型训练。两者的转换非常方便。
第三章 张量的四元身份
3.1 每个张量都有四个核心属性
理解张量的四个核心属性,是掌握 PyTorch 的关键:
| 属性 | 含义 | 类比 |
|---|---|---|
| shape | 张量的形状/维度 | 房间的大小(长×宽×高) |
| dtype | 元素的数据类型 | 房间地板的材质(木/砖/大理石) |
| device | 存储设备 | 房间所在的楼层(CPU一楼/GPU二楼) |
| layout | 内存布局方式 | 家具的排列方式(连续/稀疏) |
3.2 shape - 张量的形状
shape 描述了张量在每个维度上的大小。
import torch
# 标量(0 维)
s = torch.tensor(42)
print(f"标量: value={s}, shape={s.shape}") # torch.Size([])
# 向量(1 维)
v = torch.tensor([1, 2, 3, 4, 5])
print(f"向量: value={v}, shape={v.shape}") # torch.Size([5])
# 矩阵(2 维)
m = torch.randn(3, 4) # 3行4列
print(f"矩阵: shape={m.shape}") # torch.Size([3, 4])
# 3D 张量
t3d = torch.randn(2, 3, 4) # 2个3x4的矩阵
print(f"3D 张量: shape={t3d.shape}") # torch.Size([2, 3, 4])
# 4D 张量(常见于图像)
# batch_size × channels × height × width
batch = torch.randn(8, 3, 224, 224) # 8张RGB图像,每张224x224
print(f"4D 图像张量: shape={batch.shape}") # torch.Size([8, 3, 224, 224])
shape 的维度顺序记忆口诀
图像张量:BCHW(Batch / Channel / Height / Width)
视频张量:BCTHW(Batch / Channel / Time / Height / Width)
3.3 dtype - 张量的数据类型
dtype 指定了张量中每个元素的数值类型。
| dtype | 说明 | 内存占用 | 适用场景 |
|---|---|---|---|
| torch.float32 / torch.float | 32位浮点数 | 4 bytes | 默认,神经网络权重和计算 |
| torch.float64 / torch.double | 64位浮点数 | 8 bytes | 高精度科学计算 |
| torch.float16 / torch.half | 16位浮点数 | 2 bytes | 混合精度训练、节省显存 |
| torch.bfloat16 | Brain Float 16 | 2 bytes | 大模型训练,与 float32 指数范围相同 |
| torch.int32 / torch.int | 32位整数 | 4 bytes | 一般整数运算 |
| torch.int64 / torch.long | 64位整数 | 8 bytes | 索引、数组大小 |
| torch.int16 / torch.short | 16位整数 | 2 bytes | 节省内存 |
| torch.int8 | 8位整数 | 1 byte | 量化、字符编码 |
| torch.uint8 | 无符号8位整数 | 1 byte | 图像像素(0-255) |
| torch.bool | 布尔类型 | 1 byte | 条件判断、掩码 |
import torch
# 创建不同 dtype 的张量
t_float32 = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
t_float64 = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float64)
t_int32 = torch.tensor([1, 2, 3], dtype=torch.int32)
t_int64 = torch.tensor([1, 2, 3], dtype=torch.int64)
t_uint8 = torch.tensor([0, 127, 255], dtype=torch.uint8) # 图像像素
t_bool = torch.tensor([True, False, True], dtype=torch.bool)
print(f"float32: {t_float32.dtype}")
print(f"float64: {t_float64.dtype}")
print(f"int32: {t_int32.dtype}")
print(f"int64: {t_int64.dtype}")
print(f"uint8: {t_uint8.dtype}")
print(f"bool: {t_bool.dtype}")
# dtype 转换
t_converted = t_int32.float() # 转为 float32
print(f"转换后 dtype: {t_converted.dtype}")
3.4 device - 张量的存储设备
device 指定了张量存储在哪种硬件设备上。
| device | 说明 | 示例 |
|---|---|---|
| cpu | CPU 内存 | 默认设备 |
| cuda:0, cuda:1, ... | NVIDIA GPU | GPU 加速计算 |
| mps | Apple M 系列芯片 GPU | Mac 设备加速 |
| xpu | Intel GPU | Intel 显卡 |
import torch
# 查看当前可用的设备
print(f"CUDA 可用: {torch.cuda.is_available()}")
print(f"MPS 可用: {torch.backends.mps.is_available()}")
# 默认在 CPU 上
t_cpu = torch.tensor([1, 2, 3])
print(f"默认设备: {t_cpu.device}") # cpu
# 创建时指定设备
if torch.cuda.is_available():
t_gpu = torch.tensor([1, 2, 3], device='cuda')
print(f"GPU 设备: {t_gpu.device}") # cuda:0
# 设备切换
t = torch.randn(3, 4)
print(f"原始设备: {t.device}") # cpu
if torch.cuda.is_available():
t_gpu = t.cuda() # 或 t.to('cuda')
print(f"GPU 设备: {t_gpu.device}") # cuda:0
t_back = t_gpu.cpu() # 切回 CPU
print(f"切回 CPU: {t_back.device}")
3.5 layout - 张量的内存布局
layout 描述了张量在内存中的存储方式。
| layout | 说明 | 适用场景 |
|---|---|---|
| torch.strided | 连续内存块,按步长访问(默认) | 大多数场景,高效存储和计算 |
| torch.sparse_coo | 稀疏坐标格式,只存储非零元素 | 稀疏数据(如词向量、图数据) |
| torch.sparse_csr | 稀疏压缩行格式 | 大型稀疏矩阵 |
| torch.mkldnn | Intel MKL-DNN 格式 | 特定优化场景 |
import torch
# 默认的 strided 布局
t = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(f"默认布局: {t.layout}") # torch.strided
# 创建稀疏张量(COO 格式)
indices = torch.tensor([[0, 1, 2], [0, 1, 2]]) # 非零元素的索引
values = torch.tensor([1, 2, 3]) # 非零元素的值
size = (3, 3)
sparse_t = torch.sparse_coo_tensor(indices, values, size)
print(f"稀疏张量布局: {sparse_t.layout}")
print(f"稀疏张量值:\n{sparse_t.to_dense()}")
3.6 四元身份的综合示例
import torch
# 查看一个张量的完整四元身份
t = torch.randn(2, 3, 4, dtype=torch.float32, device='cpu', layout=torch.strided)
print("=" * 50)
print("张量的四元身份")
print("=" * 50)
print(f"1. shape (形状): {t.shape}") # torch.Size([2, 3, 4])
print(f"2. dtype (类型): {t.dtype}") # torch.float32
print(f"3. device (设备): {t.device}") # cpu
print(f"4. layout (布局): {t.layout}") # torch.strided
print(f"5. 元素总数: {t.numel()}") # 2*3*4 = 24
print(f"6. 内存占用: {t.element_size() * t.numel()} bytes")
第三章总结:张量的四元身份
- shape:描述维度大小,用 torch.Size([...]) 表示
- dtype:描述数据类型,常见有 float32/float16/int64/bool
- device:描述存储设备,常见有 cpu/cuda/mps
- layout:描述内存布局,常见有 strided(默认)/ sparse
- 这四个属性决定了张量的全部特性
第四章 张量的创建方式
4.1 直接从数据创建
最直接的方式是从 Python 列表或 NumPy 数组创建张量。
import torch
import numpy as np
# 从 Python 列表创建
t1 = torch.tensor([1, 2, 3, 4, 5])
print(f"从列表创建: {t1}")
# 从二维列表创建
t2 = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(f"从二维列表创建:\n{t2}")
# 从 NumPy 数组创建
np_arr = np.array([[1.0, 2.0], [3.0, 4.0]])
t3 = torch.from_numpy(np_arr)
print(f"从 NumPy 创建:\n{t3}")
# 指定 dtype
t4 = torch.tensor([1, 2, 3], dtype=torch.float32)
print(f"指定 dtype: {t4.dtype}")
torch.tensor() vs torch.Tensor() vs torch.as_tensor() 的区别
- torch.tensor():推荐使用,复制数据,默认创建 leaf tensor
- torch.Tensor():是 torch.FloatTensor() 的别名,创建指定类型的张量
- torch.as_tensor():尽量共享内存(但不保证),适合大数组优化
import torch
import numpy as np
# torch.tensor() - 总是复制数据
data = [1, 2, 3]
t1 = torch.tensor(data)
print(f"torch.tensor() 创建: id={id(t1)}")
# torch.Tensor() - 创建 FloatTensor,等同于 torch.FloatTensor()
t2 = torch.Tensor([1, 2, 3]) # 自动推断 dtype 为 float32
print(f"torch.Tensor() 创建: dtype={t2.dtype}")
# torch.as_tensor() - 尽量共享内存
arr = np.array([1, 2, 3])
t3 = torch.as_tensor(arr)
t4 = torch.as_tensor(arr)
print(f"as_tensor 共享内存: {np.shares_memory(arr, t3.numpy())}")
4.2 创建特殊值的张量
import torch
# 全 0 张量
zeros_1d = torch.zeros(5)
zeros_2d = torch.zeros(3, 4)
print(f"全 0 向量: {zeros_1d}")
print(f"全 0 矩阵 shape: {zeros_2d.shape}")
# 全 1 张量
ones = torch.ones(2, 3)
print(f"全 1 矩阵:\n{ones}")
# 指定填充值
full = torch.full((2, 3), fill_value=99)
print(f"填充 99:\n{full}")
# 单位矩阵
eye = torch.eye(4)
print(f"单位矩阵:\n{eye}")
# 对角矩阵
diag = torch.diag(torch.tensor([1, 2, 3]))
print(f"对角矩阵:\n{diag}")
4.3 创建随机数张量
import torch
# 均匀分布 [0, 1)
rand = torch.rand(3, 4)
print(f"均匀分布 [0,1):\n{rand}")
# 均匀分布 [a, b)
randint = torch.randint(low=1, high=10, size=(3, 4))
print(f"整数均匀分布 [1,10):\n{randint}")
# 标准正态分布 N(0, 1)
randn = torch.randn(3, 4)
print(f"标准正态分布:\n{randn}")
# 正态分布 N(mean, std)
normal = torch.normal(mean=5.0, std=2.0, size=(3, 4))
print(f"正态分布 N(5,2):\n{normal}")
# 生成随机排列
perm = torch.randperm(10) # 0到9的随机排列
print(f"随机排列: {perm}")
随机数种子
为了结果可复现,需要设置随机种子:
import torch
# 设置全局种子
torch.manual_seed(42)
# 每次运行结果相同
t1 = torch.rand(3, 3)
print(f"随机张量:\n{t1}")
# 设置 CUDA 种子(如果有 GPU)
if torch.cuda.is_available():
torch.cuda.manual_seed(42)
4.4 基于现有张量创建
import torch
# 创建全 0/1 张量,形状与现有张量相同
x = torch.randn(3, 4)
zeros_like = torch.zeros_like(x)
ones_like = torch.ones_like(x)
print(f"zeros_like shape: {zeros_like.shape}")
print(f"ones_like shape: {ones_like.shape}")
# 创建与现有张量相同 dtype 和 device 的张量
y = torch.randn(2, 3, dtype=torch.float64)
z = torch.zeros_like(y, dtype=torch.float32) # 可以覆盖 dtype
print(f"覆盖 dtype: {z.dtype}")
# torch.new_ones() - 保留原有属性,但新维度
t = torch.randn(2, 3)
t_new = t.new_ones(4, 5) # 保持 float32 和 device
print(f"new_ones dtype: {t_new.dtype}")
4.5 创建序列/范围张量
import torch
# torch.arange() - 包含 start,不包含 end
arange = torch.arange(start=0, end=10, step=2)
print(f"arange(0, 10, 2): {arange}") # [0, 2, 4, 6, 8]
# torch.linspace() - 线性间隔
linspace = torch.linspace(start=0, end=10, steps=5)
print(f"linspace(0, 10, 5): {linspace}") # [0, 2.5, 5, 7.5, 10]
# torch.logspace() - 对数间隔
logspace = torch.logspace(start=-3, end=3, steps=7)
print(f"logspace: {logspace}")
第五章 索引与切片
5.1 基本索引
PyTorch 支持与 Python 列表和 NumPy 数组类似的索引方式。
import torch
# 创建 2D 张量
t = torch.tensor([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print(f"原始张量:\n{t}")
# 索引单个元素(0-based)
print(f"t[0, 0] = {t[0, 0]}") # 1
print(f"t[2, 3] = {t[2, 3]}") # 12
# 负索引(从后往前)
print(f"t[-1, -1] = {t[-1, -1]}") # 12
# 获取行
print(f"第一行: {t[0]}") # [1, 2, 3, 4]
print(f"最后一行: {t[-1]}") # [9, 10, 11, 12]
# 获取列(需要用 :)
print(f"第一列: {t[:, 0]}") # [1, 5, 9]
5.2 切片操作
import torch
t = torch.arange(1, 13).reshape(3, 4)
print(f"原始张量:\n{t}")
# 切片语法:[start:stop:step]
print(f"前两行:\n{t[:2]}") # 行 0, 1
print(f"后两列:\n{t[:, 2:]}") # 列 2, 3
# 跳步切片
print(f"奇数行:\n{t[::2]}") # 行 0, 2
# 逆序
print(f"逆序行:\n{t[::-1]}") # 行 2, 1, 0
print(f"逆序列:\n{t[:, ::-1]}") # 列 3, 2, 1, 0
# 3D 张量切片
t3d = torch.arange(1, 25).reshape(2, 3, 4)
print(f"\n3D 张量 shape: {t3d.shape}")
print(f"第一个矩阵:\n{t3d[0]}")
print(f"所有矩阵的第二行:\n{t3d[:, 1, :]}")
5.3 高级索引
import torch
t = torch.arange(1, 13).reshape(3, 4)
print(f"原始张量:\n{t}")
# 整数数组索引
rows = torch.tensor([0, 2])
cols = torch.tensor([1, 3])
print(f"索引 [0,2] x [1,3]:\n{t[rows, cols]}")
# 布尔索引(掩码)
mask = t > 6
print(f"大于 6 的掩码:\n{mask}")
print(f"满足条件的值: {t[mask]}")
# 使用条件筛选
t_filtered = t[t % 2 == 0] # 所有偶数
print(f"所有偶数: {t_filtered}")
# torch.where() 条件替换
result = torch.where(t > 6, t, t * 10)
print(f"大于6保留,否则乘10:\n{result}")
第六章 广播机制
6.1 什么是广播?(What)
广播(Broadcasting)是 NumPy 和 PyTorch 中一种强大的机制,允许在不同形状的张量之间进行逐元素运算,而无需显式复制数据。
Why — 为什么需要广播?
想象你要把一个偏置向量加到矩阵的每一行:
- 传统做法:复制偏置向量到矩阵形状,耗时耗内存
- 广播机制:让小张量"扩展"匹配大张量形状,自动完成计算
6.2 广播规则
PyTorch/NumPy 的广播规则:从右向左比较维度,满足以下条件可以广播:
- 两个维度相同
- 其中一个维度为 1
import torch
# 案例1:标量 + 向量
a = 10
b = torch.tensor([1, 2, 3])
result = a + b
print(f"标量 + 向量: {result}") # [11, 12, 13]
# 案例2:向量 + 矩阵
vec = torch.tensor([1, 2, 3]) # shape: (3,)
mat = torch.tensor([[1, 2, 3], # shape: (2, 3)
[4, 5, 6]])
result = vec + mat # vec 自动扩展为 (1, 3) 再广播到 (2, 3)
print(f"向量 + 矩阵:\n{result}")
# 案例3:添加批次维度
batch = torch.randn(8, 3, 32, 32) # 8张图像
bias = torch.tensor([0.1, 0.2, 0.3]) # 3个通道的偏置
result = batch + bias
print(f"批次 + 偏置 result shape: {result.shape}")
6.3 广播的可视化理解
案例:向量 (3,) + 矩阵 (2, 3)
向量: [1, 2, 3] → 扩展为 (1, 3)
矩阵: [[1, 2, 3], (2, 3)
[4, 5, 6]]
对齐后: [[1, 2, 3], + [[1, 2, 3],
[4, 5, 6]] [[1, 2, 3]]
= [[2, 4, 6],
[5, 7, 9]]
6.4 广播的注意事项
无法广播的情况
当维度大小不相同且都不是 1 时,无法广播:
import torch
a = torch.randn(3, 4) # shape: (3, 4)
b = torch.randn(2, 4) # shape: (2, 4)
# a + b # RuntimeError: 不能广播!
错误原因:第一个维度 3 ≠ 2,且都不是 1。
import torch
# 解决方案:用 unsqueeze 手动扩展维度
a = torch.randn(3, 4)
b = torch.randn(2, 4)
# 方法:确保维度兼容
result = a[:2] + b # 取 a 的前两行
# 或用 expand 扩展
a_expanded = a.unsqueeze(0).expand(2, -1, -1) # (2, 3, 4)
result = a_expanded + b.unsqueeze(0) # (2, 3, 4)
6.5 广播实战:图像处理
import torch
# 批量图像归一化
batch = torch.randn(16, 3, 224, 224) # 16张 RGB 图像
mean = torch.tensor([0.485, 0.456, 0.406]) # ImageNet 均值
std = torch.tensor([0.229, 0.224, 0.225]) # ImageNet 标准差
# 广播到每张图像的每个像素
normalized = (batch - mean.view(1, 3, 1, 1)) / std.view(1, 3, 1, 1)
print(f"归一化后 shape: {normalized.shape}") # (16, 3, 224, 224)
第五章和第六章总结
- 索引:从 0 开始,支持负索引
- 切片:使用 start:stop:step 语法
- 布尔索引:用于条件筛选
- 广播规则:从右向左比较维度,相等或为 1 即可广播
- 广播优势:节省内存,自动扩展,无需手动复制
第七章 自动求导机制
7.1 先搞懂几个核心概念(通俗理解)
在学习自动求导之前,先来理解几个基础概念。
7.1.1 什么是导数?(What)
导数描述的是"一个量变化时,另一个量跟着怎么变化"。
通俗理解导数
想象你开车:
- 速度 = 位置对时间的导数(位置变化得多快)
- 加速度 = 速度对时间的导数(速度变化得多快)
数学例子:
- y = x²,当 x=3 时,导数 dy/dx = 2x = 6
- 意思是:x 增加 1,y 大约增加 6
这和高数里的导数是同一个概念!
如果你学过高等数学,应该记得这些常见函数的导数:
| 函数 f(x) | 导数 f'(x) |
|---|---|
| x² | 2x |
| sin(x) | cos(x) |
| cos(x) | -sin(x) |
| e&supx; | e&supx; |
| ln(x) | 1/x |
为什么深度学习要用导数?
神经网络训练的目标是最小化损失函数。假设损失 L = (预测值 - 真实值)²,我们要知道"每个参数怎么调能让损失变小"——这就需要对每个参数求偏导,得到梯度。
PyTorch 的作用:神经网络可能有上亿个参数,手动求导不现实。PyTorch 的 autograd 自动帮你求导,只需调用 .backward() 即可。
7.1.2 什么是梯度?(What)
梯度是导数在多维空间的推广,可以理解为"山坡最陡的方向"。
通俗理解梯度
想象你站在山坡上:
- 梯度向量指向山坡最陡的上坡方向
- 你要下山,就要往梯度的反方向走
- 梯度的大小表示山坡有多陡
在神经网络中:
- 我们想知道"如何调整参数"能让损失变小
- 梯度告诉我们参数应该往哪个方向调整
高数里的梯度定义
梯度是多元函数的偏导数组成的向量。
对于函数 f(x, y, z) = x² + y² + z²,梯度为:
∇f = (∂f/∂x, ∂f/∂y, ∂f/∂z) = (2x, 2y, 2z)
梯度的重要性质:
- 梯度向量指向函数增长最快的方向
- 梯度的反方向(负梯度)是函数下降最快的方向
- 梯度大小表示变化的剧烈程度
为什么叫"梯度"而不是"导数"?
一元函数叫"导数",多元函数叫"梯度"。神经网络有上万个参数,所以用"梯度"更准确。
7.1.3 什么是反向传播?(What)
反向传播(Backpropagation)是计算梯度的算法,让神经网络的"学习"成为可能。
通俗理解反向传播
想象你在考试中做错了题:
- 前向传播:从输入到输出,就像做题得到分数
- 反向传播:从分数倒推,找出每道题扣分的原因
- 根据扣分原因,调整学习方法(参数更新)
在神经网络中:
- 前向传播:输入数据 → 经过层层计算 → 得到预测结果
- 计算损失:预测结果 vs 真实结果,差距有多大
- 反向传播:从损失开始,反向计算每个参数对损失"贡献"了多少
反向传播的起源
反向传播算法由 David Rumelhart、Geoffrey Hinton、Ronald Williams 于 1986 年在论文《Learning representations by back-propagating errors》中提出。这是深度学习能够训练深层神经网络的关键突破。
反向传播的核心原理:链式法则
反向传播本质上是高数中的链式法则在神经网络中的应用。
假设有一个复合函数:y = f(g(x))
根据链式法则:dy/dx = dy/du · du/dx
在神经网络中,层与层之间的计算都是这种嵌套关系,反向传播就是从输出层开始,一层层用链式法则求出每个参数对损失的偏导数。
损失 L = loss(y_out, y_true)
反向传播过程:
∂L/∂W_n = ∂L/∂y_n · ∂y_n/∂W_n (输出层)
∂L/∂W_{n-1} = ∂L/∂y_n · ∂y_n/∂y_{n-1} · ∂y_{n-1}/∂W_{n-1} (隐藏层)
...逐层往前传播...
与高数概念的关系:
- 梯度:高数里的多元函数偏导数向量(数学基础)
- 链式法则:高数里的复合函数求导法则(数学工具)
- 反向传播:深度学习专用算法,结合梯度+链式法则+神经网络结构
7.1.4 什么是计算图?(What)
计算图是 PyTorch 用来记录"数据是怎么算出来的"的工具。
计算图示例:y = (a * b + c) * d
a ──┐
b ──┼──→ (*) ──→ (+) ──→ (*) ──→ y
c ──┘ ↑
d ─────────────────────────┘
步骤:
1. a * b = e
2. e + c = f
3. f * d = y
反向传播时:
1. dy/dd = f
2. dy/df = d
3. dy/de = d, dy/dc = d
4. dy/da = b * d, dy/db = a * d
计算图的作用
计算图就像一张"施工图纸",记录了:
- 数据经过了哪些运算
- 每个运算的输入是什么
- 如何从输出反推每个输入的"贡献"(梯度)
7.1.5 什么是梯度追踪?(What)
梯度追踪是 PyTorch 记录计算图的能力,只有被追踪的张量才能计算梯度。
import torch
# 默认不追踪梯度
a = torch.tensor([1.0])
print(f"默认 requires_grad: {a.requires_grad}") # False
# 开启梯度追踪
b = torch.tensor([2.0], requires_grad=True)
print(f"开启后 requires_grad: {b.requires_grad}") # True
# 追踪的计算会被记录
c = b * 3 # 这个运算会被追踪
print(f"运算后的 grad_fn: {c.grad_fn}") # 记录了是乘法
为什么需要梯度追踪?
- 节省内存:不需要追踪的参数不记录计算图
- 提高效率:推理时不需要梯度,关闭追踪加速
- 灵活控制:只想算某些张量的梯度
7.2 什么是自动求导?(What)
自动求导(Automatic Differentiation)是 PyTorch 的核心特性,它能自动计算函数的导数(梯度),无需手动编写求导代码。
Why — 为什么需要自动求导?
神经网络可能有上亿个参数,手动求导是不可能的!
- 手动求导:每个参数都要写公式,亿级参数 = 不可能
- 自动求导:PyTorch 自动构建计算图,一键 backward() 求导
7.3 requires_grad 属性
requires_grad 是张量的关键属性,控制是否追踪梯度。
import torch
# 创建默认不追踪梯度的张量
a = torch.tensor([1.0, 2.0, 3.0])
print(f"requires_grad 默认: {a.requires_grad}") # False
# 创建追踪梯度的张量
b = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
print(f"requires_grad=True: {b.requires_grad}") # True
# 方式1:创建时指定
c = torch.ones(3, requires_grad=True)
# 方式2:使用 requires_grad_() 修改
d = torch.ones(3)
d.requires_grad_(True)
print(f"修改后: {d.requires_grad}") # True
7.3 grad_fn 反向传播函数
grad_fn 记录了创建这个张量的运算操作,用于反向传播时计算梯度。
import torch
# 通过运算创建的张量会自动记录 grad_fn
a = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
b = a * 2
print(f"b 的 grad_fn: {b.grad_fn}") #
# 叶子节点 vs 非叶子节点
print(f"a 是叶子节点: {a.is_leaf}") # True
print(f"b 是叶子节点: {b.is_leaf}") # False
# grad_fn 的常见类型
# AddBackward0 - 加法
# SubBackward0 - 减法
# MulBackward0 - 乘法
# DivBackward0 - 除法
# MatMulBackward0 - 矩阵乘法
# SumBackward0 - 求和
# ReluBackward0 - ReLU 激活
7.4 反向传播实例
import torch
# 示例:计算 y = (a * b + c) * d 的梯度
a = torch.tensor([1.0], requires_grad=True)
b = torch.tensor([2.0], requires_grad=True)
c = torch.tensor([0.0], requires_grad=True)
d = torch.tensor([3.0], requires_grad=True)
# 前向传播
y = (a * b + c) * d
print(f"y = {y}") # y = (1*2 + 0) * 3 = 6
# 反向传播
y.backward()
print(f"dy/da = {a.grad}") # dy/da = d(b*d)/da = 2*3 = 6
print(f"dy/db = {b.grad}") # dy/db = d(a*d)/db = 1*3 = 3
print(f"dy/dc = {c.grad}") # dy/dc = d*1 = 3
print(f"dy/dd = {d.grad}") # dy/dd = a*b + c = 2
重要:梯度会累积
调用 backward() 会累积梯度到 .grad 属性,而不是替换。如果需要重新计算梯度,先用 optimizer.zero_grad() 清零。
# 梯度累积示例
loss.backward() # 第一次
print(f"梯度累积: {x.grad}") # 有值
loss.backward() # 第二次
print(f"梯度翻倍: {x.grad}") # 值翻倍了!
# 正确做法:每次反向传播前清零
optimizer.zero_grad()
loss.backward()
optimizer.step()
7.5 梯度计算的高级用法
import torch
# 多次梯度计算
x = torch.tensor([2.0], requires_grad=True)
y = x ** 2 # y = x^2
# 第一次求导:dy/dx = 2x = 4
gradients = torch.autograd.grad(y, x)
print(f"dy/dx = {gradients[0]}") # tensor([4.])
# 第二次求导:d²y/dx² = 2
gradients_2nd = torch.autograd.grad(gradients[0], x)
print(f"d²y/dx² = {gradients_2nd[0]}") # tensor([2.])
# 梯度不追踪(no_grad)
x = torch.tensor([1.0], requires_grad=True)
with torch.no_grad():
y = x * 2 # 这个运算不会追踪梯度
print(f"no_grad 中: {y.requires_grad}") # False
# detach - 切断梯度追踪
x = torch.tensor([1.0], requires_grad=True)
y = x * 2
z = y.detach() # 创建一个新的张量,切断与计算图的连接
print(f"detach 后: {z.requires_grad}") # False
第八章 设备迁移
8.1 什么是设备迁移?(What)
设备迁移是指将张量在不同硬件设备(CPU、GPU、MPS)之间移动。GPU 可以大幅加速张量运算。
Why — 为什么需要设备迁移?
深度学习训练需要大量矩阵运算:
- CPU:适合小数据量,灵活但速度慢
- GPU:并行计算能力强,适合大规模矩阵运算,加速 10-100 倍
- MPS:Apple M 系列芯片的 GPU 方案
8.2 检查可用设备
import torch
# 检查 CUDA 是否可用
print(f"CUDA 可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
# GPU 信息
print(f"GPU 数量: {torch.cuda.device_count()}")
print(f"当前 GPU: {torch.cuda.current_device()}")
print(f"GPU 名称: {torch.cuda.get_device_name(0)}")
print(f"CUDA 版本: {torch.version.cuda}")
# 检查 MPS 是否可用(Apple Silicon)
print(f"MPS 可用: {torch.backends.mps.is_available()}")
if torch.backends.mps.is_available():
print(f"MPS 可构建: {torch.backends.mps.is_built()}")
8.3 设备迁移方法
import torch
# 默认在 CPU 上创建
t_cpu = torch.randn(3, 4)
print(f"默认设备: {t_cpu.device}") # cpu
# 方式1:.cuda() / .cpu()
if torch.cuda.is_available():
t_gpu = t_cpu.cuda() # 移到 GPU
t_back = t_gpu.cpu() # 移回 CPU
# 方式2:.to(device)
t = torch.randn(3, 4)
if torch.cuda.is_available():
t_gpu = t.to('cuda')
t_gpu = t.to(device='cuda:0') # 指定 GPU 编号
# 方式3:创建时指定设备
if torch.cuda.is_available():
t = torch.randn(3, 4, device='cuda')
print(f"创建时指定设备: {t.device}")
# MPS(Apple Silicon)
if torch.backends.mps.is_available():
t_mps = t_cpu.to('mps')
print(f"MPS 设备: {t_mps.device}")
8.4 设备迁移的注意事项
设备不匹配错误
常见的错误是在 CPU 和 GPU 之间混合运算:
import torch
a = torch.randn(3, 4) # CPU
if torch.cuda.is_available():
b = torch.randn(3, 4).cuda() # GPU
# c = a + b # RuntimeError! CPU + GPU 不兼容
# 解决方案:确保在同一设备上
c = a + b.cpu() # 移回 CPU
if torch.cuda.is_available():
c = a.cuda() + b # 都移到 GPU
8.5 跨设备迁移的最佳实践
import torch
# 最佳实践1:设置默认设备
if torch.cuda.is_available():
torch.cuda.set_device(0) # 设置默认 GPU
# 最佳实践2:使用 device 上下文管理器
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
# 创建张量时指定设备
t = torch.randn(3, 4, device=device)
# 迁移模型到设备
model = torch.nn.Linear(4, 2)
model = model.to(device)
# 批次数据迁移
batch = torch.randn(16, 4)
batch = batch.to(device)
output = model(batch)
print(f"输出设备: {output.device}")
第九章 内存布局
9.1 什么是内存布局?(What)
内存布局(Memory Layout)描述了张量数据在内存中的存储方式。理解内存布局对于优化性能至关重要。
Why — 为什么理解内存布局重要?
- 连续内存:访问效率高,某些操作需要连续内存
- 非连续内存:切片、transpose 后会产生,访问效率低
- 了解布局可以避免不必要的内存拷贝
9.2 stride - 步长
stride 表示在某个维度上移动到下一个元素需要跳过的元素数量。
import torch
# 创建一个 2D 张量
t = torch.arange(12).reshape(3, 4)
print(f"张量:\n{t}")
print(f"shape: {t.shape}") # (3, 4)
print(f"stride: {t.stride()}") # (4, 1)
# 内存理解:
# stride[0] = 4:跳到下一行需要跳过 4 个元素
# stride[1] = 1:跳到下一列需要跳过 1 个元素
# 对比:行优先 vs 列优先
t_row = torch.arange(12).reshape(3, 4)
t_col = t_row.T # 转置
print(f"\n行优先 stride: {t_row.stride()}") # (4, 1)
print(f"列优先 stride: {t_col.stride()}") # (1, 4)
stride 的直观理解
stride 就像一个"地图",告诉我们如何用"步长"在内存中导航:
- stride[0] = 4:往下走一步 = 跳过 4 个元素
- stride[1] = 1:往右走一步 = 跳过 1 个元素
9.3 contiguous vs 非连续
contiguous 表示张量在内存中是连续存储的。
import torch
# 连续张量
t = torch.arange(12).reshape(3, 4)
print(f"连续: {t.is_contiguous()}") # True
# 非连续张量(通过切片)
t_slice = t[:, ::2] # 取偶数列
print(f"切片后连续: {t_slice.is_contiguous()}") # False
print(f"切片后 stride: {t_slice.stride()}") # (4, 2)
# transpose 也会产生非连续张量
t_t = t.T
print(f"转置后连续: {t_t.is_contiguous()}") # False
9.4 内存布局的性能影响
import torch
t = torch.randn(1000, 1000)
# 连续访问 vs 非连续访问
t_cont = t.clone() # 连续
t_noncont = t[:, ::2] # 非连续
# view() 需要连续内存
# t_noncont.view(500, 1000) # RuntimeError!
# 解决方案:调用 contiguous()
t_contiguous = t_noncont.contiguous()
print(f"contiguous 后: {t_contiguous.is_contiguous()}") # True
print(f"contiguous 后 shape: {t_contiguous.shape}") # (1000, 500)
# reshape() 更灵活,内部会自动处理
result = t_noncont.reshape(500, 1000) # 不会报错
9.5 memory_format - 内存格式
memory_format 指定张量的内存排列方式。
import torch
# torch.contiguous_format - 标准行优先
t1 = torch.randn(2, 3, 224, 224)
print(f"默认格式: {t1.memory_format}") # torch.contiguous_format
# torch.channels_last - NHWC 格式(适合某些 GPU 操作)
t_cl = t1.to(memory_format=torch.channels_last)
print(f"channels_last 格式: {t_cl.memory_format}")
# 验证 channels_last 的优势
print(f"默认 stride: {t1.stride()}") # (150528, 224, 1, 50176)
print(f"channels_last stride: {t_cl.stride()}") # (301056, 1, 150528, 224)
第七到第九章总结
- 自动求导:requires_grad 开启追踪,backward() 求导,grad_fn 记录运算
- 设备迁移:.to(device) 跨设备迁移,确保操作在同一设备上
- 内存布局:stride 控制内存访问,is_contiguous() 检查连续性,contiguous() 确保连续
第十章 类型转换
10.1 dtype 转换方法
PyTorch 提供了多种 dtype 转换方法。
import torch
# 创建不同类型的张量
t_int = torch.tensor([1, 2, 3], dtype=torch.int32)
t_float = torch.tensor([1.5, 2.5, 3.5])
# 方法1:.dtype 属性访问
print(f"当前 dtype: {t_int.dtype}") # torch.int32
# 方法2:直接指定 dtype 创建
t_new = torch.tensor([1, 2, 3], dtype=torch.float32)
# 方法3:类型转换方法
print(f"int32 -> float32: {t_int.float()}") # torch.float32
print(f"float32 -> int32: {t_float.int()}") # torch.int32
print(f"float32 -> int64: {t_float.long()}") # torch.int64
print(f"int64 -> float32: {t_int.to(torch.float32)}") # 通用方法
# 方法4:使用 .type() 方法
t = torch.tensor([1, 2, 3])
t_float = t.type(torch.float32)
print(f"type() 转换: {t_float.dtype}") # torch.float32
10.2 常用 dtype 转换场景
import torch
# 场景1:float32 与 float16(混合精度训练)
t_fp32 = torch.randn(3, 4)
t_fp16 = t_fp32.half() # 转 float16,节省显存
t_back_fp32 = t_fp16.float() # 转回 float32
print(f"fp32 -> fp16 -> fp32: {t_fp32.shape}")
# 场景2:float32 与 bfloat16(大模型训练)
t_bf16 = t_fp32.bfloat16() # 转 bfloat16
print(f"bfloat16 dtype: {t_bf16.dtype}")
# 场景3:int 与 float 互转
t_int = torch.tensor([1, 2, 3])
t_float = t_int.float() # int -> float
t_back_int = t_float.long() # float -> int(截断)
print(f"int -> float: {t_float}, float -> int: {t_back_int}")
# 场景4:bool 与数值类型
t_bool = torch.tensor([True, False, True])
t_int = t_bool.int() # True -> 1, False -> 0
print(f"bool -> int: {t_int}") # [1, 0, 1]
float16 vs bfloat16 的选择
- float16(FP16):显存节省一半,适合 RTX/消费级 GPU
- bfloat16(BF16):与 FP32 相同的指数范围,数值稳定性更好,适合大模型训练
10.3 精度损失注意事项
类型转换可能丢失精度
import torch
# float32 -> int 会截断小数部分
t = torch.tensor([1.7, 2.5, 3.9])
print(f"float -> int: {t.int()}") # [1, 2, 3]
# 大数值 float32 -> float16 可能溢出
t_large = torch.tensor([65500.0, 66000.0])
t_fp16 = t_large.half()
print(f"大数值转 fp16: {t_fp16}") # 可能出现 inf 或精度损失
# 检查数值是否在范围内
print(f"fp16 最大值: {torch.finfo(torch.float16).max}") # 65504
第十一章 与 NumPy 互转
11.1 张量转 NumPy 数组
import torch
import numpy as np
# 张量 -> NumPy
t = torch.randn(3, 4)
arr = t.numpy() # 返回共享内存的视图
print(f"张量 dtype: {t.dtype}")
print(f"NumPy dtype: {arr.dtype}")
# 注意:返回的是视图,共享内存
t[0, 0] = 999
print(f"修改张量后 NumPy: {arr[0, 0]}") # 也是 999!
# CPU 张量直接转换
t_cpu = torch.tensor([1.0, 2.0, 3.0])
arr_cpu = t_cpu.numpy()
print(f"NumPy 数组: {arr_cpu}")
11.2 NumPy 数组转张量
import torch
import numpy as np
# 方法1:torch.from_numpy() - 共享内存
arr = np.array([1, 2, 3, 4])
t = torch.from_numpy(arr)
print(f"from_numpy: {t}")
# 修改 NumPy 后张量也变(共享内存)
arr[0] = 999
print(f"修改 NumPy 后张量: {t[0]}") # 也是 999
# 方法2:torch.tensor() - 复制数据
arr = np.array([1, 2, 3, 4])
t = torch.tensor(arr) # 复制一份
arr[0] = 888
print(f"修改 NumPy 后张量: {t[0]}") # 还是 1,不变!
# 方法3:torch.as_tensor() - 尽量共享内存(不保证)
arr = np.array([1, 2, 3])
t = torch.as_tensor(arr)
arr[0] = 777
print(f"as_tensor: {t[0]}") # 可能变成 777
11.3 GPU 张量的 NumPy 转换
import torch
import numpy as np
# GPU 张量不能直接转 NumPy,需要先移回 CPU
if torch.cuda.is_available():
t_gpu = torch.randn(3, 4).cuda()
# t_gpu.numpy() # RuntimeError!
# 正确做法:先转 CPU
t_cpu = t_gpu.cpu()
arr = t_cpu.numpy()
print(f"GPU -> CPU -> NumPy: {arr.shape}")
# 封装成工具函数
def tensor_to_numpy(t):
"""安全地将张量转为 NumPy 数组"""
if t.device.type == 'cuda':
t = t.cpu()
return t.numpy()
if torch.cuda.is_available():
t_gpu = torch.randn(3, 4).cuda()
arr = tensor_to_numpy(t_gpu)
print(f"安全转换: {arr.dtype}")
11.4 互转的最佳实践
import torch
import numpy as np
# 最佳实践1:数据预处理用 NumPy,模型训练用 PyTorch
def preprocess_data(np_array):
"""NumPy 数据预处理"""
mean = np_array.mean()
std = np_array.std()
normalized = (np_array - mean) / std
return torch.from_numpy(normalized).float()
def postprocess_output(tensor):
"""后处理转回 NumPy"""
return tensor.detach().cpu().numpy()
# 最佳实践2:使用 clone() 避免共享内存问题
arr = np.array([1, 2, 3])
t = torch.from_numpy(arr).clone() # 复制一份
arr[0] = 999
print(f"张量不受影响: {t[0]}") # 仍然是 1
第十二章 从文件加载张量
12.1 从 CSV 文件加载
import torch
import numpy as np
import pandas as pd
# 方法1:使用 pandas 读取 CSV,然后用 torch.tensor()
df = pd.read_csv('data.csv')
t = torch.tensor(df.values, dtype=torch.float32)
print(f"从 CSV 加载: {t.shape}")
# 方法2:使用 NumPy 读取
data = np.loadtxt('data.csv', delimiter=',')
t = torch.from_numpy(data).float()
print(f"NumPy 加载 CSV: {t.shape}")
# 方法3:逐行读取(处理大文件)
def load_csv_as_tensor(filepath):
data = []
with open(filepath, 'r') as f:
next(f) # 跳过表头
for line in f:
row = [float(x) for x in line.strip().split(',')]
data.append(row)
return torch.tensor(data)
12.2 从 NumPy 文件加载(.npy / .npz)
import torch
import numpy as np
# 保存为 .npy 文件(单个数组)
arr = np.random.randn(100, 64)
np.save('array.npy', arr)
# 加载 .npy 文件
loaded = np.load('array.npy')
t = torch.from_numpy(loaded)
print(f"加载 .npy: {t.shape}")
# 保存为 .npz 文件(多个数组)
arr1 = np.random.randn(100, 64)
arr2 = np.random.randn(100, 10)
np.savez('arrays.npz', features=arr1, labels=arr2)
# 加载 .npz 文件
data = np.load('arrays.npz')
t_features = torch.from_numpy(data['features'])
t_labels = torch.from_numpy(data['labels'])
print(f"加载 .npz features: {t_features.shape}")
12.3 从 PyTorch 文件加载(.pt / .pth)
import torch
# 保存张量
t = torch.randn(100, 64)
torch.save(t, 'tensor.pt')
# 加载张量
loaded = torch.load('tensor.pt')
print(f"加载 .pt: {loaded.shape}")
# 保存多个张量(字典形式)
state = {
'features': torch.randn(100, 64),
'labels': torch.randn(100, 10),
'epoch': 10
}
torch.save(state, 'checkpoint.pth')
# 加载
checkpoint = torch.load('checkpoint.pth')
print(f"加载 checkpoint: epoch={checkpoint['epoch']}")
# 模型参数保存和加载
model = torch.nn.Linear(64, 10)
torch.save(model.state_dict(), 'model_weights.pth')
loaded_model = torch.nn.Linear(64, 10)
loaded_model.load_state_dict(torch.load('model_weights.pth'))
12.4 从图像文件加载
import torch
from PIL import Image
import numpy as np
# 使用 PIL 读取图像,转为张量
def load_image_as_tensor(image_path):
img = Image.open(image_path).convert('RGB')
arr = np.array(img) # (H, W, C)
tensor = torch.from_numpy(arr).permute(2, 0, 1).float() / 255.0
return tensor
# 使用 torchvision(推荐)
from torchvision import transforms
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
img = Image.open('image.jpg').convert('RGB')
tensor = transform(img)
print(f"图像张量: {tensor.shape}") # (3, 224, 224)
12.5 从音频文件加载
import torch
import torchaudio
# 加载音频文件
waveform, sample_rate = torchaudio.load('audio.wav')
print(f"音频波形: {waveform.shape}") # (channels, samples)
print(f"采样率: {sample_rate}")
# 重采样
target_sr = 16000
if sample_rate != target_sr:
waveform = torchaudio.functional.resample(waveform, sample_rate, target_sr)
print(f"重采样后: {waveform.shape}")
# 转换为 Mel 频谱图
mel_spec = torchaudio.transforms.MelSpectrogram(sample_rate=target_sr)
mels = mel_spec(waveform)
print(f"Mel 频谱: {mels.shape}")
12.6 从 JSON 文件加载
import torch
import json
# 保存张量为 JSON(需要转 NumPy)
data = {
'tensor_a': torch.randn(10, 5).numpy().tolist(),
'tensor_b': torch.randint(0, 10, (5,)).numpy().tolist()
}
with open('data.json', 'w') as f:
json.dump(data, f)
# 加载 JSON
with open('data.json', 'r') as f:
loaded = json.load(f)
t_a = torch.tensor(loaded['tensor_a'])
t_b = torch.tensor(loaded['tensor_b'], dtype=torch.long)
print(f"从 JSON 加载: {t_a.shape}, {t_b.shape}")
12.7 从二进制文件加载
import torch
import numpy as np
# 读取原始二进制数据
def load_binary_tensor(filepath, dtype=np.float32, shape=(1, -1)):
with open(filepath, 'rb') as f:
data = f.read()
arr = np.frombuffer(data, dtype=dtype)
return torch.from_numpy(arr).reshape(shape)
# 保存张量为二进制
def save_binary_tensor(tensor, filepath):
arr = tensor.numpy()
arr.tofile(filepath)
# 读取自定义格式
def read_custom_binary(filepath):
import struct
with open(filepath, 'rb') as f:
header = struct.unpack('iii', f.read(12))
height, width, channels = header
data = np.frombuffer(f.read(), dtype=np.uint8)
arr = data.reshape(height, width, channels)
return torch.from_numpy(arr)
12.8 从 Hugging Face 加载
import torch
from transformers import AutoTokenizer, AutoModel
# 加载预训练模型和分词器
model_name = 'bert-base-chinese'
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
# 文本编码
text = "今天天气真好"
inputs = tokenizer(text, return_tensors='pt')
print(f"输入张量: {inputs['input_ids'].shape}")
# 获取模型输出
with torch.no_grad():
outputs = model(**inputs)
embeddings = outputs.last_hidden_state
print(f"嵌入向量: {embeddings.shape}")
第十到第十二章总结
- 类型转换:.float()/.int()/.half()/.bfloat16() 等方法
- NumPy 互转:.numpy() 转数组,torch.from_numpy() 转张量
- 文件加载:CSV 用 pandas,NPZ 用 np.load,PT 用 torch.load,图像用 torchvision
- HuggingFace:使用 transformers 库加载预训练模型
FAQ 常见问题(20 组)
关于张量基础概念
Q1. 张量和矩阵有什么区别?
一句话结论:张量是矩阵的推广,矩阵是 2 维张量。矩阵是行×列的二维数组,张量可以是任意维度:一维向量、二维矩阵、三维及以上的数组都叫张量。
Q2. PyTorch Tensor 和 NumPy 数组哪个更快?
一句话结论:在 CPU 上差不多,GPU 上 PyTorch Tensor 更快。PyTorch Tensor 支持 GPU 加速,NumPy 只能在 CPU 上运行。但对于纯 CPU 计算,两者性能差距不大。
Q3. 张量的 shape 和 shape() 有什么区别?
一句话结论:shape 是属性,shape() 是方法,效果相同。推荐使用 .shape,因为它既可以作为属性访问,也可以用于解包。
t = torch.randn(3, 4, 5)
print(t.shape) # torch.Size([3, 4, 5])
print(t.shape[0]) # 3
rows, cols = t.shape # 解包
Q4. 什么是叶子节点(leaf tensor)?
一句话结论:直接创建的、requires_grad=True 的张量是叶子节点。叶子节点的梯度会保留在 .grad 属性中,而非叶子节点的梯度在反向传播后会被释放。
关于张量创建
Q5. torch.tensor() 和 torch.Tensor() 有什么区别?
一句话结论:torch.tensor() 复制数据推断类型,torch.Tensor() 创建 float32 张量。推荐使用 torch.tensor(),它更明确且功能更丰富。
Q6. 如何创建指定形状的张量但不初始化值?
一句话结论:使用 torch.empty() 或 torch.empty_like()。但更推荐 torch.zeros()/torch.randn()。empty() 不保证初始值,可能包含随机垃圾数据。
# 不推荐:值未初始化
t = torch.empty(3, 4)
# 推荐:明确初始值
t = torch.zeros(3, 4) # 全 0
t = torch.randn(3, 4) # 随机
Q7. 如何创建一个与现有张量相同形状的张量?
一句话结论:使用 torch.zeros_like() 或 torch.ones_like()。
x = torch.randn(3, 4, dtype=torch.float64)
zeros = torch.zeros_like(x) # 形状和 dtype 都相同
ones = torch.ones_like(x)
Q8. 如何设置随机种子保证结果可复现?
一句话结论:在训练开始前设置 torch.manual_seed()。
torch.manual_seed(42)
t = torch.randn(3, 3) # 每次运行相同
关于索引和切片
Q9. 张量切片会复制数据吗?
一句话结论:切片返回视图(共享内存),不是复制。修改切片会影响原张量。如需复制,使用 clone()。
t = torch.arange(12).reshape(3, 4)
slice_t = t[0] # 视图
slice_t[0] = 999
print(t[0, 0]) # 999,被改了!
t2 = t[0].clone() # 复制
t2[0] = 888
print(t[0, 0]) # 还是 999
Q10. 如何用条件筛选张量元素?
一句话结论:使用布尔索引或 torch.where()。
t = torch.arange(10)
mask = t > 5
filtered = t[mask] # 布尔索引
# 或使用 torch.where
result = torch.where(t > 5, t, t * 0) # 大于5保留,否则置0
关于广播机制
Q11. 什么情况下不能使用广播?
一句话结论:当维度大小不同且都不为 1 时。
# 报错!
a = torch.randn(3, 4)
b = torch.randn(2, 4)
# a + b # RuntimeError
# 可以广播
a = torch.randn(1, 4)
b = torch.randn(2, 4)
c = a + b # OK
Q12. 广播是复制数据还是虚拟扩展?
一句话结论:逻辑上虚拟扩展,不实际复制数据。广播只改变索引方式,实际内存不复制,因此高效。
关于自动求导
Q13. requires_grad=True 和 False 有什么区别?
一句话结论:True 会追踪梯度,用于需要优化的参数。模型的权重通常 requires_grad=True,数据通常 False。
Q14. 为什么反向传播后梯度会消失?
一句话结论:可能是梯度为 0(sigmoid/tanh 饱和)或计算图被切断。检查是否使用了 detach()、no_grad() 或requires_grad=False。
Q15. 梯度累积是什么意思?
一句话结论:多次 backward() 会累加梯度到 .grad,不会覆盖。因此每个训练步骤前要清零梯度。
loss.backward()
optimizer.step()
optimizer.zero_grad() # 清零,准备下一步
关于设备迁移
Q16. GPU 张量可以直接打印吗?
一句话结论:可以,但建议先移到 CPU 更直观。
t_gpu = torch.randn(3, 3).cuda()
print(t_gpu) # 可以打印
# 更推荐
print(t_gpu.cpu()) # 在 CPU 上显示
Q17. 如何判断代码在 GPU 还是 CPU 上运行?
一句话结论:检查张量的 .device 属性。
t = torch.randn(3, 4)
print(t.device) # cpu
if torch.cuda.is_available():
t = t.cuda()
print(t.device) # cuda:0
Q18. Apple M 系列芯片用哪个设备?
一句话结论:使用 MPS 后端。
if torch.backends.mps.is_available():
t = torch.randn(3, 4).to('mps')
关于内存布局
Q19. 什么时候需要调用 contiguous()?
一句话结论:当需要用 view() 但张量不连续时。
t = torch.randn(3, 4)
t_slice = t[:, ::2] # 不连续
# 报错
# t_slice.view(6) # RuntimeError
# 解决
t_cont = t_slice.contiguous()
t_cont.view(6) # OK
Q20. channels_last 内存格式有什么优势?
一句话结论:某些 GPU 的卷积操作对通道优先的数据更高效。适合图像数据(NCHW vs NHWC),可以提升推理速度。
全篇总结:张量学习的核心要点
- 四元身份:shape / dtype / device / layout 决定张量全部特性
- 创建方式:从数据、特殊值、随机数、范围、现有张量多种方式
- 索引切片:0-based 索引,支持负索引和高级索引
- 广播机制:从右向左比维度,相等或为1可广播
- 自动求导:requires_grad 开启,backward() 求导,梯度累积需清零
- 设备迁移:.to(device) 切换 CPU/GPU/MPS
- 内存布局:stride 控制访问,contiguous() 确保连续
- 类型转换:.float()/.int()/.half() 等方法
- NumPy 互转:.numpy() 和 torch.from_numpy()
- 文件加载:CSV/NPZ/PT/图像/音频/HuggingFace

浙公网安备 33010602011771号