洋食の日記

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

Rumaleにカーネルリッジ回帰を追加した

はじめに

Rumaleのversion 0.13系では、固有値分解や逆行列計算といった、Numo::Linalgにある線形代数のテクニックを利用する機械学習アルゴリズムの実装を進めている。version 0.13.3では、カーネルリッジ回帰とカーネル主成分分析を追加した。

rumale | RubyGems.org | your community gem host

使い方

Rumaleは、gemコマンドでインストールできる。データセットの読み込みでred-datasetsを使用したいので、あわせてインストールする。

$ gem install rumale red-datasets-numo-narray

そして、Numo::Linalgをインストールする。Numo::LinalgはBLAS/LAPACK系のライブラリに依存する。別途OpenBLASなどをインストールする必要がある。

$ gem install numo-linalg

Boston Housingデータセットを利用して、カーネルリッジ回帰を試してみる。Boston Housingデータセットは、地域の住宅価格を、犯罪発生率や平均部屋数などから推定するタスクである。リッジ回帰・カーネルリッジ回帰で住宅価格を推定し、決定係数により評価を行う。ハイパーパラメータはそれらしい値をいれた(※カーネルリッジ回帰は正則化パラメータを調整したほうがよい)。

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

# データセットを読み込む.
datasets = Datasets::LIBSVM.new('housing').to_narray
values = Numo::DFloat.cast(datasets[true, 0])
samples = Numo::DFloat.cast(datasets[true, 1..-1])

# 訓練とテストに分割する.
ss = Rumale::ModelSelection::ShuffleSplit.new(n_splits: 1, test_size: 0.2, random_seed: 1)
train_ids, test_ids = ss.split(samples, values).first
train_s = samples[train_ids, true]
train_v = values[train_ids]
test_s = samples[test_ids, true]
test_v = values[test_ids]

# 特徴量を正規化する.
t = Rumale::Preprocessing::MinMaxScaler.new
train_s = t.fit_transform(train_s)
test_s = t.transform(test_s)

# リッジ回帰を学習し決定係数で評価する.
ridge = Rumale::LinearModel::Ridge.new(reg_param: 0.1, solver: 'svd')
ridge.fit(train_s, train_v)
score = ridge.score(test_s, test_v)
puts "linear ridge: #{score.round(4)}"

# カーネルリッジ回帰を学習し決定係数で評価する.
kridge = Rumale::KernelMachine::KernelRidge.new(reg_param: 0.1)
## 訓練データ間のRBFカーネル関数の値をもとめ訓練を行う.
gamma = 2.0
train_kernel_mat = Rumale::PairwiseMetric::rbf_kernel(train_s, nil, gamma)
kridge.fit(train_kernel_mat, train_v)
## テストデータと訓練データ間のRBFカーネル関数の値をもとめ決定係数を計算する.
test_kernel_mat = Rumale::PairwiseMetric.rbf_kernel(test_s, train_s, gamma)
score = kridge.score(test_kernel_mat, test_v)
puts "kernel ridge: #{score.round(4)}"

これを実行すると次のようになる。

linear ridge: 0.6443
kernel ridge: 0.8655

決定係数の値が大きいほどよい。カーネルリッジ回帰のほうが、Boston Housingデータセットに関しては、回帰の推定性能がリッジ回帰よりも優れていると思われる(上記スクリプトは、シンプルなホールドアウト検証になってるので、本当は交差検証をちゃんとしたほうが良い)。

おわりに

Rumaleにカーネルリッジ回帰を実装した。カーネル法は、データ数が十分にない場合などでは、まだまだ有効であると思われる。また、ニューラルネットワークがReLUやDropoutなどの学習方法を発見して復活したように、とんでもないカーネル関数やすごいマルチカーネル手法などが発見されると、復活の可能性はあるかもしれない??

Rumaleでは、その他、カーネル主成分分析やissueでリクエストのあったShared Nearest Neighborクラスタリングなどを追加している。しばらくは、Numo::Linalgありきのアルゴリズムを追加していく。そろそろRumaleのユーザーガイドやチュートリアル的な文書が必要だな〜とかも考えている。

github.com

Rumaleの主成分分析でNumo::Linalgの固有値分解を利用できるようにした

はじめに

