洋食の日記

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

LIBSVM/SVMLight形式のデータの読み込み/書き込みをするgemを公開した

はじめに

LIBSVM形式のデータセットファイルを読み込み、データとラベルをNMatrixで返すライブラリが欲しかったので、作成してgemパッケージを公開した。

libsvmloader | RubyGems.org | your community gem host

Pythonのsvmlight-loaderにお世話になっているので、リスペクトのつもりで「LibSVMLoader」としたけど、Wekaに同名のクラスがあった。Loaderと名付けたけど、svmlight-loaderと同じく書き込みもできる。

インストール

読み込んだデータやラベルをNMatrixで返すのでNMatrixに依存する。

$ gem install libsvmloader

使い方

読み込みにはload_libsvm_fileメソッドを、書き込みにはdump_libsvm_fileメソッドを用いる。ドキュメントはこちら

require 'libsvmloader'

# 分類タスクを読み込む場合:
# samplesは、サンプル数x次元数のfloat64のNMatrix
# labelsは、サンプル数x1のint32のNMatrix
samples, labels = LibSVMLoader.load_libsvm_file('hoge.t')

# 回帰タスクを読み込む場合:
# ※ラベルに相当するNMatrixのdtypeを指定できるのでfloat64として実数とする
# samplesは、サンプル数x次元数のfloat64のNMatrix
# target_valuesは、サンプル数x1のfloat64のNMatrix
samples, target_values = LibSVMLoader.load_libsvm_file('hoge.t', label_dtype: :float64)

# libsvmファイルの特徴ベクトルの添字が「0」から始まる場合:
samples, labels = LibSVMLoader.load_libsvm_file('hoge.t', zero_based: true)

# 書き込み:
LibSVMLoader.dump_libsvm_file(samples, labels, 'foo.t')

# 書き込み(特徴ベクトルの添字を「0」から始めたい場合):
LibSVMLoader.dump_libsvm_file(samples, labels, 'foo.t', zero_based: true)

線形SVMによる分類のサンプル

例として、libsvmloaderでデータを読み込んで、liblinear-rubyで線形SVMの訓練・テストをするのは以下の様になる。

require 'libsvmloader'
require 'liblinear'

# 訓練データを読み込む.
tr_samples, tr_labels = LibSVMLoader.load_libsvm_file('pendigits')

# 線形SVMによる分類器を訓練する.
Liblinear.quiet_mode
model = Liblinear.train(
  { solver_type: Liblinear::L2R_L2LOSS_SVC },
  tr_labels.to_flat_a,
  tr_samples.to_a
)

# テストデータを読み込む.
ts_samples, ts_labels = LibSVMLoader.load_libsvm_file('pendigits.t')

# テストデータのラベルを推定する.
pr_labels = ts_samples.to_a.map { |fv| Liblinear.predict(model, fv).to_i }

# 正解数をカウントする.
n_hits = pr_labels.map.with_index { |l, n| 1 if l == ts_labels[n] }.compact.sum

# Accuracyを出力する.
n_ts_samples = ts_labels.size
accuracy = 100.0 * (n_hits / n_ts_samples.to_f)
puts(format('Accuracy = %.4f%% (%d/%d)', accuracy, n_hits, n_ts_samples))

おわりに

つまらないものですが、よろしくお願い致します。

Liblinear-Rubyによる線形SVMを試すついでにカーネル近似も試した

はじめに

Liblinearは、線形SVMの実装として有名なライブラリ/ツールである。 これをRubyから叩くライブラリとして、Liblinear-Rubyがある。 Rubyの配列で表現された、特徴ベクトルとラベルをわたすだけで、線形SVMの訓練・テストが行える。 これで線形SVMによる分類器が簡単にできるので、ついでにカーネル近似も試した。

インストール

Liblinear-Rubyでは、内部的にswigを使っているので、HomebrewでLiblinearだけでなくswigもインストールする。

$ brew install liblinear swig
$ gem install liblinear-ruby

libsvm形式ファイルの読み込み

Rubylibsvmフォーマットのファイルを読み込むメソッド書いていたが、どうにも汚くなってしまったので、 PyCallでScikit-Learnのload_svmlight_fileを叩くことにした。 トリッキーだけど、PyCallで得られたnumpy.arrayをNMatrixに変換することを、考えてみたかったのもある。 あんまりいい感じにならなかったけど。

