Ccmmutty logo
Commutty IT
10 min read

Unityで対話型進化計算(Interactive Evolutionary Computation, IEC)を試してみた

https://cdn.magicode.io/media/notebox/6d641d54-9ff7-4910-9fa3-3ffed3a897aa.jpeg

はじめに

お酒の勢いで買ったUnity シミュレーションで学ぶ人工知能と人工生命-- 創って理解するAI--を読んでみて、対話型進化計算(Interactive Evolutionary Computation, IEC)に興味が湧いたのでサンプルを動かしてみました。
「見たものの美しさ」などの人間の主観的な評価をコンピューター上で動作させるのは難しいが、人間ならできる。このことを利用して、人間を評価関数としてシステムに組み込む手法として、対話型進化計算(Interactive Evolutionary Computation, IEC)が考案されました。 本書では、そのサンプルとして球体に色づけする対話システムを実装しています。好みの球体を選択して世代を進化させていくと、徐々に自分の好みの球体に近づいていきます。
これを見てふと思い出しました。遺伝的アルゴリズムで最高にエッチな画像を作ろう!のことを。 とても興味深い企画でしたね。 本書のサンプルでは遺伝的プログラミングが用いられていましたが、根本的な概念は同じです。
また、普段から試しに作ってみたいと思っても、評価関数を用意しにくいテーマだったゆえに頓挫してしまうことが多かったので、そこへの解決策としても期待できます。
ということで、サンプルを動かしてみました。

環境

  • PC
    • windows 10
  • unity 2021.3.14f
    • もともとインストールされていたので

とりあえず動かしてみる

  1. 本のサポートページから「球体に色付けしよう(5.6節)」のプログラムをダウンロード。
  2. zipを解凍
  3. unity で「Interactive Evolution GP-copy」を開く
    • 私はunity hubを使っているので、unity hubで Projects/Open/Add project from disk で追加
    • もともとはunity 2020.3.5f1で作られていたみたいだが、自分の環境にそのversionをインストールしていなかったので、試しに2021.3.14f1で開いてみた
    • Build TtargetがMac Stand Aloneになってるよという言われるので、switch targetをする
  4. Assets/Scenes/SampleSceneが開いていることを確認し、実行する
    • Game View の解像度はFull HDにしておくと良い
  5. 自分の好みに合う球を選択し、OKボタンを押す。を繰り返す。

中身解説

Hierarchy

まずは3Dシーンの階層構造を軽く説明します。
  1. UI
    • UIはいわゆるuguiを使っていて、Canvas以下にコンポーネントがある
    • Main CameraがUIを表示する役を担っている
    • ボタンクリックなどの管理は、プログラム上から自作のEventManagerを通すようにしている模様
    • 球のところで後述するが、球の表示はUIのコンポーネントに紐づけてはいない。
    • 遺伝的プログラミングで生成したTextureを張り付ける球
    • アタッチされているスクリプトで回転させられる
    • 球ごとにカメラがいい感じに映るように配置されている
    • このカメラのview port rectをいい感じに調整することで、UI上でいい感じに表示させている
      • 最初Render TextureにしてImageとして張り付けてると思ってたけど違って混乱した
  2. 遺伝的プログラミング
    • GPMangerにアタッチされているGPManage.csで管理されている
    • 生成したテクスチャを張り付けるための球のMesh Rendererをinspectorで指定している

GPManager

遺伝的プログラミング

遺伝的プログラミング(Genetic Programing)の詳細な解説は、専門の本やWebの記事にお任せします。
ここでは、入力となる変数や定数とそれらを変換する関数を組み合わせて木構造を作る仕組み、ぐらいに思ってください。 遺伝的 はこの木構造を作るときに突然変異や交叉といった進化論の仕組みを取り入れていることに由来します。遺伝的に組み合わせを変えていくので、勾配降下法のように「評価関数で定量的に評価して、微分して、...」といったことをせずに学習を進められます。

GPManagerの構成

