洋食の日記

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

Rumaleのガウス混合モデルで共分散の種類を選べるようにした

はじめに

Rumaleのガウス混合モデル(Gaussian Mixture Model, GMM)によるクラスタリングでは、各クラスタの共分散行列には、対角要素のみの対角行列を使用していた。

yoshoku.hatenablog.com

これは、逆行列行列式の計算に特別なアルゴリズムを使用しないためであった。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')と比較して、楕円の角度が考慮されており、よりクラスタの形状を捉えることができている。

f:id:yoshoku:20191003011327p:plain
共分散行列の全要素を使用する場合

f:id:yoshoku:20190615132952p:plain
共分散行列の対角要素のみを使用する場合

共分散行列の全要素を使用する場合、各クラスタの共分散行列をすべて保持する必要があるためメモリをより多く使用する。また、逆行列行列式の計算のため実行時間も遅くなる。

おわりに

その他、因子分析(Factor Analysis)を追加したり、スペクトルクラスタリング(Spectral Clustering)を追加したり、HDBSCANを追加したりしている。これで、ひととおり、Rumaleに実装したアルゴリズムで、Numo::Linalgを使用してパワーアップできるものは対応できた気がする。

ちなみに、会社ではマネ〜ジャ〜兼エンジニアで、タスクフォースな仕事が忙しくなりそうで、これから平日のOSS開発は難しくなる。これまで、月に4回ほどのペースでRumaleをバージョンアップしてきたが、月に1〜2回とかに絞ろうと思う。Rumaleをはじめ、Rubyでデータ分析・機械学習なライブラリの開発が、昼間の仕事としてできれば良いのだけど。幸いベーシックなアルゴリズムは実装できているので、ガウス過程か、計量学習か、あらたにModuleを切るようなものを、じっくり作ろうかな〜といったところ。

github.com