libsvm.rb の中に、グッドラッパを目指すクラスを作り始めました。

hfu2007-11-19

libsvm 同梱の libsvm.jar で提供されているクラスは Ruby 言語らしくない(Java らしくもない)構造をしているので、JRuby 上で気持ちよく使えるために、グッドラッパを目指すクラスを作り始めました。
当面は、学習が済んだモデルを使って予測するための部分を作っています。学習は、libsvm に付属のツールを使って行うことを想定しています。学習の部分もあとでつくるかもしれません。

実装

現在の実装は、以下のようになっています。予測をする部分でいちいち特徴量ベクトルを構造体の配列に入れるのが面倒だったので、普通の配列を与えられるようにしました。sparse な特徴量ベクトルを扱う場合にはハッシュを入れられるようにするのが便利だと思いますが、ハッシュによる入力にはまだ対応していません。

# This script is under development and subject to major change.
require 'java'

# A (J)Ruby module to make use of libsvm in Ruby via Java
module SVM
  # A module to include Java classes in libsvm.
  module LIBSVM
    SVM = JavaUtilities.get_proxy_class('libsvm.svm')
    Model = JavaUtilities.get_proxy_class('libsvm.svm_model')
    Parameter = JavaUtilities.get_proxy_class('libsvm.svm_parameter')
    Problem = JavaUtilities.get_proxy_class('libsvm.svm_problem')
    Node = JavaUtilities.get_proxy_class('libsvm.svm_node')
  end

  # an Support Vector Machine in Ruby way.
  class Machine
    # [path] a path to SVM model stored in a file.
    def initialize(path = nil)
      @model = load_model(path) unless path == nil
    end

    # loads SVM model stored in a file.
    def load_model(path)
      @model = LIBSVM::SVM.svm_load_model(path)
    end

    # returns the number of classes.
    def n_classes
      LIBSVM::SVM.svm_get_nr_class(@model)
    end

    # checks whether the model contains required ifnormation to do
    # probability estimates.
    def has_probability_model?
      LIBSVM::SVM.svm_check_probability_model(@model) == 1
    end

    # internal method which convert a test vector represented using
    # Ruby's plain array to array of svm_node to be used in libsvm.
    def prepare_nodes(vector)
      nodes = nil
      if vector.class == Array
        nodes = LIBSVM::Node[vector.size].new
        vector.size.times do |i|
          node = LIBSVM::Node.new
          node.index = i + 1
          node.value = vector[i]
          nodes[i] = node
        end
      else
        raise "SVM::Machine#predict cannot handle vector in #{vector.class}."
      end
      nodes
    end
    private :prepare_nodes

    # does classification or regression on a test vector.
    def predict(vector)
      LIBSVM::SVM.svm_predict(@model, prepare_nodes(vector))
    end

    # does classification or regression on a test vector and
    # returns probability information in an array.
    def probs(vector)
      probs = Array.new(n_classes).to_java(:double)
      LIBSVM::SVM.svm_predict_probability(@model, 
                                          prepare_nodes(vector),
                                          probs)
      probs
    end

    # def save(path)
    # end

    # def train(input, parameters)
    # end
  end
end

使用例

基本

libsvm 付属のツール svm-train を使って作成したモデルファイル learned.model を読み込んで、特徴ベクトル (0.2, 0.4, 0.8) の分類ラベルを出力するプログラムは、以下のようになります。

require 'libsvm'
svm = SVM::Machine.new('learned.model')
p svm.predict([0.2, 0.4, 0.8])
応用

Shapefile に記録された属性を使って SVM に分類を行わせ、分類結果を加えて別の Shapefile に書き出すプログラムは、以下のようになります:

require 'libsvm'
require 'geotools'
src = '{...}.shp'
dst = '{...}.shp'

svm = SVM::Machine.new('{...}.model')
w = Geo::FeatureList.new(dst)
Geo::FeatureList.foreach(src, {:whitelist => [:feat1, :feat2, :feat3]}) do |f|
  f[:class] = svm.predict([f[:feat1], f[:feat2], f[:feat3]]).to_i
  w.write f
end
w.close

JRubyJava ラッパライブラリの作成パターン(私の)

私の中で、JRuby 用の Java ラッパライブラリの一つの作成パターンが見えてきたのでまとめておきたいと思います。
JRuby 用の Java ラッパを作る際、まずそのラッパ全体のためのモジュール(geotools.rb の Geo, libsvm.rb の SVM)を作り、その中に Java のクラスを単純に取り込む目的のモジュール(geotools.rb の Geo::Tools, libsvm.rb の SVM::LIBSVM)を作り、外側のモジュールに「Ruby らしい」グッドラッパーを目指すクラスを作っていく、というパターンができてきました。
せっかくなので、'pagan poetry pattern' という名前をつけておきます。

on the surface simplicity
but the darkest pit in me
is pagan poetry - pagan poetry

http://unit.bjork.com/specials/albums/vespertine/lyrics.htm

surface simplicity が一段目のモジュールで、二段目のモジュールを darkest pit in me と見立てます。Ruby 言語の中で Ruby 言語らしく Java ライブラリを使おうとする意図が、pagan という単語にふさわしい気もします。

指針とするライブラリ

Ruby 言語の中で Ruby 言語らしく Java ライブラリを使おうとする」と言ったとき、「Ruby 言語らしく」という意味は、私の中では「以下のライブラリのように」というのと同じです。

open-uri

open-uri で示されている考え方は、私が「Ruby 言語らしい」と思うものです。
http://cvs.m17n.org/~akr/pub/rubykaigi2006-06-10.pdf

REXML

私が XML に戸惑っていた頃、REXML を私は「Ruby 言語らしい」と思いました。
http://www.germane-software.com/software/rexml/docs/tutorial.html