在深度学习中,张量(Tensor)是一个非常基础且重要的概念。简单来说,张量可以看作是多维数组,类似于标量、向量和矩阵的扩展形式。它们用于表示数据和模型中的各种信息。张量不仅仅是数学上的概念,它还是深度学习框架(如TensorFlow和PyTorch)的核心数据结构。
一、张量的阶数(Rank)
张量的维度(或称为阶数,Rank)决定了它的形状和数据组织方式。根据维度的不同,张量可以分为以下几种类型:
- 标量(Scalar,0阶张量):没有任何维度的单一数值。例如:5。
- 向量(Vector,1阶张量):一维数组,由一组数值组成。例如:
[1, 2, 3]。 - 矩阵(Matrix,2阶张量):二维数组,可以表示为行和列的网格。例如:
[[1, 2],
[3, 4]]
- 更高阶的张量(N阶张量):当张量的维度大于2时,它被称为高阶张量。比如三维张量可以看作是矩阵的集合,四维张量常用于表示批量的图像数据(如
[batch_size, height, width, channels])。
二、张量的维度(Dimension)
在深度学习中,Rank(阶数) 和 Dimension(维度) 这两个词常常用于描述张量,但它们其实有细微的区别和各自的适用场景。通常使用 Rank 而不是 Dimension 是为了更加明确地表达概念。具体原因如下:
Rank描述的是张量的阶数
Rank(阶数) 指的是张量的“维度的数量”,即它有多少个维度。例如,标量是0阶张量(没有维度),向量是一阶张量(有一个维度),矩阵是二阶张量(有两个维度),而更高阶张量则有更多的维度。换句话说,Rank表示张量的层级或维度的深度。
Dimension描述的是每个轴上的大小
Dimension(维度) 通常指张量在每个轴(轴向)上的大小或长度。它表示某个特定阶的张量在每一维度上的形状。例如,一个三阶张量可能有维度大小为 [3, 4, 5],表示它有三个维度,分别在这三个轴上有 3、4 和 5 个元素。
- 示例:一个 2x3 的矩阵,Rank 是 2(因为是二维),维度是
[2, 3],表示它有2行3列。
三、张量在深度学习中的应用
- 数据表示:在深度学习中,张量用于表示输入数据。比如,图像数据可以用三维张量表示,其中每个维度分别对应图像的高度、宽度和通道数。
- 参数和权重:神经网络的权重和偏置参数也是以张量形式存储和更新的。
- 高效计算:张量支持在GPU和其他硬件上的高效并行计算,使得深度学习模型能够快速处理大量数据。
四、张量的操作(使用 PyTorch 框架来举例说明)
pip install torch
1. 张量的创建
import torch
# 创建0阶张量,即标量
#########################################################
scalar = torch.tensor(5)
print(f"标量:{scalar}")
print(f"标量的维度:{scalar.dim()}")
print(f"标量的形状:{scalar.shape}")
"""
标量:5
标量的维度:0
标量的形状:torch.Size([])
"""
# 向量(1阶张量)
#########################################################
vector = torch.tensor([1, 2, 3])
print(f"向量:{vector}")
print(f"向量的维度:{vector.dim()}")
print(f"向量的形状:{vector.shape}")
"""
向量:tensor([1, 2, 3])
向量的维度:1
向量的形状:torch.Size([3])
"""
# 三维张量(3阶张量)
#########################################################
tensor_3d = torch.tensor(
[
[
[1, 2], [3, 4], [5, 6]
],
[
[7, 8], [9, 10], [11, 12]
]
]
)
print(f"三维张量:{tensor_3d}")
print(f"三维张量的维度:{tensor_3d.dim()}")
print(f"三维张量的形状:{tensor_3d.shape}")
"""
三维张量:tensor([[[ 1, 2],
[ 3, 4],
[ 5, 6]],
[[ 7, 8],
[ 9, 10],
[11, 12]]])
三维张量的维度:3
三维张量的形状:torch.Size([2, 3, 2])
"""
# 快速创建一个包含以0开始的前12个整数的行向量
#########################################################
row_vector = torch.arange(12)
print(f"行向量:{row_vector}")
print(f"行向量的维度:{row_vector.dim()}")
print(f"行向量的形状:{row_vector.shape}")
"""
行向量:tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
行向量的维度:1
行向量的形状:torch.Size([12])
"""
# 创建一个元素全为0的张量
#########################################################
zeros_tensor = torch.zeros([2, 3, 4])
print(f"元素全为0的张量:\n{zeros_tensor}")
"""
全0张量:
tensor([[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]]])
"""
# 创建一个元素全为1的张量
#########################################################
ones_tensor = torch.ones([4, 2, 3])
print(f"元素全为1的张量:\n{ones_tensor}")
"""
元素全为1的张量:
tensor([[[1., 1., 1.],
[1., 1., 1.]],
[[1., 1., 1.],
[1., 1., 1.]],
[[1., 1., 1.],
[1., 1., 1.]],
[[1., 1., 1.],
[1., 1., 1.]]])
"""
# 创建一个元素都从均值为0、标准差为1的标准高斯分布(正态分布)中随机采样的张量
#########################################################
nd_tensor = torch.randn(3, 4)
print(f"元素全符合标准正态分布的张量:\n{nd_tensor}")
"""
元素全符合标准正态分布的张量:
tensor([[-0.6295, -1.4940, 1.6708, 0.6781],
[ 0.1911, -1.1643, 0.2682, 1.5085],
[-0.8502, 1.7608, 1.5371, 1.4945]])
"""
2. 张量的基本操作
# 1. 查看查看形状(各个轴的长度)
tensor = torch.arange(12)
print(f"张量的形状:{tensor.shape}")
# 张量的形状:torch.Size([12])
# 2.1 reshape方法改变张量的形状
reshaped_tensor = tensor.reshape(3, 4)
print(f"reshape方法改变形状后的张量:\n{reshaped_tensor}")
"""
reshape方法改变形状后的张量:
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
"""
print(f"改变形状后的张量的形状:{reshaped_tensor.shape}")
# 改变形状后的张量的形状:torch.Size([3, 4])
# 2.2 reshape方法改变张量的形状
reshaped_view = tensor.view(2, 6)
print(f"view方法改变形状后的张量:\n{reshaped_view}")
"""
view方法改变形状后的张量:
tensor([[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11]])
"""
view():速度快但要求张量是内存连续的,否则会报错。适合处理连续的张量,不涉及内存重分配。reshape():更灵活,能够处理非连续的张量,但在内存不连续时可能会分配新的内存进行数据拷贝。
import torch
x = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(f"张量x:\n{x}")
"""
张量x:
tensor([[1, 2, 3],
[4, 5, 6]])
"""
# 转置张量,打乱其内存布局,使其不再连续
x_t = x.t()
print(f"张量x的转置:\n{x_t}")
"""
张量x的转置:
tensor([[1, 4],
[2, 5],
[3, 6]])
"""
# 使用 view 会报错,因为 x_t 是非连续张量
try:
x_t_view = x_t.view(6)
except RuntimeError as e:
print(f"使用 view() 时出错: {e}")
"""
使用 view() 时出错: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
"""
# 使用 reshape 则可以成功
x_t_reshape = x_t.reshape(6)
print(f"x_t.reshape(6): {x_t_reshape}")
"""
x_t.reshape(6): tensor([1, 4, 2, 5, 3, 6])
"""
通常,如果不确定内存布局或想避免错误,使用 reshape() 会更安全。如果对性能要求较高且确定张量是连续的,可以选择 view()。
# 查看张量中元素的总数,即形状的所有元素乘积
print(f"张量中元素的总数:{reshaped_tensor.numel()}")
# 张量中元素的总数:12
3. 张量的运算符操作
3.1 数学运算
import torch
x = torch.tensor([[1, 2], [3, 4]])
y = torch.tensor([[5, 6], [7, 8]])
print(f"x + y = {x + y}")
print('-' * 50)
print(f"x - y = {x - y}")
print('-' * 50)
print(f"x * y = {x * y}") # 乘法
print(f"torch.mul(x, y) = {torch.mul(x, y)}")
print('-' * 50)
print(f"x / y = {x / y}")
print('-' * 50)
print(f"x ** y = {x ** y}")
print('-' * 50)
print(f"指数 exp(x) = {torch.exp(x)}")
print('-' * 50)
x1 = torch.tensor([[2, 2], [5, 4]])
print(f"判等运算 x == x1 = {x == x1}")
print('-' * 50)
print(f"求和 sum(x) = {x.sum()}")
print('-' * 50)
x + y = tensor([[ 6, 8],
[10, 12]])
--------------------------------------------------
x - y = tensor([[-4, -4],
[-4, -4]])
--------------------------------------------------
x * y = tensor([[ 5, 12],
[21, 32]])
torch.mul(x, y) = tensor([[ 5, 12],
[21, 32]])
--------------------------------------------------
x / y = tensor([[0.2000, 0.3333],
[0.4286, 0.5000]])
--------------------------------------------------
x ** y = tensor([[ 1, 64],
[ 2187, 65536]])
--------------------------------------------------
指数 exp(x) = tensor([[ 2.7183, 7.3891],
[20.0855, 54.5981]])
--------------------------------------------------
判等运算 x == x1 = tensor([[False, True],
[False, True]])
--------------------------------------------------
求和 sum(x) = 10
--------------------------------------------------
3.2 张量的拼接和分割
- 拼接:
torch.cat()可以沿指定维度拼接张量。
import torch
tensor_a = torch.tensor([[1, 2], [3, 4]])
tensor_b = torch.tensor([[5, 6], [7, 8]])
concat_tensor_d0 = torch.cat((tensor_a, tensor_b), dim=0)
print(f"在第1个维度上的拼接的张量:\n{concat_tensor_d0}")
print('-' * 50)
concat_tensor_d1 = torch.cat((tensor_a, tensor_b), dim=1)
print(f"在第2个维度上的拼接的张量:\n{concat_tensor_d1}")
在第1个维度上的拼接的张量:
tensor([[1, 2],
[3, 4],
[5, 6],
[7, 8]])
--------------------------------------------------
在第2个维度上的拼接的张量:
tensor([[1, 2, 5, 6],
[3, 4, 7, 8]])
- 分割:
torch.chunk()将张量按指定块数分割。
split_tensors = torch.chunk(concat_tensor_d0, 2, dim=0) # 将张量分成2块
print(f"在第1个维度上分割后的张量:\n{split_tensors}")
print('-' * 50)
split_tensors = torch.chunk(concat_tensor_d1, 2, dim=1) # 将张量分成2块
print(f"在第2个维度上分割后的张量:\n{split_tensors}")
在第1个维度上分割后的张量:
(tensor([[1, 2],
[3, 4]]), tensor([[5, 6],
[7, 8]]))
--------------------------------------------------
在第2个维度上分割后的张量:
(tensor([[1, 2],
[3, 4]]), tensor([[5, 6],
[7, 8]]))
4. 张量的广播机制
广播是指两个不同形状的张量在做数学运算时,PyTorch 会自动调整它们的形状,使其兼容,然后进行运算。这在执行张量加减法时尤为常见。
import torch
# 创建一个 1x3 张量和一个 2x3 张量
tensor_c = torch.tensor([[1, 2, 3]])
tensor_d = torch.tensor([[4, 5, 6], [7, 8, 9]])
# 利用广播机制进行加法
broadcast_result = tensor_c + tensor_d
print(f"广播机制下的张量加法结果:\n{broadcast_result}")
广播机制下的张量加法结果:
tensor([[ 5, 7, 9],
[ 8, 10, 12]])
在大多数情况下,我们将沿着数组中长度为1的轴进行广播。
tensor_e = torch.arange(3).reshape(3, 1)
tensor_f = torch.arange(2).reshape(1, 2)
print(f"tensor_e:\n{tensor_e}", end='\n\n')
print(f"tensor_f:\n{tensor_f}", end='\n\n')
print(f"广播机制下的张量加法结果:\n{tensor_e + tensor_f}")
tensor_e:
tensor([[0],
[1],
[2]])
tensor_f:
tensor([[0, 1]])
广播机制下的张量加法结果:
tensor([[0, 1],
[1, 2],
[2, 3]])
5. 索引和切片
首先,我们创建一个 3D 张量(例如,一个包含多张图像的数据集)。
import torch
# 创建一个 2x3x4 的张量
tensor = torch.tensor([[[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]],
[[13, 14, 15, 16],
[17, 18, 19, 20],
[21, 22, 23, 24]]])
print("原始张量:\n", tensor)
原始张量:
tensor([[[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]],
[[13, 14, 15, 16],
[17, 18, 19, 20],
[21, 22, 23, 24]]])
5.1 基本索引
可以通过整数索引访问张量的特定元素。
# 访问第 1 个图像的第 2 行第 3 列的元素
element = tensor[0, 1, 2] # 等同于 tensor[0][1][2]
print("访问第 1 个图像的第 2 行第 3 列的元素:", element)
print(f"tensor[0][1][2] = {tensor[0][1][2]}")
访问第 1 个图像的第 2 行第 3 列的元素: tensor(7)
tensor[0][1][2] = 7
5.2 切片操作
可以使用切片语法获取子张量。
# 获取第 1 个图像的所有行和前 2 列
slice_1 = tensor[0, :, :2]
print("获取第 1 个图像的所有行和前 2 列:\n", slice_1)
获取第 1 个图像的所有行和前 2 列:
tensor([[ 1, 2],
[ 5, 6],
[ 9, 10]])
5.3 多维切片
可以在不同维度上进行切片操作。
# 获取所有图像的第 1 行和第 2 列
slice_2 = tensor[:, 0, 1]
print("获取所有图像的第 1 行和第 2 列:", slice_2)
获取所有图像的第 1 行和第 2 列: tensor([ 2, 14])
5.4 高级索引
可以使用列表或张量进行高级索引。
# 获取第 1 个和第 2 个图像的第 2 行的所有列
advanced_index = tensor[[0, 1], 1, :]
print("获取第 1 个和第 2 个图像的第 2 行的所有列:\n", advanced_index)
获取第 1 个和第 2 个图像的第 2 行的所有列:
tensor([[ 5, 6, 7, 8],
[17, 18, 19, 20]])
5.5 切片与索引结合
结合使用切片和索引,可以灵活选择张量的某些部分。
# 获取第 1 个图像的前 2 行和所有列
combined_slice = tensor[0, :2, :]
print("获取第 1 个图像的前 2 行和所有列:\n", combined_slice)
获取第 1 个图像的前 2 行和所有列:
tensor([[1, 2, 3, 4],
[5, 6, 7, 8]])
5.6 索引后修改
索引后可以直接修改张量中的元素。
# 修改第 2 个图像的第 3 行第 4 列的元素
tensor[1, 2, 3] = 99
print("修改后的张量:\n", tensor)
修改后的张量:
tensor([[[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12]],
[[13, 14, 15, 16],
[17, 18, 19, 20],
[21, 22, 23, 99]]])
5.7 布尔索引
可以使用布尔数组进行索引,选择符合条件的元素。
# 创建一个布尔索引,选择大于 10 的元素
bool_index = tensor > 10
print("布尔索引张量:\n", bool_index, end='\n\n')
filtered_tensor = tensor[bool_index]
print("过滤出大于 10 的元素:\n", filtered_tensor)
布尔索引张量:
tensor([[[False, False, False, False],
[False, False, False, False],
[False, False, True, True]],
[[ True, True, True, True],
[ True, True, True, True],
[ True, True, True, True]]])
过滤出大于 10 的元素:
tensor([11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24])
6. 内存管理
6.1 引用内存的改变
import torch
# 创建两个张量
X = torch.tensor([1.0, 2.0, 3.0])
Y = torch.tensor([4.0, 5.0, 6.0])
# 显示 Y 的内存地址
before_id = id(Y)
print("Y 的内存地址:", before_id)
# 执行操作 Y = Y + X
Y = Y + X # 这里会创建一个新张量并更新 Y 的引用
# 检查 Y 的内存地址
after_id = id(Y)
print("更新后 Y 的内存地址:", after_id)
# 检查地址是否相同
print("地址相同吗?", before_id == after_id) # 输出:False
Y 的内存地址: 2643724008144
更新后 Y 的内存地址: 2643724008336
地址相同吗? False
6.2 优化内存使用:原地操作
# 创建两个张量
X = torch.tensor([1.0, 2.0, 3.0])
Y = torch.tensor([4.0, 5.0, 6.0])
# 显示 Y 的内存地址
before_id = id(Y)
print("Y 的内存地址:", before_id)
# 原地更新 Y
Y.add_(X) # 直接在 Y 的位置上进行更新
print("原地更新后的Y:", Y)
# 检查 Y 的内存地址
after_id = id(Y)
print("更新后 Y 的内存地址:", after_id)
# 检查地址是否相同
print("地址相同吗?", before_id == after_id) # 输出:True
Y 的内存地址: 2871155018640
原地更新后的Y: tensor([5., 7., 9.])
更新后 Y 的内存地址: 2871155018640
地址相同吗? True
# 显示 Y 的内存地址
before_id = id(Y)
print("Y 的内存地址:", before_id)
# 原地更新 Y
Y[:] = Y + X # 直接在 Y 的位置上进行更新
# 检查 Y 的内存地址
after_id = id(Y)
print("更新后 Y 的内存地址:", after_id)
# 检查地址是否相同
print("地址相同吗?", before_id == after_id) # 输出:True
Y 的内存地址: 2899259034032
更新后 Y 的内存地址: 2899259034032
地址相同吗? True
before_id = id(Y)
print("Y 的内存地址:", before_id)
# 原地更新 Y
Y += X # 直接在 Y 的位置上进行更新
# 检查 Y 的内存地址
after_id = id(Y)
print("更新后 Y 的内存地址:", after_id)
# 检查地址是否相同
print("地址相同吗?", before_id == after_id) # 输出:True
Y 的内存地址: 2516716029520
更新后 Y 的内存地址: 2516716029520
地址相同吗? True
6.3 避免过度分配内存
# 定义多个操作的函数
def computation(X, Y):
Z = torch.zeros_like(Y) # 分配一个全零的张量(未使用)
A = X + Y # 计算 A
B = A + Y # 计算 B
C = B + Y # 计算 C
return C + Y # 返回 C + Y
X = torch.tensor([1.0, 2.0, 3.0])
Y = torch.tensor([4.0, 5.0, 6.0])
# 调用函数
result = computation(X, Y)
print("计算结果:", result)
-
内存管理:通过使用
id()函数,可以观察到在执行Y = Y + X时会创建新的内存,而原地操作Y.add_(X)则会在同一内存位置更新。 -
避免不必要的内存分配:使用原地操作可以减少内存使用。在定义复杂计算时,合理安排内存分配可以提升性能,避免创建不必要的中间结果。
7. 转换为其他Python对象
7.1 张量与 NumPy 数组之间的转换
import torch
# 创建一个张量
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]])
# 转换为 NumPy 数组
numpy_array = tensor.numpy()
print("NumPy 数组:\n", numpy_array)
NumPy 数组:
[[1 2 3]
[4 5 6]]
import numpy as np
# 创建一个 NumPy 数组
numpy_array = np.array([[1, 2, 3], [4, 5, 6]])
# 转换为张量
tensor_from_numpy = torch.from_numpy(numpy_array)
print("张量:\n", tensor_from_numpy)
张量:
tensor([[1, 2, 3],
[4, 5, 6]], dtype=torch.int32)
7.2 张量与 Python 列表之间的转换
# 创建一个张量
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]])
# 转换为 Python 列表
python_list = tensor.tolist()
print("Python 列表:", python_list)
Python 列表: [[1, 2, 3], [4, 5, 6]]
# 创建一个 Python 列表
python_list = [[1, 2, 3], [4, 5, 6]]
# 转换为张量
tensor_from_list = torch.tensor(python_list)
print("张量:\n", tensor_from_list)
张量:
tensor([[1, 2, 3],
[4, 5, 6]])
7.3 张量与标量之间的转换
# 创建一个标量
scalar = 5.0
# 转换为张量
tensor_from_scalar = torch.tensor(scalar)
print("从标量到张量:", tensor_from_scalar)
print("从标量转化的张量的维度:", tensor_from_scalar.dim())
print("从标量转化的张量的形状:", tensor_from_scalar.shape)
从标量到张量: tensor(5.)
从标量转化的张量的维度: 0
从标量转化的张量的形状: torch.Size([])
# 创建一个包含单个元素的张量
tensor = torch.tensor(42.0)
# 转换为标量
scalar = tensor.item()
print("从张量到标量:", scalar)
print("张量转float:", float(tensor))
print("张量转int:", int(tensor))
从张量到标量: 42.0
张量转float: 42.0
张量转int: 42