32

プロファイリングは、これが私が書いた小さな単語ゲームのコードの最も遅いセグメントであることを示しています:

def distance(word1, word2):
    difference = 0
    for i in range(len(word1)):
        if word1[i] != word2[i]:
            difference += 1
    return difference

def getchildren(word, wordlist):
    return [ w for w in wordlist if distance(word, w) == 1 ]

ノート:

  • distance()は 500 万回以上呼び出されており、その大部分は getchildren からのもので、単語リスト内のword1 文字だけ異なるすべての単語を取得することになっています。
  • wordlist は、同じ数の文字を含む単語のみを持つように事前にフィルター処理されているため、同じ数の文字を持つwordことが保証されます。word1word2
  • 私は Python を初めて使用するので (3 日前に学習を開始しました)、命名規則やその他のスタイルに関するコメントも歓迎します。
  • wordlistの場合、「2+2lemma.txt」ファイルを使用して12dict の単語リストを取得します

結果:

みんなありがとう、さまざまな提案を組み合わせて、プログラムを2倍の速度で実行できるようになりました(質問する前に自分で行った最適化に加えて、最初の実装から約4倍の速度が向上しました)

AとBと呼ぶ2セットの入力でテストしました

Optimization1: word1,2 ...のインデックスを反復処理します。

for i in range(len(word1)):
        if word1[i] != word2[i]:
            difference += 1
    return difference

使用して文字のペアを反復するzip(word1, word2)

for x,y in zip (word1, word2):
        if x != y:
            difference += 1
    return difference

入力 A の実行時間は 11.92 から 9.18、入力 B の実行時間は 79.30 から 74.59 になりました

Optimization2: distance-method に加えて、different-by-one の別のメソッドを追加しました (これは、A* ヒューリスティックのために他の場所でまだ必要でした)。

def is_neighbors(word1,word2):
    different = False
    for c1,c2 in zip(word1,word2):
        if c1 != c2:
            if different:
                return False
            different = True
    return different

入力 A の実行時間は 9.18 から 8.83、入力 B の実行時間は 74.59 から 70.14 になりました

最適化 3: ここでの大きな勝者は、izip代わりに使用することでしたzip

入力 A の実行時間は 8.83 から 5.02 になり、入力 B の実行時間は 70.14 から 41.69 になりました

低レベル言語で書いたほうがいいかもしれませんが、今のところこれで満足しています。みんな、ありがとう!

もう一度編集: より多くの結果最初の文字が一致しないケースをチェックするマークの方法を使用して、5.02 -> 3.59 および 41.69 -> 29.82 からダウンしました。

それに基づいての代わりに組み込むiziprangeと、次のようになりました。

