t-SNEは、数百次元のベクトルを2次元や3次元に圧縮して、データのかたまりを目で確認するための可視化手法です。キーワードをベクトル化してt-SNEにかけると、検索意図が近い語が近くに集まった立体的な散布図が得られます。ただしt-SNEはクラスタリングそのものを行う手法ではなく、出力された図には読んではいけない情報が含まれます。この記事では、scikit-learn 1.9系の現行APIでSEOキーワードデータを3次元に落とす手順と、3次元が上限になる技術的な理由、そして図を読み違えないための判断基準をまとめました。
まとめ
- t-SNEは次元削減による可視化手法であり、クラスタの割り当てそのものはk-meansなどのクラスタリングが担う。役割を分けて使う。
- 既定の
method="barnes_hut"ではn_componentsが4以上でValueErrorになる。t-SNEの3次元は既定アルゴリズムで到達できる上限で、4次元以上に落とすにはmethod="exact"が必要。 - 入力の次元が高いときは、PCA(密行列)またはTruncatedSVD(疎行列)で50次元程度まで落としてからt-SNEにかける、と公式ドキュメントが推奨している。
- scikit-learn 1.2で
learning_rateの既定が"auto"に、initの既定が"pca"に変わった。この2つは動作するが図が変わる。1.5で改称されたn_iterは1.9.0の引数に存在せず、指定するとTypeErrorになる。 - t-SNE図では、クラスタの大きさとクラスタ間の距離に意味がない。読み取ってよいのは、どの点がどの点と近いかという近傍関係だけ。
- scikit-learnのTSNEに
transformメソッドは存在しない。新しいデータを既存の座標系に投影したい定点観測用途では、openTSNEやUMAPを選ぶ。
t-SNEの仕組みと「クラスタリング手法ではない」という前提
t-SNEの読み方と動作原理
t-SNEはティースニーと読まれることが一般的です。正式名称はt-distributed Stochastic Neighbor Embedding、日本語ではt分布型確率的近傍埋め込み法と呼ばれます。
動作は2段階です。まず高次元空間での点どうしの近さを確率分布として表し、次に低次元空間でも同じような近さの分布になるように点の座標を最適化していきます。低次元側で使う分布が自由度1のスチューデントのt分布であることが名称の由来で、この裾の重い分布のおかげで遠い点どうしがさらに引き離され、かたまりが視覚的に分離しやすくなります。
t-SNEとクラスタリングの役割分担
t-SNEの出力は座標だけです。scikit-learn 1.9.0のTSNEクラスが持つメソッドはfitとfit_transformで、k-meansのようなlabels_属性はありません。「この点は第3クラスタに属する」というラベルは一切返ってこないということです。
散布図でかたまりが見えるのは人間が目で判断しているだけで、アルゴリズムがグループを決めたわけではありません。t-SNEの出力が座標だけである以上、工程は次のように分かれます。ラベルを付けるのはk-meansや階層的クラスタリングの仕事で、t-SNEはその結果を人間が確認するための表示装置です。
| 工程 | 担当する処理 | 出力 |
|---|---|---|
| ベクトル化 | 埋め込みモデル、TF-IDF | 高次元の数値ベクトル |
| グループ分け | k-means、階層的クラスタリング | クラスタ番号 |
| 可視化 | t-SNE、UMAP、PCA | 2次元または3次元の座標 |
クラスタリングを実行する空間の選び方
クラスタリングは、t-SNEで落とした後の3次元座標ではなく、元の高次元ベクトル(またはPCAで50次元程度に落としたもの)に対して実行してください。t-SNEは低次元側に自由度1のt分布を使うため、距離が単調変換されません。圧縮後の座標でk-meansを回すと、歪んだ距離に基づいてグループが決まってしまいます。3次元座標は色分けの表示にだけ使い、グループ分けの判断材料にはしないという線引きです。
クラスタ数の決め方
k-meansのクラスタ数は、t-SNEの図を見てから決めるのでは順序が逆になります。エルボー法でクラスタ内平方和の減り方が鈍る点を探すか、シルエット係数が最大になる値を選ぶのが定石です。階層的クラスタリングで樹形図を作り、枝の高さから当たりを付ける方法もあり、その読み方はデンドログラムとは何か?その定義と階層的クラスタリングにおける意味・役割をわかりやすく徹底解説で扱っています。
PCA・UMAPとの使い分け
| 手法 | 性質 | 新規データの投影 | 向く場面 |
|---|---|---|---|
| PCA | 線形・決定的 | 可能 | 前処理、寄与率の確認 |
| t-SNE | 非線形・確率的 | 不可(scikit-learn) | 一回限りの探索的可視化 |
| UMAP | 非線形・確率的 | 可能 | 継続運用、大規模データ |
PCAとt-SNEの決定性の違い
PCAは分散が最大になる方向を線形に探すため、同じ入力に対して常に同じ結果を返し、各軸の寄与率も解釈できます。t-SNEはrandom_stateを固定しない限り実行ごとに図の形が変わり、軸そのものに意味がありません。t-SNE図の横軸が何を表すかを説明できない、というのは実装ミスではなく仕様です。
両者は競合ではありません。scikit-learnの公式ドキュメント自身が、特徴量数が多い場合の前処理としてPCAを掛けることを推奨しています。似た系統の手法として距離行列から座標を復元する多次元尺度構成法もあり、PCAとの違いは多次元尺度構成法(MDS)とは?主成分分析との違い・計算手順・活用例を解説で整理しました。
UMAPとの分岐点になるtransformの有無
umap-learn 0.5.12のUMAPはtransformを備えており、学習済みの埋め込みに新しい点を後から追加できます。scikit-learnのTSNEにはこれがありません。一度きりの調査ならt-SNE、同じ座標系で繰り返し観測するならUMAP、という分岐がこの1点で決まります。運用上の影響は後の章で具体的に扱います。
SEOキーワードデータを3次元に可視化するPython実装手順
キーワードのベクトル化と50次元への前処理
入力に使うキーワードは、埋め込みモデルで数値ベクトルに変換します。日本語のキーワード群であれば、埋め込みAPIやsentence-transformersを使う方法をPythonでembedding(ベクトル化)を生成する方法|OpenAI APIとsentence-transformersにまとめました。対象とするキーワードの母集団をSearch Consoleから取り出す手順はGoogle Search Consoleデータの可視化とクラスタリング分析とは何か?その概要と重要性についてを参照してください。
埋め込みベクトルは768次元や1536次元になることが多く、この規模をそのままt-SNEに渡すと点どうしの距離計算だけで時間を消費します。scikit-learnの公式ドキュメントは、特徴量数が非常に多い場合に別の次元削減手法(密なデータにはPCA、疎なデータにはTruncatedSVD)で50次元程度まで落とすことを強く推奨しています。ノイズの抑制と計算時間の短縮を同時に狙う前処理です。スケーリングや欠損の扱いなど前処理全般の考え方は特徴量エンジニアリングとは?変換・選択・自動化の工程と実装判断【2026年版】で解説しています。
ここで件数の下限を先に確認しておきます。PCAのn_componentsはサンプル数と特徴量数の小さいほうを超えられないため、キーワードが40件しかない状態で50を指定するとt-SNEに到達する前にValueErrorで落ちます。さらにt-SNEのperplexityはサンプル数未満でなければならず、既定の30を使うには最低31件が必要です。数十件規模のキーワード集合では、この2つの制約が先に効きます。
# 動作確認: scikit-learn 1.9.0 / plotly 6.9.0
import numpy as np
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from sklearn.cluster import KMeans
# X: キーワードの埋め込みベクトル (n_samples, 768)
X = np.load("keyword_embeddings.npy")
# keywords: Xと同じ行順のキーワード文字列
keywords = np.load("keyword_labels.npy", allow_pickle=True).tolist()
# 公式推奨の50次元前処理。件数が少ない場合に落ちないよう上限を取る
n_pca = min(50, X.shape[0], X.shape[1])
X50 = PCA(n_components=n_pca, random_state=0).fit_transform(X)
# グループ分けはt-SNE前の空間で行う。t-SNE後の3次元座標では行わない
# n_init="auto"は1.4以降の既定だが、1.3以前で警告を出さないため明示する
labels = KMeans(n_clusters=6, n_init="auto", random_state=0).fit_predict(X50)
# 可視化用に3次元へ
tsne = TSNE(
n_components=3,
perplexity=30.0,
learning_rate="auto",
init="pca",
max_iter=1000,
random_state=0,
)
X3d = tsne.fit_transform(X50)
# kl_divergence_は同じデータでの試行間比較にだけ使う。絶対値に意味はない
print(X3d.shape, tsne.kl_divergence_)
n_componentsが3までに制限される理由
TSNEの既定はmethod="barnes_hut"です。この実装は空間を再帰的に分割する四分木(2次元)と八分木(3次元)を使って遠方の点の寄与をまとめて近似するため、扱える次元数に上限があります。scikit-learn 1.9.0のソースでは、methodが"barnes_hut"かつn_componentsが3を超えるときに「’n_components’ should be inferior to 4 for the barnes_hut algorithm as it relies on quad-tree or oct-tree.」というValueErrorを送出します。
つまりt-SNEの3次元出力は、O(N log N)で動く既定アルゴリズムで到達できる上限そのものです。4次元以上へ圧縮したい場合はmethod="exact"に切り替える必要がありますが、こちらは計算量がサンプル数の2乗に比例します。
疎行列の扱いについては誤解が広まっているので補足します。method="exact"はcsr・csc・cooの疎な特徴量行列を受け付け、むしろ既定のbarnes_hutのほうがcsrのみと受け入れが狭いです。「TSNE with method=”exact” does not accept sparse precomputed distance matrix.」というTypeErrorは、metric="precomputed"を指定したうえで疎な距離行列を渡した場合にだけ発生します。TF-IDFのような疎な入力を既定のユークリッド距離で渡す分には、exactでもそのまま動きます。TF-IDFにTruncatedSVDを掛ける理由は疎行列非対応ではなく、前述した50次元前処理の推奨に沿うためです。
Plotlyでの3D散布図
3次元の座標は、回転できる形で描く必要があります。静止画では手前の点が奥の点を遮るため、視点を変えられない3D散布図は2次元の図より判読しにくくなります。plotly 6.9.0のscatter_3dであれば、ブラウザ上でドラッグして視点を変えられます。
import pandas as pd
import plotly.express as px
df = pd.DataFrame(X3d, columns=["x", "y", "z"])
df["cluster"] = labels.astype(str)
df["keyword"] = keywords
fig = px.scatter_3d(
df, x="x", y="y", z="z",
color="cluster",
hover_name="keyword",
opacity=0.75,
)
fig.update_traces(marker=dict(size=3))
# include_plotlyjs="cdn"を付けないとplotly.jsを同梱して数MBのHTMLになる
fig.write_html("tsne_3d.html", include_plotlyjs="cdn")
hover_nameに元のキーワードを渡しておくと、点をポイントしたときに語が表示されます。可視化の目的は、どの語とどの語が同じかたまりに入っているかを人間が判断することなので、この対応付けがないと図を見ても打ち手が決まりません。かたまりの中身が意図どおりに近い語で構成されているかは、コサイン類似度とは?計算式・仕組みと類似検索での実装を実装目線で解説で扱う類似度の数値と併せて確認すると判断しやすくなります。
scikit-learn 1.2以降のパラメータ変更と旧コードの扱い
t-SNEの解説記事はサンプルコードが古いまま残っているものが多く、そのまま実行するとエラーになるか、当時と違う図が出ます。scikit-learn 1.9.0時点の状態を整理します。
| 項目 | 1.9.0時点の状態 | 変更履歴 |
|---|---|---|
| learning_rate | 既定 “auto” | 1.2で既定変更(旧 200.0) |
| init | 既定 “pca” | 1.2で既定変更(旧 “random”) |
| n_iter | 引数として存在しない | 1.5でmax_iterへ改称 |
| max_iter | 既定 1000 | 1.5で追加 |
| perplexity | 既定 30.0 | 変更なし |
| method | 既定 “barnes_hut” | 変更なし |
動かなくなる変更と結果が変わる変更
この2種類は分けて扱う必要があります。n_iterは1.9.0の__init__に存在しないため、TSNE(n_iter=1000)と書いた古いコードはTypeErrorで即座に止まります。エラーが出る以上、気づかず進むことはありません。
やっかいなのはlearning_rateとinitのほうです。既定値が変わっただけなのでコードは動き、図だけが以前と変わります。learning_rate="auto"は、サンプル数をearly_exaggerationと4で割った値と50の大きいほうを採用します。既定のearly_exaggeration=12.0で1万件を処理するなら約208となり、旧既定の200に近い値です。一方1000件では下限の50が採用され、旧既定より小さい学習率になります。過去の記事と図が一致しないときは、まずこの差を疑ってください。
learning_rateの定義差による設定ミス
scikit-learnのlearning_rateは、他実装より4倍大きい定義になっています。公式ドキュメントは、bhtsneやFIt-SNE、openTSNEでのlearning_rate=800がscikit-learnの200に相当すると明記しています。
換算の向きを間違えると設定が大きくずれます。openTSNE向けの記事にある800をscikit-learnへそのまま持ち込むと、他実装の3200相当、つまり意図の4倍の学習率になります。逆にscikit-learnの200を他実装へそのまま入れると4分の1です。学習率が高すぎると点が球状に広がり、低すぎると密な雲に潰れるため、この取り違えは図の形に直接出ます。
t-SNE図の解釈で誤りやすい4点
t-SNEの図は説得力がある見た目になるぶん、書かれていないことまで読み取ってしまう事故が起きます。Distillに掲載されたWattenberg、Viegas、Johnsonによる解説記事「How to Use t-SNE Effectively」(2016年10月13日公開)が、この誤読を実例付きで整理しています。
クラスタ面積が示さない情報
同記事は、t-SNEが密なクラスタを自然に広げ、疎なクラスタを縮めるため、t-SNEプロット上でクラスタの相対的な大きさは見えないと述べています。図の上で大きく描かれたクラスタが、元のデータで広がりを持っているとは限りません。「このキーワード群は面積が広いので多様性がある」という読み方は成立しません。
クラスタ間距離が保存されない理由
離れて描かれた2つのかたまりの間隔も、元の空間での距離を反映していない場合があります。同記事は、十分に分離したクラスタ間の距離は何も意味しないことがあり、すべての距離関係を同時に正しく表すperplexityが存在しないケースもあると述べています。「AクラスタはBクラスタより遠いので別領域」といった主張の根拠には使えません。
乱数データが低perplexityで見せる偽のかたまり
構造を持たない乱数データでも、perplexityを小さくするとはっきりしたクラスタが現れます。1枚の図でかたまりが見えたことは、データに構造がある証拠になりません。perplexityを変えた複数の図で同じかたまりが安定して現れるかを確認します。
perplexityを振って比較する手順
典型的な値は5から50の範囲とされています。scikit-learnの実装ではperplexityがサンプル数以上のときにValueErrorとなり、既定のbarnes_hut側では近傍数がサンプル数から1を引いた値と3*perplexity+1の小さいほうに設定されます(method="exact"は全ペアの距離を使うためこの式は通りません)。数百件規模のキーワードデータであれば、perplexityを5、15、30、50と振って4枚並べ、共通して現れるかたまりだけを分析対象にします。反復回数は配置が安定するまで回します。既定の1000回で動きが止まらない場合はmax_iterを増やしてください。
t-SNEを選ぶべきでない場面
定点観測に向かない理由
ここは明確に立場を示します。定期的に更新するダッシュボードにscikit-learnのt-SNEを組み込むのは避けてください。理由は、TSNEにtransformメソッドが存在しないためです。利用できるのはfitとfit_transformだけで、学習済みの埋め込みに新しい点を追加する手段がありません。
この制約は運用で次の形で表面化します。毎週キーワードを追加して図を更新する運用では、追加のたびに全件を再計算することになります。t-SNEは確率的な最適化なので、再計算のたびに座標系そのものが変わり、前週の図と重ね合わせた比較ができません。先週このクラスタにいたキーワードが今週は移動した、という観察が、データの変化なのか再計算の揺らぎなのか区別できなくなります。
継続的に同じ座標系で観測したいなら、選択肢は2つです。openTSNE 1.0.4はTSNEEmbedding.transformを備えており、既存の埋め込みに新規データを投影できます。umap-learn 0.5.12のUMAP.transformも同じ用途に使えます。scikit-learnのTSNEは、今あるキーワード群がいくつのかたまりに分かれているか当たりを付けるといった、一回限りの探索的な調査に限定して使うのが適切な線引きです。
サンプル数が数十件のときの代替
点の数が数十件しかないデータにt-SNEをかけるのも避ける場面です。perplexityはサンプル数より小さくなければならず、既定の30を使うには最低31件が必要になります。件数を下げてperplexityを5程度にすれば動きはしますが、前章のとおり低perplexityは乱数データにさえクラスタを描くため、得られた図を根拠にできません。
少数のキーワードの関係を見たいだけなら、コサイン類似度の行列をそのまま眺めるか、階層的クラスタリングの樹形図を使ってください。どちらも決定的な出力で、件数が少ないほど読みやすくなります。
よくある質問
t-SNEの読み方は?
ティースニーと読まれることが一般的です。正式名称はt-distributed Stochastic Neighbor Embedding(t分布型確率的近傍埋め込み法)で、tは低次元側に用いる自由度1のスチューデントのt分布を指します。SNEは1本目の論文にあたるStochastic Neighbor Embeddingを受け継いだ部分です。
PCAとt-SNEの違いは何ですか?
PCAは分散が最大になる方向を線形に探す決定的な手法で、同じ入力からは常に同じ結果が出ます。t-SNEは近傍関係を保つように座標を最適化する非線形かつ確率的な手法で、実行ごとに図が変わります。判断の目安としては、軸の意味や寄与率を説明する必要があるならPCA、軸の説明が不要でかたまりの分離だけを見たいならt-SNEです。scikit-learnがt-SNEの前処理としてPCAを推奨していることからも、両者は競合ではなく併用する関係にあります。
t-SNEで4次元以上に圧縮できますか?
既定のmethod="barnes_hut"ではできません。四分木・八分木に依存する実装のため、n_componentsが4以上だとValueErrorになります。method="exact"に切り替えれば4次元以上も可能ですが、計算量がサンプル数の2乗に比例します。そもそも4次元は図として表示できないため、実用上の選択肢は2次元か3次元です。
t-SNEはクラスタリング手法ですか?
違います。t-SNEが返すのは低次元の座標だけで、クラスタ番号は返しません。グループ分けが必要ならk-meansや階層的クラスタリングを別途実行し、その結果を色として図に重ねます。実行する空間は、t-SNEで圧縮した後の3次元座標ではなく、元の高次元ベクトルまたはPCAで50次元程度に落としたものです。
perplexityはいくつに設定すればよいですか?
典型的な値は5から50の範囲です。件数別の目安としては、100件規模なら5から15、1000件を超えるなら30から50を起点にし、単一の値で決め打ちせず複数の図で共通して現れるかたまりだけを信頼してください。値がサンプル数以上だとValueErrorになるため、既定の30を使うには31件以上が必要です。