Python for NLPの連載は今回で15回目です。
前回は、TF-IDF法をPythonでゼロから実装する方法を説明しました。
その前に、Bag of words approachをPythonでスクラッチから実装する方法を勉強しました。
今日は、N-Gramsアプローチについて勉強し、N-Gramsアプローチを使って簡単な自動テキストフィラーやサジェストエンジンを作成する方法を紹介します。
自動テキストフィラーは非常に便利なアプリケーションで、Googleや様々なスマートフォンで広く使われています。
ユーザーがテキストを入力すると、残りのテキストが自動的に入力されたり、アプリケーションによって提案されたりします。
TF-IDFとBag of Wordsアプローチの問題点
実際にN-Gramsモデルを実装する前に、まず、Bag of WordsとTF-IDFアプローチの欠点について説明します。
Bag of WordsやTF-IDFアプローチでは、単語は個別に扱われ、一つ一つの単語はその数値に変換される。
このとき、単語の文脈情報は保持されない。
big red machine and carpet」と「big red carpet and machine」という2つの文章を考えてみよう。
Bag of wordsのアプローチを用いると、この2つの文は同じベクトルを得ることになる。
しかし、最初の文では「大きな赤い機械」について話しているのに対し、2番目の文では「大きな赤いカーペット」についての情報が含まれていることがはっきりとわかる。
したがって、文脈の情報は非常に重要である。
N-Gramsモデルは、基本的に文脈情報を捉えるのに役立つ。
Nグラムモデルの理論
Wikipediaでは、N-Gramを「与えられたテキストや音声のサンプルからのN個の連続した項目の列」と定義している。
ここで、項目は文字、単語、文のいずれかであり、Nは任意の整数である。
Nが2のとき、その列をbigramと呼ぶ。
同様に、3つの項目の列はトリグラムと呼ばれ、以下同様である。
N-グラムモデルを理解するためには、まずマルコフ連鎖の仕組みを理解する必要がある。
N-グラムとマルコフ連鎖の接続
マルコフ連鎖とは、状態の連続のことである。
マルコフ連鎖では、ある状態に留まるか、もう一方の状態に移るかのどちらかである。
この例では、状態は次のような振る舞いをする。
-
- XからYに移動する確率は50%、同様にXに留まる確率も50%である。
- 同様に、Yに留まる確率は50%で、Xに戻る可能性も50%である。
このようにして、XXYXのようなマルコフ系列を生成することができる。
N-Gramsモデルでは、シーケンス内の項目をマルコフ状態として扱うことができる。
各文字をマルコフ状態とする文字ビッグラムの簡単な例を見てみよう。
Football is a very famous game
上の文の文字ビグラムは次のようになる。
fo,
oo,
ot,
tb,
ba,
al,
ll,
l,
i,
is` といった具合です。
ビッグラムは基本的に連続して現れる2つの文字の並びであることがわかります。
同様に、trigramsは以下のように3つの連続した文字の並びである。
foo,
oot,
otb,
tba` などのようになります。
前の2つの例では、文字のビッググラムとトリグラムを見ました。
しかし、単語のビッググラムとトリグラムもあります。
前の例、”big red machine and carpet “に戻りましょう。
この文のビッグラムは、”big red”, “red machine”, “machine and”, “and carpet “となる。
同様に、”big red carpet and machine “という文のビグラムは、”big red”, “red carpet”, “carpet and”, “and machine “となります。
ここで、bigramsを用いた場合、両方の文に対して異なるベクトル表現が得られる。
以下では、N-GramsモデルをPythonで一から実装し、このようなN-Gramsを使った自動テキストフィラーを作る方法を見ていきます。
Pythonでゼロから作るN-Grams
ここでは、文字N-Gramsモデルと単語N-Gramsモデルの2種類のN-Gramsモデルを作成します。
キャラクターNグラムモデル
この節では、簡単な文字N-Gramモデルの作成方法を説明する。
次の章では、単語N-Gramモデルの実装方法を説明する。
コーパスを作成するために、テニスに関するWikipediaの記事をスクレイピングします。
まず、Wikipediaの記事をダウンロードし、解析するために必要なライブラリをインポートしましょう。
import nltk
import numpy as np
import random
import string
import bs4 as bs
import urllib.request
import re
Wikipediaのデータを解析するために、Beautifulsoup4ライブラリを使用します。
さらに、Pythonの正規表現ライブラリである re
を使って、テキストの前処理をいくつか行う予定です。
先に述べたように、コーパスを作成するためにTennisのWikipediaの記事を使います。
以下のスクリプトはWikipediaの記事を取得し、記事テキストからすべての段落を抽出する。
最後に、処理を容易にするため、テキストを小文字に変換する。
raw_html = urllib.request.urlopen('https://en.wikipedia.org/wiki/Tennis')
raw_html = raw_html.read()
article_html = bs.BeautifulSoup(raw_html, 'lxml')
article_paragraphs = article_html.find_all('p')
article_text = ''
for para in article_paragraphs:
article_text += para.text
article_text = article_text.lower()
次に、データセットから文字、ピリオド、空白を除くすべてを削除する。
article_text = re.sub(r'[^A-Za-z. ]', '', article_text)
データセットの前処理が終わったので、次はN-Gramsモデルを作成する。
今回は文字のトリグラムモデルを作成します。
以下のスクリプトを実行します。
ngrams = {}
chars = 3
for i in range(len(article_text)-chars):
seq = article_text[i:i+chars]
print(seq)
if seq not in ngrams.keys():
ngrams[seq] = []
ngrams[seq].append(article_text[i+chars])
上のスクリプトでは、辞書 ngrams
を作成する。
この辞書のキーはコーパスに含まれるcharacter trigramsで、値はtrigramsの隣に出現する文字となる。
次に、3文字のN-Gramを作成するため、変数 chars
を宣言する。
次に、コーパスに含まれる全ての文字を、4文字目から順に繰り返し処理する。
次に、ループの中で、次の3文字をフィルタリングしてトリグラムを抽出する。
このトリグラムは変数 seq
に格納される。
次に、そのトリグラムが辞書に存在するかどうかをチェックする。
もし ngrams
辞書に存在しない場合は、そのトリグラムを辞書に追加します。
その後、trigramの値として空リストを代入します。
最後に、trigramの後に存在する文字がリストの値として追加されます。
Spyderの変数エクスプローラで ngrams
という辞書を開くと、次のように表示されます。
このようなものが表示されるはずです。
このように、トリグラムをキー、トリグラムの後に現れる文字を値として見ることができます。
辞書には2文字のキーが見えるかもしれませんが、実は2文字ではありません。
3文字目は実はスペースなのです。
それでは、コーパスの最初の3文字を入力として、テキストを生成してみよう。
コーパスの最初の3文字は “ten “である。
次のスクリプトを見てほしい。
curr_sequence = article_text[0:chars]
output = curr_sequence
for i in range(200):
if curr_sequence not in ngrams.keys():
break
possible_chars = ngrams[curr_sequence]
next_char = possible_chars[random.randrange(len(possible_chars))]
output += next_char
curr_sequence = output[len(output)-chars:len(output)]
print(output)
上のスクリプトでは、まず最初のトリグラム、つまり「ten」を curr_sequence
変数に格納する。
200文字のテキストを生成するので、200回反復するループを初期化する。
各反復の間に、curr_sequence
またはトリグラムが ngrams
辞書内にあるかどうかをチェックする。
もし、trigram が ngrams
辞書中に見つからなければ、ループを抜ける。
次に、curr_sequence
のトリグラムをキーとして ngrams
辞書に渡し、次に来る可能性のある文字のリストを返します。
この辞書は次の文字のリストを返します。
次の文字のリストからランダムにインデックスが選択され、それが possible_chars
リストに渡され、現在のトリグラムの次の文字が得られます。
そして、その次の文字が output
変数に追加され、最終的な出力が格納されます。
最後に curr_sequence
がテキストコーパスから次のtrigramで更新されます。
自動的に生成された200文字を含む output
変数を表示すると、次のようになります(次の文字はランダムに選択されるので、出力は異なる可能性があることに注意してください)。
出力
tent pointo somensiver tournamedal pare the greak in the next peak sweder most begal tennis sport. the be has siders with sidernaments as was that adming up is coach rackhanced ball of ment. a game and
この場合、出力はあまり意味をなさない。
もし、変数 chars
の値を 4 に増やすと、次のような出力になるはずです。
tennis ahead with the club players under.most coaching motion us . the especific at the hit and events first predomination but of ends on the u.s. cyclops have achieved the end or net inches call over age
3-gramを使ったときよりも、少し良い結果になっているのがわかると思います。
このように、N-gramの数を増やせば増やすほど、テキスト提案・入力の精度は向上していきます。
次の章では、Words N-Gramsモデルを実装します。
単語N-gramsモデルの場合、生成されるテキストがより意味をなすことがわかると思います。
単語N-Gramsモデル
単語N-Gramsモデルでは、テキスト中の各単語は個別の項目として扱われる。
この節では、単語N-Gramsモデルを実装し、それを使って自動的なテキストフィラーを作成する。
使用するデータセットは前節で使用したものと同じである。
まず、単語三文型をキー、三文型の後に出現する単語のリストを値とする辞書を作成しましょう。
ngrams = {}
words = 3
words_tokens = nltk.word_tokenize(article_text)
for i in range(len(words_tokens)-words):
seq = ' '.join(words_tokens[i:i+words])
print(seq)
if seq not in ngrams.keys():
ngrams[seq] = []
ngrams[seq].append(words_tokens[i+words])
上のスクリプトではWords trigramモデルを作成します。
このプロセスは文字trigramを使用する場合と同様です。
しかし上記のスクリプトでは、まずコーパスを単語にトークン化する。
次に、すべての単語を繰り返し処理し、現在の3つの単語を結合してtrigramを形成する。
その後、そのtrigramがngrams
辞書に存在するかどうかを調べる。
もし、trigramがまだ存在しなければ、それをキーとして ngrams
辞書に挿入する。
最後に、コーパス全体の中でそのトリグラムに続く単語のリストを辞書の値として追加する。
ここで、変数エクスプローラで ngrams
辞書を見てみると、次のようになる。
このように、トリグラムが辞書のキー、対応する単語が辞書の値として表示されていることがわかります。
それでは、先ほど作成した単語トリグラムを使って、自動テキストフィラーを作ってみましょう。
curr_sequence = ' '.join(words_tokens[0:words])
output = curr_sequence
for i in range(50):
if curr_sequence not in ngrams.keys():
break
possible_words = ngrams[curr_sequence]
next_word = possible_words[random.randrange(len(possible_words))]
output += ' ' + next_word
seq_words = nltk.word_tokenize(output)
curr_sequence = ' '.join(seq_words[len(seq_words)-words:len(seq_words)])
print(output)
上のスクリプトでは、変数 curr_sequence
をコーパスに含まれる最初のトリグラムで初期化する。
最初のtrigramは “tennis is a “である。
この最初のトリグラムを入力として、50個の単語を生成する。
そのために、50回実行するforループを実行する。
各反復の間に、まず単語trigramが ngrams
辞書に存在するかどうかがチェックされる。
存在しない場合、ループは中断される。
そうでなければ、trigramを値として渡すことで、trigramに続く可能性のある単語のリストが ngrams
辞書から検索される。
その中からランダムに1つの単語を選び、outの末尾に追加する。
最後に、curr_sequence
変数が辞書にある次のtrigramの値で更新されます。
生成されたテキストはこのようになります。
単語のtrigramの場合、自動生成されたテキストはより意味をなすことがおわかりいただけると思います。
出力は以下の通りです。
tennis is a racket sport that can be played individually against a single opponent singles or between two teams of two players each doubles. each player uses a tennis racket include a handle known as the grip connected to a neck which joins a roughly elliptical frame that holds a matrix of
単語変数の値を4にして(4-gramを使う)テキストを生成すると、出力は以下のようにさらに強固なものになります。
tennis is a racket sport that can be played individually against a single opponent singles or between two teams of two players each doubles . each player uses a tennis racket that is strung with cord to strike a hollow rubber ball covered with felt over or around a net and into the opponents
4-gramを使うと、出力がより意味を持つことがわかります。
これは、このジェネレータがWikipediaの記事からほとんど同じテキストを再生成しているからですが、ジェネレータを少し改良し、より大きなコーパスを使えば、このジェネレータは簡単に新しいユニークな文章を生成することができます。
結論
N-Gramsモデルは、文中のN個の単語間の文脈を捉えるため、最も広く使われている文対ベクトルモデルの1つです。
この記事では、N-Gramsモデルの背後にある理論について見ました。
また、文字N-Gramsモデルや単語N-Gramsモデルの実装方法についても説明しました。
最後に、両者を用いて自動テキストフィラーを作成する方法を学びました。