重复的索引列表

4

我想创建一个索引列表,其循环范围为0m - 1,长度为n。到目前为止,我已经这样做:

import numpy as np
m = 7
n = 12
indices = [np.mod(i, m) for i in np.arange(n)]

这将导致以下结果:
[0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4]

有没有更快的方法实现这个?感谢任何建议。

定义更快。比什么更快? - ggdx
1
@ggdx 在计算时间方面更快。这行代码有点瓶颈...谢谢! - ajrlewis
6个回答

3

您可以使用 islicecycle 来自 itertools

from itertools import islice, cycle

print(list(islice(cycle(range(7)), 12)))
# [0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4]

1
太棒了 - 时间提高了4倍。谢谢! - ajrlewis

2

只需使用列表推导式将往返于Python的过程去掉即可。从numpy获得良好的速度取决于确保您的循环保持在numpy内部,而不是在Python本身中进行循环。


np.mod(np.arange(n), m)

这是唯一符合numpython标准的答案,因为它明显避免了Python中的所有for循环。(编辑:正如其他答案所示,这并不是最快的解决方案。)

1
考虑到您正在使用 numpy,一种方法是使用 np.arangenp.resize,这将用原始数组的副本填充更大的调整大小的数组:
np.resize(np.arange(7), 12)
# array([0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4])

有趣的解决方案;但这是Divakar答案中所讨论的陷阱类型的例子。np.resize在Python中包含了相当数量的逻辑;值得注意的是,每个请求的副本都将至少进行一次Python内存管理操作,因为会构造一个对输入数组的引用元组;因此,这与“真正”的向量化有所不同。我想这会在非平凡的基准测试中显示出来。 - Eelco Hoogendoorn
是的@EelcoHoogendoorn,根据快速检查,它确实包含一些基于Python的逻辑,但似乎不会有太多开销,因为该过程主要涉及计算模和进一步的连接和切片。 - yatu
@EelcoHoogendoorn 在涉及范围数组的存储器需求之上使用模数的逻辑效果并不好。这里完全采用向量化,使用resize、tile两种方法在C级别进行分配和赋值,没有任何计算,对于mod是必需的。 - Divakar
我担心的是创建一个Python元组,其中包含需要调整大小的数组的引用;这似乎会为Python GC创建大量工作;而且很难预测其性能影响。基准测试可能会受到GC工作的影响,而这些工作在基准测试本身执行期间未被计算。 - Eelco Hoogendoorn

