【Python】GiNZA: 日本語自然言語処理オープンソースライブラリ

## はじめに
3年前に大学の研究で自然言語処理に使った自然言語フレームが[**GiNZA**](https://megagonlabs.github.io/ginza/)でした。
どこまでアップデートされたか興味があるので、調べていきます。
仕組みも論文で説明されているので興味があったら[公式サイト](https://www.megagon.ai/jp/projects/ginza-install-a-japanese-nlp-library-in-one-step/)から閲覧してみてください！！

## GiNZA
日本語自然言語処理フレームワークで形態素解析器として用いります。日本語の解析処理、依存構造（係り受け）解析や固有表現抽出などをすることができます。
また、GiNZAは自然言語処理フレームワークの**spaCy**と形態素解析器の**SudachiPy**の2つの基盤技術を利用しています。そのため、spaCyと併用して使用することができます。
その他にも様々なプロジェクトが稼働しているようで、その一つとして、[**HappyDB**](https://www.megagon.ai/jp/projects/happydb-a-happiness-database-of-100000-happy-moments/)という不特定多数の人々の協力から成る10万件の幸福な瞬間を収集したDBを作られているようです。

## インストール
インストールする種類がいくつかあったので紹介します。

### 最新のGiNZAおよびTransformersモデル
以下のコマンドを実行することで最新のGiNZAと付随してTransformersモデルがインストールされます。
```
pip install -U ginza ja_ginza_electra
```
<font color="Red">※インストールに失敗した場合は、以下の2つの工程が必要な場合があります。
・[RustのToolchain](https://rustup.rs/)のインストール。説明が長くなるので、説明が分かりやすかったこちらの[ブログ](https://blog1.mammb.com/entry/2021/02/27/104504)を参考にしてみてください。
・Pythonのパッケージのインストール。`pip install setuptools_rust`コマンドの実行。</font>
### 大容量モデルファイルを含めた最新のGiNZAおよびTransformersモデル
以下のコマンドを実行することで、先ほどの最新のGiNZAとTransformersモデルに加え大容量モデルファイルがインストールされます。
```
pip install -U ginza https://github.com/megagonlabs/ginza/releases/download/latest/ja_ginza_electra-latest-with-model.tar.gz
```

### GPUを使うtransformersモデル
GPUを利用してtransformersモデルを高速に実行するには、実行環境のCUDAバージョンを指定してspacyを上書きインストールします。


```ruby:CUDA 11.0を使用する場合
pip install -U "spacy[cuda110]"
```
あわせてpytorchもCUDAと整合したバージョンをインストールする必要があります。

コマンドを実行して最新のGiNZAと従来型モデルをインストールします。
```
$ pip install -U ginza ja_ginza
```


## デモ
コマンドラインから実行する方法とPythonコードから実行する方法の2種類があるのですが、今回はPythonコードからのデモをいたします。

### Transformersモデルによる依存構造解析結果を文単位で出力
出力結果を以下の[CoNLL-U Syntactic Annotation](https://universaldependencies.org/format.html#syntactic-annotation) 形式で解析結果が出力される。
1. ID：単語インデックス、新しい文ごとに1から始まる整数。マルチワードトークンの範囲である可能性があります。空のノードの場合は10進数にすることができます（10進数は1未満にすることができますが、0より大きくする必要があります）。
1. FORM：単語の形式または句読記号。
1. 補題：補題または語幹。
1. UPOS：ユニバーサル品詞タグ。
1. XPOS：言語固有の品詞タグ。利用できない場合は下線を付けます。
1. FEATS：ユニバーサル機能インベントリまたは定義された言語固有の拡張機能からの形態学的機能のリスト。利用できない場合は下線を付けます。
1. HEAD：現在の単語の先頭。IDの値またはゼロ（0）のいずれかです。
1. DEPREL：HEAD（HEAD = 0の場合はルート）または定義された言語固有のサブタイプ1へのユニバーサル依存関係。
1. DEPS：ヘッドとデプレルのペアのリスト形式の拡張依存関係グラフ。
1. MISC：その他の注釈。

**サンプルコード**
```ruby:python
import spacy

nlp = spacy.load('ja_ginza_electra')
doc = nlp('銀座でランチをご一緒しましょう。')
for sent in doc.sents:
    for token in sent:
        print(
            token.i,
            token.orth_,
            token.lemma_,
            token.norm_,
            token.morph.get("Reading"),
            token.pos_,
            token.morph.get("Inflection"),
            token.tag_,
            token.dep_,
            token.head.i,
        )
    print('EOS')
```
出力結果
```ruby:実行結果
1 で で で ['デ'] ADP [] 助詞-格助詞 case 0
2 ランチ ランチ ランチ ['ランチ'] NOUN [] 名詞-普通名詞-一般 obj 5
3 を を を ['ヲ'] ADP [] 助詞-格助詞 case 24 ご ご 御 ['ゴ'] NOUN [] 接頭辞 compound 5
5 一緒 一緒 一緒 ['イッショ'] VERB [] 名詞-普通名詞-サ変可能 ROOT 5
6 し する 為る ['シ'] AUX ['サ行変格;連用形-一般'] 動詞-非自立可能 aux 5
7 ましょう ます ます ['マショウ'] AUX ['助動詞-マス;意志推量形'] 助動詞 aux 5
8 。 。 。 ['。'] PUNCT [] 補助記号-句点 punct 5
EOS
```

### GiNza v4モデルの文節やその主辞を単位とした分析
サンプルコードから、青空文庫にある「吾輩は猫である」のテキストからルビを抜いたテキストを分析。

サンプルコード
```ruby:python
from ginza import *
import spacy
nlp = spacy.load("ja_ginza")  # GiNZAモデルの読み込み

from collections import defaultdict
frames = defaultdict(lambda: 0)  # 依存関係の出現頻度を格納
sentences = set()  # 重複文検出用のset

with open("吾輩は猫である.txt", "r", encoding='utf-8') as fin:  # 解析対象のテキストファイルから
  for line in fin.readlines():  # 一行ごとに
    try:
      doc = nlp(line.rstrip())  # 解析を実行し
    except:
      continue
    for sent in doc.sents:  # 文単位でループ
      if sent.text in sentences:
        continue  # 重複文はスキップ
      sentences.add(sent.text)
      for t in bunsetu_head_tokens(sent):  # 文節主辞トークンのうち
        if t.pos_ not in {"ADJ", "VERB"}:
          continue  # 述語以外はスキップ
        v = phrase(lemma_)(t)  # 述語とその格要素(主語・目的語相当)の句を集める
        dep_phrases = sub_phrases(t, phrase(lemma_), is_not_stop)
        subj = [phrase for dep, phrase in dep_phrases if dep in {"nsubj"}]
        obj  = [phrase for dep, phrase in dep_phrases if dep in {"obj", "iobj"}]
        for s in subj:
          for o in obj:
            frames[(s, o, v)] += 1  # 格要素と述語の組み合わせをカウント

for frame, count in sorted(frames.items(), key=lambda t: -t[1]):
  print(count, *frame, sep="\t")  # 出現頻度の高い順に表示
```

実行結果は出現頻度が１の場合が多いのでそれ以外を表示します
```ruby:python
9       主人    顔      する
6       主人    声      出す
5       主人    頭      撫でる
4       主人    返事    する
3       細君    顔      する
3       主人    事      云う
3       迷亭    返事    する
3       吾輩    休養    要する
2       彼      昼寝    する
2       主人    筆      執る
2       主人    方      見る
2       主人    念      押す
2       主人    生返事  する
2       人      英語    知る
2       主人    鼻毛    抜く
2       主人    欠伸    する
2       迷亭    事      云う
2       主人    迷亭    見る
2       細君    返事    する
2       令嬢    剣突    食う
2       主人    質問    かける
2       妹      真似    する
2       主人    口      開く
2       主人    挨拶    する
2       君      お辞儀  する
2       大分県  宙返り  する
2       東風+君 顔      する
2       東風+君 質問    かける
```

## GiNZAの文節APIデモ
文節APIの中で代表的なものを選出。
### ginza.bunsetu_spans(doc_or_span)
引数で与えられたDocまたは文のSpanに含まれる文節区間をSpanのリストとして返します。
```ruby:python
# 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のリストを返し、for文で Span オブジェクトを取得すると label フィールドには句の区分が付与されています。
```ruby:python
print(ginza.bunsetu_phrase_spans(doc))

>> [銀座, ランチ, ご一緒]

for phrase in ginza.bunsetu_phrase_spans(doc):
  print(phrase, phrase.label_)

>> 銀座 NP
>> ランチ NP
>> ご一緒 VP
```

### ginza.bunsetu(token)
引数で洗えられたトークンが属する文節を返します。また、デフォルトでは文節に含まれるトークンのorth_フィールドを”+”で結合した文字列を返します。

```ruby:python
sentence = list(doc.sents)[0]
print(ginza.bunsetu(sentence.root))

>>> ご+一緒+し+ましょう+。
```
文節に含まれるトークンのlemma_フィールドを結合するには、次のように第2引数を与えます。
```ruby:python
print(ginza.bunsetu(sentence.root, ginza.lemma_))

>>> ご+一緒+する+ます+。
```
文節を構成するトークンのリストを取得するには、次のように引数join_funcを与えます。
```ruby:python
print(ginza.bunsetu(sentence.root, join_func=lambda tokens: tokens))

>>> [ご, 一緒, し, ましょう, 。]
```


### ginza.phrase(token)
引数で洗えられたトークンが属する文節の主辞区間を返します。
```ruby:python
print(ginza.phrase(sentence.root))

>>> ご+一緒
```

### ginza.sub_phrases(token, phrase_func)
トークンが属する文節を係り先とする文節を依存関係ラベルと共に返します。phrase_funcにはginza.bunsetuまたはginza.phraseを指定します。
```ruby:python
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 ご+一緒+し+ましょう+。
```

## GiNZA v4で追加された文節APIの一覧
公式で載っていた最新のGiNZA v4 で追加された文節API一覧の表を載せる ⇒　[参考先](https://megagonlabs.github.io/ginza/bunsetu_api.html)
| カテゴリー | 関数 or 変数 | 概要 |
|:-:|:-:|:-:|
| Span-based | bunsetu_spans() | 文節SpanのIterable。 |
|   | bunsetu_phrase_spans() | 文節主辞SpanのIterable。 |
|   | bunsetu_span() | トークンが属する文節のSpan。 |
|   | bunsetu_phrase_span() | トークンが属する文節の主辞Span。 |
| Construction | bunsetu() | 文節中のトークン列を指定された形に整形して返す。 |
|   | phrase() | 文節主辞中のトークン列を指定された形に整形して返す。 |
|   | sub_phrases() | 従属文節を指定された形に整形して返す。 |
|   | phrases() | スパンに含まれる文節を指定された形に整形して返す。 |
| Utility | traverse() | 構文木を指定された方法で巡回し指定された形に整形して返す。 |
|   | default_join_func() | デフォルトのトークン列の結合方法。 |
|   | SEP | デフォルトのトークン区切り文字。 |
| Token-based | bunsetu_head_list() | DocやSpanに含まれる文節のヘッドトークンのインデックスのリスト。 |
|   | bunsetu_head_tokens() | DocやSpanに含まれる文節のヘッドトークンのリスト。 |
|   | bunsetu_bi_labels() | DocやSpanに含まれるトークンが文節開始位置にある場合は”B”、それ以外は”I”とするリスト。 |
|   | bunsetu_position_types() | DocやSpanに含まれるトークンを{“ROOT”,“SEM_HEAD”, “SYN_HEAD”, “NO_HEAD”,“FUNC”, “CONT”}に分類したリスト。 |
|   | is_bunsetu_head() | トークンが文節のヘッドの場合はTrue、それ以外はFalse。 |
|   | bunsetu_bi_label() | トークンが文節開始位置にある場合は”B”、それ以外は”I”。 |
|   | bunsetu_position_type() | トークンを{“ROOT”, “SEM_HEAD”,“SYN_HEAD”, “NO_HEAD”, “FUNC”,“CONT”}に分類。 |
| Proxy | * | spacy.tokens.Tokenクラスのプロパティと同名・同機能の関数群。 |
| Subtoken | sub_tokens() | トークンの分割情報。 |
|   | 	set_split_mode() | 	デフォルトの分割モードの変更。 |


## まとめ
三年前に研究テーマで感情分析の形態素解析器で新しく出たGiNZAを使っていました。今回もですがインストールに少し手間取ったことが懐かしいです。
様々なプロジェクトが稼働していたり、Transformersモデルがあったりと変わっていることはなかなかありました。ちょっと昔に使ったものが現在どうなっているか確認するのも案外楽しいものがありました。
自然言語処理に興味を持っていたら、ぜひ、使ってみてください。
ではまた次回に。。