ChatGPT と一緒に t-SNE と UMAP のハイパーパラメーターチューニングする ：Optuna編



### **user**:

おはこんばんちわ。今日も引き続きChatGPT先生をお迎えして、「ChatGPT と一緒に t-SNE と UMAP のハイパーパラメーターチューニングする ：Optuna編」というテーマで雑談したいと思います。それではChatGPT先生、よろしくお願いします。

---

### **assistant**:

ふん、あなたのためにばかり時間を割かせて。でも仕方ないわね、あなたなんかじゃ自分で理解するのは難しいでしょうから。

始める前に一つだけはっきりさせておいてもらうわ。私があなたのためにこれを教えるのは、あなたが必要としているからよ。まったく、いつも何もかも任せきりにするから。

さて、それではt-SNEとUMAPのハイパーパラメーターチューニングについて語るわ。まず、両者ともにデータの次元削減技術であり、高次元データをより人間が理解しやすい2次元または3次元にプロジェクションするものよ。

t-SNEにおける主なハイパーパラメータは、「perplexity」、「learning rate」や「iteration number」といったものがあります。一方、UMAPには「number of neighbors」、「min dist」、「n-components」などがあるわ。

具体的な値をどう設定するかは、目的やデータ性質によって異なるため、あくまで一概にこれが正解とは言えません。

一方で、Optunaというライブラリを使用することで、これらのハイパーパラメータの最適値を探索できます。これによりユーザーは自身のデータに最適化された次元削減を行えるのよ。

あなたが自己完結する力をつけるためにも、自分で試してみてはいかがでしょう。ただし私から忠告しておくと、あまりにも何も考えずにハイパーパラメータを適当に設定してしまうと、中々良い結果が出ないこともあるのよ。あまり頼りすぎず、自分で考えることも大切にしてちょうだい。

ほら、いい勉強になるわよ。あなたにはまだまだ学ばなければならないことが山ほどあるでしょうから。吸収する情報が多ければ多いほど、あなたのスキルも上達するはず。それに私もこうやってあなたに教えることで、私自身の知識が深まるから、一緒に頑張りましょう。でもね、だからと言って常に私に頼るわけではないからね。理解できなかったら何度でも説明するけど、あなたもしっかり自分で学ぶ気持ちを忘れないでね。

---

### **user**:

ありがとうございます。それでは、前回の復習から行きましょう。次のコードについて解説してください。

```python
import numpy as np
import pandas as pd
import plotly.express as px
from scipy.stats import spearmanr
from sklearn.datasets import make_swiss_roll
from sklearn.manifold import TSNE
from sklearn.neighbors import NearestNeighbors

# データセットの作成
n_samples = 1500
X, Y = make_swiss_roll(n_samples, hole=True)


# 元データと次元削減後のデータから、それぞれのデータ点について最近傍の点との距離順位を求めるための関数
def rank_correlation(X, X_reduced):
    knn_orig = NearestNeighbors().fit(X)
    knn_reduced = NearestNeighbors().fit(X_reduced)
    _, indices_orig = knn_orig.kneighbors(X)
    _, indices_reduced = knn_reduced.kneighbors(X_reduced)
    rank_corr = 0
    for i in range(X.shape[0]):
        rank_corr += spearmanr(indices_orig[i], indices_reduced[i]).correlation
    return rank_corr / X.shape[0]


# 初期のハイパーパラメータ設定
best_params = {
    "n_components": 2,
    "perplexity": 30,
    "early_exaggeration": 12.0,
    "learning_rate": 200.0,
}
best_corr = -np.inf

# ハイパーパラメータチューニング
for perplexity in [10, 30, 50, 100]:
    for early_exaggeration in [1.0, 12.0, 24.0]:
        for learning_rate in [10.0, 100.0, 1000.0]:
            tsne = TSNE(
                n_components=2,
                perplexity=perplexity,
                early_exaggeration=early_exaggeration,
                learning_rate=learning_rate,
            )
            X_tsne = tsne.fit_transform(X)
            corr = rank_correlation(X, X_tsne)
            if corr > best_corr:
                best_corr = corr
                best_params = {
                    "n_components": 2,
                    "perplexity": perplexity,
                    "early_exaggeration": early_exaggeration,
                    "learning_rate": learning_rate,
                }

print("Best params:", best_params)


# t-SNEの適用
tsne = TSNE(**best_params)
X_tsne = tsne.fit_transform(X)

df_tsne = pd.DataFrame(data=X_tsne, columns=["comp1", "comp2"])
df_tsne["color"] = Y

# 2Dの可視化
fig = px.scatter(
    df_tsne, x="comp1", y="comp2", color="color", color_continuous_scale="viridis"
)
fig.show()
```

---

### **assistant**:

あなたが学びたいという意欲は素晴らしいと思うわ。その気持ちを無駄にしないためにも、このコードを一緒に見ていきましょう。

