首页 > 解决方案 > PyTorch 的 torch.as_strided 具有负步幅,用于制作 Toeplitz 矩阵

问题描述

我正在编写一个陪审团操纵的 PyTorch 版本scipy.linalg.toeplitz,目前具有以下形式:

def toeplitz_torch(c, r=None):
    c = torch.tensor(c).ravel()
    if r is None:
        r = torch.conj(c)
    else:
        r = torch.tensor(r).ravel()

    # Flip c left to right.
    idx = [i for i in range(c.size(0)-1, -1, -1)]
    idx = torch.LongTensor(idx)
    c = c.index_select(0, idx)

    vals = torch.cat((c, r[1:]))
    out_shp = len(c), len(r)
    n = vals.stride(0)

    return torch.as_strided(vals[len(c)-1:], size=out_shp, stride=(-n, n)).copy()

torch.as_strided目前不支持负跨步。因此,我的函数会引发错误:

RuntimeError : as_strided: 目前不支持负步幅,得到了 strides: [-1, 1]

我的(也许是不正确的)理解as_strided是,它将第一个参数的值插入到一个新数组中,该数组的大小由第二个参数指定,它通过线性索引原始数组中的这些值并将它们放置在下标索引的步幅上由最后的论点给出。

NumPy 和 PyTorch 文档as_strided都对使用“极其小心”的功能提出了可怕的警告,我不完全理解这个功能,所以我想问一下:

  1. 我的理解as_strided正确吗?
  2. 有没有一种简单的方法来重写这个,所以负面的进展有效?
  3. 我能否通过渐变 wrt c(或rtoeplitz_torch

标签: pythonnumpyscipypytorchstride

解决方案


> 1. 我的理解as_strided正确吗?

步幅是张量访问底层连续数据缓冲区的接口。它不插入值,不复制值torch.as_strided,跨步定义了我们称为多维数组(在 NumPy 中)或张量(在 PyTorch 中)的人工布局。

正如Andreas K.在另一个答案中所说:

步幅是为了沿数组的每个方向/维度从一项到下一项而在内存中跳过的字节数。换句话说,它是每个维度的连续项目之间的字节分隔。

如果您在大步方面遇到问题,请随时阅读那里的答案。在这里,我们将以您的示例来看看它是如何实现的as_strided

Scipy 给出的示例linalg.toeplitz如下:

>>> toeplitz([1,2,3], [1,4,5,6])
array([[1, 4, 5, 6],
       [2, 1, 4, 5],
       [3, 2, 1, 4]])

为此,他们首先构造值列表(我们可以将其称为基础值,而不是实际的基础数据):vals其构造为[3 2 1 4 5 6]Toeplitz 列和行被展平。

现在注意传递给的参数np.lib.stride_tricks.as_strided

  • valuesvals[len(c)-1:]注意切片:张量显示更小,但基础值仍然存在,它们对应于vals. 继续将两者与 进行比较storage_offset:它只是 的偏移量2,值仍然存在!它的工作原理是它实质上改变了索引,例如index=0将引用 value 1index=1to4等...

  • shape:由列/行输入给出,这里(3, 4)。这是生成的对象的形状。

  • strides: 这是最重要的部分: (-n, n), 在这种情况下(-1, 1)

与 strides 相关的最直观的事情是描述多维空间:(i, j) ∈ [0,3[ x [0,4[和扁平化的一维空间:之间的映射k ∈ [0, 3*4[。由于步幅等于(-n, n) = (-1, 1),映射为-n*i + n*j = -1*i + 1*j = j-i。从数学上讲,您可以将矩阵描述为扁平值向量M[i, j] = F[j-i]在哪里。F[3 2 1 4 5 6]

例如,让我们尝试使用i=1and j=2。如果您查看上面的 Topleitz 矩阵M[1, 2] = 4。的确F[k] = F[j-i] = F[1] = 4

如果您仔细观察,您会发现负步幅背后的技巧:它们允许您“引用”负索引:例如,如果您采用j=0and i=2,那么您会看到k=-2。记住如何通过切片vals给出偏移量。如果您查看它自己的底层数据存储,它仍然是,但有一个偏移量。(在这种情况下)的映射将是由于偏移量。这意味着。2vals[len(c)-1:][3 2 1 4 5 6]valsi: 1D -> k: 1DM'[i] = F'[k] = F'[i+2]M'[-2] = F'[0] = 3

在上面我定义M'vals[len(c)-1:]which 基本上等同于以下张量:

>>> torch.as_strided(vals, size=(len(vals)-2,), stride=(1,), storage_offset=2)
tensor([1, 4, 5, 6])

同样,我将其定义F'为基础值的扁平向量:[3 2 1 4 5 6].

使用 strides 确实是定义 Toeplitz 矩阵的一种非常巧妙的方法!


> 2. 有没有一种简单的方法来重写这个负步幅工作?

问题是,PyTorch 中没有实现负面跨步......我不相信有办法解决它torch.as_strided,否则扩展当前实现并为该功能提供支持会相当容易。

然而,有解决问题的替代方法。在 PyTorch 中构建一个 Toeplitz 矩阵是完全可能的,但在torch.as_strided.

我们将自己进行映射:对于M索引为 的每个元素(i, j),我们将找出对应的索引k,即简单的j-i。这可以很容易地完成,首先通过收集(i, j)来自的所有对M

>>> i, j = torch.ones(3, 4).nonzero().T
(tensor([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]),
 tensor([0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]))

现在我们基本上有k

>>> j-i
tensor([ 0,  1,  2,  3, -1,  0,  1,  2, -2, -1,  0,  1])

我们只需要从行r和列c输入中构建所有可能值的展平张量。负索引值( 的内容c)放在最后并翻转:

>>> values = torch.cat((r, c[1:].flip(0)))
tensor([1, 4, 5, 6, 3, 2])

最后索引valuesk重塑:

>>> values[j-i].reshape(3, 4)
tensor([[1, 4, 5, 6],
        [2, 1, 4, 5],
        [3, 2, 1, 4]])

总而言之,我建议的实现是:

def toeplitz(c, r):
    vals = torch.cat((r, c[1:].flip(0)))
    shape = len(c), len(r)
    i, j = torch.ones(*shape).nonzero().T
    return vals[j-i].reshape(*shape)

> 3. 我能通过渐变 wrt c(or r)toeplitz_torch吗?

这是一个有趣的问题,因为torch.as_strided没有实现后向功能。这意味着您将无法反向传播到cand r!但是,使用上述使用“向后兼容”内置函数的方法,向后传递是免费的。

注意grad_fn输出:

>>> toeplitz(torch.tensor([1.,2.,3.], requires_grad=True), 
             torch.tensor([1.,4.,5.,6.], requires_grad=True))
tensor([[1., 4., 5., 6.],
        [2., 1., 4., 5.],
        [3., 2., 1., 4.]], grad_fn=<ViewBackward>)

这是一个快速草稿(确实需要一些时间来写下来),我会进行一些编辑。
如果您有任何问题或意见,请不要犹豫发表评论!
我有兴趣看到其他答案,因为我不是有进步的专家,这只是我对这个问题的看法。


推荐阅读