【Python 3で機械学習】☆誰でもできる！ランダムフォレストを使ってワインの等級を予測

#記事投稿のきっかけ
研究活動の中で、機械学習を使っており備忘録として、データの用意からモデル構築、コンフュージョンマトリックスや分類クラスごとのデータの可視化、特徴量の重要度の可視化について、まとめています。

#何を分類しようか
機械学習を行う為には、まずデータセットがないと始まらない！
今回は、誰でもアクセスして使えるデータをと思い、ワインの等級についてのデータを使います。

こちらのQiitaの記事ではRを使って、ワインの等級をランダムフォレストを使用して予測しています！
[Rで遊ぶ　～ワインの等級をrandomForestで予測～](https://qiita.com/Dixhom/items/7c33a1dc85144e1da822)

今回はPython3を用いて、やってみたいと思います！

#環境
環境は、ANACONDAをインストールし、Jupyter notebook 5.5.0を使用します！
ANACONDAを使う理由は環境構築のハードルが低いからです。
参考HPを載せておきます。
[Anaconda を Windows にインストールする手順]
(https://weblabo.oscasierra.net/python-anaconda-install-windows/)

anacondaと同様に、"始めよう！"と思った時のハードルが低い、Google Colabについても途中、余談として登場します。

#ライブラリのインストール

```python:wine.py
import pandas as pd
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix
from pandas.plotting import scatter_matrix
```
モデルを作成する為のライブラリや、結果を図示する為のライブラリをインポートします。scikit-learnはPythonのオープンソース機械学習ライブラリです。多数の手法、アルゴリズムを備えています。

numpyは、数値計算を効率的に行い、pandasはデータ解析を支援、matplotlobはnumpyのためのグラフ描写ライブラリであり、%matplotlib inlineは、Jupyter notebookでノートブック内に結果を可視化するものです。

バージョンを出力するコードは以下の通り

```python:wine.py
print(pandas.__vesion__)
print(numpy.__vesion__)
print(matplotlib.__vesion__)
print(sklearn.__vesion__)
```


#データの準備
サイトからのダウンロードの方法と、ローカルにCSVを保存して取り込む方法があります。

```python:wine.py
df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data', header=None)
df = pd.read_csv('wine.csv')
```

ここで余談。
Google ColabだとCSVのアップロードが少し異なります。

```python:wine.ipynb
from google.colab import files
uploaded = files.upload()
```

実際の画面は、下記参照。
ファイル選択を押して、読み込みたいファイルを選択します。

<img width="307" alt="キャプチャ.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267677/526622ba-a29c-7368-8ef5-f438f806d39c.jpeg">



#読み込めているか確認
先頭の行(デフォルトは5行)のデータを参照してみます。

```python:wine.py
print(df.head())
```

```python:out
   Class  Alcohol  Malic acid   Ash  Alcalinity of ash    Magnesium  \
0      1    14.23        1.71  2.43                 15.6        127   
1      1    13.20        1.78  2.14                 11.2        100   
2      1    13.16        2.36  2.67                 18.6        101   
3      1    14.37        1.95  2.50                 16.8        113   
4      1    13.24        2.59  2.87                 21.0        118   

   Total phenols  Flavanoids  Nonflavanoid phenols  Proanthocyanins  \
0           2.80        3.06                  0.28             2.29   
1           2.65        2.76                  0.26             1.28   
2           2.80        3.24                  0.30             2.81   
3           3.85        3.49                  0.24             2.18   
4           2.80        2.69                  0.39             1.82   

   Color intensity   Hue  OD280/OD315 of diluted wines  Proline    
0             5.64  1.04                          3.92       1065  
1             4.38  1.05                          3.40       1050  
2             5.68  1.03                          3.17       1185  
3             7.80  0.86                          3.45       1480  
4             4.32  1.04                          2.93        735  
```

特徴量はCSVデータの1行目に書き込んでいました。

CSVデータの各カラムの有効データ数を表示します。

```python:wine.py
df.count()
```

```python:out
Class                           178
Alcohol                         178
Malic acid                      178
Ash                             178
Alcalinity of ash               178
Magnesium                       178
Total phenols                   178
Flavanoids                      178
Nonflavanoid phenols            178
Proanthocyanins                 178
Color intensity                 178
Hue                             178
OD280/OD315 of diluted wines    178
Proline                         178
dtype: int64
```

なので、全てのデータを出力するには

```python:wine.py
pd.set_option('display.max_rows', 178)
print(df)
```

上記を実行すると、全ての行を表示する事ができます。

ちなみに、余談ですが、特徴量をデータから減らしたい時には

```python:wine.py
df = df.drop(['Alcohol'],axis=1)
```

このように特徴量名を記入すれば、データフレームから削除する事ができます。

次に、分類するクラスの種類と数を確認してみます。

```python:wine.py
# 分類するクラスの種類と数を確認
df['Class'].value_counts()
```

```python:out
2    71
1    59
3    48
Name: Class, dtype: int64
```

#データセット分割(ホールドアウト法)
機械学習には、教師なし学習、教師あり学習、強化学習、深層学習など様々あります。
今回は、教師あり学習を使って、等級を予測するモデルを作成していこうと思います。
教師あり学習には、主に交差検証とホールドアウト法があります。
交差検証のイメージとしては、トレーニングデータとテストデータを入れ替えて学習を行います。
イメージとしては、下図のような感じ。
これは、例えば被験者4人からデータを集めた際に、被験者に依存しないモデルかどうかを判別する為に役に立ちます。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267677/a791ebd4-3dec-1517-dbfe-f7ddf5d3e9bf.png)

今回は、もっとシンプルに、ホールドアウト法を使ったモデル構築を行います。
トレーニング用とテスト用にデータセットに分けてから、トレーニング用でモデルを作成してから、テスト用のデータでうまく予測する事ができるか確認していきます。
基本的なホールドアウト法による分類のイメージを下の図に示しておきます。

![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/267677/a616de19-db21-e069-fbbe-bb36e9bbd7d3.png)

```python:wine.py
# 機械学習のモデルを作成するトレーニング用と評価用の2種類に分割する
train_x = df.drop(['Class'], axis=1) # 説明変数のみにする
train_y = df['Class'] # 正解クラス
(train_x, test_x ,train_y, test_y) = train_test_split(train_x, train_y, test_size = 0.3, random_state = 42)
#訓練用の説明変数と正解クラス、評価用の説明変数と正解クラスに分割 
```
これで、train_xには、13の特徴量、train_yには正解クラスに分けてから、訓練用とテスト用に分割しました。test_sizeを0.7にすれば、学習用を30%、訓練用を70%といったように分割できます。私が研究論文でよく読んでるのは、学習用に70%使っているものが多いので、今回はtest_sizeを0.3にします。
42を選んだ理由は、「生命、宇宙、そして万物についての究極の疑問の答え」だからです。これも、多くの参考書でよく使われていますね。
(この学習用データセットの比率を変えながら、識別精度の変化を分析する、Learning Curveといったものがあります。私の他の記事で、話しているので、もし良ければ。[学習曲線から過学習を検知 (機械学習、Python)](https://qiita.com/koichi_hiphopdream/items/9ad75d184aba9626c09b)

#モデル構築から予測精度の確認
学習モデルを構築する為の学習アルゴリズムには、ランダムフォレストを使用します。
高い精度を出すアルゴリズムの1つです。
ランダムフォレストのパラメータは、作成する決定木を30、最大の深さを30にしてみます。

また、学習モデルに評価用のデータセットを入力した際の制度に加えて、訓練用で学習モデルを作成した際の精度も確認してみます。

```python:wine.py
# 識別モデルの構築
random_forest = RandomForestClassifier(max_depth=30, n_estimators=30, random_state=42)
random_forest.fit(train_x, train_y)

# 予測値算出
y_pred = random_forest.predict(test_x)

#モデルを作成する段階でのモデルの識別精度
trainaccuracy_random_forest = random_forest.score(train_x, train_y)
print('TrainAccuracy: {}'.format(trainaccuracy_random_forest))

#作成したモデルに学習に使用していない評価用のデータセットを入力し精度を確認
accuracy_random_forest = accuracy_score(test_y, y_pred)
print('Accuracy: {}'.format(accuracy_random_forest))
```

```python:out
TrainAccuracy: 1.0
Accuracy: 0.9814814814814815
```

結果をみると、学習モデルを作成した際の精度は100%ですね。
そのモデルに評価用のデータセットを入力した際には98%の精度で識別できています。

もっとクラスごとの詳細な結果をみてみましょう。

```python:wine.py
from sklearn.metrics import classification_report
print(classification_report(test_y, y_pred))
```

```python:out
             precision    recall  f1-score   support

          1       1.00      1.00      1.00        19
          2       1.00      0.95      0.98        21
          3       0.93      1.00      0.97        14

avg / total       0.98      0.98      0.98        54
```

適合率(specificity, precision)とは、positiveと分類したデータ(TP + FP)の中で実際にpositiveだったデータ(TP)数の割合であり、この値が高いほど性能が良く、間違った分類が少ないということを意味します。

再現率(sensitivity, recall)では、取りこぼし無くpositiveなデータを正しくpositiveと推測できているかどうかの指標となります。この値が高いほど性能がよく、間違ったpositiveの判断が少ないということであり、別の言い方をすると本来positiveと推測すべき全てのデータを、うまく推測できたかどうかを示します。

F-MeasureはF値またはF尺度と呼ばれており、適合率と再現率の調和平均を示します。高ければ高いほど、適合率と再現率ともに高くなり、バランスを示す事ができます。

コンフュージョンマトリックスで確認してみます。

```python:wine.py
#confusion matrix
mat = confusion_matrix(test_y, y_pred)
sns.heatmap(mat, square=True, annot=True, cbar=False, fmt='d', cmap='RdPu')
plt.xlabel('predicted class')
plt.ylabel('true value')
```


![キャプチャ1.PNG](https://qiita-image-store.s3.amazonaws.com/0/267677/c213b571-2b56-28bd-600e-59034314f084.png)

評価用のデータセットのクラス1が誤ってクラス2に分類したものが1つありました。

```python:wine.py
#どのデータを、どのクラスに分類したのか。
y_pred
```
を実行する事で、各テストデータに対する予測の格納された、1次元のnumpy配列が出力され確認する事ができます。

```python:out
array([1, 1, 3, 1, 2, 1, 2, 3, 2, 3, 1, 3, 1, 3, 1, 2, 2, 2, 1, 2, 1, 2,
       2, 3, 3, 3, 2, 2, 2, 1, 1, 2, 3, 1, 1, 1, 3, 3, 2, 3, 1, 2, 2, 2,
       3, 1, 2, 2, 3, 1, 2, 1, 1, 3], dtype=int64)
```

#グリッドサーチについても触れておく(余談)

先ほどは、パラメータを固定して、その条件のもとで精度を出しました。
どのようなパラメータの際に、より良い精度を出す事ができるのか。
「ハイパーパラメータチューニング」によって、確かめてみます。
今回は、5つのパラメータを交差検証して精度の改善を行います。

```python:wine.py
# ランダムフォレストのパラメータの候補をいくつか決める
parameters = {
    'n_estimators' :[3,5,10,30,50],#作成する決定木の数
    'random_state' :[7,42],
    'max_depth' :[3,5,8,10],#決定木の深さ
    'min_samples_leaf': [2,5,10,20,50],#分岐し終わったノードの最小サンプル数
    'min_samples_split': [2,5,10,20,50]#決定木が分岐する際に必要なサンプル数
}

#グリッドサーチを使う
from sklearn.model_selection import GridSearchCV
clf = GridSearchCV(estimator=RandomForestClassifier(), param_grid=parameters, cv=2, iid=False)

#学習モデルを作成
clf.fit(train_x, train_y)
```

```python:out
GridSearchCV(cv=2, error_score='raise',
       estimator=RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
            max_depth=None, max_features='auto', max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=1,
            oob_score=False, random_state=None, verbose=0,
            warm_start=False),
       fit_params=None, iid=False, n_jobs=1,
       param_grid={'n_estimators': [3, 5, 10, 30, 50], 'random_state': [7, 42], 'max_depth': [3, 5, 8, 10], 'min_samples_leaf': [2, 5, 10, 20, 50], 'min_samples_split': [2, 5, 10, 20, 50]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring=None, verbose=0)
```

```python:wine.py
#精度を確認
best_clf = clf.best_estimator_ #ここにベストパラメータの組み合わせが入っています
print('score: {:.2%}'.format(best_clf.score(train_x, train_y)))
y_pred = clf.predict(test_x)
print('score: {:.2%}'.format(best_clf.score(test_x, test_y)))
```

```python:out
score: 100.00% #トレーニングモデルを作成した際の精度
score: 98.15% #テストデータを入力した際の精度
```

#重要特徴量
話を戻して、学習モデルに寄与した特長量を確認してみましょう。

```python:wine.py
# 変数の重要度を可視化
importance = pd.DataFrame({ '変数' :train_x.columns, '重要度' :random_forest.feature_importances_})
importance
```
![キャプチャ2.PNG](https://qiita-image-store.s3.amazonaws.com/0/267677/438068f4-ec20-6762-cd39-954f73417090.png)
Flavanoidsが18%、Color intensityが13%で大きく貢献してます。
Nonflavanoid phenolsとAshは1%にも満たない事が分かります。

この2つの特徴量をプロットしてみます。

```python:wine.py
plt.scatter(df['Color intensity'], df['Flavanoids'], c = df['Class'])
plt.ylabel('Flavanoids')
plt.xlabel('Color intensity')
plt.show()
```

![キャプチャ3.PNG](https://qiita-image-store.s3.amazonaws.com/0/267677/a16091f0-23cd-06e6-2071-ad2800ef07a4.png)

#3Dのグラフで可視化してみよう
2次元のグラフだと、2つの特徴量のプロットしかできないので、今度は3次元でプロットしてみます。

```python:wine.py
df_1 = df[df.Class == 1]
df_2 = df[df.Class == 2]
df_3 = df[df.Class == 3]
```
まずは、クラスごとにデータセットを分けてあげます。
こうする事で3次元にプロットした際にクラスごとに色づけする事ができます。

```python:wine.py
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = Axes3D(fig)
ax.set_xlabel("Flavanoids")
ax.set_ylabel("Color intensity")
ax.set_zlabel("Alcohol")
ax.plot(df_1['Flavanoids'], df_1['Color intensity'],df_1['Alcohol'],marker="o",linestyle='None', c='red')
ax.plot(df_2['Flavanoids'], df_2['Color intensity'],df_2['Alcohol'],marker="o",linestyle='None', c='blue')
ax.plot(df_3['Flavanoids'], df_3['Color intensity'],df_3['Alcohol'],marker="o",linestyle='None', c='green')
plt.show()
```

3Dにプロットするために、mpl_toolkits.mplot3dをインポートします。
特徴量を3つ選出して、特徴量を記入しプロットしてみます。

結果は、以下になります。
![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267677/894b8838-c106-3bd7-02fe-8d47b6ef81ea.png)

補足になりますが、visual studio codeで実行すると、上下左右にマウスで動かす事ができます！

#作成した多数の決定木の中身を知ろう
ここまで学習モデルを構築して精度を出してみたけど、中身は一体どうなっているのだろう。ランダムフォレストの場合、構築のステップについてはブラックボックスになりますが、作成した決定木を可視化する事はできます。

```python:wine.py
from sklearn import tree
for i in range(1, 31):
    tree.export_graphviz(random_forest.estimators_[i-1],'tree' + str(i) + '.dot')
```
今回、モデルの構築に30の決定木を作成したので、30本の決定木を可視化しました。
1つめの決定木は0番に格納されており、dotファイルとして1本ずつエクスポートします。

dotファイルの可視化について、私が思うのは以下のやり方が一番簡単な気がします。
[【Graphviz 】dotファイルを画像ファイルに変換する。(for Windows)](https://qiita.com/NumLocker/items/a145080647a69cd2997a)

dotファイルを可視化すると、こんな感じの図ができます。
![tree1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267677/dc8bfcde-e356-8edc-f3c2-bcf63a7ee057.png)

もっと決定木の可視化を綺麗にしたい場合は、以下を参考にすると良さそう。
[決定木をいろいろな方法で可視化する](https://www.haya-programming.com/entry/2019/06/25/235927)

#汎用性を確かめよう

この学習モデルに別のデータを入力して、精度を確かめたいとします。今回は、ワインのデータセットを新たに入手する事ができないので、元データをコピーしたものを使用します。

```python:wine.py
df1 = pd.read_csv('wine_copy.csv')
# 先ほど作成したモデルに未知のデータセットを入力し精度を算出
test_x1 = df1.drop(['Class'], axis=1)
test_y1 = df1['Class']
y_pred1 = random_forest.predict(test_x1)
random_forest = random_forest.score(test_x1, test_y1)
print('Accuracy: {}'.format(random_forest))
```

```python:out
Accuracy: 0.9943820224719101
```

```python:wine.py
# confusion matrix
mat = confusion_matrix(test_y1, y_pred1)
sns.heatmap(mat, square=True, annot=True, cbar=False, fmt='d', cmap='RdPu')
plt.xlabel('predicted class')
plt.ylabel('true value')
```

![キャプチャ5.PNG](https://qiita-image-store.s3.amazonaws.com/0/267677/49af578d-4cce-7389-e7da-4b2182f273e3.png)

本来は、全く新しいデータセットが入力されるので、過学習していないかなど確認する事ができます！