【问题标题】:Fast way to convert upper triangular matrix into symmetric matrix将上三角矩阵转换为对称矩阵的快速方法
【发布时间】:2020-09-25 12:44:17
【问题描述】:

我有一个np.float64 值的上三角矩阵,如下所示:

array([[ 1.,  2.,  3.,  4.],
       [ 0.,  5.,  6.,  7.],
       [ 0.,  0.,  8.,  9.],
       [ 0.,  0.,  0., 10.]])

我想把这个转换成对应的对称矩阵,像这样:

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

转换可以就地完成,也可以作为新矩阵完成。我希望它尽可能快。我怎样才能快速做到这一点?

【问题讨论】:

  • 通常的问题大小是多少?您是否有 2d 数组的列表,例如(6x6)或更简单的 3d 数组(10_000x6x6)?
  • 就我而言,我目前正在处理一个 4x4 矩阵,但也对 10x10 左右的情况感兴趣。

标签: python numpy optimization


【解决方案1】:

np.where 在异地、无缓存场景中似乎相当快:

np.where(ut,ut,ut.T)

在我的笔记本电脑上:

timeit(lambda:np.where(ut,ut,ut.T))
# 1.909718865994364

如果您安装了 pythran,您可以以几乎零的努力将其加速 3 倍。但请注意,据我所知,pythran(目前)只理解连续数组。

文件<upp2sym.py>,用pythran -O3 upp2sym.py编译

import numpy as np

#pythran export upp2sym(float[:,:])

def upp2sym(a):
    return np.where(a,a,a.T)

时间:

from upp2sym import *

timeit(lambda:upp2sym(ut))
# 0.5760842661838979

这几乎和循环一样快:

#pythran export upp2sym_loop(float[:,:])

def upp2sym_loop(a):
    out = np.empty_like(a)
    for i in range(len(a)):
        out[i,i] = a[i,i]
        for j in range(i):
            out[i,j] = out[j,i] = a[j,i]
    return out

时间:

timeit(lambda:upp2sym_loop(ut))
# 0.4794591029640287

我们也可以就地做:

#pythran export upp2sym_inplace(float[:,:])

def upp2sym_inplace(a):
    for i in range(len(a)):
        for j in range(i):
            a[i,j] = a[j,i]

时间

timeit(lambda:upp2sym_inplace(ut))
# 0.28711927914991975

【讨论】:

  • 这非常好,我机器上的 4x4 阵列需要 1.8 μs。仍然比我最快的代码慢一点,但明显更简单。
  • (注意上面的注释是针对np.where(ut,ut,ut.T)的纯Python实现)
【解决方案2】:

这是迄今为止我发现的最快的例程,它不使用 Cython 或像 Numba 这样的 JIT。我在我的机器上处理一个 4x4 数组大约需要 1.6 μs(100K 4x4 数组列表的平均时间):

inds_cache = {}

def upper_triangular_to_symmetric(ut):
    n = ut.shape[0]
    try:
        inds = inds_cache[n]
    except KeyError:
        inds = np.tri(n, k=-1, dtype=np.bool)
        inds_cache[n] = inds
    ut[inds] = ut.T[inds]

以下是我尝试过的其他一些不太快的方法:

上面的代码,但是没有缓存。每个 4x4 阵列大约需要 8.3 μs:

def upper_triangular_to_symmetric(ut):
    n = ut.shape[0]
    inds = np.tri(n, k=-1, dtype=np.bool)
    ut[inds] = ut.T[inds]

一个普通的 Python 嵌套循环。每个 4x4 阵列大约需要 2.5 μs:

def upper_triangular_to_symmetric(ut):
    n = ut.shape[0]
    for r in range(1, n):
        for c in range(r):
            ut[r, c] = ut[c, r]

使用np.triu 进行浮点加法。每个 4x4 阵列大约需要 11.9 μs:

def upper_triangular_to_symmetric(ut):
    ut += np.triu(ut, k=1).T

