动手学深度学习-预备知识1-张量(Tensor)

415 阅读15分钟

在深度学习中,张量(Tensor是一个非常基础且重要的概念。简单来说,张量可以看作是多维数组,类似于标量、向量和矩阵的扩展形式。它们用于表示数据和模型中的各种信息。张量不仅仅是数学上的概念,它还是深度学习框架(如TensorFlowPyTorch)的核心数据结构。

一、张量的阶数(Rank)

张量的维度(或称为阶数,Rank)决定了它的形状和数据组织方式。根据维度的不同,张量可以分为以下几种类型:

  1. 标量(Scalar,0阶张量):没有任何维度的单一数值。例如:5。
  2. 向量(Vector,1阶张量):一维数组,由一组数值组成。例如:[1, 2, 3]
  3. 矩阵(Matrix,2阶张量):二维数组,可以表示为行和列的网格。例如:
[[1, 2],
 [3, 4]]
  1. 更高阶的张量(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列。

三、张量在深度学习中的应用

  1. 数据表示:在深度学习中,张量用于表示输入数据。比如,图像数据可以用三维张量表示,其中每个维度分别对应图像的高度、宽度和通道数。
  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
张量转float42.0
张量转int42