将1.2GB边缘列表转换为稀疏矩阵

时间:2016-07-31 20:18:19

标签: python pandas numpy optimization scipy

我在文本文件中有一个1.2GB的边缘列表。我的ubuntu PC有8GB的RAM。输入中的每一行都是

287111206 357850135

我想将其转换为稀疏邻接矩阵并将其输出到文件。

我的数据的一些统计数据:

Number of edges: around 62500000
Number of vertices: around 31250000

我在https://stackoverflow.com/a/38667644/2179021之前问了很多相同的问题并得到了很好的答案。问题是我无法让它发挥作用。

我首先尝试使用np.loadtxt加载文件,但速度非常慢并且使用了大量内存。所以相反我转移到pandas.read_csv这是非常快,但这导致它自己的问题。这是我目前的代码:

import pandas
import numpy as np
from scipy import sparse

data = pandas.read_csv("edges.txt", sep=" ", header= None, dtype=np.uint32)
A = data.as_matrix()
print type(A)
k1,k2,k3=np.unique(A,return_inverse=True,return_index=True)
rows,cols=k3.reshape(A.shape).T
M=sparse.coo_matrix((np.ones(rows.shape,int),(rows,cols)))
print type(M)

问题是pandas数据帧data非常庞大,而且我在A中有效地复制了一个低效的副本。然而,当代码与

崩溃时情况更糟
<type 'instancemethod'>
Traceback (most recent call last):
  File "make-sparse-matrix.py", line 13, in <module>
    rows,cols=k3.reshape(A.shape).T
AttributeError: 'function' object has no attribute 'shape'
raph@raph-desktop:~/python$ python make-sparse-matrix.py 
<type 'numpy.ndarray'>
Traceback (most recent call last):
  File "make-sparse-matrix.py", line 12, in <module>
    k1,k2,k3=np.unique(A,return_inverse=True,return_index=True)
  File "/usr/local/lib/python2.7/dist-packages/numpy/lib/arraysetops.py", line 209, in unique
    iflag = np.cumsum(flag) - 1
  File "/usr/local/lib/python2.7/dist-packages/numpy/core/fromnumeric.py", line 2115, in cumsum
    return cumsum(axis, dtype, out)
MemoryError

所以我的问题是:

  1. 我可以避免在内存中同时拥有1.2GB的pandas数据帧和1.2GB的numpy数组副本吗?
  2. 有没有办法让代码在8GB内存中完成?
  3. 您可以重现我尝试处理的尺寸的测试输入:

    import random
    #Number of edges, vertices
    m = 62500000
    n = m/2
    for i in xrange(m):
        fromnode = str(random.randint(0, n-1)).zfill(9)
        tonode = str(random.randint(0, n-1)).zfill(9)
        print fromnode, tonode
    

    更新

    我现在尝试了许多不同的方法,所有方法都失败了。以下是摘要。

    1. igraphg = Graph.Read_Ncol('edges.txt')一起使用。这会占用大量RAM,导致我的计算机崩溃。
    2. networkitG= networkit.graphio.readGraph("edges.txt", networkit.Format.EdgeList, separator=" ", continuous=False)一起使用。这会占用大量RAM,导致我的计算机崩溃。
    3. 此问题中的上述代码,但使用np.loadtxt(“edges.txt”)而不是pandas。这会占用大量RAM,导致我的计算机崩溃。
    4. 然后我编写了单独的代码,将所有顶点名称重新映射为1 ... | V |中的数字其中| V |是顶点的总数。这应该保存导入边列表的代码,使其不必构建映射顶点名称的表。用这个我试过:

      1. 使用这个新的重新映射的边缘列表文件,我再次使用了g = Graph.Read_Edgelist("edges-contig.txt")。这现在可以工作,虽然它需要4GB的RAM(这远远超过它应该的理论量)。但是,没有igraph函数从图形中写出稀疏邻接矩阵。推荐的解决方案是convert the graph to a coo_matrix。不幸的是,这会占用大量的RAM,导致我的计算机崩溃。
      2. 使用重新映射的边缘列表文件,我将networkit与G = networkit.readGraph("edges-contig.txt", networkit.Format.EdgeListSpaceOne)一起使用。这也可以使用低于igraph需要的4GB。 networkit还附带了编写Matlab文件的函数(这是scipy可以读取的稀疏邻接矩阵的一种形式)。但是networkit.graphio.writeMat(G,"test.mat")使用了大量的RAM,导致我的计算机崩溃。
      3. 最后,sascha的答案确实已经完成,但大约需要40分钟。

