spaCyでしりとりアプリを作る【自然言語処理, Python】
- 作成日: 2021-03-09
- 更新日: 2023-12-26
- カテゴリ: 自然言語処理
spaCyでしりとりアプリを作る
私たちが日常的に使っている日本語は「自然言語」と呼ばれます。
この自然言語を計算機的に解析することを「自然言語処理」と言います。
自然言語の解析には、Pythonでは自然言語処理ライブラリのspaCyを使うことができます。
今回はこのspaCyを使って「しりとりアプリ」を作ってみたいと思います。
具体的には↓を見ていきます。
- spaCyとは?
- しりとりとは?
- しりとりアプリの設計
- アプリの実行結果
- しりとりアプリのソースコード
- ソースコードの解説
spaCyとは?
spaCy(スパイシー)とはPython製の自然言語処理ライブラリです。
オープンソースなソフトウェアで、MITライセンスで利用することができます。
さまざまな言語の学習済み統計モデルを最初から使うことが出来て、字句解析や依存構造の構築などが簡単に行えます。
spaCyで日本語の解析を行うにはGiNZAのモデルを使います。
GiNZAはリクルートと国立国語研究所が共同開発した日本語を扱える自然言語処理ライブラリです。
spaCyもGiNZAもどちらもpipで簡単にインストールできます。
$ # spaCyのインストール
$ pip install spacy
$ # GiNZAのインストール
$ pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"
しりとりとは?
「しりとり」とは日本の言葉遊びです。
相手が言った言葉のお尻の文字を拾って、その文字から別の言葉を連想して言います。
相手もまた、その言葉から別の言葉を繋げて言います。
どちらかの言葉のお尻が「ん」で終わっていたら、その言葉を言った人の負けです。
たとえば「リンゴ」という言葉を相手が言ったとして、別の相手は「リンゴ」の「ゴ」から別の言葉をつなげます。
「ゴリラ」とその人が言ったら、もう一方の人は「ゴリラ」の「ラ」から言葉を繋げて「ラッパ」等と言います。
これが「ランタン」とか語尾に「ン」の付く言葉だったらその言葉を言った人の負けです。
🦝 < リンゴ
🐭 < ゴリラ
🦊 < ラッパ
しりとりアプリの設計
今回作成するしりとりアプリの設計ですが、これは実は過去にJanomeで作ってあります。
基本的には設計はこのJanome版のアプリと同じです。
まずしりとりの語彙に使用するデータをロードします。
このデータは青空文庫で公開されている夏目漱石の「吾輩は猫である」の文章を使います。
↑の文章をファイルwagahai_wa_neko_de_aru.txt
に保存しておきます。
そしてアプリ実行時にこのファイルを読み込んで、spaCyを使って解析します。
この解析した結果はクラスの属性sample_doc
として持っておきます。
あとはしりとりゲームのゲームルーチンを作り、その中でユーザーから入力を受けます。
入力を受けたら、その入力もspaCyで解析して、トークン列にします。
末尾のトークンの_.reading
属性、つまりそのトークンの読み方をチェックして、「ン」が付いてないかチェックします。
(_.reading
属性にはカタカナで単語の読み方が保存されます)
語尾に「ン」が付いていれば「あなたの負けです。」と表示します。
コンピューターはユーザーの入力を解析して、その語尾にマッチするセンテンスを語彙データから探します。
センテンスが見つかったらそのセンテンスの末尾に「ン」がないかチェックし、「ン」があったら「CPUの負けです。」と表示します。
問題なければそのセンテンスをユーザーに表示します。
基本的な設計はこれだけです。
アプリの実行結果
今回作成するアプリを実行すると↓のような結果になります。
in > 猫
この書生というのは時々我々を捕えて煮て食うという話である。
in > ルンバ
バルザックが或る日自分の書いている小説中の人間の名をつけようと思っていろいろつけて見たが、どうしても気に入らない。
in > 犬
沼へでも落ちた人が足を抜こうと焦慮るたびにぶくぶく深く沈むように、噛めば噛むほど口が重くなる、歯が動かなくなる。
in > ルビイ
B氏は横膈膜で呼吸して内臓を運動させれば自然と胃の働きが健全になる訳だから試しにやって御覧という。
in > 馬
また隣りの三毛君などは人間が所有権という事を解していないといって大に憤慨している。
in > ルイジアナ
名前はまだ無い。
in > イカ
肝心の母親さえ姿を隠してしまった。
in > 蛸
この書生というのは時々我々を捕えて煮て食うという話である。
in > ルンペン
あなたの負けです。
in >
しりとりとは言っても、今回のアプリは単語ではなく文章を返して来る仕様になっています。
Janome版では単語を返してましたが、こっちも面白いかと思います。
ただ「る」で終わる文章が多いので、ユーザーはけっこうハードな思考を求められます。
しりとりアプリのソースコード
しりとりアプリのソースコードは↓になります。
このアプリを実行するには↓のコードをsample.py
などに保存し、python sample.py
と実行します。
"""
spaCyを使ったしりとりアプリ
Lisence: MIT
Created at: 2021/01/28
"""
import spacy
nlp = spacy.load('ja_ginza')
class Siritori:
def __init__(self):
# しりとりに使う語彙のデータ
# load()で構築する
self.sample_doc = None
def load(self, fname):
"""
fnameのファイルを読み込み解析してsample_docに変換する
"""
with open(fname, 'r', encoding='utf-8') as fin:
content = fin.read()
self.sample_doc = nlp(content)
def siritori(self, doc):
"""
docの末尾の読み方にヒットするセンテンスを探しそれを返す
"""
last = doc[-1]
for sent in self.sample_doc.sents:
tok = sent[0]
if last._.reading[-1:] == tok._.reading[:1]:
return sent
return None
def update(self):
"""
ゲームルーチン
継続する場合はTrueを返し、終了する場合はFalseを返す
"""
try:
sentence = input('in > ')
except (KeyboardInterrupt, EOFError):
return False
doc = nlp(sentence)
if doc[-1]._.reading[-1:] == 'ン':
print('あなたの負けです。')
return True
sent = self.siritori(doc)
if sent is None:
print('パス')
return True
if sent[-1]._.reading[-1:] == 'ン':
print('CPUの負けです。')
return True
print(sent)
return True
def main():
siritori = Siritori()
siritori.load('../../assets/wagahai_wa_neko_de_aru.txt')
while siritori.update():
pass
main()
ソースコードの解説
簡単ですがソースコードの解説になります。
spacyのインポート
今回のアプリではspaCyを使います。
そのためspacy
モジュールをインポートしておきます。
import spacy
GiNZAモデルのロード
グローバル変数nlp
を作成します。
spaCyのお約束として、最初に学習済みの統計モデルをロードしておきます。
今回は日本語の文章を解析するのでGiNZAのモデルであるja_ginza
をspacy.load()
でロードします。
spacy.load()
はja_ginza
を読み込んだ場合、ginza.Japanese
を返してきます。
このnlp
は文章の解析に使われます。このオブジェクトに文章を渡すことで文章の解析を行えます。
nlp = spacy.load('ja_ginza')
main関数の作成
このアプリはmain
関数からはじまります。
main
関数ではSiritori
クラスをオブジェクトにして、語彙データ(wagahai_wa_neko_de_aru.txt
)を読み込みます。
そして無限ループでsiritori.update()
を実行します。
def main():
siritori = Siritori()
siritori.load('../../assets/wagahai_wa_neko_de_aru.txt')
while siritori.update():
pass
main()
Siritoriクラスの作成
アプリの主な仕事はSiritori
クラスで行います。
このクラスは__init__()
では属性の初期化のみを行います。
class Siritori:
def __init__(self):
# しりとりに使う語彙のデータ
# load()で構築する
self.sample_doc = None
load()で語彙データを読み込み
load()
メソッドは引数fname
のファイルを開いて内容を読み込みます。
そして読み込んだ内容をnlp()
に渡し、spaCyで文章を解析します。
解析した結果はクラスの属性sample_doc
に保存します。
このsample_doc
のタイプはspacy.tokens.doc.Doc
になります。
def load(self, fname):
"""
fnameのファイルを読み込み解析してsample_docに変換する
"""
with open(fname, 'r', encoding='utf-8') as fin:
content = fin.read()
self.sample_doc = nlp(content)
update()メソッドでゲームを行う
アプリのメインロジックはupdate()
メソッド内にあります。
このメソッドでは最初にinput()
でユーザーからの入力を読み取ります。
このとき例外KeyboardInterrupt
やEOFError
が飛んで来たらFalse
を返し、無限ループを終了するようにします。
こうするとCtrl+C
やEOF
が入力された場合にゲームが静かに終了します。
入力(sentence
)をnlp()
に渡して自然言語として解析します。
nlp()
の結果(doc
)はspacy.tokens.doc.Doc
として返ってきますが、これはトークン列として扱うことができます。
そのためdoc[-1]
のようにすると、末尾のトークン(spacy.tokens.token.Token
)にアクセスすることができます。
末尾のトークンの_.reading
属性を参照すると、そのトークンの読み方を参照できます。
よってdoc[-1]._.reading[-1:]
という式は「トークン列の末尾のトークンの末尾の読み方を参照する」という意味になります。
reading
をスライスで参照していますが、reading
はトークンによっては長さがない空になる場合があります。
そのためreading[-1]
のようにアクセスするとエラーになることがあるため、スライスを使用しています。
このようにnlp()
は解析した文章をトークン列として返します。
トークン列と言うのは単語のリストのことで、たとえば「猫が歩く」という文章であればこれは「猫 / が / 歩く」という単語のリストに変換されます。
その単語(トークン)には↑のようにreading
などの単語の情報が保存されます。
この単語の情報を参照することでいろいろな自然言語処理ができるという寸法です。
ユーザーの入力(doc
)はそのままsiritori()
メソッドに渡します。
siritori()
メソッドは引数のdoc
にマッチする文章を語彙データから検索するメソッドです。
その結果がNone
であれば語彙が見つからなかったということになるのでパスします。
語彙が見つかったら「ン」が付いてないかチェックして、画面に出力します。
def update(self):
"""
ゲームルーチン
継続する場合はTrueを返し、終了する場合はFalseを返す
"""
try:
sentence = input('in > ')
except (KeyboardInterrupt, EOFError):
return False
doc = nlp(sentence)
if doc[-1]._.reading[-1:] == 'ン':
print('あなたの負けです。')
return True
sent = self.siritori(doc)
if sent is None:
print('パス')
return True
if sent[-1]._.reading[-1:] == 'ン':
print('CPUの負けです。')
return True
print(sent)
return True
siritori()で語彙を検索する
siritori()
メソッドは引数doc
にマッチする語彙を語彙データから検索します。
内容的には語彙データであるsample_doc
のsents
属性を参照してループを回します。
sent
はspacy.tokens.span.Span
です。
これは「。」などで区切られた文章のことで、1つのまとまりとして扱います。
このsent
もトークン列として参照することができるのでsent[0]
で先頭のトークンを取り出します。
そしてdoc
の末尾のトークンの読み方とtok
の読み方を比較します。
比較ではlast
の読み方は末尾の文字で、tok
の読み方は先頭の文字を比較しています。
つまりしりとりにおける言葉のお尻と言葉の頭を比較しているということになります。
言葉のお尻が頭にヒットしたら、その文章はしりとりとして成立するということになります。
siritori()
は文章が見つかったらspacy.tokens.span.Span
を返し、見つからなかったらNone
を返します。
def siritori(self, doc):
"""
docの末尾の読み方にヒットするセンテンスを探しそれを返す
"""
last = doc[-1]
for sent in self.sample_doc.sents:
tok = sent[0]
if last._.reading[-1:] == tok._.reading[:1]:
return sent
return None
おわりに
今回はspaCyを使ってしりとりアプリを作ってみました。
ただ、このぐらいのアプリの場合はJanomeなどでも十分かもしれません。
というのも、このアプリでは依存構造を利用していないからです。
速度的にはspaCy版のしりとりアプリはモデルのロードや依存構造の解析に時間がかかっていて、アプリの起動に時間がかかります。
速度的に見ればJanomeなどで実装したほうが速くなります。
ただspaCyの練習にはちょうどいい題材かもしれません。
気が向いた方は改造などして遊んでみてください。
🦝 < しりとりマスターを目指せ
🐭 < おれの「ん」の価値は高いぜ