研究ノート · 次元削減

MADR から ScaleRank へ

2020年に始めたマルチエージェント型の次元削減(MADR)を見直し、比較の物差しを作り直したうえで、「守るスケールを指定できる順位保存」という新しい手法 ScaleRank を試作しました。最初の版で見つかった外れ点の問題を調べて損失関数をソフトマックス型に作り直し、N² を使わない版で 7 万点まで広げ、Rust と WebAssembly でブラウザアプリにするまでの経緯と結果を、図で説明します。

2026年10月27日時点 実験: github.com/tsjshg/madr(ブランチ proto/scalerank) アプリ: tsjshg.github.io/scalerank-web English

要約

何をしたか

過去の試みを読み直し、8データ × 8手法を同じ指標で比べるベンチマーク基盤 drbench を作った。新手法を試作し、7 万点まで動く版とブラウザアプリにした。

何が分かったか

既存手法には「局所に強い」か「大域に強い」かのトレードオフがあり、全スケールで勝つ手法はない。

新手法

ScaleRank は守りたいスケールを γ で指定する。現行のソフトマックス型(γ=1)は、8データすべてで局所と大域の両方で既存手法に負けない点を作った。パラメータを調整した t-SNE・UMAP(30 設定)も、両方で上回ることはなかった。

残る課題

局所だけなら t-SNE がまだ上で、7 万点ではその差が広がる。外れ点が多い。個々の要素の多くは先行研究にあり、多重スケール t-SNE や DREAMS との比較が必要。

背景

これまでの経緯

t-SNE や UMAP に代わる次元削減を作るため、2020年から「各データ点を1体のエージェントとみなし、局所的なルールで動かして配置を決める」という方向で実験してきました。フォルダには約40本のノートブックが残っていました。

2020年前半
粒子群(PSO)風

各点が速度を持ち、似た点に引かれ、似ていない点を避ける。

2020年夏〜秋
セルオートマトン型 MADR2d → 連続空間版

格子上で「近傍の一致度」を満足度として1マスずつ動く。その後、連続空間で「本来の近傍に寄る(harmonic)」「近傍でない点から逃げる(escape)」の2ルールに。

2021年初
局所密度で移動半径を変える(AROB 発表)

メモに「各インスタンスが何らかの目的関数を最適化するのがよいのでは」とある。ルールから目的関数への転換点。

2022年
co-ranking Q スコアを直接最適化

順位のずれを測る Q スコアを Rust で実装し、ランダムな30点ずつの移動を差分進化(DE)で最適化。

2026年10月26日
再始動: drbench と ScaleRank 初期版

物差しを先に作り、その上で三つ組の損失を使う ScaleRank を試作。外れ点の原因を調査。

2026年10月27日
ソフトマックス型へ

1つの近傍に対して非近傍を20個まとめて比べる損失に切り替え、既定値にした。

2026年10月27日
7 万点とブラウザへ

N² の順位表をやめた版を作り、Rust で書き直して、データをブラウザの外に出さないアプリとして公開した。

過去の評価

2022年の結果をどう読むか

2022年の最終結果では、MADR は Q スコアで他手法に大差をつけていました。ところが同じノートブックで、近い順位(k=5)だけに絞った局所スコアを見ると順位がほぼ逆転します。

全体の Q スコア(低いほど良い)
Pollen データ。全点対の順位のずれの合計
k=5 の局所スコア(低いほど良い)
近い 5 位までの順位だけを見た場合
図1. 2022年の 結果まとめ.ipynb の数値。MADR は全体では1位だが、局所では t-SNE・UMAP に大きく負ける。

理由は Q スコアの定義にあります。Q スコアは「各点から見た他の全点の順位」が元の空間と埋め込みでどれだけずれたかの合計です。遠い順位のずれも近い順位のずれと同じ重さで数えるので、実質的には大域構造の指標になります。その指標を直接最適化すれば、その指標で勝つのは当然です。

教訓: 評価に使う指標を最適化対象にすると、その指標では必ず良く見える。新しい手法を作る前に、どの指標で何を測るかを固定しておく必要がある。

ベンチマーク

物差しを作る: drbench

