spaCy / GiNZA を Python で使ってエンティティ分析を行う

## なに？

自然言語処理、というか、テキスト解析をする際の醍醐味である「エンティティ分析」をとっても便利な spaCy / GiNZA を使ってやってみます。
こちらが GiNZA ページです。
https://megagonlabs.github.io/ginza/

エンティティ分析とは、例えば「**プレステでFINAL FANTASY VII REMAKEをする**」と言った時に「プレステ ＝ ゲーム機」「FINAL FANTASY VII REMAKE ＝ ゲーム名」みたいなカタマリ（エンティティ）を見つける技術です。

ゲーム名の辞書を作っていくととても大変です。ゲームは無限に増えていきます。そこを前後の文脈などから推測しつつエンティティを見つけていきます。

## まず GiNZA を使ってみる

まずは GiNZA を使ってみます。
GiNZA とは簡単にいうと学習済みで必要なものがそろった日本語分析用のライブラリです。

とにかく簡単に十分に使えます。

まずさくっと pip でインストール。

```bash
pip install -U ginza
```

> 実際は pip install が成功するまで色々なところにつまづいてに謎に時間がかかったのですが……終わってしまえば実は一発で通るかもしれません。もし通らない人いましたらコメントでも……。

## サンプルコード

まず１つ目のシンプルなコードです。

```python
import spacy

nlp = spacy.load('ja_ginza')  

doc = nlp("『ファイナルファンタジーVII リメイク』は、スクウェア・エニックスから発売されたゲームソフト。PlayStation 4 で先行販売され、2021年4月までは独占タイトルとなっている。当初は2020年3月3日に全世界にて発売予定だったが、同年4月10日に発売延期された。")

print("*** token ***")
for token in doc:
    print(token.i, token.orth_, token.lemma_, token.pos_, token.tag_, token.dep_, token.head.i)

print("*** entity ***")
for ent in doc.ents:
    print(ent.text, ent.label_) 
```

結果はこんな感じです。
細かな意味はここでは省略ですが「形態素解析」などしながら各単語の分析がされているのがわかると思います。

```tex
*** token ***
0 『 『 PUNCT 補助記号-括弧開 punct 4
1 ファイナル ファイナル NOUN 名詞-普通名詞-一般 compound 4
2 ファンタジー ファンタジー NOUN 名詞-普通名詞-一般 compound 4
3 VII vii NOUN 名詞-普通名詞-一般 compound 4
4 リメイク リメーク NOUN 名詞-普通名詞-サ変可能 ROOT 4
5 』 』 PUNCT 補助記号-括弧閉 punct 4
6 は は ADP 助詞-係助詞 case 4
7 、 、 PUNCT 補助記号-読点 punct 4
8 スクウェア・エニックス スクウェア・エニックス PROPN 名詞-固有名詞-一般 compound 10
  ：（省略）

*** entity ***
ファイナルファンタジーVII リメイク Book
スクウェア・エニックス Person
PlayStation 4 Product_Other
2021年4月 Date
2020年3月3日 Date
同年4月10日 Date
```

なかなか良い感じです。

token としていわゆる形態素解析が綺麗に行われていて、スクウェア・エニックス も１つの固有名詞として認識されてます。

今回やりたかった エンティティ も「ファイナルファンタジーVII リメイク」が認識されています。 Book という単語にはちょっと違和感ありますが……そこは一般的な辞書データなので仕方なく、「同年4月10日」といったちょっとトリッキーな日付の書き方でも Date として認識してくれています。一般的な単語を取り出したいならこれで十分でしょう。

## カスタム辞書を作る

とはいえ、、、やっぱり「**ファイナルファンタジーVII リメイク**」は「**Game_Title**」とかにしたいこともあります。

実際のお仕事とかで自然言語処理をする場合には、自分たちのビジネスドメイン毎に専門用語があると思います。例えば、題名は題名として扱いたいです。それをどうにかしてみたいと思います。

今度は自分用に学習したいので GiNZA `ja_ginza` ではなく spaCy の素の `ja` で学習していきます。
コードは Spacy のサンプルコードほぼそのままですがこんな感じになります。

```Python
from __future__ import unicode_literals, print_function

import plac
import random
from pathlib import Path
import spacy
from spacy.util import minibatch, compounding

# new entity label
LABEL = "Game_Title"

TRAIN_DATA = [
    (
        "『ファイナルファンタジーVII リメイク』は、スクウェア・エニックスから発売されたゲームソフト。",
        {"entities": [(1, 20, LABEL)]}
    ),
    (
        "『ファイナルファンタジーVII リメイク』のリメイク作品の公式サイトです。",
        {"entities": [(1, 20, LABEL)]}
    ),
    (
        "ファイナルファンタジーVII リメイク - PS4がゲームストアでいつでもお買い得。",
        {"entities": [(0, 19, LABEL)]}
    )
]

random.seed(0)
nlp = spacy.blank("ja")
ner = nlp.create_pipe("ner")
nlp.add_pipe(ner)
ner.add_label(LABEL)
optimizer = nlp.begin_training()
pipe_exceptions = ["ner", "trf_wordpiecer", "trf_tok2vec"]
other_pipes = [pipe for pipe in nlp.pipe_names if pipe not in pipe_exceptions]
with nlp.disable_pipes(*other_pipes):
    for itn in range(30):
        random.shuffle(TRAIN_DATA)
        losses = {}
        batches = minibatch(TRAIN_DATA, size=compounding(1.0, 4.0, 1.001))
        for batch in batches:
            texts, annotations = zip(*batch)
            nlp.update(texts, annotations, sgd=optimizer, drop=0.35, losses=losses)
        print("Losses", losses)

print()

test_text = "『ファイナルファンタジーVII リメイク』に続いて『ファイナルファンタジーII』も！"
doc = nlp(test_text)
print("Entities in '%s'" % test_text)
for ent in doc.ents:
    print(ent.text, ent.label_) 

output_dir = Path(r"適当なフォルダ名")
nlp.meta["name"] = "GameTitleModel"
nlp.to_disk(output_dir)
print("Saved model to", output_dir)
```

結果はこんな感じです。とりあえず　「ファイナルファンタジーVII リメイク Game_Title」 と認識されました。
※もちろんこんなに少ないトレーニングデータじゃダメですが試す分には十分です

```tex
Losses {'ner': 36.54436391592026}
Losses {'ner': 28.74292328953743}
Losses {'ner': 16.96098183095455}
   :
Entities in '『ファイナルファンタジーVII リメイク』に続いて『ファイナルファンタジーII』も！'
ファイナルファンタジーVII リメイク Game_Title
ファイナルファンタジーII Game_Title
```

とりあえずできました。
学習させていない「ファイナルファンタジーII」も Game_Title になってくれてますね。


ということで、すみません、ちょっと粗削りなので公開後にもちょこちょことアップデートしていきます。