掌握numpy.einsum与torch.einsum:提升科学计算与深度学习中的运算效率和代码可读性

2023-12-26 13:55:56


一、前言

在现代计算中,数组和张量运算扮演着至关重要的角色。它们广泛应用于科学计算、数据分析、机器学习和深度学习等领域,是实现高效计算和处理大规模数据的基础。

Einstein求和记号,也称为Einstein notation,是由阿尔伯特·爱因斯坦在发展相对论时引入的一种简洁的数学表示法。它通过在指标上省略求和符号∑,极大地简化了涉及多个维度的数组和张量运算的表达。

numpy.einsum和torch.einsum是Python中两个强大的库,分别在numpy和PyTorch中实现了Einstein求和记号的功能。这些工具使得程序员能够以一种直观且易于理解的方式编写复杂的数组和张量运算,从而提高代码的可读性和维护性。


二、Einstein求和记号简介

1. 规则和表达能力

Einstein求和记号的核心规则是:对于一个给定的表达式,如果某个指标在同一项的上下标中同时出现,那么就对该指标进行求和。例如,在表达式 a_ij b_jk 中,指标 _j 在下标和上标中都出现,因此我们需要对 _j 进行求和。

Einstein求和记号具有强大的表达能力,可以表示各种复杂的数组和张量运算,包括矩阵乘法、向量点积、按元素运算、转置、秩变换等。其灵活性和通用性使得它在许多计算任务中都得到了广泛应用。

2. 表示常见的数组和张量运算

Einstein求和记号的一个主要优势在于它可以显著提升代码的简洁性和可读性。通过使用简短的字符串来描述复杂的运算,程序员可以更轻松地理解和维护代码。例如,使用Einstein求和记号表示矩阵乘法比传统的嵌套循环或numpy的dot函数更为直观和简洁。这不仅有助于减少代码量,还可以降低错误发生的风险,并提高代码的可维护性和可扩展性。

以下是一些使用Einstein求和记号表示常见数组和张量运算的例子:

  • 矩阵乘法:result = einsum('ij,jk->ik', matrix1, matrix2),这等价于 result[i,k] = sum(matrix1[i,j] * matrix2[j,k])
  • 向量点积:dot_product = einsum('i,i->', vector1, vector2),这等价于 dot_product = sum(vector1[i] * vector2[i])
  • 按元素相加:summed_array = einsum('ij->ij', array1, array2),这等价于 summed_array[i,j] = array1[i,j] + array2[i,j]

三、numpy.einsum的应用与实践

1. numpy.einsum的基础使用

numpy.einsum函数允许我们使用Einstein求和记号来简洁地表示和执行矩阵乘法、向量点积和按元素运算等基础操作。按操作数和数量可分为单操作数和多操作数。

矩阵的迹

这是一个单操作数的案例

np.random.seed(0)
x = np.random.rand(3, 3)
trace = np.einsum("ii->i",x)
print(trace)

输出:

[0.5488135  0.4236548  0.96366276]

矩阵乘法

这是一个多操作数的案例

import numpy as np

matrix1 = np.array([[1, 2], [3, 4]])
matrix2 = np.array([[5, 6], [7, 8]])

result = np.einsum('ij,jk->ik', matrix1, matrix2)
print(result)

输出:

[[19, 22],
 [43, 50]]

向量点积

vector1 = np.array([1, 2, 3])
vector2 = np.array([4, 5, 6])

dot_product = np.einsum('i,i->', vector1, vector2)
print(dot_product)

输出:

32

按元素相加

array1 = np.array([[1, 2], [3, 4]])
array2 = np.array([[5, 6], [7, 8]])

summed_array = np.einsum('ij,ij->ij', array1, array2)
print(summed_array)

输出:

[[ 5, 12],
 [21, 32]]

numpy.einsum 是一个强大的函数,用于指定多维数组的操作,但对于简单的逐元素加法,建议使用 numpy.add 或直接使用加号 +。这里只是作一个示例。

求和

在数值计算和矩阵运算中,numpy.einsum可以用于各种实际问题。以下是一个简单的例子,计算一个二维数组的行和,列和和总和:

data = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

row_sums = np.einsum('ij->i', data)
col_sums = np.einsum('ij->j', data)
total_sums = np.einsum('ij->', data)
print(row_sums)
print(col_sums)
print(total_sums)

输出:

[ 6 15 24]
[12 15 18]
45

2. numpy.einsum的高级功能

numpy.einsum也可以用于实现矩阵转置、秩变换计算等操作。以下是一些示例:

矩阵转置

matrix = np.array([[1, 2], [3, 4]])

transposed_matrix = np.einsum('ij->ji', matrix)
print(transposed_matrix)

输出:

[[1, 3],
 [2, 4]]

秩变换

秩变换可以是将一个向量转换为矩阵。例如,外积运算可以看作是一种秩变换。
在 NumPy 中,计算两个向量的外积(也称为张量积)可以使用 numpy.outer 函数。外积是一个数学运算,它接受两个向量作为输入,并生成一个矩阵作为输出。如果输入向量分别是n 维和m维,那么结果矩阵将是一个n×m 的矩阵,其中矩阵的每个元素是输入向量相应元素的乘积。

vector1 = np.array([1, 2])
vector2 = np.array([3, 4])

outer_product = np.einsum('i,j->ij', vector1, vector2)
print(outer_product)

输出:

 [[3 4]
 [6 8]]

注意:numpy库提供了更直接的函数(如np.transpose()np.reshape())来执行这些操作,但在某些情况下,使用einsum可能更加直观或方便。

计算协方差矩阵

在数据分析和处理中,numpy.einsum可以帮助我们轻松计算协方差、相关性等统计量。

np.random.seed(0)
data = np.random.rand(100, 3)

covariance_matrix = np.einsum('ki,kj->ij', data - data.mean(axis=0), data - data.mean(axis=0))
covariance_matrix /= (data.shape[0] - 1)
print(covariance_matrix)
# 下面用numpy的api实现,验证结果的正确性
# 计算协方差矩阵,将每列视为一个变量
cov_matrix = np.cov(data, rowvar=False)
print(cov_matrix)

输出:

[[ 0.0887596   0.00073355  0.01251582]
 [ 0.00073355  0.08614802 -0.00740528]
 [ 0.01251582 -0.00740528  0.07927409]]

广播乘法

在 np.einsum 的上下文中,广播乘法指的是对两个数组的特定维度执行逐元素乘法,同时自动扩展(或“广播”)其他维度以匹配彼此。这意味着即使两个数组在某些维度上的大小不匹配,也可以通过扩展较小维度的大小来执行乘法。

Q = np.random.rand(32, 20, 8, 64)
K = np.random.rand(32, 20, 8, 64)

M=np.einsum("nqhd,nkhd->nhqk", Q, K) 
print(M.shape)
(32, 8, 20, 20)

这段代码使用 NumPy 的 einsum 函数来计算两个四维数组 Q 和 K 的特定形式的乘积,用来演示注意力的计算:

这里,Q 和 K 都是随机初始化的四维数组。它们的形状都是 (32, 20, 8, 64),其中可以解释为:
第一维(大小为 32)代表批次大小(batch size)。
第二维(大小为 20)代表序列长度或时间步长。
第三维(大小为 8)代表注意力头的数量(在多头注意力机制中)。
第四维(大小为 64)代表每个注意力头的特征维度。
在这个特定的操作中,einsum 计算 Q 和 K 的点积,但是以一种特殊的方式进行。具体来说,它对 Q 的第四维(d)和 K 的第四维(d)进行点积,同时保留其他维度。这类似于在每个注意力头和每个时间步上对特征向量进行点积,这是注意力机制中的一个常见操作。
以上代码可等价于

Q1=Q.transpose(0,2,1,3) # nqhd -> nhqd
K1=K.transpose(0,2,1,3) # nkhd -> nhkd
M = np.einsum('...qd, ...kd->...qk', Q1, K1) 
print(M.shape)

输出:

(32, 8, 20, 20)

