【问题标题】:Avoid nested for loops using List Comprehension and/or map使用 List Comprehension 和/或 map 避免嵌套 for 循环
【发布时间】:2019-02-18 01:45:58
【问题描述】:

几天来,我一直在苦苦思考如何优化(不仅让它看起来更漂亮)3 个 嵌套循环,其中包含一个 条件 和一个函数调用。我现在拥有的是以下内容:

def build_prolongation_operator(p,qs):
    '''
    p: dimension of the coarse basis
    q: dimension of the fine basis

    The prolongation operator describes the relationship between
    the coarse and fine bases:    
    V_coarse = np.dot(V_fine, I)
    '''

    q = sum(qs)

    I = np.zeros([q, p])

    for i in range(0, q):
        for j in range(0, p):
            for k in range(0, qs[j]):
                # if BV i is a child of j, we set I[i, j] = 1
                if i == f_map(j, k, qs):
                    I[i, j] = 1
                    break

    return I

f_map 在哪里:

def f_map(i, j, q):
    '''
    Mapping which returns the index k of the fine basis vector which
    corresponds to the jth child of the ith coarse basis vector.    
    '''

    if j < 0 or j > q[i]:
        print('ERROR in f_map')
        return None

    result = j

    for k in range(0, i):
        result += q[k]

    return result

在分析我的整个代码时,我发现 build_prolongation_operator 被调用了 45 次,f_map 大约 850 万次!!

图片如下:

我曾尝试对列表理解和地图做同样的事情,但没有任何运气。

这是build_prolongation_operator 期望的输入示例:

p = 10
qs = randint(3, size=p)

【问题讨论】:

  • 列表推导式或map仍然执行您的操作 850 万次。这些构造之间的性能差异对于任何重要的代码(例如您的代码)都可以忽略不计。如果您提供更多上下文(例如 qs 是什么),则可能设计出更好的算法。还可以考虑研究 numpy 的内置矢量化功能,或者可能是 Cython 以进行蛮力优化。
  • 您也许可以用向量乘法替换三个循环。 pqs 是什么,你认为 I 是什么?
  • 我认为您的循环之一根本没有必要。特别是外层。请提供 sample 输入,以便我们可以play
  • @Mad,这将比我建议的记忆更好:-)
  • @MisterMiyagi 谢谢你的时间。为了测试这一点,我将 {qs = np.random.randint(3, size(p))} 设为 p 只是一个整数。

标签: python python-3.x list numpy optimization


【解决方案1】:

我不知道基数和延长运算符,但您应该关注算法本身。在优化方面,这几乎总是合理的建议。

这可能是症结所在——如果不是,它可以帮助您入门:f_map 计算不依赖于 i,但您正在为每个 i 值重新计算它。由于i 的范围从零到qs 中的值之和,因此您将通过缓存结果来节省大量的重新计算;谷歌“python memoize”,它实际上会自己写。修复此问题,您可能已经完成,无需任何微优化。

您将需要足够的空间来存储 max(p) * max(qs[j]) 值,但从您报告的调用次数来看,这应该不是太大的障碍。

【讨论】:

  • 我认为你甚至不需要记忆。您可能只需要完全摆脱外部循环并正确地相对于彼此对剩余的两个循环进行排序。
【解决方案2】:

试试看是否可行,

for j in range(0,p):
    for k in range(0, qs[j]):
        # if BV i is a child of j, we set I[i,j] = 1
        val = f_map(j,k,qs)
        if I[val, j] == 0:
            I[val, j] = 1

【讨论】:

  • 男人!!这有很大帮助!大大减少了函数调用!谢谢!!我将尝试其他方法,但这是一个很好的解决方案!
  • 我只是提高了效率,也检查一下。
  • 一旦设置为 1,您应该从两个循环中中断,而不是继续检查。
  • @Mad,从我从他的函数中可以理解的是,每当 f_map 返回等于i 的值时,他就会设置 1 并且太贪心了(即第一次因为他打破了他找到了)。因此,如果 f_map 返回 2,他需要设置I[2, j] = 1。如果我的逻辑看起来不对,请纠正我。
  • @RaunaqJain 这可以完成这项工作。与之前的方案相比,时间上的差异相对较小,并且更好地摆脱了1个函数调用
【解决方案3】:

一方面,您确实不需要p 作为函数的参数:len(qs) 只需要调用一次,而且非常便宜。如果您的输入始终是一个 numpy 数组(并且在这种情况下没有理由不应该这样),qs.size 也可以。

让我们从重写f_map 开始。那里的循环只是qs 的累积总和(但从零开始),您可以预先计算一次(或每次调用外部函数至少一次)。

def f_map(i, j, cumsum_q):
    return j + cumsum_q[i]

cumsum_q 将在 build_prolongation_operator 中定义为

cumsum_q = np.roll(np.cumsum(qs), 1)
cumsum_q[0] = 0

我相信您会体会到在f_map 中拥有与在build_prolongation_operator 中相同的一组变量名的用处。为了更简单,我们可以完全删除 f_map 并在您的条件下使用它所代表的表达式:

if i == k + cumsum_q[j]:
    I[i, j] = 1

k 上的循环意味着“如果ik + cumsum[j] for any k”,则将元素设置为1。如果我们将条件重写为i - cumsum_q[j] == k,你可以看到我们根本不需要k 的循环。 i - cumsum_q[j] 将等于 some k[0, qs[j]) 范围内,如果它是非负数并且严格小于qs[j]。你可以检查一下

if i >= cumsum_q[j] and i - cumsum_q[j] < qs[j]:
    I[i, j] = 1