GPは前項で述べたように、変数・定数とそれらに処理を加える関数からなる木構造を作る仕組みです。
このサンプルは、この仕組みを使って、テクスチャのピクセル(x,y)からHSVの3次元ベクトルを生成する木構造を作り、HSVからRGBに変換してテクスチャを生成するプログラムです。
GPの実装にはEvolutionary.Netを拡張して利用しています。C#などの静的型付け言語とは相性が悪そうな中、Unityだけで動かせるよう頑張ってもらっています。
GPManagerは大きく4つの処理をしています。

①初期化

Start()内でInitiailze()を呼び出し、Evolutionary.NetのEngineクラスからBrainというインスタンスを生成し、進化計算に必要な初期化を行い(詳細は後述)、②で初回のテクスチャを表示します。
また、クリックアクションの初期化もしています。

②Texture生成

二重のループを回して1ピクセルの座標ごとにBrainフィールドからHSVを生成し、RGBに変換してTextureを作り、球のMeshRenderareに張り付けています。

③ボタン処理

球がクリックされたら、selectedフィールドに押された球のindexを詰め込みます。(すでに入ってたら取り除く)
OKボタンがクリックされたら、④の進化計算をしたのちに、②のテクスチャ生成をします。

④進化計算

selectedフィールドを参照して、選択されている球に該当する木構造の適合度(fitness)を1に、選択されていないものの適合度を0にして、次の世代に進化させます。
ここの処理はIEC用に拡張したコードがEngineクラスに追加されています。

GP(Evolution.Netでの)初期化処理詳細

初期化処理は5つの処理で構成されています。

①Engineクラスのインスタンス生成

Engineクラスのインスタンスを生成し、Brainフィールドに格納しています。
Engineクラスではジェネリクス(c++とかだとテンプレート)を利用しています。(ここでは<Vector3, ProblemState>)
githubのREADMEと挙動を見た感じ、ノードへの入出力は同じ型になるようで、このサンプルのケースではVector3が木構造のノードへの入出力になります。
due to closure, all nodes are the same type - float, in this case
ProgramStateに関しては、ターミナル関数を使うときにstateを確認できるようにするよってことらしいが、今回は使っていないので空のクラスがあるだけです。(詳細はよくわかっていない)
// save state data in an object of this type before evaluating the expression tree
// terminal functions can then look at it, and they or stateful functions can modify it, if desired
//GP全体を司るクラスのインスタンスを定義
        Brain=new Engine<Vector3, ProblemState>();

②変数名を定義

入力で渡す変数名を定義してます。
木構造にデータを渡して計算する際に、SetVaribaleValueの第一引数で使います。( 「現在のGP木(currentCandidate)を実行する仕組みを作る」を参照)
また、木構造の様子を文字列にした際にも使われます。
ex. Tree0: Lerp(Mul(sin(0XY),exp(0YX)),Div(Min(XY0,X0Y),Lerp(0YX,Y0X)))
今回のケースでは、
「入力としては画像ピクセルのx,yの二次元だが、出力をHSVの3次元にしたい」ということで入力のxyに0を加えて3次元(インスタンス生成のとこで説明した通り、データの型を一致させる必要があるので)にしています。
さらに、x,y,0の3要素から3次元ベクトルを作るとしたら、3x2=6通りつくれるので、その6通りを変数としています。
(別に"XY0"の変数だけ使うでもエラーなく動作させられます。なんで6通りにしたかの説明は本になかったのですが、複雑性をあげたかったのかな?と個人的に解釈しています)
//GPの変数名を定義
        Brain.AddVariable("XY0");
        Brain.AddVariable("X0Y");
        Brain.AddVariable("YX0");
        Brain.AddVariable("Y0X");
        Brain.AddVariable("0XY");
        Brain.AddVariable("0YX");

③関数を定義

