如何在处理大字典时摆脱MemoryError?

时间:2016-04-29 08:43:53

标签: python dictionary memory word-frequency

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

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

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

2 个答案:

答案 0 :(得分:0)

进一步编辑 - 包含的代码如果您能够将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保留在内存中,但无法创建字典。一种可能的解决方法是首先对此数组进行排序,并创建重复三元组的频率计数:

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 

答案 1 :(得分:0)

我的第一个建议是不要将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. 我们使用的是生成器而不是读取整个文件并返回三元组列表,我们通过trigram返回trigram。这有点慢,但可以节省内存。
  2. 我们确保最终,我们读取的每个字符串最多只有一个副本,方法是将字符串写入自身。虽然起初看起来很奇怪,但是从文件S时间读取相同的字节序列N会占用N*len(S)个字节。通过使用字典,我们确保输入中每个单词都有一个唯一的副本。当然,这确实消耗了一些记忆。
  3. 此功能对您来说可能有所不同,具体取决于您从哪里读取三卦。请记住,我在这里使用的标记器是非常基本的。

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

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

    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中。