SVMをJupyter Notebook上のscikit-learnで試す

線形のSVM（サポートベクターマシン）は、特徴空間を線形分離して分類する機械学習のモデルです。線形に分離できないような場合には、カーネル法を使ったSVNにより非線形に分離できます。

いままでカーネル法というのがよくわかっていなかったのですが、以下の記事が非常にわかりやすかったです。

[機械学習におけるカーネル法について - めもめも](http://enakai00.hatenablog.com/entry/2017/10/13/145337)

これ以降は、以下の記事に従って準備したJupyter Notebookの環境で試しています。
[Jupyter NotebookをDockerを使って簡単にインストールし起動（nbextensions、Scalaにも対応） - Qiita](https://qiita.com/suzuki-navi/items/37615024d4748aac6688)

この環境でブラウザで8888番ポートにアクセスして、Jupyter Notebookを使うことができます。右上のボタンのNew > Python3をたどると新しいノートを開けます。

また、適当にランダムで作成したCSVファイル
https://github.com/suzuki-navi/sample-data/blob/master/sample-data-1.csv
を使っています。

# データ準備

CSVファイルからデータを読み込んでDataFrameのオブジェクトにします。

```
import pandas as pd
from sklearn import model_selection
df = pd.read_csv("sample-data-1.csv", names=["id", "target", "data1", "data2", "data3"])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/a5764dfe-ce42-e00a-a835-ded9981d9ef8.png)

`df`はPandasのDataFrameオブジェクトです。

参考
[PandasのDataFrameに対する基本操作をJupyter Notebook上で試す - Qiita](https://qiita.com/suzuki-navi/items/6bd8377ca7974779a822)

このCSVのデータには特徴変数は`data1`, `data2`, `data3`がありますが、散布図でデータの様子を確認してみます。

```
%matplotlib inline
import matplotlib.pyplot as plt
plt.scatter(df["data1"], df["data2"], c = df["target"])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/f000d5d0-7af7-7583-3aca-05d37e5051ae.png)

```
plt.scatter(df["data1"], df["data3"], c = df["target"])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/d89b0ab3-badc-56a2-7ddd-a4544f29129d.png)

```
plt.scatter(df["data2"], df["data3"], c = df["target"])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/8ed44a73-9bad-d29c-18b4-92bf074113af.png)

