乱数降下法(3) 境界面を求める。

今回で乱数降下法(1) 関数推定 - 象徴ヶ淵rdmは一通り終わりである。ちなみにrdmがキーワードになってるけど、random descent methodの頭字語なので別物である。


今度は、測定点が空間内で、ある関係式で二分できるときに、その係数を決定させたい。実際、rdmを作ったのはこれがやりたかったためである。関数f(r,x,y,z)があって、いくつかの(r,x,y,z)の組に対して可、不可の2通りの値が与えられている。このとき、関数fを求めよという問題である。


これだけだと手がかりが少なすぎるが、関数の形は実際の条件から大体予想ができ、次のような形をしているとする。

(defun f4 (a b c)
  (lambda (r x y z) (if (< r (- (expt x a) (expt y b) (expt z c))) t nil)))

データではrの数値はマスクされていて、r_1,r_2と飛び飛びの値で与えられていることだけがわかっている。データ全部だと長くので載せないが、大体こんな感じのデータ列である。

f   r   x  y    z
t   r_1 57 1900 5
t   r_2 14 250  1
nil r_3 11 1570 6
....

そのため、r_iに対する(x,y,z)の組が可、不可になるかどうかで判定する。同じr_iに対して可、不可がどちらもある場合、f_{r_i}(x,y,z)の可となる最小値Tと不可となる最大値Fが交差しないように係数を決定する問題に変形する。また、rdmで評価するには探索空間が解の方向へ傾斜している方が良いので、T-Fを評価値とする。これが全て正になるような係数が求められればよい。ただし、rdmでは距離で評価するので、正になるときに評価値が0であるとする*1

(defun f5 (a b c)
  (lambda (success fail)
	(let* ((ev (lambda (x y z) (- (expt x a) (expt y b) (expt z c))))
		   (app (lambda (w) (apply ev w)))
		   (score (- (apply 'min (mapcar app success))
					 (apply 'max (mapcar app fail)))))
	  (min 0 score))))

テストケースはデータを整理して、((((x_t1 y_t1 z_t1) (x_t2 y_t2 z_t2)...) *2...)という形になる。r_iごとにまとめて関数にいれるので、r_iはテストケースには現れない。なんかややこしいが、ここまで出来れば関数に入れるだけである。

(rdm 'f5 test-case '((0 3)(0 3)(0 3)))
|(0 (0.9762377 0.1597804 1.774756))
|(0 (0.7801304 0.09487271 1.363998))

データが少ないので、結果にかなり幅が出る。とりあえず有効な係数が存在することが確認できたので、乱数降下法(2) 関数を解く - 象徴ヶ淵のように範囲を絞っていく。

(rdm 'f5 test-case '((0 2)(0 1)(0.5 3)) :mode '(18 2 10) :valid 0 :sample 100)
|((0.7105771 1.129198) (6.485849e-4 0.2458473) (1.203835 2.103973))

データから得られる情報はここまでである。sample数を増やせば、より範囲が広がる。係数の範囲を狭くするにはさらにデータを集めるしかない。しかし、実際に関数に係数を当てはめて運用したいので、最もそれらしい係数を探したい。例えば、2番目の係数の下限はほとんど0に近いが、これが新しいデータによく当てはまるとは思えない。ここからは自分の判断で適当に考えてみる。

(rdm 'f5 test-case '((1 1)(0.1 0.25)(1.2 2.2)) :mode '(18 2 10) :valid 0)
|(0 (1.0 0.2001176 1.810994))
(rdm 'f5 test-case '((1 1)(0.01 0.25)(2 2)) :mode '(18 2 10))
|(10.03671 (1.0 0.01000016 2.0))
(rdm 'f5 test-case '((1 1)(0.2 0.2)(1.2 2.2)) :mode '(18 2 10) :valid 0 :sample 100)
|((1 1) (0.2 0.2) (1.809875 1.831141))
(rdm 'f5 test-case '((1 1)(0.2 0.2)(1.80 1.80)) :mode '(0))
(0.2084866 (1.0 0.2 1.8))
(rdm 'f5 test-case '((1 1)(0.2 0.2)(1.82 1.82)) :mode '(0))
(0 (1.0 0.2 1.82))

まず、整数値を取る係数に着目して、その係数を固定する。特にx^1=xなので、これが採用できれば式が簡単になるので期待してみる。次に、a=1で問題がなかったので、c=2を試してみる。これは上手くいかないのであきらめる。


b=0.2とすれば1桁で決まるので、これも固定してcの範囲を調べる。次はc=1.8で決まるんじゃないかと予想して試したものである。このように範囲を固定して一度だけ調べればその係数の点数が出せる。ちなみに、modeに数値をそのまま入れると係数の個数が数値分あるときのモードになる。


こうして、a=1,b=0.2,c=1.81〜1.83くらいと求められた。ここではc=1.82を採用する。今回の元の関数は、人為的に作られたものなので、係数は簡単な有理数で表せると期待できるため、このような数値を選択した。cがあまり綺麗な数値ではないので、真の関数は別の形をしているかもしれない。しかし、考えられるいくつかの式を試してもいい結果にならないので、今回はこれで決定とする。最終的に求められた関数は

(defun f6 (x y z)
  (- x (expt y 0.2) (expt z 1.82)))

で、今度はこれを用いてr_iの値を推定する。そこまでまとめておけば、新しいデータが入ったとき、常に関数が正しいかチェックができるし、適合しない例があれば再度推定仕直すことも容易である。


と、実例を使ったので、詳細を知らない人は良く分からないかも知れないが、日記だからということで。

*1:効用最大を求めるなら評価値最大を求めるべきだとも考えたくなるが、それはデータからは読みとれない情報である。試してみると差になる係数同士で次第に大きくなっていくので有効でないことがわかる。本来は評価値は0,1の2通りになるが、今回の方法であれば0,>0と考えて2通りになるので結果を歪める心配がない。

*2:x_f1 y_f1 z_f2) ...