そこで新手法より先に、同じ条件で手法を比べる基盤 drbench を作りました。データセット・手法・指標をそれぞれ登録制にして、組み合わせを自動で回し、結果をキャッシュします。距離はすべてユークリッド距離で、既存手法も既定値のまま呼んでいます。

datasets
8 データ

合成3種(S字曲面、階層クラスタ、密度の違うクラスタ)、digits、scRNA-seq 4種

methods
8 手法 + 新手法

PCA, MDS, t-SNE, UMAP, densMAP, PaCMAP, TriMap, ランダム。関数を1つ登録すれば追加できる

metrics
局所・大域・密度・ラベル

N×N 行列を作らず行ごとに集計。sklearn や旧実装と一致することをテストで確認

report
図と表

RNX(K) 曲線、散布図、トレードオフ図、要約表

中心になる指標 RNX(K)

各点について「元の空間での K 近傍」と「2次元での K 近傍」がどれだけ重なるかを測り、ランダム配置なら 0、完全に一致すれば 1 になるよう補正したものです。K を 1 から N まで動かすと、局所(小さい K)から大域(大きい K)までの保存度を1本の曲線で見られます。

図2. 点 i の K=5 近傍。元の空間(左)での 5 点のうち 3 点が、2次元(右)でも 5 近傍に入っている。この一致率を全点で平均し、K ごとに曲線にしたものが RNX(K)。
指標種類何を見ているか
R_NX@10局所K=10 での近傍保存(ランダム=0、完全=1)
AUC_RNX局所寄り全 K の RNX を 1/K で重み付けした面積
trust@10 / cont@10局所偽の近傍が混ざっていないか / 本当の近傍を失っていないか
距離相関大域全点対の距離の順位相関(Spearman)
密度相関密度各点の 10-NN 半径(局所密度)が保たれているか
kNN 精度ラベル2次元での 10-NN 分類精度。クラスの分かれ具合
外れ点の割合見た目2次元での 10-NN 半径が中央値の 3 倍を超える点の割合

途中で見つけた不具合. PaCMAP と TriMap が近傍探索に使うライブラリ annoy が、この Mac では壊れていました。5 近傍を頼むと 1 つしか返さず、TriMap はエラーを出さずにほぼランダムな結果を返していました(digits の kNN 精度 23%)。PaCMAP は faiss に切り替え、TriMap には正確な近傍を渡すよう直し、再発を検知するテストを追加しています。

既存手法

既存手法の成績

RNX(K) 曲線を見ると、既存手法の得意なスケールがはっきり分かれます。t-SNE は小さい K で最も高く、K が 50 を超えたあたりで MDS や PCA に逆転されます。

データ:
図3. RNX(K)。横軸は対数で、右に行くほど大域的。グラフにカーソルを合わせると各手法の値が出ます。

8 データ全体で見ても、局所指標と大域指標で1位の手法が入れ替わります。下の表で指標を切り替えると、局所では t-SNE、大域では MDS / PCA が強いことが分かります。

指標:
図4. 3 seed の平均。各行で色が濃いほど良く、下線が行の最高値。右端の3列が ScaleRank(現行の γ=1 と γ=0、初期版の γ=1)。
新手法

ScaleRank の考え方

既存手法は「どのスケールを守るか」を内部に暗黙に持っています。ScaleRank はそれをユーザーが指定する分布 p(K) として外に出し、その分布で重み付けした近傍保存を直接最適化します。

1つの三つ組で考える

点 i と近傍サイズ K を決めると、元の空間で i の上位 K 近傍に入る点 j と、入らない点 m があります。2次元で j が m より i の近くにあれば正解です。K を p(K) から引き、j と m を引き、間違っていれば i と j を近づけ、i と m を遠ざけます。初期版はこの三つ組を損失の単位にしていました。現行版では m を一度に20個引きます(後述)。

図5. 三つ組 (i, j, m)。元の空間での上位 K 近傍(点線の円)に入る j を i に引き寄せ、入らない m を遠ざける。どの K で引くかを p(K) が決める。

ノブは γ

p(K) を K−γ に比例させると、γ=1 では小さい K に重みが集まり(局所寄り、AUC_RNX と同じ重み)、γ=0 では全 K が同じ重み(大域寄り)になります。スライダーで γ を動かすと、どのスケールに力を割くかが変わります。

