洋食の日記

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

Rumaleにガウス混合モデルによるクラスタリングを追加した

はじめに

Rumaleにガウス混合モデル(Gaussain Mixture Model, GMM)によるクラスタリングを追加して、ver. 0.12.2としてリリースした。

rumale | RubyGems.org | your community gem host

GMMは、データ分布をいくつかの正規分布の重み付き線形和で表現しようというもので、このいくつかの正規分布クラスタとなる。GMMは、与えられたデータから、混合比・平均ベクトル・共分散行列で表されるクラスタを解析する。共分散行列の表現方法でいくつかパターンがあるが、Rumaleでは対角要素のみを用いる方法を実装した。これは、混合比を求めるために、共分散行列の行列式逆行列を計算する必要があり、共分散行列が対角行列であると、これら計算を簡略化できることから選択した。

使い方

Rumaleはgemコマンドでインストールできる。Numo::NArrayに依存している。

$ gem install rumale

データセットの読み込みでred-datasets、データのplotにNumo::Gnuplotを使いたいので、これらもインストールする。※別途gnuplotをインストールする必要がある

$ brew install gnuplot
$ gem install numo-gnuplot red-datasets-numo-narray

Scikit-LearnのGMMの例を試してみる。アヤメデータをGMMでクラスタリングして、1次元目と2次元目の特徴を使って散布図で可視化する。

require 'numo/linalg/autoloader'
require 'numo/gnuplot'
require 'datasets-numo-narray'
require 'rumale'

# 楕円の大きさを求める.
def ellipse_size(cov11, cov22)
  cov = Numo::DFloat.zeros(2, 2)
  cov[0, 0] = cov11
  cov[1, 1] = cov22
  v, = Numo::Linalg.eigh(cov)
  sz = 2 * Math.sqrt(2) * Numo::NMath.sqrt(v)
  sz.to_a
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でクラスタリングする.
gmm = Rumale::Clustering::GaussianMixture.new(n_clusters: 3, 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(gmm.covariances[2, 0], gmm.covariances[2, 1]),
      angle: 0.0, front: true, fs: true, empty: true, bo: 1)
  set(:object, 2, 'ellipse',
      center: [gmm.means[0, 0], gmm.means[0, 1]],
      size: ellipse_size(gmm.covariances[0, 0], gmm.covariances[0, 1]),
      angle: 0.0, front: true, fs: true, empty: true, bo: 2)
  set(:object, 3, 'ellipse',
      center: [gmm.means[1, 0], gmm.means[1, 1]],
      size: ellipse_size(gmm.covariances[1, 0], gmm.covariances[1, 1]),
      angle: 0.0, front: true, fs: true, empty: true, bo: 3)
  plot(*plots)
end

これを実行すると、以下のような画像を得られる。Scikit-LearnのGMMの例でいえば、covariance_typeをdiagにした図に相当する。対角要素のみで当てはめると、楕円の回転が考慮されない(例えば左上のクラスタの楕円は斜め45度くらい傾いていると分布によりフィットした感じになるがそれがない)。それだけ分布にフィットできていないことになるわけだが、親戚的な手法であるK-Meansがクラスタ中心のみによる表現であったり、あんまりフィットし過ぎても良くない場合もあったりすることから、クラスタリングの実利用としては割とこれで十分だったりもする。

f:id:yoshoku:20190615132952p:plain

おわりに

Rumaleのver. 0.12.2では、確率モデルによるクラスタリング手法であるガウス混合モデル(Gaussain Mixture Model, GMM)を実装した。また、GMMの他に、オマケでカテゴリ変数を整数値に変換するOrdinalEncoderも追加した。こういったfeature engineeringな前処理クラスを足していっても良いかもな〜とも思ったり。

github.com