【问题标题】:How to optimize math operations on matrix in python如何在python中优化矩阵的数学运算
【发布时间】:2014-04-12 16:55:23
【问题描述】:

我正在尝试减少使用两个矩阵执行一系列计算的函数的时间。搜索这个,我听说过 numpy,但我真的不知道如何将它应用于我的问题。另外,我认为让我的功能变慢的原因之一是有很多点运算符(我在this page 中听说过)。

数学对应于二次分配问题的因式分解:

我的代码是:

    delta = 0
    for k in xrange(self._tam):
        if k != r and k != s:
            delta +=
                self._data.stream_matrix[r][k] \
                * (self._data.distance_matrix[sol[s]][sol[k]] - self._data.distance_matrix[sol[r]][sol[k]]) + \
                self._data.stream_matrix[s][k] \
                * (self._data.distance_matrix[sol[r]][sol[k]] - self._data.distance_matrix[sol[s]][sol[k]]) + \
                self._data.stream_matrix[k][r] \
                * (self._data.distance_matrix[sol[k]][sol[s]] - self._data.distance_matrix[sol[k]][sol[r]]) + \
                self._data.stream_matrix[k][s] \
                * (self._data.distance_matrix[sol[k]][sol[r]] - self._data.distance_matrix[sol[k]][sol[s]])
    return delta

在大小为 20(20x20 的矩阵)的问题上运行这个大约需要 20 段,瓶颈在这个函数中

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
303878   15.712    0.000   15.712    0.000 Heuristic.py:66(deltaC)

我尝试将map 应用于for循环,但由于循环体不是函数调用,所以不可能。

我怎样才能减少时间?

编辑1

回答艾肯伯格的评论:

sol 是一个排列,例如 [1,2,3,4]。当我生成邻居解决方案时调用该函数,因此,[1,2,3,4] 的邻居是 [2,1,3,4]。我只更改原始排列中的两个位置,然后调用deltaC,它计算位置 r,s 交换的解的因式分解(在上面的示例中 r,s = 0,1)。进行这种排列是为了避免计算邻居解决方案的全部成本。我想我可以将sol[k,r,s] 的值存储在一个局部变量中,以避免在每次迭代中查找它的值。 我不知道这是否是你在评论中提出的问题。

编辑2

一个最小的工作示例:

import random


distance_matrix = [[0, 12, 6, 4], [12, 0, 6, 8], [6, 6, 0, 7], [4, 8, 7, 0]]
stream_matrix = [[0, 3, 8, 3], [3, 0, 2, 4], [8, 2, 0, 5], [3, 4, 5, 0]]

def deltaC(r, s, S=None):
    '''
    Difference between C with values i and j swapped
    '''

    S = [0,1,2,3]

    if S is not None:
        sol = S
    else:
        sol = S

    delta = 0

    sol_r, sol_s = sol[r], sol[s]

    for k in xrange(4):
        if k != r and k != s:
            delta += (stream_matrix[r][k] \
                * (distance_matrix[sol_s][sol[k]] - distance_matrix[sol_r][sol[k]]) + \
                stream_matrix[s][k] \
                * (distance_matrix[sol_r][sol[k]] - distance_matrix[sol_s][sol[k]]) + \
                stream_matrix[k][r] \
                * (distance_matrix[sol[k]][sol_s] - distance_matrix[sol[k]][sol_r]) + \
                stream_matrix[k][s] \
                * (distance_matrix[sol[k]][sol_r] - distance_matrix[sol[k]][sol_s]))
    return delta


for _ in xrange(303878):
    d = deltaC(random.randint(0,3), random.randint(0,3))
print d

现在我认为更好的选择是使用 NumPy。我尝试使用 Matrix(),但没有提高性能。

找到最佳解决方案

好吧,最后我能够减少更多的时间,结合@TooTone 的解决方案并将索引存储在一个集合中以避免 if。时间已经从大约 18 秒下降到 8 秒。代码如下:

def deltaC(self, r, s, sol=None):
    delta = 0
    sol = self.S if sol is None else self.S
    sol_r, sol_s = sol[r], sol[s]

    stream_matrix = self._data.stream_matrix
    distance_matrix = self._data.distance_matrix

    indexes = set(xrange(self._tam)) - set([r, s])

    for k in indexes:
        sol_k = sol[k]
        delta += \
            (stream_matrix[r][k] - stream_matrix[s][k]) \
            * (distance_matrix[sol_s][sol_k] - distance_matrix[sol_r][sol_k]) \
            + \
            (stream_matrix[k][r] - stream_matrix[k][s]) \
            * (distance_matrix[sol_k][sol_s] - distance_matrix[sol_k][sol_r])
    return delta