図6. digits(N=1797)で、各範囲の K に割り当てられる重みの割合。γ=1 と 1.5 の間で K≤10 の割合が 36% から 78% に跳ねる。棒にカーソルを合わせると値が出ます。
初期版

初期版でつまずいたこと

最適化の失敗ではなく、代理指標の偏り

最初の版は γ=1 でも局所指標が t-SNE に届きませんでした(digits の RNX@10 が 0.44 対 0.58)。そこで、既存手法の埋め込みを ScaleRank 自身の目的関数で採点しました。もし t-SNE の方が低い値なら最適化がうまくいっていないことになります。

ScaleRank の目的関数で採点(γ=1、digits、低いほど良い)
図7. 最初の版の ScaleRank が最も低い。つまり最適化は成功していて、この目的関数自体が t-SNE とは違う配置を好んでいる。

原因は目的関数の作りでした。三つ組の正誤は「遠くの非近傍が少し順位を間違える」ことも同じ重さで数えますが、RNX(K) が気にするのは2次元の上位 K に入り込んでくる点だけです。そこで一部の三つ組で、m を「いま2次元で i の近くにいるが、本当は近傍でない点」から選ぶようにしました(hard negative)。

RNX@10(digits)
AUC_RNX(digits)
図8. hard negative の導入で、局所指標が大きく改善した。

もう1つのバグ. 導入直後、hard negative の割合を 0.3 / 0.6 / 0.9 と変えても結果が完全に同じでした。調べると、順位表を書き込む処理が NumPy のコピーに書き込んでいて、順位表が全部 0 のままでした。hard negative は一度も使われていなかったことになります。修正後に上の改善が出ました。

調査 1

初期版の外れ点の正体

初期版の散布図では、クラスタのまわりに点が散っていました。2次元での 10-NN 半径が中央値の 3 倍を超える点を「外れ点」として数えると、t-SNE よりはるかに多く出ていました。

初期版 γ=1 初期版 + 緩和策(局所スケーリング) 現行版 γ=1 t-SNE MDS
図9. 外れ点の割合(3 seed 平均)。t-SNE はどのデータでも 1% 未満。MDS(距離をそのまま保つ手法)でも外れ点が出るデータは、本当に孤立した点を含んでいると考えられる。

2 種類の外れ点

  • Pollen の外れ点は元の空間でも孤立していました(10-NN 半径が中央値の 3.9 倍)。MDS でも 16% 出ます。これはある程度忠実です。
  • 階層クラスタ(hier_blobs)の外れ点は元の空間では普通の密度でした(1.06 倍)。手法が作った人工物です。

犯人は hard negative と「anti-hub」

hard negative を切ると、hier_blobs の外れ点は 23% から 2% に減りました。外れる点には共通点があり、他のどの点の近傍リストにもほとんど入らない点、いわゆる anti-hub でした。高次元のデータでは、一部の点が多くの点の近傍になり(hub)、逆にほとんど誰の近傍にもならない点(anti-hub)が大量に生まれます。これを hubness と呼びます。

図10. 矢印は「この点の最近傍はあの点」という関係。中央の hub は多くの点から指されるが、右端の anti-hub はどこからも指されない。近傍関係は対称ではない。
表示:
図11. hier_blobs(3 つの大クラスタ × 4 つの小クラスタ、50 次元)。色は大クラスタ。「anti-hub を強調」にすると、元の空間で 10-NN に 2 回以下しか現れない点を黒い輪で示す。初期版(左端)では anti-hub がクラスタの外に散っているが、現行版(3番目)ではほぼ収まっている。
anti-hub が外れ点になる確率(初期版)
hard negative あり(0.3) hard negative なし
図12. 各データの anti-hub(N10 ≤ 2)のうち外れ点になった割合。括弧内は hubness の強さ(N10 の歪度)。hubness が強いデータほど、hard negative の影響が大きい。Pollen は hard negative を切っても変わらない(本当に孤立した細胞のため)。

どんな力が働いているか

学習中に各点が受ける力を、勾配の項ごとに分解し、「自分のクラスタの中心から外向き」の成分を測りました(初期版、hier_blobs、エポック 700)。

