spaCy/GiNZAを使って日本語文書のカテゴリ分類を学習する（失敗）

この記事は[ユニークビジョン株式会社 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/uniquevision)の9日目の記事です。

# はじめに

本記事は[spaCyのCLIで文書のカテゴリ分類を学習する](https://qiita.com/kyamamoto9120/items/84d62c3b33fb77c03fbe)の続編です。
前回は英語の文書を扱いましたが、本記事では日本語文書を対象に、spaCyのCLIを用いてカテゴリ分類の学習を行います。

# 前提条件
前回同様、実行環境にはgoogle ColabのGPUランタイムを使用しており、GPUはTesla P100です。
また、ライブラリのバージョンは

* ginza 2.2.1
* spacy 2.2.1

です。
GiNZAは以下の手順でインストールしておきます。

```console
$ pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"
```

今回はGoogle Colabで動かすため、以下のおまじないも実行します。

```python
import pkg_resources, imp
imp.reload(pkg_resources)
```


データセットにはライブドアニュースコーパスを使用します。
事前に、以下のようにしてデータをダウンロードしておきます。

```console
$ wget https://www.rondhuit.com/download/ldcc-20140209.tar.gz
$ tar -xvf ldcc-20140209.tar.gz
```

# データの準備
データ構造は前回と同様のため、保存処理は変わりません。

```python
import spacy
import srsly
from spacy.gold import docs_to_json

def save_to_json(model, data, targets, target_names, output_file, n_texts=0):
  def get_categories(target):
    return dict([(key, int(target == i)) for i, key in enumerate(target_names)])

  nlp = spacy.load(model)
  nlp.disable_pipes(*nlp.pipe_names)
  sentencizer = nlp.create_pipe("sentencizer")
  nlp.add_pipe(sentencizer, first=True)

  docs = []
  count = 0
  for i, doc in enumerate(nlp.pipe(data)):
    doc.cats = get_categories(targets[i])
    docs.append(doc)

    if n_texts > 0 and count == n_texts:
      break
    count += 1

  srsly.write_json(output_file, [docs_to_json(docs)])
  return count
```

この関数を利用できるようにデータを読み込みます。

```python
from pathlib import Path

data = []
targets = []
target_names = []

for target, target_name in enumerate([p for p in Path('text').iterdir() if p.is_dir()]):
  target_names.append(target_name.name)
  for news in target_name.iterdir():
    if 'LICENSE' in news.name:
      continue
    with open(news) as f:
      s = '\n'.join(f.read().splitlines()[2:])
    data.append(s)
    targets.append(target)
```

ファイルには以下のように保存します。

```python
from sklearn.model_selection import train_test_split

X_train, X_dev, y_train, y_dev = train_test_split(data, targets, test_size=0.20, random_state=42)

save_to_json(
    'ja_ginza',
    X_train,
    y_train,
    target_names,
    'train.json'
)

save_to_json(
    'ja_ginza',
    X_dev,
    y_dev,
    target_names,
    'dev.json'
)
```

前回の記事と違って、モデルに`ja_ginza`を指定している点に注意です。
なお、この方法でJSONファイルを作成すると非ASCII文字がUnicodeエスケープされて出力されますが、読み込む際によしなにしてくれるので問題ありません。

# 学習
前回同様に学習を行うと以下のようなエラーが出ます。

```console
$ !time python -m spacy train ja output train.json dev.json -v ja_ginza -p textcat -ta simple_cnn -g 0
Training pipeline: ['textcat']
Starting with blank model 'ja'
Loading vector from model 'ja_ginza'
Traceback (most recent call last):
  File "/usr/lib/python3.6/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/usr/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/usr/local/lib/python3.6/dist-packages/spacy/__main__.py", line 33, in <module>
    plac.call(commands[command], sys.argv[1:])
  File "/usr/local/lib/python3.6/dist-packages/plac_core.py", line 328, in call
    cmd, result = parser.consume(arglist)
  File "/usr/local/lib/python3.6/dist-packages/plac_core.py", line 207, in consume
    return cmd, self.func(*(args + varargs + extraopts), **kwargs)
  File "/usr/local/lib/python3.6/dist-packages/spacy/cli/train.py", line 213, in train
    _load_vectors(nlp, vectors)
  File "/usr/local/lib/python3.6/dist-packages/spacy/cli/train.py", line 530, in _load_vectors
    util.load_model(vectors, vocab=nlp.vocab)
  File "/usr/local/lib/python3.6/dist-packages/spacy/util.py", line 162, in load_model
    return load_model_from_link(name, **overrides)
  File "/usr/local/lib/python3.6/dist-packages/spacy/util.py", line 179, in load_model_from_link
    return cls.load(**overrides)
  File "/usr/local/lib/python3.6/dist-packages/spacy/data/ja_ginza/__init__.py", line 12, in load
    return load_model_from_init_py(__file__, **overrides)
  File "/usr/local/lib/python3.6/dist-packages/spacy/util.py", line 228, in load_model_from_init_py
    return load_model_from_path(data_path, meta, **overrides)
  File "/usr/local/lib/python3.6/dist-packages/spacy/util.py", line 211, in load_model_from_path
    return nlp.from_disk(model_path)
  File "/usr/local/lib/python3.6/dist-packages/spacy/language.py", line 941, in from_disk
    util.from_disk(path, deserializers, exclude)
  File "/usr/local/lib/python3.6/dist-packages/spacy/util.py", line 654, in from_disk
    reader(path / key)
  File "/usr/local/lib/python3.6/dist-packages/spacy/language.py", line 936, in <lambda>
    p, exclude=["vocab"]
  File "nn_parser.pyx", line 665, in spacy.syntax.nn_parser.Parser.from_disk
  File "nn_parser.pyx", line 77, in spacy.syntax.nn_parser.Parser.Model
  File "/usr/local/lib/python3.6/dist-packages/spacy/_ml.py", line 323, in Tok2Vec
    return _legacy_tok2vec.Tok2Vec(width, embed_size, **kwargs)
  File "/usr/local/lib/python3.6/dist-packages/spacy/ml/_legacy_tok2vec.py", line 44, in Tok2Vec
    glove = StaticVectors(pretrained_vectors, width, column=cols.index(ID))
  File "/usr/local/lib/python3.6/dist-packages/thinc/neural/_classes/static_vectors.py", line 43, in __init__
    vectors = self.get_vectors()
  File "/usr/local/lib/python3.6/dist-packages/thinc/neural/_classes/static_vectors.py", line 55, in get_vectors
    return get_vectors(self.ops, self.lang)
  File "/usr/local/lib/python3.6/dist-packages/thinc/extra/load_nlp.py", line 26, in get_vectors
    nlp = get_spacy(lang)
  File "/usr/local/lib/python3.6/dist-packages/thinc/extra/load_nlp.py", line 14, in get_spacy
    SPACY_MODELS[lang] = spacy.load(lang, **kwargs)
  File "/usr/local/lib/python3.6/dist-packages/spacy/__init__.py", line 30, in load
    return util.load_model(name, **overrides)
  File "/usr/local/lib/python3.6/dist-packages/spacy/util.py", line 169, in load_model
    raise IOError(Errors.E050.format(name=name))
OSError: [E050] Can't find model 'ja_nopn.vectors'. It doesn't seem to be a shortcut link, a Python package or a valid path to a data directory.

real	0m1.610s
user	0m1.299s
sys	0m0.394s
```

エラーメッセージを読むと`_load_vectors`に問題があるらしいので、以下のコードで状況の再現を試みます。

```python
from spacy.util import get_lang_class, load_model
lang_cls = get_lang_class('ja')
nlp = lang_cls()
load_model('ja_ginza', vocab=nlp.vocab)
```

すると、これは成功してしまいます。
ちょっとここで力尽きたので、ひとまず空のモデルから学習をしてみることにします。

# 空のモデルからの学習
上記のコマンドから`-v ja_ginza`を外すだけです。

```console
$ !time python -m spacy train ja output train.json dev.json -p textcat -ta simple_cnn -g 0
Training pipeline: ['textcat']
Starting with blank model 'ja'
Counting training words (limit=0)
tcmalloc: large alloc 2128887808 bytes == 0x629bc000 @  0x7f7744def1e7 0x5acd6b 0x7f773a41a5db 0x7f773a41abf0 0x7f773a41ae36 0x7f773a4185c1 0x50ac25 0x50c5b9 0x7f76e032ab20 0x7f76e032f98f 0x7f76e03226c5 0x7f76e0371c47 0x7f76e31d372a 0x7f76e035efce 0x7f76e31d372a 0x7f76e038b1e7 0x7f76e31d372a 0x7f76e0352148 0x7f76e035c24b 0x59509c 0x54a8a5 0x551b81 0x5aa6ec 0x50abb3 0x50d390 0x508245 0x589471 0x5a067e 0x50d966 0x508245 0x50a080
Textcat evaluation score: F1-score macro-averaged across the labels 'peachy,
smax, it-life-hack, sports-watch, movie-enter, livedoor-homme, dokujo-tsushin,
kaden-channel, topic-news'

Itn  Textcat Loss  Textcat  Token %  CPU WPS  GPU WPS
---  ------------  -------  -------  -------  -------
  1      1312.086   84.850   99.995    32519   119886
  2       181.481   89.898   99.995    32879   119157
  3       119.681   91.490   99.995    32741   122003
  4        87.429   92.677   99.995    32618   119988
  5        66.618   92.674   99.995    32005   122189
  6        45.137   92.484   99.995    32293   113857
```

どうやら学習できているらしいのですが、enに比べて非常に重いです。
気軽に学習させてみるという感じでは無いレベルの重さです。

# まとめ
spaCyのCLIを用いて日本語文書のカテゴリ分類を試しました。
結論だけ言えば失敗です。

まずはベースモデルの読み込みと重い問題を解決しなければならないかなと考えています。