【问题标题】:How to get rid of MemoryError while dealing with a large dictionary?如何在处理大字典时摆脱 MemoryError?
【发布时间】:2016-08-24 08:06:48
【问题描述】:

我正在尝试使用字典类型的结构来构建单词三元组的索引。键是字符串,值是出现次数。

for t in arrayOfTrigrams:
    if t in trigrams:
        trigrams[t] += 1
    else:
        trigrams[t] = 1

但是数据非常大 - 超过 500 MB 的原始文本,我不知道如何处理 MemoryError。 与Python memoryerror creating large dictionary 不同,我不会创造任何不相关的东西,每个三元组都很重要。

【问题讨论】:

标签: python dictionary memory word-frequency


【解决方案1】:

我的第一个建议是不要将arrayOfTrigrams 完全保存在内存中,而是使用流式传输。您正在从某个地方阅读它,因此您可以控制阅读方式。 Python 的生成器在这里非常方便。假设您正在从文件中读取它:

def read_trigrams(fobj):
    unique = {}
    def make_unique(w):
        w = w.strip("\"'`!?,.():-;{}").lower()
        return unique.setdefault(w, w)
    fobj.seek(0, 2)
    total_size = fobj.tell()
    fobj.seek(0, 0)

    read = 0
    prev_words = []
    for idx, line in enumerate(fobj):
        read += len(line)
        words = prev_words
        words.extend(filter(None, (make_unique(w) for w in line.split())))
        if len(words) > 3:
            for i in range(len(words) - 3):
                yield tuple(words[i:i+3])
        prev_words = words[-2:]

这里有两件事:

  1. 我们正在使用生成器,因此我们不是读取整个文件并返回一个三元组列表,而是一个接一个地返回三元组。这有点慢,但可以节省内存。
  2. 我们确保最终,我们读取的每个字符串最多只有一个副本,方法是自己拥有一个字符串字典。虽然一开始可能看起来很奇怪,但从文件N 读取相同的字节序列S 时间确实占用N*len(S) 字节。通过使用字典,我们确保输入中的每个单词都有一个唯一的副本。当然,这确实会消耗一些内存。

这个函数对你来说可能看起来不同,这取决于你从哪里读取你的三元组。请记住,我在这里使用的分词器非常基础。

这已经节省了一点内存,不过不会太多。

所以,让我们将中间结果存储在磁盘上:

LIMIT = 5e6
def flush(counts, idx):
    with open('counts-%d' % (idx,), 'wb') as fobj:
        p = pickle.Pickler(fobj)
        for item in sorted(counts.items()):
            p.dump(item)

import sys
import pickle
from collections import defaultdict

counts = defaultdict(int)
caches = 0
with open(sys.argv[1], 'r') as fobj:
    for t in read_trigrams(fobj):
        counts[t] += 1
        if len(counts) > LIMIT:
            flush(counts, caches)
            caches += 1
            counts.clear()
flush(counts, caches)

在此步骤中,您可以调整 LIMIT 以不使用太多内存,即减少它直到您不再遇到 MemoryError

现在,您的驱动器上有N 文件,其中包含已排序的三元组列表。在单独的程序中,您可以将它们读入并汇总所有中间计数:

import sys
import pickle

def merger(inputs):
    unpicklers = [pickle.Unpickler(open(f, 'rb')) for f in inputs]
    DONE = (object(), )
    NEXT = (object(), )

    peek = [NEXT] * len(unpicklers)

    while True:
        for idx in range(len(unpicklers)):
            if peek[idx] is NEXT:
                try:
                    peek[idx] = unpicklers[idx].load()
                except EOFError:
                    peek[idx] = DONE

        if all(v is DONE for v in peek):
            return
        min_key = min(v[0] for v in peek if v is not DONE)
        yield min_key, sum(v[1] for v in peek if v[0] == min_key)
        peek = [NEXT if (v[0] == min_key) else v for v in peek]


for trigram, count in merger(sys.argv[1:]):
    print(trigram, count)