最終的に外れ点になる点 それ以外の点
図13. 正の値は外向き、負の値は内向き。外れ点は「誰かの近傍として引かれる力」が約 1/3 しかなく、自分が anchor のときの hard negative による押し出しが約 2 倍。

外れ点への正味の力は内向きですが、他の点より弱い(−0.013 対 −0.038)。学習の後半にクラスタの芯が縮んでいく(中心からの距離 3.5 → 0.6)中で、押し出されるというより取り残されています。近傍関係が非対称なことが根本原因で、t-SNE は点ごとの帯域幅(perplexity の較正)でこれを打ち消しています。

試した対策

対策結果
最適化手法の変更(点ごと Adam / 共有 Adam / SGD)変化なし
hard positive(遠くにいる本当の近傍を優先して引く)どのデータでも悪化
hard negative を順位で重み付け(trustworthiness と同じ形)変化なし
片側 hard negative(入り込んだ点 m は動かさない)わずかに減るだけ(23.8% → 20.4%)
hard negative を学習中に減衰させるblob では減るが、digits の改善を失う
全 K で局所スケーリングした順位を使う外れ点は消えるが、Pollen の距離相関が 0.86 → −0.02 に崩れる
小さい K(≤30)だけ局所スケーリング外れ点 16% → 10〜11%。緩和にはなるが解決ではない(オプションとして残した)
損失をソフトマックス型に変え、hard negative をやめる外れ点 16% → 9%、局所も平均で改善。既定値にした(次々章)
調査 2

blob で t-SNE に負ける理由

初期版で局所指標の差が大きかったのは、高次元ガウス分布でできた2つの合成データです(hier_blobs で 0.24 対 0.34、density_blobs で 0.31 対 0.41)。目的関数を K≈10 に集中させた版(帯域プロファイル)で、t-SNE と同じ土俵にそろえて調べました。

データ手法RNX@10目的関数(低いほど良い)
hier_blobst-SNE0.3440.0364
ScaleRank(hard negative あり)0.2330.0425
ScaleRank(hard なし、予算 4 倍)0.3330.0332
density_blobst-SNE0.4080.1019
ScaleRank(hard なし、予算 4 倍)0.3270.0839
表1. 目的関数は K=10 前後に集中させた帯域プロファイル(k0=10)。
  • hier_blobs: hard negative があると、ScaleRank は自分の目的関数で t-SNE に負けます。hard negative が目的関数そのものを歪めているということです。外すと t-SNE にほぼ並び、目的関数でも上回りました。
  • density_blobs: 目的関数では t-SNE を上回っているのに、RNX@10 では負けています。目的関数と評価指標のずれがまだ残っています。
  • スケールの配分: γ=1 は設計上、K>10 にも多くの重みを割きます(図6)。digits では K=10 に集中させると RNX@10 が 0.599 になり、t-SNE(0.583)を上回りました。

hard negative のジレンマ. digits のような多様体的なデータでは必須(RNX@10 が 0.44 → 0.54)ですが、hubness の強い高次元データでは外れ点を作り、目的関数を歪めます。固定の「確率 0.3・上位 2K 以内」という決め方が、データによって効いたり害になったりしていました。

現行版

ソフトマックス型への切り替えNEW

hard negative をデータに合わせて調整するのではなく、損失関数の形で同じ効果を出せないかを考えました。1つの近傍 j に対して非近傍 m を Q 個まとめて引き、次の損失を使います(対照学習の InfoNCE と同じ形)。

損失 = −log( qij / ( qij + Σm qim ) )  q = 1 / (1 + d²)
  • Q=1 のときは初期版の三つ組と同じ式です(テストで一致を確認)。
  • Q を増やすと、いま2次元で i の近くにいる m ほど qim が大きく、押し出す力も大きくなります。m は一様に引くだけなのに、近くに入り込んでいる非近傍が自動的に強く効く、hard negative の滑らかな版になります。
  • Q を大きくするほど t-SNE に近づくことも知られています(Damrich ら 2023)。Q は γ と並ぶ、もう1つの「局所か大域か」のノブになります。

Q を振ってみる

調整に使ってきた4データで、hard negative なしの状態から Q を 1 → 5 → 20 → 100 と増やしました。

