Python中的特殊张量收缩

Python中的特殊张量收缩,python,numpy,linear-algebra,Python,Numpy,Linear Algebra,我需要做一种特殊的张量收缩。我想要这种东西: A{bg}=Sum{A,A',A'}(B{A}C{A'B}D{A''g}) 其中,所有索引的值都可以为0,1,并且对于a+a'+a''=1或a+a'+a''=2的所有情况,都会携带a、a'和a''的和。所以这就像爱因斯坦求和惯例的反面:我只想在三个指数中的一个与其他指数不同时求和 此外,我希望在不求和的索引数量上有一定的灵活性:在这个例子中,结果张量有两个索引,总和是三个张量元素的乘积,一个有一个索引,另外两个有两个索引。这些指数的数量会有所不同,因

我需要做一种特殊的张量收缩。我想要这种东西:

A{bg}=Sum{A,A',A'}(B{A}C{A'B}D{A''g})

其中,所有索引的值都可以为0,1,并且对于a+a'+a''=1或a+a'+a''=2的所有情况,都会携带a、a'和a''的和。所以这就像爱因斯坦求和惯例的反面:我只想在三个指数中的一个与其他指数不同时求和

此外,我希望在不求和的索引数量上有一定的灵活性:在这个例子中,结果张量有两个索引,总和是三个张量元素的乘积,一个有一个索引,另外两个有两个索引。这些指数的数量会有所不同,因此总体而言,我希望能够写出如下内容:

tensa = np.zeros((2,2))
for be in range(2):
    for ga in range(2):
        for al in range(2):
            for alp in range(2):
                for alpp in range(res(al,alp),prod(al,alp)):
                    tensa[be,ga] += tensb[al] * tensc[alp,be] * tensd[alpp,ga]
A{…}=Sum{A,A',A'}(B{A..}C{A..}D{A''.})

我想指出的是,索引的数量不是固定的,而是受控的:我可以知道并指定每个张量在每一步中有多少个索引

我尝试了
np.einsum()
,但很明显,我被迫对标准爱因斯坦约定中的重复索引求和,我不知道如何实现我在这里暴露的条件

我不能用各种来写所有的,因为,正如我所说的,所涉及的张量的指数的数量不是固定的

有人有主意吗


从评论中: 我会用这样的编程语言来写我放在这里的东西:

tensa = np.zeros((2,2))
for be in range(2):
    for ga in range(2):
        for al in range(2):
            for alp in range(2):
                for alpp in range(res(al,alp),prod(al,alp)):
                    tensa[be,ga] += tensb[al] * tensc[alp,be] * tensd[alpp,ga]

其中,
res
prod
是确保
al+alp+alpp=1或2的两个功能。问题是,我需要指定所有涉及的索引,而我不能在所有晶格的常规计算中这样做。

首先,让我们用Python循环写出您的示例,以便有一个比较基准。如果我理解正确,这就是你想要做的:

b, g = 4, 5
B = np.random.rand(2)
C = np.random.rand(2, b)
D = np.random.rand(2, g)

out = np.zeros((b, g))
for j in (0, 1):
    for k in (0, 1):
        for l in (0, 1):
            if j + k + l in (1, 2):
                out += B[j] * C[k, :, None] * D[l, None, :]
当我运行此命令时,我得到以下输出:

>>> out
array([[ 1.27679643,  2.26125361,  1.32775173,  1.5517918 ,  0.47083151],
       [ 0.84302586,  1.57516142,  1.1335904 ,  1.14702252,  0.34226837],
       [ 0.70592576,  1.34187278,  1.02080112,  0.99458563,  0.29535054],
       [ 1.66907981,  3.07143067,  2.09677013,  2.20062463,  0.65961165]])
使用
np.einsum
无法直接获得此结果,但您可以运行两次,并根据这两个结果的差异获得结果:

>>> np.einsum('i,jk,lm->km', B, C, D) - np.einsum('i,ik,im->km', B, C, D)
array([[ 1.27679643,  2.26125361,  1.32775173,  1.5517918 ,  0.47083151],
       [ 0.84302586,  1.57516142,  1.1335904 ,  1.14702252,  0.34226837],
       [ 0.70592576,  1.34187278,  1.02080112,  0.99458563,  0.29535054],
       [ 1.66907981,  3.07143067,  2.09677013,  2.20062463,  0.65961165]])
np.einsum
的第一个调用是将所有内容相加,而不管索引加起来是什么。第二种方法仅将所有三个指数相同的指数相加。很明显,你的结果是两者的不同

理想情况下,您现在可以继续编写以下内容:

>>>(np.einsum('i...,j...,k...->...', B, C, D) -
... np.einsum('i...,i...,i...->...', B, C, D))
无论C和D数组的维数如何,都可以得到结果。如果尝试第一种方法,您将收到以下错误消息:

ValueError: operands could not be broadcast together with remapped shapes
[original->remapped]: (2)->(2,newaxis,newaxis) (2,4)->(4,newaxis,2,newaxis)
                      (2,5)->(5,newaxis,newaxis,2)
问题是,由于您没有指定要对张量的
b
g
维度执行什么操作,因此它尝试将它们广播在一起,并且由于它们不同,因此失败。您可以通过添加尺寸为1的额外尺寸使其正常工作:

>>> (np.einsum('i...,j...,k...->...', B, C, D[:, None]) -
...  np.einsum('i...,i...,i...->...', B, C, D[:, None]))
array([[ 1.27679643,  2.26125361,  1.32775173,  1.5517918 ,  0.47083151],
       [ 0.84302586,  1.57516142,  1.1335904 ,  1.14702252,  0.34226837],
       [ 0.70592576,  1.34187278,  1.02080112,  0.99458563,  0.29535054],
       [ 1.66907981,  3.07143067,  2.09677013,  2.20062463,  0.65961165]])
如果您希望将B的所有轴放在C的所有轴之前,并且这些轴放在D的所有轴之前,那么至少在创建正确形状的输出时,以下操作似乎是可行的,尽管您可能希望再次检查结果是否确实符合您的要求:

>>> B = np.random.rand(2, 3)
>>> C = np.random.rand(2, 4, 5)
>>> D = np.random.rand(2, 6)
>>> C_idx = (slice(None),) + (None,) * (B.ndim - 1)
>>> D_idx = C_idx + (None,) * (C.ndim - 1)
>>> (np.einsum('i...,j...,k...->...', B, C[C_idx], D[D_idx]) -
...  np.einsum('i...,i...,i...->...', B, C[C_idx], D[D_idx])).shape
(3L, 4L, 5L, 6L)

编辑根据注释,如果不是每个张量的第一个轴必须缩小,而是前两个,则上述内容可以写成:

>>> B = np.random.rand(2, 2, 3)
>>> C = np.random.rand(2, 2, 4, 5)
>>> D = np.random.rand(2, 2, 6)
>>> C_idx = (slice(None),) * 2 + (None,) * (B.ndim - 2)
>>> D_idx = C_idx + (None,) * (C.ndim - 2)
>>> (np.einsum('ij...,kl...,mn...->...', B, C[C_idx], D[D_idx]) -
...  np.einsum('ij...,ij...,ij...->...', B, C[C_idx], D[D_idx])).shape
(3L, 4L, 5L, 6L)
更一般地说,如果减少超过
d
指数,
C_idx
d_idx
将如下所示:

>>> C_idx = (slice(None),) * d + (None,) * (B.ndim - d)
>>> D_idx = C_idx + (None,) * (C.ndim - d)
np.einsum
的调用需要在索引中包含
d
字母,在第一次调用中唯一,在第二次调用中重复


编辑2那么
C_idx
D_idx
到底是怎么回事?以最后一个例子为例,使用
B
C
D
以及
(2,2,3)
(2,2,4,5)
(2,2,6)
C_idx
由两个空片组成,加上
None
s的数量等于
B
的维数减2,因此当我们取
C[C_idx]
时,结果具有形状
(2,2,1,4,5)
。类似地,
D_idx
C_idx
加上与
C
的维数减2一样多的
None
s,因此
D[D_idx]
的结果具有形状
(2,2,1,1,6)
。这三个数组不会一起进行braodcast,但是
np.einsum
添加了大小为1的额外维度,即上面错误的“重新映射”形状,因此生成的数组具有额外的尾随形状,形状匹配如下:

(2, 2, 3, 1, 1, 1)
(2, 2, 1, 4, 5, 1)
(2, 2, 1, 1, 1, 6)
前两个轴减小,因此从输出中消失,在其他情况下,广播应用,其中大小为1的维度被复制以匹配更大的维度,因此输出是
(3,4,5,6)


@hpaulj提出了一种使用“Levi-Civita-like”张量的方法,理论上应该更快,见我对原始问题的评论。下面是一些比较代码:

b, g = 5000, 2000
B = np.random.rand(2)
C = np.random.rand(2, b)
D = np.random.rand(2, g)

def calc1(b, c, d):
    return (np.einsum('i,jm,kn->mn', b, c, d) -
            np.einsum('i,im,in->mn', b, c, d))

def calc2(b, c, d):
    return np.einsum('ijk,i,jm,kn->mn', calc2.e, b, c, d)
calc2.e = np.ones((2,2,2))
calc2.e[0, 0, 0] = 0
calc2.e[1, 1, 1] = 0
但在运行时:

%timeit calc1(B, C, D)
1 loops, best of 3: 361 ms per loop

%timeit calc2(B, C, D)
1 loops, best of 3: 643 ms per loop

np.allclose(calc1(B, C, D), calc2(B, C, D))
Out[48]: True

一个令人惊讶的结果,我无法解释…

你能提供更多信息吗。你在什么地方工作。你是在某种置换群上求和吗?你在研究流形的切线空间吗?你能用编程符号而不是数学符号重写吗?@kolonel我在研究一个三角形晶格,我在每个三角形上做张量收缩。晶格是由反铁磁自旋组成的,这意味着在每个三角形中必须有两个“0”和一个“1”,或者一个“0”和两个“1”,所以这是与我相关的子空间。我不知道这个子空间在数学上是否有一个名字:)@Slater Tyranus我会这样写我在这里用编程语言写的:tensa=np。0((2,2))表示在范围内(2):表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内(2)表示在范围内)表示在范围内(2)表示在[be,ga]+\tensb[al]*te