5 个答案:

答案 0 :(得分:13)

这是我的解决方案:

import numpy as np
import pandas as pd
import scipy.sparse as ss

def read_data_file_as_coo_matrix(filename='edges.txt'):
    "Read data file and return sparse matrix in coordinate format."
    data = pd.read_csv(filename, sep=' ', header=None, dtype=np.uint32)
    rows = data[0]  # Not a copy, just a reference.
    cols = data[1]
    ones = np.ones(len(rows), np.uint32)
    matrix = ss.coo_matrix((ones, (rows, cols)))
    return matrix

Pandas使用read_csv解决了繁重的问题。而且Pandas已经以列式格式存储数据。 data[0]data[1]只能获得参考,没有副本。然后我将它们提供给coo_matrix。在当地进行基准测试:

In [1]: %timeit -n1 -r5 read_data_file_as_coo_matrix()
1 loop, best of 5: 14.2 s per loop

然后将csr矩阵保存到文件中:

def save_csr_matrix(filename, matrix):
    """Save compressed sparse row (csr) matrix to file.

    Based on http://stackoverflow.com/a/8980156/232571

    """
    assert filename.endswith('.npz')
    attributes = {
        'data': matrix.data,
        'indices': matrix.indices,
        'indptr': matrix.indptr,
        'shape': matrix.shape,
    }
    np.savez(filename, **attributes)

本地基准:

In [3]: %timeit -n1 -r5 save_csr_matrix('edges.npz', matrix.tocsr())
1 loop, best of 5: 13.4 s per loop

然后从文件中加载它:

def load_csr_matrix(filename):
    """Load compressed sparse row (csr) matrix from file.

    Based on http://stackoverflow.com/a/8980156/232571

    """
    assert filename.endswith('.npz')
    loader = np.load(filename)
    args = (loader['data'], loader['indices'], loader['indptr'])
    matrix = ss.csr_matrix(args, shape=loader['shape'])
    return matrix

本地基准:

In [4]: %timeit -n1 -r5 load_csr_matrix('edges.npz')
1 loop, best of 5: 881 ms per loop

最后测试一切:

def test():
    "Test data file parsing and matrix serialization."
    coo_matrix = read_data_file_as_coo_matrix()
    csr_matrix = coo_matrix.tocsr()
    save_csr_matrix('edges.npz', csr_matrix)
    loaded_csr_matrix = load_csr_matrix('edges.npz')
    # Comparison based on http://stackoverflow.com/a/30685839/232571
    assert (csr_matrix != loaded_csr_matrix).nnz == 0

if __name__ == '__main__':
    test()

运行test()时,大约需要30秒:

$ time python so_38688062.py 
real    0m30.401s
user    0m27.257s
sys     0m2.759s

内存高水位标记约为1.79 GB。

请注意,一旦您以CSR-matrix格式将“edges.txt”转换为“edges.npz”,加载它将花费不到一秒钟。

答案 1 :(得分:3)

更新版本

如评论中所示,该方法不适合您的用例。让我们做一些改变:

  • 使用pandas读取数据(而不是numpy:我很惊讶np.loadtxt表现那么糟糕!)
  • 使用外部库sortedcontainers获得更节省内存的方法(而不是字典)
  • 基本方法是相同的

这种方法需要 ~45分钟(这很慢;但你可以腌制/保存结果,所以你需要只做一次)和 〜5 GB 内存为您的数据准备稀疏矩阵,生成时使用:

