VAEで取得した潜在変数をクラスタリングしてみた

## 概要
- はじめに
- 手順
- VAEとはなにか
- k-Means法とはなにか
- VAEの実装
- k-Means法の実装
- k-Means法の学習
- Testデータでの予測結果
- 他の手法との比較
- おわりに
- 参考

## はじめに
VAEで取得した潜在変数に対してクラスタリングすることで、MNISTを精度0.5087で分類することができました。
![vaeandkmeans.png](https://qiita-image-store.s3.amazonaws.com/0/229031/95ec9cc7-c175-b67c-10aa-dd307641429d.png)

## 手順
1. VAEを学習させる
1. VAEのEncoderにデータを渡し、潜在変数を取得する
1. 取得した潜在変数をk-Means法でクラスタリング

クラスタリングをする潜在変数として、Encoderの出力である正規分布の平均（正規分布なので、ランダムサンプリングした際に、平均の場所にマッピングされる可能性が一番高い）を使用します。

k-Means法でクラスタリングする際、最初の重心をラベル付きのデータにします。そして、各点が属するクラスタを求めます。つまり半教師あり学習を行います。なぜならMNISTの分類精度を求めるために、単なる分類ではなく、ラベル付きの分類を行いたいからです。

## VAEとはなにか
Encoderで正規分布の平均と共分散行列を出力させ、潜在変数zはその正規分布からサンプリングしたものになります。潜在変数zは同じクラスラベルのデータは近いところに集まることが期待できるそうです。

記事[1][2]でより詳しく学べます。

## k-Means法とはなにか
重心を使って、データをｋ個の点のかたまりに分類します。
本来は各点に対して、ランダムにクラスタを割り振り、その後に最初の重心を計算する教師なし学習なのですが、ここでは上にも書いたように半教師あり学習を行います。
機械学習のエッセンス[3]でより詳しく学べます。

## VAEの実装
記事[1]の実装をそのまま使用しました。

## k-Means法の実装
k-Means法でクラスタリングする際に、最初の重心をラベル付きのデータにしたいので、scikit-learnにあるk-Means法を利用するのではなく、スクラッチ実装をしなければなりません。記事[4]のコードに少し変更を加えることで、スクラッチ実装をしました。具体的には二つの変更を行いました。
一つ目はRepInitメソッドの変更です。最初の重心をラベル付きのデータにします。
まずRepInitメソッドの変更する際に使用するメソッドを示します。クラスタ代表点にするラベル付きのデータのインデックスを求めます。最初の重心の位置は重要なのでランダム性を持たせました。

```  python
import random

def returnOneLabelIndexList(labels):
    count = 0
    #クラスタ代表点にするラベル付きのデータのインデックスを格納
    firstLabelIndex = [-1]*10 
    booleanIndex = [False]*10
    randomIndex = random.randint(0, 600)
    #randomIndex番目以降の要素を取得
    labels = labels[randomIndex:]
    #firstLabelIndexに格納する各ラベルは最初に現れたラベルにする
    for label in labels:
        for i in range(10):
            if label == i:
                if firstLabelIndex[i] != -1:
                    break
                firstLabelIndex[i] = count+randomIndex
                booleanIndex[i] = True
        count += 1
        #すべてがtrueだったら終了
        if all(booleanIndex):
            break
    return firstLabelIndex
```
そして、メソッドRepInitを以下のように変更しました。重心を表すアトリビュートrepsにラベル付きのデータを格納します


```  python
 def RepInit(self, c_datas, k,firstLabelIndex):
        for i in range(k):
            self.reps[i] = c_datas[firstLabelIndex[i]]
        self.dists = [[-1 for j in range(len(self.reps))] for i in range(len(c_datas))]
        self.clusters=[-1 for i in range(len(c_datas))]
```

二つ目はpredictメソッドの追加です。学習データにない点群を与えた時に、どのクラスタに属するかを求めるメソッドです。これは単に２点間の距離を求め、最も近いクラスタはどれかを求めるだけです。

```  python
def predict(self,dataListX):
        predictLabels = []
        for x in dataListX:
            dists = []
            for i in range(self.k):
                dists.append(np.linalg.norm(np.array(x) - np.array(self.reps[i])))
            predictLabels.append(np.argmin(dists))
        return predictLabels
```

##k-Means法の学習

VAEの学習をした後、k-Means法の学習を行います。最初の重心の位置が精度に大きく影響すると思われるので、K-分割交差検証を利用して、モデルの探索を行いました。正解率はそれぞれ[0.4335, 0.5265, 0.472, 0.4945, 0.4495]となりました。

```  python
train_dataset = datasets.MNIST('./data', download=True, train=True, transform=transforms.ToTensor())
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=10000, shuffle=False)
#trainデータの数は10000になる
train_images, train_labels = iter(train_loader).next()
#[10000, 1, 28, 28] -> [10000, 784]
train_images = train_images.view(10000, -1)

#Encoderの出力である平均を取得する。
with torch.no_grad():
    z = model.encode(train_images.to(device))
train_mu, train_logvar = z
train_mu = train_mu.cpu()
#TensorをNumpy化
train_mu = train_mu.detach().numpy()

#5分割
kf = KFold(n_splits=5)
modelList = []
accuracyList = []    

for train_index, test_index in kf.split(train_mu, train_labels):
    kmeans_model =KMEANS()
    #最初の重心を求める
    firstLabelIndex = returnOneLabelIndexList(train_labels[train_index])
    #学習
    kmeans_model.Clustering(train_mu[train_index],10,firstLabelIndex)
    #予測
    predict_labels = kmeans_model.predict(train_mu[test_index])
    #精度を求める
    accuracy = accuracy_score(train_labels[test_index],predict_labels)
    accuracyList.append(accuracy)
    #モデルをリストに格納
    modelList.append(kmeans_model)
    
i = accuracyList.index(max(accuracyList))
#最も精度が良いモデルをkmeans_model格納する。
kmeans_model = modelList[i]
print("正解率=",accuracyList)
```

##Testデータでの予測結果

プログラムは以下の通りです

```  python
test_dataset = datasets.MNIST('./data', download=True, train=False, transform=transforms.ToTensor())
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=10000, shuffle=False)
test_images, test_labels = iter(test_loader).next()
test_images = test_images.view(10000, -1)

with torch.no_grad():
    #batch_size=10000
    z = model.encode(test_images.to(device))
test_mu, test_logvar = z
test_mu = test_mu.to(device)
test_mu = test_mu.cpu()
test_mu = test_mu.detach().numpy()

import pylab
import matplotlib.pyplot as plt
%matplotlib inline

#VAEの潜在変数にラベルをつけてプロット
plt.figure(figsize=(10, 10))
plt.scatter(test_mu[:, 0], test_mu[:, 1], marker='.', c=test_labels, cmap=pylab.cm.jet)
plt.colorbar()
plt.xlim((-6, 6))
plt.ylim((-6, 6))
plt.title("VAE")

#k-Means法の潜在変数に対する予測結果をプロット
predict_labels = kmeans_model.predict(test_mu)
plt.figure(figsize=(10, 10))
plt.scatter(test_mu[:, 0], test_mu[:, 1], marker='.', c=predict_labels, cmap=pylab.cm.jet)
plt.colorbar()
plt.xlim((-6, 6))
plt.ylim((-6, 6))
plt.title("kmeans")

print("正解率",accuracy_score(test_labels,predict_labels))
```




予測精度は0.5087になりました。下の画像はVAEの潜在変数にラベルをつけてプロットしたものになります。つまりこれが目指す予測結果になります。

![vaeplot.png](https://qiita-image-store.s3.amazonaws.com/0/229031/63246af6-7ff5-47d8-3391-cc546c983a46.png)


そして、下の画像が冒頭でもお見せしたk-Means法の潜在変数に対する予測結果になります。それなりの精度だと思います。
![vaeandkmeans.png](https://qiita-image-store.s3.amazonaws.com/0/229031/95ec9cc7-c175-b67c-10aa-dd307641429d.png)


##他の手法との比較
他の手法は二つあります。

1. PCAで７８４次元のデータを２次元の空間に射影し、その射影後のデータに対してk-Means法でクラスタリング
2. ７８４次元のデータをそのままk-Means法でクラスタリング

PCA（主成分分析）とはデータを低次元に圧縮するもので、次元圧縮後のデータの分散は最大になります。機械学習のエッセンス[3]でより詳しく学べます。
１の手法のプログラムはPCAを使用する以外は上で示した手法のプログラムと同じです。
正解率は[0.3075, 0.273, 0.336, 0.3315, 0.3205]となりました。

```  python
#PCAで次元圧縮
pca = PCA(n_components=2)
pca.fit(train_images)
pca_train_images = pca.transform(train_images)
```
下の画像はPCAによって、2次元に圧縮されたTestデータにラベルをつけてプロットしたものになります。VAEで取得した潜在変数に比べて、重なっている部分が多いです。
![pca.png](https://qiita-image-store.s3.amazonaws.com/0/229031/f87e0b20-d917-e544-7f17-96fc203cbab6.png)

そして、下の画像がPCAによって、2次元に圧縮されたTestデータに対して、k-Means法（上で示した手法と同じように半教師あり学習）でクラスタリングしたものです。精度は0.3238でした。上で示した手法の精度は0.5087でしたので、上で示した手法の方がより良い精度となってます。

![pcaKmeans.png](https://qiita-image-store.s3.amazonaws.com/0/229031/e03f1825-bafb-7e3c-41a1-97fab5157bd5.png)


２の手法のプログラムはMNISTの784次元ベクトルをそのままk-Means法（上で示した手法と同じように半教師あり学習）でクラスタリングします。精度は0.5524で一番良かったです。しかし784次元ベクトルをそのままk-Means法を行う計算コストは結構大きかったです。



## おわりに
個人的には、PCAで取得した圧縮データと比べて、VAEで取得した潜在変数がよりよく、データの特徴を入れ込むことができていて、面白かったです。
今後の自分の課題としては、K-分割交差検証を利用したモデルの評価や他の手法との比較がちゃんとした評価になっているかの確証が得られませんでした。そういったところを勉強したいです。

記事に間違いがあれば、コメントなどで指摘してくださると幸いです。

##参考
[1]Variational Autoencoder徹底解説
https://qiita.com/kenmatsu4/items/b029d697e9995d93aa24

[2]PyTorch (11) Variational Autoencoder
http://aidiary.hatenablog.com/entry/20180228/1519828344

[3]機械学習のエッセンス　　　　　　　　　　　　　　　　　　　　　　　　
https://www.amazon.co.jp/dp/4797393963/ref=cm_sw_r_tw_dp_U_x_ZMXMCbXTFPW4Z 

[4]Pythonでk-means法をやってみる
https://qiita.com/tatsuya-miyamoto/items/018ea59f3746be81ca16