机器学习算法原理与实践-入门(七):深度学习框架PyTorch的Tensor
在前几篇文章中,我们从基础机器学习算法逐步深入到深度学习领域。今天,我们将正式进入现代深度学习的世界,学习PyTorch框架的核心数据结构------Tensor。这是理解和使用所有深度学习框架的基础,也是从传统机器学习向深度学习过渡的关键一步。
一、PyTorch:现代深度学习的标准工具
1.1 什么是PyTorch?
PyTorch是由Meta公司(原Facebook)人工智能研究团队开发和维护的开源深度学习框架。它已经成为学术界和工业界最受欢迎的深度学习框架之一。
1.2 PyTorch的核心特点
- 动态计算图:计算图在运行时构建,更加灵活直观
- 自动求导:内置自动微分系统,简化梯度计算
- GPU加速:无缝支持GPU计算,大幅提升训练速度
- 丰富的生态系统:提供完整的神经网络库和工具集
1.3 为什么选择PyTorch?
- 易用性:Python风格的API,学习曲线平缓
- 灵活性:动态图机制适合研究和实验
- 社区支持:活跃的社区和丰富的文档
- 生产就绪:支持将模型部署到生产环境
二、Tensor:深度学习的基本数据结构
2.1 什么是Tensor?
Tensor是PyTorch中最基本的数据结构,可以看作是多维数组。它在概念上类似于NumPy的ndarray,但具有额外的功能:
- GPU支持:可以在GPU上运行加速计算
- 自动求导:自动计算梯度,支持反向传播
- 动态计算:支持动态构建计算图
2.2 Tensor的维度
Tensor可以有不同维度,对应不同的数据结构:
| 维度 | 名称 | 示例 | 说明 |
|---|---|---|---|
| 0维 | 标量 | tensor(3.14) | 单个数值 |
| 1维 | 向量 | tensor([1, 2, 3]) | 一维数组 |
| 2维 | 矩阵 | tensor([[1, 2], [3, 4]]) | 二维数组 |
| 3维及以上 | 高阶Tensor | tensor([[[1, 2]]]) | 三维及更高维数组 |
2.3 Tensor与NumPy数组的对比
虽然Tensor和NumPy数组很相似,但有几个关键区别:
| 特性 | PyTorch Tensor | NumPy ndarray |
|---|---|---|
| 设备支持 | CPU和GPU | 仅CPU |
| 自动求导 | 支持 | 不支持 |
| 计算图 | 动态计算图 | 无计算图 |
| 数据类型 | 丰富的类型系统 | 类似但不同 |
三、Tensor的存储机制:Storage与Metadata
3.1 两部分组成的Tensor
每个Tensor都由两部分组成:
- Storage(存储):实际数据的一维连续内存块
- Metadata(元数据):描述Tensor结构和属性的信息
3.2 元数据的内容
元数据包含以下关键信息:
- 形状(shape):Tensor各维度的大小
- 数据类型(dtype):Tensor中元素的数据类型
- 步长(stride):在每个维度上移动时在存储中跨越的元素数
- 存储偏移量(storage_offset):从存储开始处的偏移
- 设备(device):Tensor所在的设备(CPU或GPU)
3.3 共享存储机制
多个Tensor可以共享同一个存储,即使它们的形状和步长不同。这种机制节省内存并提高效率:
python
a = torch.arange(6).reshape(2, 3) # 原始Tensor
b = a.transpose(0, 1) # 转置Tensor,共享存储
四、Tensor的数据类型
4.1 支持的数据类型
PyTorch支持多种数据类型,主要分为三类:
整数类型
torch.int8:8位有符号整数(-128 到 127)torch.int16:16位有符号整数(-32768 到 32767)torch.int32:32位有符号整数(-2147483648 到 2147483647)torch.int64:64位有符号整数
无符号整数类型
torch.uint8:8位无符号整数(0 到 255)torch.uint16:16位无符号整数(0 到 65535)torch.uint32:32位无符号整数torch.uint64:64位无符号整数
浮点数类型
torch.float16:16位浮点数(半精度)torch.float32:32位浮点数(单精度)torch.float64:64位浮点数(双精度)
4.2 浮点数的IEEE 754标准
浮点数在计算机中按照IEEE 754标准存储:
-
float16(半精度):
- 符号位:1位
- 指数部分:5位
- 小数部分:10位
- 示例:0.75 → 二进制:0011101000000000
-
float32(单精度):
- 符号位:1位
- 指数部分:8位
- 小数部分:23位
-
float64(双精度):
- 符号位:1位
- 指数部分:11位
- 小数部分:52位
4.3 数据类型的选择原则
- 精度需求:根据计算精度选择float16/32/64
- 内存限制:内存紧张时使用较小的数据类型
- 硬件支持:某些硬件对特定类型有优化
- 计算速度:较小类型通常计算更快
五、Tensor的连续性
5.1 连续Tensor的特点
连续Tensor的元素在内存中按照其在Tensor中的顺序紧密存储:
- 内存访问效率高
- 支持view()等操作
- 计算性能优化
5.2 不连续Tensor的产生
某些操作会导致Tensor不连续,例如:
- 转置(transpose):交换维度
- 切片(slice):部分选取
- 跨步索引(strided indexing):非连续索引
5.3 连续性的判断与转换
PyTorch提供了判断和转换Tensor连续性的方法:
is_contiguous():判断Tensor是否连续contiguous():将不连续Tensor转换为连续Tensor
转换过程会创建新的内存空间复制数据,因此有内存和时间开销。
六、代码:深入理解Tensor
python
import torch
# ============ 1. 创建不同类型和维度的Tensor ============
print("=== 1. 创建不同类型和维度的Tensor ===")
# 创建标量(0维Tensor)
scalar_tensor = torch.tensor(3.14)
print(f"标量Tensor: {scalar_tensor}")
print(f"维度: {scalar_tensor.dim()}")
print(f"形状: {scalar_tensor.shape}")
print()
# 创建向量(1维Tensor)
vector_tensor = torch.tensor([1, 2, 3, 4, 5, 6])
print(f"向量Tensor: {vector_tensor}")
print(f"维度: {vector_tensor.dim()}")
print(f"形状: {vector_tensor.shape}")
print()
# 创建矩阵(2维Tensor)
matrix_tensor = torch.tensor([[1, 2], [3, 4]])
print(f"矩阵Tensor: {matrix_tensor}")
print(f"维度: {matrix_tensor.dim()}")
print(f"形状: {matrix_tensor.shape}")
print()
# ============ 2. 查看Tensor的存储信息 ============
print("=== 2. 查看Tensor的存储信息 ===")
# 创建一个2x3的Tensor
tensor1 = torch.tensor([[1., 2, 3], [4, 5, 6]], dtype=torch.float32)
print(f"Tensor内容:\n{tensor1}")
print(f"形状: {tensor1.shape}")
print(f"数据类型: {tensor1.dtype}")
print(f"设备: {tensor1.device}")
print(f"存储地址: {tensor1.storage().data_ptr()}")
print(f"存储内容: {tensor1.storage().tolist()}")
print()
# ============ 3. 理解共享存储机制 ============
print("=== 3. 理解共享存储机制 ===")
# 创建原始Tensor
a = torch.arange(12).reshape(3, 4)
print(f"原始Tensor a:\n{a}")
print(f"a的存储地址: {a.storage().data_ptr()}")
# 创建转置Tensor(共享存储)
b = a.transpose(0, 1)
print(f"\n转置Tensor b:\n{b}")
print(f"b的存储地址: {b.storage().data_ptr()}")
print(f"a和b是否共享存储: {a.storage().data_ptr() == b.storage().data_ptr()}")
print()
# ============ 4. 理解步长(Stride) ============
print("=== 4. 理解步长(Stride) ===")
# 查看原始Tensor的步长
print(f"a的形状: {a.shape}")
print(f"a的步长: {a.stride()}")
# 步长的解释:
# stride(0) = 4: 在第0维(行)移动一个元素,在存储中需要跨越4个元素
# stride(1) = 1: 在第1维(列)移动一个元素,在存储中需要跨越1个元素
# 查看转置Tensor的步长
print(f"\nb的形状: {b.shape}")
print(f"b的步长: {b.stride()}")
# stride(0) = 1: 在转置后的第0维移动一个元素,在存储中需要跨越1个元素
# stride(1) = 4: 在转置后的第1维移动一个元素,在存储中需要跨越4个元素
print()
# ============ 5. 理解Tensor的连续性 ============
print("=== 5. 理解Tensor的连续性 ===")
# 检查连续性
print(f"a是否连续: {a.is_contiguous()}")
print(f"b是否连续: {b.is_contiguous()}")
# 尝试对不连续Tensor进行view操作
try:
b_view = b.view(2, 6)
print(f"b.view(2, 6)成功: \n{b_view}")
except Exception as e:
print(f"b.view(2, 6)失败: {e}")
# 将不连续Tensor转换为连续Tensor
b_contiguous = b.contiguous()
print(f"\nb转换为连续后: {b_contiguous.is_contiguous()}")
# 现在可以进行view操作
b_contiguous_view = b_contiguous.view(2, 6)
print(f"连续化后的b.view(2, 6): \n{b_contiguous_view}")
print()
# ============ 6. 数据类型转换 ============
print("=== 6. 数据类型转换 ===")
# 创建float32类型的Tensor
float_tensor = torch.tensor([1.5, 2.5, 3.5], dtype=torch.float32)
print(f"原始Tensor (float32): {float_tensor}, 数据类型: {float_tensor.dtype}")
# 转换为float16
float16_tensor = float_tensor.to(torch.float16)
print(f"转换为float16: {float16_tensor}, 数据类型: {float16_tensor.dtype}")
# 转换为int
int_tensor = float_tensor.to(torch.int32)
print(f"转换为int32: {int_tensor}, 数据类型: {int_tensor.dtype}")
print()
# ============ 7. 设备转移 ============
print("=== 7. 设备转移 ===")
# 检查是否有可用的GPU
if torch.cuda.is_available():
print("CUDA可用,将Tensor转移到GPU")
gpu_tensor = a.to('cuda')
print(f"GPU Tensor设备: {gpu_tensor.device}")
# 转移回CPU
cpu_tensor = gpu_tensor.to('cpu')
print(f"转移回CPU的设备: {cpu_tensor.device}")
else:
print("CUDA不可用,使用CPU")
print()
# ============ 8. Tensor的基本运算 ============
print("=== 8. Tensor的基本运算 ===")
# 创建两个Tensor进行运算
x = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
y = torch.tensor([[5, 6], [7, 8]], dtype=torch.float32)
print(f"x:\n{x}")
print(f"y:\n{y}")
# 加法
add_result = x + y
print(f"\n加法 (x + y):\n{add_result}")
# 矩阵乘法
matmul_result = torch.matmul(x, y)
print(f"\n矩阵乘法 (x @ y):\n{matmul_result}")
# 逐元素乘法
mul_result = x * y
print(f"\n逐元素乘法 (x * y):\n{mul_result}")
# 求和
sum_result = x.sum()
print(f"\n所有元素求和: {sum_result}")
# 按维度求和
sum_dim0 = x.sum(dim=0) # 按第0维(行)求和
sum_dim1 = x.sum(dim=1) # 按第1维(列)求和
print(f"按第0维求和(列和): {sum_dim0}")
print(f"按第1维求和(行和): {sum_dim1}")
代码关键点解析
1. Tensor的创建与基本属性
python
# 创建Tensor
tensor1 = torch.tensor([[1., 2, 3], [4, 5, 6]])
# 查看属性
print(tensor1.shape) # 形状
print(tensor1.dtype) # 数据类型
print(tensor1.device) # 设备
print(tensor1.stride()) # 步长
这些基本属性是理解和使用Tensor的基础。
2. 共享存储机制
python
a = torch.arange(12).reshape(3, 4)
b = a.transpose(0, 1)
print(a.storage().data_ptr() == b.storage().data_ptr()) # True
a和b共享相同的存储,这意味着修改一个Tensor会影响另一个,同时也节省了内存。
3. 步长的意义
python
a = torch.arange(12).reshape(3, 4)
print(a.stride()) # (4, 1)
步长表示在每个维度上移动一个位置时,在底层存储中需要跳过的元素数量。对于形状为(3,4)的矩阵:
- 在行方向移动:跳过4个元素
- 在列方向移动:跳过1个元素
4. 连续性的重要性
python
b = a.transpose(0, 1)
print(b.is_contiguous()) # False
# 不连续Tensor不能直接view
b_contiguous = b.contiguous() # 转换为连续
b_view = b_contiguous.view(2, 6) # 现在可以view
连续性影响内存访问效率和某些操作的支持。contiguous()方法可以创建连续的副本。
5. 设备转移
python
if torch.cuda.is_available():
gpu_tensor = tensor.to('cuda') # 转移到GPU
cpu_tensor = gpu_tensor.to('cpu') # 转移回CPU
PyTorch支持在CPU和GPU之间无缝转移Tensor,这是深度学习加速的关键。
6. 自动求导的基础
虽然本文未涉及自动求导的具体实现,但Tensor的自动求导功能是PyTorch的核心特性:
python
x = torch.tensor([1.0], requires_grad=True) # 启用梯度追踪
y = x ** 2
y.backward() # 自动计算梯度
print(x.grad) # 梯度值
requires_grad=True参数告诉PyTorch需要追踪该Tensor的所有操作,以便后续计算梯度。
Tensor在深度学习中的应用
1. 神经网络中的数据表示
在深度学习中,不同类型的数据用不同维度的Tensor表示:
| 数据类型 | Tensor维度 | 示例形状 | 说明 |
|---|---|---|---|
| 标量 | 0维 | [] | 损失值、准确率 |
| 向量 | 1维 | [batch_size] | 批量标量值 |
| 矩阵 | 2维 | [batch_size, feature_size] | 批量特征向量 |
| 图像 | 3维 | [channels, height, width] | 单张图像 |
| 图像批次 | 4维 | [batch_size, channels, height, width] | 批量图像 |
| 视频 | 5维 | [batch_size, channels, frames, height, width] | 视频数据 |
2. 计算图的构建
Tensor和自动求导共同构成了PyTorch的计算图:
输入Tensor → 操作1 → 中间Tensor → 操作2 → 输出Tensor
↓ ↓
梯度计算 ←--- 反向传播 ←--- 损失计算
每个Tensor不仅存储数据,还存储了创建它的操作历史,这使得自动求导成为可能。
3. 内存优化技巧
- 使用合适的数据类型:训练时可用float32,推理时尝试float16
- 利用共享存储:避免不必要的数据复制
- 及时释放内存 :使用
del关键字和torch.cuda.empty_cache() - 使用原地操作 :如
x.add_(y)而不是x = x + y
下一篇预告
在深入理解了PyTorch的Tensor基础之后,我们将正式进入深度学习模型的构建:
机器学习算法原理与实践-入门(八):基于PyTorch框架的线性回归
我们将使用PyTorch重新实现线性回归模型,体验现代深度学习框架的便利性,并理解自动求导如何简化模型训练过程。