オートエンコーダによる次元圧縮

# オートエンコーダとは

通常の教師あり学習では入力から異なる種類の出力を得ますが、オートエンコーダでは入力と出力の形式は同じになります.

では何を学習するのかといいうと、データの潜在変数を学習します.
以下がモデルの構造になります.

![Deep Learning - VAE.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315124/835f9513-9d61-b241-278c-da31c4dfe9f4.jpeg)


まず、入力値はエンコーダーを介して、より次元の少ない潜在変数に変換されます.

その後、潜在変数はデコーダを介して元の次元に復元されます.

デコーダによって潜在変数から元の情報を復元できるなら、うまく学習できているこということになります.

ちなみにこうした、**ある次元のデータを元の情報を含むより少ない次元に変換する**ことを**次元削減**といます.

# オートエンコーダの用途

こことでは、オートエンコーダのさまざまな用途を紹介していきます.

### 事前学習

最近では、学習アルゴリズムが改善され、あまり使われなくなった方法です.
学習ずみのエンコーダの重みを他の識別器の重みの初期値として用います.
これにより、ランダムな値で初期値をセットした場合と比較して、勾配消失が抑制されます.

### **次元圧縮**

うまく学習されれば、潜在変数はにより元の次元数より削減されており、よりデータの特徴を抽出した変数になっているといえます.
情報量がほぼ変わらず、**次元が減っているので、より**データが疎(**スパース)であるといったりします.**

教師あり学習では、入力データのスパース性が高いほど、**過学習しにくくなります.**
つまり、**次元圧縮した潜在変数を入力値に用いて学習することで、過学習を抑制できます.**

### クラスタリング

教師なし学習であるクラスタリングに用いられることもあります.

### 異常検知

オートエンコーダを正常なデータを使って学習した場合、オートエンコーダは正常なデータの特徴を捉えます.

そこに異常な値を入れると、正常なデータを復元する作用が働きます.
するとどうなるかというと、異常な入力値と、最終的に出力値の間には差が生まれます.
逆に、正常なデータ（学習に使用したデータ)であればすでに学習ずみなので、入出力の差は小さいでしょう.

つまり、入力値とデコーダーの出力値を比較して入力値と出力値との差が大きければ大きいほど、入力値は学習データから遠いデータ、つまり異常値であると言えます.
このことを利用して、オートエンコーダは異常検知によく用いられます.

# 様々な派生モデル

オートエンコーダを基本形としてさまざまな派生モデルがあります.
ここではいくつか代表的なものを紹介します.

## 変分オートエンコーダ(VAE)

生成モデルとして使われるVAEをオートエンコーダの潜在変数に正規分布を仮定してランダム性を取り入れています.

これにより以下のメリットが得られます
- データがどんな分布から生成されたのかを推定可能になる.
- 連続的なデータが生成可能になる.

## 畳み込みオートエンコーダ(CAE)

画像をオートエンコーダで扱うために畳み込み層を取り入れたものです.
通常のオートエンコーダは全結合層でエンコード、デコードしますが、
CAEでは、全結合層が畳み込み層、プーリングに置き換わります.

# 実装例

ここでは簡単な実装例を紹介します.
ライブラリ: Tensorflow
データセット: FashionMnist

FashionMnistはモノクロの服のイラストとラベルがセットになったデータセットです. 学習用に60000枚,テスト用に10000枚用意されています.

