Эффективные альтернативы словарям Python

В одном из моих текущих проектов стороны, я просматриваю какой-то текст, смотрящий на частоту слов триплетов. Вначале я использовал словарь по умолчанию для трех уровней. Другими словами, topDict[word1][word2][word3] возвращает количество раз, когда эти слова появляются в тексте, topDict[word1][word2] возвращает словарь со всеми словами, которые появляются после слов 1 и 2 и т.д.

Это работает правильно, но очень интенсивно. В моих первоначальных тестах он использовал что-то вроде 20-кратной памяти только для хранения триплетов в текстовом файле, что кажется чрезмерно большим объемом служебных данных памяти.

Мое подозрение в том, что многие из этих словарей создаются с большим количеством слотов, чем на самом деле используются, поэтому я хочу заменить словари чем-то другим, более эффективным с точки зрения памяти при использовании таким образом. Я бы предпочел решение, позволяющее искать ключевые слова по линиям словарей.

Из того, что я знаю о структурах данных, сбалансированное двоичное дерево поиска, использующее что-то вроде red-black или AVL, вероятно, было бы идеальным, но я бы предпочел не реализовывать их самостоятельно. Если возможно, я бы предпочел придерживаться стандартных библиотек python, но я определенно открыт для других альтернатив, если они будут работать лучше всего.

Итак, есть ли у кого-нибудь предложения для меня?

Отредактировано для добавления:

Спасибо за ответы до сих пор. Несколько ответов до сих пор предлагали использовать кортежи, что на самом деле не сильно помогло мне, когда я скомбинировал первые два слова в кортеж. Я не решаюсь использовать все три в качестве ключа, так как я хочу, чтобы было легко найти все третьи слова с учетом первых двух. (т.е. я хочу что-то вроде результата topDict[word1, word2].keys()).

Текущий набор данных, с которым я играю, - это самая последняя версия Wikipedia For Schools. Например, результаты разбора первой тысячи страниц - это что-то вроде 11 МБ для текстового файла, в котором каждая строка - это три слова, а каждая вкладка подсчитывается. Сохранение текста в формате словаря, который я сейчас использую, занимает около 185 МБ. Я знаю, что будут дополнительные накладные расходы для указателей и еще много чего, но разница кажется чрезмерной.

Ответ 1

Некоторые измерения. Я взял 10 МБ свободного текста электронной книги и вычисленных частот триграмм, создав файл 24 МБ. Хранение его в разных простых структурах данных Python заняло это много места в kB, измеренное как RSS от запуска ps, где d - это dict, ключи и freqs - это списки, а a, b, c, freq - поля записи триграмм:

295760     S. Lott answer
237984     S. Lott 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, как указано ниже), или реализовал сжатые структуры fancier, также описанные ниже.

Исходный ответ: Простой отсортированный массив строк, каждая строка которого представляет собой разделение пробела между словами, поиск по используемому модулю bisect, стоит попробовать начать. Это экономит место на указателях и т.д. Оно все еще тратит пространство из-за повторения слов; там стандартный трюк, чтобы вырезать общие префиксы, с другим уровнем индекса, чтобы вернуть их, но это более сложный и медленный. (Идея состоит в том, чтобы хранить последовательные фрагменты массива в сжатой форме, которые должны последовательно сканироваться вместе со индексом произвольного доступа к каждому фрагменту. Чункты достаточно большие, чтобы сжимать, но достаточно малые для разумного времени доступа. схема, применимая здесь: если последовательные записи являются "hello george" и "hello world", сделайте вторую запись "6world" вместо этого. (6 - длина общего префикса.) Или, может быть, вы могли бы избежать использования zlib? В любом случае, вы можете узнать больше в этом ключе, изучая структуры словаря, используемые в полнотекстовом поиске.) Так что конкретно, я бы начните с массива с ключами, являющимися первыми двумя словами, с параллельным массивом, чьи записи перечисляют возможные третьи слова и их частоты. Тем не менее, он все равно может сосать - я думаю, вам может быть не повезло, если бы батареи были включены в оперативную память.

