はじめに
Rumaleに計量学習のNeighbourhood Component Analysis (NCA) とフィッシャー判別分析 (Fisher Discriminant Analysis, FDA) を追加した。これを version 0.18.0 としてリリースした。
rumale | RubyGems.org | your community gem host
使い方
Rumaleはgemコマンドでインストールできる。線形代数の計算にNumo::Linalg、実行結果の描画にNumo::Gnuplotを使いたいので、一緒にインストールする。
$ gem install rumale numo-linalg numo-gnuplot
Numo::Gnuplotのためにgnuplotをインストールする。
$ brew install gnuplot
NCAのような計量学習は、教師あり特徴変換・次元削減と捉えることができる。一般にk-近傍法で分類するとこが想定されていて、弁別性の高い変換・部分空間への射影を行う。Rumaleではfitメソッドで学習し、transformメソッドで変換・次元削減を行う。
合成データセットに対する次元削減の例を示す。データは以下のような、4つの円筒がならぶもので、高さがランダムになっている。k-近傍法による分類のためには、円の形が部分空間に保存されていると良い。
これを、主成分分析 (Principal Component Analysis, PCA)、FDA、NCAで2次元空間に射影するコードは次のようになる。
require 'numo/linalg/autoloader' require 'numo/gnuplot' require 'rumale' # 円筒の合成データを作成する. x1, y1 = Rumale::Dataset.make_circles(1500, random_seed: 1) x2, y2 = Rumale::Dataset.make_circles(1500, random_seed: 2) x = x1.class.vstack([x1, x2 * 0.6]) y = y1.class.hstack([y1, y2 + 2]) x = x.class.hstack([x, 0.6 * Rumale::Utils.rand_normal([x.shape[0], 1], Random.new(3))]) # 合成データをプロットする. plots = y.to_a.uniq.sort.map { |l| [x[y.eq(l), 0], x[y.eq(l), 1], x[y.eq(l), 2], t: l.to_s] } Numo.gnuplot do set(terminal: 'png') set(output: 'circles.png') set(ticslevel: 0) splot(*plots) end # 各種手法で二次元空間に射影する. #z = Rumale::Decomposition::PCA.new( # n_components: 2, solver: 'evd').fit_transform(x) #z = Rumale::MetricLearning::FisherDiscriminantAnalysis.new( # n_components: 2).fit_transform(x, y) z = Rumale::MetricLearning::NeighbourhoodComponentAnalysis.new( n_components: 2, init: 'pca', max_iter: 20, verbose: true, random_seed: 1 ).fit_transform(x, y) # 射影したデータをプロットする. plots = y.to_a.uniq.sort.map { |l| [z[y.eq(l), 0], z[y.eq(l), 1], t: l.to_s] } Numo.gnuplot do set(terminal: 'png') set(output: 'nca_circles.png') plot(*plots) end
これで次元削減した結果が以下のようになる。PCAは、教師なし次元削減であるためラベル情報が反映されず、円筒を横に倒したようなかたちとなっている。FDAは、教師ありでラベル情報が与えられているが、それでもPCAと同様の結果となっている。NCAだけが、円の形を捉えることができ、k-近傍法で正しくラベルを推定できるようになっている。
NCAが最も良いように見える。とはいえ、やはり得手不得手とするデータは存在する。離れて位置する3つの楕円体からなるデータを試す。
これを、PCA、FDA、NCAで2次元空間に射影するコードは次のようになる。
require 'numo/gnuplot' require 'numo/linalg/autoloader' require 'rumale' # 3つの団子からなる合成データを作成する. centers = Numo::DFloat[[0, 5], [-5, -5], [5, -5]] x, y = Rumale::Dataset.make_blobs(300, centers: centers, cluster_std: 0.5, random_seed: 1) x = x.class.hstack([x, 4 * Rumale::Utils.rand_normal([x.shape[0], 1], Random.new(1))]) # 合成データをプロットする. plots = y.to_a.uniq.sort.map { |l| [x[y.eq(l), 0], x[y.eq(l), 1], x[y.eq(l), 2], t: l.to_s] } Numo.gnuplot do set(terminal: 'png') set(output: 'blobs.png') set(ticslevel: 0) splot(*plots) end # 各種手法で二次元空間に射影する. #z = Rumale::Decomposition::PCA.new(n_components: 2, solver: 'evd').fit_transform(x) #z = Rumale::MetricLearning::FisherDiscriminantAnalysis.new(n_components: 2).fit_transform(x, y) z = Rumale::MetricLearning::NeighbourhoodComponentAnalysis.new( n_components: 2, init: 'random', max_iter: 10, verbose: true, random_seed: 1 ).fit_transform(x, y) # 射影したデータをプロットする. plots = y.to_a.uniq.sort.map { |l| [z[y.eq(l), 0], z[y.eq(l), 1], t: l.to_s] } Numo.gnuplot do set(terminal: 'png') set(output: 'nca_blobs.png') plot(*plots) end
次元削減した結果は以下のようになる。PCAは、緑色と青色のデータが混在してしまっている。FDAは、3つがキレイに離れていて、正しくラベルを推定できるようになっている。NCAは、3つのデータが離れてはいるが、FDAほどはキレイに離れていない。
おわりに
開発当初は機械学習の古典的な手法である、FDAを追加することを考えていた。しかし、discriminant analysisで名前空間を切っても広がりがないと思い、FDAを教師あり次元削減と捉え、metric learningで名前空間を切って、NCAとともに追加した。NCAの最適化では、scaled conjugate gradientを使用した。Pythonでは最適化にscipy.optimizeが使えるが、Rubyではそういったライブラリはあまりない。そこで最適化の部分だけgemに切り出し、これをRumaleから使うことにした。このgemも、徐々に代表的な手法を追加していき、育てていこうと思う。