这里,einsum 使用了省略号(…)表示法,它是一种更简洁的方式来表示相同的操作。省略号代表了所有未指定的维度,这在处理具有相同模式的多维数组时非常有用。在这个例子中,‘…qd, …kd->…qk’ 与 ‘nhqd, nhkd->nhqk’ 完成相同的操作。

通过以上示例,我们可以看到numpy.einsum在数值计算、矩阵运算、数据分析和处理中的广泛应用。此外,它还可以用于优化现有numpy运算,提高代码效率和可读性。在接下来的部分中,我们将探讨一些使用numpy.einsum优化numpy运算的实践案例。


四、torch.einsum的应用与实践

1. torch.einsum的基础使用

torch.einsum函数在pytorch中提供了与numpy.einsum类似的功能,可以用于执行矩阵乘法、向量点积和按元素运算等基础操作。这部分和numpy的使用基本是一样的,不作详细说明。

2. 动态计算图环境中的适应性和优势

torch.einsum在动态计算图环境中表现出良好的适应性。由于pytorch支持动态计算图,我们可以方便地在运行时修改和优化einsum表达式。此外,torch.einsum还具有以下优势:

  • 自动微分支持:torch.einsum能够无缝地集成到PyTorch的自动微分系统中,使得梯度计算变得简单且高效。
  • GPU加速:torch.einsum能够利用GPU进行并行计算,极大地提高了大规模数据处理和深度学习模型训练的效率。

3. torch.einsum在深度学习中的应用

复杂运算(如注意力机制)中的作用和优势

在深度学习中,torch.einsum常用于实现复杂的运算,如注意力机制。在 pytorch 中,可以使用 torch.einsum 来实现一个简单的注意力机制。注意力机制通常涉及到计算查询(Query)、键(Key)和值(Value)之间的关系。以下是一个使用 torch.einsum 实现的基本注意力机制示例:

# 设置随机种子
torch.manual_seed(0)
# 假设我们有一批查询(Query),键(Key)和值(Value)
# 这里使用随机数据来模拟这些张量
# 假设 batch_size = 2, seq_len = 3, feature_dim = 4
batch_size, seq_len, feature_dim = 2, 3, 4
Query = torch.rand((batch_size, seq_len, feature_dim))
Key = torch.rand((batch_size, seq_len, feature_dim))
Value = torch.rand((batch_size, seq_len, feature_dim))

# 计算注意力得分
# 使用 einsum 计算 Query 和 Key 的点积
attention_scores = torch.einsum('bqd,bkd->bqk', Query, Key)

# 应用 softmax 来获取注意力权重
attention_weights = torch.nn.functional.softmax(attention_scores, dim=-1)

# 使用 einsum 将注意力权重应用于 Value
attention_output = torch.einsum('bqk,bkv->bqv', attention_weights, Value)

print(attention_output)

输出:

tensor([[[0.5015, 0.4117, 0.6472, 0.5333],
         [0.5330, 0.4551, 0.7061, 0.4878],
         [0.5052, 0.4295, 0.6739, 0.5327]],

        [[0.7687, 0.7422, 0.2450, 0.5191],
         [0.7614, 0.7036, 0.2652, 0.5533],
         [0.7594, 0.7353, 0.2341, 0.5472]]])

这个简单的注意力机制示例展示了如何使用 einsum 来高效地实现矩阵运算,这在实现更复杂的注意力机制(如 Transformer 中的自注意力)时非常有用。

自动微分和梯度计算的具体实现

torch.einsum支持自动微分,这意味着我们可以轻松地计算包含einsum操作的复杂函数的梯度。以下是一个使用torch.einsum计算梯度的示例:
假设我们有一个简单的函数,它计算两个矩阵的点积,然后计算结果矩阵的元素和。我们将使用 torch.einsum 来实现矩阵乘法,并计算其中一个矩阵相对于这个函数的梯度。

# 开启梯度计算
torch.manual_seed(0)
A = torch.rand((2, 3), requires_grad=True)
B = torch.rand((3, 2), requires_grad=True)

# 使用 einsum 计算矩阵乘法
C = torch.einsum('ik,kj->ij', A, B)

# 计算结果矩阵的元素和
D = C.sum()

# 反向传播计算梯度
D.backward()