局所 RNX@10(高いほど良い)
大域 距離相関(高いほど良い)
図14. γ=1、hard negative なし、2 seed 平均。Q=1 は三つ組、Q=5・20 は1点あたり32グループ、Q=100 は4グループ。左端の × は初期版(三つ組 + hard negative)の値。局所は Q とともにほぼ単調に上がり、大域は少し下がる。

Q=20 で1点あたり8グループにすると、32グループとほぼ同じ質を 1/4 の計算で出せました。これを現行の既定値(n_neg=20, per_point=8, hard=0)にしました。8 データ × 3 seed での比較が次の表です。

手法外れ点局所 RNX@10AUC_RNX大域 距離相関密度相関時間
表2. 8 データ × 3 seed の平均(drbench の結果から計算)。外れ点は初期版の半分になり、局所も平均で上がった。大域は少し下がるが、t-SNE よりははるかに上。

低次元の多様体では局所が下がる

すべてのデータで良くなったわけではありません。s_curve(2次元の曲面を3次元に埋め込んだもの)では局所が 0.830 から 0.797 に下がりました。一様乱数の2次元正方形を復元させる単純なテストでも、同じ傾向がはっきり出ます。

2次元の一様な正方形(300点)の復元、RNX@10
図15. 2 seed 平均、ランダム初期化。現行版は学習を長くしても PCA 初期化にしても 0.80 前後で頭打ちになる。ソフトマックス型は hubness の強い高次元データには効くが、内在次元の低いデータでは少し損をする。
結果

現行版の結果NEW

現行版で γ = 1 / 0.5 / 0 を 8 データ × 3 seed で走らせ、既存7手法と比べました。縦軸に局所(RNX@10)、横軸に大域(距離相関)をとると、γ=1 と 0.5 は8データすべてで、両方の軸で上回る既存手法がない位置にあります。γ=0 は2つのデータで既存手法に負けました(density_blobs で PaCMAP、s_curve で TriMap)。

ScaleRank:
図16. 右上ほど良い。赤い実線は現行版で γ を 1 → 0.5 → 0 と動かしたときの軌跡、赤い破線(白抜き)は初期版。点にカーソルを合わせると値が出ます。

γ を下げたときの振る舞い(新しい実験)

8 データの平均で見ると、現行版は軌跡全体が局所寄りに移動しています。γ=1 では初期版より局所が高い一方、γ=0 まで下げても大域は初期版ほど上がりません(距離相関 0.863 対 0.906)。負例を多くすると t-SNE に近づく性質が、γ を下げても残るためと考えられます。また、どちらの版でも γ を下げるほど外れ点が増えます。

8 データ平均のトレードオフ
γ と外れ点の割合(8 データ平均)
現行版(ソフトマックス) 初期版(三つ組 + hard) t-SNE MDS PaCMAP
図17. 左: 局所と大域の平均。現行版の軌跡は初期版より左上(局所寄り)にある。右: γ を下げると、どちらの版でも外れ点が増える。
データ:
図18. RNX(K) 曲線で見た ScaleRank。γ=1 は小さい K で t-SNE に近く、大きい K では t-SNE を大きく上回る。γ=0 は MDS に近い形になる。
Pollen での比較
手法局所 RNX@10大域 距離相関AUC_RNX
表3. ScaleRank γ=1 は局所で t-SNE に近く、大域では MDS 並み。

散布図で見ると、t-SNE はクラスタをきれいに分けますがクラスタ同士の位置関係は保ちません。MDS は位置関係を保ちますがクラスタが重なります。ScaleRank はその中間で、クラスタを分けつつ配置も保とうとします。初期版で目立ったクラスタまわりの散らばりは、現行版では少なくなっています。

図19. digits(seed 0)。色は数字のクラスで、8 と 9 は三角形。点にカーソルを合わせるとクラスが出ます。
調査 3

調整した t-SNE・UMAP との比較NEW

既定値の t-SNE や UMAP に負けなくても、パラメータを調整すれば ScaleRank の位置まで届くかもしれません。そこで t-SNE は perplexity {5, 30, 100, 300} × exaggeration {1, 2, 4} とランダム初期化の 13 設定、UMAP は n_neighbors {5, 15, 50, 200} × min_dist {0, 0.1, 0.5, 0.99} とランダム初期化の 17 設定を、8 データ × 3 seed で走らせました。

