web-dev-qa-db-ja.com

Python辞書のメモリ効率の良い代替手段

現在のサイドプロジェクトの1つで、Wordトリプレットの頻度を調べてテキストをスキャンしています。最初に行ったときは、デフォルトの辞書を3レベル深く使用しました。つまり、_topDict[Word1][Word2][Word3]_はこれらの単語がテキストに出現する回数を返し、_topDict[Word1][Word2]_は単語1と2の後に出現するすべての単語を含む辞書を返します。

これは正しく機能しますが、メモリを大量に消費します。私の最初のテストでは、トリプレットをテキストファイルに保存するだけの20倍のメモリを使用しました。これは、メモリのオーバーヘッドが大きすぎるようです。

私の疑惑は、これらの辞書の多くが実際に使用されているよりもはるかに多くのスロットで作成されているため、この方法で使用すると、辞書をよりメモリ効率の高いものに置き換えたいと思います。辞書に沿ってキーを検索できるソリューションを強くお勧めします。

私がデータ構造について知っていることから、赤黒やAVLのようなものを使用した平衡二分探索木がおそらく理想的ですが、私はそれらを自分で実装したくないのです。可能であれば、標準のpythonライブラリを使用することをお勧めしますが、他の選択肢が最適に機能する場合は、他の選択肢を受け入れることは間違いありません。

それで、誰かが私に何か提案がありますか?

追加するために編集:

これまでの回答ありがとうございます。これまでの回答のいくつかは、タプルの使用を提案していますが、最初の2つの単語をタプルに凝縮したときはあまり効果がありませんでした。最初の2つを指定して、3つすべての単語を簡単に検索できるようにしたいので、3つすべてをキーとして使用することを躊躇します。 (つまり、topDict[Word1, Word2].keys()の結果のようなものが必要です)。

私が遊んでいる現在のデータセットは、 Wikipedia For Schools の最新バージョンです。たとえば、最初の1000ページを解析した結果は、各行が3語で、すべてのタブが区切られたテキストファイルの場合は11MBのようになります。現在使用している辞書形式でテキストを保存するには、約185MBかかります。ポインタなどに追加のオーバーヘッドがあることは知っていますが、違いは大きすぎるようです。

43
ricree

いくつかの測定。私は10MBの無料の電子書籍テキストと計算されたトリグラム頻度を取り、24MBのファイルを作成しました。異なる単純なPythonデータ構造に格納すると、実行中のpsからのRSSとして測定され、kBでこれだけのスペースが必要になります。ここで、dはdict、keysとfreqsはリスト、a、b、c、 freqは、トリグラムレコードのフィールドです。

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)を検索する効率的な方法がありません。他の人がそれら(またはそれらの変形)を提案したという理由だけでそれらはリストされています。 (表に示されているように、上位投票の回答が役に立たなかったため、私はこれを作成することに少しイライラしました。)

「ペア配列」は、私の元の回答(「キーが最初の2単語である配列から始めます...」)の以下のスキームであり、各ペアの値テーブルは単一の文字列として表されます。 「スクイーズドペア配列」は同じですが、1に等しい頻度値が除外されています(最も一般的なケース)。 「スクイーズドシングル配列」はスクイーズドペア配列に似ていますが、キーと値を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モジュールを使用して検索されます。スタート。これにより、ポインタなどのスペースが節約されます。単語が繰り返されるため、スペースが無駄になります。一般的なプレフィックスを削除する標準的なトリックがあり、それらを取り戻すために別のレベルのインデックスがありますが、それはかなり複雑で低速です。 (アイデアは、配列の連続するチャンクを、各チャンクへのランダムアクセスインデックスとともに、順次スキャンする必要がある圧縮形式で格納することです。チャンクは、圧縮するには十分な大きさですが、妥当なアクセス時間には十分に小さいです。特定の圧縮ここで適用可能なスキーム:連続するエントリが「hellogeorge」と「helloworld」の場合は、代わりに2番目のエントリを「6world」にします(6は共通のプレフィックスの長さです)。または、-を使用して回避することもできます。 zlib ?とにかく、フルテキスト検索で使用される辞書構造を調べることで、この静脈でより多くを見つけることができます。)したがって、具体的には、キーが最初の2つの単語である配列から始めます。エントリに可能な3番目の単語とその頻度がリストされている並列配列。ただし、それでも問題が発生する可能性があります。バッテリーにメモリ効率の高いオプションが含まれている限り、運が悪いかもしれません。

