書籍「15Stepで踏破 自然言語処理アプリケーション開発入門」をやってみる - 2章Step06メモ「識別器」

# 内容
[15stepで踏破 自然言語処理アプリケーション入門](https://www.amazon.co.jp/dp/4865941320) を読み進めていくにあたっての自分用のメモです。
今回は2章Step06で、自分なりのポイントをメモります。

# 準備
- 個人用MacPC：MacOS Mojave バージョン10.14.6
- docker version：Client, Server共にバージョン19.03.2

# 章の概要
特徴ベクトルとクラスIDを入力として学習を行うことで、特徴ベクトルからクラスIDを予測できるようになるのが識別器である。

- SVM(Support Vector Machine)
- 決定木アンサンブル
- k近傍法

# 06.1識別器を使いこなすために
`classifier`変数に識別器を代入する構成にしておけば、識別器を簡単に切り替えられる。

```python
# SVC
from sklearn.svm import SVC
classifier = SVC()

# RandomForest
from sklearn.ensemble import RandomForestClassifier
classifier = RandomForestClassifier()
```

# 06.2 SVM(Support Vector Machine)
- 比較的高い性能が安定して得られる
- 比較的学習データが少ないタスクにも適用しやすい
- カーネル法により、複雑な問題にも対処できる

基本的なSVMの概要については省略。
直線や平面の識別面（線形識別面）では特徴空間を区切って識別することができない問題を**線形分離不可能**な問題と呼ぶ。これに対応できるように下記の手法がある。

- ソフトマージン
- カーネル

## ハードマージンSVM、ソフトマージンSVM
「境界をはみ出すことを許容しつつ、なるべくはみ出しを小さくする」ように境界面を設定しようとする**ソフトマージンSVM**と、単純な**ハードマージンSVM**の2種類ある。
はみ出しの許容程度はインスタンス生成時に指定できる（デフォルト1.0）。

## カーネル
境界のはみ出しを許容した程度では分離できないようなケースは、特徴ベクトルを元の特徴空間よりも高次元の特徴空間に写してそこで識別面を設定するカーネル法を用いる。カーネルの種類はインスタンス生成時に指定できる。

- RBFカーネル(ガウスカーネル)：デフォルト指定、最もオーソドックス
- 多項式カーネル：自然言語処理で人気
- その他
- カーネル未使用（線形SVM）

## Scikit-learnが提供するクラス
- sklearn.svm.SVC
    - 線形、カーネルどちらも利用可能
- sklearn.svm.LinearSVC
    - 線形SVMに特化した実装
- sklearn.linear_model.SGDClassifier
    - sklearn.svmは学習データを全てメモリに載せて一度に計算するので、学習データ量によっては大量のメモリ容量が必要
    - StochasticGradientDescent（確率的勾配降下法）なので、学習データを少しずつ読み込んで学習を進めていくが、若干の誤差はある


# 06.3 アンサンブル
複数の識別器を組み合わせて1つの識別器を構成する手法をアンサンブルと呼ぶ。
決定木の説明は省略するが、決定木アンサンブルの性質は下記の通りである。

- 特徴量の前処理が不要
- 設計者が設定するパラメータの数が少ない

アンサンブル手法の代表的なものはバギングとブースティング[(参考)](https://qiita.com/ground0state/items/105d16cdd6fb7a74d309)である。

- バギング
    - データの一部を使って複数回別々に学習し、最後に結果を合わせる
    - Random Forest
- ブースティング
    - データの一部を使って学習し、前回の結果を利用し何度も学習を繰り返す
    - 勾配ブースティング(GradientBoostedDecisionTrees;GBDT)

|項目 |Random Forest |GBDT |
|--- |--- |--- |
|アンサンブル手法 |バギング |ブースティング |
|メソッド |sklearn.ensemble.RandomForestClassifier() |skleran.ensemble.GradientBoostingClassifier() |
|決定木の作成 |深い木を少数 |浅い木を多数 |
|実行 |-|高速でメモリ効率がRandomForestより良い|

# 06.4 k近傍法
入力された特徴ベクトルに近い特徴ベクトルを学習データからk個選び、それらのクラスIDで多数決をとる識別器である。
kの数値によっては識別されるクラスIDが変わったり、学習データにクラスIDの個数に偏りがある場合は望ましい結果にならないこともある。

## 近さを測る距離
|種類 |内容 |
|--- |--- |
|ユークリッド距離 |空間上でのベクトルの長さ |
|マンハッタン距離 |空間上での軸方向の長さの和 |
|ミンコフスキー距離 |ユークリッド距離やマンハッタン距離を一般化した距離 |
|レーベンシュタイン距離 |文字列同士の距離を表す際、1文字ずつ挿入/削除/置換して同じ文字列を作るための回数|

## パラメトリックとノンパラメトリック
|項目 |パラメトリック |ノンパラメトリック |
|--- |--- |--- |
|パラメータ設定|識別面を適切に設定できるようパラメータに気を配る必要がある |識別面に関するパラメータを考えなくて良い|
|計算コスト |学習時に識別面の計算コストがかかる<br>識別時の計算コストはほぼ一定 |学習時の計算コストは基本的にゼロ<br>識別時の計算コストは学習データの量を反映して増えていく |
|必要な学習データの数 |比較的少ない |比較的多い |
|識別器例 |SVM |k近傍法 |

# 06.5 対話エージェントへの適用
前章からの追加・変更点

1. 識別器：SVM → Random Forest
2. TF-IDFのngram_range：1~3 → 1~2

```python
~~
pipeline = Pipeline([
#    ('vectorizer', TfidfVectorizer(tokenizer=self._tokenize, ngram_range=(1, 3))),
    ('vectorizer', TfidfVectorizer(tokenizer=self._tokenize, ngram_range=(1, 2))),
#    ('classifier', SVC()),
    ('classifier', RandomForestClassifier(n_estimators=30)),
])
~~
```

```bash:実行結果
# evaluate_dialogue_agent.pyの読み込みモジュール名を必要に応じて修正
from dialogue_agent import DialogueAgent

$ docker run -it -v $(pwd):/usr/src/app/ 15step:latest python evaluate_dialogue_agent.py
0.61702127
```

- 通常実装(Step01)：37.2%
- 前処理追加(Step02)：43.6%
- 前処理＋特徴抽出変更(Step04)：58.5%
- 前処理＋特徴抽出変更＋識別器変更(Step06)：61.7%