spaCyで虫食い文章を穴埋めする【自然言語処理, Python】
目次
spaCyで文章を穴埋めする
私たちが話す言葉は「自然言語」と呼ばれます。
この自然言語を計算機的に解析するのが「自然言語処理」です。
Pythonには自然言語処理を行うライブラリとしてspaCyがあります。
今回はこのspaCyを使ってPythonで虫食い文章を穴埋めするスクリプトを作ってみたいと思います。
具体的には↓を見ていきます。
虫食い文章の穴埋めとは?
スクリプトの実行結果
スクリプトの仕様
スクリプトのソースコード
ソースコードの解説
虫食い文章の穴埋めとは?
穴埋めとは、文章中の特定のワードを別のワードで置き換えることを指します。
たとえば↓のような虫食いされた文章があるとします。
今日はxxxへ行った。
↑のxxx
が虫食いされている部分です。
このxxx
を適当な言葉で置き換えます。たとえば「山」とか「川」です。
今日は山へ行った。 今日は川へ行った。
穴埋めすることで文章に違う意味が生まれます。
文章の穴埋めとはこのように行います。
スクリプトの実行結果
今回作成するスクリプトを実行すると↓のような結果になります(見易いように一部整形しています)。
in > 今日はbbbに行った。 今日は御師匠に行った。 in > 今日はaaaがbbbだった。 今日は情ない事が美学上だった。 in > aaaにbbbした。 おしいつくつくに一大仮定した。 in > なんだかんだbbbが好き。 なんだかんだ注文通りが好き。 in > おいaaa、bbbはやめろ。 おいうまい肴、万事積極的はやめろ。 in > なんかbbbがaaaみたいだ。 なんか未来記が重い事みたいだ。
↑の出力を見ると、文章として成立してない文もあります。
穴埋めの精度としては低いことがわかります。
「今日はbbbに行った。」という虫食い文章は「今日は御師匠に行った。」と言う風に穴埋めされています。
「aaaにbbbした。」という虫食い文章は「おしいつくつくに一大仮定した。」という風にです。
文章として成立させるには、依存構造と言葉の選択を考慮する必要がありますが、今回のスクリプトではそこまでやっていません。
aaa
とbbb
で表示する文章を変更するという工夫はやっていますが、これは品詞の並びを変えるという工夫です。
スクリプトの仕様
今回作るスクリプトの仕様についてです。
まず虫食い文章のフォーマットですが、これは↓のようなものです。
虫食い部分にはaaaまたはbbbを使う
虫食い部分にはaaa
またはbbb
の文字列を使います。
つまり虫食い文章は↓のようなものになります。
今日はaaaとbbbへ行った。
aaa
とbbb
にはそれぞれ違う方法で生成された文章を割り振ります。
割り振る文章を生成する必要がありますが、これの元になるデータは青空文庫で公開されている夏目漱石の「吾輩は猫である」を使います。
このテキストをスクリプトの最初の方でロードし、それを自然言語処理で解析して、語彙データを作ります。
その語彙データから穴埋め用の文章を取り出して、虫食いされた文章に当てはめます。
虫食い文章自体はユーザーからの入力を標準入力から受けます。
つまりスクリプトは↓のような手順で動作します。
語彙データの生成
ユーザーからの入力を受け取る
ユーザーの入力を解析して穴埋めする
2, 3をループ
穴埋めする文章自体はランダムに選択します。
そのため今回のスクリプトではテストを書いていません。
あらかじめご了承ください。
スクリプトのソースコード
スクリプトのソースコードは↓になります。
このスクリプトを実行するには↓のソースコードをsample.py
などに保存し、python sample.py
を実行してください。
""" 虫食い文章に穴埋めするスクリプト 虫食い文章の例: aaaはbbbだ License: MIT Created at: 2021/01/31 """ import spacy import random # GiNZAのモデルのロード nlp = spacy.load('ja_ginza') class Anaume: def load(self, fname): """ fnameのファイルを読み込んで解析して語彙データとして保存する """ with open(fname, 'r', encoding='utf-8') as fin: content = fin.read() doc = list(nlp(content)) self.aaa_db = self.create_aaa_db(doc) self.bbb_db = self.create_bbb_db(doc) def analyze(self, sentence): """ sentenceを解析して穴埋めする """ doc = nlp(sentence) # 文章を解析してdocに s = '' for tok in doc: if tok.text == 'aaa': # 穴埋め対象のトークン(aaa)を見つけた anaume = random.choice(self.aaa_db) s += anaume # 穴埋めする elif tok.text == 'bbb': # 穴埋め対象のトークン(bbb)を見つけた anaume = random.choice(self.bbb_db) s += anaume # 穴埋めする else: s += tok.text return s def create_aaa_db(self, doc): """ docからaaa用の語彙データベースを作る """ db = [] # 保存先のDB i = 0 while i < len(doc) - 1: t1 = doc[i] t2 = doc[i + 1] i += 2 # ADJ + (NOUN | PRON | PROPN)の組み合わせだったらテキストを保存する if t1.pos_ == 'ADJ' and t2.pos_ in ('NOUN', 'PRON', 'PROPN'): db.append(t1.text + t2.text) return db def create_bbb_db(self, doc): """ docからbbb用の語彙データベースを作る """ db = [] # 保存先のDB i = 0 while i < len(doc) - 1: t1 = doc[i] t2 = doc[i + 1] i += 2 # NOUN + (NOUN | PRON | PROPN)の組み合わせだったらテキストを保存する if t1.pos_ == 'NOUN' and t2.pos_ in ('NOUN', 'PRON', 'PROPN'): db.append(t1.text + t2.text) return db def main(): anaume = Anaume() # ファイルを読み込む anaume.load('../../assets/wagahai_wa_neko_de_aru.short.txt') while True: try: sentence = input('in > ') except (KeyboardInterrupt, EOFError): break # 穴埋めする result = anaume.analyze(sentence) print(result) main()
ソースコードの解説
簡単ですがソースコードの解説になります。
必要モジュールのインポート
必要モジュールをインポートします。
今回は自然言語処理にspaCyを使うためspacy
をインポートします。
それからrandom
モジュールもインポートしておきます。これは穴埋めに使う文章をランダムに選択するのに使います。
import spacy import random
GiNZAモデルのロード
スクリプトの最初の方でGiNZAのモデルをロードしておきます。
spaCyで日本語を解析したい場合は、↓のようにspacy.load()
でGiNZAのモデルja_ginza
をロードします。
# GiNZAのモデルのロード nlp = spacy.load('ja_ginza')
nlp
はginza.Japanese
クラスのオブジェクトです。
このnlp
に文章を渡すことで解析を行うことができます。
GiNZAはリクルートと国立国語研究所が共同開発した自然言語処理ライブラリです。
最近までspaCyは構文解析以上のことが日本語の処理でできなかったんですが、このGiNZAの登場によりspaCyでも構文解析以上の解析処理が書けるようになったとのことです。
main()関数を作る
スクリプトはmain()
関数から始まります。
main()
関数内ではAnaume
クラスからオブジェクトを作って、load()
で語彙データを作成しています。
語彙データの作成に使うファイルは夏目漱石の「吾輩は猫である」の文章が保存されたテキストファイルです。
語彙データを作成したらwhile
文で無限ループに入ります。
そしてinput()
でユーザーからの入力を受けます。
このときinput()
から例外KeyboardInterrupt
またはEOFError
が送出された場合はループから抜けるようにしています。
KeyboardInterrupt
はCtrl+C
などが入力されたとき、EOFError
はEOF
が入力されたときに発生します。
ユーザーからの入力はsentence
に保存します。sentence
は「文章」という意味です。
このsentence
をanaume.analyze()
に渡して虫食い文章への穴埋めを実行します。
analyze()
からの結果をresult
で受け取り、それを画面に出力してループ内はひと段落します。
(^ _ ^) | ひと段落、自然言語だけに |
(・ v ・) | うまい |
def main(): anaume = Anaume() # ファイルを読み込む anaume.load('../../assets/wagahai_wa_neko_de_aru.short.txt') while True: try: sentence = input('in > ') except (KeyboardInterrupt, EOFError): break # 穴埋めする result = anaume.analyze(sentence) print(result) main()
Anaumeクラスの作成
今回のスクリプトでは主な処理はAnaume
というクラスで行います。
このクラスは語彙データのロードと保持、それから実際の穴埋めの解析を行います。
class Anaume: ...
クラスのメソッドについては後述します。
load()でファイルの読み込みと語彙データの作成
load()
メソッドは引数fname
のファイルを開いてその内容を読み込み、その内容をspaCyで解析してdoc
にします。
doc
はspacy.tokens.doc.Doc
です。このdoc
には解析した結果が格納されています。つまりdoc
を参照すれば解析結果にアクセスできるということになります。内容的にはトークン列になっています。これは単語の列のことです。
nlp
()に文章を渡すとspaCyは文章を解析してトークン列にします。そして各トークンには単語の情報が保存されます。
解析ではこの単語の情報を参照することで自然言語処理を行うことができます。
doc
をcreate_aaa_db
に渡してキーワードaaa
用のデータベース(語彙データ)をaaa_db
に保存します。
bbb
についても同様です。
このaaa_db
とbbb_db
は虫食い文章に穴埋めするときに参照されます。
内容的にはただの文字列のリストです。穴埋め用の文字列が保存されます。
def load(self, fname): """ fnameのファイルを読み込んで解析して語彙データとして保存する """ with open(fname, 'r', encoding='utf-8') as fin: content = fin.read() doc = list(nlp(content)) self.aaa_db = self.create_aaa_db(doc) self.bbb_db = self.create_bbb_db(doc)
analyze()で入力文章を穴埋めする
analyze()
メソッドは引数sentence
の文字列を解析して、虫食い部分に穴埋めするメソッドです。
sentence
をnlp()
に渡してspaCyで解析しdoc
にします。
doc
はトークン列として扱うことができます。そのためfor
文で回せます。
↓のようにfor tok in doc:
でトークンを1つずつ取り出して処理しています。
tok
はspacy.tokens.token.Token
です。
トークンの属性text
には元の文章のそのままの表記が保存されています。
このtext
を参照してaaa
だったらaaa_db
から穴埋め用の文字列を取ってきて、s
に溜めます。
bbb
だったらbbb_db
から穴埋め用の文字列を取ってきます。
それ以外であれば単純にs
にtext
を溜めていきます。
たとえば「私はbbbです
」という文章であれば、トークン列は「私 / は / bbb / です
」になります。
トークン「私
」のtext
は「私
」になるわけです。
random.choice()
はリストからランダムに要素を選択するメソッドです。
aaa_db
はただの文字列のリストなので、これによってランダムに穴埋め用の文字列が取得できます。
ループが完了したらs
をreturn
してメソッドは終わりです。
def analyze(self, sentence): """ sentenceを解析して穴埋めする """ doc = nlp(sentence) # 文章を解析してdocに s = '' for tok in doc: if tok.text == 'aaa': # 穴埋め対象のトークン(aaa)を見つけた anaume = random.choice(self.aaa_db) s += anaume # 穴埋めする elif tok.text == 'bbb': # 穴埋め対象のトークン(bbb)を見つけた anaume = random.choice(self.bbb_db) s += anaume # 穴埋めする else: s += tok.text return s
create_aaa_db(), create_bbb_db()で語彙データを作る
create_aaa_db()
とcreate_bbb_db()
はそれぞれキーワードaaa
とbbb
用の語彙データを作ります。
内容的には引数doc
のトークン列を参照し、トークンの品詞を参照します。
トークンt1
のpos_
属性がADJ
で、トークンt2
のpos_
属性がNOUN
, PRON
, PROPN
のいずれかであればデータベースにトークンのtext
を合成して追加します。
トークンのpos_
属性には品詞の種類を表す文字列が保存されています。
↑に出てきた品詞については↓の通りです。
ADJ ... 形容詞
NOUN ... 名詞
PRON ... 代名詞
PROPN ... 固有名詞
create_aaa_db()
とcreate_bbb_db()
の違いですが、それぞれキーとなる品詞が一部違っています。
この2つのメソッドはほぼ同じことをやっているため、共通化したほうが良さそうですが、今回は共通化していません。
def create_aaa_db(self, doc): """ docからaaa用の語彙データベースを作る """ db = [] # 保存先のDB i = 0 while i < len(doc) - 1: t1 = doc[i] t2 = doc[i + 1] i += 2 # ADJ + (NOUN | PRON | PROPN)の組み合わせだったらテキストを保存する if t1.pos_ == 'ADJ' and t2.pos_ in ('NOUN', 'PRON', 'PROPN'): db.append(t1.text + t2.text) return db def create_bbb_db(self, doc): """ docからbbb用の語彙データベースを作る """ db = [] # 保存先のDB i = 0 while i < len(doc) - 1: t1 = doc[i] t2 = doc[i + 1] i += 2 # NOUN + (NOUN | PRON | PROPN)の組み合わせだったらテキストを保存する if t1.pos_ == 'NOUN' and t2.pos_ in ('NOUN', 'PRON', 'PROPN'): db.append(t1.text + t2.text) return db
おわりに
今回は虫食い文章を穴埋めするというスクリプトを作ってみました。
文章の生成と言う点で穴埋めのアルゴリズムはなかなか奥が深そうだと思いました。
(^ _ ^) | aaaはbbbだ |
(・ v ・) | 天気は晴れだ |