また、ここでは、メモリ効率のためにバイナリツリー構造は推奨されていません。たとえば、 このペーパー は、同様の問題(ただし、トリグラムではなくユニグラム)でさまざまなデータ構造をテストし、その尺度ですべてのツリー構造を打ち負かすハッシュテーブルを見つけます。

他の誰かがしたように、ソートされた配列は、バイグラムやトリグラムではなく、ワードリストにのみ使用できることを述べておかなければなりません。次に、「実際の」データ構造には、それが何であれ、文字列の代わりに整数キーを使用します。つまり、ワードリストへのインデックスです。 (ただし、これにより、ワードリスト自体を除いて、一般的なプレフィックスを悪用することができなくなります。結局、これを提案するべきではないかもしれません。)

30
Darius Bacon

タプルを使用します。
タプルは辞書のキーになる可能性があるため、辞書をネストする必要はありません。

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がある場合、スライスを使用して最初の2つのアイテムを取得できます。

>>> 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番目のアイテムとして表示されるすべてのアイテムのリストを取得しました。

9
hasen

この場合、 [〜#〜] zodb [〜#〜] ¹BTreeは、メモリへの負荷がはるかに少ないため、役立つ場合があります。 BTrees.OOBtree(オブジェクト値へのオブジェクトキー)またはBTrees.OIBTree(整数値へのオブジェクトキー)を使用し、キーとして3ワードタプルを使用します。

何かのようなもの:

from BTrees.OOBTree import OOBTree as BTree

インターフェイスは、多かれ少なかれdictに似ており、(あなたにとって)追加のボーナスが.keys.items.iterkeysおよび.iteritems2つあります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
tzot

いくつかの試み:

私はあなたがこれに似た何かをしていると思います:

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かかります。私の言葉は入力で繰り返されないので、私の場合は少し特別です。いくつかのバリエーションを自分で試して、これらのいずれかが役立つかどうかを確認することができます。

3
Dustin

マルコフテキスト生成を実装していますか?

チェーンが2つの単語を3番目の確率にマッピングする場合、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,)
2
orip

さて、あなたは基本的にまばらな3D空間を保存しようとしています。このスペースに必要なアクセスパターンの種類は、アルゴリズムとデータ構造を選択するために重要です。データソースを考慮して、これをグリッドにフィードしますか? O(1)アクセスが必要ない場合:

メモリ効率を上げるには、そのスペースを同じ数のエントリを持つサブスペースに分割する必要があります。 (BTreeのように)。したがって、次のデータ構造:

  • firstWordRange
  • secondWordRange
  • thirdWordRange
  • numberOfEntries
  • ソートされたエントリのブロック。
  • 3次元すべての次および前のブロック
1

同じ辞書を1レベルだけ使用してみることができます。

topDictionary[Word1+delimiter+Word2+delimiter+Word3]

区切り文字はプレーンな ""である可能性があります。 (または(Word1、Word2、Word3)を使用)

これは実装が最も簡単です。それが十分でなければ、少し改善が見られると思います......何かを考えます...

1
user39307

Scipyにはスパース行列があるため、最初の2つの単語をタプルにできる場合は、次のようにすることができます。

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
1
Rick Copeland

ずんぐりした多次元配列を使用できます。配列にインデックスを付けるには、文字列ではなく数字を使用する必要がありますが、単一の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配列はあなたが見つけそうなものとほぼ同じくらい効率的です。また、私はこのコードを試したことがないので、詳細の一部がずれている可能性があることにも注意してください。ここでメモリから移動します。

0
nase

メモリが単に十分に大きくない場合、 pybsddb はディスク永続マップの保存に役立ちます。

0
orip

これは、バイセクトライブラリを使用して単語の並べ替えられたリストを維持するツリー構造です。 [〜#〜] 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ステートメントが保存されます。空になるのは1回だけなので、ifステートメントはすべてのn-1つの単語に対して無駄になります。

0
S.Lott