import random
N = 62500000
for i in xrange(N):
    print random.randint(10**8,10**9-1), random.randint(10**8,10**9-1)

代码

import numpy as np
from scipy.sparse import coo_matrix
import pandas as pd
from sortedcontainers import SortedList
import time

# Read data
# global memory usage after: one big array
df = pd.read_csv('EDGES.txt', delimiter=' ', header=None, dtype=np.uint32)
data = df.as_matrix()
df = None
n_edges = data.shape[0]

# Learn mapping to range(0, N_VERTICES)  # N_VERTICES unknown
# global memory usage after: one big array + one big searchtree
print('fit mapping')
start = time.time()
observed_vertices = SortedList()
mappings = np.arange(n_edges*2, dtype=np.uint32)  # upper bound on vertices
for column in range(data.shape[1]):
    for row in range(data.shape[0]):
        # double-loop: slow, but easy to understand space-complexity
        val = data[row, column]
        if val not in observed_vertices:
            observed_vertices.add(val)
mappings = mappings[:len(observed_vertices)]
n_vertices = len(observed_vertices)
end = time.time()
print(' secs: ', end-start)

print('transform mapping')
# Map original data (in-place !)
# global memory usage after: one big array + one big searchtree(can be deleted!)
start = time.time()
for column in range(data.shape[1]):
    for row in range(data.shape[0]):
        # double-loop: slow, but easy to understand space-complexity
        val = data[row, column]
        mapper_pos = observed_vertices.index(val)
        data[row, column] = mappings[mapper_pos]
end = time.time()
print(' secs: ', end-start)
observed_vertices = None  # if not needed anymore
mappings = None  # if not needed anymore

# Create sparse matrix (only caring about a single triangular part for now)
# if needed: delete dictionary before as it's not needed anymore!
sp_mat = coo_matrix((np.ones(n_edges, dtype=bool), (data[:, 0], data[:, 1])), shape=(n_vertices, n_vertices))

第一个版本

以下是构建此稀疏矩阵的非常简单非常低效(关于时间和空间)代码。我发布了这段代码,因为我认为如果在较大的东西中使用这些代码,理解核心部分非常重要。

让我们看看,如果此代码对您的用例足够有效或者需要工作。从远处看,很难说,因为我们没有你的数据。

用于映射的字典部分是炸毁记忆的候选者。但是,在不知道是否需要的情况下优化它是毫无意义的。特别是因为这部分代码依赖于图表中的顶点数量(而且我不知道这个基数)。

""" itertools.count usage here would need changes for py2 """

import numpy as np
from itertools import count
from scipy.sparse import coo_matrix


# Read data
# global memory usage after: one big array
data = np.loadtxt('edges.txt', np.uint32)
n_edges = data.shape[0]
#print(data)
#print(data.shape)

# Learn mapping to range(0, N_VERTICES)  # N_VERTICES unknown
# global memory usage after: one big array + one big dict 
index_gen = count()
mapper = {}
for column in range(data.shape[1]):
    for row in range(data.shape[0]):
        # double-loop: slow, but easy to understand space-complexity
        val = data[row, column]
        if val not in mapper:
            mapper[val] = next(index_gen)
n_vertices = len(mapper)

# Map original data (in-place !)
# global memory usage after: one big array + one big dict (can be deleted!)
for column in range(data.shape[1]):
    for row in range(data.shape[0]):
        # double-loop: slow, but easy to understand space-complexity
        data[row, column] = mapper[data[row, column]]
#print(data)

# Create sparse matrix (only caring about a single triangular part for now)
# if needed: delete dictionary before as it's not needed anymore!
sp_mat = coo_matrix((np.ones(n_edges, dtype=bool), (data[:, 0], data[:, 1])), shape=(n_vertices, n_vertices))
#print(sp_mat)

edge-10.txt的输出