t-SNE(13 設定) UMAP(17 設定) ScaleRank(γ=1, 0.5, 0)
図20. 右上ほど良い。小さな点が調整した t-SNE と UMAP の各設定、赤い線が ScaleRank で γ を 1 → 0.5 → 0 と動かしたときの軌跡。点にカーソルを合わせると設定名が出ます。
  • どのデータでも、30 設定のどれも、局所と大域の両方で ScaleRank を上回りませんでした。データごとに最良の設定を選んでも同じです。
  • t-SNE は perplexity を上げると大域が上がる(8 データ平均 0.52 → 0.71)代わりに局所が下がります(0.543 → 0.415)。exaggeration を上げると局所が大きく下がり、外れ点も増えます。
  • UMAP は n_neighbors を上げても大域が 0.43 前後で頭打ちになり、min_dist はほとんど効きません。
  • 差が小さいのは s_curve と density_blobs で、調整した t-SNE が ScaleRank の近くまで来ます。

割り引いて読む点: 大域の指標(距離の順位相関)は、ScaleRank が小さい γ で最適化している「各点から見た全点の順位」に近い量です。t-SNE と UMAP はもともと距離を保つ設計ではないので、大域での差の一部は設計目標の違いによります。局所と大域の両立を狙った手法(次の節)とはまだ比べていません。

文献

先行研究との位置づけNEW

ScaleRank のどこまでが新しいかを確かめるため、関連する研究を調べました。結論から言うと、個々の要素の多くはすでに知られています。

ScaleRank の要素先行研究
局所と大域を 1 つのノブで動かすNeRV の λ(Venna ら 2010)、Local MDS の τ(Chen & Buja 2009)、t-SNE の exaggeration(Böhm ら 2022)、対比学習での負例の数(Damrich ら 2023)、DREAMS の λ(Kury ら 2026)
すべてのスケールを log K で一様に守る(γ=1 に相当)多重スケール SNE(Lee ら 2015)、多重スケール t-SNE(de Bodt ら 2018, 2020)
距離ではなく順位を守る非計量 MDS(Kruskal 1964)、RankVisu(Lespinats ら 2009)、順位保存による次元削減(Onclinx ら 2010)
三つ組の損失、InfoNCE と Cauchy 核t-STE(van der Maaten & Weinberger 2012)、TriMap(Amid & Warmuth 2019)、Damrich ら 2023
評価指標 RNX とその対数スケールの AUCLee & Verleysen 2009、Lee ら 2015
局所(近傍の再現率)と大域(距離の順位相関)の 2 軸で比べるDREAMS(Kury, Kobak, Damrich; TMLR 2026)
表4. ScaleRank の要素と、それを先に扱った研究。

新しい可能性があるのは次の組み合わせです。利用者が任意のスケール分布 p(K) を与え、それで重み付けした近傍の保存を目的関数とし、そこからサンプリングの規則(K を p から引き、正例を上位 K から、負例をその外から引く)と三つ組の重みを導いていること。この組み合わせをそのまま述べた文献は見つかりませんでした。

ただし、多重スケール t-SNE のスケールの重みを p(K) に合わせて変えるのは素直な拡張です。それとの違いを実験で示せない限り、新しさは弱いと考えています。次に比べるべき相手は、p(K) で重みを変えた多重スケール t-SNE、DREAMS の λ の掃引、対比学習での負例の数の掃引です。

規模

N² を使わない版と 7 万点NEW

ここまでの ScaleRank は、各点から見た全点の順位表(N × N)を持っていたので、1 万点程度が限界でした。そこで順位表をやめ、O(N log N) の大きさの構造だけで同じ目的関数を最適化する版を作りました。

  • 1〜100 位は、正確な k 近傍で求める。
  • それより大きい順位は、何段階かのランダムな部分標本から推定する。大きさ n の部分標本で a 番目に近い点は、全体ではおよそ a·N/(n+1) 位にあたる。これで各点に「距離 → 順位」の表を作る。
  • 負例は、エポックごとに一様に引き直した候補を表で順位付けし、順位が K を超えるものから選ぶ。

8 データ × 3 seed で元の版と比べると、差はどの指標でもほぼ ±0.01 に収まりました(表5)。評価も、1 万点を超えるデータでは 2000 行だけを使って推定するようにしました。各指標は行ごとの値の平均なので、この推定に偏りはありません。

