irisで試す相互情報量の最大化による教師なし学習手法IIC

# 概要

[実験結果のファイルをGitHubに保存しています](https://github.com/KTaskn/StudiesForML)

## 概要


未知ラベルの画像にノイズをのっけて、相互情報量を最大化するように学習することで画像のクラスタリングを行えるとのこと。
**つまり、画像に対して事前のアノテーション（ラベリング）作業不要でクラスタリングが可能**
詳細はarxiv読んでください。

* [Invariant Information Clustering for
Unsupervised Image Classification and Segmentation（arxiv）](https://arxiv.org/pdf/1807.06653.pdf#page9)
* [xu-ji/IIC （本家Gitリポジトリ）](https://github.com/xu-ji/IIC)
* [教師あり学習の精度を超えた！？相互情報量の最大化による教師なし学習手法IICの登場！ （日本語解説）](https://ai-scholar.tech/treatise/iic-ai-367/)
* [RuABraun/phone-clustering （比較的シンプルな実装例Gitリポジトリ）](https://github.com/RuABraun/phone-clustering)

MNISTはいろんなひとが実装しているので、
画像ではなく、もっとスモールなirisデータセットを使って教師なしを実装してみる。

* [iris データセット](https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_iris.html)


# 損失関数

論文中のコードは一部間違っているはず

``` math
\sum^{C}_{c=1} \sum^{C}_{c'=1} P_{cc'} \ln{\frac{P_{cc'}}{P_{c}P_{c'}}}
= \sum^{C}_{c=1} \sum^{C}_{c'=1} P_{cc'} (\ln{P_{cc'} - \ln{P_{c}} - \ln{P_{c'}}})
```

``` python
import torch

class IIC(torch.nn.Module):
    def __init__(self):
      super(IIC, self).__init__()

    def IIC(self, z, zt, C):
      P = (z.unsqueeze(2) * zt.unsqueeze(1)).sum(dim=0)
      P = ((P + P.t()) / 2) / P.sum()
      EPS = torch.finfo(P.dtype).eps
      P[(P < EPS).data] = EPS
      Pi = P.sum(dim=1).view(C, 1).expand(C, C)
      Pj = P.sum(dim=0).view(1, C).expand(C, C)

      # 論文中の式は計算間違っているのでは？
      # (P * (log(Pi) + log(Pj) - log(P))).sum() (元計算)

      # 損失関数なので、最大化->最小化に切り替えるために負にする
      loss = (-1.0 * P * (torch.log(P) - torch.log(Pi) - torch.log(Pj))).sum()
      return loss

    def forward(self, Z, ZT, C=3):
      # headと呼んでいる処理のためにこのような計算になる
      # headで学習が早くなるのだが、なぜ早くなるのかは不勉強。。コメント待っています。
      return torch.sum(torch.stack([
                self.IIC(z, zt, C) for z, zt in zip(Z, ZT)
              ]))
```

# データ

定番のirisを使う。

``` python
from torch.utils.data import Dataset
from sklearn.datasets import load_iris
import pandas as pd

class Iris_forIIC(Dataset):
    def __init__(self):
      # irisデータセットをロード
      iris = load_iris()
      self.df = pd.DataFrame(iris.data).assign(label=iris.target)

    def __len__(self):
        return len(self.df.index)

    def __getitem__(self, idx):
      # idx行目を取り出して、データとラベルにわける
      row = self.df.iloc[idx].values
      label = row[4]

      # データはTensorのfloat型じゃないといけない
      data_target = torch.from_numpy(row[:4]).float()
      # IICのキモ！
      # 今回は雑に元データにノイズをのっける。
      data_other = torch.from_numpy(row[:4]).float() * torch.normal(torch.tensor(1).float(), torch.tensor(0.1).float())

      return data_target, data_other
```

# モデル

モデルがかなり苦労した。
通常の教師あり学習ならオーバーキルなモデルを使わないと学習が進まない。

``` python
import torch.nn as nn
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # irisデータにあわせて4次元入力の、3次元出力。
        self.nn = nn.Sequential(
            nn.Linear(4, 100),
            # バッチ正規化を利用しないと学習が進まない
            nn.BatchNorm1d(100),
            nn.ReLU(),
            nn.Linear(100, 50),
            nn.BatchNorm1d(50),
            nn.ReLU(),
            nn.Linear(50, 3),
            nn.BatchNorm1d(3),
            nn.ReLU())

      # 普通の教師ありなら下記のようなモデルで十分
      # self.nn = nn.Sequential(
      #     nn.Linear(4, 100),
      #     nn.ReLU(),
      #     nn.Linear(100, 50),
      #     nn.ReLU(),
      #     nn.Linear(50, 3))
      
    def forward(self, x):
        # headと呼んでいる処理。不勉強で何しているかわからない。
        # 学習がとても早くなる。
        return [F.softmax(self.nn(x), dim=1) for _ in range(5)]
        # return F.softmax(self.nn(x), dim=1)
```

# 学習
``` python
import torch.optim as optim
import torch.nn.functional as F
from sklearn.metrics import confusion_matrix

# cpu/gpuを指定
device = torch.device('cpu')
# モデルを生成
model = Net()
model = model.to(device)
# 損失関数
criterion = IIC()
# 最適化関数
optimizer = optim.Adam(model.parameters(), lr=0.0001)

# Dataloader
kwargs = {'num_workers': 1, 'pin_memory': True}
train_set = Iris_forIIC()
train_loader = torch.utils.data.DataLoader(train_set, batch_size=len(train_set), shuffle=True, **kwargs)

EPOCH = 250
for idx in range(EPOCH):
  model.train()
  for X, XT in train_loader:
    # 元データ
    X = X.to(device)
    # ノイズ付与データ
    XT = XT.to(device)
    # XとXTについてそれぞれ分類
    Z = model(X)
    ZT = model(XT)
    # 損失=相互情報量を計算
    loss = criterion(Z, ZT)

    # 勾配計算・学習
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

# 学習済みモデルを評価
model.eval()
# irisデータを読み込み
iris = load_iris()
df_iris = pd.DataFrame(iris.data).assign(label=iris.target)
X = torch.Tensor(df_iris.drop('label', axis=1).values).to(device)
Y_predict = model.nn(X).argmax(1).cpu()
Y_actual = df_iris['label'].values
# 混同行列を表示
display(confusion_matrix(Y_actual, Y_predict))
```

# 結果
もともとのラベルと数が異なるので対角に並ぶわけではないが、
ちゃんと分類されていることがわかる。

``` python
[[ 0, 49,  1],
 [ 5,  0, 45],
 [50,  0,  0]]
```