参考
[ヒストグラム・散布図をJupyter Notebook上で表示する - Qiita](https://qiita.com/suzuki-navi/items/9f5fd47734b22b4adf71)

散布図を見ると`data2`と`data3`の2つでなんとなく分類できそうな気がしますので、これで試してみます。

```
feature = df[["data2", "data3"]]
target = df["target"]
```

`feature`はPandasのDataFrameオブジェクト、`target`はPandasのSeriesオブジェクトになります。

300レコードありますが、これを特徴変数と目的変数それぞれ訓練データと検証データに分割します。単にレコードを2つに分割するだけですが、`model_selection.train_test_split`を使うと簡単に分割できます。これによりランダムに分割されます。

```
feature_train, feature_test, target_train, target_test = model_selection.train_test_split(feature, target, test_size=0.2)
```

`test_size=0.2`は全データの2割を検証データにする指定です。

特徴変数(`df[["data2", "data3"]]`, `feature_train`, `feature_test`)はPandasのDataFrameオブジェクト、目的変数(`df["target"]`, `target_train`, `target_test`)はSeriesオブジェクトです。

# 学習

作成した訓練データ(`feature_train`, `target_train`)をもとに学習します。

```
from sklearn import svm
model = svm.SVC(kernel="linear")
model.fit(feature_train, target_train)
```

`SVC(kernel="linear")`は線形分離するSVMの分類機のモデルです。`fit`で学習をさせます。

参考
[sklearn.svm.SVC — scikit-learn 0.21.3 documentation](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html)

# 評価

学習したモデルで訓練データの特徴変数(`feature_train`)から推論結果(`pred_train`)を作成し、それと目的変数(`target_train`)とを比べて、正解率を評価します。`metrics.accuracy_score`という関数で簡単に評価できます。

```
from sklearn import metrics
pred_train = model.predict(feature_train)
metrics.accuracy_score(target_train, pred_train)
```

ロジックにランダムさがあるので、毎回結果は違うかもしれませんが、`0.95`と表示されました。

過学習していないか汎化性能を見るために、訓練データで評価します。

```
pred_test = model.predict(feature_test)
metrics.accuracy_score(target_test, pred_test)
```

`0.9333333333333333`と表示されました。まあまあいいのかな、よくわかりませんが。

scikit-learnとは別に`mlxtend`というパッケージに含まれる`plotting.plot_decision_regions`を使うとどのように分類しているのかを散布図で視覚化してくれます。`plot_decision_regions`にはPandasのオブジェクトではなくNumPyの配列を渡す必要がありますので、`to_numpy()`というメソッドで変換します。

```
from mlxtend import plotting
plotting.plot_decision_regions(feature.to_numpy(), target.to_numpy(), clf=model)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/c0f5a661-c991-b976-1492-75135d45fe7f.png)

いい感じ。

参考
[plot_decision_regions - Mlxtend.plotting - mlxtend](http://rasbt.github.io/mlxtend/api_subpackages/mlxtend.plotting/#plot_decision_regions)
[pandas.DataFrame.to_numpy — pandas 0.25.3 documentation](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_numpy.html)

# カーネル法を使ってみる

非線形分離を試してみたいと思います。RBFカーネルというのを使ってみます。

`svm.SVC(kernel="linear")`を`svm.SVC(kernel="rbf", gamma="scale")`にすればいいだけです。`gamma="scale"`はRBFカーネルの場合のハイパーパラメータで、`"scale"`を指定すると訓練データの数と特徴変数の分散から自動で計算してくれます。

以下のコードで、モデルの作成、学習、推論、評価までします。

```
model = svm.SVC(kernel="rbf", gamma="scale")
model.fit(feature_train, target_train)
pred_train = model.predict(feature_train)
metrics.accuracy_score(target_train, pred_train)
```

`0.95`と表示されました。

汎化性能を見るために、訓練データで評価します。

```
pred_test = model.predict(feature_test)
metrics.accuracy_score(target_test, pred_test)
```

`0.95`と表示されました。さきほどの線形分離よりちょっとだけいい数字です。

```
plotting.plot_decision_regions(feature.to_numpy(), target.to_numpy(), clf=model)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/ca9222d0-89cb-0ee5-da91-9e2dc36e3864.png)

非線形というだけあってたしかに曲線で分離されています。

今回のサンプルは線形分離しやすいものだったので、非線形にするほどでもなかったのかもしれません。

# ほかのデータでカーネル法を試す

`data2`と`data3`では線形分離できてしまうので、ほかのデータの組み合わせでRBFカーネルを試してみます。

まずは`data1`と`data2`。以下のコードで分離している様子の図だけ作ります。

```
feature = df[["data1", "data2"]]
target = df["target"]
feature_train, feature_test, target_train, target_test = model_selection.train_test_split(feature, target, test_size=0.2)
model = svm.SVC(kernel="rbf", gamma="scale")
model.fit(feature_train, target_train)
plotting.plot_decision_regions(feature.to_numpy(), target.to_numpy(), clf=model)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/a0be2379-1177-8c60-ec89-5f3e64b2ca2d.png)

いちおう正解率を見てみます。

```
pred_train = model.predict(feature_train)
metrics.accuracy_score(target_train, pred_train)
```

`0.7583333333333333`でした。

```
pred_test = model.predict(feature_test)
metrics.accuracy_score(target_test, pred_test)
```

`0.7833333333333333`でした。

ちなみに同じデータで線形(`kernel="linear"`)で試しても0.71～0.74でした。図を見るとカーネル法ががんばってそうに見えますが、数値にするとそんなに大差はない？非線形ができるからといってあまり期待しないほうがいいのかな。

`data1`と`data3`でも試しましたが、同じような感じでしたので、省略・・・

以上。

続編
[混合ガウスモデルによるクラスタリングをJupyter Notebook上で試す - Qiita](https://qiita.com/suzuki-navi/items/d7c27c9ce815750cdc59)