为了进一步减少时间,我认为最好的方法是编写一个模块。

【问题讨论】:

  • 你应该看看 numpy 以优化数值计算。它是一个成熟的库,正是为此而设计的。而且他们的代码几乎总是比您自己编写的代码更优化。
  • 第一次尝试应该总是尝试向量化你的 numpy 操作。到目前为止,您的代码对于 numpy 来说是次优的:使用 for 循环并查找例如sol[s] 每次迭代,尽管它保持不变。在尝试提出解决方案之前,如果您能告诉我们是否必须对所有r, s 执行此操作以及sol 是否是索引的固定排列,那就太好了。如果矢量化不起作用(但它应该),那么您可以查看编译数值表达式,使用例如numexpr,但我会在以后保留它
  • 另外,你能告诉我们你想在哪个维度上使用它吗?
  • 谢谢,我试着回答你编辑我原来的问题。
  • 如果 self._data.stream_matrixself._data.distance_matrix 是一个 numpy 矩阵类,它会更有效吗?

标签: python numpy matrix heuristics


【解决方案1】:

在您给出的简单示例中,使用for k in xrange(4):,循环体只执行两次(如果是r==s)或三次(如果是r!=s),并且下面的初始numpy实现慢了一个大的因素。 Numpy 已针对在长向量上执行计算进行了优化,如果向量很短,则开销可能会超过好处。 (请注意,在这个公式中,矩阵被分割成不同的维度,并且索引不连续,这只会使向量化实现变得更加复杂)。

import numpy as np

distance_matrix_np = np.array(distance_matrix)
stream_matrix_np = np.array(stream_matrix)
n = 4

def deltaC_np(r, s, sol):
    delta = 0
    sol_r, sol_s = sol[r], sol[s]

    K = np.array([i for i in xrange(n) if i!=r and i!=s])

    return np.sum(
        (stream_matrix_np[r,K] - stream_matrix_np[s,K]) \
        *  (distance_matrix_np[sol_s,sol[K]] - distance_matrix_np[sol_r,sol[K]]) + \
        (stream_matrix_np[K,r] - stream_matrix_np[K,s]) \
        * (distance_matrix_np[sol[K],sol_s] - distance_matrix_np[sol[K],sol_r]))

在这个 numpy 实现中,不是对 K 中的元素进行 for 循环,而是将操作应用于 numpy 中 K 中的所有元素。另外,请注意您的数学表达式可以简化。左边括号里的每一项都是右边括号里的词的负数。

这也适用于您的原始代码。比如(self._data.distance_matrix[sol[s]][sol[k]] - self._data.distance_matrix[sol[r]][sol[k]])等于-1乘以(self._data.distance_matrix[sol[r]][sol[k]] - self._data.distance_matrix[sol[s]][sol[k]]),所以你做了不必要的计算,你的原始代码可以在不使用numpy的情况下进行优化。

事实证明,numpy 函数的瓶颈在于看似无辜的列表理解

K = np.array([i for i in xrange(n) if i!=r and i!=s])

一旦将其替换为矢量化代码

if r==s:
    K=np.arange(n-1)
    K[r:] += 1
else:
    K=np.arange(n-2)
    if r<s:
        K[r:] += 1
        K[s-1:] += 1
    else:
        K[s:] += 1
        K[r-1:] += 1

numpy 函数要快得多

运行时间图如下所示(此答案底部的右侧是优化 numpy 函数之前的原始图)。您可以看到使用优化后的原始代码或 numpy 代码是否有意义,具体取决于矩阵的大小。

下面是完整的代码供参考,部分以防其他人可以更进一步。 (函数deltaC2 是您的原始代码优化考虑了数学表达式可以简化的方式。)

def deltaC(r, s, sol):
    delta = 0
    sol_r, sol_s = sol[r], sol[s]
    for k in xrange(n):
        if k != r and k != s:
            delta += \
                stream_matrix[r][k] \
                * (distance_matrix[sol_s][sol[k]] - distance_matrix[sol_r][sol[k]]) + \
                stream_matrix[s][k] \
                * (distance_matrix[sol_r][sol[k]] - distance_matrix[sol_s][sol[k]]) + \
                stream_matrix[k][r] \
                * (distance_matrix[sol[k]][sol_s] - distance_matrix[sol[k]][sol_r]) + \
                stream_matrix[k][s] \
                * (distance_matrix[sol[k]][sol_r] - distance_matrix[sol[k]][sol_s])
    return delta

import numpy as np