如果您有 4 GiB 的内存,您实际上可能必须使用拆分功能。使用 8 GiB,您应该能够将其全部保存在 RAM 中。

【讨论】:

    【解决方案2】:

    在进一步编辑时 -- 包含代码 如果您能够将 arrayOfTrigrams 保存在内存中,请参阅底部的原始解决方案。但是,如果您无法创建arrayOfTrigrams(鉴于数据大小,我有点怀疑您已经走到了那一步),您仍然可以为创建重复三元组​​字典而努力。重复的二元组总是包含重复的单词,而重复的三元组包含重复的二元组。分阶段处理您的 500 MB 数据。首先创建一组重复的单词。使用它,创建一个重复二元组的字典。首先对包含重复单词之一的二元组进行原始频率计数,然后丢弃任何计数仅为 1 的单词。然后第三次处理数据,创建重复三元组​​字典。再次,对包含重复二元组(应该是所有可能三元组的一个小子集)的三元组进行原始频率计数,然后从字典中丢弃最终计数仅为 1 的三元组。这样,您可以构建字典而无需曾经需要一次将所有三元组保存在内存中。

    概念证明:

    from collections import defaultdict
    
    chars = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
    
    def cleanWord(s):
        return ''.join(c for c in s if c in chars)
    
    f = open('moby dick.txt') #downloaded from Project Gutenberg: http://www.gutenberg.org/ebooks/2701 -- Thanks!
    words = f.read().split()
    f.close()
    
    words = [cleanWord(w.upper()) for w in words]
    words = [w for w in words if len(w) > 1 and not(w in set('AIOY'))]
    
    repeatedWords = defaultdict(int)
    for w in words:
        repeatedWords[w] += 1
    
    repeatedWords = set(w for w in repeatedWords if repeatedWords[w] > 1)
    
    repeatedBigrams = defaultdict(int)
    for i in range(len(words) - 1):
        x,y = words[i:i+2]
        if x in repeatedWords or y in repeatedWords:
            repeatedBigrams[x + ' ' + y] +=1
    
    repeatedBigrams = set(b for b in repeatedBigrams if repeatedBigrams[b] > 1)
    
    repeatedTrigrams = defaultdict(int)
    
    for i in range(len(words) - 2):
        x,y,z = words[i:i+3]
        if x + ' ' + y in repeatedBigrams and y + ' ' + z in repeatedBigrams:
            repeatedTrigrams[x + ' ' + y + ' ' + z] +=1
    
    repeatedTrigrams = {t:c for t,c in repeatedTrigrams.items() if c > 1}
    

    此代码出现了 10016 个多次出现的三元组。相反,当我评估时

    len(set(' '.join(words[i:i+3]) for i in range(len(words)-2)))
    

    我得到 188285,所以在这个相当大的自然语言示例中,只有 10016/188285 = 5.3% 的可能三元组是重复三元组​​。假设您的数据具有相似的比率,我估计重复三元组​​的频率字典大小约为 100 MB。


    原解决方案:


    您的代码和您的问题表明您可以将arrayOfTrigrams 保存在内存中,但无法创建字典。一种可能的解决方法是首先对该数组进行排序并创建 repeated trigrams 的频率计数:

    arrayOfTrigrams.sort()
    repeatedTrigrams = {}
    
    for i,t in enumerate(arrayOfTrigrams):
        if i > 0 and arrayOfTrigrams[i-1] == t:
            if t in repeatedTrigrams:
                repeatedTrigrams[t] += 1
            else:
                repeatedTrigrams[t] = 2
    

    在构造 repeatedTrigrams 之后,您可以使用集合推导:

    uniques = {t for t in arrayOfTrigrams if not t in repeatedTrigrams}
    

    然后t in uniques 将传达t 的计数为 1 的信息,我怀疑绝大多数三元组都是如此。在这个阶段,您拥有所有相关的频率信息,并且可以丢弃三元组列表以释放您消耗的一些内存:

    arrayOfTrigrams = None 
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-09
      • 1970-01-01
      • 2016-07-27
      • 2021-04-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多