Python字典的内存有效替代品

时间:2008-11-29 05:33:27

标签: python memory data-structures

在我目前的一个侧面项目中,我正在扫描一些文字,查看三联词的频率。在我第一次使用它时,我使用了三级深度的默认字典。换句话说,topDict[word1][word2][word3]会返回这些字词出现在文本中的次数,topDict[word1][word2]会返回一个字典,其中包含出现在单词1和2之后的所有单词等。

此功能正常,但内存密集。在我的初始测试中,它使用了将三元组存储在文本文件中的内存的20倍,这看起来像是一个过大的内存开销。

我怀疑这些词典中的很多都是使用比实际使用的更多的插槽创建的,所以我想用这种方式替换字典时使用更高效的内存。我强烈希望有一种解决方案,允许按字典的方式进行密钥查找。

据我所知的数据结构,使用红黑或AVL之类的平衡二叉搜索树可能是理想的,但我真的不愿意自己实现它们。如果可能的话,我宁愿坚持使用标准的python库,但如果它们最好的话,我肯定会接受其他选择。

那么,有人对我有任何建议吗?

编辑添加:

感谢目前为止的回复。到目前为止,一些答案建议使用元组,当我将前两个单词浓缩为元组时,这对我来说并没有什么作用。我犹豫是否要将所有这三个用作关键因为我希望它能够很容易地查找前两个字的所有第三个字。 (即我想要topDict[word1, word2].keys()的结果。

我正在使用的当前数据集是Wikipedia For Schools的最新版本。例如,对于文本文件,解析前几千页的结果类似于11MB,其中每行是三个单词并且计数所有选项卡分开。以我现在使用的字典格式存储文本大约需要185MB。我知道指针和诸如此类的东西会有一些额外的开销,但差异似乎过大。

12 个答案:

答案 0 :(得分:29)

一些测量。我拿了10MB的免费电子书文本并计算了三元组频率,产生了24MB的文件。将它存储在不同的简单Python数据结构中需要在kB中占用这么多空间,测量为运行ps的RSS,其中d是dict,keys和freqs是列表,a,b,c,freq是trigram记录的字段:

295760     S. Lott's answer
237984     S. Lott's with keys interned before passing in
203172 [*] d[(a,b,c)] = int(freq)
203156     d[a][b][c] = int(freq)
189132     keys.append((a,b,c)); freqs.append(int(freq))
146132     d[intern(a),intern(b)][intern(c)] = int(freq)
145408     d[intern(a)][intern(b)][intern(c)] = int(freq)
 83888 [*] d[a+' '+b+' '+c] = int(freq)
 82776 [*] d[(intern(a),intern(b),intern(c))] = int(freq)
 68756     keys.append((intern(a),intern(b),intern(c))); freqs.append(int(freq))
 60320     keys.append(a+' '+b+' '+c); freqs.append(int(freq))
 50556     pair array
 48320     squeezed pair array
 33024     squeezed single array

标记为[*]的条目没有有效的方法来查找一对(a,b);它们只是因为其他人提出它们(或它们的变体)而被列出。 (我对此表示厌恶,因为表格显示,最高投票的答案没有用。)

'配对数组'是我原来的答案中的下面的方案(“我将从带有键的数组开始 是前两个单词......“),其中每对的值表是 表示为单个字符串。 '压缩对阵列'是一样的, 省略等于1的频率值(最常见的 案件)。 “压缩单个数组”就像压缩对数组一样,但是将键和值一起作为一个字符串(带有分隔符)。压缩的单个数组代码:

import collections

def build(file):
    pairs = collections.defaultdict(list)
    for line in file:  # N.B. file assumed to be already sorted
        a, b, c, freq = line.split()
        key = ' '.join((a, b))
        pairs[key].append(c + ':' + freq if freq != '1' else c)
    out = open('squeezedsinglearrayfile', 'w')
    for key in sorted(pairs.keys()):
        out.write('%s|%s\n' % (key, ' '.join(pairs[key])))

def load():
    return open('squeezedsinglearrayfile').readlines()

if __name__ == '__main__':
    build(open('freqs'))

我没有编写代码来查找此结构中的值(使用bisect,如下所述),或者实现了下面描述的更高级的压缩结构。

原始答案:一个简单的排序字符串数组,每个字符串是一个以空格分隔的单词串联,使用bisect模块进行搜索,应该值得一试。这节省了指针等空间。由于重复单词,它仍然浪费空间;有一个标准的技巧来删除常见的前缀,使用另一个级别的索引来恢复它们,但这更复杂,更慢。 (这个想法是以压缩的形式存储阵列的连续块,必须按顺序扫描,以及每个块的随机访问索引。块大到足以压缩,但足够小以便合理访问时间。特定的压缩这里适用的方案:如果连续的条目是'hello george'和'hello world',则将第二个条目改为'6world'。(6是前缀的长度。)或者也许你可以使用{{{ 3}}?无论如何,你可以通过查找全文搜索中使用的字典结构来了解更多信息。)具体来说,我从数组开始,键是前两个字,有一个并行数组条目列出可能的第三个单词及其频率。尽管如此,它可能仍然很糟糕 - 我认为就电池包含的内存效率选项而言,你可能会失去运气。

此外,二进制树结构建议用于内存效率。例如,zlib测试关于类似问题的各种数据结构(虽然是unigrams而不是trigrams),并找到一个哈希表来通过该度量来击败所有树结构。

我应该像其他人一样提到,排序的数组只能用于wordlist,而不是bigrams或trigrams;那么对于你的“真实”数据结构,无论它是什么,你都使用整数键而不是字符串 - 索引到单词列表中。 (但这会阻止你利用常用的前缀,除了词汇表本身。也许我不应该建议这一点。)

答案 1 :(得分:9)

使用元组 元组可以是字典的关键,因此您不需要嵌套字典。

d = {}
d[ word1, word2, word3 ] = 1

另外,您可以使用defaultdict

  • 以便没有条目的元素始终返回0
  • 以便您可以在不检查密钥是否已存在的情况下说d[w1,w2,w3] += 1

示例:

from collections import defaultdict
d = defaultdict(int)
d["first","word","tuple"] += 1

如果你需要找到所有与word1,word2相关的单词“word3”,那么使用list comprehension在dictionary.keys()中搜索它

如果你有一个元组,t,你可以使用切片得到前两个项目:

>>> a = (1,2,3)
>>> a[:2]
(1, 2)

使用列表推导搜索元组的一个小例子:

>>> b = [(1,2,3),(1,2,5),(3,4,6)]
>>> search = (1,2)
>>> [a[2] for a in b if a[:2] == search]
[3, 5]

你在这里看到,我们得到了一个列表,列出了以(1,2)开头的元组中的第三个项目

答案 2 :(得分:4)

在这种情况下,ZODB¹BTrees可能会有所帮助,因为它们对内存的需求要少得多。使用BTrees.OOBtree(对象键对象值)或BTrees.OIBTree(对象键为整数值),并使用3个字元组作为键。

类似的东西:

from BTrees.OOBTree import OOBTree as BTree

界面或多或少是类似dict的,为.keys.items.iterkeys.iteritems提供了两个{{ {1}}可选参数:

min, max

¹请注意,如果您使用的是Windows并使用Python> 2.4,我知道有更新的python版本的软件包,但我无法回忆起哪里。

PS它们存在于CheeseShop

答案 3 :(得分:3)

几次尝试:

我认为你正在做类似的事情:

from __future__ import with_statement

import time
from collections import deque, defaultdict

# Just used to generate some triples of words
def triplegen(words="/usr/share/dict/words"):
    d=deque()
    with open(words) as f:
        for i in range(3):
            d.append(f.readline().strip())

        while d[-1] != '':
            yield tuple(d)
            d.popleft()
            d.append(f.readline().strip())

if __name__ == '__main__':
    class D(dict):
        def __missing__(self, key):
            self[key] = D()
            return self[key]
    h=D()
    for a, b, c in triplegen():
        h[a][b][c] = 1
    time.sleep(60)

这给了我~88MB。

将存储更改为

h[a, b, c] = 1

需要~25MB

实习a,b和c使得大约需要31MB。我的情况有点特殊,因为我的话从不重复输入。您可以自己尝试一些变体,看看其中一个是否对您有帮助。

答案 4 :(得分:2)

您是否正在实施马尔可夫文字生成?

如果您的链将2个单词映射到第三个单词的概率,我会使用字典映射K-tuples到第3个单词的直方图。实现直方图的一种简单但需要内存的方法是使用带有重复的列表,然后random.choice会给出一个具有正确概率的单词。

以下是以K-tuple为参数的实现:

import random

# can change these functions to use a dict-based histogram
# instead of a list with repeats
def default_histogram():          return []
def add_to_histogram(item, hist): hist.append(item)
def choose_from_histogram(hist):  return random.choice(hist)

K=2 # look 2 words back
words = ...
d = {}

# build histograms
for i in xrange(len(words)-K-1):
  key = words[i:i+K]
  word = words[i+K]

  d.setdefault(key, default_histogram())
  add_to_histogram(word, d[key])

# generate text
start = random.randrange(len(words)-K-1)
key = words[start:start+K]
for i in NUM_WORDS_TO_GENERATE:
  word = choose_from_histogram(d[key])
  print word,
  key = key[1:] + (word,)

答案 5 :(得分:1)

您可以尝试使用相同的字典,只有一个级别。

topDictionary[word1+delimiter+word2+delimiter+word3]

分隔符可以是简单的“”。 (或使用(word1,word2,word3))

这是最容易实现的。 如果不够,我相信你会看到一点改进...... ......我会想到......

答案 6 :(得分:1)

好的,所以你基本上试图存储一个稀疏的3D空间。您想要这种空间的访问模式对于算法和数据结构的选择至关重要。考虑到您的数据源,您想将其提供给网格吗?如果您不需要O(1)访问:

为了获得内存效率,您希望将该空间细分为具有相似数量条目的子空间。 (像BTree一样)。所以数据结构包含:

  • firstWordRange
  • secondWordRange
  • thirdWordRange
  • numberOfEntries
  • 一个已排序的条目块。
  • 所有3个维度中的下一个和前一个块

答案 7 :(得分:1)

这是一个树结构,它使用bisect库来维护一个排序的单词列表。每次查找 O (log2(n))。

import bisect

class WordList( object ):
    """Leaf-level is list of words and counts."""
    def __init__( self ):
        self.words= [ ('\xff-None-',0) ]
    def count( self, wordTuple ):
        assert len(wordTuple)==1
        word= wordTuple[0]
        loc= bisect.bisect_left( self.words, word )
        if self.words[loc][0] != word:
            self.words.insert( loc, (word,0) )        
        self.words[loc]= ( word, self.words[loc][1]+1 )
    def getWords( self ):
        return self.words[:-1]

class WordTree( object ):
    """Above non-leaf nodes are words and either trees or lists."""
    def __init__( self ):
        self.words= [ ('\xff-None-',None)  ]
    def count( self, wordTuple ):
        head, tail = wordTuple[0], wordTuple[1:]
        loc= bisect.bisect_left( self.words, head )
        if self.words[loc][0] != head:
            if len(tail) == 1:
                newList= WordList()
            else:
                newList= WordTree()
            self.words.insert( loc, (head,newList) )
        self.words[loc][1].count( tail )
    def getWords( self ):
        return self.words[:-1]

t = WordTree()
for a in ( ('the','quick','brown'), ('the','quick','fox') ):
    t.count(a)

for w1,wt1 in t.getWords():
    print w1
    for w2,wt2 in wt1.getWords():
        print " ", w2
        for w3 in wt2.getWords():
            print "  ", w3

为简单起见,这在每个树和列表中使用虚拟值。这节省了无穷无尽的if语句,以便在我们进行比较之前确定列表是否实际为空。它只是空的一次,因此所有 n -1的其他单词都浪费了if语句。

答案 8 :(得分:1)

Scipy有稀疏矩阵,所以如果你能把前两个单词作为元组,你可以这样做:

import numpy as N
from scipy import sparse

word_index = {}
count = sparse.lil_matrix((word_count*word_count, word_count), dtype=N.int)

for word1, word2, word3 in triple_list:
    w1 = word_index.setdefault(word1, len(word_index))
    w2 = word_index.setdefault(word2, len(word_index))
    w3 = word_index.setdefault(word3, len(word_index))
    w1_w2 = w1 * word_count + w2
    count[w1_w2,w3] += 1

答案 9 :(得分:0)

如果内存不够大,pybsddb可以帮助存储磁盘持久性映射。

答案 10 :(得分:0)

您可以使用numpy多维数组。你需要使用数字而不是字符串来索引数组,但这可以通过使用单个字典将单词映射到数字来解决。

import numpy
w = {'word1':1, 'word2':2, 'word3':3, 'word4':4}
a = numpy.zeros( (4,4,4) )

然后索引到你的数组,你会做类似的事情:

a[w[word1], w[word2], w[word3]] += 1

这种语法并不美观,但是numpy数组与你可能找到的任何数据一样高效。另请注意,我还没有尝试过这个代码,所以我可能会关注一些细节。只是从记忆中走出来。

答案 11 :(得分:-1)

您可以将所有单词放在字典中。 key是word,value是number(index)。

然后你就这样使用它:

Word1=indexDict[word1]
Word2=indexDict[word2]
Word3=indexDict[word3]

topDictionary[Word1][Word2][Word3]

在indexDict中插入:

if word not in indexDict:
    indexDict[word]=len(indexDict)