require 'nmatrix/lapacke'

require 'pycall/import'
include PyCall::Import
pyfrom 'sklearn.datasets', import: 'load_svmlight_file'

def load_libsvm_file(filename)
  # PyCallで呼び出したload_svmlight_fileでlibsvm形式のデータを読み込む。
  py_samples, py_labels = load_svmlight_file.(filename, zero_based: false)
  # サンプル数と次元数を得て、サンプル用のNMatrixとラベル用のNVectorを用意する。
  n_samples, n_dimensions = py_samples.shape
  samples = NMatrix.zeros [n_samples, n_dimensions]
  labels = NVector.zeros n_samples
  # NMatrixとNVectorに変換する。
  n_samples.times do |r|
    labels[r] = py_labels[r].to_i
    n_dimensions.times do |c|
      samples[r,c] = py_samples[r,c].to_f
    end
  end
  return samples, labels
end

Liblinear-Rubyでの分類

まず、動作確認のためのサンプルデータとして、libsvmのサイトからpendigitsデータをダウンロードしておく。

$ wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/pendigits
$ wget https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/pendigits.t

これらを読み込んで、訓練・テストを行うコードは次の様になる。

# 訓練データを読み込む。
samples, labels = load_libsvm_file 'pendigits'

# Liblinear-Rubyで線形SVMな分類器の訓練を行う。
# データの入力にはRubyの配列を用いるので、
# to_aやto_flat_aでNMatrixを配列に変換する。
Liblinear.quiet_mode
model = Liblinear.train(
  { solver_type: Liblinear::L2R_L2LOSS_SVC },
  labels.to_flat_a,
  samples.to_a)

# テストデータを読み込む。
test_samples, test_labels = load_libsvm_file 'pendigits.t'

# テストデータのラベルを推定する。
# predictメソッドでは、テストデータをまとめて与えるのではなく、
# 特徴ベクトルの一つ一つを与える形になる。
pred_labels = test_samples.to_a.map do |smpl|
  Liblinear.predict(model, smpl).to_i
end

# 正解しているラベルをカウントする。
n_correctly_preds = 0
n_test_samples = test_labels.size
n_test_samples.times do |n|
  n_correctly_preds += 1 if test_labels[n] == pred_labels[n]
end

# Accuracyを計算して出力する。
accuracy = n_correctly_preds / n_test_samples.to_f
puts sprintf("Accuracy = %.4f%% (%d/%d)", 
  accuracy * 100.0, n_correctly_preds, n_test_samples)

これを実行すると、次の様な結果を得られた。 これは、当然ながら、Liblinearのコマンドで訓練・テストした場合のAccuracyと一致した。

$ ruby hoge.rb
Accuracy = 87.8788% (3074/3498)

カーネル近似による非線形分類

Liblinearで得られるのは線形分類器なので、特徴空間上で非線形に分布するデータは、正しく分類できない。 これを解決する方法で有名なのが、カーネル法だが、データ数が多いと重くなったり、下手をすると動かなかったりする。 この問題に対して、カーネルを近似するような変換を考えよう、というアプローチがある。 最も有名なのが、ガウスカーネルを近似した、Random Fourier Features(RFF)である。 RFFでは、D個の正規ランダムベクトルwと特徴ベクトルxとの積による変換で表される。

 \displaystyle
\phi(\mathbf{x})=\sqrt{1/D}[ \sin(\mathbf{w}_{1}^{\top}\mathbf{x}),\ldots,sin(\mathbf{w}_{D}^{\top}\mathbf{x}), \cos(\mathbf{w}_{1}^{\top}\mathbf{x}),\ldots,cos(\mathbf{w}_{D}^{\top}\mathbf{x}) ]

これは、正規ランダムベクトルを並べた正規ランダム行列Wを考えると、処理自体は行列-ベクトル積となるので重くない。

 \displaystyle
W=[\mathbf{w}_{1},\ldots,\mathbf{w}_{D}]^{\top}