[[287111206 357850135]
 [512616930 441657273]
 [530905858 562056765]
 [524113870 320749289]
 [149911066 964526673]
 [169873523 631128793]
 [646151040 986572427]
 [105290138 382302570]
 [194873438 968653053]
 [912211115 195436728]]
(10, 2)
[[ 0 10]
 [ 1 11]
 [ 2 12]
 [ 3 13]
 [ 4 14]
 [ 5 15]
 [ 6 16]
 [ 7 17]
 [ 8 18]
 [ 9 19]]
  (0, 10)   True
  (1, 11)   True
  (2, 12)   True
  (3, 13)   True
  (4, 14)   True
  (5, 15)   True
  (6, 16)   True
  (7, 17)   True
  (8, 18)   True
  (9, 19)   True

答案 2 :(得分:3)

除了已经使用的方法之外,我尝试了不同的方法。我发现以下情况很好。

方法1 - 将文件读入字符串,使用numpy的fromstring将字符串解析为1-D数组。

import numpy as np
import scipy.sparse as sparse

def readEdges():
    with open('edges.txt') as f:
        data = f.read()  
    edges = np.fromstring(data, dtype=np.int32, sep=' ')
    edges = np.reshape(edges, (edges.shape[0]/2, 2))
    ones = np.ones(len(edges), np.uint32)
    cooMatrix = sparse.coo_matrix((ones, (edges[:,0], edges[:,1])))
%timeit -n5 readEdges()

输出:

5 loops, best of 3: 13.6 s per loop

方法2 - 与方法1相同,不同之处在于使用内存映射接口将文件加载到字符串中。

def readEdgesMmap():
    with open('edges.txt') as f:
        with contextlib.closing(mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)) as m: 
            edges = np.fromstring(m, dtype=np.int32, sep=' ')
            edges = np.reshape(edges, (edges.shape[0]/2, 2))
            ones = np.ones(len(edges), np.uint32)
            cooMatrix = sparse.coo_matrix((ones, (edges[:,0], edges[:,1])))
%timeit -n5 readEdgesMmap()

输出:

5 loops, best of 3: 12.7 s per loop

使用/usr/bin/time进行监控,两种方法都使用最大约2GB的内存。

很少注意到:

  1. 它似乎比pandas read_csv略胜一筹。使用pandas read_csv,同一台机器上的输出是

    5 loops, best of 3: 16.2 s per loop

  2. 从首席运营官到CSR / CSC的转换也耗费大量时间。在@ GrantJ的答案中,由于COO矩阵初始化不正确,所以花费的时间更少。这个论点需要作为一个元组给出。我想在那里留言,但我还没有评论权。

  3. 我猜测为什么这比pandas read_csv稍微好一点是先前的1D数据假设。

答案 3 :(得分:2)

在我的回答中,我考虑了节点的id由9个字符长字符串给出的情况,每个字符来自[0-9A-Za-z]。这些节点ID中的n应该映射到值[0,n-1](这可能对您的应用程序来说不是必需的,但仍然是普遍感兴趣的。)

