【问题标题】:Speeding up dynamic programming in python/numpy在 python/numpy 中加速动态编程
【发布时间】:2013-12-24 01:52:47
【问题描述】:

我有一个 2D 成本矩阵 M,可能是 400x400,我正在尝试计算通过它的最佳路径。因此,我有一个类似的功能:

M[i,j] = M[i,j] + min(M[i-1,j-1],M[i-1,j]+P1,M[i,j-1]+P1)

这显然是递归的。 P1 是一些附加常数。我的代码或多或少是:

def optimalcost(cost, P1=10):
    width1,width2 = cost.shape
    M = array(cost)
    for i in range(0,width1):
       for j in range(0,width2):
          try:
              M[i,j] = M[i,j] + min(M[i-1,j-1],M[i-1,j]+P1,M[i,j-1]+P1)
          except:
              M[i,j] = inf
    return M

现在我知道在 Numpy 中循环是一个糟糕的主意,对于诸如计算初始成本矩阵之类的事情,我已经能够找到缩短时间的捷径。但是,由于我需要潜在地评估整个矩阵,所以我不确定该怎么做。这在我的机器上每次调用大约需要 3 秒,并且必须应用于大约 300 个这些成本矩阵。我不确定这个时间是从哪里来的,因为分析表明 200,000 次调用 min 只需要 0.1 秒 - 也许是内存访问?

有没有办法以某种方式并行执行此操作?我认为可能有,但对我来说,似乎每次迭代都是依赖的,除非有更聪明的方法来记忆事物。

与这个问题有相似之处:Can I avoid Python loop overhead on dynamic programming with numpy?

如有必要,我很乐意切换到 C,但我喜欢 Python 用于快速测试的灵活性以及缺乏文件 IO 的繁琐。在我的脑海中,类似以下代码的代码可能会明显更快吗?

#define P1 10
void optimalcost(double** costin, double** costout){
    /* 
        We assume that costout is initially
        filled with costin's values.
    */
    float a,b,c,prevcost;

    for(i=0;i<400;i++){
        for(j=0;j<400;j++){
            a = prevcost+P1;
            b = costout[i][j-1]+P1;
            c = costout[i-1][j-1];
            costout[i][j] += min(prevcost,min(b,c));
            prevcost = costout[i][j];
        }
    }
}

return;

更新:

我在 Mac 上,不想安装全新的 Python 工具链,所以我使用了Homebrew

> brew install llvm --rtti
> LLVM_CONFIG_PATH=/usr/local/opt/llvm/bin/llvm-config pip install llvmpy
> pip install numba

新的“麻木”代码:

from numba import autojit, jit
import time
import numpy as np

@autojit
def cost(left, right):
    height,width = left.shape
    cost = np.zeros((height,width,width))

    for row in range(height):
        for x in range(width):
            for y in range(width):
                cost[row,x,y] = abs(left[row,x]-right[row,y])

    return cost

@autojit
def optimalcosts(initcost):
    costs = zeros_like(initcost)
    for row in range(height):
        costs[row,:,:] = optimalcost(initcost[row])
    return costs

@autojit
def optimalcost(cost):
    width1,width2 = cost.shape
    P1=10
    prevcost = 0.0
    M = np.array(cost)
    for i in range(1,width1):
        for j in range(1,width2):
            M[i,j] += min(M[i-1,j-1],prevcost+P1,M[i,j-1]+P1)
            prevcost = M[i,j]
    return M

prob_size = 400
left = np.random.rand(prob_size,prob_size)
right = np.random.rand(prob_size,prob_size)

print '---------- Numba Time ----------'
t =  time.time()
c = cost(left,right)
optimalcost(c[100])
print time.time()-t

print '---------- Native python Time --'
t =  time.time()
c = cost.py_func(left,right)
optimalcost.py_func(c[100])
print time.time()-t

用 Python 编写代码很有趣,它是如此非 Pythonic。请注意,任何对编写 Numba 代码感兴趣的人都需要在代码中显式表达循环。以前,我有整洁的 Numpy 单线,

abs(left[row,:][:,newaxis] - right[row,:])

计算成本。使用 Numba 大约需要 7 秒。正确写出循环需要 0.5 秒。

将其与原生 Python 代码进行比较是不公平的,因为 Numpy 可以很快做到这一点,但是:

Numba 编译:0.509318113327s

原生:172.70626092s

我对数字和转换的完全简单印象深刻。

【问题讨论】:

  • 索引的环绕是故意的还是错误的?当你运行你的代码时,看到的第一个项目是i = 0j = 0,所以你得到M[0, 0] = M[0, 0] + min(M[-1, -1], M[-1, 0] + P1, M[0, -1] + P1)。在我看来,您的 try 试图捕获超出范围的索引(顺便说一下,您应该明确说明您要捕获的内容,即执行 except IndexError),但是您的索引中的 -1s被认为是“沿着那个维度的最后一个元素”,所以没有任何东西被设置为np.inf。我认为这不是您想要的,但请确认。
  • 是的,无意的。在实践中,它似乎并没有影响任何东西,这就是我没有抓住它的原因!
  • 请注意,您实际上不需要 需要展开所有循环。在 Numba 中,无论您编写原生 Python for 循环还是编写基于 Numpy 的矢量化操作,Numba JIT 都会自动将任何一种情况转换为完全相同优化的 C 代码。如果您使用常规装饰器而不是autojit 为它提供一些类型信息,那就更好了。
  • 我发现这通常很好,但是我在 Numpy 中为一行代码给出的情况只有在我表达循环后才有所改善。
  • +1 用于 numba 的出色独立应用程序。正是我想要的。我冒昧地更正了“numba'd”代码中的缩进和其他一些内容。

标签: python numpy dynamic-programming


【解决方案1】:

如果您不难切换到 Python 的 Anaconda 发行版,您可以尝试使用 Numba,对于这种特别简单的动态算法,它可能会在不让您离开 Python 的情况下提供很多加速。

【讨论】:

  • 感谢您的建议,看起来它减少了运行时间,因此非常易于管理。我认为降低它需要在 C 中进行修补,但这会做!我没有打扰 Anaconda,我已经通过 brew/pip 安装了它。有关详细信息,请参阅我的编辑。
【解决方案2】:

Numpy 通常不擅长迭代工作(尽管它确实有一些常用的迭代函数,例如np.cumsumnp.cumprodnp.linalg.* 等)。但是对于像上面找到最短路径(或最低能量路径)这样的简单任务,您可以通过考虑可以同时计算什么来将问题向量化(同时尽量避免复制:

假设我们在“行”方向(即水平方向)找到一条最短路径,我们可以首先创建我们的算法输入:

# The problem, 300 400*400 matrices
# Create infinitely high boundary so that we dont need to handle indexing "-1"
a = np.random.rand(300, 400, 402).astype('f')
a[:,:,::a.shape[2]-1] = np.inf

然后准备一些我们稍后将使用的实用程序数组(创建需要恒定时间):

# Create self-overlapping view for 3-way minimize
# This is the input in each iteration
# The shape is (400, 300, 400, 3), separately standing for row, batch, column, left-middle-right
A = np.lib.stride_tricks.as_strided(a, (a.shape[1],len(a),a.shape[2]-2,3), (a.strides[1],a.strides[0],a.strides[2],a.strides[2]))

# Create view for output, this is basically for convenience
# The shape is (399, 300, 400). 399 comes from the fact that first row is never modified
B = a[:,1:,1:-1].swapaxes(0, 1)

# Create a temporary array in advance (try to avoid cache miss)
T = np.empty((len(a), a.shape[2]-2), 'f')

最后进行计算和计时:

%%timeit
for i in np.arange(a.shape[1]-1):
    A[i].min(2, T)
    B[i] += T

我的(超级旧笔记本电脑)机器上的计时结果是 1.78 秒,这已经比 3 分钟快了很多。我相信您可以通过优化内存布局和对齐方式(以某种方式)改进更多(同时坚持使用 numpy)。或者,您可以简单地使用multiprocessing.Pool。它易于使用,而且这个问题很容易拆分为更小的问题(通过在批处理轴上划分)。

【讨论】:

    猜你喜欢
    • 2021-11-05
    • 1970-01-01
    • 1970-01-01
    • 2013-12-09
    • 1970-01-01
    • 2015-12-11
    • 2012-03-08
    • 2021-10-16
    • 1970-01-01
    相关资源
    最近更新 更多