このRFFの改良版が、NIPS'16においてGoogle ResearchのF X. Yuらが提案した、Orthogonal Random Features(ORF)である。 ORFでは、ランダム行列Wを、カイ分布からサンプルされた要素からなる対角行列S、一様分布なランダム直交行列Qで表す。 ※Qは正規ランダム行列GをQR分解することで得られるそうな。

 \displaystyle
W=\frac{1}{\sigma}SQ

また、次の簡単化したモノも提案されている。

 \displaystyle
W=\frac{\sqrt{d}}{\sigma}Q

直交化することで、カーネル近似の精度が上がるとのこと。 さらに論文では、Structured ORFとしてWalsh-Hadamard行列を用いたものも提案されている。

ORFの実装

今回、ORFを試そうと思ったのは、NMatrixのQR分解を試したかったからである。 NMatrixには、正規ランダム行列を生成するメソッドがないので、まずこれを実装する。よくあるBox-Muller法を用いた。

def randn(shape, mu=0.0, sigma=1.0)
  x = NMatrix.random shape
  y = NMatrix.random shape
  ((x.log * -2.0).sqrt * (y * 2.0 * Math::PI).sin) * sigma + mu
end

さらに、正規ランダム行列をQR分解して直交行列を得るメソッドと、特徴変換を行うメソッドは、次のようになる。

# Random Fourier Featuresのための変換行列を得る。
def rff_transform_mat(samples, sigma=100.0)
  n_samples, n_dimensions = samples.shape
  rand_mat = randn [n_dimensions, n_dimensions]
  transform_mat = rand_mat * (1.0 / sigma)
end

# Orthogonal Random Featuresのための変換行列を得る。
def orf_transform_mat(samples, sigma=100.0)
  n_samples, n_dimensions = samples.shape
  rand_mat = randn [n_dimensions, n_dimensions]
  orth_rand_mat, r = rand_mat.factorize_qr
  transform_mat = orth_rand_mat * (Math.sqrt(n_dimensions) / sigma)
end

# 特徴変換を行う。
def mapping(samples, transform_mat)
  n_samples, n_dimensions = samples.shape
  features = samples.dot transform_mat.transpose
  mapped = (features.sin.hconcat features.cos) * Math.sqrt(1.0 / n_dimensions.to_f)
end

これを、訓練データとテストデータに適応する。

# 訓練データを読み込む。
samples, labels = load_libsvm_file 'pendigits'

# 変換行列を計算し、特徴変換を行う。
trans_mat = orf_transform_mat samples
samples = mapping samples, trans_mat

...

# テストデータを読み込む。
test_samples, test_labels = load_libsvm_file 'pendigits.t'

# 特徴変換を行う。
test_samples = mapping test_samples, trans_mat

...

これで実験してみると、RFFのAccuracyが91.84%±1.41で、ORFのAccuracyが93.94%±0.67となった。 たしかに、ORFの方がわずかに良い。 データセットによりけりだろうけど、 元の特徴ベクトルのAccuracyが87.88%なので、カーネル近似による特徴変換により分類精度が向上することが確認できた。

おわりに

思いのほか長くなってしまったので、Liblinear-Rubyカーネル近似の話で、記事を分けたほうが良かった感。

はてなブログProにしても広告を消すのには設定が必要だった

はてなブログProにすれば勝手に広告が消えると思ってたら、設定が必要だった。だいたい3ステップぐらい必要だった。広告を消したのは「広告って読み込み重いから鬱陶しいよね」という軽い気持ちから。

  • 設定から、詳細設定タブを選択する。

f:id:yoshoku:20170730094852p:plain

  • 広告を非表示で、はてなによる広告を表示しない、を選択する。

f:id:yoshoku:20170730094908p:plain

  • 画面最下部の変更するボタンを押すことを忘れずに。

f:id:yoshoku:20170730094923p:plain

以上。※画像がドーンってデカくなったので記事を後で修正しました。

MacではRubyのNMatrixでOpenBLASを使わなくてもLAPACKで十分に速かった

はじめに

