【问题标题】:Non-trivial sums of outer products without temporaries in numpynumpy 中没有临时变量的外部产品的非平凡总和
【发布时间】:2023-03-30 09:48:01
【问题描述】:

我希望解决的实际问题是,给定一组 N 个单位向量和另一组 M 个向量,计算每个单位向量的平均值它与每个 M 向量的点积的绝对值。本质上,这是计算两个矩阵的外积,然后用介于两者之间的绝对值求和和平均。

对于不太大的 NM 这并不难,并且有很多方法可以继续(见下文)。问题是当 NM 很大时,创建的临时变量很大,并为所提供的方法提供了实际限制。可以在不创建临时对象的情况下完成此计算吗?我遇到的主要困难是由于绝对值的存在。是否有用于“线程化”此类计算的通用技术?

以下面的代码为例

N = 7
M = 5

# Create the unit vectors, just so we have some examples,
# this is not meant to be elegant
phi = np.random.rand(N)*2*np.pi
ctheta = np.random.rand(N)*2 - 1
stheta = np.sqrt(1-ctheta**2)
nhat = np.array([stheta*np.cos(phi), stheta*np.sin(phi), ctheta]).T

# Create the other vectors
m = np.random.rand(M,3)

# Calculate the quantity we desire, here using broadcasting.
S = np.average(np.abs(np.sum(nhat*m[:,np.newaxis,:], axis=-1)), axis=0)

这很好,S 现在是一个长度为 N 的数组,并包含所需的结果。不幸的是,在这个过程中,我们创建了一些潜在的巨大数组。结果

np.sum(nhat*m[:,np.newaxis,:], axis=-1)

是一个 M X N 数组。当然,最终结果的大小只有 N。开始增加 NM 的大小,我们很快就会遇到内存错误。

如上所述,如果不需要绝对值,那么我们可以如下进行,现在使用einsum()

T = np.einsum('ik,jk,j', nhat, m, np.ones(M)) / M

即使对于相当大的 NM ,这也可以快速运行。对于具体问题,我需要包含abs(),但更通用的解决方案(也许是更通用的 ufunc)也很有趣。

【问题讨论】:

  • 你能用dot 和一些轴摆弄来做吗?
  • tensordot 可能更有用。
  • 由于需要abs(),这有什么帮助并不明显。如果不是因为问题中的einsum() 表达式将是理想的!
  • 我认为您可以使用dottensordot 来生成传递给abs 的临时值,从而节省一两个数量级的内存,因为您不必使用sum。不过,我怀疑你能否避免abs 电话。
  • 嗯...如果您正在使用 R^3 之类的东西而不是无数参数配置空间,这只会将您的最大内存消耗降低到旧值的 1/3。这可能还不够。

标签: python optimization numpy


【解决方案1】:

根据一些 cmets,似乎使用 cython 是最好的方法。我愚蠢地从未考虑过使用cython。事实证明,生成工作代码相对容易。

经过一番搜索,我整理了以下 cython 代码。这不是最通用的代码,可能不是最好的编写方式,并且可能会变得更高效。即便如此,它只比原始问题中的einsum() 代码慢了大约 25%,所以还不错!它已被编写为与原始问题中创建的数组一起显式工作(因此假定输入数组的模式)。
尽管有一些警告,但它确实为原始问题提供了一个相当有效的解决方案,并且可以作为类似情况的起点。

import numpy as np
cimport numpy as np
import cython
DTYPE = np.float64
ctypedef np.float64_t DTYPE_t
cdef inline double d_abs (double a) : return a if a >= 0 else -a

@cython.boundscheck(False)
@cython.wraparound(False)
def process_vectors (np.ndarray[DTYPE_t, ndim=2, mode="fortran"] nhat not None,
                     np.ndarray[DTYPE_t, ndim=2, mode="c"] m not None) :
    if nhat.shape[1] != m.shape[1] :
        raise ValueError ("Arrays must contain vectors of the same dimension")
    cdef Py_ssize_t imax = nhat.shape[0]
    cdef Py_ssize_t jmax = m.shape[0]
    cdef Py_ssize_t kmax = nhat.shape[1] # same as m.shape[1]
    cdef np.ndarray[DTYPE_t, ndim=1] S = np.zeros(imax, dtype=DTYPE)
    cdef Py_ssize_t i, j, k
    cdef DTYPE_t val, tmp
    for i in range(imax) :
        val = 0
        for j in range(jmax) :
            tmp = 0
            for k in range(kmax) :
                tmp += nhat[i,k] * m[j,k]
            val += d_abs(tmp)
        S[i] = val / jmax
    return S

