Эффективные альтернативы словарям 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)