Unityで遺伝アルゴリズム用ライブラリ "GeneticSharp" を使う

## 本記事の概要
本記事では、C#向け遺伝アルゴリズム用ライブラリ "GeneticSharp" に関して

- Unity上に導入する方法
- Unity上でのGeneticSharpの使い方
- 遺伝アルゴリズムを使って簡単な問題を解く方法

をまとめています。
## 遺伝アルゴリズムとは？
本記事では詳細の解説はしませんが「進化的計算」の一つです。
最適化を図りたいパラメタの集合を生物の遺伝子に見立て、ある状況・環境での適応度が高い遺伝子を次の世代に残していくという形で遺伝子（パラメタ）を進化をさせることで最適なパラメタの組み合わせを探索します。
ある状況・環境での適応度というのは例えば

- ゲーム：得点や対戦での勝敗
- 乗り物：速さ、燃費、安全度

などが該当します。
## Unityで遺伝アルゴリズムを簡単に実装するためには？
Unityで人工知能や機械学習を扱う場合、最初に候補に挙がるのが [**UnityMLAgents**](https://unity.com/ja/products/machine-learning-agents) かと思います。しかし、MLAgents は深層強化学習がメインで実装されていて、遺伝アルゴリズムは扱っていません。そこで、今回使用するのは [**GeneticSharp**](https://github.com/giacomelli/GeneticSharp) というC#向けの遺伝アルゴリズム用ライブラリです。
このライブラリは

- 内部処理がC#で記述されているので、（C#が読めれば）困った時に確認しやすい
- Unityへの導入が非常に簡単（unitypackage が用意されている + NuGet に対応している）

というところが良いと思っています。

## GeneticSharpの導入方法
導入方法は以下の2通りです。
- [GeneticSharpのReleases](https://github.com/giacomelli/GeneticSharp/releases)からunitypackageをダウンロードしてUnityへドラッグ&ドロップ
- UnityNuGetを利用してインストールする（参考：[Unity で NuGet パッケージを利用する](https://qiita.com/akiojin/items/ac05392d97abb8797dcd)）
## 入門編 「OneMax問題を解いてみる」

### OneMax問題とは
OneMax問題は
「0,1の値を取るパラメタがn個ある時、パラメタの合計値を最大になるパラメタの組み合わせ」
を求めるという問題です。
考えるまでもなく、この問題の答えは「すべてのパラメタが1の時」です。
### Unity上での実装
[GithubのUsage](https://github.com/giacomelli/GeneticSharp#usage)にも書いてあるように
GeneticSharpを使うのに最低限必要なのは以下の3つです。
（クラス名は好きなように変えて大丈夫です。）

- **MyChromosome.cs**：遺伝子（パラメタ）をひとまとめにした染色体用のクラス
- **MyFitness.cs**：遺伝子の適合度計算用クラス
- **MyGA.cs**：遺伝アルゴリズムのメイン処理を記述する用のクラス

```C#:MyChromosome.cs
using GeneticSharp.Domain.Chromosomes;

public class MyChromosome : ChromosomeBase
{
	public System.Random random = new System.Random();

    // Baseクラスのコンストラクタ引数で染色体を構成する遺伝子の個数を指定できる
	public MyChromosome() : base(8)
	{
		CreateGenes();
	}

	public override Gene GenerateGene(int index)
	{
        // 0か1の値を取る乱数を発生させて初期化する
		var geneValue = random.Next(0,2);
		return new Gene(geneValue);
	}

	public override IChromosome CreateNew()
	{
		return new MyChromosome();
	}

}

```

```C#:MyFitness.cs
using System;
using GeneticSharp.Domain.Fitnesses;
using GeneticSharp.Domain.Chromosomes;

public class MyFitness : IFitness
{
	// 遺伝子を評価して適合度を返すメソッド
    // 返り値はdoubleである必要がある
	public double Evaluate(IChromosome chromosome)
	{
		double fitness = 0;

        // 染色体から遺伝子をひとつづつ取り出して値を取得する
		foreach(var gene in chromosome.GetGenes())
		{
            // gene.Valueはobject型なので変換する必要がある
			fitness += Convert.ToDouble(gene.Value);
		}
        // 各遺伝子の値の合計値を適合度として返す
		return fitness;
	}
}
```

```C#:MyGA.cs
using UnityEngine;
using GeneticSharp.Domain;
using GeneticSharp.Domain.Chromosomes;
using GeneticSharp.Domain.Crossovers;
using GeneticSharp.Domain.Mutations;
using GeneticSharp.Domain.Populations;
using GeneticSharp.Domain.Selections;
using GeneticSharp.Domain.Terminations;

public class MyGA : MonoBehaviour
{
    void Start()
    {
        // 選択の手法
		var selection = new EliteSelection();
        // 交叉の手法
		var crossover = new UniformCrossover();
        // 突然変異の手法
		var mutation = new ReverseSequenceMutation();
        // 適合度計算の手法
		var fitness = new MyFitness();
        // 染色体の構造
		var chromosome = new MyChromosome();
        // 染色体の集団の生成
		var population = new Population(50,70,chromosome);

		var ga = new GeneticAlgorithm(population, fitness, selection, crossover, mutation);
        // 第何世代まで到達したら探索を終了させるか
		ga.Termination = new GenerationNumberTermination(10);

		Debug.Log("遺伝アルゴリズムによる探索開始");

		ga.Start();

        Debug.Log("探索終了");

        // 最終的に最も適合度が高くなった染色体を取得
        var bestChromosome = ga.BestChromosome;
        var bestGenes = bestChromosome.GetGenes();
        string stringGenes = "";
        // 各遺伝子を表示
        foreach (var gene in bestGenes)
        {
            var geneValue = gene.Value;
            stringGenes += geneValue + " ";
        }

        Debug.Log("遺伝子配列: " + stringGenes);

		Debug.Log("最終適合度: " + ga.BestChromosome.Fitness);
    }
}
```

上記の3つのスクリプトを作成し、MyGA.cs を適当な GameObject にアタッチして実行ボタンを押すと遺伝アルゴリズムによる探索がスタートします。
### 実行結果
・実行環境
M1 Mac-mini 16GB
Unity 2021.3.4f1

・パラメタの設定
染色体母集団の大きさ：50 ~ 70
探索世代数：10

↓実行時のコンソール出力
![スクリーンショット 2023-02-21 17.54.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685272/287f6ef8-7fc6-b6be-0807-03c0a9fd0525.png)
確かに全てのパラメタが1の最適解を探索できています。
探索にはランダム性があるため、何回か試すと適合度が8にならないで探索終了してしまう場合もあります。

また、適合度の評価基準を下記のように変更して「和が最小になるパラメタの組み合わせ」を探索してみると

```C#:MyFitness.cs
        // 染色体から遺伝子をひとつづつ取り出して値を取得する
		foreach(var gene in chromosome.GetGenes())
		{
            // gene.Valueはobject型なので変換する必要がある
            // 値が大きいほど適合度が低くなるようにしてみる
			fitness -= Convert.ToDouble(gene.Value);
		}

```

↓和が最小になる組み合わせを探索した場合のコンソール出力
![スクリーンショット 2023-02-21 18.16.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685272/face0de7-d581-b430-5dbe-83b9bea0fb19.png)
このようにすべてのパラメタが0になる最適解が得られています。


## まとめ
今回はGeneticSharpをUnityに導入して簡単な問題を遺伝アルゴリズムによって解いてみました。今後は遺伝アルゴリズムに関する詳しい解説、遺伝アルゴリズムをもう少し複雑な問題や実際のゲームに適用する方法について解説などの記事を書こうと思っています。

## 参考文献
- [遺伝アルゴリズム Wikipedia](https://ja.wikipedia.org/wiki/%E9%81%BA%E4%BC%9D%E7%9A%84%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0)
- [アルゴリズム初心者向けの基礎と入門(C#, Pythonとか) 遺伝アルゴリズム](https://algoful.com/Archive/Algorithm/ga)
- [Unity で NuGet パッケージを利用する](https://qiita.com/akiojin/items/ac05392d97abb8797dcd)