Кроме того, двоичные древовидные структуры не рекомендуются для эффективности памяти. Например, в этой статье тестирует множество структур данных по аналогичной проблеме (хотя это и униграммы вместо триграмм), и находит хэш-таблицу, чтобы бить все дерево структур по этой мере.

Я должен был упомянуть, как и кто-то другой, что отсортированный массив можно использовать только для словарного списка, а не для биграмм или триграмм; то для вашей "реальной" структуры данных, что бы это ни было, вы используете целые ключи вместо строк - индексы в список слов. (Но это не позволяет вам использовать общие префиксы, кроме самого списка слов. Возможно, я не должен предлагать это в конце.)

Ответ 2

Используйте кортежи.
Кортежи могут быть ключом к словарям, поэтому вам не нужно вставлять словари.

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

Также как плюс, вы можете использовать defaultdict

  • чтобы элементы, у которых нет записей, всегда возвращают 0
  • и так, что u может сказать d[w1,w2,w3] += 1, не проверяя, существует ли ключ или нет

Пример:

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

Если вам нужно найти все слова "word3", которые чередуются с (word1, word2), ищите его в словарях .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)

Ответ 3

В этом случае ZODB ¹ BTrees может оказаться полезным, так как они намного менее голодны. Используйте BTrees.OOBtree(ключи объекта для значений Object) или BTrees.OIBTree(ключи объекта к значениям Integer) и используйте 3-словные кортежи в качестве ключа.

Что-то вроде:

from BTrees.OOBTree import OOBTree as BTree

Интерфейс, более или менее говорящий по типу, с добавленным бонусом (для вас), что .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)

Это дает мне ~ 88 МБ.

Изменение хранилища на

h[a, b, c] = 1

занимает ~ 25 МБ

интернирование a, b и c делает его занятием около 31 МБ. Мой случай немного особенный, потому что мои слова никогда не повторяются на входе. Вы можете попробовать некоторые варианты самостоятельно и посмотреть, поможет ли вам одна из них.

Ответ 5

Выполняете ли вы марковское построение текста?

Если ваши цепочки сопоставляют 2 слова с вероятностями третьего, я бы использовал словарь, сопоставляющий K-кортежи с гистограммой 3-го слова. Тривиальный (но голодный) способ реализовать гистограмму состоял бы в том, чтобы использовать список с повторами, а затем random.choice дает вам слово с правильной вероятностью.

Здесь реализация с K-кортежем в качестве параметра:

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

Это было бы проще всего реализовать. Я считаю, что вы увидите небольшое улучшение, если этого недостаточно... ... я что-нибудь придумаю...

Ответ 7

Итак, вы в основном пытаетесь сохранить разреженное трехмерное пространство. Тип шаблонов доступа, которые вы хотите использовать для этого пространства, имеет решающее значение для выбора алгоритма и структуры данных. Учитывая ваш источник данных, вы хотите передать это в сетку? Если вам не нужен O (1) доступ:

Чтобы получить эффективность памяти, вы хотите разбить это пространство на подпространства с таким же количеством записей. (например, BTree). Таким образом, структура данных с:

  • firstWordRange
  • secondWordRange
  • thirdWordRange
  • numberOfEntries
  • отсортированный блок записей.
  • Следующий и предыдущие блоки во всех трех измерениях

Ответ 8

Здесь древовидная структура, которая использует библиотеку 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 других слов.

Ответ 9

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

Ответ 10

Если память просто недостаточно большая, pybsddb может помочь сохранить постоянную карту на диске.

Ответ 11

Вы можете использовать многомерный массив 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 примерно так же эффективны, как и все, что вы, вероятно, найдете. Также обратите внимание, что я не пробовал этот код, поэтому я могу быть в некоторых деталях. Просто переходите от памяти здесь.

Ответ 12

Вы можете поместить все слова в словарь. ключ будет словом, а значение - числом (индексом).

Затем вы используете его следующим образом:

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

topDictionary[Word1][Word2][Word3]

Вставить в indexDict с помощью:

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