Python字典的高效内存替代方案

Python字典的高效内存替代方案,python,memory,data-structures,Python,Memory,Data Structures,在我目前的一个辅助项目中,我正在浏览一些文本,查看单词三元组的频率。在我的第一次尝试中,我使用了三个层次的默认字典。换句话说,topDict[word1][word2][word3]返回这些单词在文本中出现的次数,topDict[word1][word2]返回包含单词1和2后面出现的所有单词的词典,以此类推 这项功能正常,但内存非常密集。在我最初的测试中,它使用的内存大约是将三元组存储在文本文件中的内存的20倍,这似乎是一个过大的内存开销 我的怀疑是,这些字典中有许多是用比实际使用的更多的插槽创

在我目前的一个辅助项目中,我正在浏览一些文本,查看单词三元组的频率。在我的第一次尝试中,我使用了三个层次的默认字典。换句话说,
topDict[word1][word2][word3]
返回这些单词在文本中出现的次数,
topDict[word1][word2]
返回包含单词1和2后面出现的所有单词的词典,以此类推

这项功能正常,但内存非常密集。在我最初的测试中,它使用的内存大约是将三元组存储在文本文件中的内存的20倍,这似乎是一个过大的内存开销

我的怀疑是,这些字典中有许多是用比实际使用的更多的插槽创建的,因此我想用其他一些以这种方式使用时更节省内存的东西来替换这些字典。我强烈希望有一个解决方案,允许沿着字典的行进行键查找

d = {}
d[ word1, word2, word3 ] = 1
根据我对数据结构的了解,使用诸如红黑或AVL之类的东西的平衡二叉搜索树可能是理想的,但我真的不希望自己实现它们。如果可能的话,我更愿意坚持使用标准的python库,但如果它们能发挥最佳效果,我肯定会接受其他替代方案

那么,有人对我有什么建议吗

编辑以添加:

感谢到目前为止的回复。到目前为止,一些答案建议使用元组,当我将前两个单词压缩成一个元组时,这对我并没有多大帮助。我很犹豫是否将这三个词都用作键,因为我希望在给定前两个词的情况下,可以轻松查找所有第三个词。(也就是说,我想要类似于topDict[word1,word2].keys()的结果。)


我正在使用的当前数据集是的最新版本。例如,解析前1000页的结果对于一个文本文件来说大约是11MB,其中每行是三个单词,count all选项卡是分开的。以我现在使用的字典格式存储文本大约需要185MB。我知道指针之类的东西会有一些额外的开销,但差别似乎太大了。

您可以尝试使用同一个字典,只有一层深度

topDictionary[word1+delimiter+word2+delimiter+word3]
分隔符可以是普通的“”。(或使用(单词1、单词2、单词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。我的情况有点特殊,因为我的话在输入上从不重复。您可以自己尝试一些变体,看看其中一种是否对您有帮助。

一些测量值。我获取了10MB的免费电子书文本和计算出的三叉记忆频率,生成了一个24MB的文件。将其存储在不同的简单Python数据结构中占用了这么多的空间(以kB为单位),以运行ps的RSS来衡量,其中d是dict,keys和freq是list,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);它们被列出只是因为其他人建议了它们(或它们的变体)。(如表所示,我之所以这么做,是因为排名靠前的答案毫无帮助。)

“Pair array”是我原始答案中的方案(“我从带密钥的数组开始 是前两个单词…),其中每对的值表为 表示为单个字符串。”“压缩对数组”是相同的, 忽略等于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'))
我还没有编写从这个结构中查找值的代码(使用二分法,如下所述),也没有实现下面描述的更高级的压缩结构

原始答案:一个简单的字符串排序数组,每个字符串都是用对分模块搜索的单词的空格分隔串联,应该值得一试。这节省了指针等的空间。由于重复单词,它仍然浪费空间;有一个标准的技巧可以去掉常见的前缀,通过另一个级别的索引将它们取回来,但这相当复杂和缓慢。(想法是以压缩的形式存储数组的连续块,必须按顺序扫描,以及每个块的随机访问索引。块大到可以压缩,但小到可以合理访问时间。此处适用的特定压缩方案:如果连续条目是“hello george”和“hello world”,则m将第二个条目改为“6world”。(6是前缀的共同长度。)或者您可以不使用?无论如何,您可以通过查找全文搜索中使用的词典结构来了解更多内容。)因此,具体来说,我将从数组开始,其中键是前两个单词,使用并行数组,其条目列出可能的第三个单词及其频率。不过,它可能仍然很糟糕——我认为你可能运气不好,因为电池包括内存效率高的选项

此外,这里不建议使用二叉树结构来提高内存效率。例如,在一个类似的问题上测试各种数据结构(虽然是单图而不是三元图),然后找到一个哈希表,以该度量击败所有树结构

我应该像其他人一样提到,排序数组可以只用于单词列表,而不是bigram或trigram;然后,对于您的“真实”数据结构,不管它是什么,您使用整数键而不是字符串——索引到单词列表中。(但这会让你感到不安
if word not in indexDict:
    indexDict[word]=len(indexDict)
d = {}
d[ word1, word2, word3 ] = 1
from collections import defaultdict
d = defaultdict(int)
d["first","word","tuple"] += 1
>>> 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]
from BTrees.OOBTree import OOBTree as BTree
>>> t=BTree()
>>> t['a', 'b', 'c']= 10
>>> t['a', 'b', 'z']= 11
>>> t['a', 'a', 'z']= 12
>>> t['a', 'd', 'z']= 13
>>> print list(t.keys(('a', 'b'), ('a', 'c')))
[('a', 'b', 'c'), ('a', 'b', 'z')]
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,)
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
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
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