RubyでUMAPをできるgemを作った話


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/2b5c78d4-6f36-c5f5-82b1-73edefa8d169.png" width="50%" height="50%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/6576e6ea-5765-4193-44a9-1bc0adb8aafc.gif" width="50%" height="50%"> 
[![Gem Version](https://badge.fury.io/rb/umappp.svg)](https://badge.fury.io/rb/umappp)

## はじめに

Uniform manifold approximation and projection ([UMAP](https://github.com/lmcinnes/umap)) はt-SNEとならんでよく使われる次元圧縮による可視化手法です。

Ruby言語で、機械学習を実行する場合、[Rumale](https://github.com/yoshoku/rumale)というgemを使うケースが多いと思います。Rumaleには[t-SNE](https://yoshoku.github.io/rumale/doc/Rumale/Manifold/TSNE.html)が搭載されていますが、UMAPは搭載されていません。

今回、C++のライブラリである[Umappp](https://github.com/LTLA/umappp)の[Rubyバインディング](https://github.com/kojix2/ruby-umappp)を作成したので忘れないうちに記録します。

https://github.com/kojix2/ruby-umappp

## Rubyのライブラリがないときはバインディングを作ろう

Ruby言語は、データ解析の分野では比較的マイナーな言語なので、やりたいことを実装したライブラリーが存在しないことがよくあります。そんな時はC言語やRust言語などのライブラリを探してRubyバインディングを作る方法があります。 GitHubの検索では、言語を指定してコードを検索できます。 これを使用して、C言語のライブラリーを見つけます。 GitHubではプロジェクトにタグを付けることができるので、対象のタグを確認することも効果的です。 ただ、UMAPは実装が難しいようで、UMAPを実装しているC言語ライブラリは見当たりませんでした。 代わりに、C++でUMAPを実装するライブラリを見つけました。それが[Umappp](https://github.com/LTLA/umappp)です。

## Umappp - UMAPのC++実装

[Umappp](https://github.com/LTLA/umappp)は、Aaron Lun さんによって実装されたC++のライブラリです。Rの[uwot](https://github.com/jlmelville/uwot)を参考に開発されているようです。計測はしていませんが、C++で実装されOpenMPも利用しているため高いパフォーマンスが期待できます。

https://github.com/LTLA/umappp

## Ruby言語からC++の関数を呼び出す

R言語ではC++を使った拡張ライブラリは一般的ですが、Ruby言語ではC++による拡張ライブラリはそれほど広く利用されていません。最近はRustが人気で、Rubyの拡張機能をRustで作っている人をたまに見かけますが、新規のC++の拡張ライブラリはあまり見かけません。

しかし、RubyでC++のバインディングを作るのは思ったより簡単です。私はC++の経験がありませんが、RubyからC++を呼び出すことができました。

Rubyの拡張をC++で書く方法は2つあります。一つは[Rice](https://github.com/jasonroelofs/rice)、もう一つは[extpp](https://github.com/red-data-tools/extpp)です。今回は[Numo::NArray](https://github.com/ruby-numo/numo-narray) とC++を連携する[numo.hpp](https://github.com/ankane/numo.hpp)を使いたかったのでRiceを使用しました。

実行時にC++をコンパイルできるように、UmapppとUmapppの依存するすべてのC++のファイルをVendorディレクトリに入れてGemとして配布する方針にしました。`extconf.rb` の書き方がよくわからなかったので、他のプロジェクトを参考に調節しました。

## Rice - C++拡張のためのRubyインターフェース

[Rice](https://github.com/jasonroelofs/rice)は、Paul Brannanさん、Charlie Savageさん、 Jason Roelofsさんらによって開発された、RubyでC++拡張を書くためのライブラリです。Riceを使うことで、下記のようにC++のコードからRubyのモジュールやメソッドを定義することができます。

```cpp
#include <rice/rice.hpp>
#include <rice/stl.hpp>

using namespace Rice;

Hash umappp_default_parameters(Object self)
{
  Hash d;
//<中略>
  return d;
}

//<中略>

extern "C" void Init_umappp()
{
  Module rb_mUmappp =
      define_module("Umappp")
          .define_singleton_method("umappp_run", &umappp_run)
          .define_singleton_method("umappp_default_parameters", &umappp_default_parameters);
}
```

## RubyでUMAPを実行する

有名な irisデータセットと、MNISTデータベースに対してUMAPを実行します。

* ここでは自作の可視化ライブラリ[GR.rb](https://github.com/red-data-tools/GR.rb)を使って可視化しています。
* データの取得には [red-datasets](https://github.com/red-data-tools/red-datasets) とその派生ライブラリの [red-datasets-numo-narray](https://github.com/red-data-tools/red-datasets-numo-narray) を使いました。

### Irisデータセット

3種類のアヤメの花の、がく片（sepal）の長さ、幅、および花弁（petal）の長さ、幅を測定したデータセットです。

![iris](https://machinelearninghd.com/wp-content/uploads/2021/03/iris-dataset.png)
画像出典：https://machinelearninghd.com/iris-dataset-uci-machine-learning-repository-project/

Rubyのコード：

```ruby
require "umappp"
require "datasets-numo-narray"
require "gr/plot"

iris = Datasets::LIBSVM.new("iris").to_narray
d = iris[true, 1..-1]
l = iris[true, 0]

r = Umappp.run(d)
x = r[true, 0]
y = r[true, 1]
s = [2000] * l.size

GR.scatter(
  x, y, s, l,
  title: "iris",
  colormap: 16,
  colorbar: true
)
gets
```

![iris.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/43c3735e-7e68-9bd1-a912-e2aa95dd9dd7.png)

明確にクラスタが分かれました。ここでは、ラベル0(setosa)が水色、ラベル1(versicolor)が藤色、ラベル2(virginica)がマゼンタです。setosaのグループは他のグループと明瞭に分かれていますが、versicolorとvirginicaは一部重なっているのがわかります。

### （余談）テーブルデータのGUI表示

Rubyでは、大きなサイズのテーブルデータをCUIで確認しにくい場合があります。そんな時に[RedAmber](https://github.com/heronshoes/red_amber)というデータフレームと、そのGUI表示用の自作ツール[red-amber-view](https://github.com/kojix2/red-amber-view)を使うと、テーブルデータをGUIで表示することもできます。

```ruby
# irisデータセットをテーブルとして表示する
iris = Datasets::LIBSVM.new("iris")
dataframe = RedAmber::DataFrame.new(iris).view
```

```ruby
# 結果をテーブルとして表示する
RedAmber::DataFrame.new(x: x.to_a, y: y.to_a, l: l.to_a).view
```

※これは作りかけなので、こんなこともできますよ、というぐらいの話です。

![Screenshot from 2022-10-11 23-01-58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/c9520468-4b8b-ec73-a658-2f4e8752b964.png)

### MNISTデータベース

次は手書き数字の画像データセットであるMNISTの次元削減をしてみます。

![mnist](https://upload.wikimedia.org/wikipedia/commons/2/27/MnistExamples.png)

Rubyのコード：

```ruby
require "umappp"
require "datasets"
require "gr/plot"
require "etc"

mnist = Datasets::MNIST.new

pixels = []
labels = []
mnist.each_with_index do |r, _i|
  pixels << r.pixels
  labels << r.label
end

puts "start umap"
nproc = Etc.nprocessors
n = nproc > 4 ? nproc - 1 : nproc
d = Umappp.run(pixels, num_threads: n, a: 1.8956, b: 0.8006)
puts "end umap"

x = d[true, 0]
y = d[true, 1]
s = [500] * x.size

GR.scatter(x, y, s, labels, colormap: 0)

gets
```

OpenMPをインストールしている環境では `htop` コマンドなどでCPUの使用率を監視すると、途中から多数のコアを使って計算しているのがわかると思います。非常にきれいにクラスタが分かれました。

![mnist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/2b5c78d4-6f36-c5f5-82b1-73edefa8d169.png)

GR.rbのカラーマップからラベルを比定すると、以下のようになります。これは、[公式によるUMAPの結果](https://speakerdeck.com/lmcinnes/umap-uniform-manifold-approximation-and-projection-for-dimension-reduction?slide=57)とほぼ一致しておりUMAPが正しく実行できていることがわかります。５ー３ー８と、４ー９ー７が類似しているようです。４と９の手書き文字の類似は直感的にもわかりやすく、実際もっとも境界が接しています。私は１を７に似たように書いてしまう癖があるのですが、そういう人はあまりいないようですね。

![mnist-label.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/801c74d4-42cc-2acf-07a1-e88a36a23097.png)


3D表示に挑戦してみます。ここでは、`ndim: 3` を指定します。GR.rb はGIFアニメ出力機能がついているのでそれを使います。

```ruby
require "umappp"
require "datasets" # red-datasets https://github.com/red-data-tools/red-datasets
require "gr/plot"  # GR.rb https://github.com/red-data-tools/GR.rb
require "etc"

mnist = Datasets::MNIST.new

pixels = []
labels = []
mnist.each_with_index do |r, _i|
  pixels << r.pixels
  labels << r.label
end

puts "start umap"
nproc = Etc.nprocessors
n = nproc > 4 ? nproc - 1 : nproc
d = Umappp.run(pixels, ndim: 3, num_threads: n, a: 1.8956, b: 0.8006)
puts "end umap"

x = d[true, 0]
y = d[true, 1]
z = d[true, 2]

GR.beginprint("mnist.gif")
30.times do |i|
  GR.scatter3(x, y, z, labels, title: "mnist", colormap: 0, backgroundcolor: 1, rotation: i * 3)
end
GR.endprint
```

![mnist.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/6576e6ea-5765-4193-44a9-1bc0adb8aafc.gif)

残念ながら、GR.rbでは、`polymarker3d` を順番に呼び出しているだけなので、本来は背後に来るべき点が手前に来たり、手前に来るべき点が背後にきたりしています。が、雰囲気は伝わると思います。2Dのときと位置関係の整合性が保たれているのが面白いですね。

反省点としては、GR.rbを使った可視化では、どの色の集合がどの数字に対応するのかわからない（colorbarのようなものが表示できない）ので、その点の改善が必要だなと思いました。

この記事を書いた人は数学が苦手で、UMAPの具体的な仕組みを全く理解していません。 なのでUMAPについてはうまいことは言えませんが、これだけ高速に動作するなら、気になるデータを調べるのにUMAPを使うと便利だなと思いました。 t-SNEよりかなり早く結果が出る印象です。また、UMAPの結果は思ったよりも再現性があるんだなと感じました。

## おわりに

RubyからC++を呼び出すことにより、RubyからUMAPを使うことができるようにしました。私の知る限りでは、バインディングも含めてRubyでUMAPを実行できるライブラリはこれがはじめてです。こうしてまた一つ、Rubyでできることが増えました。

【PR】
Rubyでデータ解析をツールを作成したい方は [red-data-tools](https://github.com/red-data-tools) というコミュニティがあります。基本的には個人で開発しているのですが、実装上で困っていることや、実現したいけどやり方がわからないアイディアがあったら相談に乗れる場合がありますので、ぜひお立ち寄りください。

この記事は以上です。