spaCyで「~って何?」に回答するスクリプトを作る【自然言語処理, Python】
目次
spaCyで「~って何?」に回答する
我々が話す言葉は「自然言語」と呼ばれます。
この自然言語をプログラム的に解析することを「自然言語処理」と言います。
spaCy(スパイシー)というPythonの自然言語処理ライブラリを使うと簡単に日本語を処理することができます。
今回はこのspaCyを使って「~って何?」という質問に回答するスクリプトをルールベースで作ってみたいと思います。
具体的には↓を見ていきます。
spaCyとは?
スクリプトの実行結果
スクリプトの設計
スクリプトのソースコード
ソースコードの解説
spaCyとは?
spaCy(スパイシー)とはPythonとCythonで作られたPythonの自然言語処理ライブラリです。
オープンソースで開発されていて、MITライセンスで利用することができます。
さまざまな言語の統計モデルをあらかじめ利用することができます。
最近になってGiNZA(ギンザ)という日本製の自然言語処理ライブラリが登場し、これのモデルがspaCyに組み込まれました。
GiNZAは日本語の形態素解析、構文解析を行うことができるライブラリで、spaCyもこのGiNZAを利用することで日本語の処理が強化されました。
spaCy, GiNZAともこれからの日本語の自然言語処理で活躍しそうな雰囲気のあるライブラリです。
要チェックしておきましょう。
- spaCy · Industrial-strength Natural Language Processing in Python
- GiNZA - Japanese NLP Library | Universal Dependenciesに基づくオープンソース日本語NLPライブラリ
(^ _ ^) | GiNZAに類似するライブラリは…… |
(・ v ・) | もちろんSHiBUYA |
スクリプトの実行結果
今回のスクリプトの動作検証は単体テストで行っています。
単体テストにはPythonの標準ライブラリであるunittest
を使います。
今回のスクリプトのソースコードをsample.py
などのファイルに保存し、↓のようにコマンドを実行します。
$ python -m unittest sample
すると↓のような出力が出ます。
. ---------------------------------------------------------------------- Ran 1 test in 0.806s OK
↑の表示はテストケースがすべて実行され、それが成功している時の表示です。
テストケースは↓のように定義しています。
class Test(unittest.TestCase): def eq(self, a, b): an = Analyzer() c = an.analyze(a) self.assertEqual(c, b) def test_analyze(self): self.eq('本日は晴天なり', None) self.eq('何ですか', None) self.eq('肉って何ですか?', '肉は食べ物です') self.eq('ねぇ、肉ってなに?', '肉は食べ物です') self.eq('鹿って何だよ?', '鹿は動物です') self.eq('あのー、鹿って何ですか?', '鹿は動物です')
↑のテストケースを見ると「肉って何ですか?」という入力に対して「肉は食べ物です」という出力が得られているのがわかります。
また「あのー、鹿って何ですか?」という入力に対しては「鹿は動物です」と出力しています。
今回のスクリプトはこのように「~って何?」とか「~ってなんですか?」という問いかけに対して答えを生成するスクリプトです。
スクリプトの設計
繰り返しになりますが、スクリプトの動作検証は単体テストで行います。
これはPythonの標準ライブラリであるunittest
を使います。
スクリプトのメインの処理を行うのはAnalyzer
というクラスです。
このクラスにanalyze()
というメソッドを定義し、このメソッドに入力となる文字列を渡したら、それを解析して回答を生成して返すという具合です。
analyze()
は内部でspaCyを利用します。
spaCyで入力となる文字列(日本語の文章、質問文)を解析し、これをspaCyのオブジェクトであるトークン列に変換します。
文章をspaCyのトークン列に変換したら、このトークン列をfor
文で走査します。
そして「何」を原形に持つトークンを見つけたら、そのトークンを入り口にして解析を分岐します。
トークン列から「何」のトークンに係っているトークン(名詞)を探して、そのトークンを見つけたらそのトークンのテキストから意味データベースを参照します。
意味データベースには単語の意味が保存されています。このデータベースを参照することで単語の意味を解析器に理解させます。
これはいわゆるルールベースと呼ばれる自然言語処理のアプローチです。ルール(辞書)を参照して、そのルールに当てはまるように処理を行います。
トークンの意味が判明したら、その意味にもとづいて結果となるテキストを生成します。
あとはそのテキストを返せば終わりです。
基本的な設計は↑のようになります。
(^ _ ^) | シンプル・イズ・ベスト |
スクリプトのソースコード
今回作成するスクリプトは↓になります。
""" 「~って何?」に回答するスクリプト License: MIT Created at: 2021/02/02 """ import spacy import unittest # GiNZAのモデルをロード nlp = spacy.load('ja_ginza') class Analyzer: """ 「~って何?」に回答するアナライザー """ def __init__(self): # 意味データベース self.imi_db = { '肉': { # 肉 is 食べ物 'is': '食べ物', # is-a の関係で表現されるテキストが保存される }, '鹿': { # 鹿 is 動物 'is': '動物', } } def analyze(self, text): """ 引数textを解析して「~って何?」の質問に答える """ doc = nlp(text) return self.analyze_nani(doc) def analyze_nani(self, doc): # トークン列を走査する for tok in doc: if tok.lemma_ == '何': # 原形で「何」が見つかった target = self.find_target(tok) # 「何」に係っているトークンを探す if not target: continue # 見つからなかった imi = self.grep_imi(target.text) # トークンの意味を求める if not imi: continue # 見つからなかった # 意味が見つかったのでテキストを整形して返す return f'{target.text}は{imi["is"]}です' return None def find_target(self, tok): """ 「何」に係っているトークンを探す """ for child in tok.children: if child.pos_ == 'NOUN': # 名詞が見つかったので return child # それを返す return None # 見つからなかった def grep_imi(self, text): """ 引数textの意味を求める """ if text in self.imi_db.keys(): return self.imi_db[text] return None # 意味が見つからなかった class Test(unittest.TestCase): def eq(self, a, b): an = Analyzer() c = an.analyze(a) self.assertEqual(c, b) def test_analyze(self): self.eq('本日は晴天なり', None) self.eq('何ですか', None) self.eq('肉って何ですか?', '肉は食べ物です') self.eq('ねぇ、肉ってなに?', '肉は食べ物です') self.eq('鹿って何だよ?', '鹿は動物です') self.eq('あのー、鹿って何ですか?', '鹿は動物です')
ソースコードの解説
簡単ですがソースコードの解説になります。
必要モジュールをインポート
スクリプトの冒頭で必要となるモジュールをインポートしておきます。
spaCyを使うにはspacy
モジュールをインポートします。
それから単体テスト用にunittest
をインポートします。
import spacy import unittest
GiNZAのモデルをロードする
spaCyでは利用前に学習済みの統計モデルをロードして使います。
今回は日本語の解析のためにGiNZAのモデルを利用します。
↓のようにspacy.load()
にja_ginza
を指定するとGiNZAのモデルをロードできます。
ja_ginza
をロードした場合、spacy.load()
はginza.Japanese
を返します。
このspacy.load()
の返り値には慣例的にnlp
と命名するようになっています。
このnlp
に文字列を渡すことで解析を実行することが可能です。
# GiNZAのモデルをロード nlp = spacy.load('ja_ginza')
Analyzerクラスを定義する
今回のメインの処理はAnalyzer
クラスに実装します。
Analyzer
には__init__()
メソッドを定義します。
このメソッド内で意味データベースを初期化します。
意味データベースは仰々しい名前ですが、他にこの名前を使ってる所があるのかはしりません。
意味データベースは内容的にはただのPythonの辞書です。
↓のように単語をキーにしてその意味を辞書で定義します。
↓の例で言えば、「肉」という単語の意味は「is
」が「食べ物」になります。
このis
というのは、「~は~です」というフォーマットで利用されるテキストになります。
「肉」の場合はis
が「食べ物」なのでこれから生成される文章は「肉は食べ物です」になります。
意味データベースに登録されていない単語には非対応になります.
この意味データベースが充実すれば生成できる回答も充実しますが、手作業でこれを整えるのはかなり大変かと予想できます。
class Analyzer: """ 「~って何?」に回答するアナライザー """ def __init__(self): # 意味データベース self.imi_db = { '肉': { # 肉 is 食べ物 'is': '食べ物', # is-a の関係で表現されるテキストが保存される }, '鹿': { # 鹿 is 動物 'is': '動物', } }
analyze()で入力文章を解析する
analyze()
メソッドは引数text
の文章を解析して、回答となるテキストを返り値で返します。
nlp()
にテキストを渡すと、spaCyによる解析が実行され、その結果をdoc
として返します。
このdoc
はspacy.tokens.doc.Doc
になります。
このdoc
はトークン列を抽象化したオブジェクトです。そのためfor
文などで回すことができます。
doc
を生成したらanalyze_nani()
メソッドに処理を任せて終わりです。
def analyze(self, text): """ 引数textを解析して「~って何?」の質問に答える """ doc = nlp(text) return self.analyze_nani(doc)
analyze_nani()でトークン列を解析する
analyze_nani()
メソッドは引数doc
を解析して回答となるテキストを返すメソッドです。
doc
はfor
文で回せるのでfor tok in doc:
とやって回します。
このときのtok
はspacy.tokens.token.Token
になります。
spaCyは日本語の文章を解析すると、トークン列に変換します。
これは形態素解析と構文解析の結果のトークン列です。
形態素解析と言うのは簡単に言うと文章を単語のリストに変換する解析です。
たとえば「犬が笑う」という文章を形態素解析すると「犬 / が / 笑う」という単語列になります。
このとき、単語(トークン)にその単語の持つ情報を保存します。
情報とは単語の読み方や単語の原形などのことです。
この形態素解析が終わると構文解析がはじまります。
構文解析は単語と単語の関係を依存構造で構築する解析です。
この解析によってどの単語がどの単語に係(かか)っているかと言うのがプログラム的に解析できるようになります。
たとえば「猫が鳴く」という文章を解析すると↓のような依存構造になります。
猫 NOUN ═╗<╗ iobj が ADP <╝ ║ case 鳴く VERB ═══╝ ROOT
「鳴く」という単語から矢印が伸びて「猫」に係っているのがわかります。
どの単語がどの単語にかかっているかがわかれば、単語から関係性のある単語を辿ることが可能になります。
これは意味解析以上の自然言語処理で必要となる情報です。
analyze_nani()
はトークン列を走査し、トークンのlemma_
属性を参照します。
このlemma_
属性は単語の「原形」を表す属性です。
原形とはたとえば「鳴いた」の「鳴い」の原形は「鳴く」です。それから「立った」の「立っ」の原形は「立つ」です。
lemma_
が「何」だったらそのトークンをキーにして処理を分岐します。
そのトークンに係っているトークンをfind_target()
メソッドで検索します。
係っているトークン(ターゲット)が見つかったら、そのターゲットのtext
属性を参照します。
トークンのtext
属性は元の文章のそのままの表記の文字列が保存されています。
そのtext
をgrep_imi()
メソッドに渡して意味データベースからトークンの意味を求めます。
意味が見つかったらその情報とトークンの情報を元に回答となる文章を生成してreturn
します。
def analyze_nani(self, doc): # トークン列を走査する for tok in doc: if tok.lemma_ == '何': # 原形で「何」が見つかった target = self.find_target(tok) # 「何」に係っているトークンを探す if not target: continue # 見つからなかった imi = self.grep_imi(target.text) # トークンの意味を求める if not imi: continue # 見つからなかった # 意味が見つかったのでテキストを整形して返す return f'{target.text}は{imi["is"]}です' return None
find_target()で係っているトークンを探す
find_target()
メソッドは引数tok
に係っているトークン(名詞)を探します。
トークンのchildren
属性にはそのトークンに係っているトークン列が保存されています。
つまりspacy.tokens.token.Token
のリスト(正確にはジェネレーター)です。
このchildren
属性をfor
文で回し、そのトークンのpos_
属性を調べます。
pos_
属性はトークンの品詞を表す文字列が保存されています。
このpos_
属性がNOUN
だった場合はそのトークンは名詞になります。
名詞のトークンが見つかったらそのトークンをreturn
します。
見つからなかったらNone
を返します。
def find_target(self, tok): """ 「何」に係っているトークンを探す """ for child in tok.children: if child.pos_ == 'NOUN': # 名詞が見つかったので return child # それを返す return None # 見つからなかった
grep_imi()で意味を求める
grep_imi()
は引数text
をキーにしてその文字列の意味を求めるメソッドです。
内容的にはtext
をキーにしてimi_db
を参照します。
imi_db
内にデータがあればそれを返し、無ければNone
を返します。
def grep_imi(self, text): """ 引数textの意味を求める """ if text in self.imi_db.keys(): return self.imi_db[text] return None # 意味が見つからなかった
テストケースを書く
テストケースを書きます。
unittest
を使ったテストケースを書くにはunittest.TestCase
を継承したクラスを作ります。
そしてtest_
で始まるメソッドを定義します。このメソッドがテスト時に実行されます。
eq()
メソッドは引数a
とb
を比較するメソッドです。
内部ではAnalyzer
を使って引数a
を解析し、その結果のc
をb
比較しています。
assertEqual()
は第1引数と第2引数が違う場合エラーを出します。
エラーが出るとテストに失敗します。
class Test(unittest.TestCase): def eq(self, a, b): an = Analyzer() c = an.analyze(a) self.assertEqual(c, b) def test_analyze(self): self.eq('本日は晴天なり', None) self.eq('何ですか', None) self.eq('肉って何ですか?', '肉は食べ物です') self.eq('ねぇ、肉ってなに?', '肉は食べ物です') self.eq('鹿って何だよ?', '鹿は動物です') self.eq('あのー、鹿って何ですか?', '鹿は動物です')
おわりに
今回はspaCyを使って「~って何?」という問いかけに答えるスクリプトを作ってみました。
人工知能などでユーザーの問いかけに答えるAIというのは需要が高いジャンルかと思いますが、今回はそれを目指してやってみました。
何かの参考になれば幸いです。
(^ _ ^) | 自然言語処理 is 何? |
(・ v ・) | ぼっちを救う科学 |