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
,并且一次分配的内存更少,而且k分配的内存更少

但测量结果表明:

times1 = []
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
的大值来说是如此。我贴了一个答案来说明这一点。