ドライ理論
そして、私たちの仕事はテキストを生成することです。 これは、単語を特定の順序で並べる必要があることを意味します。 この順序を決定する方法は? 私たちは次のように行くことができます:ロシア語の可能性が高いフレーズを構築する。 しかし、言語フレーズの確率はどういう意味ですか? 常識の観点から、これはナンセンスです。 それでも、この確率は、特定のコーパス(テキストのセット)内の一連の単語の確率として正式に定義できます。 たとえば、 「幸せは後悔のない喜び」というフレーズの確率は、このフレーズの各単語の確率の積として計算できます。
P = P(幸福)P(ある|幸福)P(喜び|幸福がある)P(なし|幸福は喜び)P(後悔|幸福はない喜び)
確率P(幸福度)を計算するのは簡単です。テキスト内でこの単語が出現した回数を数え、この値を単語の総数で割るだけです。 しかし、確率P(後悔|幸福はなしの喜び)の計算は、それほど単純ではありません。 幸いなことに、このタスクを簡素化できます。 テキスト内の単語の確率は前の単語にのみ依存すると仮定します。 次に、フレーズを計算するための式は次の形式になります。
P = P(幸福)P(あり|幸福)P(喜び|あり)P(なし|喜び)P(後悔|なし)
もう簡単です。 条件付き確率Pの計算は簡単です(|幸福があります)。 これを行うために、「幸福は」というペアの数を考慮し、「幸福」という単語のテキストの数で除算します。
P(|幸福がある)= C(幸福がある)/ C(幸福)
その結果、テキスト内のすべての単語のペアを数えると、任意のフレーズの確率を計算できます。 また、フレーズの確率を計算できる場合は、自動生成のためにこのテキストで最も「妥当な」単語の組み合わせを選択できます。 ところで、これらの単語のペアはバイグラムと呼ばれ、計算された確率のセットはバイグラムモデルと呼ばれます。
私たちのアルゴリズムでは、バイグラムではなく、トライグラムを使用しています。 違いは、単語の条件付き確率が1つではなく、前の2つの単語によって決定されることです。 つまり、P(喜び|幸福)の代わりに、P(喜び|幸福は)を計算します。 計算式はバイグラムの式に似ています:
P(喜び|幸福は)= C(幸福は喜び)/ C(幸福は)
したがって、テキストは次のように生成できます。 次の手順に従って、各提案を個別に策定します。
*文を開始する可能性が最も高い単語を選択します。
*前の2つの単語に応じて、最も可能性の高い継続単語を選択します。
*文末記号に達するまで前の手順を繰り返します。
練習する
まず、モデルを訓練する軍団を準備する必要があります。 たとえば、サイトlib.ruのLeo Tolstoyからサンプルを取り、1つのテキストファイルを作成しました( ここからダウンロードできます)。
このテキストから、必要な単語のシーケンスを選択します。
import re
r_alphabet = re . compile( u'[--0-9-]+|[.,:;?!]+' )
def gen_lines (corpus):
data = open (corpus)
for line in data:
yield line . decode( 'utf-8' ) . lower()
def gen_tokens (lines):
for line in lines:
for token in r_alphabet . findall(line):
yield token
lines = gen_lines( 'tolstoy.txt' )
tokens = gen_tokens(lines)
結果として生成されるトークンジェネレーターは、単語と句読点の「クリーンな」シーケンスを生成します。 ただし、単純なシーケンスには興味がありません。 トークンのトリプルに興味があります(ここでのトークンは、単語または句読点、つまりテキストのアトミックな要素を意味します)。 これを行うには、別のジェネレーターを追加します。その出力には、3つの連続したトークンがあります。
import re
r_alphabet = re . compile( u'[--0-9-]+|[.,:;?!]+' )
def gen_lines (corpus):
data = open (corpus)
for line in data:
yield line . decode( 'utf-8' ) . lower()
def gen_tokens (lines):
for line in lines:
for token in r_alphabet . findall(line):
yield token
def gen_trigrams (tokens):
t0, t1 = '$' , '$'
for t2 in tokens:
yield t0, t1, t2
if t2 in '.!?' :
yield t1, t2, '$'
yield t2, '$' , '$'
t0, t1 = '$' , '$'
else :
t0, t1 = t1, t2
lines = gen_lines( 'tolstoy.txt' )
tokens = gen_tokens(lines)
trigrams = gen_trigrams(tokens)
gen_trigramsメソッドには明確化が必要です。 文字「$」は、文の始まりを示すために使用されます。 これにより、生成されたフレーズの最初の単語を選択しやすくなります。 一般に、このメソッドは次のように動作します。3つの連続したトークンを返し、各反復で1トークンずつシフトします。
入り口で:
「幸福は後悔のない喜びです」
出力:
反復トークン
0:$ $幸せ
1:幸福は
2:幸せは喜びです
3:なしで楽しんでください
...
次に、トライグラムモデルを計算します。
import re
from collections import defaultdict
r_alphabet = re . compile( u'[--0-9-]+|[.,:;?!]+' )
...
def train (corpus):
lines = gen_lines(corpus)
tokens = gen_tokens(lines)
trigrams = gen_trigrams(tokens)
bi, tri = defaultdict( lambda : 0.0 ), defaultdict( lambda : 0.0 )
for t0, t1, t2 in trigrams:
bi[t0, t1] += 1
tri[t0, t1, t2] += 1
model = {}
for (t0, t1, t2), freq in tri . iteritems():
if (t0, t1) in model:
model[t0, t1] . append((t2, freq / bi[t0, t1]))
else :
model[t0, t1] = [(t2, freq / bi[t0, t1])]
return model
model = train( 'tolstoy.txt' )
このメソッドの最初の部分では、ジェネレーターを定義します。 次に、バイグラムとトライグラムを計算します(実際、テキスト内の同一のペアとトリプルの単語の数をカウントします)。 次に、前の2つに応じて単語の確率を計算し、この単語とその確率を辞書に入れます。 メモリを大量に消費するため、これは最適な方法ではないと言わなければなりません。 しかし、小さな場合にはこれで十分です。
これで、すべてがテキスト生成の準備ができました。 次の関数は文を返します。
...
model = train( 'tolstoy.txt' )
def generate_sentence (model):
phrase = ''
t0, t1 = '$' , '$'
while 1 :
t0, t1 = t1, unirand(model[t0, t1])
if t1 == '$' : break
if t1 in ( '.!?,;:' ) or t0 == '$' :
phrase += t1
else :
phrase += ' ' + t1
return phrase . capitalize()
この方法の本質は、次のフレーズの開始記号($記号)に達するまで、最も可能性の高い単語または句読点を順番に選択することです。 最初の単語は、セットモデル['$'、 '$']から文を開始する可能性が最も高いものとして選択されます。
ここで重要な点に注意する必要があります。 単語の各ペアのモデル辞書には、ペアのリスト(単語、確率)が含まれています。 このセットから単語を1つだけ選択する必要があります。 「額」オプションは、最も高い確率で単語を選択することです。 しかし、生成されたすべてのフレーズは互いに類似しています。 より適切な方法は、単語の確率に依存する特定のランダム性を持つ単語を選択することです(フレーズがまれな組み合わせで構成されることは望ましくありません)。 これはunirandメソッドが行うことで、前の2つに応じて特定の単語の確率と等しい確率でランダムな単語を返します。
合計で、ジェネレーターの完全なコードは次のとおりです。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import re
from random import uniform
from collections import defaultdict
r_alphabet = re . compile( u'[--0-9-]+|[.,:;?!]+' )
def gen_lines (corpus):
data = open (corpus)
for line in data:
yield line . decode( 'utf-8' ) . lower()
def gen_tokens (lines):
for line in lines:
for token in r_alphabet . findall(line):
yield token
def gen_trigrams (tokens):
t0, t1 = '$' , '$'
for t2 in tokens:
yield t0, t1, t2
if t2 in '.!?' :
yield t1, t2, '$'
yield t2, '$' , '$'
t0, t1 = '$' , '$'
else :
t0, t1 = t1, t2
def train (corpus):
lines = gen_lines(corpus)
tokens = gen_tokens(lines)
trigrams = gen_trigrams(tokens)
bi, tri = defaultdict( lambda : 0.0 ), defaultdict( lambda : 0.0 )
for t0, t1, t2 in trigrams:
bi[t0, t1] += 1
tri[t0, t1, t2] += 1
model = {}
for (t0, t1, t2), freq in tri . iteritems():
if (t0, t1) in model:
model[t0, t1] . append((t2, freq / bi[t0, t1]))
else :
model[t0, t1] = [(t2, freq / bi[t0, t1])]
return model
def generate_sentence (model):
phrase = ''
t0, t1 = '$' , '$'
while 1 :
t0, t1 = t1, unirand(model[t0, t1])
if t1 == '$' : break
if t1 in ( '.!?,;:' ) or t0 == '$' :
phrase += t1
else :
phrase += ' ' + t1
return phrase . capitalize()
def unirand (seq):
sum_, freq_ = 0 , 0
for item, freq in seq:
sum_ += freq
rnd = uniform( 0 , sum_)
for token, freq in seq:
freq_ += freq
if rnd < freq_:
return token
if __name__ == '__main__' :
model = train( 'tolstoy.txt' )
for i in range ( 10 ):
print generate_sentence(model),
終わりました。 あなたの忍耐がうらやましい:)。
なぜトライグラム
トライグラムモデルは、単純さと明確さのために選択されています。 4グラムではかなり多くのリソースが必要になりますが、Bigramsの結果は良くありません。 いずれにせよ、このアルゴリズムを拡張してN-gramの一般的なケースを処理するのは非常に簡単です。 ただし、Nが大きいほど、テキストが元のケースに似ていることを考慮する価値があります。