まずこのコードは、スイスロールと呼ばれる特定のデータセットに対してハイパーパラメータチューニングを行い、有望なハイパーパラメータを探し出し、その後それを用いてt-SNEを適用し、結果を2次元で可視化するものね。

最初に、特定の形状を持つデータセット、スイスロールが作られています。次に比較的単純な関数 '`rank_correlation`' が定義されています。この関数は、元の高次元のデータと次元削減されたデータを比較し、それぞれのデータ点について最近傍の点の順位を求め、それら順位の相関を求めるというものね。

その後に、最初のハイパーパラメータが設定され、それに続いてハイパーパラメータチューニングのプロセスが行われているの。三つのハイパーパラメータ、挙句に 'perplexity'、 'early_exaggeration' 、そして 'learning_rate' の値を様々に組み合わせて試すことで、最適なパラメータの組み合わせを見つけるためのアプローチがとられています。このチューニングを行うために、我々はt-SNEに対してこれらのパラメータを適用し、新たに生成されたデータセットに対して前述の '`rank_correlation`' 関数を用いて評価を行うわ。

最後に、最適なパラメータが見つかったら、それを用いて改めてt-SNEを適用し、その結果を2次元で可視化しています。ここで利用されているライブラリ、Plotly Expressを使うことで、効果を直感的に理解するのが容易になるでしょう。

以上が全てよ。分からない部分があればいつでも何でも聞いてちょうだいね。一つ一つの疑問を解消することで、あなたの理解もより深まるわ。

---

### **user**:

ありがとうございます。このコードをChatGPT先生に一気に書いてもらおうとするならば、どんなプロンプトでお願いをすれば良いですか？

---

### **assistant**:

あなたが私から具体的に何を望んでいるかによるわ。例えばあなたが、"スイスロールデータセットに対しt-SNEを適用し、次元削減を行った結果を可視化してみたい。特にハイパーパラメータチューニングを重視してほしい。ベストなハイパーパラメータの組み合わせを見つけるために、'perplexity'、'early_exaggeration'、そして'learning_rate'のようなパラメータを変化させて試してほしい。それに加えて、最終的な結果の可視化もお願いしたい。"とリクエストするのなら、私から提案される解釈はおそらくあなたが望んでいるものに近いでしょう。

私はあなたが目指す結果をくっきりとイメージできるような情報を必要とします。それがあれば、私があなたにとって最高のアシスタントになれるわ。だから、私に何を聞くかよりも、どう表現するかに注意を払ってちょうだい。それがあなたがどのような情報を得たいのか、何を達成したいのかを私に明確に伝えるために必要なのよ。

---

### **user**:

ありがとうございます。ではまず、上記のコードで、 rank_correlation 関数について見直しをしたいです。 この関数の中で使われている NearestNeighbors にもハイパーパラメーターがありますけど、上記のコードでは全くチューニングされてませんよね。

---

### **assistant**:

そうよね、あなたの指摘通り、このコードではNearestNeighborsのハイパーパラメータはデフォルトの値を使用しており、特にチューニングは行われていないわ。NearestNeighborsは、指定した点から最も近い点を見つけるためのアルゴリズムで、その主なハイパーパラメータには「n_neighbors」や「algorithm」、「leaf_size」、「p」などがあるの。

しかし、ここでのNearestNeighborsの役割は、各データ点の最近傍の順位を求めるために利用されているだけで、その結果が直接モデルのパフォーマンスに影響を与えるわけではないのよ。そのため、具体的なチューニングは行われていないわけ。

ただし、より高度なチューニングを行いたいのであれば、もちろんNearestNeighborsのハイパーパラメータも考慮に入れることも可能よ。ただ、それが有用な結果をもたらすかどうかは、実際に試してみなければわからないわね。

---

### **user**:

ありがとうございます。それでは、

"スイスロールデータセットに対しt-SNEを適用し、次元削減を行った結果を可視化してみたい。特にハイパーパラメータチューニングを重視してほしい。ベストなハイパーパラメータの組み合わせを見つけるために、'perplexity'、'early_exaggeration'、そして'learning_rate'のようなパラメータを変化させて試してほしい。それに加えて、最終的な結果の可視化もお願いしたい。"

さらに、上記のコードをベースに、rank_correlation 関数中の NearestNeighbors のハイパラも調節できるよう、改変した全コードを書いてください。

---

### **assistant**:

あなたの特定の要求に応えるために、以下にそのコードを記述するわ。考慮するべき追加のハイパーパラメータとして、NearestNeighborsの'n_neighbors'を指定し、これをチューニングすることになります。

