Tensorflow2(Subclassing API)によるMNIST画像分類の実装～精度評価


# はじめに

今回は以下について紹介します。

* 手書き数字のデータセットとして有名なMnistをTensoflowAPIを使わずにダウンロード
* Tensorflow Subclass APIによる分類モデルの実装～評価まで一連の流れ

    + データのダウンロード・読み込み
    + データの前処理およびジェネレータによる出力
    + モデルの学習
    + モデルの評価

なお[ここ](https://qiita.com/hiro871_/items/8e8fd65c28d1e2a13fa9)でSubclass APIも含めた機械学習のモデルを構築する3つの方法について網羅的に紹介しているのでご覧ください。

## 【対象読者】

+ 色んなpythonライブラリを活用して分類モデルを実装してみたい人
+ TensorFlow2.xのチュートリアルレベルよりスキルをアップグレードしたい人
+ 実装した分類モデルを 様々な指標で評価したい人

# 1. データの準備

## 1-1) Mnist csvファイルのダウンロード

まず、Mnistデータセットを準備しましょう。 

[前の記事](https://qiita.com/hiro871_/items/8e8fd65c28d1e2a13fa9)でも紹介しましたが、
Mnistとは‘Mixed National Institute of Standards and Technology database’の略語で手書き数字画像60,000枚と、テスト画像10,000枚を集めた画像データセットです。
[前回](https://qiita.com/hiro871_/items/8e8fd65c28d1e2a13fa9)同様、`tensorflow.keras.datasets.mnist`でMnistデータセットを簡単にロードできますが、
今回はMnistデータセットを私が作りたい形で変更してPickle形式で保存して利用しようと思っているので、
 [Mnist in CSV（kaggle）](https://www.kaggle.com/oddrationale/mnist-in-csv)でMnistのcsvファイルをダウンロードしました。

 MnistのCSVファイルを開くと数字の羅列がされたデータなので、どのような意味を持ったファイルなのか画像処理初心者では把握するのが難しい印象です。 
しかし、意味が分かれば難しくなく…

- 結論から言うとMnistのCSVファイルの1つの行は1つの画像情報に当たります。 
- 各行の先頭の数字はラベルとしてその行のイメージがどんな数字か表示しています。 
- それ以外の784個の数字は0～255の間の値(画素値)で縦横28×28(784)を表現しているにすぎません。

![mnist_2_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206716/41d23339-f854-55f4-8de2-b60bbe6c5f8d.png)


## 2-1) Pickle形式で保存

csvファイルそのまま使用して学習させても大丈夫ですが、Pickle形式のファイルで保存してみましょう。
Pickleはオブジェクトをファイルとして保存するものであり、保存時・読み込み時に特別な設定や処理をすることなくオブジェクトをそのまま読み書きできるという利点があるのでPickle形式で保存することが都合のいい場合があります。
csvファイルをPickle形式で保存するには`pandas.DataFrame`を使うと簡単に作成できます。
( ※ tf.data.Datasetの活用が推奨されているようですが、今回の記事では説明を割愛します。)

### (1) ライブラリのインポート

ライブラリをインポートします。

```python
import os, sys
import csv
import numpy as np
import pandas as pd
import tensorflow as tf
```

`pandas`はPythonのデータ解析用のライブラリで機械学習ではデータの整理、可視化、前処理するため使います。

### (2) データの読み込み

Mnistデータセットをダウンロード(URLは上記1-1参照)して保存したパスを指定し`pd.read_csv`でcsvファイルを読み込んでいます。

```python
file = open("/Mnistデータセットが保存されているパス/mnist_train.csv")
data_train = pd.read_csv(file)
```

`read_csv`関数の引数で区切り文字の指定する`sep`やインデックスラベルの指定する`index_col`等を設定することができますが、ここではcsvファイルを読み込むだけなので詳しい説明は省略します。

### (3) イメージとラベルを分離してpandas.DataFrameへの変更

pythonのスライス記法という要素の指定方法でcsvファイルのイメージとラベルを分離します。

```python
 x_train = np.array(data_train.iloc[:, 1:]) # イメージのデータ
 y_train = np.array(data_train.iloc[:, 0]) # ラベルのデータ

img_id = list(range(len(x_train)))

df = pd.DataFrame({'img_id':img_id,'mnist_img':list(x_train), 'mnist_label':list(y_train)})
```

`pd.DataFrame`は二次元の表形式のデータ（テーブルデータ）を取り扱う、pandasのインスタンスです。
（※ 一次元の表形式は`pd.Series`です。）

`pd.DataFrame`はまずPythonの辞書型データの定義した後、DataFrameを定義する流れになります。

+ Pythonの辞書型データの定義
  `｛“キー”：list型のデータ,...｝`
  + 'キー'の数　＝　列（Columns）の数
  + `list`型のデータの数　＝　行（index）の数

+ 作った辞書型データを`pd.DataFrame`メソッドに引数として入力

x_tain, y_trainはデータ型は`numpy.ndarray`のため、上記の通り`list`型で変更しなければ`pandas.DataFrame`で保存できません。

```Python
display(df) # 確認
```

 上記のコードで確認してみると下記の表ように画面で見目よい表示されます。

| | img_id |mnist_img|mnist_label|
|----------:|----------:|----------:|----------:|
| 0 | 0 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |5|
| 1 | 1 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |0|
| 2 | 2 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |4|
| ...| ... | ... |...|
| 59998 | 59998 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |6|
| 59999 | 59999 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |8|

60000 rows × 3 columns

### (4) 指定したパスでPickle形式のファイルを保存

```python
dataset_name = 'ファイルの名'
filename = '{}_train_df.pkl'.format(dataset_name)
filepath = os.path.join(ファイルまでのパス, filename)
df.to_pickle(filepath)
```

テストデータセットのファイルも同じ方法でPickle形式への保存ができますので、上のコードを参考して作成してください。



# 2. ジェネレータの実装

今回はオリジナルのクラスでジェネレータを実装します。
`generator.py`というファイルにジェネレータを実装します。

ジェネレータとはイテレータ(；反復可能オブジェクト)の一種であり、1要素を取り出そうとする度に処理を行い、要素をジェネレートするタイプのものです。
AIのプログラムにおいては、モデルに数枚づつ画像を提供し続ける処理(ミニバッチ処理)が必要になる為、ジェネレータでその機能を実現します。

イテレータを理解すればジェネレータがどんなものかすぐわかると思いますので話が横道にそれますが、イテレータの起動についてしばらく説明します。

1. オブジェクトの`__iter__()`メソッドが呼ばれ、イテレータ実装を返すことが求められます。
2. この返り値で得られたオブジェクトは`__next__()`というメソッドが呼ばれます。
3. `__next__()`メソッドは１つずつ要素を取り出します。
4. `__next__()`メソッドはイテレータが尽きている場合、 default が与えられていればそれが返され、 そうでなければ StopIteration が送出されます。

下記のコードは私が作ったジェネレータの全体です。上記の内容を参考にコードの動作を考えてみると良いかと思われます。
以降でコードの重要な部分を少し詳しく説明します。

```python
class BatchGenerator():
    def __init__(self, df, shuffle = False, random_state = None):
        self.data_list = df.to_dict(orient='records')
        self.batch_size = 32
        self.input_shape = (28, 28, 1)
        self.img_size = 28
        self.shuffle = shuffle
        if random_state is None:
            random_state = np.random.RandomState(1234)
        self.random_state = random_state
        self._idx = 0
        self._reset()

    def __len__(self):
        N = len(self.data_list)
        b = self.batch_size
        return N // b + bool(N % b)

    def __iter__(self):
        return self

    def __next__(self):
        if self._idx >= len(self.data_list):
            self._reset()
            raise StopIteration()

        selected_data_list = self.data_list[self._idx:(self._idx + self.batch_size)]

        img_list = []
        label_list = []
        batch = {}

        for data in selected_data_list:
            img = self.load_img(data)
            agu_img = self.data_augmentation(img)
            agu_img = np.expand_dims(agu_img, axis=-1)
            one_hot_labeled = self.one_hot_labeling(data)
            img_list.append(agu_img)
            label_list.append(one_hot_labeled)

        batch['batch_id'] = np.asarray([i['img_id']for i in selected_data_list])
        batch['batch_mnist_img'] = np.asarray(img_list)
        batch['batch_mnist_label'] = np.asarray(label_list)

        self._idx += self.batch_size

        return batch

    def _reset(self):
        if self.shuffle:
            self.data_list = shuffle(self.data_list, random_state=self.random_state)
        self._idx = 0

    def load_img(self, data):
        img = data['mnist_img']
        img = img/255.0
        img = img.reshape(28, 28, 1)
        img = img.astype('float32')

        return img

    def one_hot_labeling(self, data):
        one_hot_label = tf.keras.utils.to_categorical(data['mnist_label'], num_classes=10)

        return one_hot_label

    def data_augmentation(self,image):

        bg_value = np.median(image)

        angle_1 = np.random.randint(-45, 45, 1)
        mat_1 = cv2.getRotationMatrix2D((self.img_size/2, self.img_size/2), angle_1[0], 1)
        affine_img = cv2.warpAffine(image, mat_1, (self.img_size, self.img_size), borderValue=(0, 0, 0))

        tr_x = self.img_size*np.random.uniform()-self.img_size/2
        tr_y = self.img_size*np.random.uniform()-self.img_size/2
        mat_2 = np.array([[1,0,tr_x],[0,1,tr_y]], dtype=np.float32)
        affine_img_translation = cv2.warpAffine(affine_img, mat_2, (self.img_size, self.img_size))

        return affine_img_translation  
```

## 2-1) データをbatch_sizeほど読み込み

`__iter__()`メソッドにより`__next__()`メソッドが呼ばれます。
`__next__()`メソッドでは設定した`self.batch_size`だけ`self.data_list`からデータを取得します。

`self.data_list`は`df.to_dict(orient)`が割り当てられていますが、これは`pandas.DataFrame`を辞書型（dict）に変換することです。

```
 df.to_dict(orient)
```

__引数__

+ orient : 以下の形式を指定することが可能です。
  + dict（デフォルト） : keyが列ラベル、valueが行ラベルと値の辞書となる。
    `{column -> {index -> value}}`
  + list：keyが列ラベル、valueが値のリストとなる。行名の情報は失われる。
    `{column -> [values]}`
  + series：keyが列ラベル、valueが行ラベルと値のpandas.Seriesとなる。
    `{column -> Series(values)}`
  + split：keyが'index', 'columns', 'data'となり、それぞれのvalueが行ラベル、列ラベル、値のリストとなる。
    `{index -> [index], columns -> [columns], data -> [values]}`
  + records : keyが列ラベル、valueが値となる辞書を要素とするリストとなる。行名の情報は失われる。
    `[{column -> value}, ... , {column -> value}]`
  + index：keyが行ラベル、valueが列ラベルと値の辞書となる。
    `{index -> {column -> value}}`

__戻り値__

+ `dict`、 `list`または`collections.abc.Mapping`

<br/>

## 2-2) データの前処理

`load_img`と`one_hot_labeling`関数でデータの前処理を行います。Mnistのデータの普通の前処理流れはです。

### (1) イメージの前処理

```python
def load_img(self, data):
    img = data['mnist_img']
    img = img/255.0
    img = img.reshape(28, 28, 1)
    img = img.astype('float32')

    return img
```

+ データを (600000, 784) から (60000,28,28,1)に`reshape`します。
  + 最後の１はチャンネルでグレースケール化するために追加します。

+ イメージを255.で割り算を行い、0~1に正規化(normalization)します。
  + 正規化をすることで、精度がよくなります。

+ データ型を`float32`で変更する。
  + ニューラル ネットワークをトレーニングする時は32ビット整数を使用するのが一般的なため、`float32`で変更します。

### (2) ラベルの前処理

ラベルはOne-Hotエンコーディングします。One-Hotエンコーディングとはカテゴリカル変数に対して、各要素が該当するなら1、該当しないなら0とするカラムを変数の要素数分作ることです。

Mnistのラベルは[0 ~ 9]の10個の数字が存在するのでone-hot結果の次元数は10になります。
例えば、‘5’である正解ラベルをOne-Hotエンコーディング場合は、[0.,0.,0.,0.,0.,1.,0.,0.,0.,0.]になります。

One-hot表現のメリットとデメリットは下記の通りです。

|メリット|デメリット|
|:----------:|:----------:|
|変数の全ての値を平等に扱えて、機械学習アルゴリズムの予想（prediction）結果向上|次元数が増えて、メモリ使用量や計算量が爆発的に増加|

```python
def one_hot_labeling(self, data):
    one_hot_label = tf.keras.utils.to_categorical(data['mnist_label'], num_classes=10)

    return one_hot_label
```

今回は`keras.utils`に実装された`to_categorical`を使います。
( ※ NumPyの`eye`または`identity`等もone-hot表現に変換できます。)

```
keras.utils.to_categorical(y, num_classes=None)
```

クラスベクトル（0からnb_classesまでの整数）を`categorical_crossentropy`とともに用いるためのバイナリのクラス行列に変換します。

__引数__

+ y: 行列に変換されるクラスベクトル（0からnum_classesまでの整数）
+ num_classes: 総クラス数

__戻り値__

+ 入力のバイナリ行列表現

## 2-3) DataAugumentation（データ拡張）

DataAugumentationとは学習データ(訓練データ)の画像に対して平行移動、拡大縮小、回転、ノイズの付与等の人為的に処理を加えてデータ水増しするテクニックです。機械学習における普遍的な課題である過学習（Overfitting）を予防する利点があります。

> 【過学習とは？】
統計学や機械学習において、あるデータ群を基につくったモデルが、そのデータ群に含まれない新しいデータ群に対しては、同水準の予測精度を示すことができない状態

DataAugumentationでは`tf.keras.preprocessing.image.ImageDataGenerator`を利用して簡単に色んな処理の実装ができますが、ここでは直接openCV（；画像や動画を処理するのに必要な、さまざま機能が実装されているライブラリ）を使ってイメージをを回転と平行移動の処理を加えてみました。

```python
def data_augmentation(self,image):

    bg_value = np.median(image)

    # 回転の処理
    angle_1 = np.random.randint(-45, 45, 1)
    mat_1 = cv2.getRotationMatrix2D((self.img_size/2, self.img_size/2), angle_1[0], 1)
    affine_img = cv2.warpAffine(image, mat_1, (self.img_size, self.img_size), borderValue=(0, 0, 0))

    # 平行移動の処理
    tr_x = self.img_size*np.random.uniform()-self.img_size/2
    tr_y = self.img_size*np.random.uniform()-self.img_size/2
    mat_2 = np.array([[1,0,tr_x],[0,1,tr_y]], dtype=np.float32)
    affine_img_translation = cv2.warpAffine(affine_img, mat_2, (self.img_size, self.img_size))

    return affine_img_translation  
```

openCVを使うためにデータ型が`numpy.ndarray`である必要がありますが、これらのイメージは既に`numpy.ndarray`データ型なので型変換はしなくても大丈夫です。

`data_augmentation`関数を適用したMnistは下のようになります。

![mnist_2_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206716/9fb569c6-1598-78d6-231d-222b0702ab8d.png)

左図は普通のMnistイメージで右図は回転と平行移動の処理されたイメージです。openCVを利用するこのように画像で細かいAugmentation処理を加えることができることがわかります。

# 3. Subclassing APIによるモデルを作成

TendorFlow 2.0以降ではモデルを構築する方法がSequential API、Functional API、Subclassing API 3つあります。各々の詳しい説明は前回の記事を参考してください。ここでは優柔なモデルを構築するため、３つの中で一番優柔性が高いKerasのmodel Subclassing APIを使って`tf.keras`モデルを作ります。

## 3-1) モデルの構築

モデルの構成は[25 Million Images! (0.99757) MNIST](https://www.kaggle.com/cdeotte/25-million-images-0-99757-mnist#Build-15-Convolutional-Neural-Networks!)を参考に作成してみました。モデルの具体的な実装方法を知りたい方は[前回の記事](https://qiita.com/hiro871_/items/8e8fd65c28d1e2a13fa9)をご覧ください。

```Python
class ClassificationModel(tf.keras.Model):
   def __init__(self):
       super(ClassificationModel, self).__init__()
       self.conv1_1 = layers.Conv2D(32, 3, activation='relu')
       self.conv1_2 = layers.BatchNormalization()
       self.conv1_3 = layers.Conv2D(32, 3, activation='relu')
       self.conv1_4 = layers.BatchNormalization()
       self.conv1_5 = layers.Conv2D(32, 5, strides=2, padding='same', activation='relu')
       self.conv1_6 = layers.BatchNormalization()
       self.drop1 = layers.Dropout(0.4)

       self.conv2_1 = layers.Conv2D(64, 3, activation='relu')
       self.conv2_2 = layers.BatchNormalization()
       self.conv2_3 = layers.Conv2D(64, 3, activation='relu')
       self.conv2_4 = layers.BatchNormalization()
       self.conv2_5 = layers.Conv2D(64, 5, strides=2, padding='same', activation='relu')
       self.conv2_6 = layers.BatchNormalization()
       self.drop2 = layers.Dropout(0.4)

       self.conv3_1 = layers.Conv2D(128, kernel_size = 4, activation='relu')
       self.conv3_2 = layers.BatchNormalization()
       self.conv3_3 = layers.Flatten()
       self.drop3 = layers.Dropout(0.4)
       self.d3 = layers.Dense(10, activation='softmax')

   def call(self, x):
       x = self.conv1_1(x)
       x = self.conv1_2(x)
       x = self.conv1_3(x)
       x = self.conv1_4(x)
       x = self.conv1_5(x)
       x = self.conv1_6(x)
       x = self.drop1(x)
       x = self.conv2_1(x)
       x = self.conv2_2(x)
       x = self.conv2_3(x)
       x = self.conv2_4(x)
       x = self.conv2_5(x)
       x = self.conv2_6(x)
       x = self.drop2(x)
       x = self.conv3_1(x)
       x = self.conv3_2(x)
       x = self.conv3_3(x)
       x = self.drop3(x)
       return self.d3(x)
```

## 3-2) モデルのビルド

```Python
model = ClassificationModel()
model.build((32, 28, 28, 1))
model.summary()
```

+ `model.build()`メソッド : 重みを定義するメソッド
+ `model.summary()`メソッド : 作成すると作ったモデルの構成を簡単確認ができるメソッド

```
Model: "classification_model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              multiple                  320       
_________________________________________________________________
batch_normalization (BatchNo multiple                  128       
_________________________________________________________________
conv2d_1 (Conv2D)            multiple                  9248      
_________________________________________________________________
batch_normalization_1 (Batch multiple                  128       
_________________________________________________________________
conv2d_2 (Conv2D)            multiple                  25632     
_________________________________________________________________
batch_normalization_2 (Batch multiple                  128       
_________________________________________________________________
dropout (Dropout)            multiple                  0         
_________________________________________________________________
conv2d_3 (Conv2D)            multiple                  18496     
_________________________________________________________________
batch_normalization_3 (Batch multiple                  256       
_________________________________________________________________
conv2d_4 (Conv2D)            multiple                  36928     
_________________________________________________________________
batch_normalization_4 (Batch multiple                  256       
_________________________________________________________________
conv2d_5 (Conv2D)            multiple                  102464    
_________________________________________________________________
batch_normalization_5 (Batch multiple                  256       
_________________________________________________________________
dropout_1 (Dropout)          multiple                  0         
_________________________________________________________________
conv2d_6 (Conv2D)            multiple                  131200    
_________________________________________________________________
batch_normalization_6 (Batch multiple                  512       
_________________________________________________________________
flatten (Flatten)            multiple                  0         
_________________________________________________________________
dropout_2 (Dropout)          multiple                  0         
_________________________________________________________________
dense (Dense)                multiple                  1290      
=================================================================
Total params: 327,242
Trainable params: 326,410
Non-trainable params: 832
_________________________________________________________________
```

## 3-3) 損失関数・オプティマイザの定義

```Python
loss_object = tf.keras.losses.SparseCategoricalCrossentropy()
optimizer = tf.keras.optimizers.Adam()

train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')

test_loss = tf.keras.metrics.Mean(name='test_loss')
test_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')
```

# 4. 学習

まず、先ほどの作った`generator.py`を呼び出すために、インポートします。
今回のケースでは`./data/にgenerator.py`を配置しています。

```Python
from data.generator import BatchGenerator
```

そして、保存したpickle形式のファイルを読み込みます。

```Python
train_df = pd.read_pickle("/フォルダのパス/指定したファイルの名_train_df.pkl")
```

インポートしたジェネレータを定義しましょう。

```python
train_gen = BatchGenerator(train_df, config)
batch = next(train_gen)
```

学習とテストを行います。

```Python
@tf.function
def train_step(x, t):
    with tf.GradientTape() as tape:
        predictions = model(x, training=True)
        loss = loss_object(t, predictions)
    gradients = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(gradients, model.trainable_variables))

    train_loss(loss)
    train_accuracy(t, predictions)

@tf.function
def test_step(x, t):
    test_predictions = model(x)
    t_loss = loss_object(t, test_predictions)

    test_loss(t_loss)
    test_accuracy(t, test_predictions)

EPOCHS = 5

for epoch in range(EPOCHS):
    for batch in tqdm(train_gen):
        x = batch['batch_mnist_img']
        t = batch['batch_mnist_label']
        t = np.argmax(t, axis=1).reshape(-1,1)
        train_step(x, t)　#学習

    for batch in tqdm(train_gen):
        x = batch['batch_mnist_img']
        t = batch['batch_mnist_label']
        t = np.argmax(t, axis=1).reshape(-1,1)
        test_step(x, t)　#テスト


    template = 'Epoch {}, Loss: {}, Accuracy: {}, test-Loss: {}, test-Accuracy:{}'
    print(template.format(epoch + 1,
                          train_loss.result(),
                          train_accuracy.result() * 100,
                          test_loss.result(),
                          test_accuracy.result()*100))
```

# 5. 分類モデルの評価

分類モデルを未知データ（；あるモデルの訓練データに含まれない新しいデータ群）を利用して評価しましょう。そのため、先ほどの作ったpickle形式のテストデータを読み込みます。

```python
test_df = pd.read_pickle("/テストデータが保存されているパス/ファイル名_test_df.pkl")
display(test_df)
```

| | img_id |mnist_test_img|mnist_real_label|
|----------:|----------:|----------:|----------:|
| 0 | 0 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |7|
| 1 | 1 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |2|
| 2 | 2 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |1|
| ...| ... | ... |...|
| 9998 | 9998 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |5|
| 9999 | 9999 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... |6|

10000 rows × 3 columns

今回はジェネレータを利用せずに、評価用のデータを準備します。

```python
img_id = pd.Series.tolist(test_df['img_id'])
mnist_test_img = pd.Series.tolist(test_df['mnist_test_img'])
mnist_test_label= pd.Series.tolist(test_df['mnist_real_label'])

mnist_test_img_list = [] #イメージ
mnist_test_label_list = []　#ラベル

for i in range(len(img_id)):
    mnist_test_img_list.append((mnist_test_img[i].reshape(28,28,1))/255.)
    mnist_test_label_list.append(mnist_test_label[i])
```

<br/>

分類モデルの評価に次のような指標を使います。
<br/>

## 5-1) 混同配列
まずは、混同配列です。混同行列の主対角成分の要素は、正確にクラス分類されたサンプル数を示し、それ以外の要素は、実際とは違うクラスに分類されたサンプル数を示します。

混同配列を作るにはsklearnライブラリまたはseabornライブラリを利用することができます。

### (1) sklearnライブラリを利用する方法

scikit-learnは、分類、回帰、クラスタリング等が実装されているPythonの機械学習ライブラリです。scikit-learnライブラリを読み込む時は`sklearn`と書きます。

```python
from sklearn.metrics import confusion_matrix

y_true = np.asarray(mnist_test_label_list)
y_pred = model(np.asarray(mnist_test_img_list))
y_pred = np.argmax(y_pred, axis=1) #reshape(-1,1)
confusion_matrix(y_true, y_pred)
```

```
array([[ 976,    0,    0,    0,    0,    1,    1,    1,    1,    0],
       [   0, 1127,    1,    2,    1,    0,    3,    0,    1,    0],
       [   3,    1, 1010,    2,    1,    0,    2,    8,    4,    1],
       [   0,    0,    0, 1003,    0,    2,    0,    1,    4,    0],
       [   0,    1,    0,    0,  975,    0,    2,    0,    0,    4],
       [   2,    0,    1,   12,    0,  862,    5,    0,    4,    6],
       [   8,    4,    0,    1,    6,    1,  934,    0,    4,    0],
       [   1,    1,    8,    1,    0,    0,    0, 1013,    1,    3],
       [   6,    2,    0,    3,    2,    0,    0,    4,  951,    6],
       [   3,    3,    0,    2,   13,    1,    0,    2,    4,  981]])
```

（※ 結果は実装したゼネレーターやモデルによって異なります。）

### (2) seabornライブラリを利用する方法

seabornはデータの可視化(グラフ作成)を行うPythonライブラリです。sklearnライブラリを利用すると結果は同じですが、sklearnライブラリより視覚的に見えます。インポート文で`seaborn`書くことで使用できます。

```python
import seaborn as sn
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt

def print_cmx(y_true, y_pred):
    labels = sorted(list(set(y_true)))
    cmx_data = confusion_matrix(y_true, y_pred, labels=labels)

    df_cmx = pd.DataFrame(cmx_data, index=labels, columns=labels)

    plt.figure(figsize = (12,7))
    sn.heatmap(df_cmx, annot=True, fmt='g' ,square = True)
    plt.show()

print_cmx(y_true , y_pred)
```

![mnist_2_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206716/d5dc8656-e1ed-930d-4c89-d314fa0b349b.png)


（※ 結果は実装したジェネレーターやモデル、乱数によって異なります。）

<br/>

## 5-2) 精度（正解率）

精度は、すべてのサンプルについて、正確に予測できた割合を示します。

```python
from sklearn.metrics import accuracy_score
print('正解率（accuracy）:', accuracy_score(y_true, y_pred))
```

```
正解率（accuracy）: 0.9832
```

（※ 結果は実装したジェネレーターやモデル、乱数によって異なります。）

感覚的にはこの指標が一番分かりやすいですが、陽性クラスと陰性クラスでサンプル数に大きな差があるとき、うまく性能を表せない場合があります。それで他の指標で確認する必要があります。

<br/>

## 5-3) 適合率

適合率は、陽性であると予測されたものが、実際にどのくらい陽性だったのかを示す指標です。偽陽性と予測することを低く抑えたい場合は、適合率を用います。

```python
from sklearn.metrics import accuracy_score
print('正解率（accuracy）:', accuracy_score(y_true, y_pred))
```

```
適合率（precision）: 0.9832536618523209
```

（※ 結果は実装したジェネレーターやモデル、乱数によって異なります。）

<br/>

## 5-4) 再現率

再現率は、実際に陽性のサンプルのうち、どのくらい陽性と予測されたかを示す指標です。偽陰性と予測することを低く抑えたい場合は、再現率を用います。

```python
from sklearn.metrics import recall_score
print('再現率（recall）:',recall_score(y_true, y_pred, average='macro'))
```

```
再現率（recall）: 0.9828852947538065
```

（※ 結果は実装したジェネレーターやモデル、乱数によって異なります。）

<br/>

## 5-5) F1値

適合率と再現率は重要な指標ですが、片方だけではモデルの全体像が把握できません。この2つの指標をまとめた評価指標がF値で、適合率と再現率の調和平均がF1値です。

```python
from sklearn.metrics import f1_score
print('F1値（F1-measure）:',f1_score(y_true, y_pred, average='macro'))
```

```
F1値（F1-measure）: 0.9830195547575524
```
（※ 結果は実装したジェネレーターやモデル、乱数によって異なります。）

# 6. まとめ

前回の記事より多くのpythonライブラリを利用して画像分類モデル実装・学習・評価しました。
またPytorchの書き方に似ているSubclass　APIによるモデルを実装したため、Pytorchユーザーにも馴染みやすい書き方だったと思います。
ぜひ試してみてください。

以上です。
記事に誤り等ありましたら、ご指摘ください。

# 7. 参考資料

1. [pandas.DataFrame, Seriesをpickleで保存、読み込み（to_pickle, read_pickle）][1]

2. [Pythonのイテレータとジェネレータ][2]

3. [TensorFlow, Kerasの基本的な使い方（モデル構築・訓練・評価・予測）][3]

4. [ディープラーニングの基礎(5) - DataAugumentation][4]

5. [Pythonのイテレータとジェネレータ（qiita）][5]

6. [Numpyユーティリティ ][6]

7. [pandas.DataFrame, Seriesを辞書に変換（to_dict）][7]

8. [機械学習の天敵「過学習」とは何か？][8]

9. [Data Augmentation Techniques in CNN using Tensorflow][9]

[1]: https://note.nkmk.me/python-pandas-to-pickle-read-pickle/
[2]: https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09
[3]: https://note.nkmk.me/python-tensorflow-keras-basics/
[4]: https://news.mynavi.jp/article/cv_future-46/
[5]: https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09
[6]: https://keras.io/ja/utils/np_utils/
[7]: https://note.nkmk.me/python-pandas-to-dict/
[8]: https://ailearn.biz/learn/20171119349
[9]: https://medium.com/@prasad.pai/data-augmentation-techniques-in-cnn-using-tensorflow-371ae43d5be9