Jubatusの文書分類をゴリゴリチューニングしてみた(COTOHA API)

## はじめに
[以前、Jubatusでやった文書分類](https://qiita.com/TkrUdagawa/items/a0749b770c8d4474d63b)をいろいろチューニングしてみました。
チューニングで試すものは以下の通り。順番に試していって、精度が上がったものを採用していく方針でやっていきます。

- Jubatusで頑張る
    - 学習回数を増やす
    - 特徴抽出を頑張る
        - 辞書を変える
        - 重みづけを変える
        - 形態素n-gram使ってみる
    - アルゴリズムとハイパーパラメータを変えてみる
- Jubatus以外の特徴を混ぜる
    - COTOHA APIの分析結果を入れてみる

サンプルコードは[こちら](https://github.com/TkrUdagawa/sample_codes/blob/master/livedoor/livedoor-tuning-sample.ipynb)

## ベースライン

はじめに問題設定のおさらいとベースラインとなる精度を測ります。

問題設定は[livedoor ニュースコーパス](https://www.rondhuit.com/download.html#ldcc)で、記事のタイトルのみを使って各記事のカテゴリ(9種類)を推定する問題です。タイトル以外の情報は使わないという制約でやっていきます。

ベースラインとしてJubatus で MeCabの形態素解析のみを使い、交差検証、ホールドアウト検証をした場合の精度を測ります。

<details>
<summary>コード:</summary><div>

```python

import os
import pandas as pd
import random
import json
from jubatus.common import Datum
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.metrics import classification_report, confusion_matrix
from embedded_jubatus import Classifier

# データを読み込んでdataframeを作る
categories = [f for f in os.listdir("text") if os.path.isdir(os.path.join("text", f))]
print(categories)
articles = []
for c in categories:
    articles = articles + [(c, os.path.join("text", c, t)) for t in os.listdir(os.path.join("text", c)) if t != "LICENSE.txt"]
df = pd.DataFrame(articles, columns=["target", "data"])

# datumのリストを作成しておく
datum_list = []
for d in df["data"]:
    dt = Datum()
    with open(d) as f:
        l = f.readlines()
        doc = l[2].rstrip()
        dt.add_string("title", doc) # Datumにテキストデータを追加
    datum_list.append(dt)

# 訓練用、テスト用にデータセットをわける
X_train, X_test, y_train, y_test = train_test_split(df["data"], df["target"], random_state=42, stratify=df["target"])
num_splits = 4

# 交差検証の準備
kf = StratifiedKFold(n_splits=num_splits, random_state=42, shuffle=True)


# Jubatusの準備
config = {"converter" : {
        "string_filter_types" : {},
        "string_filter_rules" : [],
        "num_filter_types" : {},
        "num_filter_rules" : [],
        "string_types": {
                "mecab": {
                    "method": "dynamic",
                    "path": "libmecab_splitter.so",
                    "function": "create",
                    "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
                    "ngram": "1",
                    "base": "true",
                    "include_features": "*",
                    "exclude_features": ""
                }
        },
        "string_rules" : [
            { "key" : "*", "type" : "mecab", "sample_weight" : "bin", "global_weight" : "bin" }
        ],
        "num_types" : {},
        "num_rules" : [
            { "key" : "*", "type" : "num" }
        ]
    },
    "parameter" : {
        "regularization_weight" : 1.0
    },
    "method" : "AROW"
}
cl = Classifier(config)

# 交差検証実行用の関数
def do_cv(cl, n=3):
    random.seed(42)
    y_cv_results = []
    for fold, indexes in enumerate(kf.split(X_train.index, y_train)):
        cl.clear()
        train_index, test_index = indexes

        # (ラベル, Datum)のリストを作る
        training_data = [(df["target"][X_train.index[i]], datum_list[X_train.index[i]]) for i in train_index]

        # Jubatusに学習させる
        for i in range(n):
            cl.train(training_data)

        test_data = [datum_list[X_train.index[i]] for i in test_index]

        # Jubatusに分類させる
        result = cl.classify(test_data)

        # 分類スコアが最大のラベルを予測結果として取り出す
        y_pred = [max(x, key=lambda y:y.score).label  for x in result]

        # 正解を取り出す
        y = [df["target"][X_train.index[i]] for i in test_index]

        y_cv_results.append([y, y_pred])
    y_sum = []
    y_pred_sum = []
    for y, y_pred in y_cv_results:
        y_sum.extend(y)
        y_pred_sum.extend(y_pred)
    print(classification_report(y_sum, y_pred_sum, digits=4))
    print(confusion_matrix(y_sum, y_pred_sum))

# ホールドアウト検証実行用の関数
def do_holdout(cl, n):
    random.seed(42)
    training_data = [(df["target"][i], datum_list[i]) for i in X_train.index]
    test_data = [datum_list[i] for i in X_test.index]
    y_true = [df["target"][i] for i in X_test.index]

    for i in range(n):
        cl.train(training_data)
    result = cl.classify(test_data)
    y_pred = [max(x, key=lambda y:y.score).label  for x in result]

    print(classification_report(y_true=y_true, y_pred=y_pred, digits=4))

do_cv(cl, 1)
do_holdout(cl, 1)
```
</div>
</details>

```
交差検証
                precision    recall  f1-score   support

dokujo-tsushin     0.7581    0.8267    0.7909       652
  it-life-hack     0.8193    0.8469    0.8328       653
 kaden-channel     0.9074    0.9074    0.9074       648
livedoor-homme     0.8345    0.6188    0.7106       383
   movie-enter     0.7791    0.8006    0.7897       652
        peachy     0.7244    0.6820    0.7025       632
          smax     0.9104    0.9509    0.9302       652
  sports-watch     0.9050    0.8326    0.8673       675
    topic-news     0.7500    0.8304    0.7882       578

   avg / total     0.8218    0.8203    0.8192      5525


ホールドアウト検証
                precision    recall  f1-score   support

dokujo-tsushin     0.7214    0.8670    0.7875       218
  it-life-hack     0.8515    0.8986    0.8744       217
 kaden-channel     0.9439    0.9352    0.9395       216
livedoor-homme     0.9231    0.6562    0.7671       128
   movie-enter     0.8106    0.8440    0.8270       218
        peachy     0.8085    0.7238    0.7638       210
          smax     0.9067    0.9358    0.9210       218
  sports-watch     0.8815    0.8267    0.8532       225
    topic-news     0.8103    0.8229    0.8165       192

   avg / total     0.8481    0.8436    0.8430      1842
```

いろいろ数値は出ますが、モデルの汎化性能を保つため、交差検証のf1-scoreのtotalを指標にチューニングしていきます。スタートは `0.8192` です。

## Jubatusで頑張る

### 繰り返し学習

Jubatusで使用しているアルゴリズムはオンライン学習のものが大半です。オンライン学習は与えられたデータを逐次学習していくため、データの学習順序によってできるモデルが異なります。そのため、同じデータでも複数回学習させると精度が向上する場合があります。
まずは学習回数を増やすとどうなるか確かめてみます。

```python
do_cv(cl, 2)
do_cv(cl, 3)
do_cv(cl, 4)
do_cv(cl, 5)
```

結果:

```
2回
   avg / total     0.8329    0.8324    0.8316      5525

3回
   avg / total     0.8358    0.8351    0.8345      5525

4回
   avg / total     0.8351    0.8340    0.8337      5525

5回
   avg / total     0.8347    0.8337    0.8334      5525
```

3回目まで精度の向上が見られ、0.8345まで精度が上がりました。
繰り返し学習は効果があるようなので採用です。
今回は3回目が最大となりましたが、今後特徴量が増えたりアルゴリズムを変えた場合には回数を増減させて精度を測っていきます。

### 辞書を変える

形態素解析に用いる辞書をJubatusデフォルトのものから[Neologd] (https://github.com/neologd/mecab-ipadic-neologd)に変えてみます。インストールはNeologdの公式サイトを参考にしてください。ソースコードの変更点は、Jubatusのconfigで辞書を指定するディレクトリのみです。
私の環境ではhomeディレクトリのlocal/lib以下にインストールしてあるため、そこを指定します。

```diff

config = {"converter" : {
        "string_filter_types" : {},
        "string_filter_rules" : [],
        "num_filter_types" : {},
        "num_filter_rules" : [],
        "string_types": {
                "mecab": {
                    "method": "dynamic",
                    "path": "libmecab_splitter.so",
                    "function": "create",
-                   "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/mecab-ipadic/",
+                   "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/mecab-ipadic-neologd/",
                    "ngram": "1",
                    "base": "true",
                    "include_features": "*",
                    "exclude_features": ""
                }
        },
        "string_rules" : [
            { "key" : "*", "type" : "mecab", "sample_weight" : "bin", "global_weight" : "bin" }
        ],
        "num_types" : {},
        "num_rules" : [
            { "key" : "*", "type" : "num" }
        ]
    },
    "parameter" : {
        "regularization_weight" : 1.0
    },
    "method" : "AROW"
}
cl = Classifier(config)
```

結果:

```
   avg / total     0.8245    0.8221    0.8217      5525
```

Neologdを入れて繰り返し学習も5回まで試しましたが、若干精度が落ちたため不採用です。


### n-gramを使う

Jubatusでは連続するn個の形態素を抽出する形態素n-gramを使うことができます。形態素n-gramもJubatusのコンフィグファイルに変更を加えるだけで使うことができます。下記のように `string_types` 中で `ngram` の数字を変更した特徴抽出方法を作成し、 `string_rules` でそれを適用する設定を入れます。

<details><summary>bi-gram</summary>
<div>

```diff

config = {"converter" : {
        "string_filter_types" : {},
        "string_filter_rules" : [],
        "num_filter_types" : {},
        "num_filter_rules" : [],
        "string_types": {
                "mecab": {
                    "method": "dynamic",
                    "path": "libmecab_splitter.so",
                    "function": "create",
                    "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
                    "ngram": "1",
                    "base": "true",
                    "include_features": "*",
                    "exclude_features": ""
                },
+           "mecab-bi": {
+                   "method": "dynamic",
+                   "path": "libmecab_splitter.so",
+                   "function": "create",
+                   "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
+                   "ngram": "2",
+                  "base": "true",
+                   "include_features": "*",
+                   "exclude_features": ""                
+           }
        },
        "string_rules" : [
            { "key" : "*", "type" : "mecab", "sample_weight" : "bin", "global_weight" : "bin" },
+           { "key" : "*", "type" : "mecab-bi", "sample_weight" : "bin", "global_weight" : "bin" }
        ],
        "num_types" : {},
        "num_rules" : [
            { "key" : "*", "type" : "num" }
        ]
    },
    "parameter" : {
        "regularization_weight" : 1.0
    },
    "method" : "AROW"
}
```

</div></details>

<details><summary>tri-gram</summary>
<div>

```diff

config = {"converter" : {
        "string_filter_types" : {},
        "string_filter_rules" : [],
        "num_filter_types" : {},
        "num_filter_rules" : [],
        "string_types": {
                "mecab": {
                    "method": "dynamic",
                    "path": "libmecab_splitter.so",
                    "function": "create",
                    "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
                    "ngram": "1",
                    "base": "true",
                    "include_features": "*",
                    "exclude_features": ""
                },
+           "mecab-bi": {
+                   "method": "dynamic",
+                   "path": "libmecab_splitter.so",
+                   "function": "create",
+                   "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
+                   "ngram": "2",
+                  "base": "true",
+                   "include_features": "*",
+                   "exclude_features": ""                
+           },
+           "mecab-tri": {
+                   "method": "dynamic",
+                   "path": "libmecab_splitter.so",
+                   "function": "create",
+                   "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
+                   "ngram": "3",
+                  "base": "true",
+                   "include_features": "*",
+                   "exclude_features": ""                
+           }
        },
        "string_rules" : [
            { "key" : "*", "type" : "mecab", "sample_weight" : "bin", "global_weight" : "bin" },
+           { "key" : "*", "type" : "mecab-bi", "sample_weight" : "bin", "global_weight" : "bin" },
+           { "key" : "*", "type" : "mecab-tri", "sample_weight" : "bin", "global_weight" : "bin" }


        ],
        "num_types" : {},
        "num_rules" : [
            { "key" : "*", "type" : "num" }
        ]
    },
    "parameter" : {
        "regularization_weight" : 1.0
    },
    "method" : "AROW"
}
```

</div></details>
交差検証の結果は以下のようになりました。

```
bi-gram追加
   avg / total     0.8492    0.8456    0.8458      5525

tri-gramも追加
   avg / total     0.8482    0.8424    0.8427      5525
```

bi-gramの追加は精度の向上に寄与しているので採用します。一方tri-gramまで入れると精度が下がってしまったので、不採用とします。

### 重みづけの変更

これまで、抽出した特徴量はすべて等しく重みを `1`として学習させていました。しかし、実際には非常に特徴的な特徴にはより大きな重みを、全ての文書に現れて分類に寄与しないような特徴には小さな重みを与えることで、精度があがることがあります。Jubatusでは TF-IDF, Okapi BM25 の2種類の重みづけが使えます。

変更点は下記のように `string_rules` のなかで `sample_weight` と `global_weight` の設定を変更します。 `sample_weight` は `tf` に `global_weight` は TF-IDF利用の場合は `idf`, BM25 利用の場合は
`bm25`を指定します。

<details><summary>tf-idf</summary>
<div>

```diff

config = {"converter" : {
        "string_filter_types" : {},
        "string_filter_rules" : [],
        "num_filter_types" : {},
        "num_filter_rules" : [],
        "string_types": {
                "mecab": {
                    "method": "dynamic",
                    "path": "libmecab_splitter.so",
                    "function": "create",
                    "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
                    "ngram": "1",
                    "base": "true",
                    "include_features": "*",
                    "exclude_features": ""
                },
            "mecab-bi": {
                    "method": "dynamic",
                    "path": "libmecab_splitter.so",
                    "function": "create",
                    "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
                    "ngram": "2",
                    "base": "true",
                    "include_features": "*",
                    "exclude_features": ""                
            }
        },
        "string_rules" : [
-           { "key" : "*", "type" : "mecab", "sample_weight" : "bin", "global_weight" : "bin" },
+           { "key" : "*", "type" : "mecab", "sample_weight" : "tf", "global_weight" : "idf" },
-           { "key" : "*", "type" : "mecab-bi", "sample_weight" : "bin", "global_weight" : "bin" }
+           { "key" : "*", "type" : "mecab-bi", "sample_weight" : "tf", "global_weight" : "idf" }
        ],
        "num_types" : {},
        "num_rules" : [
            { "key" : "*", "type" : "num" }
        ]
    },
    "parameter" : {
        "regularization_weight" : 1.0
    },
    "method" : "AROW"
}
```
</div></details>

<details><summary>BM25</summary>
<div>

```diff

config = {"converter" : {
        "string_filter_types" : {},
        "string_filter_rules" : [],
        "num_filter_types" : {},
        "num_filter_rules" : [],
        "string_types": {
                "mecab": {
                    "method": "dynamic",
                    "path": "libmecab_splitter.so",
                    "function": "create",
                    "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
                    "ngram": "1",
                    "base": "true",
                    "include_features": "*",
                    "exclude_features": ""
                },
            "mecab-bi": {
                    "method": "dynamic",
                    "path": "libmecab_splitter.so",
                    "function": "create",
                    "arg": "-d /home/TkrUdagawa/local/lib/mecab/dic/ipadic",
                    "ngram": "2",
                    "base": "true",
                    "include_features": "*",
                    "exclude_features": ""                
            }
        },
        "string_rules" : [
-           { "key" : "*", "type" : "mecab", "sample_weight" : "bin", "global_weight" : "bin" },
+           { "key" : "*", "type" : "mecab", "sample_weight" : "tf", "global_weight" : "bm25" },
-           { "key" : "*", "type" : "mecab-bi", "sample_weight" : "bin", "global_weight" : "bin" }
+           { "key" : "*", "type" : "mecab-bi", "sample_weight" : "tf", "global_weight" : "bm25" }
        ],
        "num_types" : {},
        "num_rules" : [
            { "key" : "*", "type" : "num" }
        ]
    },
    "parameter" : {
        "regularization_weight" : 1.0
    },
    "method" : "AROW"
```
</div></details>
交差検証結果は下記のようになりました。

```
tf-idf
   avg / total     0.8487    0.8454    0.8455      5525

bm25
   avg / total     0.8471    0.8452    0.8450      5525
```

bm25は6回学習まで精度が向上しましたが、0.8450で打ち止めでした。どちらの手法も精度は向上しなかったため、不採用とします。

### パラメータチューニング

アルゴリズムとパラメータのチューニングをやってみます。
真面目にやるならすべてのアルゴリズムを細かくパラメータ調整しながらやっていくべきですが、時間と労力の関係で `CW`, `AROW` の2つのアルゴリズムをいろいろパラメータ振って試してみました。
`CW`, `AROW` はそれぞれ `regularization_weight` というパラメータを調整できます。
決め打ちで 0.01, 0.1, 0.5, 1.0, 10.0 の5種類の値を試してみます。

```
CW/0.01
   avg / total     0.8402    0.8371    0.8360      5525

CW/0.1
   avg / total     0.8466    0.8449    0.8437      5525

CW/0.5 
   avg / total     0.8541    0.8525    0.8510      5525

CW/1.0
   avg / total     0.8540    0.8525    0.8508      5525

CW/10.0
   avg / total     0.8375    0.8376    0.8351      5525

AROW/0.01
   avg / total     0.8438    0.8402    0.8394      5525

AROW/0.1
   avg / total     0.8506    0.8471    0.8469      5525

AROW/0.5
   avg / total     0.8492    0.8460    0.8460      5525

AROW/1.0
   avg / total     0.8492    0.8456    0.8458      5525

AROW/10.0
   avg / total     0.8482    0.8445    0.8447      5525
```

上記の通り `CW` で `regularization_weight` を0.5にした時が最大となりました。

## Jubatus以外の特徴追加

ここまでJubatusだけを使ってできることのみでチューニングをしてきて、交差検証の精度が0.8192 から 0.8510 まで精度を向上させることができました。ここから更にダメ押しで精度をあげるためにJubatusの外で特徴を作り、追加することを試みていきます。

### COTOHA API

NTTコミュニケーションズが公開している自然言語処理のAPI。
構文解析や固有表現抽出、キーワード抽出などの7種類の日本語処理のAPIを利用できます。
Developerユーザなら誰でも無料で登録できてAPIを使うことができるようです。
詳しくは公式サイトや関連記事を参照してください。

- [Communication Engine "COTOHA® API"](https://www.ntt.com/business/services/application/ai/cotoha-nlp.html)
- [自然言語処理を簡単に扱えると噂のCOTOHA APIをPythonで使ってみた]
(https://qiita.com/gossy5454/items/83072418fb0c5f3e269f)

#### データの収集

COTOHA APIのDeveloperユーザは、無料ではあるのですが1日のリクエスト数に制限があります。
分析用のデータセットをつくるために数日にわけてリクエストを投げてはデータを保存するよう作業を行いました。。。
今回は構文解析と固有表現抽出も使ったため、さらに時間がかかりました。。。

COTOHA APIを実行するコードは以下の通りです。

```python

import requests
import json
import os

# 下記の情報はCOTOHA API Portalにログインすると確認できます。
CLIENT_SECRET = "CLIENT SECRETを入れる"
CLIENT_ID = "CLIENT IDを入れる"
TOKEN_URL = "TOKEN_URLを入れる"
API_BASE = "API_BASEを入れる"

def  get_token():
    """トークン認証を行う
    """
    headers = {
        "Content-Type": "application/json",
        "charset": "UTF-8"
    }
    data = {
        "grantType": "client_credentials",
        "clientId": CLIENT_ID,
        "clientSecret": CLIENT_SECRET
    }
    r = requests.post(TOKEN_URL, headers=headers, data=json.dumps(data))
    return r.json()


def parse(text, token):
    """構文解析を実行する
    """
    headers = {
        "Content-Type": "application/json",
        "charset": "UTF-8",
        "Authorization": "Bearer {}".format(token)
    }
    data = {
        "sentence": text,
        "type": "default"
    }
    r = requests.post(API_BASE + "v1/parse", headers=headers, data=json.dumps(data))
    if r.json()["status"] != 0:
        print(r.json()["status"], text)
    return r.json()


def ne(text, token):
    """固有表現抽出を行う
    """
    headers = {
        "Content-Type": "application/json",
        "charset": "UTF-8",
        "Authorization": "Bearer {}".format(token)
    }
    data = {
        "sentence": text,
        "type": "default",
        "dic_type": []
    }
    r = requests.post(API_BASE + "v1/ne", headers=headers, data=json.dumps(data))
    if r.json()["status"] != 0:
        print(r.json()["status"], text)
    return r.json()

TOKEN = get_token()["access_token"]
text = "週末映画まとめ読み】 『モテキ』初登場2位でトップ3を邦画が独占＜10月1日号＞"
print(json.dumps(parse(text, TOKEN), indent=2, ensure_ascii=False))
print(json.dumps(ne(text, TOKEN), indent=2, ensure_ascii=False))
```

<details><summary>構文解析結果</summary>
<div>

```json

{
  "result": [
    {
      "chunk_info": {
        "id": 0,
        "head": 5,
        "dep": "D",
        "chunk_head": 1,
        "chunk_func": 1,
        "links": []
      },
      "tokens": [
        {
          "id": 0,
          "form": "【",
          "kana": "",
          "lemma": "【",
          "pos": "括弧",
          "features": [
            "開括弧"
          ],
          "attributes": {}
        },
        {
          "id": 1,
          "form": "週末",
          "kana": "シュウマツ",
          "lemma": "週末",
          "pos": "名詞",
          "features": [
            "時",
            "連用"
          ],
          "dependency_labels": [
            {
              "token_id": 0,
              "label": "punct"
            }
          ],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 1,
        "head": 2,
        "dep": "D",
        "chunk_head": 0,
        "chunk_func": 0,
        "links": []
      },
      "tokens": [
        {
          "id": 2,
          "form": "映画",
          "kana": "エイガ",
          "lemma": "映画",
          "pos": "名詞",
          "features": [],
          "dependency_labels": [],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 2,
        "head": 3,
        "dep": "A",
        "chunk_head": 1,
        "chunk_func": 1,
        "links": [
          {
            "link": 1,
            "label": "other"
          }
        ]
      },
      "tokens": [
        {
          "id": 3,
          "form": "まとめ",
          "kana": "マトメ",
          "lemma": "まとめ",
          "pos": "名詞",
          "features": [],
          "attributes": {}
        },
        {
          "id": 4,
          "form": "読み",
          "kana": "ヨミ",
          "lemma": "読み",
          "pos": "名詞",
          "features": [],
          "dependency_labels": [
            {
              "token_id": 2,
              "label": "nmod"
            },
            {
              "token_id": 3,
              "label": "compound"
            },
            {
              "token_id": 5,
              "label": "punct"
            },
            {
              "token_id": 6,
              "label": "punct"
            }
          ],
          "attributes": {}
        },
        {
          "id": 5,
          "form": "】",
          "kana": "",
          "lemma": "】",
          "pos": "括弧",
          "features": [
            "閉括弧"
          ],
          "attributes": {}
        },
        {
          "id": 6,
          "form": " ",
          "kana": "",
          "lemma": " ",
          "pos": "空白",
          "features": [],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 3,
        "head": 4,
        "dep": "D",
        "chunk_head": 1,
        "chunk_func": 1,
        "links": [
          {
            "link": 2,
            "label": "other"
          }
        ]
      },
      "tokens": [
        {
          "id": 7,
          "form": "『",
          "kana": "",
          "lemma": "『",
          "pos": "括弧",
          "features": [
            "開括弧"
          ],
          "attributes": {}
        },
        {
          "id": 8,
          "form": "モテキ",
          "kana": "モテキ",
          "lemma": "モテキ",
          "pos": "名詞",
          "features": [],
          "dependency_labels": [
            {
              "token_id": 4,
              "label": "nmod"
            },
            {
              "token_id": 7,
              "label": "punct"
            },
            {
              "token_id": 9,
              "label": "punct"
            }
          ],
          "attributes": {}
        },
        {
          "id": 9,
          "form": "』",
          "kana": "",
          "lemma": "』",
          "pos": "括弧",
          "features": [
            "閉括弧"
          ],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 4,
        "head": 5,
        "dep": "D",
        "chunk_head": 0,
        "chunk_func": 0,
        "links": [
          {
            "link": 3,
            "label": "other"
          }
        ]
      },
      "tokens": [
        {
          "id": 10,
          "form": "初",
          "kana": "ハツ",
          "lemma": "初",
          "pos": "冠名詞",
          "features": [],
          "dependency_labels": [
            {
              "token_id": 8,
              "label": "dep"
            }
          ],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 5,
        "head": 6,
        "dep": "D",
        "chunk_head": 0,
        "chunk_func": 0,
        "links": [
          {
            "link": 0,
            "label": "time"
          },
          {
            "link": 4,
            "label": "other"
          }
        ]
      },
      "tokens": [
        {
          "id": 11,
          "form": "登場",
          "kana": "トウジョウ",
          "lemma": "登場",
          "pos": "名詞",
          "features": [
            "動作"
          ],
          "dependency_labels": [
            {
              "token_id": 1,
              "label": "nmod"
            },
            {
              "token_id": 10,
              "label": "dep"
            }
          ],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 6,
        "head": 10,
        "dep": "D",
        "chunk_head": 1,
        "chunk_func": 2,
        "links": [
          {
            "link": 5,
            "label": "other"
          }
        ]
      },
      "tokens": [
        {
          "id": 12,
          "form": "2",
          "kana": "ニ",
          "lemma": "2",
          "pos": "Number",
          "features": [],
          "attributes": {}
        },
        {
          "id": 13,
          "form": "位",
          "kana": "イ",
          "lemma": "位",
          "pos": "助数詞",
          "features": [],
          "dependency_labels": [
            {
              "token_id": 11,
              "label": "nmod"
            },
            {
              "token_id": 12,
              "label": "compound"
            },
            {
              "token_id": 14,
              "label": "cop"
            }
          ],
          "attributes": {}
        },
        {
          "id": 14,
          "form": "で",
          "kana": "デ",
          "lemma": "で",
          "pos": "判定詞",
          "features": [
            "連用"
          ],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 7,
        "head": 8,
        "dep": "D",
        "chunk_head": 0,
        "chunk_func": 0,
        "links": []
      },
      "tokens": [
        {
          "id": 15,
          "form": "トップ",
          "kana": "トップ",
          "lemma": "トップ",
          "pos": "名詞",
          "features": [],
          "dependency_labels": [],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 8,
        "head": 10,
        "dep": "D",
        "chunk_head": 0,
        "chunk_func": 1,
        "links": [
          {
            "link": 7,
            "label": "time"
          }
        ]
      },
      "tokens": [
        {
          "id": 16,
          "form": "3",
          "kana": "サン",
          "lemma": "3",
          "pos": "Number",
          "features": [],
          "dependency_labels": [
            {
              "token_id": 15,
              "label": "nmod"
            },
            {
              "token_id": 17,
              "label": "case"
            }
          ],
          "attributes": {}
        },
        {
          "id": 17,
          "form": "を",
          "kana": "ヲ",
          "lemma": "を",
          "pos": "格助詞",
          "features": [
            "連用"
          ],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 9,
        "head": 10,
        "dep": "D",
        "chunk_head": 0,
        "chunk_func": 1,
        "links": []
      },
      "tokens": [
        {
          "id": 18,
          "form": "邦画",
          "kana": "ホウガ",
          "lemma": "邦画",
          "pos": "名詞",
          "features": [],
          "dependency_labels": [
            {
              "token_id": 19,
              "label": "case"
            }
          ],
          "attributes": {}
        },
        {
          "id": 19,
          "form": "が",
          "kana": "ガ",
          "lemma": "が",
          "pos": "格助詞",
          "features": [
            "連用"
          ],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 10,
        "head": 11,
        "dep": "D",
        "chunk_head": 0,
        "chunk_func": 0,
        "links": [
          {
            "link": 6,
            "label": "other"
          },
          {
            "link": 8,
            "label": "object"
          },
          {
            "link": 9,
            "label": "agent"
          }
        ],
        "predicate": []
      },
      "tokens": [
        {
          "id": 20,
          "form": "独占",
          "kana": "ドクセン",
          "lemma": "独占",
          "pos": "名詞",
          "features": [
            "動作"
          ],
          "dependency_labels": [
            {
              "token_id": 13,
              "label": "nmod"
            },
            {
              "token_id": 16,
              "label": "dobj"
            },
            {
              "token_id": 18,
              "label": "nsubj"
            }
          ],
          "attributes": {}
        }
      ]
    },
    {
      "chunk_info": {
        "id": 11,
        "head": -1,
        "dep": "O",
        "chunk_head": 2,
        "chunk_func": 2,
        "links": [
          {
            "link": 10,
            "label": "other"
          }
        ]
      },
      "tokens": [
        {
          "id": 21,
          "form": "<",
          "kana": "",
          "lemma": "<",
          "pos": "Symbol",
          "features": [],
          "attributes": {}
        },
        {
          "id": 22,
          "form": "10月1日",
          "kana": "ジュウガツイチニチ",
          "lemma": "10月1日",
          "pos": "名詞",
          "features": [
            "日時"
          ],
          "attributes": {}
        },
        {
          "id": 23,
          "form": "号",
          "kana": "ゴウ",
          "lemma": "号",
          "pos": "名詞",
          "features": [],
          "dependency_labels": [
            {
              "token_id": 20,
              "label": "nmod"
            },
            {
              "token_id": 22,
              "label": "compound"
            },
            {
              "token_id": 21,
              "label": "compound"
            },
            {
              "token_id": 24,
              "label": "punct"
            }
          ],
          "attributes": {}
        },
        {
          "id": 24,
          "form": ">",
          "kana": "",
          "lemma": ">",
          "pos": "Symbol",
          "features": [],
          "attributes": {}
        }
      ]
    }
  ],
  "status": 0,
  "message": ""
}
```

</div></details>

<details><summary>固有表現抽出結果</summary>
<div>

```json

{
  "result": [
    {
      "begin_pos": 13,
      "end_pos": 16,
      "form": "モテキ",
      "std_form": "モテキ",
      "class": "ART",
      "extended_class": "",
      "source": "basic"
    },
    {
      "begin_pos": 34,
      "end_pos": 39,
      "form": "10月1日",
      "std_form": "10月1日",
      "class": "DAT",
      "extended_class": "",
      "source": "basic"
    },
    {
      "begin_pos": 20,
      "end_pos": 22,
      "form": "2位",
      "std_form": "2位",
      "class": "NUM",
      "extended_class": "Rank",
      "source": "basic"
    },
    {
      "begin_pos": 23,
      "end_pos": 27,
      "form": "トップ3",
      "std_form": "トップ3",
      "class": "NUM",
      "extended_class": "Rank",
      "source": "basic"
    }
  ],
  "status": 0,
  "message": ""
}
```

</div>
</details>

構文解析では、形態素の情報とそれの文法的な役割や特殊な特徴などを抽出できます。
固有表現抽出では、固有名詞や数量表現など特別な意味を持つ表現を抽出することができます。

これらの分析結果を以下のようなディレクトリ構造で保存しています。
それぞれの記事カテゴリの下に分析結果のjsonファイルが格納されています。

```
├── ne_title
│   ├── dokujo-tsushin
│   ├── it-life-hack
│   ├── kaden-channel
│   ├── livedoor-homme
│   ├── movie-enter
│   ├── peachy
│   ├── smax
│   ├── sports-watch
│   └── topic-news
└── parse_title
    ├── dokujo-tsushin
    ├── it-life-hack
    ├── kaden-channel
    ├── livedoor-homme
    ├── movie-enter
    ├── peachy
    ├── smax
    ├── sports-watch
    └── topic-news
```

これらのデータから、構文解析結果の形態素のlemma(標準形)と固有表現を抽出するために以下のような関数を用意しました。

<details><summary>特徴抽出関数</summary>
<div>

```python

def get_tokens(result):
    tokens = []
    for r in result:
        for t in r["tokens"]:
            tokens.append(t)
    return tokens

def make_datum_list_with_cotoha(df, add_lemma=False,
                                add_ne_form=False, ne_filter=[]):
    datum_list = []
    for d in df["data"]:
        dt = Datum()
        with open(d) as f:
            l = f.readlines()
            doc = l[2].rstrip()
            dt.add_string("title", doc) # Datumにテキストデータを追加
    
        parse_file = d.replace("text", "parse_title").replace("txt", "json")
        ne_file = d.replace("text", "ne_title").replace("txt", "json")
        with open(parse_file) as f, open(ne_file) as ne:
            j = json.load(f)
            ne_j = json.load(ne)
            tokens = get_tokens(j["result"])
            
            # 固有表現を入れる
            for r in ne_j["result"]:
                if add_ne_form:
                    if ne_filter:
                        if r["class"] in ne_filter:
                            dt.add_number("ne-{}".format(r["form"]), 1.0)                            
                    else:
                        dt.add_number("ne-{}".format(r["form"]), 1.0)

            # token情報からlemmaを取得
            for r in j["result"]:
                for t in r["tokens"]:
                    k = "lemma-{}".format(t["lemma"])
                    v = 1.0
                    if add_lemma:
                        dt.add_number(k, v)
        datum_list.append(dt)
    print(len(datum_list))
    return datum_list
```

</div></details>

### lemmaの追加

COTOHA APIとMeCabでは形態素解析の結果が結構違うようです。
[(参考) COTOHA APIとMeCabの比較](https://qiita.com/Haruki314/items/799fa25173f2edf92982)

COTOHA APIの方が長めに形態素を構築してくれる傾向があるようなので、より特徴的な言葉を拾って分類精度が向上するかもしれません。

lemma情報を追加して3回学習で交差検証を行ったところ下記のような結果となりました。

```python

datum_list = make_datum_list_with_cotoha(df, add_lemma=True)
do_cv(cl, 3) # jubatusは CW:0.5 で動作

   avg / total     0.8548    0.8530    0.8516      5525
```

少し精度が上がったので、採用とします。

### 固有表現の追加

固有表現とは人名や地名、数量表現など特定の意味を持つ表現のことです。
COTOHA APIにはそういった固有表現を抽出する機能があり、これは形態素をまたいだ単位で文字列を抜き出してくれるので、今までと違った特徴を作ってくれる可能性があります。
COTOHA APIで取れる固有表現は[こちら](https://api.ce-cotoha.com/contents/reference-detail.html#ne-class-list)の通りです。
日付や時刻を抜き出しても分類にはあまり意味がなさそうなので、今回は`組織名`、`人名`、`場所`、`固有物名`を抜き出すようにします。

```python

datum_list = make_datum_list_with_cotoha(
    df, add_lemma=True,
    add_ne_form=True, ne_filter=set(["ORG", "PSN", "LOC", "ART"]))

do_cv(cl, 3) # jubatusは CW:0.5 で動作

   avg / total     0.8550    0.8534    0.8518      5525
```

こちらも少しですが精度が向上したので採用します。


### アルゴリズム選択とパラメータチューニング
特徴量が増えたところで再度アルゴリズムとハイパーパラメータを選択します。

```
AROW 0.01
   avg / total     0.8505    0.8485    0.8474      5525
AROW 0.1
   avg / total     0.8543    0.8516    0.8513      5525
AROW 0.5
    avg / total     0.8526    0.8501    0.8499      5525
AROW 1.0
   avg / total     0.8547    0.8523    0.8522      5525
AROW 10.0
   avg / total     0.8528    0.8500    0.8498      5525

CW 0.01
   avg / total     0.8426    0.8393    0.8379      5525
CW 0.1
   avg / total     0.8522    0.8509    0.8495      5525
CW 0.5
   avg / total     0.8553    0.8538    0.8522      5525
CW 1.0
   avg / total     0.8538    0.8521    0.8506      5525
CW 10.0
   avg / total     0.8467    0.8460    0.8439      5525
```

AROWの1.0とCWの0.5が並びました。学習回数をさらに増やすとCWの方が伸びたのでこちらを採用します。
最終的に学習回数を増やしていくとCWは以下のような精度となりました。

```
3回学習
   avg / total     0.8553    0.8538    0.8522      5525

4回学習
   avg / total     0.8578    0.8565    0.8551      5525

5回学習
   avg / total     0.8582    0.8567    0.8553      5525

6回学習
   avg / total     0.8590    0.8576    0.8563      5525

7回学習
   avg / total     0.8595    0.8581    0.8569      5525

8回学習
   avg / total     0.8588    0.8577    0.8564      5525
```

7回学習で作成したモデルが最も精度が良く、0.8569まで精度が伸びました。
このモデルで最後にホールドアウト検証を行います。

```python

do_holdout(cl, 7)

               precision    recall  f1-score   support

dokujo-tsushin     0.8201    0.8991    0.8578       218
  it-life-hack     0.8879    0.9124    0.9000       217
 kaden-channel     0.9807    0.9398    0.9598       216
livedoor-homme     0.9362    0.6875    0.7928       128
   movie-enter     0.8369    0.8945    0.8647       218
        peachy     0.8534    0.7762    0.8130       210
          smax     0.8921    0.9862    0.9368       218
  sports-watch     0.9163    0.8756    0.8955       225
    topic-news     0.8492    0.8802    0.8645       192

   avg / total     0.8841    0.8817    0.8806      1842
```


##まとめ

Jubatus のみでのチューニング

```
0.8192 
-> 繰り返し学習: 0.8345 
-> 形態素bi-gram: 0.8458 
-> アルゴリズム選択: 0.8510
```

COTOHA APIの追加

```
0.8510
-> lemma追加: 0.8516
-> 固有表現追加: 0.8518
-> アルゴリズム選択: 0.8522
-> 学習回数追加: 0.8569
```

ホールドアウト検証

```
0.8430 -> 0.8806
```

記事タイトルだけという限られた情報の中でもいろいろ試行錯誤することで、少し精度を上げることができました。COTOHA APIについては、文字列を取り出す以外にも形態素間の依存関係や形態素のfeature(副品詞？)なども出てるのでもう少し活用できそうな気はします。