Numba 版本的 Python 嵌套循环。这是我发现的最快的东西(每个 4x4 数组大约 0.4 μs),并且是我最终在生产中使用的东西,至少在我开始遇到 Numba 问题并不得不恢复到纯 Python 版本之前:

import numba

@numba.njit()
def upper_triangular_to_symmetric(ut):
    n = ut.shape[0]
    for r in range(1, n):
        for c in range(r):
            ut[r, c] = ut[c, r]

Python 嵌套循环的 Cython 版本。我是 Cython 的新手,所以这可能没有完全优化。由于 Cython 增加了运营开销,我有兴趣听到 Cython 和纯 Numpy 的答案。每个 4x4 阵列大约需要 0.6 μs:

cimport numpy as np
cimport cython

@cython.boundscheck(False)
@cython.wraparound(False)
def upper_triangular_to_symmetric(np.ndarray[np.float64_t, ndim=2] ut):
    cdef int n, r, c
    n = ut.shape[0]
    for r in range(1, n):
        for c in range(r):
            ut[r, c] = ut[c, r]

【讨论】:

  • ut += ut.T; ut.flat[::ut.shape[0]+1] *= 0.5 怎么样?
  • @MarkDickinson 也很慢(~5.7 μs)。问题是您正在执行浮点运算(加法和乘法),这比复制数据要慢得多。
  • @KerrickStaley 我不确定是 fp 操作。单独尝试ut+ut.T。它非常快。在这个操作数大小下,主要是 Python 开销减慢了速度。顺便提一句。我已经更新了我的答案。
【解决方案3】:

你主要是测量这些小问题的函数调用开销

另一种方法是使用 Numba。让我们从一个只有一个 (4x4) 数组的实现开始。

只有一个 4x4 阵列

import numpy as np
import numba as nb

@nb.njit()
def sym(A):
    for i in range(A.shape[0]):
        for j in range(A.shape[1]):
            A[j,i]=A[i,j]
    return A


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

%timeit sym(A)
#277 ns ± 5.21 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

更大的例子

@nb.njit(parallel=False)
def sym_3d(A):
    for i in nb.prange(A.shape[0]):
        for j in range(A.shape[1]):
            for k in range(A.shape[2]):
                A[i,k,j]=A[i,j,k]
    return A

A=np.random.rand(1_000_000,4,4)

%timeit sym_3d(A)
#13.8 ms ± 49.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
#13.8 ns per 4x4 submatrix

【讨论】:

  • 不错! sym 在我的机器上获得大约 0.5 µs / 阵列。您可以通过仅处理您需要的索引而不是数组中的每个索引来使其低于 0.4 µs(因此将 numba.njit() 应用于我的代码中的“普通 Python 嵌套循环”版本)。
  • @KerrickStaley 0.5µs 看起来非常慢(比我的测量慢 2 倍)。你如何得到这个时间?我也没有真正与您提出的方法有任何区别。即使我将所有有用的代码注释掉(直接在第二行返回),它也需要 248ns vs.277ns。
  • 我有一个包含 100 万个 4x4 矩阵的列表,我正在 Jupyter 笔记本中为 for 循环“输入中的 ut:upper_triangular_to_symmetric(ut)”计时,然后除以 100 万。当我将 upper_triangular_to_symmetric 的实现更改为无操作时,我得到 0.1 µs,因此显然不是所有的函数开销。
  • @KerrickStaley 如果您有所有 4x4 大小数组的列表,您可以将其转换为 3D 数组,并使用基于掩码的解决方案以矢量化方式工作。应该直截了当。
  • @Divakar 这实际上并不是我的代码在生产中的工作方式,这只是我用来比较不同方法的综合基准。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-10-03
  • 1970-01-01
  • 1970-01-01
  • 2017-06-17
  • 1970-01-01
  • 1970-01-01
  • 2022-11-29
相关资源
最近更新 更多