spaCy + GiNZAで構文解析してみる
はじめに
Pythonではじめるテキストアナリティクス入門という本を読んで、spaCyとGiNZAで形態素解析や単語間の係り受け解析をやってみて、もう少しいろんなことをやってみたいと思いました。
各ツールの関係
spaCyは、Explosion AI社が開発しているオープンソースの自然言語処理ライブラリです。公式サイトのspaCy is designed specifically for production use and ... ということから製品への本格活用を想定して開発されています。
また、spaCyは、構文解析のための基本的な機能を持っており、spaCy単体で日本語の形態素解析や係り受け解析等を行うことができます。日本語だけではなく、多数の言語に対応していて、日本語用の解析モデルや英語用の解析モデルが付属しており、それらのモデルを切り替えて使用することで多数の言語に対応することができます。
GiNZAは、spaCyで使用できる日本語用の解析モデルを提供します。こちらの記事を参考に、spaCy標準のモデルとの解析精度を比較すると、GiNZAを使用するのがよさそうです。
加えて、from ginza import * として使用するライブラリも提供しており、単語に分割した後にその単語が属する文節を取得する等の機能が使用できます。
ライブラリのインストール
pip install spacy ginza ja-ginza
単語の分割
まず、単語の分割をやってみます。
import ginza
import spacy
from spacy import Language
from spacy.tokens.doc import Doc
# 解析機の初期化
natural_language_processing: Language = spacy.load("ja_ginza")
# Sudachi辞書modeをCにする
ginza.set_split_mode(natural_language_processing, "C")
# 単語に分割する対象の文章
text = "NCDC株式会社では、内製化支援も行っています。"
# 解析後の文章を取得する
# この時点で形態素解析や単語間の依存関係解析が同時に行われている
doc: Doc = natural_language_processing(text)
# 単語ごとにループ
for token in doc:
print(
f"{token}\t{token.lemma_}\t{token.pos_}\t{token.tag_}",
)
NCDC ncdc PROPN 名詞-普通名詞-一般
株式会社 株式会社 NOUN 名詞-普通名詞-一般
で で ADP 助詞-格助詞
は は ADP 助詞-係助詞
、 、 PUNCT 補助記号-読点
内製化 内製化 NOUN 名詞-普通名詞-一般
支援 支援 NOUN 名詞-普通名詞-サ変可能
も も ADP 助詞-係助詞
行っ 行く VERB 動詞-非自立可能
て て SCONJ 助詞-接続助詞
い いる VERB 動詞-非自立可能
ます ます AUX 助動詞
。 。 PUNCT 補助記号-句点
実行結果は、1列目が単語、2列目が単語の原形、3列目が品詞、4列目が詳細品詞を表しています。
品詞は、NOUNが名詞、VERBが動詞...となっており、これは、UD品詞と呼ばれるものらしいです(参考)。
また、ソースコードでSudachi辞書modeをCにするコードを入れています。これは、単語に分割する際に、できるだけ複合語で分割してほしかったからです。
もう少し詳しく説明すると、まず、GiNZAは形態素解析のために内部でSudachiPy(SudachiのPython版)を使用しています。GiNZAは、辞書としてSudachi-coreを使用しています(他に、small、fullが存在しています)。本辞書には、単語単独だけでなく、複合語も登録されています(参考)。
SudachiPyは、形態素解析するときに単語の分割モードを選択できます。単語単独で分割してほしければモードをAにしたり、複合語で分割してほしければモードをCにしたりすることができます。このモードの選択がGiNZAでできるようになっています。
ここで、単語分割の対象の文に「内製化」という言葉を入れています。形態素解析のモードをAにした場合、「内製」と「化」で分割されることになりますが、モードをCにしているため、「内製化」として分割してくれているということになります。
この後、単語の出現回数=重要度として、ある文章の中でどういう単語が重要なのかを見たいと仮定して進めていきます。その時に、「化」だけで上位に来られると困るので、このように設定したということになります。
名詞句の分割
次に、名詞句の分割をやってみます。例えば、上記では、「NCDC株式会社」が「NCDC」と「株式会社」と分割されていたり、「内製化支援」が「内製化」と「支援」に分割されています。複合語よりもさらにまとめて名詞として取得できれば、単語ごとの重要度を見る時に、単語がどのような文脈で使われたのかをよりわかりやすくできるのではないでしょうか。
from collections import Counter
import ginza
import spacy
from spacy import Language
from spacy.tokens.doc import Doc
# 解析機の初期化
natural_language_processing: Language = spacy.load("ja_ginza")
# Sudachi辞書modeをCにする
ginza.set_split_mode(natural_language_processing, "C")
# 単語に分割する対象の文章
text = "NCDC株式会社では、内製化支援も行っています。"
# 解析後の文章を取得する
# この時点で形態素解析や単語間の依存関係解析が同時に行われている
doc: Doc = natural_language_processing(text)
# 句の出現回数をカウントする
counter = Counter(chunk.text for chunk in doc.noun_chunks)
# 出現回数top 10を表示する
for chunk_text, count in counter.most_common(10):
print(f"{chunk_text}\t{count}")
NCDC株式会社 1
内製化支援 1
「NCDC株式会社」が「NCDC株式会社」として取得できたことで、表示される単語がどのようなことを表しているのかがよりわかりやすくなりました。
名詞句に係る単語も同時に取得する
次に、名詞句に係る単語も同時に取得してみます(厳密には、名詞句の主辞となる単語に係る単語を取得します)。名詞句に加えて、それがどうなのかを表す単語が同時に取得できると、よりその単語がどのような文脈で使われたのかがわかりそうです。
from collections import Counter
import ginza
import spacy
from spacy import Language
from spacy.tokens.doc import Doc
# 解析機の初期化
natural_language_processing: Language = spacy.load("ja_ginza")
# Sudachi辞書modeをCにする
ginza.set_split_mode(natural_language_processing, "C")
# 単語に分割する対象の文章
text = "NCDC株式会社では、内製化支援も行っています。"
# 解析後の文章を取得する
# この時点で形態素解析や単語間の依存関係解析が同時に行われている
doc: Doc = natural_language_processing(text)
counter = Counter()
# 名詞句(chunk)ごとにループ
for chunk in doc.noun_chunks:
counter[(chunk.text, chunk.root.head.text, chunk.root.dep_)] += 1
# 出現回数top 10を表示する
for key, count in counter.most_common(10):
print(f"{key}\t{count}")
('NCDC株式会社', '行っ', 'obl') 1
('内製化支援', '行っ', 'nsubj') 1
名詞句に係る単語も同時に取得できました。dep_("obl"や"nsubj")は、係り先となる名詞句(の主辞)とその係り元となる単語の関係を表しています。oblなら斜格名詞(主語以外の名詞)、nsubjなら主語名詞(主語と述語)を表しています。(係っていること自体は合ってそうだけど、関係性は合っているのか…?)
ただ、これだと「行っ」が「行う」なのか「行く」の意味で使われたのかが少しわかりづらいですね。
単語が属する文節を取得する
上記で名詞句の係り元となる単語として取得するとわかりづらかったので、取得した単語が属する文節を取得することで、よりわかりやすくしたいです。
from collections import Counter
import ginza
import spacy
from spacy import Language
from spacy.tokens.doc import Doc
from spacy.tokens.span import Span
# 解析機の初期化
natural_language_processing: Language = spacy.load("ja_ginza")
# Sudachi辞書modeをCにする
ginza.set_split_mode(natural_language_processing, "C")
# 単語に分割する対象の文章
text = "NCDC株式会社では、内製化支援も行っています。"
# 解析後の文章を取得する
# この時点で形態素解析や単語間の依存関係解析が同時に行われている
doc: Doc = natural_language_processing(text)
counter = Counter()
# 名詞句ごとにループ
for chunk in doc.noun_chunks:
# 名詞句の主辞の親となる単語が属する文節を取得する
head_of_bunsetsu: Span = ginza.bunsetu_span(chunk.root.head)
counter[(chunk.text, head_of_bunsetsu.text, chunk.root.dep_)] += 1
# 出現回数top 10を表示する
for key, count in counter.most_common(10):
print(f"{key}\t{count}")
('NCDC株式会社', '行っています。', 'obl') 1
('内製化支援', '行っています。', 'nsubj') 1
「行っ」が「行っています。」として取得されたことで上記よりわかりやすくなったと思います。
文節に係る文節を取得する
上記では、名詞句に係る単語が属する文節を取得していましたが、文節に係る文節を取得することもできます。
from collections import Counter
import ginza
import spacy
from spacy import Language
from spacy.tokens.doc import Doc
from spacy.tokens.span import Span
# 解析機の初期化
natural_language_processing: Language = spacy.load("ja_ginza")
# Sudachi辞書modeをCにする
ginza.set_split_mode(natural_language_processing, "C")
# 単語に分割する対象の文章
text = "NCDC株式会社では、内製化支援も行っています。"
# 解析後の文章を取得する
# この時点で形態素解析や単語間の依存関係解析が同時に行われている
doc: Doc = natural_language_processing(text)
counter = Counter()
# 文節ごとにループ
for span in ginza.bunsetu_spans(doc):
# 現在の文節の左側にある単語のうち、係り受け関係のあるものについてループ
for token in span.lefts:
counter[(
ginza.bunsetu_span(token), # 現在の単語が属する文節
span, # 現在の文節
token.dep_ 文節と文節の関係
)] = +1
(NCDC株式会社では、, 行っています。, 'obl') 1
(内製化支援も, 行っています。, 'nsubj') 1
以上です。ご覧いただきありがとうございました。
NCDC株式会社( ncdc.co.jp/ )のエンジニアチームです。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください! ※エンジニア以外も記事を投稿することがあります
Discussion
sample01.pyの実行結果が、私の手元では
となってしまいました。「行う」が正しいはずなのに「行く」になってしまいます。ちなみに、私の手元のバージョンは、ginza 5.2.0、sudachipy 0.6.8、sudachidict-core 20240409、spacy 3.7.4です。
本記事をご覧いただき、ありがとうございます。
僕のバージョンも同様でして、再度sample01.pyを実行してみたところ、たしかに安岡さんの実行結果の通りになりました。
手動で実行結果を変えたりはしていないはずなのですが…
本記事のsample01.pyの実行結果を修正しておきます。
ご指摘いただきありがとうございます!