[https://github.com/zalandoresearch/fashion-mnist](https://github.com/zalandoresearch/fashion-mnist)

## ネットワーク定義

```python
from dataclasses import dataclass

import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras import Model

@dataclass
class VAELoss:
    rc: float
    kl: float

    @property
    def total(self):
        return self.rc + self.kl

class CAE(Model):

    def __init__(self, optimizer):
        super().__init__()
        self.encoder = Encoder(latent_dim=20)
        self.decoder = Decoder()
        self.loss_function = LossFunction()
        self.optimizer = optimizer

    def call(self, x: tf.Tensor):
        z = self.encoder(x)
        y = self.decoder(z)
        return z, y

    @tf.function
    def train_step(self, y_true: tf.Tensor) -> (tf.Tensor, tf.Tensor, float):
        with tf.GradientTape() as tape:
            z, y = self.call(y_true)
            loss = self.loss_function(y, y_true)
        grads = tape.gradient(loss, self.trainable_weights)
        self.optimizer.apply_gradients(zip(grads, self.trainable_weights))
        return z, y, loss

    def test_step(self, y_test: tf.Tensor) -> (tf.Tensor, tf.Tensor, float):
        z, y = self.call(y_test)
        loss = self.loss_function(y, y_test)
        return z, y, loss

    def predict(self, image):
        tensor = tf.convert_to_tensor(image, dtype=tf.float32)
        tensor = tf.expand_dims(tensor, axis=0)
        z, y = self.call(tensor)
        z = tf.squeeze(z, [0])
        y = tf.squeeze(y, [0])
        return z, y

class Encoder(layers.Layer):

    def __init__(self, latent_dim=10):
        super().__init__()
        self.c1 = layers.Conv2D(32, 3, activation="relu", strides=2, padding="same")
        self.c2 = layers.Conv2D(64, 3, activation="relu", strides=2, padding="same")
        self.f1 = layers.Flatten()
        self.d1 = layers.Dense(16, activation="relu")
        self.d2 = layers.Dense(latent_dim, name="z")

    def call(self, x):
        x = self.c1(x)
        x = self.c2(x)
        x = self.f1(x)
        x = self.d1(x)
        z = self.d2(x)
        return z

class Decoder(layers.Layer):

    def __init__(self):
        super().__init__()
        self.d1 = layers.Dense(7 * 7 * 64, activation="relu")
        self.r1 = layers.Reshape((7, 7, 64))
        self.c1 = layers.Conv2DTranspose(64, 3, activation="relu", strides=2, padding="same")
        self.c2 = layers.Conv2DTranspose(32, 3, activation="relu", strides=2, padding="same")
        self.c3 = layers.Conv2DTranspose(1, 3, activation="sigmoid", padding="same")

    def call(self, x):
        x = self.d1(x)
        x = self.r1(x)
        x = self.c1(x)
        x = self.c2(x)
        y = self.c3(x)
        return y

class LossFunction:

    def __call__(self, y, y_true):
        rc_loss = tf.reduce_mean(
            tf.reduce_sum(
                tf.keras.losses.binary_crossentropy(y_true,  y), axis=(1, 2)
            )
        )
        return rc_loss
```

## **メトリクスの定義**

```python
import tensorflow as tf
from tensorflow.keras import metrics

class CAETrainMetrics:

    def __init__(self):
        self.loss = metrics.Mean(name='train_rc_loss')

    def update_on_train(self, z, y, loss):
        self.loss.update_state(loss)

    def update_on_test(self, z, y, loss):
        pass

    def reset(self):
        self.loss.reset_states()

    def display(self, epoch: int):
        template = 'Epoch {}, Loss: {:.2g}'
        print(
            template.format(
                epoch,
                self.loss.result()
            )
        )
```

## **学習器の定義**

以下のイテレーションを書いてます

1. バッチ化したデータをループで取得
2. ネットワークに突っ込んで予測と損失を取得
3. メトリクスに記録
4. 1~3を繰り返す

```python
class CAETrainer:

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

    def fit(self, dataset,  epochs: int = 3):
        """ Train mdoel by epochs
        """
        for e in range(1, epochs + 1):

            # train model
            for images in dataset.train_loop():
                z, y, loss = self.model.train_step(images)
                self.metrics.update_on_train(z, y, loss)

            # test model
            for images in dataset.test_loop():
                z, y, loss = self.model.test_step(images)
                self.metrics.update_on_test(z, y, loss)

            # show metrics
            self.metrics.display(e)
            self.metrics.reset()
```

## **データセットの定義**

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

1. 255で割って正規化
2. tf.data.Dataset.from_tensor_slicesでバッチ化

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

def build_images_dataset(images, batch_size=32):
    dataset = tf.data.Dataset.from_tensor_slices(
        images
    ).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

        self.train_images = self.train_images.reshape((*self.train_images.shape, 1))
        self.test_images = self.test_images.reshape((*self.test_images.shape, 1))

    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_test_image(self, i: int):
        return self.test_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).squeeze(), cmap=plt.cm.binary)
            plt.xlabel(self.get_train_label_name(i))

    def train_loop(self, batch_size: int = 28):
        return build_images_dataset(self.train_images)

    def test_loop(self):
        return  build_images_dataset(self.test_images)
```

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

```python
from tensorflow.keras import optimizers

# モデル初期化
cae = CAE(optimizer=optimizers.Adam())

# メトリクス初期化
cae_metrics = CAETrainMetrics()

# 学習器初期化
cae_trainer = CAETrainer(cae, cae_metrics)

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

## **データの可視化**

データの中身を確認します。

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

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

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

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315124/4b3da6e5-43e4-dae1-9b18-68419cecc3e6.png)

## 学習

```python
# 学習実行
cae_trainer.fit(fashion_mnist_dataset, epochs=3)
```

```
Epoch 1, Loss: 2.7e+02
Epoch 2, Loss: 2.5e+02
Epoch 3, Loss: 2.4e+02
```

## **学習結果**

```python
from PIL import Image

class ImageView:

    def display(self, image):
        if hasattr(image, 'numpy'):
            image = image.numpy()
        plt.figure(figsize=(3,3))
        plt.xticks([])
        plt.yticks([])
        plt.grid(False)
        plt.imshow(image.squeeze(), cmap=plt.cm.binary)

INDEX = 0
image = fashion_mnist_dataset.get_test_image(INDEX)
z, y = cae.predict(image)

view = ImageView()
view.display(imag
```

ぼやけて影みたいにはなってますが、シルエットは学習できているようです(細かい明暗ができていないのはなぜでしょう。。)

![download-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315124/405115a0-a2ff-a497-72fe-77dccf932958.png)
![download-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315124/83059546-ff55-6730-0571-968072705501.png)



# おわりに

オートエンコーダの理論と実装方法を紹介してみました。
実装は割と簡単な方なので初学者がtryするにはいいかもです。

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