はじめに
Rumaleのガウス混合モデル(Gaussian Mixture Model, GMM)によるクラスタリングでは、各クラスタの共分散行列には、対角要素のみの対角行列を使用していた。
これは、逆行列や行列式の計算に特別なアルゴリズムを使用しないためであった。Rumaleのver. 0.13系では、Numo::Linalgへの対応を進めており、逆行列や行列式の計算が可能になったため、共分散行列の全要素を用いる方法を実装した。
rumale | RubyGems.org | your community gem host
使い方
Rumaleはgemコマンドでインストールできる。Numo::NArrayに依存している。
$ gem install rumale
データセットの読み込みでred-datasets、データのplotにNumo::Gnuplotを使いたいので、これらもインストールする。※別途gnuplotをインストールする必要がある
$ brew install gnuplot $ gem install numo-gnuplot red-datasets-numo-narray
Numo::Linalgもインストールする。Numo::Linalgがロードされていなければ、共分散行列の全要素を使ったバージョンは使用できない。
$ brew install openblas $ gem install numo-linalg
Scikit-LearnのGMMの例を試してみる。アヤメデータをGMMでクラスタリングして、1次元目と2次元目の特徴を使って散布図で可視化する。
require 'numo/linalg/autoloader' require 'numo/gnuplot' require 'datasets-numo-narray' require 'rumale' # 楕円の大きさを求める def ellipse_size_full(covar) v, = Numo::Linalg.eigh(covar) nv = 2 * Math.sqrt(2) * Numo::NMath.sqrt(v) nv.to_a end # 楕円の回転角を求める def ellipse_angle_full(covar) _v, w = Numo::Linalg.eigh(covar) u = w[0, true] / Numo::Linalg.norm(w[0, true]) angle = Numo::NMath.atan2(u[1], u[0]) (180.0 * angle[0] / Math::PI).to_i end # Numo::NArray形式でIRISデータセットを読み込む. dataset = Datasets::LIBSVM.new('iris').to_narray labels = Numo::Int32.cast(dataset[true, 0]) samples = Numo::DFloat.cast(dataset[true, 1..-1]) # GMMでクラスタリングする. # covariance_typeで共分散行列の要素をどう使うかを指定できる. gmm = Rumale::Clustering::GaussianMixture.new(n_clusters: 3, covariance_type: 'full', random_seed: 5) cluster_ids = gmm.fit_predict(samples) # ※クラスタラベルは今回の可視化では使用しない. # Numo::GnuplotでPNGファイルに書き出す. ## サンプル x = samples[true, 0] y = samples[true, 1] plots = labels.to_a.uniq.sort.map { |l| [x[labels.eq(l)], y[labels.eq(l)], t: l.to_s] } ## 各クラスタの平均ベクトル plots.push([Numo::DFloat[gmm.means[2, 0]], Numo::DFloat[gmm.means[2, 1]], t: 'center 1']) plots.push([Numo::DFloat[gmm.means[0, 0]], Numo::DFloat[gmm.means[0, 1]], t: 'center 2']) plots.push([Numo::DFloat[gmm.means[1, 0]], Numo::DFloat[gmm.means[1, 1]], t: 'center 3']) ## 各クラスタの共分散による楕円とともにプロットする. Numo.gnuplot do set(terminal: 'png') set(output: 'iris.png') set(:object, 1, 'ellipse', center: [gmm.means[2, 0], gmm.means[2, 1]], size: ellipse_size_full(gmm.covariances[2, 0..1, 0..1]), angle: ellipse_angle_full(gmm.covariances[2, 0..1, 0..1 ]), front: true, fs: true, empty: true, bo: 1) set(:object, 2, 'ellipse', center: [gmm.means[0, 0], gmm.means[0, 1]], size: ellipse_size_full(gmm.covariances[0, 0..1, 0..1]), angle: ellipse_angle_full(gmm.covariances[0, 0..1, 0..1]), front: true, fs: true, empty: true, bo: 2) set(:object, 3, 'ellipse', center: [gmm.means[1, 0], gmm.means[1, 1]], size: ellipse_size_full(gmm.covariances[1, 0..1, 0..1]), angle: ellipse_angle_full(gmm.covariances[1, 0..1, 0..1]), front: true, fs: true, empty: true, bo: 3) plot(*plots) end
これを実行すると、以下のような画像を得られる。以前の対角要素のみを使う方法(covariance_type: 'diag')と比較して、楕円の角度が考慮されており、よりクラスタの形状を捉えることができている。
共分散行列の全要素を使用する場合、各クラスタの共分散行列をすべて保持する必要があるためメモリをより多く使用する。また、逆行列や行列式の計算のため実行時間も遅くなる。
おわりに
その他、因子分析(Factor Analysis)を追加したり、スペクトルクラスタリング(Spectral Clustering)を追加したり、HDBSCANを追加したりしている。これで、ひととおり、Rumaleに実装したアルゴリズムで、Numo::Linalgを使用してパワーアップできるものは対応できた気がする。
ちなみに、会社ではマネ〜ジャ〜兼エンジニアで、タスクフォースな仕事が忙しくなりそうで、これから平日のOSS開発は難しくなる。これまで、月に4回ほどのペースでRumaleをバージョンアップしてきたが、月に1〜2回とかに絞ろうと思う。Rumaleをはじめ、Rubyでデータ分析・機械学習なライブラリの開発が、昼間の仕事としてできれば良いのだけど。幸いベーシックなアルゴリズムは実装できているので、ガウス過程か、計量学習か、あらたにModuleを切るようなものを、じっくり作ろうかな〜といったところ。