这将您的循环减少到矩阵的每个元素一次迭代。没有比这更好的了:

def build_prolongation_operator_optimized(qs):
    '''
    The prolongation operator describes the relationship between
    the coarse and fine bases:    
    V_coarse = np.dot(V_fine, I)
    '''
    qs = np.asanyarray(qs)
    p = qs.size
    cumsum_q = np.roll(np.cumsum(qs), 1)
    q = cumsum_q[0]
    cumsum_q[0] = 0

    I = np.zeros([q, p])

    for i in range(0, q):
        for j in range(0, p):
            # if BV i is a child of j, we set I[i, j] = 1
            if 0 <= i - cumsum_q[j] < qs[j]:
                I[i, j] = 1
    return I

现在您已经知道每个单元格的公式,您可以让 numpy 使用广播在基本上一行中为您计算整个矩阵:

def build_prolongation_operator_numpy(qs):
    qs = np.asanyarray(qs)
    cumsum_q = np.roll(np.cumsum(qs), 1)
    q = cumsum_q[0]
    cumsum_q[0] = 0
    i_ = np.arange(q).reshape(-1, 1)  # Make this a column vector
    return (i_ >= cumsum_q) & (i_ - cumsum_q < qs)

我运行了一个小型演示,以确保 (A) 建议的解决方案获得与原始解决方案相同的结果,并且 (B) 工作得更快:

In [1]: p = 10
In [2]: q = np.random.randint(3, size=p)

In [3]: ops = (
...     build_prolongation_operator(p, qs),
...     build_prolongation_operator_optimized(qs),
...     build_prolongation_operator_numpy(qs),
...     build_prolongation_operator_RaunaqJain(p, qs),
...     build_prolongation_operator_gboffi(p, qs),
... )

In [4]: np.array([[(op1 == op2).all() for op1 in ops] for op2 in ops])
Out[4]: 
array([[ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True]])

In [5]: %timeit build_prolongation_operator(p, qs)
321 µs ± 890 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [6]: %timeit build_prolongation_operator_optimized(qs)
75.1 µs ± 1.85 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [7]: %timeit build_prolongation_operator_numpy(qs)
24.8 µs ± 77.7 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [8]: %timeit build_prolongation_operator_RaunaqJain(p, qs)
28.5 µs ± 1.55 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [9]: %timeit build_prolongation_operator_gboffi(p, qs)
31.8 µs ± 772 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [10]: %timeit build_prolongation_operator_gboffi2(p, qs)
26.6 µs ± 768 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

如您所见,最快的选项是完全矢量化的选项,但 @RaunaqJain's@gboffi's 选项紧随其后。

注意

我的矢量化解决方案创建了一个布尔数组。如果您不希望使用 I.astype(...) 转换为所需的 dtype,或将其视为 np.uint8 数组:I.view(dtype=np.uint8)

【讨论】:

  • 我有点困惑 gboffi1 的输出速度比 RaunaqJain 慢,我不得不考虑一下我的 “调用内置应该比函数调用更有效,并且一个循环,不是吗?” 一会儿……
  • 感谢您抽出宝贵时间@Mad,基准测试真的很有帮助!
【解决方案4】:

这是Raunaq Jaintheir answer中提出的优化循环

for j in range(0,p):
    for k in range(0, qs[j]):
        # if BV i is a child of j, we set I[i,j] = 1
            val = f_map(j,k,qs)
            if I[val, j] == 0:
                I[val, j] = 1

这里是 f_map 函数,我在其中编辑了参数的名称以反映调用者使用的名称

def f_map(j,k,qs):
    if k < 0 or k > qs[j]:
        print('ERROR in f_map')
        return None
    result = k
    for i in range(0, j):
        result += qs[i]
    return result

我们首先注意到它始终是0 ≤ k &lt; qs[j],因为在k 上定义了循环,因此我们可以安全地删除健全性检查并写入

def f_map(j,k,qs):
    result = k
    for i in range(0, j):
        result += q[i]
    return result

现在,这是 sum 内置的文档字符串

签名:sum(iterable, start=0, /)
文档字符串:
返回一个“开始”值(默认值:0)加上一个可迭代的数字的总和

当iterable为空时,返回起始值。
此函数专门用于数值,可能会拒绝非数值类型。
类型:builtin_function_or_method

显然我们可以写

def f_map(j,k,qs):
    return sum(qs[:j], k)

很明显,我们也可以不使用函数调用

for j in range(0,p):
    for k in range(0, qs[j]):
        # if BV i is a child of j, we set I[i,j] = 1
            val = sum(qs[:j], k)
            if I[val, j] == 0:
                I[val, j] = 1

调用内置函数应该比函数调用和循环更有效,不是吗?


针对疯狂物理学家的言论

我们可以预先计算qs 的部分和以进一步加快速度

sqs = [sum(qs[:i]) for i in range(len(qs))] # there are faster ways...
...
for j in range(0,p):
    for k in range(0, qs[j]):
        # if BV i is a child of j, we set I[i,j] = 1
            val = k+sqs[j]
            if I[val, j] == 0:
                I[val, j] = 1

【讨论】:

  • 正如我的回答所示,您可以通过不多次计算总和来进一步优化。
  • 看起来你的关闭括号键被卡住了:)
  • 我希望你不介意我修复它以及你的总和限制。
  • 这次更新带来了巨大的变化。我也为此添加了一个基准。
猜你喜欢
  • 2012-06-25
  • 2017-08-27
  • 2017-11-11
  • 1970-01-01
  • 2020-05-21
  • 2019-12-26
  • 1970-01-01
  • 2021-08-19
  • 2015-01-24
相关资源
最近更新 更多