機械学習アルゴリズムでは、固有値分解が逆行列計算といった、線形代数のテクニックを利用する場合がある。これらをRubyで使用するには、Numo::Linalgが適している。一方で、Numo::Linalgは、Numo::NArrayと異なり、BLAS/LAPACK系の外部ライブラリに依存するため、Rumaleでは依存することを避けてきた。ただ、今後のRumaleの拡張を考えると必要となる。そこで、以前にParallelを導入したときのように、別でNumo::Linalgをrequireしている場合にのみ、機能が有効になるよう実装することにした。

まず、試験的に、主成分分析で固有値分解を使うものを実装してみた。主成分分析は、主成分ベクトルを、共分散行列の固有値分解により求める方法がメジャーである。他にも主成分ベクトルを求めるアルゴリズムはあり、Rumaleでは不動点法によるアルゴリズムを実装していた。今回、solverオプションを追加して、固有値分解によるアルゴリズムを選択できるようにした。これを version 0.13.0 としてリリースした。

rumale | RubyGems.org | your community gem host

使い方

Rumaleは、gemコマンドでインストールできる。データセットの読み込みでred-datasetsを、主成分分析したデータの可視化にNumo::Gnuplotを使いたいので、あわせてインストールする。

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

そして、Numo::Linalgをインストールする。

$ gem install numo-linalg

USPSという手書き数字画像のデータセットを、主成分分析で二次元に射影して可視化する。

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

# Numo::Linalgを明示的にrequireしない限りは有効にならない.
require 'numo/linalg/autoloader' 

# Numo::NArray形式でUSPSデータセットを読み込む.
dataset = Datasets::LIBSVM.new('usps').to_narray
labels = Numo::Int32.cast(dataset[true, 0])
samples = Numo::DFloat.cast(dataset[true, 1..-1])

# 適当にサブサンプリングした.
rand_id = Array.new(samples.shape[0]) { |n| n }.sample(1000)
labels = labels[rand_id]
samples = samples[rand_id, true]

# 共分散行列を固有値分解することによる主成分分析で、二次元空間にデータを射影する.
# solverオプションに 'evd' を指定する(指定しなければコレまでどおり不動点法を使う).
pca = Rumale::Decomposition::PCA.new(n_components: 2, solver: 'evd', random_seed: 1)
low_samples = pca.fit_transform(samples)

# Numo::GnuplotでPNGファイルに書き出す.
x = low_samples[true, 0]
y = low_samples[true, 1]
plots = labels.to_a.uniq.sort.map { |l| [x[labels.eq(l)], y[labels.eq(l)], t: l.to_s] }
Numo.gnuplot do
  set(terminal: 'png')
  set(output: 'pca.png')
  plot(*plots)
end

以上のスクリプトを実行した結果は次のようになる。USPSを主成分分析で可視化するとだいたいこんな感じになる。上手く動いていることがわかる。

f:id:yoshoku:20190824181414p:plain

おわりに

Numo::Linalgを導入したので、カーネル主成分分析やカーネルRidge回帰など、固有値分解や逆行列計算を必要とする機械学習アルゴリズムを実装していこうと思っている。

github.com

Numo::Pocketfftにフーリエ変換による畳み込み演算するメソッドを追加した

はじめに

畳み込み演算は、そのまま実装すると、データが大きくなると重くなる。一方、フーリエ変換により、畳み込み演算は単純な掛け算に変換される。これを応用して、畳み込み演算したい二つの配列をフーリエ変換し、乗算を行った後に、フーリエ逆変換する高速化手法が一般に利用される。これをNumo::Pocketfftに実装しversion 0.2.0とした。

numo-pocketfft | RubyGems.org | your community gem host

使い方

Numo::Pocketfftはgemコマンドでインストールできる。pocketfftのコードを同梱しているので、外部ライブラリを別でインストールする必要はない。

$ gem install numo-pocketfft

SciPyにあやかって、メソッド名はfftconvolveとした。二つのNumo::NArrayな配列を渡せば、その(離散)畳み込み演算の結果を返す。

irb(main):001:0> require 'numo/pocketfft'
=> true
irb(main):002:0> Numo::Pocketfft.fftconvolve(Numo::DFloat[1, 2, 3], Numo::DFloat[4, 5])
=> Numo::DFloat#shape=[4]
[4, 13, 22, 15]

画像処理への応用