# 输出 A 的梯度
print(A.grad)

输出:

tensor([[1.3865, 1.0879, 0.7506],
        [1.3865, 1.0879, 0.7506]])

在这个例子中,我们定义了一个使用torch.einsum的损失函数,并通过调用.backward()方法自动计算梯度。
由于torch.einsum能够充分利用现代硬件(如GPU)的并行计算能力,因此在处理大规模数据和深度学习模型时,其性能表现通常优于传统的numpy库。此外,torch.einsum的简洁语法也使得代码更易于阅读和维护。


五、复杂张量运算的嵌套循环手动实现

下面我们展示一下如何使用 pytorch 的 torch.einsum 函数来执行一个复杂的张量运算,以及如何通过嵌套循环手动实现相同的运算。

b_=2
n_=5
a_=3
m_=4
torch.manual_seed(0)
A = torch.randn(a_,n_,m_)
l = torch.randn(b_,n_)
r = torch.randn(b_,m_)
target = torch.einsum('bn,anm,bm->ba', l, A, r)
print(target)
target = torch.empty(2, 3)
for b in range(b_):
    for a in range(a_):
        temp = 0
        for n in range(n_):
            for m in range(m_):
                temp += l[b, n].item() * A[a, n, m].item() * r[b, m].item()
        target[b, a] = temp
print(target)

输出:

tensor([[-3.4482, -1.9134, -2.6599],
        [ 1.6663,  3.9233, -8.0698]])
tensor([[-3.4482, -1.9134, -2.6599],
        [ 1.6663,  3.9233, -8.0698]])

我们重点说明一下嵌套循环的代码部分

在这个嵌套循环中:

  • 外层循环:遍历 b 和 a 的所有可能组合。这对应于最终结果张量 target 的两个维度 (b_, a_)。
  • 内层循环:对于每一对 (b, a),通过两个更内层的循环遍历 n 和 m 的所有可能值。这些循环实际上是在计算 l[b, n] * A[a, n, m] * r[b, m] 的和。
  • 计算和累加:在最内层的循环中,对于每个 n 和 m,计算 l[b, n] * A[a, n, m] * r[b, m],并将结果累加到 temp 变量中。这个累加过程实际上是在模拟 einsum 中的求和操作。
  • 存储结果:完成内层循环后,将累加的结果 temp 存储到 target[b, a] 中。

计算原理
这个计算过程实际上是在对每个 (b, a) 组合,计算 l 的第 b 行、A 的第 a 个矩阵和 r 的第 b 行之间的所有元素的乘积和。这可以看作是一种复杂的点积运算,其中 l 和 r 分别为 A 的每个矩阵提供了行和列的加权。

这段代码展示了如何使用 torch.einsum 来执行复杂的张量运算,这在深度学习和科学计算中非常有用。同时,它也展示了如何通过更传统的编程方法(嵌套循环)来实现相同的计算,虽然这种方法在大规模数据上通常不如 einsum 高效,但是可以更形象的理解复杂einsum的机制。


六、总结

本文探讨了numpy.einsum和torch.einsum在科学计算和深度学习中的应用与实践。我们首先介绍了Einstein求和记号的概念及其在简化复杂运算中的作用,随后详细阐述了numpy.einsum和torch.einsum的使用方法。

对于numpy.einsum,我们展示了其进行矩阵乘法、向量点积和按元素运算等基础操作。在讨论torch.einsum时,进一步分析了torch.einsum在复杂运算(如注意力机制)中的作用和优势,以及在自动微分和梯度计算中的具体实现。我们还分享了使用 pytorch 的 torch.einsum 函数来执行一个复杂的张量运算,以及如何通过嵌套循环手动实现相同的运算,以达到对einsum机制的深入理解。

这篇博客旨在提供全面而深入的理解,使读者能够熟练掌握numpy.einsum和torch.einsum的使用,并能够在实际项目中有效地利用它们来提高计算效率和代码可读性。通过学习和实践这些知识,读者将能够更好地应对各种科学计算和深度学习任务。

文章来源:https://blog.csdn.net/m0_51661400/article/details/135215516
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。