機械学習アルゴリズムで実数を保持する場合のfloat型とdouble型の比較

機械学習アルゴリズムC++等で実装する場合には型をどうするか悩む.きっかけはオープンソースsvm_lightやLIBSVMはふつうにdouble型だったのに対し,ollではfloat型で実装されていたから.「あれ,別にdoubleほどの精度要らないの?」「floatの方が半分のメモリ量で済むし,キャッシュも効くから高速になるのかな」というようなことを考えていた.

そのまますっかり忘れていたのだけれど,GW中の課題のひとつとして検証してみることにした.

以下の2つについてfloat型とdouble型で比較する.面倒なのでぜんぶfloat型かぜんぶdouble型か.

  • 訓練データ事例の特徴量 (素性) の型
  • 重みベクトル等,すべてのパラメータの型

それぞれについて以下の3つの値を比較する.

  • 実行速度
  • モデル精度
  • 使用メモリ量

なお,実験に利用したマシン環境は以下のとおり

  • Core2Duo E8500 (3.16GHz)
  • 3GB Memory
  • gcc 4.3.2 (-O3オプション)
  • Linux kernel 2.6.26

実験に利用したアルゴリズムSVM学習法のひとつであるSMO.詳細については明日のPRML勉強会で発表予定 (予定) なので,詳しくはそちらを参照.

実行速度

以下の3ロジックの時間をgettimeofday(2)を利用して測定.LIBSVM datasetのdiabetes_scaleを利用.

  • LIBSVM形式のファイルを読み込む
  • 訓練事例からパラメータを推定
  • モデルファイルを書き出す

10回測定した結果の平均と標準偏差を記録.

  • 結果
 float: 4.1940 (±0.0032) sec.
double: 4.2566 (±0.0026) sec.

わずかにfloatの方が速い.

モデル精度

どの程度モデルに差が生まれるのかどう比較してよいかわからなかったので (SVMの場合はサポートベクタとそれに対する重みの値で比較すればよいのだろうか) & 面倒なので訓練データに対する精度で比較した.

カーネルパラメータ等の条件は書かないが,同じ値を利用している.

  • 結果
* Gauss kernel
 float: 0.7565
double: 0.7539

* Linear kernel
 float: 0.6927
double: 0.6680

なんとfloat型の方が精度が高くなっている.これはどういうことだろうか.今回の実装では目的関数や停止条件もfloat型になっているため,停止条件による収束判定の差かもしれないが,ちょっと考えづらい.なお,libsvm-3.17でGauss kernelと同条件でモデル構築したところaccuracyが0.7813であったため,SMO実装の誤りがあるのかもしれない.ということでこの結果は棚上げ.

使用メモリ量の比較

実測するまでもなく2倍になるはず.メモリを解放する前に標準入力待ち状態にして/proc//status を眺める方法で測定.VmDataを眺めると2308kB vs. 4316kBと約2倍になっている.自分用のメモのため,すべての値を記録

  • float型
Name:   smo_train
State:  S (sleeping)
Tgid:   13440
Pid:    13440
PPid:   13356
TracerPid:      0
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
FDSize: 32
Groups: 20 24 25 29 44 46 1000 
VmPeak:     5080 kB
VmSize:     5080 kB
VmLck:         0 kB
VmHWM:      3328 kB
VmRSS:      3328 kB
VmData:     2308 kB
VmStk:        88 kB
VmExe:        60 kB
VmLib:      2568 kB
VmPTE:        16 kB
Threads:        1
SigQ:   0/32768
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000000000000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: ffffffffffffffff
Cpus_allowed:   0f
Cpus_allowed_list:      0-3
Mems_allowed:   1
Mems_allowed_list:      0
voluntary_ctxt_switches:        1
nonvoluntary_ctxt_switches:     51
  • double型
Name:   smo_train
State:  S (sleeping)
Tgid:   13393
Pid:    13393
PPid:   13356
TracerPid:      0
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
FDSize: 32
Groups: 20 24 25 29 44 46 1000 
VmPeak:     7088 kB
VmSize:     7088 kB
VmLck:         0 kB
VmHWM:      5408 kB
VmRSS:      5408 kB
VmData:     4316 kB
VmStk:        88 kB
VmExe:        60 kB
VmLib:      2568 kB
VmPTE:        20 kB
Threads:        1
SigQ:   0/32768
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000000000000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: ffffffffffffffff
Cpus_allowed:   0f
Cpus_allowed_list:      0-3
Mems_allowed:   1
Mems_allowed_list:      0
voluntary_ctxt_switches:        2
nonvoluntary_ctxt_switches:     70

voluntary_ctxt_switchesは自発的にコンテキストスイッチを発生した数.nonvoluntaryは強制的なコンテキストスイッチ発生回数.コンテキストスイッチの数もここに記述されているのを知らなかった.sleep状態にして待ってしまっているのでcontext switchが発生しているので今回の場合は特に意味はないはず (もういちど実行したらdouble型のvoluntaryが1になったので).

結論

ちゃんと比較をしなかったけれど

  • 速度はたいして速くならない.
  • 精度はデータセット依存.数値誤差を気にしないならfloatでいいのかも.
    • 今回の実験結果からはちょっとなんともいえない
  • メモリ量は文字通り倍つかうので増える.

さきに書かなかったけれど本件は事例のfeature vectorをどのように実装するかということにも深く関係している.これについては[1]や昔のブログ記事などが参考になるかも.

「メモリ量が気にならないならdouble型でいいんじゃないかな」というのをしばらく自分ルールにしてみよう.

まずはSMOの実装チェックと明日のPRML勉強会の準備からしなければ...