【讨论】:

    【解决方案2】:

    我认为没有任何简单的方法(除了 Cython 等)来加快您的精确操作。但是您可能需要考虑是否真的需要计算您正在计算的内容。因为如果您可以使用root mean square 而不是绝对值的平均值,您仍然会以某种方式平均内积的大小,但您可以一次性获得它:

    rms = np.sqrt(np.einsum('ij,il,kj,kl,k->i', nhat, nhat, m, m, np.ones(M)/M))
    

    这和做的一样:

    rms_2 = np.sqrt(np.average(np.einsum('ij,kj->ik', nhat, m)**2, axis=-1))
    

    是的,这并不完全符合您的要求,但恐怕它与您使用矢量化方法所获得的一样接近。如果你决定走这条路,看看np.einsum 对大型NM 的表现如何:当传递太多参数和索引时,它往往会陷入困境。

    【讨论】:

      【解决方案3】:

      这有点慢,但不会创建大的中间矩阵。

      vals = np.zeros(N)
      for i in xrange(N):
          u = nhat[i]
          for v in m:
              vals[i]+=abs(np.dot(u,v))
          vals[i]=vals[i]/M
      

      编辑:将除以 M 移到 for 循环之外。

      edit2:新想法,保留旧想法以供后代和相关评论。

      m2 = np.average(m,0)
      vals = np.zeros(N)
      for i in xrange(N):
          u=nhat[i]
          vals[i]=abs(np.dot(u,m2))
      

      这很快,但有时会给出不同的值,我正在研究为什么但同时它可能会有所帮助。

      edit 3:啊,这是绝对价值的东西。嗯

      >>> S
      array([ 0.28620962,  0.65337876,  0.37470707,  0.46500913,  0.49579837,
              0.29348924,  0.27444208,  0.74586928,  0.35789315,  0.3079964 ,
              0.298353  ,  0.42571445,  0.32535728,  0.87505053,  0.25547394,
              0.23964505,  0.44773271,  0.25235646,  0.4722281 ,  0.33003338])
      >>> vals
      array([ 0.2099343 ,  0.6532155 ,  0.33039334,  0.45366889,  0.48921527,
              0.20467291,  0.16585856,  0.74586928,  0.31234917,  0.22198642,
              0.21013519,  0.41422894,  0.26020981,  0.87505053,  0.1199069 ,
              0.06542492,  0.44145805,  0.08455833,  0.46824704,  0.28483342])
      
      time to compute S: 0.000342130661011 seconds
      time to compute vals: 7.29560852051e-05 seconds
      

      编辑 4:好吧,如果您的单位向量大部分为正值,这应该运行得更快,假设 m 中的向量总是正的,就像它们在您的虚拟数据中一样。

      m2 = np.average(m,0)
      vals = np.zeros(N)
      for i in xrange(N):
          u=nhat[i]
          if u[0] >= 0 and u[1] >= 0 and u[2] >= 0:
              vals[i] = abs(np.dot(u,m2))
          else:
              for j in xrange(M):
                  vals[i]+=abs(np.dot(u,m[j]))
              vals[i]/=M
      

      【讨论】:

      • 这当然可行,但是对于任何较大的 NM 值,这将非常缓慢。它可以通过使用单个循环并仅在 NM 中的 one 上广播(类似于我在问题中提供的代码)来加快速度但适用于单个nhatm)。但即便如此,对于 NM large 而言,这也是非常缓慢的。
      • 您可以使用 cython 进一步提高 for 循环的速度。它还可以改进唯一值的索引,例如 vals[i](不是切片)。
      • 在 python 中只执行一个循环时,只要至少 N 和 M 中较大的一个被矢量化,就会出现显着的性能差异,这听起来令人难以置信。如果 N 或 M 真的那么大,那么相对于 numpy 计算,循环开销不应该占主导地位。你实际上测试过这个吗?如果您确实决定需要一个 C 循环,请查看 numba。还没有 cython 成熟,但对于这种类型的功能,它可以完美运行。
      猜你喜欢
      • 2021-06-28
      • 1970-01-01
      • 2013-10-24
      • 1970-01-01
      • 2012-08-16
      • 1970-01-01
      • 1970-01-01
      • 2017-06-08
      • 1970-01-01
      相关资源
      最近更新 更多