Rejectセキュリティ&プログラミングキャンプでは極度の体調不良*1により、あんまりコーディングしてる時間が取れなかったのですが、それでもなんとか自分がやろうと思っていたところができました。集合知プログラミングの11章「進化する知性」ということで、遺伝的アルゴリズムをやってみました。最初はRubyで書いてたんですが、Rubyのことをあんまり分かってないらしく、バグってるっぽい流れ*2。ということで手がちょっとは動くRで書いてみました。クラスとかを作らないと厳しい感じだったので、初めてのS4とかに挑戦してみたんですが、あんまりいいコードではない気がする。
この章でやっていることは、xとyに0から40までの一様乱数を用意して、の結果を予測する関数を生成するプログラムを書こう、ということです。
関数を生成するプログラム、ということで、「解析木」というものが必要になるそうです。解析木というのはこういうようなもの。
これを下の木から評価していきます。で、どんどん木の上に渡していって、最後に一つの値を返します。で、最初はランダムな木を生成して、それを突然変異とか、交叉とかを繰り返していきます。
突然変異
突然変異は一つの木を受け取って、それをちょっと変更するという操作をするものです。サブツリーがごっそリ変わる時もあるし、全く変更しない時もあります。が、突然変異が起こる割合はあんまり高すぎないほうがいいそうです。ただ、突然変異が起こらなすぎても、多様性が保てなくなるので、そこら辺の調整が必要なようです。
交叉
交叉というのは、二つの木を受け取って、それらを組み合わせて、新しい木を作るという操作です。あとで出てきますが、この二つの木はなるべく成功しているものが組み合わせられます。成功しているもの同士を組み合わせると、もっといいものができるだろう、という予想に基づいているようです。進化論的とかそういう感じなんでしょうか、よく分からない。多様性
例えば、下のような関数の最大化を考えましょう。右の赤が最もよいところです。x軸とy軸の二次元平面であると考えます。x軸の値をちょっとづつずらしていって、最大になる点というのを見ていくんですが、左の赤の最大と見なしてしまうことがあります。これを「局所最大に逹する」といいます。わりといいんだけど、一番よいというわけではない、という感じです。低次元なら、こういう図が書けるので、いいんですが、高次元だと図とか書けないので、色々な最適化の手法が考えられています。
遺伝的アルゴリズムで局所最適に陥いらないようにするための方法が、突然変異です。なんだろう、さっきの図で行くと、あるの周辺でよい点を探していたんだけど、突然そこから離れた点もよいものにならないか見にいくというような感じです。交叉は近くでいい点を探しに行くような操作と見ることもできますかね。
数学的関数を再構築する
一様乱数X、Yがあった時に、に適用したものと、遺伝的アルゴリズムで構築された解析木で計算した結果の絶対値の和を考えます。解析木で構築されたものが、元の関数をうまく表現できれいれば、この絶対値の和は0になります。0になるか、規定のループ回数を終えても0にならない時終了となって、最後に木の構造が出力されます。例えばこんなのが出力される。
add add add if subtract p1 add p1 p0 p1 p1 5 add p1 subtract p0 subtract subtract subtract isgreater p0 p0 p0 0 p0 multiply p0 p0
この木をくずしていくとこんな数式が得られます。あれ、微妙に違うんだけど、絶対値の和は0になっているらしい。。。
x^2 + 4x + y + 5
元の関数がなので、まあ似ているかなという感じです。どんぴしゃじゃない時もあるのかな*3。
感想
遺伝的アルゴリズムを使って、なんとかフィルタリングみたいなのをやりたいなーと思っていたんですが、それにしては時間がかかりすぎます。二重のforの中で再帰を呼びまくっているので、まあ仕方ないと言えば仕方ないのか*4。半日とか普通にかかってしまう計算量です。なので、遺伝的アルゴリズムを使うコストに見合うものに適用するとかがいいのかなーと思います。リアルタイムでフィルタリングとかは明かに間に合いません。そういう場合は単純な計算な割りに結構精度が出るナイーブベイズとかのほうがいいようです。
- 遺伝的アルゴリズムは(この場合は)問題の性質とかを見ずに色々な問題に対応できる
- でも、時間はかかる
- その他の手法は問題の性質に着目することで計算を早くしている
- しかし、適用できるリージョンは狭くなってしまう
ということで、全ての側面でよいアルゴリズムとかはないんだよねーという当たり前の性質を再確認するということになりました。
- 作者: Toby Segaran,當山仁健,鴨澤眞夫
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/07/25
- メディア: 大型本
- 購入: 91人 クリック: 2,220回
- この商品を含むブログ (277件) を見る
以下ソースを載せていきます。
ソースコード
最新のコードはgithubに上げてあります。なんかファイルがごちゃごちゃしているけど、気にしないでくださいwww。Rのコード
S4のところがよく分からないのですが、特にこれがないとevaluate.node関数とかが定義できないのがよく分からない。Rの基礎とプログラミング技法とかを見るといいのかなー?Rプログラミングマニュアル (新・数理工学ライブラリ 情報工学)もよい本だけど、この辺はあんまり載っていない。Rjpwikiの例もあんまりよく分からないし。。。
setGeneric("evaluate", function(this,inp) standardGeneric("evaluate")) setGeneric("display", function(this,indent=0) standardGeneric("display"))
Rだと
- 関数オブジェクトを自然に扱えるのが非常によい
- データフレームや行列に任意のクラスのインスタンスをぶちこめないのが、ちょっとだめかな?
- おかげでlistとして扱わないといけないので、コードが見づらくなってしまう
- 評価値ともとのnodeを入れておきたいんだけど、それができない。RubyとかPythonの配列ではもちろんできるんだけど
- よい解決方法を知っている人は教えてください
という感じですね。一長一短。ということで、Rのソースコード。
#関数のラッパークラス setClass("fwrapper", representation(f="function", childcount="numeric",name="character")) #何かよく分からないけど、必要っぽい #ToDo:後で調べる setGeneric("evaluate", function(this,inp) standardGeneric("evaluate")) setGeneric("display", function(this,indent=0) standardGeneric("display")) #nodeクラスの定義 setClass("node", representation(fw="fwrapper", children = "list")) setGeneric("evaluate.node", function(this,inp) standardGeneric("evaluate.node")) setMethod("evaluate","node",function(this,inp){ result <- unlist(Map(function(x){evaluate(x,inp)},this@children),recursive=FALSE) return(this@fw@f(result)) }) setGeneric("display.node", function(this,indent) standardGeneric("display.node")) setMethod("display","node",function(this,indent){ cat(paste(Reduce(function(init,x){paste(init,x,sep="")},rep(" ",indent),""),this@fw@name,sep=""),fill=TRUE) for(c in this@children){ display(c,indent+1) } }) #paramnodeクラスの定義 setClass("paramnode", representation(idx = "numeric")) setGeneric("evaluate.paramnode",function(this,inp){ standardGeneric("evaluate.paramnode") }) setMethod("evaluate","paramnode",function(this,inp){ #PythonとRのoriginの違いを吸収するために1を足す return(inp[this@idx +1]) }) setGeneric("display.paramnode",function(this,indent){ standardGeneric("display.paramnode") }) setMethod("display","paramnode",function(this,indent){ cat(sprintf("%sp%d", Reduce(function(init,x){paste(init,x,sep="")},rep(" ",indent),""), this@idx) ,fill=TRUE) }) #constnodeクラスの定義 setClass("constnode", representation(v = "numeric")) setGeneric("evaluate.constnode",function(this,inp){ standardGeneric("evaluate.constnode") }) setMethod("evaluate",signature(this="constnode"),function(this,inp){ return(this@v) }) setGeneric("display.constnode",function(this,indent){ standardGeneric("display.constnode") }) sprintf("%d%s",0,"hoge") setMethod("display","constnode",function(this,indent){ cat(sprintf("%s%d", Reduce(function(init,x){paste(init,x,sep="")},rep(" ",indent),""), this@v), fill=TRUE) }) #関数のラッパークラスのインスタンス群 addw <- new("fwrapper",f=function(x){return(x[1]+x[2])},childcount=2,name="add") subw <- new("fwrapper",f=function(x){return(x[1]-x[2])},childcount=2,name="subtract") mulw <- new("fwrapper",f=function(x){return(x[1]*x[2])},childcount=2,name="multiply") ifw <- new("fwrapper",f=function(x){ return(ifelse(x[1] > 0,x[2],x[3])) },childcount=3,name="if") gtw <- new("fwrapper",f=function(x){ return(ifelse(x[1] > x[2],1,0)) },childcount=2,name="isgreater") flist <- c(addw,mulw,subw,ifw,gtw) exampletree <- function(){ return(new("node",fw=ifw, children=c( new("node",fw=gtw,children=c( new("paramnode",idx=0),new("constnode",v=3) )), new("node",fw=addw,children=c( new("paramnode",idx=1),new("constnode",v=5) )), new("node",fw=subw,children=c( new("paramnode",idx=1),new("constnode",v=2) )) ) )) } makerandomtree <- function(pc,maxdepth=4,fpr=0.5,ppr=0.6){ if((runif(1) < fpr) && (maxdepth > 0)){ f <- sample(flist,1)[[1]] children <- Map(function(x){makerandomtree(pc,maxdepth-1,fpr,ppr)},seq(f@childcount)) return(new("node",fw=f,children=children)) }else if(runif(1) < ppr){ return(new("paramnode",idx=sample(0:(pc-1),1))) }else{ return(new("constnode",v=sample(0:10,1))) } } hiddenfunction <- function(x,y){ return(x^2 + 2*y + 3*x +5) } buidhiddenset <- function(){ rows <- matrix(0,200,3,byrow = TRUE) for(i in seq(200)){ x <- sample(0:40,1) y <- sample(0:40,1) rows[i,] <- c(x,y,hiddenfunction(x,y)) } return(rows) } scorefunction <- function(tree,s){ dif <- 0 for(i in seq(nrow(s))){ v <- evaluate(tree,c(s[i,1],s[i,2])) dif = dif + abs(v-s[i,3]) } return(dif) } mutate <- function(t,pc,probchange=0.1){ if(runif(1) < probchange){ return(makerandomtree(2)) }else{ result <- t if(class(t)=="node"){ result@children <- Map(function(c){mutate(c,pc,probchange)},t@children) } return(result) } } crossover <- function(t1,t2,probswap=0.7,top=1){ if((runif(1) < probswap) && (top != 0)){ return(t2) }else{ result <- t1 if((class(t1)=="node") && (class(t2)=="node")){ result@children <- unlist(Map(function(c){crossover(c,sample(t2@children,1))},t1@children),recursive=FALSE) } return(result) } } evolve <- function(pc,popsize,rankfunction,maxgen=50,mutationrate=0.1,breedingrate=0.4,pexp=0.7,pnew=0.05){ selectindex <- function(){ #配列のindexが0にならないように調整 return(round(log(runif(1)) / log(pexp)) + 1) } population <- Map(function(x){makerandomtree(pc)},seq(popsize)) for(i in seq(maxgen)){ scores <- rankfunction(population) cat(scores[[1]][[1]],fill=TRUE) if(scores[[1]][[1]] == 0){ break } newpop <- c(scores[[1]][[2]],scores[[2]][[2]]) while(length(newpop) < popsize){ if(runif(1) > pnew){ newpop[[length(newpop) + 1]] <- mutate(crossover(scores[[selectindex()]][[2]], scores[[selectindex()]][[2]], probswap=breedingrate), pc,probchange=mutationrate) }else{ newpop[[length(newpop) + 1]] <- makerandomtree(2) } } population <- newpop } display(scores[[1]][[2]]) return(scores[[1]][[2]]) } getrankfunction <- function(dataset){ rankfunction <- function(population){ #任意のデータ型を入れられるのは、listのみなので、仕方なくlistでscoreを生成 s <- c() p <- list() for(i in seq(length(population))){ s[i] <- scorefunction(population[i][[1]],dataset) p[i] <- population[[i]] } l <- c() n <- 1 for(i in order(s)){ l[[n]] <- c(s[i],p[[i]]) n <- n + 1 } return(l) } return(rankfunction) } rf <- getrankfunction(buidhiddenset()) e <- evolve(2,500,rf)
Rubyのコード
それっぽくは動きますが、PythonやRのやつに比べると収束がいまいちすぎるので、どっかばぐってると思われます。deep copyの付近が怪しいような気がします。
require 'pp' class Fwrapper attr_accessor :function attr_accessor :childcount attr_accessor :name def initialize(function,childcount,name) @function = function @childcount = childcount @name = name end def deep_copy() return self.class.new(Proc.new{@function}, Marshal.load(Marshal.dump(@childcount)), Marshal.load(Marshal.dump(@name))) end end class AbstractNode #あったほうが安心した def evaluate() end def display() end def deep_copy() end end class Node < AbstractNode attr_accessor :function attr_accessor :name attr_accessor :children def initialize(fw,children) @function = fw.function @name = fw.name @children = children end def evaluate(inp) results = @children.map{|n| n.evaluate(inp)} return @function.call(results) end def display(indent=0) puts ' '*indent + @name @children.each{|c| c.display(indent+1) } end def deep_copy() #self.childrenにもFwrapperオブジェクトが入る場合があるので、再帰的にdeep copyをやっていく function = @function childcount = Flist.map{|x|x.childcount if x.name == @name}.compact name = @name return self.class.new(Fwrapper.new(function,childcount,name),deep_copy_children) # return self.class.new(Fwrapper.new(function,childcount,name).deep_copy,deep_copy_children) end def deep_copy_children() return @children.map{|c| if c.class == Node c.deep_copy else c.deep_copy #Marshal.load(Marshal.dump(c)) end } end end class Paramnode < AbstractNode attr_accessor :idx def initialize(idx) @idx = idx end def evaluate(inp) return inp[@idx] end def display(indent=0) puts ' '*indent + @idx.to_s end def deep_copy() Marshal.load(Marshal.dump(self.class.new(@idx))) end end class Constnode < AbstractNode attr_accessor :v def initialize(v) @v = v end def evaluate(inp) return @v end def display(indent=0) puts ' '*indent + @v.to_s end def deep_copy() Marshal.load(Marshal.dump(self.class.new(@v))) end end module MyMethods Addw = Fwrapper.new(lambda{|l|l[0]+l[1]},2,'add') Subw = Fwrapper.new(lambda{|l|l[0]-l[1]},2,'subtract') Mulw = Fwrapper.new(lambda{|l|l[0]*l[1]},2,'multiply') def iffunc(l) if l[0] > 0 return l[1] else return l[2] end end module_function :iffunc Ifw = Fwrapper.new(self.method(:iffunc),3,'if') def isgreater(l) if l[0] > l[1] return 1 else return 0 end end module_function :isgreater Gtw = Fwrapper.new(self.method(:isgreater),2,'isgreater') Flist = [Addw,Mulw,Ifw,Gtw,Subw] end def exampletree() include MyMethods return Node.new(Ifw,[ Node.new(Gtw,[Paramnode.new(0),Constnode.new(3)]), Node.new(Addw,[Paramnode.new(1),Constnode.new(5)]), Node.new(Subw,[Paramnode.new(1),Constnode.new(2)]), ]) end def makerandomtree(pc,maxdepth=4,fpr=0.5,ppr=0.6) include MyMethods if rand < fpr and maxdepth > 0 f = Flist[rand(Flist.size)] children = (1..f.childcount).map{makerandomtree(pc,maxdepth-1,fpr,ppr)} return Node.new(f,children) elsif rand < ppr return Paramnode.new(rand(pc-1)) else return Constnode.new(rand(10)) end end def hiddenfunction(x,y) return x**2 + 2*y + 3*x +5 end def buidhiddenset() rows = [] (1..200).each{|i| x = rand(40) y = rand(40) rows.push([x,y,hiddenfunction(x,y)]) } return rows end def scorefunction(tree,s) dif = 0 s.each{|data| v = tree.evaluate([data[0],data[1]]) dif += (v - data[2]).abs } return dif end def mutate(t,pc,probchange=0.1) if rand < probchange return makerandomtree(pc) else result = t.deep_copy if t.instance_variable_defined?("@children") result.children = t.children.each{|c|mutate(c,pc,probchange)} end return result end end def crossover(t1,t2,probswap=0.7,top=1) if rand < probswap and top == 0 return t2.deep_copy else result = t1.deep_copy if t1.instance_variable_defined?("@children") && t2.instance_variable_defined?("@children") choice = t2.children[rand(t2.children.length)] result.children = t1.children.map{|c|crossover(c,choice,probswap,0)} return result else return result end end end def selectindex(pexp) return rand((Math::log(rand) / Math::log(pexp)).to_i + 1) end def myselectindex(length) sum = 0.0 (1..length).each{|i|sum += 1.0 / (i**2)} h = Hash.new tmp = 0 (1..length).each{|i| tmp += (1.0 / (i**2)) / sum h[i] = tmp } i = 0 r = rand h.sort.map{|x|x[1]}.each_with_index{|item,index| i = index if item > r break end } return i end def evolve(pc,popsize,rankfunction,maxgen=500,mutationrate=0.2,breedingrate=0.1,pexp=0.7,pnew=0.1) population = (1..popsize).map{makerandomtree(pc)} scores = Array.new(Array.new) (1..maxgen).each{ scores = rankfunction.call(population) if scores[0][0] == 0.0 break end puts scores[0][0] newpop = [scores[0][1],scores[1][1]] while newpop.length < popsize if rand > pnew l = scores.length newpop.push mutate(crossover(scores[myselectindex(l)][1], scores[myselectindex(l)][1], probswap=breedingrate), pc,probchange=mutationrate) else newpop.push makerandomtree(pc) end end population = newpop } scores[0][1].display return scores[0][1] end def getrankfunction(dataset) return lambda{|population| population.map{|t|[scorefunction(t,dataset),t]}.sort{|x,y|x[0] <=> y[0]}} #配列の配列のsortだから注意が必要。最初の要素が同じだと後のでも比較するけど、後のが比較可能でないといけない end