scikit-learn備忘録　手書き数字の認識サンプル

# scikit-learn備忘録
pythonの機械学習ライブラリ、scikit-learnの学習を開始したので、備忘録として記事を残します。
## 手書き数字の認識サンプル
まずは手書き数字の認識サンプルが以下のURLにあるので、このコードの読解をやってみます。

環境はAnaconda3 python3.8で専用の環境を構築して行いました。
あらかじめscikit-learn関係のライブラリとして

+ scikit-image
+ scikit-learn
+ scikit-rf

をインストールしておきます。

実行環境はjupyterです。

[Recognizing hand-written digits](https://scikit-learn.org/stable/auto_examples/classification/plot_digits_classification.html#recognizing-hand-written-digits)

```python
print(__doc__)

# Author: Gael Varoquaux <gael dot varoquaux at normalesup dot org>
# License: BSD 3 clause

# Standard scientific Python imports
import matplotlib.pyplot as plt

# Import datasets, classifiers and performance metrics
from sklearn import datasets, svm, metrics
from sklearn.model_selection import train_test_split

# The digits dataset
digits = datasets.load_digits()

# The data that we are interested in is made of 8x8 images of digits, let's
# have a look at the first 4 images, stored in the `images` attribute of the
# dataset.  If we were working from image files, we could load them using
# matplotlib.pyplot.imread.  Note that each image must have the same size. For these
# images, we know which digit they represent: it is given in the 'target' of
# the dataset.
_, axes = plt.subplots(2, 4)
images_and_labels = list(zip(digits.images, digits.target))
for ax, (image, label) in zip(axes[0, :], images_and_labels[:4]):
    ax.set_axis_off()
    ax.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
    ax.set_title('Training: %i' % label)

# To apply a classifier on this data, we need to flatten the image, to
# turn the data in a (samples, feature) matrix:
n_samples = len(digits.images)
data = digits.images.reshape((n_samples, -1))

# Create a classifier: a support vector classifier
classifier = svm.SVC(gamma=0.001)

# Split data into train and test subsets
X_train, X_test, y_train, y_test = train_test_split(
    data, digits.target, test_size=0.5, shuffle=False)

# We learn the digits on the first half of the digits
classifier.fit(X_train, y_train)

# Now predict the value of the digit on the second half:
predicted = classifier.predict(X_test)

images_and_predictions = list(zip(digits.images[n_samples // 2:], predicted))
for ax, (image, prediction) in zip(axes[1, :], images_and_predictions[:4]):
    ax.set_axis_off()
    ax.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
    ax.set_title('Prediction: %i' % prediction)

print("Classification report for classifier %s:\n%s\n"
      % (classifier, metrics.classification_report(y_test, predicted)))
disp = metrics.plot_confusion_matrix(classifier, X_test, y_test)
disp.figure_.suptitle("Confusion Matrix")
print("Confusion matrix:\n%s" % disp.confusion_matrix)

plt.show()
```
このコードを読んでみることにします。

## 環境の情報
```python
print(__doc__)
```
これは**ドキュメントスリング**と呼ばれるもので、モジュールやメソッドのドキュメント、要するに説明を参照できるコードです。

```
Automatically created module for IPython interactive environment
```
実行結果としては

```
IPythonの対話型環境のためのモジュールを自動的に作成
```
要はjupyterのシェル情報が出てきています。

## グラフ描画環境の読み込み
```python
# Standard scientific Python imports
import matplotlib.pyplot as plt
```
pythonでグラフを表示する際に使う**matplotloib.pyplot**をimportしています。

公式のドキュメントは以下になります。

[matplotlib.pyplot](https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.html)

## データセット、機械学習システム、モデル評価指標、データの分割関数の読み込み

```python
# Import datasets, classifiers and performance metrics
from sklearn import datasets, svm, metrics
from sklearn.model_selection import train_test_split
```
**datasets**というのはsc可視化t-learnに標準で付属しているデータセットです。学習用の小規模なデータセットですが、これで十分に活用できます。

[Dataset loading utilities](https://scikit-learn.org/stable/datasets/index.html#datasets)

**svm**というのは機械学習システムのサポートベクターマシンのライブラリで教師あり学習を簡単に行うことができます。
[Support Vector Machines](https://scikit-learn.org/stable/modules/svm.html)

**metric**というのはモデルの分類能力を分析するための関数のモジュールです。学習したモデルがデータを正しく分類できるかどうか判定するのに必要です。
[Classification metrics](https://scikit-learn.org/stable/modules/model_evaluation.html#classification-metrics)

**train_test_split**というのはデータを学習用データとテストデータに分割するためのモジュールです。
[sklearn.model_selection.train_test_split](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html?highlight=split#sklearn.model_selection.train_test_split)

学習用データとテストデータを分ける理由は、過学習、overfitと呼ばれる現象に対応するためのものです。

機械学習ではただデータを与えればいいというものではありません。

学習用データだけを機械学習システムに読み込ませ続けると、学習用データでは正解率100%に限りなく近くなますが、実際に運用して、現実のデータを読み込ませると使い物にならないという現象が生じます。

要は機械学習システムがテストのデータを丸暗記してしまい、暗記した答えを出すだけのシステムになってしまうのです。

問題集の問題を丸暗記して100点満点をとれるようになって、さあ試験ですよ、で少し文字や数値が変わったらもうわからない。

できのわるい学生がテストでやりがちなミスですが、機械学習も同じようなミスをします。いや、ミスをしているのはプログラマーの方なんですが。

なので機械学習では、データセットという「問題集」の一部だけで学習させ、システムが知らない問題を「試験」として解かせることで、本当に学習できているか確かめるという手法が確立されました。

この問題集を分割してくれるのがこのモジュールです。

## データセットの読み込み 
```python
# The digits dataset
digits = datasets.load_digits()
```
datasetsモジュールから、手書き数字のデータをロードする

## 手書き数字データの可視化

```python
# The data that we are interested in is made of 8x8 images of digits, let's
# have a look at the first 4 images, stored in the `images` attribute of the
# dataset.  If we were working from image files, we could load them using
# matplotlib.pyplot.imread.  Note that each image must have the same size. For these
# images, we know which digit they represent: it is given in the 'target' of
# the dataset.
_, axes = plt.subplots(2, 4)
images_and_labels = list(zip(digits.images, digits.target))
for ax, (image, label) in zip(axes[0, :], images_and_labels[:4]):
    ax.set_axis_off()
    ax.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
    ax.set_title('Training: %i' % label)
```
手書き文字のデータを画像にするコードです。
まず

```python
_, axes = plt.subplots(2, 4)
```
これは公式のドキュメントでは戻り値がfig,axになっています。

[matplotlib.pyplot.subplots ](https://matplotlib.org/3.3.3/api/_as_gen/matplotlib.pyplot.subplots.html)

これによるとfigを捨ててaxのみを使用するようです。
このコードを実行すると、２行４列の空白のグラフが表示されます。

![subplot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216739/772ae2c5-577d-1113-0c4d-3be60a6dc450.png)


```python
images_and_labels = list(zip(digits.images, digits.target))
```
zipはリストやタプル等のイテラブルオブジェクトを要素ごとに取り出す組み込み関数で単体では機能しません。
digits.imagesとdigits.targetの要素を一つずつ取り出してlistにするというのがこの行です
これにより、次にようなimages_and_labelsリストが生まれます。

![image_and_labels.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216739/fcaaff75-0a38-2788-1413-00172fa2a7f0.png)
このリストをfor文で表示していきます

```python
for ax, (image, label) in zip(axes[0, :], images_and_labels[:4]):
    ax.set_axis_off()
    ax.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
    ax.set_title('Training: %i' % label)
```
axesというのは plt.subplots(2, 4)の返り値でグラフのオブジェクトのことです。
![axes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216739/e2d38d56-d22c-8b75-8969-684d9a211277.png)
axes[0:]は1行目の4つのグラフが該当します。
images_and_labels[:4]ということはimages_and_labels[0],images_and_labels[1],images_and_labels[2],images_and_labels[3]を順番に取り扱うということです。
ax.set_axis_off()で１行面のグラフの軸を消しています。

imshowは[matplotlib.pyplot.imshow](https://matplotlib.org/3.3.3/api/_as_gen/matplotlib.pyplot.imshow.html)

cmap=plt.cm.gray_rというのはカラーマップに逆グレースケールを設定しています。
後ろについている_rは逆転で、値が大きい程黒くなります。
cmap=plt.cm.grayの場合は値が大きい程白くなるので、次の画像のようになります。
![グレーパターンを逆.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216739/93f07541-808d-d80e-453e-de0740037f60.png)
interpolation='nearest'はドット間の補間の設定です。

ax.set_title('Training: %i' % label)はグラフタイトルです。
これは古いpythonの書き方で、公式でも使用を控えるようにとのお達しが出ています。[printf 形式の文字列書式化](https://docs.python.org/ja/3/library/stdtypes.html#printf-style-string-formatting)

このコードを実行することにより、2行4列のグラフ画像のうち、1行目のみに画像が出力されます。

![上だけ画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216739/33d1e9b6-6dcc-5575-1407-b6009f1f6985.png)


## リスト形状の変更
```python
# To apply a classifier on this data, we need to flatten the image, to
# turn the data in a (samples, feature) matrix:
n_samples = len(digits.images)
data = digits.images.reshape((n_samples, -1))
```
この分類機は画像を1次元のリストにして入力する必要があるので、reshapeメソッドで変形しています。
dataはnumpy.darrayオブジェクトなので[numpy.ndarray.reshape](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.reshape.html#numpy.ndarray.reshape)で形状を変更しています。これにより1797×8×8の3次元リストだったdigits.imagesを1797(n_sample)×64(-1はリスト形状からの自動決定を指示)の2次元リストに変更しています。

## 学習機の作成
```python
# Create a classifier: a support vector classifier
classifier = svm.SVC(gamma=0.001)
```
ここでC-サポートベクターマシン分類機を作成しています。

[sklearn.svm.SVC](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html#sklearn.svm.SVC)

gammaは分類の境界を決定するためのハイパーパラメータです。小さいと分類の境界が直線的になります。

## データの分割
```python
# Split data into train and test subsets
X_train, X_test, y_train, y_test = train_test_split(
    data, digits.target, test_size=0.5, shuffle=False)
```
[sklearn.model_selection.train_test_split ](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html#sklearn.model_selection.train_test_split)

データセットを訓練データとテストデータを分割しています。

test_sizeはfloatの場合は0.0～1.0でテストデータの割合を、intの場合はテストデータの絶対数を表します。指定しない場合は0.25に指定されます。

ここでは表示されていませんがtrain_sizeという訓練データの割合やサイズを決定する変数もあります。
指定していない場合はtest_sizeから自動決定されます。

つまりこの場合、テストデータが0.5ということで50%がテストデータになります。

shuffleはデータをシャッフル、つまり順番に取り出すかランダムに取り出すかどうかを指定します。

これはデータセットの並び方の偏りによって変える項目です。

今回のデータセットは0,1,2,3,4,5,6,7,8,9が満遍なく分布していますが、これがもし0のデータが100個、次に1のデータが100個と、次に2データ100個といった並び方をしている場合に、データの前方50%を学習データに、後方50%をテストデータにするといった選び方をすると、学習できるのは0,1,2,3,4　テストに使われるのは5,6,7,8,9となるので全く役に立たないでしょう。shuffleをTrueに指定することでランダムにデータを取り出し、この問題を避けることができます。このshuffleはデフォルトではTrueです。


## 学習
```python
# We learn the digits on the first half of the digits
classifier.fit(X_train, y_train)
```
サポートベクターマシン分類機にデータセットの画像データとラベルを与えて学習を行っています。


## 判別
```python
# Now predict the value of the digit on the second half:
predicted = classifier.predict(X_test)
```
学習を終えたサポートベクターマシン分類機にテスト用画像データだけを与えて、サポートベクターマシンが出力した識別結果ラベルをpredictedに保存しています。

## 画像と判別結果の表示
```python
images_and_predictions = list(zip(digits.images[n_samples // 2:], predicted))
for ax, (image, prediction) in zip(axes[1, :], images_and_predictions[:4]):
    ax.set_axis_off()
    ax.imshow(image, cmap=plt.cm.gray_r, interpolation='nearest')
    ax.set_title('Prediction: %i' % prediction)
```
*手書き数字データの可視化*に近いところがあります。

あちらはimages_and_labelsに画像とラベルを代入していましたが、今回はテストデータになっているデータセットの後ろ半分と識別結果ラベルで画像を作成し、2×4のグラフ画像の下4つを埋めています。

データセットの後ろ半分と識別結果ラベルで画像を描いているので、分割時の変数を変更した際等に問題が発生しそうな実装になっています。というかしました。分割の割合を変更したときに、こちらのデータセットのインデックスを変更していないと画像おかしくなります。

```python
images_and_predictions = list(zip(digits.images[n_samples // 2:], predicted))
```
y_testを使わなかったのは、y_testが一次元化された画像の配列だからでしょう。

このままでは汎用性が無くなるので、X_testの1次元配列の画像を2次元にしてみます。

リストの形状を*リスト形状から自動決定×8×8*に変更してやります。

```python
images_and_predictions = list(zip(X_test.reshape((-1,8,8)), predicted))
```
この実装だと、分割時の変数を変更しても問題無く実行できました。
![prediction.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216739/0b6b5aa5-1e9c-ecbe-6551-c9490e8376fc.png)

## 分類機の性能の表示
```python
print("Classification report for classifier %s:\n%s\n"
      % (classifier, metrics.classification_report(y_test, predicted)))
```
分類機の性能評価に移ります。分類機名としてSVC(gamma=0.001)、分類機の評価指標を示します。

[sklearn.metrics.classification_report](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.classification_report.html?highlight=metrics%20classification_report)
![指標.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216739/3c80f519-fa87-e3c3-a77f-30148b560f93.png)
分類機には3つの性能指標があります
A,Bの2つを判別する分類機があったとして、主要な指標は以下のようになります。例えばレントゲンの画像診断でガン患者を検出する際に、ガンならA、ガンではないならBとする学習機があったとします。

### *Presion* 精度　予測の正しさ
```math
\dfrac {Aと予測してAだった回数} {Aと予測してAだった回数 + Aと予測してBだった回数}
```
*予測*がAの場合の正しい確率

これは予測の正しさを示す指標です。正しく予測できるのか否かということを示す指標で、「とりあえず全部Aと回答する」という不良学習機と比較します。


### *Recall* 再現率　発見率
```math
\dfrac {Aと予測してAだった回数} {Aと予測してAだった回数 + Bと予測してAだった回数}
```
*結果*がAの場合の正しい確率

これは結果の正しさを示す指標です。結果がAのものをどれだけ見逃さずに発見できるかを示す指標です。


例えば、「これは明らかにガンである」と断言できない場合は「ガンではない」と診断する学習機があれば、Presionは限りなく1に近づきますが、Recallは低くなります。

逆に「怪しいものは全てガンだと診断する」学習機の場合はRecallは限りなく1に近づきますが、Presionは低くなります。

Presionだけが高い場合は*見逃し*が多いのであまり良い学習機とは言えませんし、Recallだけが高い場合は*間違い*が多くなりますから、こちらもあまり良いい学習機とは言えません。
理想的な学習機では、PresionとRecallのどちらも1に近くなります。
しかし現実ではそうも言っていられないので、この2つの指標を組み合わせて学習機を評価する指標が要求されました。

### *f1-score* 
presionを*P*と置き、Recallを*R*とした場合

```math
\dfrac {2PR} {P+R}
```
で算出される調和平均スコアで、慣例として用いられており、大きい程良いです。

画面にはこの３つのスコアと、対応するデータ数として*support*が表示されます。

## 混同行列の表示
予測ラベルと実際のラベルの比較結果を示す行列である[混同行列](https://scikit-learn.org/stable/auto_examples/model_selection/plot_confusion_matrix.html#sphx-glr-auto-examples-model-selection-plot-confusion-matrix-py)を、配列と画像で表示します。

まずは画像です。
横軸が推測したラベル、縦軸が実際のラベルで、交点が該当した回数を示しています。
![混同行列.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216739/c424adc3-5fd9-65cc-44e3-86ebcd2ecef6.png)

```python
disp = metrics.plot_confusion_matrix(classifier, X_test, y_test)
disp.figure_.suptitle("Confusion Matrix")
```

[sklearn.metrics.plot_confusion_matrix](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.plot_confusion_matrix.html?highlight=plot_confusion_matrix)に分類機とテストデータを与えることで返り値が[sklearn.metrics.ConfusionMatrixDisplay](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.ConfusionMatrixDisplay.html#sklearn.metrics.ConfusionMatrixDisplay)になります。返り値の図に[matplotlib.pyplot.suptitle](https://matplotlib.org/3.3.3/api/_as_gen/matplotlib.pyplot.suptitle.html)でタイトルを付与しています。

```python
print("Confusion matrix:\n%s" % disp.confusion_matrix)
```
これは単純に配列を文字列として表示しているだけです。
![混同行列リスト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216739/b71f6cd5-7c4f-2661-aef7-501c2b81594d.png)

```python
plt.show()
```
最後にplt.show()でmatplotlibの画像を表示します。
このとき、画像の表示は文字列の表示よりも時間がかかるので、表示順が前後します。

#おわりに
scikit-learnにより、機械学習の取り扱いが相当に簡単になったように思います。ライブラリを有効活用することで、効率的な開発を行っていきたいところです。