γ局所 RNX@10AUC_RNX大域 距離相関密度相関kNN 精度
表5. N² を使わない版 − 元の版(8 データ × 3 seed の平均)。0 に近いほど元の版と同じ。

MNIST と Fashion-MNIST の全 7 万点(PCA で 50 次元)で試した結果が表6と図21です。

データ:
手法局所 RNX@10AUC_RNX大域 距離相関密度相関kNN 精度時間
表6. 7 万点(seed 0、10 スレッド)。t-SNE は openTSNE、UMAP は umap-learn。ScaleRank は Rust 版の値で、Python 版とほぼ同じ。
図21. MNIST 7 万点のうちランダムな 5000 点。色は数字のクラスで、8 と 9 は三角形。
  • 大域(距離相関)は、ScaleRank が t-SNE や UMAP よりはっきり高くなりました。
  • 一方、局所(RNX@10)は N が大きくなると弱くなります。MNIST の γ=1 では 1 万点で 0.27 だったのが 7 万点で 0.10 になり、t-SNE(0.39)の約 4 分の 1、UMAP と同じくらいです。

図21 では、ScaleRank γ=1(neg_scale 0.5)は数字ごとのまとまりを保つものの、各クラスが小さな塊に分かれ、周りに散らばる点も多くなっています。γ=0 は全体の配置を保つ代わりに、クラスが重なります。

局所が N とともに弱くなる理由

負例の数 Q を固定したまま N を大きくすると、Q 個の負例が代表する「それ以外のすべての点」が増え、反発が相対的に弱まります。対比学習で知られている性質で(Damrich ら 2023)、負例を少なく引く UMAP に近い振る舞いになります。

そこで、負例の項に重み w = ((N−1−K)/Q)a をかける設定(neg_scale = a)を入れました。a = 0.5 で局所はほぼ 2 倍(MNIST 0.10 → 0.20、Fashion-MNIST 0.12 → 0.20)になりますが、t-SNE にはまだ届きません。小さいデータでは密度相関が下がる(digits で 0.66 → 0.48)ので、既定値にはしていません。

MNIST の 1 万点では、ほかの設定も試しました。γ を上げると局所は最大 0.36(γ=2)、負例を 100 個にすると 0.35、neg_scale 0.5 で 0.36 になりました(既定の γ=1 は 0.28、t-SNE は 0.45)。1 点あたりの組の数を 4 倍にしたり、エポックを 800 から 2000 に増やしたりしても、ほとんど変わりませんでした(0.29)。

実装

Rust 版とブラウザアプリNEW

データをブラウザの外に出さずに使えるように、ScaleRank を Rust で書き直し、WebAssembly でブラウザ上で動くアプリにしました。比較用に t-SNE と UMAP も同じ Rust のコードに入れ、利用者が指示したときだけ計算します。近似 k 近傍(NN-descent)も実装しましたが、50 次元・7 万点では総当たり(10 スレッドで 4.3 秒)より速くならなかったので、今は総当たりを使っています。

Rust 版が参照実装と同じものを計算していることを、drbench で確かめました(表7)。t-SNE は openTSNE、UMAP は umap-learn(PCA 初期化)、ScaleRank は Python 版と比べています。

Rust 版比較相手局所 RNX@10AUC_RNX大域 距離相関密度相関kNN 精度時間(Rust / 相手)
表7. Rust 版 − 比較相手(8 データ × 3 seed の平均)。0 に近いほど同じ結果。

7 万点でも同じで、t-SNE の RNX@10 は 0.386(openTSNE 0.391)、UMAP は 0.108(umap-learn 0.109)でした。10 スレッドでの計算時間は、t-SNE が 28 秒(openTSNE 64 秒)、UMAP が 22 秒(35 秒)、ScaleRank が 44 秒(Python 版 1347 秒)です。

ブラウザ版は今のところ単一スレッドで、6000 点なら ScaleRank 17 秒、UMAP 11 秒、t-SNE 7 秒ほどです。2 万点では ScaleRank に約 1 分かかるので、マルチスレッド化で短くする予定です。

まとめ

割り引いて読むべき点と次の一手

