はじめに
Rumaleに高次元データの可視化として定番の一つになっている t-distributed Stochastic Neighbor Embedding(t-SNE)を実装した。「教師なし学習を増やさないとな〜少ないよな〜」と思っているところに、issueでリクエストを頂いたので実装してver. 0.10.0としてリリースした。
rumale | RubyGems.org | your community gem host
t-SNEが最初に提案された当時、Numpyで実装した経験があるが、勾配法により最適化するのもあって結構遅かった。t-SNEの高速化については、いくつか提案されているが、Rumaleでは不動点法による手法を採用した。
論文では、Symmetric SNE全般で使える手法として提案されている。t-SNEもSymmetric SNEの一種なので、これを応用できる。不動点法は、独立成分分析のFast ICAなどでも使用されており、一般に勾配法よりも速く収束する。また、不動点法は、勾配法と異なり、学習率やモーメンタム係数といったハイパーパラメータを必要としない。これらの利点から採用した。
使い方
Rumaleはgemコマンドでインストールできる。Numo::NArrayに依存している。
$ gem install rumale
データセットの読み込みでred-datasetsを使いたいので、これもインストールする。
$ gem install red-datasets-numo-narray
可視化したデータのplotにはNumo::Gnuplotを用いる。
$ brew install gnuplot $ gem install numo-gnuplot
また、t-SNEはアルゴリズム上行列計算を繰り返すことになるので、Numo::Linalgのインストールすることをお薦めする。Intel MKLやOpenBLASと組み合わせると、行列計算が並列化されるので、計算が速くなる。詳しくはコチラを。
$ gem install numo-linalg
MNISTデータセットの可視化を試みる。
require 'rumale' require 'numo/linalg/autoloader' require 'datasets-numo-narray' require 'numo/gnuplot' # Numo::NArray形式でMNISTデータセットを読み込む. dataset = Datasets::LIBSVM.new('mnist').to_narray labels = Numo::Int32.cast(dataset[true, 0]) samples = Numo::DFloat.cast(dataset[true, 1..-1]) # データ数が多いと遅いので適当にサブサンプリングした. rand_id = Array.new(samples.shape[0]) { |n| n }.sample(3000) labels = labels[rand_id] samples = samples[rand_id, true] # データを[-1,1]の範囲に正規化する. normalizer = Rumale::Preprocessing::MinMaxScaler.new(feature_range: [-1.0, 1.0]) samples = normalizer.fit_transform(samples) # 元データの次元数が大きいと, 高次元空間の確率を計算する部分で計算時間を要する. # MNISTの特徴量はほとんど背景部分の0なので, 主成分分析で1/10程度に削減する. pca = Rumale::Decomposition::PCA.new(n_components: 80, random_seed: 1) samples = pca.fit_transform(samples) # t-SNEで2次元空間にマッピングすることで可視化する. # verboseでtrueを指定すると, 最適化過程の数値を出力する. tsne = Rumale::Manifold::TSNE.new( perplexity: 30.0, max_iter: 1000, verbose: true, random_seed: 1) low_samples = tsne.fit_transform(samples) # Numo::GnuplotでPNGファイルに書き出す. x = low_samples[true, 0] y = low_samples[true, 1] plots = labels.to_a.uniq.sort.map { |l| [x[labels.eq(l)], y[labels.eq(l)], t: l.to_s] } Numo.gnuplot do set(terminal: 'png') set(output: 'tsne.png') unset(:xtics) unset(:ytics) unset(:border) plot(*plots) end
これを実行すると以下のようになる。
$ ruby rumale_tsne.py [t-SNE] Computed conditional probabilities for sample 1000 / 3000 [t-SNE] Computed conditional probabilities for sample 2000 / 3000 [t-SNE] Computed conditional probabilities for sample 3000 / 3000 [t-SNE] Mean sigma: 4.30353336663795 [t-SNE] KL divergence after 100 iterations: 2.2765667341716527 [t-SNE] KL divergence after 200 iterations: 1.9882779434413718 [t-SNE] KL divergence after 300 iterations: 1.856052564481442 [t-SNE] KL divergence after 400 iterations: 1.7744255471415573 [t-SNE] KL divergence after 500 iterations: 1.7163771284160634 [t-SNE] KL divergence after 600 iterations: 1.6715272565792092 [t-SNE] KL divergence after 700 iterations: 1.6372943878281605 [t-SNE] KL divergence after 800 iterations: 1.6084045578469424 [t-SNE] KL divergence after 900 iterations: 1.5847659188134602 [t-SNE] KL divergence after 1000 iterations: 1.5633048653537873
そして、出力された結果は次のようになる。色のバリエーションがアレだったりして分かりにくいが、MNISTデータセットの各手書き数字が、種類別にある程度固まって可視化されている。
おわりに
可視化手法には、その他、多次元尺度構成法(Multi-dimensional scaling, MDS)やSammon nonlinear mappingなどがある。これらも順次、実装していきたい。t-SNEのように多様体学習ベースの次元削減手法という文脈では、Laplacian EigenmapsやLocally Linear Embeddingもある。これらは、k-近傍グラフの作成など、近傍探索が必要になる。データ数が大きくなると遅くなるので、近似最近傍探索の実装も考えないといけない。まだまだRumaleでできることあるな〜という感じ。