spaCyでNERの訓練をする

　spaCyでフィンランド語のNERをしたので記録しておく。使用しているのはspaCy ver. 3.1である。

## フィンランド語NLPの状況
　spaCyにフィンランド語のモデルはない（[モデル一覧](https://spacy.io/usage/models)）。Antti Ajanki氏が非公式に[フィンランド語のspaCy用のモデル](https://github.com/aajanki/spacy-fi)を公開している。しかし，このモデルには固有表現抽出（NER）が含まれていない。精度が悪いため入れていないと書かれている。

　他方，[フィンランド語のNER用のデータセット](https://github.com/TurkuNLP/turku-ner-corpus)がTurku大学によって公開されている（LREC 2020）。従って，spaCyをこのデータセットで訓練すればspaCyのパイプラインでNERができるはずである。

## spaCyに学習させる

### データ形式
　spaCyは，訓練時には.spacy形式しか入力として受け付けない。この形式に変換する機能も同時に提供されているので，それを用いる。[spaCyのドキュメント](https://spacy.io/api/cli#convert)によれば，以下のコマンドで変換できる。

```shell
$ python -m spacy convert <input_file> <output_dir> --converter <入力ファイルの形式>
```

　入力ファイルの形式は，以下に対応している。

|引数|形式|説明|
|--|--|--|
|`json`|spaCy2.xで使われていた形式||
|`conllu`|UDで使われる.connlu形式|[CoNLL-U形式](https://universaldependencies.org/format.html)である。|
|`ner`|NERで使われるCoNLL形式|`-DOCSTART-`で始まるCoNLLのデータがそのまま入力できる。|
|`iob`||自分で作るならこれが楽。1文1行で，各単語はスペース区切り。各単語は，「word&#124;pos&#124;BIO」または「word&#124;BIO」の形式で記述される。|

　今回使うデータセットはCoNLL形式だったので，`ner`を指定すれば何ら前処理の必要はなかった。また，上述のコマンドを使うと，`-n 10`を指定するように推奨される（どうなるのかよくわからない）。

### 設定ファイルの作成
　spaCyの訓練では，コマンドに設定を書くのではなく，設定ファイルに設定を書くことになっている（その方が後から分かりやすいから）。設定ファイルは，[ここ](https://spacy.io/usage/training#quickstart)で簡単に作れる。これに入出力を若干書き足す。今回はNERのみなので，以下のようになった（GPUありにしている（GPUだから速いということは無いが，BERTを使っているようなので精度は良いかも））。

```yaml
# This is an auto-generated partial config. To use it with 'spacy train'
# you can run spacy init fill-config to auto-fill all default settings:
# python -m spacy init fill-config ./base_config.cfg ./config.cfg
[paths]
train = "turku-ner-corpus/data/conll/converted/train.spacy" # ←ここを追加
dev = "turku-ner-corpus/data/conll/converted/dev.spacy" # ←ここを追加
vectors = "spacy_fi_experimental_web_md" # ←なければ空欄でも動くっぽい。今回は先述のフィンランド語のspaCyモデルを指定

[system]
gpu_allocator = "pytorch"

[nlp]
lang = "fi"
pipeline = ["transformer","ner"]
batch_size = 128

[components]

[components.transformer]
factory = "transformer"

[components.transformer.model]
@architectures = "spacy-transformers.TransformerModel.v1"
name = "TurkuNLP/bert-base-finnish-cased-v1"
tokenizer_config = {"use_fast": true}

[components.transformer.model.get_spans]
@span_getters = "spacy-transformers.strided_spans.v1"
window = 128
stride = 96

[components.ner]
factory = "ner"

[components.ner.model]
@architectures = "spacy.TransitionBasedParser.v2"
state_type = "ner"
extra_state_tokens = false
hidden_width = 64
maxout_pieces = 2
use_upper = false
nO = null

[components.ner.model.tok2vec]
@architectures = "spacy-transformers.TransformerListener.v1"
grad_factor = 1.0

[components.ner.model.tok2vec.pooling]
@layers = "reduce_mean.v1"

[corpora]

[corpora.train]
@readers = "spacy.Corpus.v1"
path = ${paths.train}
max_length = 0

[corpora.dev]
@readers = "spacy.Corpus.v1"
path = ${paths.dev}
max_length = 0

[training]
accumulate_gradient = 3
dev_corpus = "corpora.dev"
train_corpus = "corpora.train"

[training.optimizer]
@optimizers = "Adam.v1"

[training.optimizer.learn_rate]
@schedules = "warmup_linear.v1"
warmup_steps = 250
total_steps = 20000
initial_rate = 5e-5

[training.batcher]
@batchers = "spacy.batch_by_padded.v1"
discard_oversize = true
size = 2000
buffer = 256

[initialize]
vectors = ${paths.vectors}
```

この設定ファイルは，いくつか項目を省略している（デフォルト値があるため）。しかし，spaCyの訓練用設定ファイルでは，デフォルト値が決まっていても省略が許されない（その方が後から分かりやすいので）。そこで，省略された部分を補う機能が提供されている。

```shell
$ python -m spacy init fill-config <上述の設定ファイル> <出力する設定ファイル>
```

### 学習させる
　以下のコマンドで学習が開始される。

#### GPUを使用

```shell
python -m spacy train <設定ファイル> --output <出力先ディレクトリ> --gpu-id 0
```
※GPUのIDは必要に応じて変更する

　GPUを使う場合はGPU対応版のspaCyを入れておく必要がある（`pip install spacy[cuda]`などとする）。また，内部ではtransformersを使うので，`pytorch`と`transformers`は必須である。

#### CPUを使用

```shell
python -m spacy train <設定ファイル> --output <出力先ディレクトリ> 
```

## できあがったモデルを読み込む

　`--output`で指定したディレクトリの内部に，`model-best`と`model-last`という2つのディレクトリが作られる。このいずれもがspaCyのモデルである。これを使用するためには，以下のようにする。

```python
import spacy

nlp=spacy.load('foobar/model-last')
```

　できあがったモデルはNERの訓練しかしていないのでNERしかできない。これはこれで不便で，なんとかならないものか・・・