MLPによる画像分類の実装

# はじめに

MLPを使った簡単な画像分類を実装方法を紹介していこうと思います.

理論も大事ですが、実装して動かさないとわからないことも多いので、とりあえず動かしてみるということはとても大事なことだと思います.

この記事ではMLPの画像分類を題材に、tensorflowを使って基本的なニューラルネットワークの定義から学習までを一通り実装していきます.

本記事のソースコードはこちらで実行できるので、是非動かしてみてくだい.

[https://colab.research.google.com/drive/10znVBfU2yw2mh2XFHasSmwSR5PuLPxyY?usp=sharing](https://colab.research.google.com/drive/10znVBfU2yw2mh2XFHasSmwSR5PuLPxyY?usp=sharing)

# データセット

データセットにはFashionMnistを用います.

FashionMnistはモノクロの服のイラストとラベルがセットになったデータセットです.

学習用に60000枚,テスト用に10000枚用意されています.

https://github.com/zalandoresearch/fashion-mnist

従来の手書き文字のMNISTでは簡単すぎるため、その代替として作られました.

# Step1: ネットワークの定義

まずネットワークを定義します.

今回はシンプルにニューロン数128の中間層を2つ挟んでみます.

Tensorrflowでの書き方は大きく

1. keras.Sequentialを用いる ( https://www.tensorflow.org/guide/keras/sequential_model?hl=ja)

2. keras.Modelを継承して定義する(https://www.tensorflow.org/api_docs/python/tf/keras/Model)

の2通りありますが、今回は2の方法で紹介します.

1はかなりサンプルコードが溢れているのと、よりモデルが複雑になった際に、拡張性や可読性の面で、2の方に優位性があるため、今後の発展を考えると2のやり方に慣れておくといいかと思います.

```python
import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras import losses
from tensorflow.keras import Model

class MLPClassifier(Model):

    def __init__(self, num_classes: int, optimizer):
        super().__init__()
        self.flat = layers.Flatten()
        self.d1 = layers.Dense(128, activation='relu')
        self.d2 = layers.Dense(128, activation='relu')
        self.d3 = layers.Dense(num_classes, activation='softmax')
        self.optimizer = optimizer
        self.loss_function = losses.SparseCategoricalCrossentropy()

    def call(self, x):
        x = self.flat(x)
        x = self.d1(x)
        x = self.d2(x)
        y = self.d3(x)
        return y

    @tf.function
    def train_step(self, x, y_true):
        """
        Args:
          x: shape (batch_size, height, width, channels)
          labels: shape (batch_size, num_labels, 1)
        """
        with tf.GradientTape() as tape:
            y = self.call(x)
            loss = self.loss_function(y_true, y)

        grad = tape.gradient(loss, self.trainable_variables)
        self.optimizer.apply_gradients(zip(grad, self.trainable_variables))
        return y, loss

    def test_step(self, images, y_true):
        y = self.call(images)
        loss = self.loss_function(y_true, y)
        return y, loss
```

# Step2: メトリクスの定義

メトリクスとは評価指標のことです.
機械学習全般で一般的に、損失と精度の二つを記録してプロットすることが多いです.
損失

- Step1のモデル内で計算しているので、平均をとります.

精度

- 単純に正解と予測のラベルが一致している割合を計算します.
- SparseCategoricalAccuracyを使います.

```python
from tensorflow.keras import metrics

class MLPMetrics:

    def __init__(self):
        self.train_loss = metrics.Mean(name='train_loss')
        self.train_accuracy = metrics.SparseCategoricalAccuracy(name='train_accuracy')
        self.test_loss = metrics.Mean(name='test_loss')
        self.test_accuracy = metrics.SparseCategoricalAccuracy(name='test_accuracy')

    def update_on_train(self, y_true, y, loss):
        self.train_loss(loss)
        self.train_accuracy(y_true, y)

    def update_on_test(self, y_true, y, loss):
        self.test_loss(loss)
        self.test_accuracy(y_true, y)

    def reset(self):
        self.train_loss.reset_states()
        self.train_accuracy.reset_states()
        self.test_loss.reset_states()
        self.test_accuracy.reset_states()

    def summary(self):
        return dict(
            train_loss=self.train_loss.result(),
            train_accuracy=self.train_accuracy.result() * 100,
            test_loss=self.test_loss.result(),
            test_accuracy=self.test_accuracy.result() * 100
        )
```

# Step3: 学習器の定義

学習する際の処理を書いてます.

kerasのfit APIを使えばいいのですが、今後の応用も考えてカスタムでの書き方を紹介します.

基本は

1. バッチ化したデータをループで取得

2. ネットワークに突っ込んで予測と損失を取得

3. メトリクスに記録

4. 1~3を繰り返す

の流れです

1~3全体ステップを**エポック**と言います

```python
class MLPTrainer:

    def __init__(self, model, metrics, history):
        self.model = model
        self.metrics = metrics
        self.history = history

    def fit(self, train_ds, epochs: int = 3):
        """ Train mdoel by epochs
        """
        for e in range(1, epochs + 1):
            # train model
            for images, labels in train_ds.train_loop():
                pred, loss = self.model.train_step(images, labels)
                self.metrics.update_on_train(labels, pred, loss)

            # validate model
            for images, labels in train_ds.test_loop():
                pred, loss = self.model.test_step(images, labels)
                self.metrics.update_on_test(labels, pred, loss)

            # get metrics
            summary = self.metrics.summary()
            self.metrics.reset()

            # diisplay learnig state
            self.history.update(summary)
            self.history.display_current()

        # plot learnig process
        self.history.plot_learning_process()

class MLPTrainHistory:

    def __init__(self):
        self.init()

    def init(self):
        self.train_accuracies = []
        self.train_losses = []
        self.test_accuracies = []
        self.test_losses = []
        self.epochs = 0

    def update(self, summary: dict):
        self.train_losses.append(summary["train_loss"])
        self.train_accuracies.append(summary["train_accuracy"])
        self.test_losses.append(summary["test_loss"])
        self.test_accuracies.append(summary["test_accuracy"])
        self.epochs += 1

    def display_current(self):
        template = 'Epoch {}, Loss: {:.2g}, Accuracy: {:.2g} Test Loss: {:.2g}, Test Accuracy: {:.2g}'
        print(
            template.format(
                self.epochs,
                self.train_losses[-1],
                self.train_accuracies[-1],
                self.test_losses[-1],
                self.test_accuracies[-1]
            )
        )

    def plot_learning_process(self):
        epochs = range(1, self.epochs+1)
        plt.plot(epochs, self.train_accuracies, label='Train Accuracy', ls="-", marker="o")
        plt.plot(epochs, self.test_accuracies, label='Validation Accuracy', ls="-", marker="x")
        plt.title('Accuracy')
        plt.ylabel("accuracy")
        plt.xlabel("epoch")
        plt.legend(loc="best")
        plt.show()

        plt.plot(epochs, self.train_losses, label='Training Accuracy', ls="-", marker="o")
        plt.plot(epochs, self.test_losses, label='Validation Accuracy', ls="-", marker="x")
        plt.title('Loss')
        plt.ylabel("loss")
        plt.xlabel("epoch")
        plt.legend(loc="best")
        plt.show()
```

# Step4: データセットの定義

FashionMnistをロードして学習で使える形に以下の加工をしています

1. 255で割って正規化

2. tf.data.Dataset.from_tensor_slicesでバッチ化

```python
import matplotlib.pyplot as plt
from tensorflow.keras import datasets

def build_image_labels_dataset(images, labels, batch_size=32):
    dataset = tf.data.Dataset.from_tensor_slices(
        (images, labels)
    ).batch(batch_size)
    return dataset

class FashionMnistDataset:
    class_names = (
        'T-shirt/top',
        'Trouser', 
        'Pullover', 
        'Dress', 
        'Coat', 
        'Sandal', 
        'Shirt', 
        'Sneaker',
        'Bag', 
        'Ankle boot')

    def __init__(self):
        (train_images, train_labels), (test_images, test_labels) = datasets.fashion_mnist.load_data()
        self.train_images = train_images.astype("float32") / 255.0
        self.train_labels = train_labels
        self.test_images = test_images.astype("float32") / 255.0
        self.test_labels = test_labels

    def info(self):
        return dict(
            train=dict(
                shape=self.train_images.shape,
                size=len(self.train_images)
            ),
            test=dict(
                shape=self.test_images.shape,
                size=len(self.test_images)
            ),
        )

    def get_train_image(self, i: int):
        return self.train_images[i]

    def get_train_label_name(self, i: int):
        return self.class_names[self.train_labels[i]]

    def display(self):
        plt.figure(figsize=(10,10))
        for i in range(25):
            plt.subplot(5,5,i+1)
            plt.xticks([])
            plt.yticks([])
            plt.grid(False)
            plt.imshow(self.get_train_image(i), cmap=plt.cm.binary)
            plt.xlabel(self.get_train_label_name(i))

    def train_loop(self, batch_size: int = 28):
        ds = build_image_labels_dataset(self.train_images, self.train_labels)
        return ds

    def test_loop(self):
        ds = build_image_labels_dataset(self.test_images, self.test_labels)
        return ds
```

# **Step5: 各クラスの初期化**

定義した各クラスを初期化します

```python
from tensorflow.keras import optimizers

# モデル初期化
mlp_classifier = MLPClassifier(num_classes=10, optimizer=optimizers.Adam())

# メトリクス初期化
mlp_metrics = MLPMetrics()

# 履歴初期化
histiry = MLPTrainHistory()

# 学習用クラス初期化
mlp_trainer = MLPTrainer(mlp_classifier, mlp_metrics, histiry)

# データセットクラス初期化
fashion_mnist_dataset = FashionMnistDataset()
```

# Step6: データの可視化

データの中身を見てみましょう

```python
fashion_mnist_dataset.info()
```

結果

```python
{'test': {'shape': (10000, 28, 28), 'size': 10000},
 'train': {'shape': (60000, 28, 28), 'size': 60000}}
```

プロットしてみます

```python
fashion_mnist_dataset.display()
```

結果
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315124/169c0747-02a8-bfd1-c59f-141298eb56f9.png)


# Step7: 学習の実行

学習を実行していきます.
適当にエポック数10で回してます.
学習曲線を見ると3エポックくらいから学習データに対して精度が上がってるけど、評価データに対してはあまり上がっていないですね
これは一般的に過学習と言われる現象で、ニューラルネットの学習の課題の1つになります.
ここで詳細に触れませんでしたが、学習における課題についてはまた別の機会に書こうと思います.

学習実行

```python
mlp_trainer.fit(fashion_mnist_dataset, epochs=10)
```

実行結果

![download-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315124/1ae741d8-9ebc-6fe1-384d-1bf9173f067a.png)
![download-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315124/6591abb9-f200-fe35-19d2-66421026e755.png)



# おわりに

最後まで読んでいただきありがとうございました！

https://www.wantedly.com/companies/sweeep/projects