spaCyで文章から「私の~」を抽出する【自然言語処理, Python】
- 作成日: 2021-01-26
- 更新日: 2023-12-24
- カテゴリ: 自然言語処理
spaCyで「私の~」を抽出
私たちが使う言語は「自然言語」と呼ばれます。
この自然言語をプログラム的に解析することを「自然言語処理」と言います。
Pythonには自然言語処理ライブラリであるspaCyがあり、今回はこれを使って日本語の文章から「私の~」や「僕の~」を抽出してみたいと思います。
具体的には↓を見ていきます。
- spaCyとは?
- プログラムの設計
- プログラムの実行結果
- プログラムのソースコード
- ソースコードの解説
spaCyとは?
spaCyとはPython製の自然言語処理ライブラリです。
MITライセンスでオープンソースソフトウェアとして開発されています。
多数の自然言語の学習済みモデルを搭載していて、簡単に自然言語を統計モデルを使って解析することができます。
日本語の文章解析でspaCyと一緒によく使われるのがGiNZAです。
GiNZAはリクルートと国立国語研究所が共同開発した自然言語処理ライブラリです。
spaCyは日本語の文章の解析ではGiNZAのモデルを使用します。
つまりspaCyはGiNZAの提供する機能を使って一部の解析を行います。
プログラムの設計
今回作成するプログラムの設計についてです。
プログラムは標準入力から日本語の文章を読み取って、その入力を解析器に渡します。
そして解析器は文章の中から「私の~」や「僕の~」を抽出して文字列として返します。
解析器が返した文字列を画面に出力してまた処理先頭からループします。
入力文章の解析ではspaCyを使います。
spaCyでGiNZAのモデルをロードし、そのモデルを使って文章を解析します。
解析した結果からトークン列を読みだして、そのトークン列の中に名詞のトークンがあるか調べます。
名詞のトークンが見つかったら、その名詞に係っているトークン列を探し、そのトークン列に「私の~」が含まれているか調べ、それらのトークン列を保存します。
トークン列を保存したら、それを元に文字列を生成して、関数からreturn
します。
基本的にはこれだけの設計です。
設計上、名詞に係っていない「私の」などは抽出できなくなってます。
気になる方は改造してみてください。
プログラムの実行結果
今回作成するプログラムを実行すると↓のような結果になります。
$ python sample.py
日本語の文章を入力してください > 私の犬
私の犬
日本語の文章を入力してください > 私は猫
None
日本語の文章を入力してください > 林の中を歩く私の可愛い猫
私の猫
日本語の文章を入力してください >
プログラムを実行すると最初に「日本語の文章を入力してください >
」というプロンプトが表示されます。
このプロンプトに「私の犬」や「林の中を歩く私の可愛い猫」と入力すると、その文章から「私の~」を抽出します。
↑の例では、「私の犬」の入力から「私の犬」が抽出され、「林の中を歩く私の可愛い猫」から「私の猫」が抽出されているのがわかります。
プログラムのソースコード
今回作成したプログラムのソースコードです。
プログラムを実行するには↓のコードをsample.py
などに保存します。
そしてpython sample.py
を実行してください。
テストを実行したい場合はpython -m unittest sample
で実行します。
"""
日本語の文章から「私の~」や「僕の~」を抽出するスクリプト
License: MIT
Created at: 2021/01/26
"""
import spacy
import unittest
nlp = spacy.load('ja_ginza')
def collect_children(toks, tok):
"""
子供のトークン列を集める
"""
for t in tok.children:
toks.append(t)
collect_children(toks, t)
def find_watasi_no(toks):
"""
「代名詞 + 'の'」の組み合わせを探し、そのトークン列を返す
"""
for t in toks:
# 格表示の親が代名詞なら
if (t.dep_ == 'case' and t.text == 'の') and t.head.pos_ == 'PRON':
return [t.head, t]
return []
def grep_watashi_no(doc):
"""
docから「私の~」を抽出する
"""
for tok in doc:
if tok.pos_ == 'NOUN':
childs = []
collect_children(childs, tok)
toks = find_watasi_no(childs)
if not len(toks):
continue
toks.append(tok)
return ''.join([t.text for t in toks])
return None
def main():
while True:
try:
sentence = input('日本語の文章を入力してください > ')
except (KeyboardInterrupt, EOFError):
break
doc = nlp(sentence)
result = grep_watashi_no(doc)
print(result)
if __name__ == '__main__':
main()
class Test(unittest.TestCase):
def eq(self, a, b):
doc = nlp(a)
c = grep_watashi_no(doc)
self.assertEqual(c, b)
def test_grep(self):
# 期待通りのケース
self.eq('私の犬', '私の犬')
self.eq('私は犬', None)
self.eq('僕の猫', '僕の猫')
self.eq('僕のペルシャ猫', '僕の猫')
self.eq('僕の巨大猫', '僕の猫')
self.eq('拙者の鳥', '拙者の鳥')
self.eq('私の犬と猫', '私の犬')
self.eq('私のかわいい犬', '私の犬')
self.eq('林の中を歩いている私の犬', '私の犬')
self.eq('かわいらしい私の犬と一緒に歩く', '私の犬')
# うまく行かないケース
self.eq('相棒と私の頼りになる猫', '私の頼り')
ソースコードの解説
簡単ですがソースコードの解説になります。
必要モジュールのインポート
最初に必要モジュールをインポートします。
今回は解析用にspacy
を使うのでこれをインポートします。
それから単体テスト用にunittest
もインポートしておきます。
import spacy
import unittest
モデルのロードとnlpの作成
グローバル変数nlp
を作成します。
これはspacy
でモデルをロードした結果が入ります。
モデルはspacy.load()
でja_ginza
をロードします。
こうすることでspaCyでGiNZAのモデルを使えるようになります。
spacy.load()
の返り値はja_ginza
をロードした場合、ginza.Japanese
クラスが返ってきます。
nlp = spacy.load('ja_ginza')
main関数の作成
プログラムはmain
関数から始まります。
内容的には標準入力をinput()
で読み取って、その内容をnlp()
に渡します。
nlp()
に文章を渡すと、その文章が解析されdoc
というオブジェクトになります。
このdoc
はspacy.tokens.doc.Doc
クラスのオブジェクトです。
doc
のメソッドや属性を使うことによって、解析した結果を参照することが出来るようになります。
このdoc
をgrep_watashi_no()
関数に渡して、「私の~」を抽出します。
grep_watashi_no()
は抽出に成功すると文字列を返してくるので、その文字列をprint()
で出力してループ内の一連の処理は完了です。
if
文で__name__
を調べてmain()
を実行してます。
__name__
が__main__
の場合はコマンドラインからこのスクリプトが実行されていることを表します。
テストの実行と区別をつけるためこのif
文を書いています。
def main():
while True:
try:
sentence = input('日本語の文章を入力してください > ')
except (KeyboardInterrupt, EOFError):
break
doc = nlp(sentence)
result = grep_watashi_no(doc)
print(result)
if __name__ == '__main__':
main()
grep_watashi_no()で「私の~」を抽出
grep_watashi_no()
はdoc
から「私の~」という文章を抽出する関数です。
doc
をfor
文に渡すと、トークン列を取得できます。
そしてその中のトークンのpos_
属性を調べます。
pos_
が文字列のNOUN
だった場合、そのトークンが名詞ということになります。
spaCyは文章を解析すると、このように文章をトークン列に分割します。
トークン列と言うのは単語のリストのことです。
たとえば「私の猫」という文章だったら、「私 / の / 猫」という単語のリストに分割されます。
名詞のトークンが見つかったらcollect_children()
でそのトークンの子供のトークン列を集めます。
その集めたトークン列(childs
)をfind_watashi_no()
に渡して、「私の」や「僕の」の形のトークン列を抽出します。
そのトークン列が存在していたら、そのトークン列の末尾に名詞のトークンを追加します。
あとは''.join(...)
でトークン列内のtext
属性を参照し、1つの文字列に整形します。
トークンのtext
属性はそのトークンが持つ文章上の表示の文字列です。
def grep_watashi_no(doc):
"""
docから「私の~」を抽出する
"""
for tok in doc:
if tok.pos_ == 'NOUN':
childs = []
collect_children(childs, tok)
toks = find_watasi_no(childs)
if not len(toks):
continue
toks.append(tok)
return ''.join([t.text for t in toks])
return None
collect_children()で子トークンを集める
collect_children()
はトークンのchildren
属性を参照し、そのトークンを再帰的にtoks
に保存します。
children
属性はトークン列です。これはspaCyの依存関係構造における子要素のことです。
def collect_children(toks, tok):
"""
子供のトークン列を集める
"""
for t in tok.children:
toks.append(t)
collect_children(toks, t)
find_watashi_no()で「私の」トークン列を探す
find_watasi_no()
にトークン列を渡すと、関数はその中から「私の」の形のトークン列を返します。
見つからなかったら空のリストを返します。
「私の」にマッチする条件はトークンのdep_
属性がcase
であること、かつtext
属性が「の」であること、かつトークンの親(head
)のpos_
属性がPRON
(代名詞)であることです。
dep_
属性というのはspaCyで文章を解析したときに生成される、単語や句、節などの係り受けの関係性を表すラベルです。
このラベルがcase
であるとき、そのトークンは「格表示」のトークンになります。
トークンのpos_
属性はそのトークンの品詞を表します。
pos_
がPRON
の場合はそのトークンは名詞になります。
トークンの依存構造上における親はトークンの属性head
で参照することができます。
↓のif
文を日本語で表現すると「現在のトークンが『の』で、その親のトークンが代名詞だったらTrue」という内容になります。
つまり「私の」という文章は「の」の親のトークンが「私」なわけで、「私」の品詞は代名詞ってことになります。
def find_watasi_no(toks):
"""
「代名詞 + 'の'」の組み合わせを探し、そのトークン列を返す
"""
for t in toks:
# 格表示の親が代名詞なら
if (t.dep_ == 'case' and t.text == 'の') and t.head.pos_ == 'PRON':
return [t.head, t]
return []
テストを書く
今回はPythonのunittest
モジュールを使って簡単な単体テストを書きます。
↓がテスト内容です。
unittest
を使って単体テストを書くには↓のようにunittest.TestCase
を継承したクラスを作ります。
eq()
メソッドは引数a
とb
がイコールになるか判定します。
イコールにならなければテストに失敗します。
eq()
の内部ではnlp()
で引数a
を解析して、その結果をgrep_watashi_no()
に渡しています。
そしてgrep_watashi_no()
の返り値c
を引数b
と比較します。
比較にはTestCase
のメソッドassertEqual()
を使います。
python -m unittest sample
などでテストを実行したときに実行されるメソッドはtest_grep()
です。
test_
という接頭辞をメソッド名につけるとそのメソッドがテストされます。
class Test(unittest.TestCase):
def eq(self, a, b):
doc = nlp(a)
c = grep_watashi_no(doc)
self.assertEqual(c, b)
def test_grep(self):
# 期待通りのケース
self.eq('私の犬', '私の犬')
self.eq('私は犬', None)
self.eq('僕の猫', '僕の猫')
self.eq('僕のペルシャ猫', '僕の猫')
self.eq('僕の巨大猫', '僕の猫')
self.eq('拙者の鳥', '拙者の鳥')
self.eq('私の犬と猫', '私の犬')
self.eq('私のかわいい犬', '私の犬')
self.eq('林の中を歩いている私の犬', '私の犬')
self.eq('かわいらしい私の犬と一緒に歩く', '私の犬')
# うまく行かないケース
self.eq('相棒と私の頼りになる猫', '私の頼り')
↑のテスト内容を見ると、「期待通りのケース」ではうまくテストがいってますが、「うまく行かないケース」のテストはあんまり期待した結果になっていません。
↑の場合「相棒と私の頼りになる猫」は「私の猫」という出力を期待したいところですが、実際には出力は「私の頼り」になります。
これは「頼り」という単語が名詞扱いになるためです。
ネットの辞書でも「頼り」は名詞という扱いです。
🦝 < 「頼り」が名詞とは
🐭 < 意外やね
おわりに
今回はspaCyを使って日本語の文章から「私の~」を抽出してみました。
モノの所有関係を要約して表示できるプログラムになりましたが、今のところ使いどころは浮かびません。
🦝 < うーん、考え付いた人はコメントください
🐭 < モノの所有関係の要約ねぇ