Ruby線形代数機械学習アルゴリズムの実装を行う場合、SciRubyのNMatrixが最適だろう。 このNMatrixで、 RやPythonのNumpyと同様に、後ろで叩いているBLAS/LAPACKを、OpenBLASやIntel MKLに変えることで、高速化することを考えた。 しかし、Macでは、BLAS/LAPACKはAccelerate Frameworkに含まれていて、OpenBLASに変えるまでもなく十分に速かった。 環境は、MacBookIntel Core m3 1.1Ghz、メモリ8G、macOS Sierra 10.12.6 である。

行列積での実行速度の計測

まずは、素のNMatrixと、MacBLAS/LAPACKを使う場合の実行速度を計測した。 インストールは、gemからもできるが、測定条件を一致させるため最新版をビルドする方法を採用した。

$ git clone https://github.com/SciRuby/nmatrix.git
$ cd nmatrix/
$ bundle install
$ bundle exec rake compile nmatrix_plugins=lapacke
$ bundle exec rake spec nmatrix_plugins=lapacke
$ bundle exec rake install nmatrix_plugins=lapacke

単純な行列積で、実行速度を計測する。行列積が速くなると色々と嬉しい。

require 'nmatrix'
require 'benchmark'

Benchmark.bm do |x|
  x.report do
    50.times do
      a = NMatrix.random [1000, 1000]
      b = NMatrix.random [1000, 1000]
      c = a.dot b
    end
  end
end

これを、実行すると以下の様になる。

$ ruby test_nmat.rb
       user     system      total        real
  53.790000   1.400000  55.190000 ( 56.935680)

さらにLAPACKプラグインの実行速度を確認するため、require ‘nmatrix'を、

require 'nmatrix/lapacke'

と変更する。これを実行すると、素のNMatrixと比較してグンと速くなった。

$ ruby test_nmat.rb
       user     system      total        real
  23.050000   1.090000  24.140000 ( 18.840261)

OpenBLASに差し替えた場合での実行速度

OpenBLASは、Homebrewでインストールできる。 「–build-from-source」や「–with-openmp」といったオプションも指定できるが、 何も指定しないものが、行列積では最も速かった。

brew install openblas

「ext/nmatrix_lapacke/extconf.rb」を編集して、OpenBLASを使用するようにする。

...
#ldefaults = {lapack: ["/usr/local/lib"].delete_if { |d| !Dir.exists?(d) } }
ldefaults = {lapack: ["/usr/local/opt/openblas/lib"] }
...
#$libs += " -llapack "
$libs += " -L/usr/local/opt/openblas/lib -lopenblas "
...

NMatrixを再インストールする。

$ gem uninstall namtrix nmatix-lapacke
$ bundle exec rake clean
$ bundle exec rake compile nmatrix_plugins=lapacke
$ bundle exec rake spec nmatrix_plugins=lapacke
$ bundle exec rake install nmatrix_plugins=lapacke

OpenBLASを叩くLAPACKプラグインという状態なので、

require 'nmatrix/lapacke'

とする。実行してみると、LAPACKの結果と大差ないものとなった。

$ ruby test_nmat.rb
       user     system      total        real
  27.400000   4.530000  31.930000 ( 17.920017)

特異値分解での実行速度の計測

以下の特異値分解を含むプログラムでも比較を行った。

require 'nmatrix/lapacke'
require 'benchmark'

Benchmark.bm do |x|
  x.report do
    50.times do
      a = NMatrix.random [200, 2000]
      b = a.dot a.transpose
      #
      b.gesvd
    end
  end
end

LAPACKを使用した場合では、

$ ruby test_nmat2.rb
       user     system      total        real
   6.040000   0.300000   6.340000 (  5.908795)

となり、OpenBLASを使用した場合では、

$ ruby test_nmat2.rb
       user     system      total        real
  27.220000   4.920000  32.140000 ( 16.595134)

となった。OpenBLASよりもLAPACKの方が速く、 MacではAccelerate FrameworkのBLAS/LAPACKを使用した方が良いことがわかる。

ちなみに、Intel MKLでも試したが、Accelerate FrameworkのBLAS/LAPACKの方が速かった。 また、Intel MKLでは、rake spec した際にコケてしまった。

おわりに

MacでNMatrixを使用する場合は、

gem install nmatrix nmatrix-lapacke

として、

require 'nmatrix/lapacke'

とすれば良い。