1
基于NumPy的代码,其中使用了np.tile函数。
np.tile(np.arange(m),(n+m-1)//m)[:n]

示例运行 -

In [58]: m,n = 7,12

In [59]: np.tile(np.arange(m),(n+m-1)//m)[:n]
Out[59]: array([0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4])

基准测试

如果您想要高效地处理中等到大规模的数据,NumPy是一个不错的选择。在本节中,我们将测试 NumPy 在 mn 变化的情况下的解决方案速度。

设置:

import numpy as np

def resize(m,n):
    return np.resize(np.arange(m), n)

def mod(m,n):
    return np.mod(np.arange(n), m)

def tile(m,n):
    return np.tile(np.arange(m),(n+m-1)//m)[:n]

在IPython控制台上运行计时代码:
# Setup inputs and timeit those on posted NumPy approaches
m_ar = [10,100,1000]
s_ar = [10,20,50,100,200,500,1000] # scaling array

resize_timings = []
mod_timings = []
tile_timings = []
sizes_str = []
for m in m_ar:
    for s in s_ar:
        n = m*s+m//2
        size_str = str(m) + 'x' + str(n)
        sizes_str.append(size_str)

        p = %timeit -o -q resize(m,n)
        resize_timings.append(p.best)

        p = %timeit -o -q mod(m,n)
        mod_timings.append(p.best)

        p = %timeit -o -q tile(m,n)
        tile_timings.append(p.best)

在图表上获取结果:
# Use pandas to study results  
import pandas as pd

df_data = {'Resize':resize_timings,'Mod':mod_timings,'Tile':tile_timings}
df  = pd.DataFrame(df_data,index=sizes_str)

FGSZ = (20,6)
T = 'Timings(s)'
FTSZ = 16
df.plot(figsize=FGSZ,title=T,fontsize=FTSZ).get_figure().savefig("timings.png")

结果

比较所有三个

enter image description here

resizetile 相关的表现都不错。

比较 resizetile

让我们只绘制这两个:

enter image description here

tile 在这两者之间表现得更好。

分块学习

现在,让我们按照三个不同的 m's 将时间划分为块来学习:

enter image description here

enter image description here

enter image description here

< p > mod based one 只在小的 m 和小的 n 上获胜,而且这些 m 和 n 的计时是在 5-6 微秒左右,但在大多数其他情况下都会输掉,并且是与它相关的计算使其失去优势。


这可能在 CPU 周期方面更快;模运算不是单周期操作。但我预计平铺操作的成本将使粗暴地强制执行模操作的成本相形见绌,因为 numpy 不会将此表达式优化为单个循环而没有临时变量。 - Eelco Hoogendoorn
@EelcoHoogendoorn 瓷砖是一种内存操作,而模数运算涉及内存和计算。为什么您会认为瓷砖的成本比基于模数的成本更高呢? - Divakar
好的,不是所有的numpy函数本身都是真正的向量化的,有些可能涉及到Python for循环或其他次优构造。我检查了一下;tile调用repeat函数,repeat函数会调用C语言,因此可能会导致没有中间变量的优化代码路径。所以你可能是正确的;但是仅仅通过调用本身来看,我不会立即相信它是真正的向量化的。没有看到基准测试的情况下,也不能肯定地表明它是否真正的向量化。 - Eelco Hoogendoorn
@EelcoHoogendoorn 添加了基准测试。使用模运算的计算在时间上效果不佳。 - Divakar
有趣的是,感谢您提供了更详细的基准测试。我知道模运算并不便宜;但是我的经验法则是,在使用numpy时,很难注意到单个指令的成本,因为它们不是紧密循环的一部分;即使对于像加法这样的单周期指令,由于内存带宽限制和其他限制,您的实际吞吐量往往远低于每个周期的一个操作。但是从一些参考资料中可以看出,在许多体系结构上,模运算实际上需要数百个周期;也许它不能被流水线化?所以这是这个经验法则的一个例外... - Eelco Hoogendoorn

1

既然您要求最快的可能性,那么提供一些测试时间就很好了。因此,我使用timeit模块测试了大部分发布的代码片段。

为了更轻松地调用timeit,定义了快速函数。

def list_comp(m, n):
    return [np.mod(i, m) for i in np.arange(n)]

def leftover(m, n):
    nb_cycles = n//m
    leftover = n-m*nb_cycles
    indices = list(range(m))*nb_cycles + list(range(leftover))

def islice_cycle(m, n):
    return list(islice(cycle(range(m)), n))

def npmod(m, n):
    return mod(np.arange(m), n)

def resized(m, n):
    return np.resize(np.arange(m), n)

测试环境:

timer = timeit.Timer(stmt="function_name(7, 12)", globals=globals()).repeat(repeat=100, number =10000)
print(f'Min: {min(timer):.6}s,\n Avg: {np.average(timer):.6}s')

Results

| Function       |   Minimum   |    Average   |  
|:---------------|------------:|:------------:|  
| list_comp      | 0.156117s   |  0.160433s   |  
| islice_cycle   | 0.00712442s |  0.00726821s |  
| npmod          | 0.0118933s  |  0.0123122s  |  
| leftover       | 0.00943538s |  0.00964464s |  
| resized        | 0.0818617s  |  0.0851646s  |  

@Austin使用islicecycle的答案目前是最快的。@T.Lucas稍微慢一点,但仅仅是一点点,对于纯Python来说非常快。

其他答案要慢一个数量级。


1
感谢这个基准测试;这总是一个有用的补充。但是,从原始帖子中并不明显他的数字常量是否具有代表性。在更大的N下发布结果将会很有意思,因为在这种情况下,性能论点并不是无关紧要的。 - Eelco Hoogendoorn
有趣的是,当我在更大的数据上进行测试时,得到了不同的结果。稍后我会进行更多的测试并更新我的答案。 - user3053452

0
如果你想要速度,这是我找到的最快的方法:
nb_cycles = n//m
leftover = n-m*nb_cycles
indices = list(range(m))*nb_cycles + list(range(leftover))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接