def is_neighbors(word1,word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    different = False
    for x,y in izip(word1[1:],word2[1:]):
        if x != y:
            if different:
                return False
            different = True
    return different

これにより、タイムが 3.59 -> 3.38 および 29.82 -> 27.88 に短縮されました。

さらに成果が!

「単語」から1文字離れたすべての文字列のリストを生成し、 is_neighbor 関数の代わりに wordlist にあるものを確認するというSumuduの提案を試してみると、次のようになりました。

def one_letter_off_strings(word):
    import string
    dif_list = []
    for i in xrange(len(word)):
        dif_list.extend((word[:i] + l + word[i+1:] for l in string.ascii_lowercase if l != word[i]))
    return dif_list

def getchildren(word, wordlist):
    oneoff = one_letter_off_strings(word)
    return ( w for w in oneoff if w in wordlist )

最終的には遅くなりましたが (3.38 -> 3.74 および 27.88 -> 34.40)、有望に思えました。最初は、最適化する必要がある部分は「one_letter_off_strings」だと思っていましたが、プロファイリングではそうではなく、遅い部分は実際には

( w for w in oneoff if w in wordlist )

「oneoff」と「wordlist」を切り替えて、逆に比較すると、2 つのリストの共通点を探していることに気が付いたときに、何か違いがあるのではないかと考えました。それを文字の set-intersectionに置き換えます:

return set(oneoff) & set(wordlist)

バム!3.74 -> 0.23 および 34.40 -> 2.25

これは本当に驚くべきことであり、元の素朴な実装との合計速度の差: 23.79 -> 0.23 および 180.07 -> 2.25 であり、元の実装よりも約 80 倍から 100 倍高速です。

誰かが興味を持っている場合は、プログラム説明し、ここで言及されていないものを含めて行われた最適化について説明するブログ投稿を作成しました (コードの別のセクションにあるため)。

大論争:

わかりました、私と Unknown は大きな議論をしています。彼の回答のコメントで読むことができます。彼は、C に移植した場合、元の方法 (セットを使用する代わりに is_neighbor を使用) を使用した方が高速になると主張しています。thisthisの例に従ってください。Windows ではプロセスが少し異なるように見えますか? わかりませんが、私はそれをあきらめました。とにかく、ここにプログラムの完全なコードがあり、テキストファイルは12dict 単語リストから来ています「2+2lemma.txt」ファイルを使用します。コードが少し乱雑で申し訳ありませんが、これは私が一緒にハッキングしたものです。また、単語リストからコンマを削除するのを忘れていたので、実際には同じ比較のためにそのままにしておくか、cleanentries の文字のリストにコンマを追加して修正できるバグです。

from itertools import izip
def unique(seq):  
    seen = {} 
    result = [] 
    for item in seq: 
        if item in seen:
            continue 
        seen[item] = 1 
        result.append(item) 
    return result
def cleanentries(li):
    pass
    return unique( [w.strip('[]') for w in li if w != "->"] )
def distance(word1, word2):
    difference = 0
    for x,y in izip (word1, word2):
        if x != y:
            difference += 1
    return difference
def is_neighbors(word1,word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    different = False
    for x,y in izip(word1[1:],word2[1:]):
        if x != y:
            if different:
                return False
            different = True
    return different
def one_letter_off_strings(word):
    import string
    dif_list = []
    for i in xrange(len(word)):
        dif_list.extend((word[:i] + l + word[i+1:] for l in string.ascii_lowercase if l != word[i]))
    return dif_list

def getchildren(word, wordlist):
    oneoff = one_letter_off_strings(word)
    return set(oneoff) & set(wordlist)
def AStar(start, goal, wordlist):
    import Queue
    closedset = []
    openset = [start]
    pqueue = Queue.PriorityQueue(0)
    g_score = {start:0}         #Distance from start along optimal path.
    h_score = {start:distance(start, goal)}
    f_score = {start:h_score[start]}
    pqueue.put((f_score[start], start))
    parent_dict = {}
    while len(openset) > 0:
        x = pqueue.get(False)[1]
        if x == goal:
            return reconstruct_path(parent_dict,goal)
        openset.remove(x)
        closedset.append(x)
        sortedOpen = [(f_score[w], w, g_score[w], h_score[w]) for w in openset]
        sortedOpen.sort()
        for y in getchildren(x, wordlist):
            if y in closedset:
                continue
            temp_g_score = g_score[x] + 1
            temp_is_better = False
            appended = False
            if (not y in openset): 
                openset.append(y)
                appended = True
                h_score[y] = distance(y, goal)
                temp_is_better = True
            elif temp_g_score < g_score[y] :
                temp_is_better = True
            else :
                pass
            if temp_is_better:
                parent_dict[y] = x
                g_score[y] = temp_g_score
                f_score[y] = g_score[y] + h_score[y]
                if appended :
                    pqueue.put((f_score[y], y))
    return None


def reconstruct_path(parent_dict,node):
     if node in parent_dict.keys():
         p = reconstruct_path(parent_dict,parent_dict[node])
         p.append(node)
         return p
     else:
         return []        

wordfile = open("2+2lemma.txt")
wordlist = cleanentries(wordfile.read().split())
wordfile.close()
words = []
while True:
    userentry = raw_input("Hello, enter the 2 words to play with separated by a space:\n ")
    words = [w.lower() for w in userentry.split()]
    if(len(words) == 2 and len(words[0]) == len(words[1])):
        break
print "You selected %s and %s as your words" % (words[0], words[1])
wordlist = [ w for w in wordlist if len(words[0]) == len(w)]
answer = AStar(words[0], words[1], wordlist)
if answer != None:
    print "Minimum number of steps is %s" % (len(answer))
    reply = raw_input("Would you like the answer(y/n)? ")
    if(reply.lower() == "y"):
        answer.insert(0, words[0])
        print "\n".join(answer)
    else:
        print "Good luck!"
else:
    print "Sorry, there's no answer to yours"
reply = raw_input("Press enter to exit")

is_neighbors メソッドは使用されていませんが、残しました。これは、C への移植が提案されているメソッドです。これを使用するには、getchildren を次のように置き換えます。

def getchildren(word, wordlist):
    return ( w for w in wordlist if is_neighbors(word, w))

Cモジュールとして動作させることに関しては、私はそこまで行きませんでしたが、これは私が思いついたものです:

#include "Python.h"

static PyObject *
py_is_neighbor(PyObject *self, Pyobject *args)
{
    int length;
    const char *word1, *word2;
    if (!PyArg_ParseTuple(args, "ss", &word1, &word2, &length))
        return NULL;

    int i;
    int different = 0;
    for (i =0; i < length; i++)
    {
        if (*(word1 + i) != *(word2 + i))
        {
            if (different)
            {
                return Py_BuildValue("i", different);
            }
            different = 1;
        }
    }
    return Py_BuildValue("i", different);
}

PyMethodDef methods[] = {
    {"isneighbor", py_is_neighbor, METH_VARARGS, "Returns whether words are neighbors"},
    {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
initIsNeighbor(void)
{
    Py_InitModule("isneighbor", methods);
}

私はこれを使用してプロファイリングしました:

python -m cProfile "Wordgame.py"

記録された時間は、AStar メソッド呼び出しの合計時間です。高速入力セットは「詩の詩人」であり、長い入力セットは「詩人の詩」でした。タイミングは明らかに異なるマシン間で異なるため、誰かがこれを試した場合は、プログラムそのままの結果と C モジュールの結果を比較してください。

4

12 に答える 12

24

単語リストが非常に長い場合、「単語」からすべての可能な 1 文字の違いを生成し、リストにあるものを確認する方が効率的でしょうか? 私はPythonを知りませんが、ログ時間の検索を可能にするワードリストに適したデータ構造が必要です。

これをお勧めするのは、単語が妥当な長さ (~10 文字) の場合、250 の潜在的な単語のみを探すことになるためです。これは、単語リストが数百単語よりも大きい場合におそらく高速になります。

于 2009-04-25T07:38:18.173 に答える
10

distance実際に距離= 1のみを気にする場合、関数は合計距離を計算しています。ほとんどの場合、数文字以内で 1 より大きいことがわかるため、早めに戻って多くの時間を節約できます。

それを超えて、より良いアルゴリズムがあるかもしれませんが、私には思いつきません。

編集:別のアイデア。

最初の文字が一致するかどうかによって、2 つのケースを作成できます。一致しない場合は、単語の残りの部分が正確に一致する必要があり、それを 1 回でテストできます。それ以外の場合は、以前と同じように実行してください。再帰的に行うこともできますが、それが高速になるとは思いません。

def DifferentByOne(word1, word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    same = True
    for i in range(1, len(word1)):
        if word1[i] != word2[i]:
            if same:
                same = False
            else:
                return False
    return not same

編集 2:文字列が同じ長さであるかどうかを確認するためのチェックを削除しました。冗長であるとのことです。自分のコードと MizardXが提供するis_neighbors 関数でRyan のテストを実行すると、次の結果が得られます。

  • 元の距離(): 3.7 秒
  • 私の DifferentByOne(): 1.1 秒
  • MizardX の is_neighbors(): 3.7 秒

編集 3: (おそらくここでコミュニティ wiki の領域に入りますが...)

zip の代わりに izip を使用して is_neighbors() の最終的な定義を試す: 2.9 秒。

これが私の最新バージョンで、まだ 1.1 秒です。

def DifferentByOne(word1, word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    different = False
    for i in range(1, len(word1)):
        if word1[i] != word2[i]:
            if different:
                return False
            different = True
    return different
于 2009-04-25T02:30:29.147 に答える
6
from itertools import izip

def is_neighbors(word1,word2):
    different = False
    for c1,c2 in izip(word1,word2):
        if c1 != c2:
            if different:
                return False
            different = True
    return different

izipまたは、コードをインライン化することもできます:

def is_neighbors(word1,word2):
    different = False
    next1 = iter(word1).next
    next2 = iter(word2).next
    try:
        while 1:
            if next1() != next2():
                if different:
                    return False
                different = True
    except StopIteration:
        pass
    return different

そして書き直されたgetchildren

def iterchildren(word, wordlist):
    return ( w for w in wordlist if is_neighbors(word, w) )
  • izip(a,b)aとからの値のペアに対するイテレータを返しますb
  • zip(a,b)aとからペアのリストを返しますb
于 2009-04-25T02:41:49.370 に答える
4

人々は主に、より速い関数を書こうとすることでこれに取り組んでいますが、別の方法があるかもしれません..

「距離」は500万回以上呼び出されます

どうしてこれなの?おそらく、最適化するためのより良い方法は、ミリ秒単位の実行時間distanceを削るよりも、への呼び出しの数を減らしてみることです。distance'sスクリプト全体を見ないとわかりませんが、通常、特定の関数を最適化する必要はありません。

それが無理ならCモジュールとして書いていただけないでしょうか?

于 2009-04-25T03:25:37.377 に答える
3

同じ引数で距離関数が呼び出される頻度はどれくらいですか?最適化を実装する簡単な方法は、メモ化を使用することです。

おそらく、1つ異なる文字のフリーズセットと単語のリストを使用して、ある種の辞書を作成し、その中の値を検索することもできます。このデータ構造は、pickleを介して保存およびロードするか、起動時に最初から生成することができます。

使用しているハミング距離アルゴリズムは基本的にO(n)であり、nは単語の長さであるため、評価を短絡すると、使用している単語が非常に長い場合にのみゲインが得られます。

実例となる可能性のあるいくつかの代替アプローチについて、timeitを使用していくつかの実験を行いました。

Timeitの結果

あなたのソリューション

d = """\
def distance(word1, word2):
    difference = 0
    for i in range(len(word1)):
        if word1[i] != word2[i]:
            difference += 1
    return difference
"""
t1 = timeit.Timer('distance("hello", "belko")', d)
print t1.timeit() # prints 6.502113536776391

一発ギャグ

d = """\
from itertools import izip
def hamdist(s1, s2):
    return sum(ch1 != ch2 for ch1, ch2 in izip(s1,s2))
"""
t2 = timeit.Timer('hamdist("hello", "belko")', d)
print t2.timeit() # prints 10.985101179

ショートカット評価

d = """\
def distance_is_one(word1, word2):
    diff = 0
    for i in xrange(len(word1)):
        if word1[i] != word2[i]:
            diff += 1
        if diff > 1:
            return False
    return diff == 1
"""
t3 = timeit.Timer('hamdist("hello", "belko")', d)
print t2.timeit() # prints 6.63337
于 2009-04-25T03:14:32.177 に答える
2

違いが 2 以上の場合は、ループを中断することから始めることができます。

また、あなたは変更することができます

for i in range(len(word1)):

for i in xrange(len(word1)):

xrange は、数値の範囲全体を一度に生成するのではなく、オンデマンドでシーケンスを生成するためです。

単語の長さを比較してみることもできます。また、word1 が word2 より大きい場合、コードは機能しないことに注意してください。

その後、アルゴリズム的にできることは他にあまりありません。つまり、そのセクションを C に移植することで、おそらくさらに高速化されるでしょう。

編集 2

文字ごとの違いを検証することと比較して、Sumudu のアルゴリズムの私の分析を説明しようとしています。

長さ L の単語がある場合、生成される「1 つずつ異なる」単語の数は 25L になります。最新のコンピューターでのセットの実装から、検索速度はおよそlog(n) base 2であることがわかっています。ここで、n は検索する要素の数です。

テスト対象の 500 万語のほとんどがセットに含まれていないことがわかります。ほとんどの場合、セット全体をトラバースします。つまり、log(25L)/2 だけではなく、実際にはlog(25L)になります。(これは、文字列ごとの比較が文字ごとの比較と同等であるセットの最良のシナリオを想定しています)

ここで、"differs-by-one" を決定するための時間計算量を見ていきます。単語全体をチェックする必要があると仮定すると、単語あたりの操作数はLになります。私たちは、ほとんどの単語がすぐに 2 ずつ異なることを知っています。そして、ほとんどの接頭辞が単語のごく一部を占めることを知っているので、論理的には、ほとんどの場合L/2、つまり単語の半分で区切られると想定できます (これは控えめな見積もりです)。

ここで、L/2 と log(25L) の 2 つの検索の時間の複雑さをプロットします。これは、文字列の一致と同じ速度で文字列の一致を考慮していることに留意してください (セットに非常に有利です)。方程式 log(25*L) > L/2 があり、log(25) > L/2 - log(L) に簡略化できます。グラフからわかるように、非常に大きな数の Lに到達するまでは、char マッチング アルゴリズムを使用する方が高速です。

代替テキスト

また、最適化で2以上の差を破ることを数えているかどうかはわかりませんが、マークの回答から、私はすでに2以上の差を破っています。実際、最初の文字の差があれば、それはこれらすべての最適化にもかかわらず、セットを使用するように変更しただけで、水から吹き飛ばされました。私はあなたのアイデアを試すことに興味があります

私は、この質問で 2 以上の差を破ることを提案した最初の人物です。問題は、マークの文字列スライシングのアイデア (if word1[0] != word2[0]: return word1[1:] == word2[1:]) は、私たちが行っていることを単に C に入れているということです。 word1 [1:] == word2[1:]が計算されると思いますか? 私たちがやっているのと同じ方法です。

あなたの説明を何度か読みましたが、よく理解できませんでした。もう少し詳しく説明していただけませんか。また、私は C にあまり詳しくなく、ここ数年高級言語で働いています (最も近いのは 6 年前に高校で C++ を学んでいました)。

C コードの作成に関しては、少し忙しいです。以前に C で書いたことがあるので、きっとできると思います。おそらく同様のパフォーマンス特性を持つ C# を試すこともできます。

詳細説明

Davy8のより詳細な説明はこちら

def getchildren(word, wordlist):
    oneoff = one_letter_off_strings(word)
    return set(oneoff) & set(wordlist)

one_letter_off_strings 関数は、25L 文字列のセットを作成します (L は文字数です)。

ワードリストからセットを作成すると、D 文字列のセットが作成されます (D は辞書の長さです)。これから交差を作成することにより、各 oneoff を繰り返し処理し、wordlist存在するかどうかを確認する必要があります

この操作の時間計算量については、上記で詳しく説明しています。この操作は、必要な単語をwordlist内の各単語と比較するよりも効率的ではありません。Sumudu の方法は、アルゴリズムではなく C での最適化です。

詳細説明 2

合計 4500 単語しかなく (アルゴリズムに渡される前に、単語リストが 5 文字の単語に対して事前にフィルター処理されているため)、125 の 1 文字の単語と交差しています。交差はlog(小さい)、つまりlog(125, 2)と言っていたようです。これを、あなたが言ったことをもう一度仮定して比較してください。単語を比較すると L/2 文字に分割されます。5 文字の単語の場合は 3 になる可能性が高くなりますが、これを切り捨てて 2 にします。この比較は 4500 回行われます。 log(125,2) は約 6.9 で、log(4500,2) は約 12 です。

125 個の一文字違いの単語と 4500 個の辞書の共通部分を作成するには、125 * 4500 回の比較を行う必要があります。これは log(125,2) ではありません。辞書が事前にソートされていると仮定すると、せいぜい 125 * log(4500, 2) です。セットへの魔法の近道はありません。ここでは、文字ごとの比較ではなく、文字列ごとの比較も行っています。

于 2009-04-25T02:27:44.787 に答える
1

パフォーマンスに大きな影響を与えるこのような単純な関数の場合、おそらく C ライブラリを作成し、ctypesを使用して呼び出します。reddit の創設者の 1 人は、この手法を使用して Web サイトの速度が 2 倍になったと主張しています。

この関数でpsycoを使用することもできますが、多くのメモリを消費する可能性があることに注意してください。

于 2009-04-25T02:44:00.177 に答える
0

最初に思いついたのは:

from operator import ne

def distance(word1, word2):
    return sum(map(ne, word1, word2))

これは、解釈されたループがなく、Python プリミティブを呼び出すだけなので、人々が投稿した他の関数よりも高速になる可能性が十分にあります。そして、呼び出し元に合理的にインライン化できるほど十分に短いです。

あなたのより高いレベルの問題については、距離空間での類似性検索用に開発されたデータ構造を調べます。たとえば、この論文この本など、どちらも読んだことはありません (読んだ論文を検索して出てきたものです)。しかし思い出せない)。

于 2009-04-25T04:55:54.303 に答える
0

このスニペットの場合:

for x,y in zip (word1, word2):
    if x != y:
        difference += 1
return difference

私はこれを使用します:

return sum(1 for i in xrange(len(word1)) if word1[i] == word2[i])

提供されたコード全体で同じパターンが続きます...

于 2009-08-19T17:41:47.787 に答える
0

他の誰もが、距離 1 候補の構築については何もせずに、明示的な距離計算だけに集中していました。Trieと呼ばれるよく知られたデータ構造を使用して、暗黙的な距離計算を距離 1 のすべての隣接語を生成するタスクとマージすることで改善できます。Trie は、各ノードが文字を表すリンク リストであり、「next」フィールドは最大 26 エントリの辞書であり、次のノードを指します。

擬似コードは次のとおりです。指定された単語に対してトライを繰り返します。各ノードで、すべての距離 0 および距離 1 の隣接ノードを結果に追加します。距離のカウンターを保持し、それを減らします。再帰は必要ありません。追加の distance_so_far 整数引数を取るルックアップ関数だけです。

長さ 3、長さ 4、長さ 5 などの単語に対して個別のトライを構築することにより、O(N) スペースの増加に対する特別な速度の小さなトレードオフを得ることができます。

于 2016-10-14T20:54:28.553 に答える
0

それが速度に大きな影響を与えるかどうかはわかりませんが、リスト内包表記をジェネレーター式に変えることから始めることができます。それはまだ反復可能であるため、使用法に大きな違いはないはずです:

def getchildren(word, wordlist):
    return [ w for w in wordlist if distance(word, w) == 1 ]

def getchildren(word, wordlist):
    return ( w for w in wordlist if distance(word, w) == 1 )

主な問題は、リスト内包表記がメモリ内に構築され、かなりのスペースを占有することです。一方、ジェネレーターはその場でリストを作成するため、すべてを保存する必要はありません。

また、未知の答えに続いて、これは距離()を書くより「pythonic」な方法かもしれません:

def distance(word1, word2):
    difference = 0
    for x,y in zip (word1, word2):
        if x == y:
            difference += 1
    return difference

しかし、len (単語 1) != len (単語 2) の場合は何が意図されているのか混乱します。zip の場合、最も短い単語と同じ数の文字しか返されません。(これは最適化になる可能性があります...)

于 2009-04-25T02:29:48.227 に答える
0

これを試して:

def distance(word1, word2):
  return sum([not c1 == c2 for c1, c2 in zip(word1,word2)])

また、あなたのゲームへのリンクはありますか?言葉遊びで潰されるのが好き

于 2009-04-25T02:48:35.030 に答える