我确信你知道的下一个考虑因素是为了完整起见:

  1. 记忆是瓶颈。
  2. 文件中有10^8个字符串。
  3. 一个9个字符长的string + int32值对在字典中花费大约120个字节,导致该文件的内存使用量为12GB。
  4. 来自文件的字符串ID可以映射到int64:有62个不同的字符 - &gt;可以用6位编码,字符串中有9个字符 - &gt; 6 * 9 = 54 <64位。另请参阅下面的toInt64()方法。
  5. 有int64 + int32 = 12字节&#34;真实&#34; information =&gt;约1.2 GB就足够了,但字典中这样一对的成本约为60字节(需要大约6 GB RAM)。
  6. 创建小对象(在堆上)会导致大量内存开销,因此将这些对象捆绑在数组中是有利的。有关python对象使用的内存的有趣信息可以在他的教程阶段article中找到。减少内存使用量的有趣体验在此blog entry
  7. 中公开
  8. python-list作为数据结构和字典是不可能的。 array.array可以替代,但我们使用np.array(因为np.array但不是array.array的排序算法。)
  9. <强> 1。步骤:读取文件并将字符串映射到int64。让np.array动态增长是一件痛苦的事情,所以我们现在假设文件中的边数(将它放在标题中会很好,但也可以从文件大小中推断出来) :

    import numpy as np
    
    def read_nodes(filename, EDGE_CNT):   
        nodes=np.zeros(EDGE_CNT*2, dtype=np.int64)
        cnt=0
        for line in open(filename,"r"):
            nodes[cnt:cnt+2]=map(toInt64, line.split())  # use map(int, line.split()) for cases without letters
        return nodes
    

    <强> 2。步骤:将int64值转换为值[0,n-1]

    可能性A ,需要3 * 0.8GB:

    def maps_to_ids(filename, EDGE_CNT):
    """ return number of different node ids, and the mapped nodes"""
        nodes=read_nodes(filename, EDGE_CNT)
        unique_ids, nodes = np.unique(nodes, return_index=True)  
        return (len(unique_ids), nodes)
    

    可能性B ,需要2 * 0.8GB,但有点慢:

    def maps_to_ids(filename, EDGE_CNT):
        """ return number of different node ids, and the mapped nodes"""
        nodes=read_nodes(filename, EDGE_CNT)
        unique_map = np.unique(nodes)
        for i in xrange(len(nodes)):
            node_id=np.searchsorted(unique_map, nodes[i]) # faster than bisect.bisect
            nodes[i]=node_id  
        return (len(unique_map), nodes)  
    

    第3。步骤:将其全部放入coo_matrix:

    from scipy import sparse
    def data_as_coo_matrix(filename, EDGE_CNT)
        node_cnt, nodes = maps_to_ids(filename, EDGE_CNT)    
        rows=nodes[::2]#it is only a view, not a copy
        cols=nodes[1::2]#it is only a view, not a copy
    
        return sparse.coo_matrix((np.ones(len(rows), dtype=bool), (rows, cols)), shape=(node_cnt, node_cnt))
    

    要调用data_as_coo_matrix("data.txt", 62500000),内存需要达到2.5GB的峰值(但需要int32而不是int64,只需要1.5GB。我的机器花了大约5分钟,但我的机器很慢......

    那么与您的解决方案有何不同?

    1. 我只从np.unique得到唯一的值(而不是所有的索引和反转),所以保存了一些内存 - 我可以用新的原位替换旧的ID。
    2. 我没有使用pandas的经验,所以在pandas&lt; - &gt; numpy数据结构之间可能会有一些复制?
    3. 与sascha的解决方案有什么不同?

      1. 列表不需要一直排序 - 在列表中的所有项目之后排序就足够了,这就是np.unique()所做的事情。 sascha的解决方案使列表按时排序 - 即使运行时间保持O(n log(n)),您也必须至少以常数因子为此付费。我假设,添加操作为O(n),但指出它是O(log(n)
      2. GrantJ的解决方案有什么区别?

        1. 生成的稀疏矩阵的大小为NxN - N - 不同节点的数量,而不是2^54x2^54(有很多空行和列)。
        2. PS:
          这是我的想法,9 char字符串id如何映射到int64值,但我想这个函数可能会成为一个瓶颈,就像它的编写方式一样,应该得到优化。

          def toInt64(string):
              res=0L
              for ch in string:
                  res*=62
                  if ch <='9':
                    res+=ord(ch)-ord('0')
                  elif ch <='Z':
                    res+=ord(ch)-ord('A')+10
                  else:
                    res+=ord(ch)-ord('a')+36
              return res
          

答案 4 :(得分:0)

你可能想看看igraph项目,这是一个C代码的GPL库,它是为这类东西设计的,并且有一个很好的Python API。我认为在你的情况下你的Python代码就像

from igraph import Graph
g = Graph.Read_Edgelist('edges.txt')
g.write_adjacency('adjacency_matrix.txt')
相关问题