画像のフィルタリングは、画像とフィルタの畳み込みで実現される。Magroも使って、アイコンに使ってるハンバーグの画像にSobelフィルタをかけて、エッジを抽出してみる。

require 'magro'
require 'numo/pocketfft'

# Lena画像を読み込む.
img = Magro::IO.imread('lena.png')

# グレイスケール化する.
gray = img.median(axis: 2)

# Sobelフィルタを定義する.
fx = Numo::DFloat[[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]
fy = Numo::DFloat[[-1, -2, -1], [0, 0, 0], [1, 2, 1]]

# 畳み込みによりSobelフィルタをかける.
gx = Numo::Pocketfft.fftconvolve(gray, fx)
gy = Numo::Pocketfft.fftconvolve(gray, fy)

# エッジ画像を得る.
g = Numo::NMath.sqrt(gx**2 + gy**2)
g[g>255] = 255
g[g<0] = 0

# 畳み込みによりできた画像の端を削る.
# ※
# 元の画像サイズは512x512であり, フィルタのサイズが3x3であることから, 
# 畳み込みにより (512 + 3 - 1) x (512 + 3 - 1) の画像ができる.
# 上下左右の端を1ピクセルずつ削り512x512の画像にする.
h, w = gray.shape
fh, fw = g.shape
sy = (fh - h) / 2
sx = (fw - w) / 2
g = Numo::UInt8.cast(g[sy..h, sx..w])

# 画像を保存する.
Magro::IO.imsave('lena_fil.png', g)

これを実行すると、以下のような画像が得られる。Sobelフィルタによりエッジが抽出されていることがわかる。

f:id:yoshoku:20200712193223p:plain

おわりに

Numo::NArrayな配列で畳み込み演算ができるようになった。使い方の例のように、画像のフィルタリングも簡単になったので、Magroの開発も進むかな?

github.com

Numo::NArrayをpocketfftでフーリエ変換するGemを作成した

はじめに

NumPyが、ver. 1.17から、フーリエ変換にFFTPACKではなくpocketfftを使う様になった。pocketfftはFFTPACKを修正したもので、速度と精度が改善されている。

「NumPy 1.17」リリース | OSDN Magazine

ENH: Add pocketfft sources to numpy for testing, benchmarks, etc. by mreineck · Pull Request #11888 · numpy/numpy · GitHub

https://gitlab.mpcdf.mpg.de/mtr/pocketfft

pocketfftは、単一のC言語のソースファイルからなるので「これはソースを同梱する形でNumo::NArrayな配列をフーリエ変換するなにかが作れるぞ」と思い、早速作ってみた。Numo::NArrayでは、フーリエ変換APIは提供されておらず、別でNumo::FFTWNumo::FFTEをインストールする形になっている。そこで、Numo::Pocketfftとして公開した。

numo-pocketfft | RubyGems.org | your community gem host

使い方

pocketfftを同梱しているので、普通にgemコマンドでインストールできる。MacUbuntuWindows 10(RubyInstallerな環境)でインストールできることを確認した。

$ gem install numo-pocketfft

メソッド名や動きはnumpy.fftに従った。普通の高速フーリエ変換fftメソッド、ifftメソッドで、実数な配列を想定した高速フーリエ変換をrfftメソッド、irfftメソッドで行う。

Module: Numo::Pocketfft — Documentation for numo-pocketfft (0.1.0)

まずは、実数の1次元配列のフーリエ変換・逆変換は以下のようになる。

irb(main):001:0> require 'numo/pocketfft'
=> true
irb(main):002:0> a = Numo::DFloat.new(4).rand
=> Numo::DFloat#shape=[4]
[0.0617545, 0.373067, 0.794815, 0.201042]
irb(main):003:0> x = Numo::Pocketfft.rfft(a)
=> Numo::DComplex#shape=[3]
[1.43068+0i, -0.733061-0.172025i, 0.282461+0i]
irb(main):004:0> Numo::Pocketfft.irfft(x)
=> Numo::DFloat#shape=[4]
[0.0617545, 0.373067, 0.794815, 0.201042]
irb(main):005:0>

逆変換の結果が複素数で返ってきても良いなら、全てfft系メソッドでもよい。例えば実数の2次元配列のフーリエ変換・逆変換は以下のようになる。fft2と2をつける(これはnumpy.fftにあわせた)。

irb(main):001:0> require 'numo/pocketfft'
=> true
irb(main):002:0> a = Numo::DFloat.new(4, 4).rand
=> Numo::DFloat#shape=[4,4]
[[0.0617545, 0.373067, 0.794815, 0.201042],
 [0.116041, 0.344032, 0.539948, 0.737815],
 [0.165089, 0.0508827, 0.108065, 0.0687079],
 [0.904121, 0.478644, 0.342969, 0.164541]]
irb(main):003:0> Numo::Pocketfft.ifft2(Numo::Pocketfft.fft2(a))
=> Numo::DComplex#shape=[4,4]
[[0.0617545+0i, 0.373067+0i, 0.794815+0i, 0.201042+0i],
 [0.116041+0i, 0.344032+0i, 0.539948+0i, 0.737815+0i],
 [0.165089+0i, 0.0508827+0i, 0.108065+0i, 0.0687079+0i],
 [0.904121+0i, 0.478644+0i, 0.342969+0i, 0.164541+0i]]

任意の次元数を受け付けるfftnとrfftnメソッドもある。

irb(main):001:0> require 'numo/pocketfft'
=> true
irb(main):002:0> a = Numo::DFloat.new(2,2,2).rand + Complex::I * Numo::DFloat.new(2,2,2).rand
=> Numo::DComplex#shape=[2,2,2]
[[[0.0617545+0.165089i, 0.373067+0.0508827i],
  [0.794815+0.108065i, 0.201042+0.0687079i]],
 [[0.116041+0.904121i, 0.344032+0.478644i],
  [0.539948+0.342969i, 0.737815+0.164541i]]]
irb(main):003:0> Numo::Pocketfft.ifftn(Numo::Pocketfft.fftn(a))
=> Numo::DComplex#shape=[2,2,2]
[[[0.0617545+0.165089i, 0.373067+0.0508827i],
  [0.794815+0.108065i, 0.201042+0.0687079i]],
 [[0.116041+0.904121i, 0.344032+0.478644i],
  [0.539948+0.342969i, 0.737815+0.164541i]]]

といった感じで、フーリエ変換・逆変換できる。

git submodule による外部ライブラリの同梱

pocketfftの同梱は、Gitlab上のpocketfftのリポジトリを、submoduleとして取り込むことで実現した。submoduleの追加は、特別な工夫はなくgitコマンド通りになる。

$ cd ext/numo/pocketfft
$ git submodule add https://gitlab.mpcdf.mpg.de/mtr/pocketfft pocketfft

Ruby extensionでは、mkmfというライブラリでMakefileを作成する。mkmfでは、基本的には、全てのコードが同一のディレクトリ下(Numo::Pocketfftを例にすればext/numo/pocketfft下)にあることを想定している。submoduleで追加したpocketfftは、サブディレクトリにある。この階層構造を、mkmfに教えるようなことが必要になる。

# extconf.rb
# ...省略

# ext/numo/pocketfft下のコードを$srcsに追加する
$srcs = Dir.glob("#{$srcdir}/*.c").map { |path| File.basename(path) }

# pocketfftのコードを$srcsに追加する
$srcs << 'pocketfft.c'

# ext/numo/pocketfft/pocketfftをインクルードパスやmakeの探索パスに追加する
# ※ インクルードパスはいらないかも
Dir.glob("#{$srcdir}/*/") do |path|
  dir = File.basename(path)
  $INCFLAGS << " -I$(srcdir)/#{dir}"
  $VPATH << "$(srcdir)/#{dir}"
end

# ...省略

これで、rake compileでpocketfftも含めてコンパイルできる。そして、gemファイルを作る際に、submoduleなpocketfftを含めるために、gemspecファイルで、Gem::Specificationのfilesにsubmoduleへのパスを追加する。

# numo-pocketfft.gemspec
# ...省略

Gem::Specification.new do |spec|
# ...省略

  # submoduleのディレクトリ下のファイルをfilesに追加する
  # 参考: https://gist.github.com/mattconnolly/5875987
  gem_dir = __dir__ + '/'
  `git submodule --quiet foreach pwd`.split($OUTPUT_RECORD_SEPARATOR).each do |submodule_path|
    Dir.chdir(submodule_path) do
      submodule_relative_path = submodule_path.sub gem_dir, ''
      `git ls-files`.split($OUTPUT_RECORD_SEPARATOR).each do |filename|
        spec.files << "#{submodule_relative_path}/#{filename}"
      end
    end
  end

# ...省略

これでsubmoduleにより外部ライブリをgemに同梱できた。後で発見したが、同様のことがruby-eigenでも行われていた。ruby-eigenでは、C++線形代数ライブラリEigenが、submoduleにより同梱されている。

おわりに

画像にフィルタを掛ける場合、画像とフィルタとの畳み込み演算なる。畳み込み演算は、フーリエ変換により複素数の世界に持っていくと、シンプルな乗算になる。これを利用すると、畳み込み演算をフーリエ変換・逆変換で実装できる。画像処理ライブラリのMagroの開発で、このフーリエ変換による畳み込みを必要としていたので、良い機会だと思いpocketfftによるフーリエ変換のgemを作った。NumPyのフーリエ変換では、正規化のオプションがあったりするが、Numo::Pocketfftでは、まずは、シンプルなフーリエ変換・逆変換を実装した。今後bugfixとかも含めて、バージョンアップしていくつもりでいる。

github.com

画像をNumo::NArrayで表現するRubyの画像処理ライブラリを作り始めた

はじめに

PythonではOpenCVとScikit-imageという画像処理ライブラリがある。これらライブラリでは、画像データをNumPyのndarrayで表す。

>>> from skimage import io
>>> img = io.imread('lena.png')
>>> type(img)
<class 'numpy.ndarray'>

同じような感じで、画像データをNumo::NArrayで扱うものが欲しくなったので作り始めた。名前は、Image ProcessingからとってMagroとした(Gem名を考えているときに、ふと「そういえば鮪ペイントって昔あったよな」と思い出したのもあって)。実はまだ、pngjpegを読み書きすることしかできないが...

magro | RubyGems.org | your community gem host

インストール

Magroでは、pngjpegの読み書きにlibpngとlibjpegを使用するため、これらを先にインストールする。メジャーなライブラリなので、なにかしらパッケージがある(環境によってはすでにインストールされている場合もある)。

macOSであればbrewで:

$ brew install libpng libjpeg

Ubuntuなどlinux系であれば適当なパッケージマネージャで:

$ sudo apt-get install libpng-dev libjpeg-dev

Magroは、gemコマンドでインストールできる。

$ gem install magro

使い方

Magroでは、まだ、pngjpeg画像の読み書きしかできない。画像の形式はファイル名の拡張子で判定している。例えば、有名なRGBなPNG画像ファイルをグレイスケールにするならば、以下のようになる。

[1] pry(main)> require 'magro'
=> true
[2] pry(main)> img = Magro::IO.imread('lena.png')
=> Numo::UInt8#shape=[512,512,3]
[[[226, 137, 125],
  [226, 137, 125],
  [223, 137, 133],
...
[3] pry(main)> gray = img.median(axis: 2)
=> Numo::UInt8#shape=[512,512]
[[137, 137, 137, 136, 138, 129, 138, 134, 140, 136, 135, 134, 130, 139, ...],
 [137, 137, 137, 136, 138, 129, 138, 134, 140, 136, 135, 134, 130, 139, ...],
 [137, 137, 137, 136, 138, 129, 138, 134, 140, 136, 135, 134, 130, 139, ...],
...
[4] pry(main)> Magro::IO.imsave('lena_gray.png', gray)
=> true

画像データはNumo::UInt8で表現されるので、Red ChainerのConvolution Netsに投げるとか、Numo::DFloatなベクトルに変換してRumaleのRandom Forestに投げるとかできる。

おわりに

Rubyの画像処理ライブラリとしては、RMagickをはじめImageMagickをbindingしたものや、OpenCVをbindingしたものがある。OpenCVでは、画像をCvMatというOpenCVの行列型で表現する。行列の表現がNumo::NArrayでなくてもOKで、画像を行列で欲しい場合は、RubyであればOpenCVのbindingが良い(ただし対応しているOpenCVのバージョンは2系の様子)。

Magroは、PythonのScikit-Imageや、C言語のVLFeatあたりの雰囲気を目指して、あまり大きくならない程度に開発する予定。次は、画像にフィルタをかけるには、畳み込み演算が必要で、そのためのフーリエ変換をなにかしら組み込むとかかなぁ。

github.com