def deltaC_np(r, s, sol):
    delta = 0
    sol_r, sol_s = sol[r], sol[s]

    if r==s:
        K=np.arange(n-1)
        K[r:] += 1
    else:
        K=np.arange(n-2)
        if r<s:
            K[r:] += 1
            K[s-1:] += 1
        else:
            K[s:] += 1
            K[r-1:] += 1
    #K = np.array([i for i in xrange(n) if i!=r and i!=s]) #TOO SLOW

    return np.sum(
        (stream_matrix_np[r,K] - stream_matrix_np[s,K]) \
        *  (distance_matrix_np[sol_s,sol[K]] - distance_matrix_np[sol_r,sol[K]]) + \
        (stream_matrix_np[K,r] - stream_matrix_np[K,s]) \
        * (distance_matrix_np[sol[K],sol_s] - distance_matrix_np[sol[K],sol_r]))

def deltaC2(r, s, sol):
    delta = 0
    sol_r, sol_s = sol[r], sol[s]
    for k in xrange(n):
        if k != r and k != s:
            sol_k = sol[k]
            delta += \
                (stream_matrix[r][k] - stream_matrix[s][k]) \
                * (distance_matrix[sol_s][sol_k] - distance_matrix[sol_r][sol_k]) \
                + \
                (stream_matrix[k][r] - stream_matrix[k][s]) \
                * (distance_matrix[sol_k][sol_s] - distance_matrix[sol_k][sol_r])
    return delta


import time

N=200

elapsed1s = []
elapsed2s = []
elapsed3s = []
ns = range(10,410,10)
for n in ns:
    distance_matrix_np=np.random.uniform(0,n**2,size=(n,n))
    stream_matrix_np=np.random.uniform(0,n**2,size=(n,n))
    distance_matrix=distance_matrix_np.tolist()
    stream_matrix=stream_matrix_np.tolist()
    sol  = range(n-1,-1,-1)
    sol_np  = np.array(range(n-1,-1,-1))

    Is = np.random.randint(0,n-1,4)
    Js = np.random.randint(0,n-1,4)

    total1 = 0
    start = time.clock()
    for reps in xrange(N):
        for i in Is:
            for j in Js:
                total1 += deltaC(i,j, sol)
    elapsed1 = (time.clock() - start)
    start = time.clock()

    total2 = 0
    start = time.clock()
    for reps in xrange(N):
        for i in Is:
            for j in Js:
                total2 += deltaC_np(i,j, sol_np)
    elapsed2 = (time.clock() - start)

    total3 = 0
    start = time.clock()
    for reps in xrange(N):
        for i in Is:
            for j in Js:
                total3 += deltaC2(i,j, sol_np)
    elapsed3 = (time.clock() - start)

    print n, elapsed1, elapsed2, elapsed3, total1, total2, total3
    elapsed1s.append(elapsed1)
    elapsed2s.append(elapsed2)
    elapsed3s.append(elapsed3)

    #Check errors of one method against another
    #err = 0
    #for i in range(min(n,50)):
    #    for j in range(min(n,50)):
    #        err += np.abs(deltaC(i,j,sol)-deltaC_np(i,j,sol_np))
    #print err
import matplotlib.pyplot as plt

plt.plot(ns, elapsed1s, label='Original',lw=2)
plt.plot(ns, elapsed3s, label='Optimized',lw=2)
plt.plot(ns, elapsed2s, label='numpy',lw=2)
plt.legend(loc='upper left', prop={'size':16})
plt.xlabel('matrix size')
plt.ylabel('time')
plt.show()

这是优化deltaC_np 中的列表理解之前的原始图表

【讨论】:

  • 非常感谢您的出色工作。我正在尝试实现 delta_np,但我在 sol[k] 中得到了only integer arrays with one element can be converted to an index。以我的最小示例,在调用delta_np 后,这是调试时的图片:postimg.org/image/p276kbcxf
  • 也许我把你弄糊涂了,sol 必须是解决方案的排列,如果问题的大小为 4,则 sol 必须是 len 4 的列表。
  • @algui91 检查 sol 是一个 numpy 数组。当我开发这个时,我遇到了很多这样的错误(如果你有严重的问题,让我的完整程序在答案的底部首先工作)。如果可以的话,尝试在整个过程中使用 numpy 数组——见证我删除列表理解时的加速。
  • @algui91 我可能因为坚持一个最小的例子而把自己搞糊涂了!我给出的代码应该适用于任意矩阵大小。 (我发现对于非常大的矩阵,但是我开始收到带有整数元素的溢出消息,所以我切换到双精度。)出于兴趣什么是典型的矩阵大小?
  • 最大尺寸为 90x90
猜你喜欢
  • 2015-10-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-29
  • 2022-08-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多