Pandasで「ビジネス活用事例で学ぶデータサイエンス入門」を勉強する(第6章)

はじめに

マーケティング寄りのデータ分析の知識を補うため、以下の本で勉強を始めた。事例ベースな内容で、とても読みやすい。 Pandasも習得したいので、Pandasに翻訳しながら読み進めている。今回は第6章を勉強した。

ビジネス活用事例で学ぶ データサイエンス入門

ビジネス活用事例で学ぶ データサイエンス入門

ちなみに、CSVファイルなどのデータは、本のサポートページ(SBクリエイティブ:ビジネス活用事例で学ぶ データサイエンス入門)で配布されている。

翻訳したコード

第6章では、重回帰分析を扱っている。作業はJupyter Notebook上で行った。回帰にはScikit-Learnを用いる。 本ではひとまず、散布図を出力して、テレビ広告・雑誌広告とアプリのインストール数との関係を見ている。

%matplotlib inline # Jupyter Notebookでmatplotlibの図が表示されないときにつけるおまじない
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression

# データを読み込む。
ad_data = pd.read_csv('ad_result.csv')
ad_data.head(2)

 

month tvcm magazine install
0 2013-01 6358 5955 53948
1 2013-02 8176 6069 57300

 

# 散布図の出力にはscatterメソッドを用いる。
# x軸とy軸は、データのヘッダにある列名で指定できる。
ad_data.plot.scatter(x='tvcm', y='install')
ad_data.plot.scatter(x='magazine', y='install')

f:id:yoshoku:20170716150546p:plain f:id:yoshoku:20170716150557p:plain

散布図から何かしらの相関があることが伺える。回帰分析によりモデル化する。

# Numpy形式でデータを取り出す。
X=np.array(ad_data[['tvcm', 'magazine']])
y=np.array(ad_data['install'])

# 回帰モデルを得る。
model=LinearRegression().fit(X, y)

# 回帰モデルの切片と係数を確認する。
#  インストール数とテレビ広告・雑誌広告には以下の関係があることがわかる。
#  「インストール数 = 188.174 + 1.361 x テレビ広告 +7.250 x 雑誌広告」
print(model.intercept_)
print(model.coef_)
# 188.17427483
# [ 1.3609213   7.24980915]

# 決定係数を確認する。
#  1に近いので、当てはまりが良いことがわかる。
print(model.score(X,y))
# 0.937901430104

別の選択肢としては、StatsModelのOLS(Ordinary Least Squares)を用いる方法がある。 StatsModelの回帰分析は、summaryメソッドを持っており、R言語のlm関数に似た結果を得られる。

from statsmodels.regression.linear_model import OLS
results=model = OLS(y, X).fit()
print(results.summary())
#                             OLS Regression Results                            
# ==============================================================================
# Dep. Variable:                      y   R-squared:                       1.000
# Model:                            OLS   Adj. R-squared:                  0.999
# Method:                 Least Squares   F-statistic:                     8403.
# Date:                Sun, 16 Jul 2017   Prob (F-statistic):           5.12e-14
# Time:                        16:15:12   Log-Likelihood:                -84.758
# No. Observations:                  10   AIC:                             173.5
# Df Residuals:                       8   BIC:                             174.1
# Df Model:                           2                                         
# Covariance Type:            nonrobust                                         
# ==============================================================================
#                  coef    std err          t      P>|t|      [0.025      0.975]
# ------------------------------------------------------------------------------
# x1             1.3540      0.405      3.347      0.010       0.421       2.287
# x2             7.2892      0.476     15.320      0.000       6.192       8.386
# ==============================================================================
# Omnibus:                        1.009   Durbin-Watson:                   0.876
# Prob(Omnibus):                  0.604   Jarque-Bera (JB):                0.804
# Skew:                           0.539   Prob(JB):                        0.669
# Kurtosis:                       2.123   Cond. No.                         14.0
# ==============================================================================

おわりに

6章から、機械学習に関係する話がでてくる。PandasというよりもScikit-Learnが活躍する感じ。7章にある「本当の意味での正解データがないなかで、なんらかの(ビジネス上意味のある)示唆を出さなければならない」という記述は、「うっ」という苦しい気持ちになる。