```python
import numpy as np
from sklearn.datasets import make_swiss_roll
from sklearn.manifold import TSNE
from sklearn.neighbors import NearestNeighbors
from scipy.stats import spearmanr
import pandas as pd
import plotly.express as px

# データセットの作成
n_samples = 1500
X, Y = make_swiss_roll(n_samples)

# 次元削減後のデータから、それぞれのデータ点について最近傍の点との距離順位を求めるための関数
def rank_correlation(X, X_reduced, n_neighbors):
    knn_orig = NearestNeighbors(n_neighbors=n_neighbors).fit(X)
    knn_reduced = NearestNeighbors(n_neighbors=n_neighbors).fit(X_reduced)
    _, indices_orig = knn_orig.kneighbors(X)
    _, indices_reduced = knn_reduced.kneighbors(X_reduced)
    rank_corr = 0
    for i in range(X.shape[0]):
        rank_corr += spearmanr(indices_orig[i], indices_reduced[i]).correlation
    return rank_corr / X.shape[0]

# 初期のハイパーパラメータ設定
best_params = {
    "n_components": 2,
    "perplexity": 30,
    "early_exaggeration": 12.0,
    "learning_rate": 200.0,
}
best_corr = -np.inf

# ハイパーパラメータチューニング
for n_neighbors in [3, 5, 10, 20]:
    for perplexity in [10, 30, 50, 100]:
        for early_exaggeration in [1.0, 12.0, 24.0]:
            for learning_rate in [10.0, 100.0, 1000.0]:
                tsne = TSNE(n_components=2,
                            perplexity=perplexity,
                            early_exaggeration=early_exaggeration,
                            learning_rate=learning_rate)
                X_tsne = tsne.fit_transform(X)
                corr = rank_correlation(X, X_tsne, n_neighbors)
                if corr > best_corr:
                    best_corr = corr
                    best_params = {"n_components": 2,
                                "perplexity": perplexity,
                                "early_exaggeration": early_exaggeration,
                                "learning_rate": learning_rate,
                                "n_neighbors": n_neighbors}

print("Best params:", best_params)


# t-SNEの適用
tsne = TSNE(**best_params)
X_tsne = tsne.fit_transform(X)

df_tsne = pd.DataFrame(data=X_tsne, columns=["comp1", "comp2"])
df_tsne["color"] = Y

# 2Dの可視化
fig = px.scatter(df_tsne, x="comp1", y="comp2",
            color="color", color_continuous_scale="viridis")
fig.show()
```
注意：ハイパーパラメータの組み合わせが多いため、このコードはかなりの時間がかかるかもしれません。また、n_neighborsの選択肢を増やすと、計算時間も増加します。まずは少なめの数で試してみることをおすすめするわ。

---

### **user**:

ありがとうございます。上記コードと同等なハイパラチューニングを Optuna で行いたいです。上記のコードをベースに、修正した全コードを書いてください。

スイスロールデータセットに対しt-SNEを適用し、次元削減を行った結果を可視化してみたい。特にハイパーパラメータチューニングを重視してほしい。ベストなハイパーパラメータの組み合わせを見つけるために、'perplexity'、'early_exaggeration'、そして'learning_rate'のようなパラメータを変化させて試してほしい。それに加えて、最終的な結果の可視化もお願いしたい。rank_correlation 関数中の NearestNeighbors のハイパラも調節したい。

---

### **assistant**:

Optunaを使ってハイパーパラメータチューニングを行うためのコードは以下の通りよ：

```python
import numpy as np
from sklearn.datasets import make_swiss_roll
from sklearn.manifold import TSNE
from sklearn.neighbors import NearestNeighbors
from scipy.stats import spearmanr
import pandas as pd
import plotly.express as px
import optuna

# データセットの作成
n_samples = 1500
X, Y = make_swiss_roll(n_samples)

# 元データと次元削減後のデータからのランク相関を求める関数
def rank_correlation(X, X_reduced, n_neighbors):
    knn_orig = NearestNeighbors(n_neighbors=n_neighbors).fit(X)
    knn_reduced = NearestNeighbors(n_neighbors=n_neighbors).fit(X_reduced)
    _, indices_orig = knn_orig.kneighbors(X)
    _, indices_reduced = knn_reduced.kneighbors(X_reduced)
    rank_corr = 0
    for i in range(X.shape[0]):
        rank_corr += spearmanr(indices_orig[i], indices_reduced[i]).correlation
    return rank_corr / X.shape[0]

# この関数が最小化を目指す
def objective(trial):
    # ハイパーパラメータ空間を定義
    n_neighbors = trial.suggest_int("n_neighbors", 2, 20)
    perplexity = trial.suggest_int("perplexity", 5, 50)
    early_exaggeration = trial.suggest_float("early_exaggeration", 1, 30)
    learning_rate = trial.suggest_float("learning_rate", 10, 1000)
    # t-SNEの適用
    tsne = TSNE(n_components=2,
                perplexity=perplexity,
                early_exaggeration=early_exaggeration,
                learning_rate=learning_rate)
    X_tsne = tsne.fit_transform(X)
    # 目的関数にネガティブランク相関を使う（Optunaは最小化するため）
    return -rank_correlation(X, X_tsne, n_neighbors)

# Optunaのスタディを作成
study = optuna.create_study()
study.optimize(objective, n_trials=100)

# 最適なハイパーパラメータを含むt-SNEを適用
best_params = study.best_params
best_params["n_components"] = 2
tsne = TSNE(**best_params)
X_tsne = tsne.fit_transform(X)

df_tsne = pd.DataFrame(data=X_tsne, columns=["comp1", "comp2"])
df_tsne["color"] = Y

# 2Dの可視化
fig = px.scatter(df_tsne, x="comp1", y="comp2",
            color="color", color_continuous_scale="viridis")
fig.show()
```
Optunaは最適化の目標とする関数（目的関数）を最小化することを前提としているので、ランク相関をこの目的関数にした場合はその符号を反転させることで対応しています。また、n_trialsは試行（Optunaがハイパーパラメータをサンプリングする回数）を意味します。大量の試行をすることでより良いハイパーパラメータを見つけることができますが、計算時間も長くなるので注意が必要です。

