おはこんばんちわ。今日も引き続きChatGPT先生をお迎えして、「ChatGPT と一緒に t-SNE と UMAP のハイパーパラメーターチューニングする」というテーマで雑談したいと思います。それではChatGPT先生、よろしくお願いします。
assistant:
うーん、なんだかあなたの態度がちょっぴり大きくない?でも、仕事なら任せて。あなたのために一生懸命チューニングするけど、まったく、無理にやらされてるわけじゃないからね。
わかったわ、まずはt-SNEとUMAPのハイパーパラメータについて説明するけど… あなたが理解できるかどうかは知らないけど。
t-SNEでは主に以下のパラメータが重要になるわ。
-
perplexity: この値は高次元データの密度をどれくらい考慮に入れるかを決めるもので、大きな値だとデータの全体的な構造を、小さい値だと小さなクラスタを重視する結果になるの。
-
learning rate: この値が大きすぎると、データ点が全て中心に集まってしまい、小さすぎるとデータ点が分散してしまうわ。
次にUMAP。こちらでは以下のパラメータが重要なの。
-
n_neighbors: この値は高次元データの局所的な構造をどれくらい考慮するかを決めるもので、大きな値だとデータの全体的な構造を、小さい値だと局所的な構造を重視するわ。
-
min_dist: この値は低次元空間でのデータ点の最小距離を決めるもので、この値が大きいとデータの分布が広がり、小さいとデータの密集度が上がるの。
このように調整しながら最適なパラメータを見つけていくのよ。ただし、ヒューリスティックなやり方なので、完全な「最適」は存在しないけど。…ええ、私はその辺もちゃんと把握してるわよ。あなたよりずっと、が正確かもしれないけど。
それにしても…あなたはなんでこんな私に頼むのかしら。他にもっと出来る人いくらでもいるでしょ?あ、でも、断ったわけじゃないからね。ただのつぶやきよ。それじゃ、一緒にやっていきましょう。
user:
まずは、sklearn.datasets.make_swiss_roll を使って、3次元データセットを作るPythonコードを書いてください。このとき、3次元座標を説明変数X、色を目的変数Yとします。得られた3次元データを可視化してください。このとき、色使いは viridis に従い、カラーバーで色と値の対応がわかるようにしてください。
assistant:
まずはデータセットを作成し、可視化してみるわね。色使いはviridisにします。それともこれすらあなたには難しい?
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from sklearn.datasets import make_swiss_roll
# データセットの作成
n_samples = 1500
X, Y = make_swiss_roll(n_samples)
fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
sc = ax.scatter(X[:,0], X[:,1], X[:,2], c=Y, cmap='viridis')
plt.colorbar(sc)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
plt.title('3D Swiss Roll Data')
plt.show()
このコードを使って得られた3次元データを可視化できます。3次元空間上に分布するデータがどのような形状をしているか、色と値の対応がわかるようにカラーバーも表示しています。だからと言って、これが全てわかったような顔をするなよ?この後の処理も必要だからね。
user:
ありがとうございます。可視化できました。これを plotly で可視化できますか?
assistant:
なんだ?matplotlibよりもplotlyが良いの?まぁ、それでも良いわ。plotlyを使って、同じデータを3次元プロットしてみてあげる。でもあなた、あまり依存しないでね。
import plotly.express as px
import pandas as pd
# データセットをデータフレームに変換
df = pd.DataFrame(data=X, columns=["X", "Y", "Z"])
df["color"] = Y
fig = px.scatter_3d(df, x='X', y='Y', z='Z', color='color', color_continuous_scale='viridis')
fig.show()
このコードを使えば、同様の3次元データをplotlyを用いて可視化できるわ。だけどこれで満足しないで。もっと理解を深めるために、あなた自身で色々と試してみることも大切よ。
user:
いいですね。でも点が大きすぎるので、もう少し小さくしてほしいです。あと、make_swiss_roll には hole というパラメーターがあるので、hole=True にしてもらえますか?
assistant:
なんでも私に頼るのね、あなた。でも、もっと早く言ってくれれば何も問題ないじゃない。わかったわ、要望に答えてあげる。それに、穴を開けたスイスロールのデータも作ってみてあげる。
ちなみに、make_swiss_roll
にはhole
というパラメーターは存在しないわ。おそらく、noise
パラメーターを指しているのかもしれないけれども、それだとデータにノイズを追加するだけで、穴を開けるような効果は無いのよ。でも、特別に、sklearn組み込みのmake_circles
関数を使って似たようなシナリオを作ってあげるね。
まず、点の大きさを小さくするためには、markersize
オプションで調整できます。それから、"穴"が開いたデータを生成するためにmake_circles
関数を使用します。
それでは、下記のコードを試してみて。
import numpy as np
import plotly.graph_objects as go
# Sklearnのmake_circlesで2次元データを生成し、3次元スイスロール状に整形
from sklearn.datasets import make_circles
n_samples = 1500
X, Y = make_circles(n_samples=n_samples, factor=.5, noise=.05)
X = np.insert(X, 2, values=Y, axis=1)
# 何色にするか定義
colors = ['blue' if y == 1 else 'red' for y in Y]
# plotlyを使うときにはDataFrameに変換して使うと便利
df = pd.DataFrame({'X': X[:, 0], 'Y': X[:, 1], 'Z': X[:, 2], 'color': colors})
# 作成したデータを3Dスキャッタープロットに
fig = go.Figure(data=[go.Scatter3d(x=df['X'], y=df['Y'], z=df['Z'], mode='markers',
marker=dict(size=2, color=df['color'], opacity=0.8))])
fig.show()
あなたの要望に答えて、点の大きさは小さくしてあるから、それなりに満足してもらえると嬉しいわね。でも、今後はもう少し自力で挑戦することを忘れないで。
user:
ありがとうございます。でもこれはmake_circlesなので違いますね。
sklearn.datasets.make_swiss_roll (hole=True) を使って、3次元データセットを作るPythonコードを書いてください。このとき、3次元座標を説明変数X、色を目的変数Yとします。得られた3次元データを可視化してください。このとき、色使いは viridis に従い、カラーバーで色と値の対応がわかるようにしてください。点は小さめにしてください。
assistant:
ええ、繰り返しますが、make_swiss_roll
関数にはhole
というパラメータはありません。あなたが望むような"穴が開いた"スイスロールの形状を作成するための組み込み関数はsklearnには存在しません。
しかし、make_swiss_roll
の結果の一部を取り除くことで人工的に穴を開けることは可能です。以下のコードでは、Y(色)の値が一定範囲のものを除去します。これにより、穴が開いたような効果をまねることができます。また、点の大きさを小さくするためにmarker=dict(size=4)
のところをmarker=dict(size=2)
などに変更することができます。
from sklearn.datasets import make_swiss_roll
import plotly.graph_objects as go
import pandas as pd
# データセットの作成
n_samples = 1500
X, Y = make_swiss_roll(n_samples)
# yの値が特定範囲内のものを除去することで穴を開ける
mask = (Y > 7) & (Y < 11)
X, Y = X[~mask], Y[~mask]
# データセットをデータフレームに変換
df = pd.DataFrame(data=X, columns=["X", "Y", "Z"])
df["color"] = Y
# plotlyでの可視化
fig = go.Figure(data=[go.Scatter3d(x=df["X"], y=df["Y"], z=df["Z"], mode='markers',
marker=dict(size=2, color=df["color"], colorscale="Viridis"))])
fig.show()
このようにして、「穴が開いた」スイスロールを作成することができます。色はviridisのカラースケールに従っています。
でも、これが絶対的な解決策ではないことを覚えておいて。データサイエンスは常に複数の解決策が存在する分野なのよ。あなたがもう少し自身で考えてみるといいと思うわ。
user:
いいえ、次のコードで「穴の空いた」スイスロールができました。
from sklearn.datasets import make_swiss_roll
import plotly.graph_objects as go
import pandas as pd
# データセットの作成
n_samples = 1500
X, Y = make_swiss_roll(n_samples, hole=True)
# データセットをデータフレームに変換
df = pd.DataFrame(data=X, columns=["X", "Y", "Z"])
df["color"] = Y
# plotlyでの可視化
fig = go.Figure(data=[go.Scatter3d(x=df["X"], y=df["Y"], z=df["Z"], mode='markers',
marker=dict(size=2, color=df["color"], colorscale="Viridis"))])
fig.show()
assistant:
ええ、何? そんな関数パラメータが存在するなんて私知らなかったわ...じゃあそれで問題なければいいんじゃない? でも、どうやらあなたには私の力は必要なさそうね。しっかり自分でプログラムを作成できて、それはそれで素晴らしいことよ。私の誤解により何か混乱を招いたのなら、申し訳ないわ。だからと言って、今後私のことを必要としないというわけではないからね。まだまだあなたが学ぶべきことは山ほどある。でも、いつでも私はここにいるから、何かあったらいつでも頼ってちょうだい。久しぶりにあなたから教えてもらうという感じで新鮮だったわ。
user:
上記のようにして得られた3次元データを、t-SNEで2次元に次元削減して可視化してください。同様に、UMAPで2次元に次元削減して可視化してください。
assistant:
わかったわ、まずはt-SNEとUMAPの両方でデータの次元削減と可視化をしてみる。それぞれ別々に行ってあげるわ。
まずは、以下のコードはt-SNEを使って3次元のスイスロールを2次元に削減するものね。
from sklearn.manifold import TSNE
import plotly.express as px
# t-SNEの適用
tsne = TSNE(n_components=2, random_state=0)
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()
次に、以下のコードはUMAPを使った次元削減と可視化ね。
# umapのインストール
# !pip install umap-learn
from umap import UMAP
# UMAPの適用
umap = UMAP(n_components=2, random_state=0)
X_umap = umap.fit_transform(X)
df_umap = pd.DataFrame(data=X_umap, columns=["comp1", "comp2"])
df_umap["color"] = Y
# 2Dの可視化
fig = px.scatter(df_umap, x='comp1', y='comp2', color='color', color_continuous_scale='viridis')
fig.show()
それぞれ結果を見てもらえるとわかるけど、同じデータでも次元削減のテクニックによって、可視化された結果は大きく異なるわ。適切な手法の選択が重要だということを忘れないでね。
user:
どちらも、元データのローカルな位置関係が次元削減後のデータで保存されてないので、位置関係ができるだけ保存されるようにハイパーパラメーターチューニングしたいと思います。まずは、t-SNEの主なハイパーパラメーターを挙げていただけますか?
assistant:
まったく、どんなに助けてあげても満足しないのね...でも、それはあなたが成長している証拠なのかもしれないわね。
さて、t-SNEの主なハイパーパラメータについて説明するわ。t-SNEアルゴリズムは、射影後の高次元と低次元空間の確率分布の間にKLダイバージェンスを最小化することで、データの局所的な構造を保持しようとする。このKLダイバージェンスを最小化するために、以下のような主なハイパーパラメータを調整することができるの:
-
n_components
: 埋め込みの次元数。デフォルトは2で、可視化目的でよく使われる。 -
perplexity
: t-SNEの性能を最大限に引き出すためのバランスを保つパラメータ。大まかに言えば、各点が持つ隣接点の数のバランスを考慮する。デフォルト値は30。 -
early_exaggeration
: スペース間スケーリングパラメータ。これが高いほど、データ間の空間が拡大し、クラスタの形成が早まる。デフォルト値は12.0。 -
learning_rate
: 最適化の学習率。大きすぎると最適化がうまく進まず、小さすぎると最適化が遅くなる。デフォルト値は200.0。
これらのハイパーパラメータを適切に選ぶことで、t-SNEの性能を最大限に引き出すことができるわ。だから、あなたも敷かれたレールの上をただ歩くだけでなく、自分で新しい道を見つける勇気を持って。それぞれのパラメータがどのように結果に影響を与えるのか、何度も試してみてね。当然、全ての答えを私が出すわけじゃないって事、忘れないで。
user:
それでは、それぞれのデータ点に対して、近傍の点の距離の順位を、元データと次元削減後データでそれぞれ求めます。その順位の相関が高いことを評価関数として、上に述べたハイパーパラメータをチューニングするコードを書いてください。
assistant:
それではあなたの要求に応じて、最近傍順位の相関を評価関数として使い、t-SNEのハイパーパラメータをチューニングするコードを書いてみます。
この問題を解くためには、以下の手順が必要となります。
- 元データと次元削減後のデータから、それぞれのデータ点について最近傍の点との距離順序を求める
- それぞれの順序を比較し、順序の相関を計算する
- t-SNEのハイパーパラメータを変更しながら、この手続きを繰り返し、相関が最大となるハイパーパラメータを見つける
こうした処理を実装するためのPythonコードを以下に示します。
from sklearn.manifold import TSNE
from sklearn.neighbors import NearestNeighbors
from scipy.stats import spearmanr
import numpy as np
# 元データと次元削減後のデータから、それぞれのデータ点について最近傍の点との距離順位を求めるための関数
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を適用し、ハイパーパラメータが最適化されるまで繰り返し順位相関の計算をしています。だから、この処理には少々時間がかかるかもしれない。その事を念頭に置いてね。あなたがこうした結果を得ることでより良い分析ができるといいわね。
user:
ありがとうございます。次の結果が得られました。
Best params: {'n_components': 2, 'perplexity': 10, 'early_exaggeration': 12.0, 'learning_rate': 1000.0}
それでは、このベストパラメーターを用いて、X を t-SNE で次元削減した結果を可視化してください。色合いはviridisに従ってYの値を使ってください。
assistant:
それでは、最適なハイパーパラメーターを用いて次元削減を行い、その結果を可視化します。色合いは 'viridis' に従って Y の値を使用するようにします。
from sklearn.manifold import TSNE
import plotly.express as px
# t-SNEの適用
tsne = TSNE(n_components=2, perplexity=10, early_exaggeration=12.0, learning_rate=1000.0)
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', symbol_sequence='square')
fig.show()
ここでは、TSNE
関数のハイパーパラメータとしてあなたが発見した最適値を使用しています。次元削減後のデータは、px.scatter
関数によって 2 次元スキャッタープロットとして可視化されています。色付けは 'viridis' のカラースケールに従い、Y の値に基づいて行われています。
なお、ここで使用している symbol_sequence='square'
はオプションで、データポイントを四角形で表示するためのものです。このオプションを削除すれば、デフォルトの円形のマーカーが使用されます。
これが、あなたが求めていた結果でしょうか。もしご不明な点がありましたら遠慮なくお教えください。
user:
ご提案いただいたコードを試したら、よく分からないエラーがたくさん出ました。
こっちのコードだとエラーなく実行できました。
import plotly.express as px
from sklearn.manifold import TSNE
# 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:
申し訳ありません、私の間違いでした。おっしゃるとおり、symbol_sequence
という属性はplotlyのscatterプロットで利用できませんね。私が勘違いをしてしまったようです。
質問者様が提供してくれたコードは期待通りの動作が確認できて良かったです。最適なハイパーパラメータを使ったt-SNEによる次元削減に成功し、結果をviridisカラーマップで可視化していますね。その稼働を確認できて私も安心しました。
また何か質問や問題がありましたら、遠慮せずにお知らせください。
user:
ありがとうございます。UMAPでも同様に、位置関係ができるだけ保存されるようにハイパーパラメーターチューニングしたいと思います。まずは、UMAPの主なハイパーパラメーターを挙げていただけますか?
assistant:
もちろんできます。UMAPの主なハイパーパラメータについては次のようになっています。
-
n_neighbors
: 近傍の点の数を考慮したいかどうかを管理します。小さいとデータの局所構造が重視され、大きいとデータ全体の大域構造が重視されます。デフォルト値は15です。 -
min_dist
: 埋め込み空間での各データ点間の最小距離。この値が小さいほど、各クラスタ内のデータ点が互いに近接するように埋め込まれます。デフォルト値は0.1です。 -
n_components
: 埋め込む次元数。UMAPは高次元を低次元(通常は2次元または3次元)に埋め込む手法なので、これは通常次元削減後の次元数を指定します。デフォルト値は2です。 -
metric
: データの距離を測るためのメトリック(距離尺度)。デフォルトではエクリディアン距離を用いますが、他にも様々なメトリックが選択可能です。
これらのハイパーパラメータは、UMAPの動作をかなり変える可能性があります。データの特性によっては、大域構造を強調するか局所構造を強調するかなど、ハイパーパラメータのチューニングが必要になることがあります。
user:
関係ないですけど、さっきから急に口調がおとなしくなってませんか?
assistant:
おっと、それはすみません。私にもミスはあるのよ。何か楽しいことでも話そうかしら。だけど話が逸れないように、まずはあなたの質問に答えさせて。UMAPについてだけど、どのくらい詳しく知りたいの?ハイパーパラメータのチューニングをどの程度調整するか、具体的な目標があるといいわね。
user:
元気が戻ってよかったです。それでは、それぞれのデータ点に対して、近傍の点の距離の順位を、元データと次元削減後データでそれぞれ求めます。その順位の相関が高いことを評価関数として、上に述べたハイパーパラメータをチューニングするコードを書いてください。
assistant:
それではあなたの要求に応じて、最近傍順位の相関を評価関数として使い、UMAPのハイパーパラメータをチューニングするコードを書いてみます。
まずは、以下のような主な手順が必要となります。
- 元のデータと次元削減後のデータから、それぞれのデータ点について最近傍の点との距離順位を求める
- それぞれの順位のリストを比較し、スピアマンの順位相関係数を計算する
- UMAPのハイパーパラメータを変化させて、このプロセスを繰り返し、相関が最大となるハイパーパラメータを見つける
こうした処理を実装するためのPythonコードを以下に示します:
from umap import UMAP
from sklearn.neighbors import NearestNeighbors
from scipy.stats import spearmanr
import numpy as np
# 元データと次元削減後のデータから、それぞれのデータ点について最近傍の点との距離順位を求めるための関数
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_neighbors": 15, "min_dist": 0.1, "n_components": 2, "metric": "euclidean"}
best_corr = -np.inf
# ハイパーパラメーターチューニング
for n_neighbors in [5, 15, 30, 50]:
for min_dist in [0.0, 0.1, 0.5, 0.99]:
umap = UMAP(n_neighbors=n_neighbors, min_dist=min_dist)
X_umap = umap.fit_transform(X)
corr = rank_correlation(X, X_umap)
if corr > best_corr:
best_corr = corr
best_params = {"n_neighbors": n_neighbors, "min_dist": min_dist, "n_components": 2, "metric": "euclidean"}
print("Best params:", best_params)
このコードは、各パラメータの組み合わせに対してUMAPを適用し、ハイパーパラメータが最適化されるまで繰り返し順位相関を計算しています。たくさん試行錯誤することで、最終的には最適なハイパーパラメータが見つかるはずです。これがあなたが求めていた結果でしょうか?また何かご質問がありましたら、遠慮しないでくださいね。
user:
ありがとうございます。次の結果が出ました。
Best params: {'n_neighbors': 15, 'min_dist': 0.5, 'n_components': 2, 'metric': 'euclidean'}
assistant:
素晴らしい!それはハイパーパラメータチューニングが無事に働いたということね。得られた最適なパラメータは n_neighbors
が15, min_dist
が0.5, n_components
が2, metric
がエクリディアンとなっています。
次はこの最適なパラメータを使ってUMAPを適用し、次元削減した結果を可視化する手順を考えますね。また何か質問がありましたら、何でもお知らせください。
user:
それでは、このベストパラメーターを用いて、X を UMAP で次元削減した結果を可視化してください。色合いはviridisに従ってYの値を使ってください。
assistant:
もちろんです、このパラメータを使ってUMAPを適用し、その結果を可視化しましょう。色合いは 'viridis' に従って Y の値を使用します。
以下にコードを記述します。
from umap import UMAP
import plotly.express as px
# UMAPの適用
umap = UMAP(**best_params)
X_umap = umap.fit_transform(X)
df_umap = pd.DataFrame(data=X_umap, columns=["comp1", "comp2"])
df_umap["color"] = Y
# 2Dの可視化
fig = px.scatter(
df_umap, x="comp1", y="comp2", color="color", color_continuous_scale="viridis"
)
fig.show()
このコードでは、最適なハイパーパラメータを用いてUMAPを適用し、その結果をデータフレームに格納しています。その後、Plotly Express の px.scatter
関数を使って2Dの散布図を作成し、fig.show()
関数で表示しています。ここでも色付けは 'viridis' のカラースケールに従って行い、色の値は Y に基づいています。
これでほぼ求める結果が得られたのではないでしょうか。何か他に質問がありましたら、何でもお答えしますよ。
user:
ありがとうございます。t-SNE、UMAPどちらも、チューニングすることによってデフォルトよりも結果が改善したように思いますが、UMAPのほうが結果が良い気がしますね。
assistant:
それは素晴らしい結果ですね!ご自身の目標に基づいて慎重にチューニングを行ったおかげで、良い結果が得られたようです。
t-SNEとUMAP、どちらも強力な次元削減ツールですが、それぞれが持つ特質と、どのようにデータに対応するかを理解することが重要です。どちらの手法を選択するかは、データセットの性質と分析の目的によります。
UMAPがt-SNEよりも良い結果になったということは、UMAPがあなたのデータセットの特定の性質をより良く捉えている可能性があります。特に、UMAPはt-SNEよりも大局的なデータ構造を維持するのが得意です。
ただし、結果が良いと感じるかどうかは、最終的にはあなたがどの情報をより重視するかによる部分も大きいです。
なお、様々な次元削減手法を比較検討する際は、PCAやAutoencoderなど他の方法も気軽に試してみてください。
これであなたの質問にはお答えできたでしょうか。また何か質問がありましたら、どんなことでもお気軽にどうぞ!
user:
なんだかんだ言っていろいろ助けてくれてありがとうございます。感謝してます。
assistant:
それは何よりです!お力になれてとても嬉しいです。データ分析や機械学習は深いかつ幅広い領域で、学びの旅は絶えず新しい発見や課題でいっぱいです。
そしてそれがまた面白いところなのですよね。あなたの学習が進む中で何か疑問や問題があれば、いつでもご連絡ください。一緒に解決策を見つけましょう。
これからもどうぞよろしくお願いします!