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

Posted

技术标签:

【中文标题】Python 字典的内存高效替代方案【英文标题】:Memory Efficient Alternatives to Python Dictionaries 【发布时间】:2010-09-24 13:00:45 【问题描述】:

在我目前的一个副项目中,我正在浏览一些文本,查看单词三连音的频率。在我第一次尝试时,我使用了默认字典深度三层。换句话说,topDict[word1][word2][word3] 返回这些单词在文本中出现的次数,topDict[word1][word2] 返回一个字典,其中包含单词 1 和 2 之后出现的所有单词,等等。

这可以正常工作,但它非常占用内存。在我最初的测试中,它使用的内存是仅将三元组存储在文本文件中的 20 倍,这似乎是过多的内存开销。

我的怀疑是,这些字典中的许多创建时使用的插槽比实际使用的插槽多得多,因此我想用以这种方式使用时内存效率更高的其他字典替换字典。我强烈希望有一种解决方案,它允许按照字典的方式进行键查找。

根据我对数据结构的了解,使用红黑或 AVL 之类的平衡二叉搜索树可能是理想的,但我真的不想自己实现它们。如果可能的话,我更愿意坚持使用标准的 Python 库,但如果它们效果最好,我绝对愿意接受其他替代方案。

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

编辑添加:

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

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

【问题讨论】:

您能提供一个指向您的示例单词列表的链接吗? Wikipedia For Schools 已禁用下载。您的 11MB 文件以及您计划从中获得的内容(可能是您当前的实现)非常适合测试。 【参考方案1】:

一些测量。我获取了 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);它们被列出只是因为其他人建议了它们(或它们的变体)。 (我有点厌烦做这个,因为投票最多的答案没有帮助,如表格所示。)

'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'))

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

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

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

我应该像其他人一样提到,排序后的数组只能用于单词列表,而不是二元组或三元组;然后对于您的“真实”数据结构,无论它是什么,您都使用整数键而不是字符串——词表中的索引。 (但这可以防止您利用除了单词表本身之外的常见前缀。也许我毕竟不应该建议这个。)

【讨论】:

您可以访问原始字典吗?如果是这样,那么测试d[(intern(a),intern(b)] = sorted([(int(freq), intern(c))]) 将是有益的......重点是单词对返回第三个单词的(排序)列表...... @F1Rumors,恐怕不会。此外,从今天的角度来看,这一切都是在一个古老的 Python 版本上完成的。【参考方案2】:

使用元组。 元组可以是字典的键,所以你不需要嵌套字典。

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”,则使用列表推导在 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) 开头的元组中作为第三项出现的所有项的列表

【讨论】:

em...对于如此大的输入,使用列表推导式搜索将非常慢(嗯,这是一个线性搜索,但“n”会非常大)。在这里使用 dict 的目的是为了快速查找【参考方案3】:

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

类似:

from BTrees.OOBTree import OOBTree as BTree

界面或多或少类似于 dict,另外(对您而言).keys.items.iterkeys.iteritems 有两个 min, max 可选参数:

>>> 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')]

¹ 请注意,如果您在 Windows 上使用 Python >2.4,我知道有一些适用于最新 Python 版本的软件包,但我不记得在哪里。

PS 它们存在于CheeseShop ☺

【讨论】:

【参考方案4】:

几次尝试:

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

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。我的情况有点特别,因为我的话从不重复输入。您可以自己尝试一些变体,看看其中一种是否对您有帮助。

【讨论】:

【参考方案5】:

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

如果您的链将 2 个单词映射到第三个单词的概率,我会使用字典将 K 元组映射到第 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,)

【讨论】:

【参考方案6】:

你可以尝试使用相同的字典,只有一层深度。

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

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

这将是最容易实现的。 我相信你会看到一点进步,如果这还不够…… ...我会想办法的...

【讨论】:

我试着做两层深度,其中键是单词 1 和 2 的元组,它实际上增加了内存使用量。我强烈希望能够轻松访问给定 1 和 2 的所有第三个单词,因此将它们全部用作键可能已经失效。 另外,我的理解是 dict 是使用某种哈希表实现的,尽管我从来没有找到明确的来源。 1.使用散列函数计算键的散列值。 2. 哈希值寻址 d.data 中的一个位置,该位置应该是包含(键,值)对的“桶”或“冲突列表”数组。 3. 按顺序搜索碰撞列表__我认为RB在第二步。 我想我可能会继续询问 dict() 的实现作为它自己的问题。顺便说一句,谢谢您的回答。【参考方案7】:

好的,所以您基本上是在尝试存储稀疏的 3D 空间。您希望对该空间的访问模式类型对于算法和数据结构的选择至关重要。考虑到您的数据源,您是否要将其提供给网格?如果您不需要 O(1) 访问权限:

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

第一个词范围 secondWordRange 第三个词范围 numberOfEntries 排序的条目块。 所有 3 个维度的下一个和上一个块

【讨论】:

【参考方案8】:

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】:

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

【讨论】:

【参考方案10】:

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

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 数组的效率与您可能找到的任何东西差不多。另请注意,我还没有尝试过这段代码,所以我可能会在一些细节上有所偏差。只是从这里回忆。

【讨论】:

这个总体思路可能会有所帮助,但不会自行发挥作用。在我的测试输入中有 100000 个不同的单词;一个 3d 数组需要 10^15 个条目。【参考方案11】:

这是一个使用 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 语句,以便在我们进行比较之前确定列表是否实际上是空的。它只有一次是空的,所以 if 语句对于所有 n-1 其他词都是浪费的。

【讨论】:

【参考方案12】:

您可以将所有单词放入字典中。 键是单词,值是数字(索引)。

然后你这样使用它:

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

topDictionary[Word1][Word2][Word3]

在 indexDict 中插入:

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

【讨论】:

我希望这与字符串的实习大致相同。 它只使用整数而不是字符串作为键。他只需要对其进行基准测试即可确定。 当我尝试这个时,有一个节省,但并不是那么多。如果我没记错的话,大概是 165MB 和 185MB。 您在 topDictionary[][][] 中使用什么作为值?

以上是关于Python 字典的内存高效替代方案的主要内容,如果未能解决你的问题,请参考以下文章

Python内置库itertools生成穷举字典

将大型列表保存在内存中的替代方法(python)

高并发内存池的介绍

高并发内存池的介绍

高并发内存池的介绍

Python 2.7中的无内存泄漏OrderedDict替代方案?