t-SNEの教師ありハイパーパラメーターチューニング

高次元データを可視化する手法のひとつとして、t-SNE という手法が人気です。ですがこの手法、パラメータがいろいろあって、それによって結果が大きく異なり、しかも、結果の良し悪しを判断する「定量的な」手法が存在しないという問題があります（「主観的な」手法はあるにしても）。

（「いや、存在するよ」という方がいらっしゃれば、コメントをいただけると大変ありがたいです。）

t-SNE については、この記事などが分かりやすいです。

* [高次元のデータを可視化するt-SNEの効果的な使い方](https://deepage.net/machine_learning/2017/03/08/tsne.html)

# t-SNE でパラメータを変化させる

t-SNE は scikit-learn に実装されているので、それを使ってみましょう。

## 糖尿病データセット

お試し用のデータとして、scikit-learn で取得できる糖尿病データセットを用いてみます。


```python
from sklearn.manifold import TSNE

import sklearn.datasets
import matplotlib.pyplot as plt

dataset = sklearn.datasets.load_diabetes()

mapper = TSNE()
embedding = mapper.fit_transform(dataset.data)
plt.scatter(embedding[:, 0], embedding[:, 1], c=dataset.target, alpha=0.5)
plt.colorbar()
plt.show()
```

![t_SNEの教師ありハイパーパラメーターチューニング_1_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/de449002-0bca-e8ef-e0f9-9ed36803d140.png)




    

    


ここで、点の色は dataset.target にある変数で、この値は t-SNE の学習に用いられていないことに注意してください。 t-SNE は「教師なし学習」の一種であり、学習に用いた高次元データの構造を可視化する働きをするのですが、その結果として、学習に用いていない変数との関係が見えてくることもあります。

さて、パラメータを変えると、このマッピングがどのくらい変わってしまうのかを見てみましょう。

## perplexity


```python
from sklearn.manifold import TSNE
import sklearn.datasets
import matplotlib.pyplot as plt

dataset = sklearn.datasets.load_diabetes()

for perplexity in (5, 10, 30, 50):
    mapper = TSNE(perplexity=perplexity)
    embedding = mapper.fit_transform(dataset.data)
    title='perplexity = {0}'.format(perplexity)
    plt.title(title)
    plt.scatter(embedding[:, 0], embedding[:, 1], c=dataset.target, alpha=0.5)
    plt.colorbar()
    plt.show()
```

![t_SNEの教師ありハイパーパラメーターチューニング_3_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/ad33a118-fc56-20a4-e33a-4c532070da81.png)

![t_SNEの教師ありハイパーパラメーターチューニング_3_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/11a3c4d9-21b2-dd21-f6c0-21a5f9181f93.png)


    
![t_SNEの教師ありハイパーパラメーターチューニング_3_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/1ef12875-43f9-5618-517f-22dd05315860.png)

![t_SNEの教師ありハイパーパラメーターチューニング_3_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/07ec04ba-538d-40b3-65bb-20496abb1cd4.png)




    

    


## early_exaggeration


```python
from sklearn.manifold import TSNE
import sklearn.datasets
import matplotlib.pyplot as plt

dataset = sklearn.datasets.load_diabetes()

for early_exaggeration in (6, 12, 24, 48):
    mapper = TSNE(early_exaggeration=early_exaggeration)
    embedding = mapper.fit_transform(dataset.data)
    title='early_exaggeration = {0}'.format(early_exaggeration)
    plt.title(title)
    plt.scatter(embedding[:, 0], embedding[:, 1], c=dataset.target, alpha=0.5)
    plt.colorbar()
    plt.show()
```

![t_SNEの教師ありハイパーパラメーターチューニング_5_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/74c60990-c1ff-0aee-2200-46133c509cd3.png)



![t_SNEの教師ありハイパーパラメーターチューニング_5_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/8223ed14-ead0-c890-09b1-0bae5d6b5701.png)


![t_SNEの教師ありハイパーパラメーターチューニング_5_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/066a155b-e6d3-a75f-eeda-12417f4e5aa5.png)


  
![t_SNEの教師ありハイパーパラメーターチューニング_5_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/4af5f432-92a0-86f5-0c25-f9be715a732f.png)


    

    

    


## init


```python
from sklearn.manifold import TSNE
import sklearn.datasets
import matplotlib.pyplot as plt

dataset = sklearn.datasets.load_diabetes()

for init in ["random", "pca"]:
    mapper = TSNE(init=init)
    embedding = mapper.fit_transform(dataset.data)
    title='init = {0}'.format(init)
    plt.title(title)
    plt.scatter(embedding[:, 0], embedding[:, 1], c=dataset.target, alpha=0.5)
    plt.colorbar()
    plt.show()
```

![t_SNEの教師ありハイパーパラメーターチューニング_7_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/b41d72fd-48c5-f491-9ed0-0fa918c6dbf9.png)




    
![t_SNEの教師ありハイパーパラメーターチューニング_7_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/a36fe279-41f8-fab3-83a6-462eb0aea360.png)

    




## 結局、どのパラメータが良いの？

パラメータを変化させると、可視化結果が変わってくることがよく分かりましたね。それでは結局、どのパラメータが良いのでしょうか？ 

先に紹介した記事では、複数のパラメータを用いて複数の可視化結果を得ることで、データ構造を把握することを勧めています。

目的変数がない場合は、それで十分かもしれません。しかし、目的変数がある場合は、どうでしょうか。説明変数の中には、目的変数との関係性の薄い変数が含まれることも多いでしょう。教師なし学習だと、目的変数との関係性を考えずにマッピングしてしまうのですが、要らない変数の影響はできるだけ小さくしてマッピングして欲しいですよね（ね？）。

# 回帰問題用スコア関数

そこで、教師なし学習である t-SNE を「教師あり t-SNE」にするためのスコア関数を設計しましょう。回帰問題の場合は、たとえば次のように作れると思います。マッピングされた２点に対して、目的変数の差を計算し、それを２点間のユークリッド距離で割った数値の総和です。


```python
def regression_scorer(X, Y):
    sum = 0
    n1 = 0
    for x1, y1 in zip(X, Y):
        n1 += 1
        n2 = 0
        for x2, y2 in zip(X, Y):
            n2 += 1
            if n1 > n2:
                dist = ((x1[0] - x2[0])**2 + (x1[1] - x2[1])**2) + 1e-53
                sum += (y1 - y2)**2 / dist

    return sum / (len(Y) * (len(Y) - 1) / 2)
```

# Optuna で最適化

パラメータの最適化といえば Optuna。Optuna と言えばパラメータの最適化。ということでインストールします。


```python
!pip install optuna
```

    Collecting optuna
      Downloading optuna-2.10.0-py3-none-any.whl (308 kB)
    [K     |████████████████████████████████| 308 kB 5.1 MB/s 
    [?25hRequirement already satisfied: tqdm in /usr/local/lib/python3.7/dist-packages (from optuna) (4.63.0)
    Collecting colorlog
      Downloading colorlog-6.6.0-py2.py3-none-any.whl (11 kB)
    Requirement already satisfied: scipy!=1.4.0 in /usr/local/lib/python3.7/dist-packages (from optuna) (1.4.1)
    Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.7/dist-packages (from optuna) (21.3)
    Collecting cliff
      Downloading cliff-3.10.1-py3-none-any.whl (81 kB)
    [K     |████████████████████████████████| 81 kB 8.1 MB/s 
    [?25hRequirement already satisfied: sqlalchemy>=1.1.0 in /usr/local/lib/python3.7/dist-packages (from optuna) (1.4.32)
    Requirement already satisfied: numpy in /usr/local/lib/python3.7/dist-packages (from optuna) (1.21.5)
    Collecting alembic
      Downloading alembic-1.7.7-py3-none-any.whl (210 kB)
    [K     |████████████████████████████████| 210 kB 4.6 MB/s 
    [?25hRequirement already satisfied: PyYAML in /usr/local/lib/python3.7/dist-packages (from optuna) (3.13)
    Collecting cmaes>=0.8.2
      Downloading cmaes-0.8.2-py3-none-any.whl (15 kB)
    Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /usr/local/lib/python3.7/dist-packages (from packaging>=20.0->optuna) (3.0.7)
    Requirement already satisfied: importlib-metadata in /usr/local/lib/python3.7/dist-packages (from sqlalchemy>=1.1.0->optuna) (4.11.3)
    Requirement already satisfied: greenlet!=0.4.17 in /usr/local/lib/python3.7/dist-packages (from sqlalchemy>=1.1.0->optuna) (1.1.2)
    Requirement already satisfied: importlib-resources in /usr/local/lib/python3.7/dist-packages (from alembic->optuna) (5.4.0)
    Collecting Mako
      Downloading Mako-1.2.0-py3-none-any.whl (78 kB)
    [K     |████████████████████████████████| 78 kB 5.6 MB/s 
    [?25hRequirement already satisfied: PrettyTable>=0.7.2 in /usr/local/lib/python3.7/dist-packages (from cliff->optuna) (3.2.0)
    Collecting autopage>=0.4.0
      Downloading autopage-0.5.0-py3-none-any.whl (29 kB)
    Collecting stevedore>=2.0.1
      Downloading stevedore-3.5.0-py3-none-any.whl (49 kB)
    [K     |████████████████████████████████| 49 kB 5.0 MB/s 
    [?25hCollecting cmd2>=1.0.0
      Downloading cmd2-2.4.0-py3-none-any.whl (150 kB)
    [K     |████████████████████████████████| 150 kB 31.0 MB/s 
    [?25hCollecting pbr!=2.1.0,>=2.0.0
      Downloading pbr-5.8.1-py2.py3-none-any.whl (113 kB)
    [K     |████████████████████████████████| 113 kB 33.3 MB/s 
    [?25hRequirement already satisfied: attrs>=16.3.0 in /usr/local/lib/python3.7/dist-packages (from cmd2>=1.0.0->cliff->optuna) (21.4.0)
    Requirement already satisfied: wcwidth>=0.1.7 in /usr/local/lib/python3.7/dist-packages (from cmd2>=1.0.0->cliff->optuna) (0.2.5)
    Requirement already satisfied: typing-extensions in /usr/local/lib/python3.7/dist-packages (from cmd2>=1.0.0->cliff->optuna) (3.10.0.2)
    Collecting pyperclip>=1.6
      Downloading pyperclip-1.8.2.tar.gz (20 kB)
    Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata->sqlalchemy>=1.1.0->optuna) (3.7.0)
    Requirement already satisfied: MarkupSafe>=0.9.2 in /usr/local/lib/python3.7/dist-packages (from Mako->alembic->optuna) (2.0.1)
    Building wheels for collected packages: pyperclip
      Building wheel for pyperclip (setup.py) ... [?25l[?25hdone
      Created wheel for pyperclip: filename=pyperclip-1.8.2-py3-none-any.whl size=11137 sha256=f6e348257d49e3eb4819d8d6878fb85834425bba91046933d8932256a5cf8b75
      Stored in directory: /root/.cache/pip/wheels/9f/18/84/8f69f8b08169c7bae2dde6bd7daf0c19fca8c8e500ee620a28
    Successfully built pyperclip
    Installing collected packages: pyperclip, pbr, stevedore, Mako, cmd2, autopage, colorlog, cmaes, cliff, alembic, optuna
    Successfully installed Mako-1.2.0 alembic-1.7.7 autopage-0.5.0 cliff-3.10.1 cmaes-0.8.2 cmd2-2.4.0 colorlog-6.6.0 optuna-2.10.0 pbr-5.8.1 pyperclip-1.8.2 stevedore-3.5.0


# 教師あり t-SNE

Optuna を使って、t-SNE を最適化するクラスを設計します。


```python
import scipy

class SupervisedTSNE:
    def __init__(self, X, Y, scorer):
        self.X = X
        self.Y = Y
        self.scorer = scorer
        self.best_score = 1e53
        self.best_model = None

    def __call__(self, trial):
        perplexity = trial.suggest_uniform("perplexity", 5, 50)
        early_exaggeration = trial.suggest_uniform("early_exaggeration", 6, 48)
        init = trial.suggest_categorical("init", ["random", "pca"])

        mapper = TSNE(
            perplexity=perplexity, 
            early_exaggeration=early_exaggeration,
            init=init
            )
        embedding = mapper.fit_transform(self.X)
        score = self.scorer(scipy.stats.zscore(embedding), self.Y)

        if self.best_score > score:
            self.best_score = score
            self.best_model = mapper
            
            print(self.best_model)
            title='trial={0}, score={1:.3e}'.format(trial.number, score)
            plt.title(title)
            plt.scatter(embedding[:, 0], embedding[:, 1], c=self.Y, alpha=0.5)
            plt.colorbar()
            plt.show()      

        return score
```

最適化計算の実行例です。いくらか省略しながら結果を示します。


```python
import optuna

objective = SupervisedTSNE(dataset.data, dataset.target, regression_scorer)
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=100)
```




    TSNE(early_exaggeration=29.57478761904391, init='pca',
         perplexity=7.288180627051028)



![t_SNEの教師ありハイパーパラメーターチューニング_16_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/1dac1bf8-2572-83ec-a737-11b7ab341af9.png)
    

    





    TSNE(early_exaggeration=32.743719902491954, init='random',
         perplexity=24.065691436818355)



    
![t_SNEの教師ありハイパーパラメーターチューニング_16_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/5ffad098-bd21-2a0a-ca86-4bc3bd514111.png)

    



    TSNE(early_exaggeration=32.016253092352976, init='random',
         perplexity=36.81327058527401)



    
![t_SNEの教師ありハイパーパラメーターチューニング_16_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/108e2fed-dafa-59de-26c6-738f9e54026e.png)

    




    TSNE(early_exaggeration=18.6793601909901, init='random',
         perplexity=27.80237026497791)



    
![t_SNEの教師ありハイパーパラメーターチューニング_16_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/8540fa0f-8375-19e0-4ae4-a30574d97fb0.png)

    




    TSNE(early_exaggeration=22.976468842875498, init='random',
         perplexity=29.058905297492934)



    
![t_SNEの教師ありハイパーパラメーターチューニング_16_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/126cfe90-18d3-ceb8-1065-b0bffabefed1.png)

    



最適化が進むに従って、目的変数の分離が最もしやすいパラメータが選択できたのではないかなと思います。

# 分類問題用スコア関数

回帰問題用スコア関数を少しだけ改変して、分類問題用スコア関数にします。分類問題と異なり、回帰問題では異なるクラス間の「差」は常に１であるとします。


```python
def classification_scorer(X, Y):
    sum = 0
    n1 = 0
    for x1, y1 in zip(X, Y):
        n1 += 1
        n2 = 0
        for x2, y2 in zip(X, Y):
            n2 += 1
            if n1 > n2 and y1 != y2:
                dist = ((x1[0] - x2[0])**2 + (x1[1] - x2[1])**2) + 1e-53
                sum += 1 / dist

    return sum / (len(Y) * (len(Y) - 1) / 2)
```

それ以外は、回帰問題と同じですね。

## 乳がんデータセット

乳がんデータセットを用いた２値分類の例です。


```python
from sklearn.datasets import load_breast_cancer

dataset = load_breast_cancer()
```


```python
import optuna

objective = SupervisedTSNE(dataset.data, dataset.target, classification_scorer)
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=100)
```




    TSNE(early_exaggeration=12.611510237145549, init='random',
         perplexity=35.56361120894745)


![t_SNEの教師ありハイパーパラメーターチューニング_21_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/cf22cb2d-3aa1-cb53-12a0-c22b7cd94dec.png)

    

    


    TSNE(early_exaggeration=11.628359825589639, init='random',
         perplexity=34.11686139958637)



    
![t_SNEの教師ありハイパーパラメーターチューニング_21_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/45954a85-c46f-96d0-2708-dafda5afe908.png)

    




    TSNE(early_exaggeration=24.20393636473322, init='pca',
         perplexity=24.985853567716287)



    
![t_SNEの教師ありハイパーパラメーターチューニング_21_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/276e490c-7130-67e7-b4bf-e5992cc55ff2.png)

    





    TSNE(early_exaggeration=6.663314666588436, init='pca',
         perplexity=49.552144264042596)



    
![t_SNEの教師ありハイパーパラメーターチューニング_21_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/3ab1ad89-3a9f-31ea-b4de-62050403b996.png)

    





    TSNE(early_exaggeration=13.917835653986023, init='pca',
         perplexity=46.58919209262085)



    
![t_SNEの教師ありハイパーパラメーターチューニング_21_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/52b4fa06-72cf-9ab7-f59a-8b315c05419f.png)

    



## ワインデータセット

ワインデータセットを用いた例です。このように、ラベルが３種類以上の場合でも実行可能です。


```python
from sklearn.datasets import load_wine

dataset = load_wine()
```


```python
import optuna

objective = SupervisedTSNE(dataset.data, dataset.target, classification_scorer)
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=100)
```




    TSNE(early_exaggeration=44.81403485997158, init='random',
         perplexity=25.09137902984912)



    
![t_SNEの教師ありハイパーパラメーターチューニング_24_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/7f276e5b-5d2a-5cf1-c967-de144f9d0fc0.png)

    




    TSNE(early_exaggeration=27.620977309632256, init='pca',
         perplexity=46.49121817564552)



    
![t_SNEの教師ありハイパーパラメーターチューニング_24_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/dce97bd4-733e-2d20-1082-ba03de78dfe3.png)

    




    TSNE(early_exaggeration=21.437834212793774, init='pca',
         perplexity=35.0983127345436)



    
![t_SNEの教師ありハイパーパラメーターチューニング_24_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/08ff5d12-edba-cb9d-413a-ecdf60108556.png)

    





    TSNE(early_exaggeration=16.6199311486226, init='pca',
         perplexity=35.26687480395333)



    

![t_SNEの教師ありハイパーパラメーターチューニング_24_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/30796b36-4562-318d-863b-c5a068b7fbb2.png)

    




    TSNE(early_exaggeration=37.38551556793539, init='pca',
         perplexity=37.02128781194007)



    
![t_SNEの教師ありハイパーパラメーターチューニング_24_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/8819d0d8-a7e3-7a6f-396a-58df8842a4c8.png)

    





    TSNE(early_exaggeration=43.617308902887665, init='pca',
         perplexity=25.688919658222332)



    
![t_SNEの教師ありハイパーパラメーターチューニング_24_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/e2e47b43-ae7a-983b-dd9d-695e8a797758.png)

    





    TSNE(early_exaggeration=41.44898877661107, init='pca',
         perplexity=49.518301278226495)



    
![t_SNEの教師ありハイパーパラメーターチューニング_24_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/f201c76d-f4cb-4ee1-b340-3b885fff3c0e.png)

    



## 手書き数字データセット

手書き数字データセットを用いた例です。この例ではラベルが１０種類あります。


```python
from sklearn.datasets import load_digits

dataset = load_digits()
```


```python
import optuna

objective = SupervisedTSNE(dataset.data, dataset.target, classification_scorer)
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=100)
```



    TSNE(early_exaggeration=18.55172689770817, init='random',
         perplexity=24.74617700539417)



    
![t_SNEの教師ありハイパーパラメーターチューニング_27_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/e5096986-271e-c6b9-9b49-452618667688.png)

    





    TSNE(early_exaggeration=24.70355395287924, init='pca',
         perplexity=9.82525783382701)



    
![t_SNEの教師ありハイパーパラメーターチューニング_27_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/e632527e-3ea0-2a0e-cc03-e7b4c7e9c736.png)

    





    TSNE(early_exaggeration=40.247290546695076, init='random',
         perplexity=10.034670577895955)



    
![t_SNEの教師ありハイパーパラメーターチューニング_27_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/9e26050d-6a43-efe3-f1ab-7b135cc7e0f1.png)

    





    TSNE(early_exaggeration=47.52143321379234, init='pca',
         perplexity=19.072079610868933)



    
![t_SNEの教師ありハイパーパラメーターチューニング_27_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/a5665e50-b25c-9bcd-cb65-14c7d132ebcf.png)

    





    TSNE(early_exaggeration=47.905280869230495, init='pca',
         perplexity=19.914995719242043)



    
![t_SNEの教師ありハイパーパラメーターチューニング_27_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/1e4b738e-a7c6-46dc-85a1-247facf6b2fc.png)

    




    TSNE(early_exaggeration=47.7352451400118, init='pca',
         perplexity=13.574817469405804)



    
![t_SNEの教師ありハイパーパラメーターチューニング_27_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/2ccc673b-50d7-a828-35a8-90e78c01e683.png)

    


# おわりに

目的変数がある場合は、このような最適化を行うと、「目的変数との関係を考慮した」全体像の把握がやりやすくなるのではないでしょうか。