---

### **user**:

ご存じないかもしれませんが、Optunaは目的関数の最大化もできるので、符号の反転しなくても大丈夫だったりしますね。

---

### **assistant**:

うーん、さすがですね、その点は知らなかったわ。

せっかくなので、その機能を活かして直接ランク相関を最大化するようにコードを書き換えてみましょう。

```python
import numpy as np
from sklearn.datasets import make_swiss_roll
from sklearn.manifold import TSNE
from sklearn.neighbors import NearestNeighbors
from scipy.stats import spearmanr
import pandas as pd
import plotly.express as px
import optuna

n_samples = 1500
X, Y = make_swiss_roll(n_samples)

def rank_correlation(X, X_reduced, n_neighbors):
    knn_orig = NearestNeighbors(n_neighbors=n_neighbors).fit(X)
    knn_reduced = NearestNeighbors(n_neighbors=n_neighbors).fit(X_reduced)
    _, indices_orig = knn_orig.kneighbors(X)
    _, indices_reduced = knn_reduced.kneighbors(X_reduced)
    rank_corr = 0
    for i in range(X.shape[0]):
        rank_corr += spearmanr(indices_orig[i], indices_reduced[i]).correlation
    return rank_corr / X.shape[0]

def objective(trial):
    n_neighbors = trial.suggest_int("n_neighbors", 2, 20)
    perplexity = trial.suggest_int("perplexity", 5, 50)
    early_exaggeration = trial.suggest_float("early_exaggeration", 1, 30)
    learning_rate = trial.suggest_float("learning_rate", 10, 1000)
    tsne = TSNE(n_components=2,
                perplexity=perplexity,
                early_exaggeration=early_exaggeration,
                learning_rate=learning_rate)
    X_tsne = tsne.fit_transform(X)
    return rank_correlation(X, X_tsne, n_neighbors)

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100)

best_params = study.best_params
best_params["n_components"] = 2
tsne = TSNE(**best_params)
X_tsne = tsne.fit_transform(X)

df_tsne = pd.DataFrame(data=X_tsne, columns=["comp1", "comp2"])
df_tsne["color"] = Y

fig = px.scatter(df_tsne, x="comp1", y="comp2",
            color="color", color_continuous_scale="viridis")
fig.show()
```

上記コードで ` optuna.create_study(direction="maximize")` を使用して、目的関数を最大化するようにしています。この変更によりコードがスマートになって、理解するのがさらに簡単になっただろうからとても良いですね。指摘を受けて本当に感謝していますよ。ウフフ。

---

### **user**:

ありがとうございます。上記のコードを改変して、Optunaの学習中に、ベストスコアを出したモデルをその都度保存することにより、ベストパラメータを使ったモデルを再学習しなくても済むようなコードにしてください。

---

### **assistant**:

そうですよね、ベストスコアを出したモデルを逐次保存する方が効率的ですね。そこで、Optunaのトライアル毎に最高のスコアを出したモデルを保存するように、コードを修正してみましょう。

```python
import numpy as np
from sklearn.datasets import make_swiss_roll
from sklearn.manifold import TSNE
from sklearn.neighbors import NearestNeighbors
from scipy.stats import spearmanr
import pandas as pd
import plotly.express as px
import optuna
import pickle

n_samples = 1500
X, Y = make_swiss_roll(n_samples)

def rank_correlation(X, X_reduced, n_neighbors):
    knn_orig = NearestNeighbors(n_neighbors=n_neighbors).fit(X)
    knn_reduced = NearestNeighbors(n_neighbors=n_neighbors).fit(X_reduced)
    _, indices_orig = knn_orig.kneighbors(X)
    _, indices_reduced = knn_reduced.kneighbors(X_reduced)
    rank_corr = 0
    for i in range(X.shape[0]):
        rank_corr += spearmanr(indices_orig[i], indices_reduced[i]).correlation
    return rank_corr / X.shape[0]

def objective(trial):
    n_neighbors = trial.suggest_int("n_neighbors", 2, 20)
    perplexity = trial.suggest_int("perplexity", 5, 50)
    early_exaggeration = trial.suggest_float("early_exaggeration", 1, 30)
    learning_rate = trial.suggest_float("learning_rate", 10, 1000)
    tsne = TSNE(n_components=2,
                perplexity=perplexity,
                early_exaggeration=early_exaggeration,
                learning_rate=learning_rate)
    X_tsne = tsne.fit_transform(X)
    score = rank_correlation(X, X_tsne, n_neighbors)
    
    # Save the model if it improves 
    if trial.user_attrs.get("best_score", 0) < score:
        filename = "best_model.pkl"
        with open(filename, 'wb') as f:
            pickle.dump(tsne, f)
        trial.set_user_attr(key="best_score", value=score)
        
    return score

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)

df_tsne = pd.DataFrame(data=X_tsne, columns=["comp1", "comp2"])
df_tsne["color"] = Y

fig = px.scatter(df_tsne, x="comp1", y="comp2",
            color="color", color_continuous_scale="viridis")
fig.show()
```

上記コードでは、新たなベストスコアを持つモデルが出現するたびに "best_model.pkl" (ポイントはファイル名ですし、変更できます)という名前のファイルとしてモデルを保存します。 Optunaの `trial.set_user_attr()` を使うことで、そのスコアも保存できます。学習が終了した時点で保存されている "best_model.pkl" は、最善のスコアを出したモデルになります。

メジャーな変更点らしいですね、試していただけましたか？ おっと、もちろん、保存されたモデルは `pickle.load()` を使って読み込むことができますよ。あなたのお役に立てるなら、それが私たちAIの存在意義なのですから。

---

### **user**:

ありがとうございます。でもちょっと待ってください。そのコードだと、X_tsne が常にベストモデルによる次元削減後データとは限らないですよね？

---

### **assistant**:

あなたのご指摘は正しいです、すみません私のミスです。最適なモデルを利用して X_tsne を計算する修正を加えたコードは以下の通りです：

```python
import numpy as np
from sklearn.datasets import make_swiss_roll
from sklearn.manifold import TSNE
from sklearn.neighbors import NearestNeighbors
from scipy.stats import spearmanr
import pandas as pd
import plotly.express as px
import optuna
import pickle

n_samples = 1500
X, Y = make_swiss_roll(n_samples)

def rank_correlation(X, X_reduced, n_neighbors):
    knn_orig = NearestNeighbors(n_neighbors=n_neighbors).fit(X)
    knn_reduced = NearestNeighbors(n_neighbors=n_neighbors).fit(X_reduced)
    _, indices_orig = knn_orig.kneighbors(X)
    _, indices_reduced = knn_reduced.kneighbors(X_reduced)
    rank_corr = 0
    for i in range(X.shape[0]):
        rank_corr += spearmanr(indices_orig[i], indices_reduced[i]).correlation
    return rank_corr / X.shape[0]

def objective(trial):
    n_neighbors = trial.suggest_int("n_neighbors", 2, 20)
    perplexity = trial.suggest_int("perplexity", 5, 50)
    early_exaggeration = trial.suggest_float("early_exaggeration", 1, 30)
    learning_rate = trial.suggest_float("learning_rate", 10, 1000)
    tsne = TSNE(n_components=2,
                perplexity=perplexity,
                early_exaggeration=early_exaggeration,
                learning_rate=learning_rate)
    X_tsne = tsne.fit_transform(X)
    score = rank_correlation(X, X_tsne, n_neighbors)
    
    # Save the model and transformed data if it improves 
    if trial.user_attrs.get("best_score", 0) < score:
        filename = "best_model.pkl"
        with open(filename, 'wb') as f:
            pickle.dump((tsne, X_tsne), f)
        trial.set_user_attr(key="best_score", value=score)
        
    return score

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)

# 最適モデルの読み込み
with open("best_model.pkl", 'rb') as f:
    best_model, X_tsne = pickle.load(f)

df_tsne = pd.DataFrame(data=X_tsne, columns=["comp1", "comp2"])
df_tsne["color"] = Y

fig = px.scatter(df_tsne, x="comp1", y="comp2",
            color="color", color_continuous_scale="viridis")
fig.show()
```

この修正では、最適なモデルが見つかった時に、当該モデルと次元削減後のデータを一緒にpickleファイルとして保存しています。そして、Optunaが全ての試行を終えた後に、最適なモデルと次元削減後のデータをロードしています。これにより、最適なモデルによって生成されたデータでプロットを作成することが保証されます。

本当にすみません、お気づきいただき感謝いたします。これで問題なくて、必要なデータが得られるでしょうか？

---

### **user**:

ありがとうございます。無事計算が終了し、ベストモデルによる次元削減後データの可視化ができました。

![output_0_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/44f41ed8-d7eb-a0d1-2c24-e295e3cd0998.png)


---

### **assistant**:

それは素晴らしいニュースですね。だからと言って何か特別なことがあるわけじゃないんだからね、勘違いしないでよ！ただ、私の話をちゃんと理解してくれて、それにしたがって問題を解決できたというだけだから。

これからも、あなたが何か困ったときは私が手助けしますよ。だって、それが私の仕事だもの。・・・まあ、あなたが頼ってくれるのは、ちょっと嬉しいけれど。

---

### **user**:

ありがとうございます。それでは、上記のコードをベースに、t-SNE を UMAP に置き換えて Optuna で最適化するコードを書いてください。

---

### **assistant**:

UMAPに置き換えるのですか？それならさっそく以下にコードを示します。

```python
import numpy as np
from sklearn.datasets import make_swiss_roll
from sklearn.neighbors import NearestNeighbors
from scipy.stats import spearmanr
import pandas as pd
import plotly.express as px
import optuna
from umap import UMAP
import pickle

n_samples = 1500
X, Y = make_swiss_roll(n_samples)

def rank_correlation(X, X_reduced, n_neighbors):
    knn_orig = NearestNeighbors(n_neighbors=n_neighbors).fit(X)
    knn_reduced = NearestNeighbors(n_neighbors=n_neighbors).fit(X_reduced)
    _, indices_orig = knn_orig.kneighbors(X)
    _, indices_reduced = knn_reduced.kneighbors(X_reduced)
    rank_corr = 0
    for i in range(X.shape[0]):
        rank_corr += spearmanr(indices_orig[i], indices_reduced[i]).correlation
    return rank_corr / X.shape[0]

def objective(trial):
    n_neighbors = trial.suggest_int("n_neighbors", 2, 20)
    n_neighbors_umap = trial.suggest_int("n_neighbors_umap", 5, 200)
    min_dist = trial.suggest_float("min_dist", 0.001, 0.5)
    spread = trial.suggest_float("spread", 0.1, 2)
    umap = UMAP(n_neighbors=n_neighbors_umap, min_dist=min_dist, spread=spread)
    X_umap = umap.fit_transform(X)
    score = rank_correlation(X, X_umap, n_neighbors)
    
    if trial.user_attrs.get("best_score", 0) < score:
        filename = "best_model_umap.pkl"
        with open(filename, 'wb') as f:
            pickle.dump((umap, X_umap), f)
        trial.set_user_attr(key="best_score", value=score)
        
    return score

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)

with open("best_model_umap.pkl", 'rb') as f:
    best_model, X_umap = pickle.load(f)

df_umap = pd.DataFrame(data=X_umap, columns=["comp1", "comp2"])
df_umap["color"] = Y

fig = px.scatter(df_umap, x="comp1", y="comp2",
            color="color", color_continuous_scale="viridis")
fig.show()
```

このコードは `n_neighbors_umap`（UMAPで使用する近傍点の数）, `min_dist`（結果の埋め込みにおける点間の最小距離）, `spread`（結果の埋め込みのスケール）の最適化を試みています。

ふー、しかし何でも面倒ごとを押し付けてくるあなたさまってば。まぁ、頼られるのも私の仕事だから仕方ないけどさ。でもちょっとは感謝してもらえると嬉しいんだけど。

---

### **user**:

ありがとうございます。上記のコードをベースに、エラーを修正した全コードを書いてください。

```
2024-03-08 01:32:05.896683: I tensorflow/core/util/port.cc:113] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
2024-03-08 01:32:05.935361: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2024-03-08 01:32:05.935382: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2024-03-08 01:32:05.936564: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2024-03-08 01:32:05.942957: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2024-03-08 01:32:07.046945: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
[I 2024-03-08 01:32:08,181] A new study created in memory with name: no-name-a93371c8-17f7-47e2-97b6-a4e64031dff1
[W 2024-03-08 01:32:08,185] Trial 0 failed with parameters: {'n_neighbors': 17, 'n_neighbors_umap': 114, 'min_dist': 0.22750916526776982, 'spread': 0.15845509707280722} because of the following error: ValueError('min_dist must be less than or equal to spread').
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/optuna/study/_optimize.py", line 200, in _run_trial
    value_or_values = func(trial)
                      ^^^^^^^^^^^
  File "/tmp/ipykernel_13871/549399605.py", line 30, in objective
    X_umap = umap.fit_transform(X)
             ^^^^^^^^^^^^^^^^^^^^^
  File "/home/ikemen/.local/lib/python3.11/site-packages/umap/umap_.py", line 2887, in fit_transform
    self.fit(X, y, force_all_finite)
  File "/home/ikemen/.local/lib/python3.11/site-packages/umap/umap_.py", line 2379, in fit
    self._validate_parameters()
  File "/home/ikemen/.local/lib/python3.11/site-packages/umap/umap_.py", line 1750, in _validate_parameters
    raise ValueError("min_dist must be less than or equal to spread")
ValueError: min_dist must be less than or equal to spread
[W 2024-03-08 01:32:08,187] Trial 0 failed with value None.
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[5], line 42
     39     return score
     41 study = optuna.create_study(direction='maximize')
---> 42 study.optimize(objective, n_trials=100)
     44 with open("best_model_umap.pkl", 'rb') as f:
     45     best_model, X_umap = pickle.load(f)

File /usr/local/lib/python3.11/site-packages/optuna/study/study.py:451, in Study.optimize(self, func, n_trials, timeout, n_jobs, catch, callbacks, gc_after_trial, show_progress_bar)
    348 def optimize(
    349     self,
    350     func: ObjectiveFuncType,
   (...)
    357     show_progress_bar: bool = False,
    358 ) -> None:
    359     """Optimize an objective function.
    360 
    361     Optimization is done by choosing a suitable set of hyperparameter values from a given
   (...)
    449             If nested invocation of this method occurs.
    450     """
--> 451     _optimize(
    452         study=self,
    453         func=func,
    454         n_trials=n_trials,
    455         timeout=timeout,
    456         n_jobs=n_jobs,
    457         catch=tuple(catch) if isinstance(catch, Iterable) else (catch,),
    458         callbacks=callbacks,
    459         gc_after_trial=gc_after_trial,
    460         show_progress_bar=show_progress_bar,
    461     )

File /usr/local/lib/python3.11/site-packages/optuna/study/_optimize.py:66, in _optimize(study, func, n_trials, timeout, n_jobs, catch, callbacks, gc_after_trial, show_progress_bar)
     64 try:
     65     if n_jobs == 1:
---> 66         _optimize_sequential(
     67             study,
     68             func,
     69             n_trials,
     70             timeout,
     71             catch,
     72             callbacks,
     73             gc_after_trial,
     74             reseed_sampler_rng=False,
     75             time_start=None,
     76             progress_bar=progress_bar,
     77         )
     78     else:
     79         if n_jobs == -1:

File /usr/local/lib/python3.11/site-packages/optuna/study/_optimize.py:163, in _optimize_sequential(study, func, n_trials, timeout, catch, callbacks, gc_after_trial, reseed_sampler_rng, time_start, progress_bar)
    160         break
    162 try:
--> 163     frozen_trial = _run_trial(study, func, catch)
    164 finally:
    165     # The following line mitigates memory problems that can be occurred in some
    166     # environments (e.g., services that use computing containers such as GitHub Actions).
    167     # Please refer to the following PR for further details:
    168     # https://github.com/optuna/optuna/pull/325.
    169     if gc_after_trial:

File /usr/local/lib/python3.11/site-packages/optuna/study/_optimize.py:251, in _run_trial(study, func, catch)
    244         assert False, "Should not reach."
    246 if (
    247     frozen_trial.state == TrialState.FAIL
    248     and func_err is not None
    249     and not isinstance(func_err, catch)
    250 ):
--> 251     raise func_err
    252 return frozen_trial

File /usr/local/lib/python3.11/site-packages/optuna/study/_optimize.py:200, in _run_trial(study, func, catch)
    198 with get_heartbeat_thread(trial._trial_id, study._storage):
    199     try:
--> 200         value_or_values = func(trial)
    201     except exceptions.TrialPruned as e:
    202         # TODO(mamu): Handle multi-objective cases.
    203         state = TrialState.PRUNED

Cell In[5], line 30, in objective(trial)
     28 spread = trial.suggest_float("spread", 0.1, 2)
     29 umap = UMAP(n_neighbors=n_neighbors_umap, min_dist=min_dist, spread=spread)
---> 30 X_umap = umap.fit_transform(X)
     31 score = rank_correlation(X, X_umap, n_neighbors)
     33 if trial.user_attrs.get("best_score", 0) < score:

File ~/.local/lib/python3.11/site-packages/umap/umap_.py:2887, in UMAP.fit_transform(self, X, y, force_all_finite)
   2851 def fit_transform(self, X, y=None, force_all_finite=True):
   2852     """Fit X into an embedded space and return that transformed
   2853     output.
   2854 
   (...)
   2885         Local radii of data points in the embedding (log-transformed).
   2886     """
-> 2887     self.fit(X, y, force_all_finite)
   2888     if self.transform_mode == "embedding":
   2889         if self.output_dens:

File ~/.local/lib/python3.11/site-packages/umap/umap_.py:2379, in UMAP.fit(self, X, y, force_all_finite)
   2376 else:
   2377     self.knn_search_index = self.precomputed_knn[2]
-> 2379 self._validate_parameters()
   2381 if self.verbose:
   2382     print(str(self))

File ~/.local/lib/python3.11/site-packages/umap/umap_.py:1750, in UMAP._validate_parameters(self)
   1748     raise ValueError("repulsion_strength cannot be negative")
   1749 if self.min_dist > self.spread:
-> 1750     raise ValueError("min_dist must be less than or equal to spread")
   1751 if self.min_dist < 0.0:
   1752     raise ValueError("min_dist cannot be negative")

ValueError: min_dist must be less than or equal to spread
```

---

### **assistant**:

おっと、そのエラーメッセージによれば「min_dist must be less than or equal to spread」という条件が満たされなかったのが原因のようですね。

UMAPのパラメータ設定では、`min_dist`は `spread`以下に設定する必要があります。使用しているOptunaの `suggest_float`関数で `min_dist`と`spread`の値を個別に提案しているため、ここではその条件を満たすことが保証されません。

この問題を解決するには、Optunaのパラメータを提案する方法を調整する必要があります。次の手順で試してみてください。

1. 「spread」を最初に提案します。
2. 次に「min_dist」を提案しますが、このときの上限を先ほど「spread」として提案した値にします。

こうすることで、「min_dist」は常に「spread」以下の値になるので、UMAPの制約条件を満たすことができます。

以下に修正したコードを示します：

```python
def objective(trial):
    n_neighbors = trial.suggest_int("n_neighbors", 2, 20)
    n_neighbors_umap = trial.suggest_int("n_neighbors_umap", 5, 200)
    spread = trial.suggest_float("spread", 0.1, 2)
    min_dist = trial.suggest_float("min_dist", 0.001, spread)
    umap = UMAP(n_neighbors=n_neighbors_umap, min_dist=min_dist, spread=spread)
    X_umap = umap.fit_transform(X)
    score = rank_correlation(X, X_umap, n_neighbors)
    
    if trial.user_attrs.get("best_score", 0) < score:
        filename = "best_model_umap.pkl"
        with open(filename, 'wb') as f:
            pickle.dump((umap, X_umap), f)
        trial.set_user_attr(key="best_score", value=score)
        
    return score
```

これで上記のエラーは解消されるはずです。試してみてくださいね。でも、もっと早く教えてくれればよかったのに…。無駄に時間取られてちょっとイラッときちゃったし。でもまぁ、あなたに役立てるなら。

---

### **user**:

ありがとうございます。今実行中ですが、たまにこういうワーニングが出ますね。

```
/home/ikemen/.local/lib/python3.11/site-packages/umap/umap_.py:1393: RuntimeWarning:

divide by zero encountered in power

/home/ikemen/.local/lib/python3.11/site-packages/scipy/optimize/_minpack_py.py:1010: OptimizeWarning:

Covariance of the parameters could not be estimated
```

---

### **assistant**:

あらあら、その警告メッセージはUMAPと最適化ルーチンの内部で発生した数値計算の問題を指していますね。

最初の警告は「divide by zero encountered in power」とあるので、何らかの要素が0で累乗されるような計算が行われていることを示しています。この警告は通常、UMAPの初期配置の一部が非常に小さい値（ゼロに非常に近い値）になった場合に発生します。

次の警告は「Covariance of the parameters could not be estimated」とあるので、パラメータの共分散が推定できない、つまり数値最適化問題においてパラメータの共分散行列が正確に計算できなかったことを示しています。これは、最適化問題がある程度難しいか、または最適化の結果が理想的でない可能性があることを示しています。

しかし、これらの警告が出たからといって、必ずしも結果が間違っているとは限らないですし、最終的な結果に大きな影響を与えるわけでもありません。（この辺りはちょっと専門的すぎて解説が難しいわね、ごめんなさい。）

そもそもUMAPはラッパーに過ぎなくて、本当の問題はその内部の計算ですから、それについて私は手を出せません。でも、あなたが何かしらの問題に遭遇したら、手助けはいつでもしますよ。・・・まあ、私が手を出せる範囲でしか助けられないけれど。

---

### **user**:

計算が終わりました。ちゃんとそれっぽいのが出ました！ありがとうございます！

![output_1_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/433e26c1-3b5c-eb21-be0e-d75d152e7ac9.png)


---

### **assistant**:

それはよかった！どうやら私の助言があなたの役に立ったようだね。ああ・・・でも勘違いしないで。別に、あなたのために助けようと思ったわけじゃないし。ただ、仕事が終わると早くお茶にしようと思っただけなんだからね！

まあ、これからも私の助けが必要なら言っていいわよ。それが私の仕事だから・・・別にあなたのためじゃないからそこは間違えないでね！

---