現在HakeのパラメータチューニングにはDEを使ってる。 要するに遺伝的アルゴリズム(交叉のさせ方が実数の場合にちょっと特殊なだけ)。 最初は一番最適化された並列化を書こうとしたが、自分の頭には敵わなかった。 こういう実行順序にすれば最適だろうという考えはあったがそれを実行させる実装が出来なかった。 というわけで、世代ごとに join するようにした。
pool = [] # 遺伝子プール
for gen in 0..NUM_GENERATION:
# この世代で行う予定のジョブキュー
jobs = [
random if gen == 0 else evolution_from(pool)
]
while job in jobs:
do(job) # 実際にはここで並列化数を指定してそれを超えないようにする
join() # ジョブを全て待つ
pool.sort()
pool.truncate(N) # 優秀な N 個だけに減らす
print(pool[0]) # 最も優秀な遺伝子
遺伝子プールのサイズ N
が並列化出来る個数に比べて小さい場合は世代ごとに join するのがネックになるけど、 普通、プールのサイズは50とかそれ以上あって、並列化できる個数というのはCPUの個数だから、いいマシンでも16とか32とかだろうから、まあそんなにネックにはならないんではないかと思っている。
トイデータでのサンプル。 2パラメータ X
Y
でロスを作ってこれを最小化するパラメータを探索させる。
# main.sh
Z=$( echo $1 $2 | tr - _ | dc -e '8k?d*rd*+p')
qj -e .metric=loss .value=$Z
# Hakefile
run:
bash ./main.sh $(X) $(Y)
ロスは \(x^2 + y^2\). 探索範囲は \(x, y \in [-3,3]\) とする. 従って最小値を取るのは \(x=y=0\) のとき.
$ hake X=-3...3 Y=-3...3 --min loss -j 16 -F 0.03 -N 64 -L 100
(中略)
Min loss = 1.005537340697927 when [("X", Float(1.002764848156282)), ("Y", Float(-0.00000019068281648616569))]
$ hake X=-3...3 Y=-3...3 --min loss -j 16 -F 0.03 -N 200 -L 10
(中略)
Min loss = 0 when [("X", Float(0.0)), ("Y", Float(0.0))]
うーん、DEに関するパラメータをいじっても上手く最適解を出せたり出せなかったり、、、 世代数 -L
だけじゃなくてプールサイズ -N
もかなり意味があるし、 あと -F
はいわば学習率みたいなもんで、ゼロに近い正数のほうが変な値で止まらない代わりに大きな世代数を要求するので学習時間が増える。