Universal Dependenciesのもとで日本語文法に根ざした直感的な統語解析を可能にしたい。GiNZAが目指してきた自然言語処理のゴールにまた一歩近づきました。2020年8月16日にリリースした「GiNZA version 4.0」ですが、日本語の公式サポートが始まったspaCy version 2.3を土台とし、機能と性能を隅々までブラッシュアップしています。これまで以上に日本語の分析が容易になったGiNZA v4の文節APIについて詳しく解説します。
GiNZAでできること
NLP(自然言語処理)技術は人が日常的に使う言葉を機械的に分析するための一連の解析処理に用いる技術の総称です。この「一連の解析処理」という部分が非常に重要で、例えば日本語の書き言葉の文であれば、最初に単語を区切ってからそれらを文節にまとめて係り受け関係を解釈する、という流れになります。英語の文の場合、単語はほぼスペースで区切られていますが、”New York”のような複合語をまとめたり、”don’t”を”do”と”not”に分けて解釈する必要があるため、最初にトークナイズ処理を行ってから名詞句・動詞句などの基本句の認識と句の間の依存関係の解釈を行う流れになります。また言語を問わず人名・地名などの固有名詞は複合語で表現されることが多いため、それらの単語をまとめ上げて分類を与える固有表現認識処理も必要となります。
GiNZAは多言語NLPフレームワークであるspaCyを利用して、日本語の解析に必要な一連の処理の流れ(パイプライン)を実装しています。spaCyは主に西洋言語を念頭に設計・開発されたフレームワークのため、日本語の形態素解析処理は標準では組み込まれていません。spaCyは日本語モデルをインストールする際に形態素解析器のSudachiPyを同時にインストールします。GiNZAもパイプラインの先頭でSudachiPyを利用してトークン化 ( 単語区切り ) と品詞付与を行っています。
パイプラインの次のステージではspaCy標準のCNN依存構造解析器でトークン間の依存関係を決定し、文の中心となるトークンをrootとして、主語 ( nsubj ) や目的語 ( obj・iobj ) のトークンとの関係を有向グラフとして出力します。この有向グラフをNLP用語で構文木と呼びます。さらにパイプラインの次のステージではCNN固有表現認定器が入力された構文木から固有表現区間を認定し分類を付与します。ここまでの流れはspaCyの公式日本語モデルとGiNZAでほぼ同じです。
GiNZAのパイプラインの末尾にはCompoundSplitterとBunsetuRecognizerの2つの独自コンポーネントが追加されています。CompoundSplitterでは設定によりトークンを2段階に細分化して出力することが可能です。パイプライン末尾のBunsetuRecognizerは、文節のヘッドとなるトークンをマーキングする特別な依存関係ラベルを検知して、構文木から文節およびその主辞の区間を高い精度で認定します。この文節認定処理結果を取得するAPIについて次の節で説明します。
GiNZAの文節API
GiNZA v4でginzaパッケージに文節を処理単位とするAPIが多数追加されました( import ginza で利用可能)。文節APIの一覧はこちらで確認してください。ここではそのうち代表的なものを紹介します。
ginza.bunsetu_spans(doc_or_span)
引数で与えられたDocまたは文のSpanに含まれる文節区間をSpanのリストとして返します。:
# Sample code
import spacy
import ginza
nlp = spacy.load("ja_ginza")
doc = nlp("銀座でランチをご一緒しましょう。")
print(ginza.bunsetu_spans(doc)))
>> [銀座で, ランチを, ご一緒しましょう。]
ginza.bunsetu_phrase_spans(doc_or_span)
文節の主辞区間をSpanのリストを返します。:
print(ginza.bunsetu_phrase_spans(doc))
>> [銀座, ランチ, ご一緒]
bunsetu_phrase_spans()が返すSpanオブジェクトのlabelフィールドには句の区分が付与されています。:
for phrase in ginza.bunsetu_phrase_spans(doc):
print(phrase, phrase.label_)
>> 銀座 NP
>> ランチ NP
>> ご一緒 VP
ginza.bunsetu(token)
引数で洗えられたトークンが属する文節を返します。また、デフォルトでは文節に含まれるトークンのorth_フィールドを”+”で結合した文字列を返します。:
sentence = list(doc.sents)[0]
print(ginza.bunsetu(sentence.root))
>>> ご+一緒+し+ましょう+。
文節に含まれるトークンのlemma_フィールドを結合するには、次のように第2引数を与えます。:
print(ginza.bunsetu(sentence.root, ginza.lemma_))
>>> ご+一緒+する+ます+。
文節を構成するトークンのリストを取得するには、次のように引数join_funcを与えます。:
print(ginza.bunsetu(sentence.root, join_func=lambda tokens: tokens))
>>> [ご, 一緒, し, ましょう, 。]
ginza.phrase(token)
引数で洗えられたトークンが属する文節の主辞区間を返します。:
print(ginza.phrase(sentence.root))
>>> ご+一緒
その他、引数の与え方はginza.bunsetu()と同じです。
ginza.sub_phrases(token, phrase_func)
トークンが属する文節を係り先とする文節を依存関係ラベルと共に返します。phrase_funcにはginza.bunsetuまたはginza.phraseを指定します。:
for sentence in doc.sents:
for relation, bunsetu in ginza.sub_phrases(sentence.root, ginza.bunsetu):
print(relation, bunsetu)
print("root", ginza.bunsetu(sentence.root))
>> obl 銀座+で
>> obj ランチ+を
>> root ご+一緒+し+ましょう+。
文節APIの設計理念
from ginza import lemma_, bunsetu, sub_phrases
前述の bunsetu() について、次の2つの呼び出し方は完全に同じ結果となります。
- bunsetu(token, lemma_)
- bunsetu(lemma_)(token)
後者は前者をカリー化したバリエーションで、前者の第一引数tokenだけを与えていない状態の関数を生成して返します。つまり、後者は関数を戻り値に返す関数で、実際に処理結果を受け取るには追加の引数(token)を戻り値の関数に与える必要があります。カリー化は一見まどろっこしいだけのように見えるかもしれません。しかし、これにより次のようなコーディングスタイルが可能になります。
sub_phrases(token, bunsetu(lemma_))
この sub_phrases() の第二引数は bunsetu() のカリー化バージョンで、サブフレーズの取得方法の定義として使われています。
ところで、上で示したbunsetu()の引数バリエーションについて、Pythonに慣れた方には違和感があったかもしれません。Pythonは近年のバージョンで型ヒントの付与が可能なりましたが、動的型付け言語であるため多態性のような高度な型付け機能は標準では提供されていません。しかし、第一引数に限定した型ディスパッチングを提供するsingledipatchがPython標準パッケージに存在します。GiNZA v4の文節APIのいくつかはこのsingledispatchを用いて(擬似的な)カリー化を実現しています。
次のソースコードはbunsetu()の主要な実装となるtraverse()で(疑似的に)カリー化されています。 @singledispatch でアノテーションされたtravers()は戻り値としてトークンを引数とする関数を返します。対して @traverse.register(Token) でアノテーションされた_traverse()はtraverse()のバリエーションとして解釈されて、traverse()が呼び出された際に第一引数がTokenクラスのインスタンスであった場合に_traverse()に処理がdispatchされます。なお、singledispatchでregisterを行う関数の第一引数の型はクラスである必要がありますが、逆に @singledispatch でアノテーションする基本バリエーションの型指定は必須ではありません。
@singledispatch
def traverse(
traverse_func: Callable[[Token], Iterable[Token]],
element_func: Callable[[Token], T] = lambda token: token,
condition_func: Callable[[Token], bool] = lambda token: True,
join_func: Callable[[Iterable[T]], U] = lambda lst: lst,
) -> Callable[[Union[Token, Span]], U]:
return lambda token: join_func([
element_func(t) for t in traverse_func(token) if condition_func(t)
])
# overload: ex. traverse(token, children, lemma_)
@traverse.register(Token)
def _traverse(
token: Token,
traverse_func: Callable[[Token], Iterable[Token]],
element_func: Callable[[Token], T] = lambda token: token,
condition_func: Callable[[Token], bool] = lambda token: True,
join_func: Callable[[Iterable[T]], U] = lambda lst: lst,
) -> U:
return traverse(traverse_func, element_func, condition_func, join_func)(token)
文節認定精度の評価
GiNZA v4の文節主辞認定精度を評価した結果を次の表にまとめました。GiNZAは学習データとしてUD_Japanese-BCCWJ v2.6を使用していますが、このコーパスは一般公開されていないため、誰でも利用可能なUD_Japanese-GSD v2.6を用いて精度評価を行っています。GiNZA v4の学習データの依存関係ラベルは文節主辞間である場合に”_bunsetu”のsuffixをつける拡張を行うことで、解析結果の依存関係ラベルから文節主辞を同定することができます。実験結果から文節主辞の関係について、全体で96%程度の精度で同定できています。さらに、SudachiPyの形態素解析結果やspaCyの依存構造解析結果が正しい場合は、ほぼパーフェクトに文節の主辞を同定できていることがわかりました。
GiNZAのアドバンテージ
最終的にリリースされたspacy v2.3の日本語モデルはSudachiPyをトークン化に使用するなどGiNZAのアーキテクチャとの共通点が多くあります。spaCy日本語モデルは学習をシンプルにするためにSudachiPyを最も短い単位で単語を区切るモードAで使用しているのに対して、GiNZAは最も長いモードCを使用することでより具体的な意味を扱えるよう設計されています。またGiNZAが使用する学習データ ( UD_Japanese-BCCWJ ) はspaCy日本語モデルの学習データ(UD_Japanese-GSD)より数倍大きいことも、解析精度においてGiNZAにアドバンテージを与えています。
spaCy公式日本語モデル公開までの道のり
spaCyは複数言語の解析を単一ライブラリで実行できることで近年欧米言語を中心に利用者が増えているフレームワークですが、実は初期バージョンから日本語モデルの利用は可能でした。しかし、従来の日本語モデルで提供される機能はトークン化に限定されていました。日本語においては商用利用可能なオープンライセンスを持つ構文情報つきデータセット( Treebank ) が存在しなかったことが主たる原因でした。
2019年春にMegagon Labsと国立国語研究所が商用利用可能なMITライセンスのもとでGiNZAをリリースすると一気に反響が広まり、この技術分野のニーズが顕在化しました。同時期に、GiNZAプロジェクトでは、その過程で得たノウハウを活かし、spaCyに日本語依存構造解析モデルを統合するための課題を提示し働きかけを開始しました。2019年秋にはUniversal Dependencies日本語ワーキンググループが主導してUD_Japanese-GSD のライセンスからNon-commercial条項が削除され、spaCyの依存構造解析器の学習データとしてUD_Japanese-GSDを利用するための前提条件が整いましたが、産業応用においても重要な固有表現抽出を行うためには、UD_Japanese-GSDに対してその正解情報を付与する作業が必要でした。
そこで、Megagon LabsはUD Japaneseワーキンググループに参画し、UD v2.6の構築に並行して正解固有表現ラベルの付与作業を行い、2020年5月にUD_Japanese-GSD v2.6-NEのリリースを行いました。このデータセットの公開により、今後の日本語NLPのさらなる発展に寄与できることを強く願っています。
おわりに
GiNZA version 4.0は、高精度なモデルとともに日本語のテキストの分析の現場ですぐに役立つ機能を多数提供しています。Python 3.6以上を導入済みであれば `pip install -U ginza` を実行すれば今すぐ分析機能を使用することができます。Universal Dependenciesによる日本語の解析やNLPフレームワークにご興味のある方は是非GiNZAの公開ページにもアクセスしてください。
(筆者: 松田 寛 / Megagon Labs Tokyo)
Tag: GiNZA, NLP, spaCy, Universal Dependencies, 依存構造解析, 係り受け解析
References: