spaCyで名詞に係る動詞を抽出する【自然言語処理, Python】
- 作成日: 2021-03-10
- 更新日: 2023-12-24
- カテゴリ: 自然言語処理
spaCyで名詞に係る動詞を抽出する
私たちが使う言葉は「自然言語」と言われていますが、この自然言語を解析するのが「自然言語処理」です。
自然言語処理を行えるライブラリは多数ありますが、PythonにはspaCyという自然言語処理ライブラリがあります。
今回はこのspaCyを使って日本語の文章から、「名詞に係っている動詞」を抽出してみたいと思います。
具体的には↓を見ていきます。
- spaCyとは?
- spaCyの名詞と動詞について
- プログラムの実行結果
- プログラムの設計
- プログラムのソースコード
- ソースコードの解説
spaCyとは?
spaCyはPython製の自然言語処理ライブラリです。
オープンソースで開発されており、MITライセンスで利用することができます。
さまざまな言語の学習済み統計モデルを搭載していて、モデルをロードすることで簡単に利用することができます。
それらのモデルを利用して自然言語を解析することが可能です。
近年まで日本語の解析に弱かったspaCyですが、GiNZAの登場によって日本語の解析が簡単に可能になりました。
GiNZAとはリクルートと国立国語研究所が共同で開発した日本語を扱える自然言語処理ライブラリです。
spaCyによる日本語の解析では、GiNZAのモデルをロードして解析するのが一般的な流れになります。
spaCyの名詞と動詞について
spaCyでは解析したデータをトークン列として扱います。
トークン列とは単語のリストのことです。
たとえば「猫が笑う」という文章をトークン列に変換すると「猫 / が / 笑う」のようになります。
それぞれの単語は情報を持っていて、その単語の情報を参照することでその単語を解析することができます。
その単語の情報の1つがトークンの持つpos_
属性です。
これは単語の品詞を表す属性です。
品詞とは単語の分類のことで、名詞や動詞などがそれに当たります。
たとえばトークンのpos_
がVERB
だった場合、その単語は動詞になります。
いっぽうpos_
がNOUN
だった場合、その単語は名詞になります。
プログラムの解析ではこのpos_
属性をひんぱんに参照します。
プログラムの実行結果
後述のプログラムのソースコードをsample.py
等に保存し、python -m unittest sample
とやって単体テストを実行すると↓のような結果になります。
.
----------------------------------------------------------------------
Ran 1 test in 0.993s
OK
↑の出力はテストがすべて通過したことを表しています。
テストの詳細は後述のソースコードの解説を見て頂くとして、テスト内容は↓のようになります。
# うまくいくケース
self.eq('猫は走った。', '猫 = 走る')
self.eq('可愛い猫は走った。', '猫 = 走る')
self.eq('猫は走り去った。', '猫 = 走り去る')
self.eq('可愛い猫は華麗に走り去った。', '猫 = 走り去る')
self.eq('猫と犬は走った。', '猫, 犬 = 走る')
self.eq('走った猫が。', '猫 = 走る')
self.eq('走る猫。', '猫 = 走る')
self.eq('やみくもに走る猫。', '猫 = 走る')
self.eq('笑う猫は走った。', '猫 = 笑う, 走る')
# うまくいかないケース
self.eq('猫は元気よく走った。', '元気, 猫 = 走る') # 「元気」が名詞扱い
self.eq('犬は走り猫は笑った。', '走り, 猫, 犬 = 笑う') # 「走り」が名詞扱い(CaboChaでも同じ)
↑のテストでは「猫は走った。」という入力から「猫 = 走る」という出力が生成されています。
「うまくいくケース」ではテストが期待通りの結果になっていますが、「うまくいかないケース」では疑問符の付く結果となっています。
「猫は元気よく走った。」という入力からは「猫 = 走る」という出力を期待したいところですが、現実の結果は「元気, 猫 = 走る」になっています。
これは「元気」という単語が名詞になっているためです。
プログラムの設計
今回作るプログラムの設計についてです。
プログラムの動作テストは単体テストで行います。
これはPythonのunittest
モジュールを使います。
spaCyで日本語の文章を解析してdoc
オブジェクトに変換し、トークン列にします。
そしてそのトークン列を回して、名詞と動詞のトークンを探します。
名詞のトークンが見つかったら、そのトークンに係っている動詞のトークン列を探します。
動詞のトークンの場合は、そのトークンに係っている名詞のトークンを探します。
トークン列が見つかったらそれぞれ保存し、最後にトークン列をテキストに変換します。
テキストのフォーマットは↓のようになります。
名詞 = 動詞
基本形のフォーマット↑ですが↓のフォーマットにも対応します。
名詞, 名詞 = 動詞
名詞 = 動詞, 動詞
名詞, 名詞 = 動詞, 動詞
これは名詞と動詞が複数ある場合があるからです。
そのため↑のようなフォーマットにして対応しています。
プログラムのソースコード
↓がプログラムのソースコードです。
繰り返しになりますが、このプログラムを実行するには↓のソースコードをsample.py
などに保存し、python -m unittest sample
を実行してください。
"""
入力された文章の名詞に係っている動詞を抽出する
Licence: MIT
Created at: 2021/01/29
"""
import spacy
import unittest
# モデルをロード
nlp = spacy.load('ja_ginza')
class Analyzer:
def analyze(self, sentence):
"""
sentenceを解析して名詞と動詞を抽出する
"""
doc = nlp(sentence)
verbs = [] # 動詞のリスト
nouns = [] # 名詞のリスト
for tok in doc:
if tok.pos_ == 'VERB':
# 動詞に係っている名詞を集める
self.collect_nouns_from_children(nouns, tok)
if len(nouns):
verbs.append(tok)
break
elif tok.pos_ == 'NOUN':
# 名詞に係っている動詞を集める
verb = self.find_verb_from_children(tok)
if verb:
verbs.append(verb)
nouns.append(tok)
if not len(verbs):
return '' # 動詞が空なら抽出失敗
# 重複したトークンを除く
nouns = self.unique(nouns)
verbs = self.unique(verbs)
# トークン列を文字列に整形
s = ', '.join([tok.text for tok in nouns[::-1]])
s += ' = ' + ', '.join([tok.lemma_ for tok in verbs])
return s
def unique(self, toks):
"""
toksから重複したトークンを除く
"""
dst = []
def has(arg):
for t in dst:
if t == arg:
return True
return False
for tok in toks:
if not has(tok):
dst.append(tok)
return dst
def find_verb_from_children(self, tok):
"""
tok.childrenから再帰的にverbを見つける
"""
if tok.pos_ == 'VERB':
return tok
for t in tok.children:
found = self.find_verb_from_children(t)
if found:
return found
return None
def collect_nouns_from_children(self, nouns, tok):
"""
tok.childrenから再帰的に名詞を集める
"""
if tok.pos_ == 'NOUN':
nouns.append(tok)
for t in tok.children:
self.collect_nouns_from_children(nouns, t)
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('猫は走った。', '猫 = 走る')
self.eq('可愛い猫は走った。', '猫 = 走る')
self.eq('猫は走り去った。', '猫 = 走り去る')
self.eq('可愛い猫は華麗に走り去った。', '猫 = 走り去る')
self.eq('猫と犬は走った。', '猫, 犬 = 走る')
self.eq('走った猫が。', '猫 = 走る')
self.eq('走る猫。', '猫 = 走る')
self.eq('やみくもに走る猫。', '猫 = 走る')
self.eq('笑う猫は走った。', '猫 = 笑う, 走る')
# うまくいかないケース
self.eq('猫は元気よく走った。', '元気, 猫 = 走る') # 「元気」が名詞扱い
self.eq('犬は走り猫は笑った。', '走り, 猫, 犬 = 笑う') # 「走り」が名詞扱い(CaboChaでも同じ)
ソースコードの解説
簡単ですがソースコードの解説になります。
必要モジュールのインポート
日本語の文章の解析にspaCyを使うのでspacy
をインポートします。
それから単体テスト用にunittest
をインポートしておきます。
import spacy
import unittest
GiNZAモデルのロード
グローバル変数nlp
を作成します。
# モデルをロード
nlp = spacy.load('ja_ginza')
spacy.load()
は統計モデルをロードします。
ja_ginza
を指定していますが、これはGiNZAのモデルです。
このモデルをロードするとspaCyで日本語の文章を解析できます。
spacy.load()
の返り値はginza.Japanese
です。
nlp
をグローバル変数にしているのは、この変数は複数のクラスから使用される可能性があるからです。
nlp()
に文章を渡すとその文章を解析することが可能です。
Analyzerクラスの作成
今回のプログラムの主な処理はAnalyzer
クラスで行います。
class Analyzer:
...
メソッドは後述します。
analyze()で文章を解析
解析の本体はanalyze()
メソッドです。
analyze()
は引数のsentence
をnlp()
で解析して、doc
にします。
これはspacy.tokens.doc.Doc
です。
doc
はトークン列として扱うことが可能です。そのためfor
文で回すことができます。
doc
をfor
文で回すとトークンを取り出せます。
このトークンが動詞(VERB
)または名詞(NOUN
)だった場合に処理が分岐します。
VERB
の場合はそのトークンに係っている名詞のトークン列をcollect_nouns_from_children()
で集めます。
NOUN
の場合は動詞のトークンをfind_verb_from_children()
で探します。
VERB
の場合は名詞が見つかった場合はループからbreak
してます。
NOUN
の場合はループを続行してます。
この違いは文脈の解釈的にこっちのほうが良い精度が出るからです。
verbs
とnouns
のトークン列を保存したら、そのトークン列から重複したトークンを除きます。
重複したトークンを除いたらそのトークン列を使ってフォーマットのテキストを生成します。
あとはそれをreturn
して終わりです。
def analyze(self, sentence):
"""
sentenceを解析して名詞と動詞を抽出する
"""
doc = nlp(sentence)
verbs = [] # 動詞のリスト
nouns = [] # 名詞のリスト
for tok in doc:
if tok.pos_ == 'VERB':
# 動詞に係っている名詞を集める
self.collect_nouns_from_children(nouns, tok)
if len(nouns):
verbs.append(tok)
break
elif tok.pos_ == 'NOUN':
# 名詞に係っている動詞を集める
verb = self.find_verb_from_children(tok)
if verb:
verbs.append(verb)
nouns.append(tok)
if not len(verbs):
return '' # 動詞が空なら抽出失敗
# 重複したトークンを除く
nouns = self.unique(nouns)
verbs = self.unique(verbs)
# トークン列を文字列に整形
s = ', '.join([tok.text for tok in nouns[::-1]])
s += ' = ' + ', '.join([tok.lemma_ for tok in verbs])
return s
collect_nouns_from_children()で名詞を集める
collect_nouns_from_children()
は引数tok
のchildren
から再帰的に名詞を集めます。
トークンの属性children
はそのトークンに係っているトークン列を表します。
このchildren
を辿ることで依存構造のツリーを辿ることができます。
名詞のトークンは引数nouns
に保存されます。
def collect_nouns_from_children(self, nouns, tok):
"""
tok.childrenから再帰的に名詞を集める
"""
if tok.pos_ == 'NOUN':
nouns.append(tok)
for t in tok.children:
self.collect_nouns_from_children(nouns, t)
find_verb_from_children()で動詞を見つける
find_verb_from_children()
は引数tok
のchildren
を再帰的に検索して動詞のトークンを返します。
動詞のトークンが見つかった場合はその時点で再帰を終了します。
見つからなかった場合はNone
を返します。
def find_verb_from_children(self, tok):
"""
tok.childrenから再帰的にverbを見つける
"""
if tok.pos_ == 'VERB':
return tok
for t in tok.children:
found = self.find_verb_from_children(t)
if found:
return found
return None
unique()で重複したトークンを除く
unique()
メソッドは引数toks
から重複したトークンを除きます。
内部関数のhas()
は引数arg
がdst
の中に含まれていたらTrue
を返します。
has()
でトークンがdst
に含まれているか調べて、含まれていなかったらdst
に追加するという具合です。
def unique(self, toks):
"""
toksから重複したトークンを除く
"""
dst = []
def has(arg):
for t in dst:
if t == arg:
return True
return False
for tok in toks:
if not has(tok):
dst.append(tok)
return dst
単体テストを書く
今回はmain
関数などは定義しておらず、動作のテストは↓の単体テストで行います。
単体テストはunittest.TestCase
を継承したクラスを作ります。
そしてtest_
で始めるメソッドを定義します。このメソッドがテストで実行されます。
eq()
はAnalyzer
をオブジェクトにして引数a
を渡してanalyze()
を実行します。
その結果c
を引数b
とassertEqual()
で比較します。
assertEqual()
はTestCase
のメソッドで、これは第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('猫は走った。', '猫 = 走る')
self.eq('可愛い猫は走った。', '猫 = 走る')
self.eq('猫は走り去った。', '猫 = 走り去る')
self.eq('可愛い猫は華麗に走り去った。', '猫 = 走り去る')
self.eq('猫と犬は走った。', '猫, 犬 = 走る')
self.eq('走った猫が。', '猫 = 走る')
self.eq('走る猫。', '猫 = 走る')
self.eq('やみくもに走る猫。', '猫 = 走る')
self.eq('笑う猫は走った。', '猫 = 笑う, 走る')
# うまくいかないケース
self.eq('猫は元気よく走った。', '元気, 猫 = 走る') # 「元気」が名詞扱い
self.eq('犬は走り猫は笑った。', '走り, 猫, 犬 = 笑う') # 「走り」が名詞扱い(CaboChaでも同じ)
おわりに
今回はspaCyを使って名詞に係る動詞を抽出するというプログラムを作ってみました。
このロジックを上手く使えば意味解析に発展できるかと思います。
🦝 < AIに猫の状態を把握させたりとかね
🐭 < AIの状態だよね
🐭 < AIの脳にcat_stateという変数を作っておくと
🦝 < それでこの解析でcat_stateを変化させれば……