洋食の日記

「た・である」調ではなく「です・ます」調で書きはじめれば良かったなと後悔してる人のブログです

Rumaleにt-SNEを実装した

はじめに

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では不動点法による手法を採用した。

Z. Yang, I. King, Z. Xu, and E. Oja, “Heavy-Tailed Symmetric Stochastic Neighbor Embedding,” Proc. NIPS'09, pp. 2169–2177, 2009.

論文では、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データセットの各手書き数字が、種類別にある程度固まって可視化されている。

f:id:yoshoku:20190518094404p:plain

おわりに

可視化手法には、その他、多次元尺度構成法(Multi-dimensional scaling, MDS)やSammon nonlinear mappingなどがある。これらも順次、実装していきたい。t-SNEのように多様体学習ベースの次元削減手法という文脈では、Laplacian EigenmapsやLocally Linear Embeddingもある。これらは、k-近傍グラフの作成など、近傍探索が必要になる。データ数が大きくなると遅くなるので、近似最近傍探索の実装も考えないといけない。まだまだRumaleでできることあるな〜という感じ。

github.com