【Ruby】t-SNEでMNISTを次元圧縮して可視化してみた

## はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/7f9ad58b-6318-86eb-4a1b-9421ade4a35c.png)
Rumaleという、Rubyで書かれた高速な機械学習のライブラリがあります。

こちらに軽い気持ちでt-SNEをリクエストしたところ、あっという間に実装されていました。
責任を感じてQiitaに記事を書いています。

t-SNEは教師なし学習のアルゴリズムです。
この記事ではRumaleでMNISTを3D可視化してみます。こんな感じでMNISTの画像が分類できているのがわかります。
![rumale.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/cea6dac6-2378-3520-b898-1ca8dbc4534a.gif)

## Rumale とは
https://github.com/yoshoku/rumale
Rubyで機械学習ができると評判のライブラリ。
scikit-learn の教科書を片手にRubyで機械学習の勉強ができる。

リファレンス：
ターミナルで `yard server -gd` と打って、ブラウザで `http://localhost:8808` を開く


## t-SNE とはなにか？
t-SNEは高次元データの次元削減と可視化ができる人気のアルゴリズムです。
ではt-SNEはどんな仕組みで動いているのでしょうか？　ぜんぜんわかりませんっ！（こころの悲鳴）
なので申し訳ありませんがt-SNEの中味についてはほかの記事の解説をご参照ください（…こんなことでいいのだろうか）。

* [t-SNEによるイケてる次元圧縮&可視化](https://qiita.com/stfate/items/8988d01aad9596f9d586)
* [Qiitaに投稿されたt-SNEの記事一覧](https://qiita.com/search?q=t-sne)


## Red-datasets
[Red-datasets](https://github.com/red-data-tools/red-datasets)でMNISTを読み込んでいます。

* [RubyでMNISTやCIFAR-10を呼び出すRed Datasetsを使ってみる](https://qiita.com/kojix2/items/41fec8ec264c26e261bf#mnist--fashionmnist--cifar-10--cifar-100-%E3%82%92%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%99)

## コードを書く
t-SNEの実行速度は、OpenBLASを使用しているかどうかで圧倒的な差が出ますので今後Rumaleを真剣に使っていく場合はOpenBLASとnumo-linalgをインストールしてください。しかし、Rumaleを試すだけなら、OpenBLASは要りません。
t-SNEにかける前に、主成分分析で50〜80程度に次元を下げます。

```ruby
# require 'numo/linalg' # OpenBLAS
require 'rumale'
require 'datasets'
require 'numo/gnuplot'

# 省略してみる
N = Numo
R = Rumale

# 最初のn個のデータを使う
n = 2000
pixels = []
labels = []

# mnistからデータを用意する
mnist = Datasets::MNIST.new
mnist.take(n).map do |record|
  pixels << record.pixels
  labels << record.label
end

# Rubyの配列を Numo::NArrayの配列に変換する
samples = N::DFloat[*pixels]
labels  = N::Int32[*labels]

# 主成分分析
pca = R::Decomposition::PCA.new(
  n_components: 50
)
samples = pca.fit_transform(samples)

# t-SNE
tsne = R::Manifold::TSNE.new(
  n_components: 3, perplexity: 50.0, max_iter: 500, verbose: true
)
low_samples = tsne.fit_transform(samples)
```

t-SNEの `max_iter` や `perplexity` を変更すると、プロットの様子が変わります。
再現性を重視する場合は`random_seed`を設定することができます。今回は3Dなので、`n_components` に3を指定します。

続いて、numo-gnuplotを用いて3Dプロットをします。

```ruby
# Gnuplot用のデータを用意する
data_for_gnuplot = (0..9).map do |i|
  w = labels.eq(i).where
  x = low_samples[w, 0]
  y = low_samples[w, 1]
  z = low_samples[w, 2]
  [x, y, z, { t: i.to_s, pt: 7 }]
end

# Gnuplotで表示する
N.gnuplot do
  reset
  splot *data_for_gnuplot
  gets # 待ち
end
```

こんな感じのウィンドウが立ち上がれば成功です！
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/905b5182-8b6a-fc0c-5129-bdf2b553abc0.gif)
うおおお！！　なんとなく同じラベルの色が固まっていて、いい感じに分類されていることがわかります。

しかし、よくみると0と8, 1と9が同じ色になっていたりして、表示としてはイマイチです。なので色や透過性を指定して、出力をcairopngに変更して、連番で出力した画像をGIFアニメーションに変換したりといろいろ見た目を整えてパラメータも若干変えると冒頭の画像になります。（コードがゴチャゴチャして時間も割とかかるので省略。）

![rumale.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/cea6dac6-2378-3520-b898-1ca8dbc4534a.gif)

こんな感じで簡単に機械学習のアルゴリズムをいろいろ実行できます。とても便利！

## 参考資料
この記事はRumaleの公式ブログ：[洋食の日記 Rumaleにt-SNEを実装した](https://yoshoku.hatenablog.com/entry/2019/05/18/100204)を参照しながら書きました。公式資料の方が詳しいのでご参照ください。

この記事は以上です。