Python 创建环图的两种方法
假设我们有一个具有以下属性的图:Python 创建环图的两种方法,python,performance,numpy,Python,Performance,Numpy,假设我们有一个具有以下属性的图: 节点排列成圆形 每个节点都连接到其k下一个邻居 我有两个函数可以使图形变大: def ring_graph1(n, k): graph = nx.Graph() sources = np.arange(n) for i in range(1, k + 1): targets = np.roll(sources, i) graph.add_edges_from(zip(sources, targets))
- 节点排列成圆形
- 每个节点都连接到其k下一个邻居
def ring_graph1(n, k):
graph = nx.Graph()
sources = np.arange(n)
for i in range(1, k + 1):
targets = np.roll(sources, i)
graph.add_edges_from(zip(sources, targets))
return graph
def ring_graph2(n, k):
graph = nx.Graph()
for i in range(n):
sources = [i] * k
targets = range(i + 1, i + k + 1)
targets = [node % n for node in targets]
graph.add_edges_from(zip(sources, targets))
return graph
我天真地认为第一个会更快,因为它处理的是np.array
,并且一次分配的内存更少,而且ktimes1 = []
times2 = []
for k in [2, 10, 50, 100, 200, 500]:
t = %timeit -o ring_graph1(1000, k)
times1.append(t.average)
t = %timeit -o ring_graph2(1000, k)
times2.append(t.average)
第二种方法执行速度大约快1.5倍。为什么会这样?为了它的价值
def ring_graph3(n, k):
edges = set()
node_ids = list(range(n)) * 2
for i in range(n):
sources = [i] * k
targets = node_ids[i + 1 : i + k + 1]
edges.update(set(zip(sources, targets)))
return edges
在不必进行模传递的情况下,传递速度更快(n=1000,k=500):
Numpy魔术编辑!
经过一点工作后,将以更快的速度返回同一组边对:
def ring_graph5(n, k):
nxk = np.arange(0, n).repeat(k)
src = nxk.reshape(n, k)
dst = np.mod(np.tile(np.arange(0, k), n) + (nxk + 1), n).reshape((n, k))
flat_pairs = np.dstack((src, dst)).flatten().tolist()
return zip(flat_pairs[::2], flat_pairs[1::2])
不管它值多少钱
def ring_graph3(n, k):
edges = set()
node_ids = list(range(n)) * 2
for i in range(n):
sources = [i] * k
targets = node_ids[i + 1 : i + k + 1]
edges.update(set(zip(sources, targets)))
return edges
在不必进行模传递的情况下,传递速度更快(n=1000,k=500):
Numpy魔术编辑!
经过一点工作后,将以更快的速度返回同一组边对:
def ring_graph5(n, k):
nxk = np.arange(0, n).repeat(k)
src = nxk.reshape(n, k)
dst = np.mod(np.tile(np.arange(0, k), n) + (nxk + 1), n).reshape((n, k))
flat_pairs = np.dstack((src, dst)).flatten().tolist()
return zip(flat_pairs[::2], flat_pairs[1::2])
听起来瓶颈似乎是内置的
从添加边,而不是边生成。这可能是意料之中的,因为networkx
设计用于处理更复杂的情况,例如边缘属性。事实上,有一个内置的方法可以让你构造环图,例如
nx.circulant_graph(1000, range(1, 501))
相当于
ring_graph2(1000, 500)
但是内置版本实际上比您的版本慢
回到边缘生成,考虑以下实现:
def get_edges(n, k):
modmap = np.tile(np.arange(n), 2)
a, b = np.meshgrid(range(n), range(k))
return zip(a.T.ravel().tolist(), modmap[(a+b+1).T.ravel()].tolist())
assert set(get_edges(1000, 500)) == set(ring_graph5(1000, 500))
%timeit get_edges(1000, 500) # 10 loops, best of 3: 32 ms per loop
%timeit ring_graph5(1000, 500) # 10 loops, best of 3: 61 ms per loop
def graph_from_edge_generator(f, n, k):
g = nx.Graph()
g.add_edges_from(f(n, k))
return g
%timeit graph_from_edge_generator(get_edges, 1000, 500) # 1 loop, best of 3: 772 ms per loop
%timeit graph_from_edge_generator(ring_graph5, 1000, 500) # 1 loop, best of 3: 783 ms per loop
在这两种情况下,边缘生成占用的时间都不到运行时间的10%。听起来瓶颈似乎是内置的从
添加边缘,而不是边缘生成。这可能是意料之中的,因为networkx
设计用于处理更复杂的情况,例如边缘属性。事实上,有一个内置的方法可以让你构造环图,例如
nx.circulant_graph(1000, range(1, 501))
相当于
ring_graph2(1000, 500)
但是内置版本实际上比您的版本慢
回到边缘生成,考虑以下实现:
def get_edges(n, k):
modmap = np.tile(np.arange(n), 2)
a, b = np.meshgrid(range(n), range(k))
return zip(a.T.ravel().tolist(), modmap[(a+b+1).T.ravel()].tolist())
assert set(get_edges(1000, 500)) == set(ring_graph5(1000, 500))
%timeit get_edges(1000, 500) # 10 loops, best of 3: 32 ms per loop
%timeit ring_graph5(1000, 500) # 10 loops, best of 3: 61 ms per loop
def graph_from_edge_generator(f, n, k):
g = nx.Graph()
g.add_edges_from(f(n, k))
return g
%timeit graph_from_edge_generator(get_edges, 1000, 500) # 1 loop, best of 3: 772 ms per loop
%timeit graph_from_edge_generator(ring_graph5, 1000, 500) # 1 loop, best of 3: 783 ms per loop
在这两种情况下,边缘生成占用的运行时间都少于10%。np.roll()
返回一个新数组,因此它也会在每次调用时进行分配。是否检查执行时间或更多内存分配?在我看来,它们是两个不同的性能参数,除非机器资源不足。这可能是错误的,但第一个实现似乎多次添加相同的边缘,这被networkx
抑制。也就是说,zip(源、目标)
在第一个版本中总是有大小n
,在第二个版本中总是有大小k
。@hilberts\u我读到的问题是,多次添加相同的边没有效果,但会导致不成功的操作,我认为最初的图形是由default引导的,如果您将函数更改为不使用Networkx(现在),并且只返回一组边2元组,那么这两个函数的返回值就不一样了。要使它们相等,请使用np.roll(sources,-i)
(翻转滚动方向)。np.roll()
返回一个新数组,因此它也会在每次调用时进行分配。是否检查执行时间或更多内存分配?在我看来,它们是两个不同的性能参数,除非机器资源不足。这可能是错误的,但第一个实现似乎多次添加相同的边缘,这被networkx
抑制。也就是说,zip(源、目标)
在第一个版本中总是有大小n
,在第二个版本中总是有大小k
。@hilberts\u我读到的问题是,多次添加相同的边没有效果,但会导致不成功的操作,我认为最初的图形是由default引导的,如果您将函数更改为不使用Networkx(现在),并且只返回一组边2元组,那么这两个函数的返回值就不一样了。要使它们相等,请使用np.roll(sources,-i)
(翻转滚动方向)。当我在您的最新基准测试中包括图形创建和调用add\u edges\u from
时,对于n=1000
和k=500
@hilberts\u饮酒问题,它似乎比OP的ring\u graph2
要慢,我又添加了一个实现,这只是原始的numpy魔法:)@spiridon\u\u太阳旋转器您可能也对新版本感兴趣implementation@AKX这是一个创造性的numpy解决方案。我的看法是边缘生成并不是这个问题的真正限制因素,至少对于k
的大值来说是如此。我发布了一个答案来说明这一点。当我在你的最新基准测试中包括图形创建和调用从添加边时,对于n=1000
和k=500
@hilberts\u饮酒问题,它似乎比OP的环图2
要慢,我又添加了一个实现,这只是原始的numpy魔法:)@spiridon\u\u太阳旋转器您可能也对新版本感兴趣implementation@AKX这是一个创造性的numpy解决方案。我的看法是边缘生成并不是这个问题的真正限制因素,至少对于k
的大值来说是如此。我贴了一个答案来说明这一点。