足し算とか掛け算とか、Vector3を入力にVector3を返す処理をラムダ式で登録しています。
これらが木構造のノードになります。
//GPで使う演算子を定義
        //unary
        Brain.AddFunction((a) => new Vector3 (Mathf.Abs(a.x),Mathf.Abs(a.y),Mathf.Abs(a.z)), "abs");
        Brain.AddFunction((a) => new Vector3 (CustomLog(a.x),CustomLog(a.y),CustomLog(a.z)), "Log");
        Brain.AddFunction((a) => new Vector3 (Mathf.Exp(a.x),Mathf.Exp(a.y),Mathf.Exp(a.z)), "exp");
        Brain.AddFunction((a) => new Vector3 (Mathf.Sin(a.x),Mathf.Sin(a.y),Mathf.Sin(a.z)), "sin");
        Brain.AddFunction((a) => new Vector3 (Mathf.Cos(a.x),Mathf.Cos(a.y),Mathf.Cos(a.z)), "cos");
        Brain.AddFunction((a) => new Vector3 (Sqrt(a.x),Sqrt(a.y),Sqrt(a.z)), "sqrt");
        //Brain.AddFunction((a) => new Vector3 (Mathf.Sign(a.x),Mathf.Sign(a.y),Mathf.Sign(a.z)), "sign");
        Brain.AddFunction((a) => -1.0f*a, "Reverse");
        
        //binary
        Brain.AddFunction((a, b) => a + b, "Plus");
        Brain.AddFunction((a, b) => a - b, "Minus");
        Brain.AddFunction((a, b) => new Vector3(a.x*b.x,a.y*b.y,a.z*b.z), "Mul");
        Brain.AddFunction((a, b) => new Vector3(CustomDiv(a.x,b.x),CustomDiv(a.y,b.y),CustomDiv(a.z,b.z)), "Div");
        Brain.AddFunction((a, b) => Vector3.Max(a,b), "Max");
        Brain.AddFunction((a, b) => Vector3.Min(a,b), "Min");
        //Brain.AddFunction((a, b) => new Vector3(CustomPow(a.x,b.x),CustomPow(a.y,b.y),CustomPow(a.z,b.z)), "Pow");
        Brain.AddFunction((a, b) => new Vector3(Hypot(a.x,b.x),Hypot(a.y,b.y),Hypot(a.z,b.z)), "Hypot");
        Brain.AddFunction((a, b) => Vector3.Lerp(a,b,0.5f), "Lerp");
        Brain.AddFunction((a, b) => new Vector3(Mix(a.x,b.x),Mix(a.y,b.y),Mix(a.z,b.z)), "Mix");

④現在のGP木(currentCandidate)を実行する仕組みを作る

github上にはないのでおそらく独自の拡張。
ピクセルの座標となるx,yを入力して、GP木に用意した6通りの変数を格納し、計算結果を返す。
(たぶんstaticメソッドである必要はない。)
public static Vector3 ScanTree(CandidateSolution<Vector3, ProblemState> candidate, Vector2 pos){
        //GPの変数ノードに実際に値を代入。x座標、y座標、0を並び替えた6通りのベクトル。
        candidate.SetVariableValue("XY0",new Vector3(pos.x,pos.y,0));
        candidate.SetVariableValue("X0Y",new Vector3(pos.x,0,pos.y));
        candidate.SetVariableValue("YX0",new Vector3(pos.y,pos.x,0));
        candidate.SetVariableValue("Y0X",new Vector3(pos.y,0,pos.x));
        candidate.SetVariableValue("0XY",new Vector3(0,pos.x,pos.y));
        candidate.SetVariableValue("0YX",new Vector3(0,pos.y,pos.x));

        //GPの1個体の木の計算結果を取得
        return candidate.Evaluate();
    }

    void Initialize(){
        ...

        //GP木の評価を定義
        Brain.AddScanTreeFunction((c, p) => ScanTree(c, p));
        ...
   }

⑤最初のテクスチャを作る

//個体の生成など、GPを初期化
        Brain.InitTrainer();
        //GP木をもとにテクスチャを生成して貼り付け
        GetTextureFromTree();

終わりに

Unityで遺伝的プログラミング(GP)を用いた対話型進化計算(IEC)のサンプルを動かしてみました。
サンプルとしてUnityで完結できるようになっていますが、GPの部分はpythonとかの方がよさそうだなーと思いましたが、初めて触ってみる分にはやりやすかったです。
対話的に自分で評価できるのは、色々正解データや評価関数作るのに悩まなくて済みますね。一方、学習結果の精度・品質を出すとかは大変そうかなと思うので、なんとなくの値をルールとか正解とかにとらわれずに出してくれる対話システムに向いていると思います。

Discussion

コメントにはログインが必要です。