割り引いて読むべき点

  • 局所指標だけなら、t-SNE が全データで1位のまま(平均 0.543 対 現行版 0.511)。
  • 現行版は低次元の多様体(s_curve、2次元の正方形)で局所が初期版より下がる。
  • γ を下げて大域寄りにすると、外れ点が増え、初期版ほど大域に届かない。
  • 外れ点はまだ t-SNE(0.3%)よりずっと多い(8.8%)。一部は本当に孤立した点だが、全部ではない。
  • AUC_RNX は γ=1 の目的関数に近いものを測っているので、そこでの優位は割り引く必要がある。RNX@10 と距離相関は直接は最適化していない。
  • ハイパーパラメータは digits、Pollen、hier_blobs、density_blobs で調整した。調整に使っていない4データでも傾向は同じだった。
  • 7 万点では局所が t-SNE の約 4 分の 1 まで下がる。neg_scale で約 2 倍になるが、まだ届かない。
  • 個々の要素の多くは先行研究にある。p(K) で重みを変えた多重スケール t-SNE や DREAMS とはまだ比べていない。
  • ブラウザ版は単一スレッドで、2 万点の ScaleRank に約 1 分かかる。

次の一手の候補

  • 2次元での順位を直接推定する. 負例を Q 個引けば「2次元で j より近い非近傍の割合」から j の順位を推定できる。それが K を超えたときだけ罰すれば、RNX(K) をほぼそのまま最適化できる。
  • Q を K に合わせて変える. 大域寄り(大きい K)では Q を減らし、局所寄りでは増やす。γ を下げたときに大域へ届かない問題に効く可能性がある。
  • 正例側の非対称性を直す. t-SNE のように点ごとの帯域で「近傍として引かれる量」をならし、anti-hub にも十分な引力を与える。
  • 大きい N で局所を保つ. neg_scale の既定値を N に合わせて決める。t-SNE の early exaggeration にあたる段階を試す。
  • 先行研究と直接比べる. p(K) で重みを変えた多重スケール t-SNE、DREAMS の λ の掃引、対比学習での負例の数の掃引を drbench に入れる。
  • ブラウザで速くする. マルチスレッド化と SIMD で 10 万点を目指す。t-SNE には FFT 版を入れる。
付録

再現方法

リポジトリ tsjshg/madr の proto/scalerank ブランチで、次のコマンドで再現できます。元データの pickle は親フォルダにある前提です。

uv sync
uv run python scripts/convert_legacy.py      # 2020年の pickle → data/raw/*.npz
uv run drbench run                           # 既存手法(8データ × 3 seed)

# 現行版(ソフトマックス型、既定値)
uv run drbench run -m scalerank:gamma=1.0 scalerank:gamma=0.5 scalerank:gamma=0.0
# 初期版(三つ組 + hard negative)
uv run drbench run -m "scalerank:gamma=1.0,hard=0.3,n_neg=1,per_point=32"
uv run drbench report                        # results/report/ に図と表

# 調査用スクリプト
uv run python scripts/sweep_scalerank.py -d digits --grid n_neg=1,5,20 hard=0.0
uv run python scripts/score_under_objective.py digits --gammas 1
uv run python scripts/diagnose_outliers.py hier_blobs -m tsne "scalerank:gamma=1.0"
uv run python scripts/force_decomposition.py hier_blobs --epochs 700
uv run python scripts/plot_scale_profile.py

# 調整した t-SNE / UMAP との比較(30 設定 × 8 データ × 3 seed の drbench run の後)
uv run python scripts/compare_tuned_baselines.py

# N² を使わない版、Rust 版(scalerank-web の Python バインディングを入れた後)
uv run drbench run -m scalerank_fast:gamma=1.0 scalerank_fast:gamma=0.5 scalerank_fast:gamma=0.0
uv run drbench run -m scalerank_rs:gamma=1.0 tsne_rs umap_rs umap:init=pca
uv run python scripts/compare_fast.py
uv run python scripts/compare_rs.py
uv run drbench run -d mnist70k fmnist70k -s 0 -m tsne umap scalerank_rs:gamma=1.0 scalerank_rs:gamma=0.0 "scalerank_rs:gamma=1.0,neg_scale=0.5"

Rust 版とアプリのコードは tsjshg/scalerank-web にあります。