2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【レポ】ChatGPTを使ってUnityで動く遺伝的アルゴリズムを実装

Posted at

諸言

Unityで遺伝的アルゴリズム(GA)のコードを実装し,遺伝アルゴリズムの挙動を可視化したかったが,簡単なサンプルが見つからなかった(強化学習とかは沢山あるけど).
そこで今話題のChatGPTのコード支援を使いながらGAのコードを実装,可視化モデルを作成してみました.
本レポートを通して,ChatGPTを用いたCopilotの理解を深めていきます.

まずはChatGPTにGAのサンプルコードを作成するように指示し,次に可視化モデルの作成方法を教えてもらいます.
ChatGPTの出力に基づいてC#コードとUnity上での可視化モデルを作成します.
エラーを基に更にChatGPTに修正案をなげて,ソースコードを修正します.

image.png

実際に動かすことでGAが動作していることを確認します.

video.gif

Unity,C#,ChatGPTの基礎的な使い方がわかる人向けの記事です.
できるだけ時系列順かつ簡潔にまとめようと思います.

環境

Unity 2021.3.16f1
ChatGPT Feb 13 Version (Free)

※2023/03/10時点での最新バージョンを使用
※ChatGPTに関しては出力が一定ではないので,記事の内容は保証できません

ChatGPTに指示を投げる

まずはChatGPTと会話していきます.

image.png

上記画像のようなやり取りを以下で記載してきます.
ユーザの入力を"入力",ChatGPTの出力を"出力として",対話を時系列順に引用形式で表記します.
あとそれぞれの出力に対する私の感想も入れます.
ユーザの入力ミスなどは省き,コードは長いので折りたたみで載せます.

まずはUnity上でのGAのソースコードを示すように指示します.

【入力】
Unityで動く遺伝アルゴリズムのソースコードを示してください(できるだけシンプルな)。

【出力】

以下は、Unityで実装された遺伝アルゴリズムのシンプルな例です。この例では、個体はランダムに生成された2次元の座標を持ち、適応度はその座標が目標点に近いかどうかによって評価されます。

using System.Collections.Generic;
using UnityEngine;

public class GeneticAlgorithm : MonoBehaviour
{
    // 目標点
    public Vector2 targetPoint;

    // 個体数
    public int populationSize;

    // 突然変異率
    public float mutationRate;

    // 現在の世代数
    private int generation;

    // 現在の個体群
    private List<Individual> population;

    // 初期化
    void Start()
    {
        // 個体群をランダムに生成
        population = new List<Individual>();
        for (int i = 0; i < populationSize; i++)
        {
            population.Add(new Individual(Random.insideUnitCircle));
        }

        // 世代数を初期化
        generation = 0;
    }

    // 毎フレーム更新
    void Update()
    {
        // 世代を進める
        generation++;

        // 個体を評価し、適応度に基づいてソートする
        population.Sort();

        // エリート個体を選択して次世代に残す
        List<Individual> nextGeneration = new List<Individual>();
        for (int i = 0; i < populationSize / 2; i++)
        {
            nextGeneration.Add(population[i]);
        }

        // 残りの個体をランダムに選択して交叉させ、次世代に追加する
        for (int i = populationSize / 2; i < populationSize; i++)
        {
            Individual parent1 = population[Random.Range(0, populationSize / 2)];
            Individual parent2 = population[Random.Range(0, populationSize / 2)];
            nextGeneration.Add(new Individual(parent1, parent2, mutationRate));
        }

        // 次世代を現在の個体群として保存する
        population = nextGeneration;
    }

    // 個体クラス
    private class Individual : System.IComparable<Individual>
    {
        // 位置
        public Vector2 position;

        // 適応度
        public float fitness;

        // コンストラクタ(ランダムに生成する場合)
        public Individual()
        {
            position = Random.insideUnitCircle;
            CalculateFitness();
        }

        // コンストラクタ(指定した位置を持つ場合)
        public Individual(Vector2 position)
        {
            this.position = position;
            CalculateFitness();
        }

        // コンストラクタ(交叉する場合)
        public Individual(Individual parent1, Individual parent2, float mutationRate)
        {
            // 交叉する
            if (Random.value < 0.5f)
            {
                position = parent1.position;
            }
            else
            {
                position = parent2.position;
            }

            //

トークン上限に達してコードが途中までしか表示されませんでした.その場合は”続き”と入力します.

【入力】
続き

【出力】


        // 突然変異する
        if (Random.value < mutationRate)
        {
            position += Random.insideUnitCircle * 0.1f;
        }

        // 適応度を計算する
        CalculateFitness();
    }

    // 適応度を計算する
    private void CalculateFitness()
    {
        fitness = 1.0f / (1.0f + (position - targetPoint).sqrMagnitude);
    }

    // 適応度に基づいて比較する
    public int CompareTo(Individual other)
    {
        return other.fitness.CompareTo(fitness);
    }
}
}

この例では、GeneticAlgorithmクラスが遺伝アルゴリズムを実装しています。Startメソッドで初期値を設定し、Updateメソッドで世代を進め、新しい個体群を生成しています。

個体はIndividualクラスで表され、位置と適応度を持ちます。コンストラクタは3つあり、ランダムに生成する場合、指定した位置を持つ場合、または2つの親を交叉させる場合があります。交叉時には、50%の確率で親のどちらかの位置を受け継ぎ、突然変異率に基づいてランダムな方向に変異させます。適応度は、位置と目標点の距離の2乗の逆数で計算されます。

この例では、シンプルな遺伝アルゴリズムの実装を示していますが、実際の問題に適用するためには、より多くの工夫が必要です。また、遺伝アルゴリズムは最適化問題に対して有効な手法の1つであることを覚えておいてください。

それっぽいコードと実装内容の概要が文章として生成されました.
ただ,これだけだと動かないで,このコードに基づいて色々指示してあげる必要があります.
とりあえず遺伝子が書かれていないので,以下のようにしてみます.

【入力】
遺伝子を示すint[] geneを追加して、上記のコードを全て書き直してください。ただしint[] geneは個体を動かすための時系列データです。

【出力】

以下は、遺伝アルゴリズムにint[] geneを追加し、個体を時系列データで表現する例です。


using System.Collections.Generic;
using UnityEngine;

public class GeneticAlgorithm : MonoBehaviour
{
    public int populationSize = 100;
    public float mutationRate = 0.01f;
    public float crossoverRate = 0.7f;
    public int geneLength = 20;
    public int maxGeneration = 100;
    public float targetFitness = 0.99f;

    private List<Individual> population;
    private Vector2 targetPoint;

    private void Start()
    {
        population = new List<Individual>();

        // 目標点をランダムに設定する
        targetPoint = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));

        // 初期個体群を生成する
        for (int i = 0; i < populationSize; i++)
        {
            int[] gene = new int[geneLength];
            for (int j = 0; j < geneLength; j++)
            {
                gene[j] = Random.Range(0, 4);
            }

            population.Add(new Individual(gene));
        }
    }

    private void Update()
    {
        // 世代を進める
        for (int i = 0; i < maxGeneration; i++)
        {
            // 選択する
            List<Individual> selected = Selection();

            // 交叉する
            List<Individual> offspring = Crossover(selected);

            // 突然変異する
            Mutation(offspring);

            // 新しい個体群を生成する
            population = offspring;

            // 最も適応度の高い個体を取得する
            Individual best = population[0];
            foreach (Individual individual in population)
            {
                if (individual.fitness > best.fitness)
                {
                    best = individual;
                }
            }

            // 目標適応度に達した場合、終了する
            if (best.fitness >= targetFitness)
            {
                Debug.Log("Generation: " + i + ", Fitness: " + best.fitness);
                break;
            }
        }
    }

    // 選択する
    private List<Individual> Selection()
    {
        List<Individual> selected = new List<Individual>();

        // 選択個体数が個体数よりも多い場合、個体数に合わせる
        int selectionSize = Mathf.Min(populationSize, Mathf.RoundToInt(populationSize * 0.5f));

        // トーナメント選択する
        for (int i = 0; i < selectionSize; i++)
        {
            int index1 = Random.Range(0, populationSize);
            int index2 = Random.Range(0, populationSize);

            if (population[index1].fitness > population[index2].fitness)
            {
                selected.Add(population[index1]);
            }
            else
            {
                selected.Add(population[index2]);
            }
        }

        return selected;
    }

    // 交叉する
    private List<Individual> Crossover(List<Individual> selected)
    {
        List<Individual> offspring = new List<Individual>();

        for (int i = 0; i < populationSize

やはり,ソースコードが長すぎて途中までしか生成されませんでした.続きを生成させます.

【入力】
C#のソースコードの続き

【出力】


{
            // 交叉する個体を選択する
            Individual parent1 = selected[Random.Range(0, selected.Count)];
            Individual parent2 = selected[Random.Range(0, selected.Count)];

            // 交叉しない場合
            if (Random.value >= crossoverRate)
            {
                offspring.Add(parent1);
                offspring.Add(parent2);
                continue;
            }

            // 一点交叉する
            int crossoverPoint = Random.Range(1, geneLength - 1);
            int[] gene1 = new int[geneLength];
            int[] gene2 = new int[geneLength];
            for (int j = 0; j < geneLength; j++)
            {
                if (j < crossoverPoint)
                {
                    gene1[j] = parent1.gene[j];
                    gene2[j] = parent2.gene[j];
                }
                else
                {
                    gene1[j] = parent2.gene[j];
                    gene2[j] = parent1.gene[j];
                }
            }

            offspring.Add(new Individual(gene1));
            offspring.Add(new Individual(gene2));
        }

        return offspring;
    }

    // 突然変異する
    private void Mutation(List<Individual> offspring)
    {
        foreach (Individual individual in offspring)
        {
            for (int i = 0; i < geneLength; i++)
            {
                // 突然変異しない場合
                if (Random.value >= mutationRate)
                {
                    continue;
                }

                // 塩基をランダムに変更する
                individual.gene[i] = Random.Range(0, 4);
            }
        }
    }

    // 個体クラス
    private class Individual
    {
        public int[] gene;
        public float fitness;

        public Individual(int[] gene)
        {
            this.gene = gene;
            CalculateFitness();
        }

        // 適応度を計算する
        private void CalculateFitness()
        {
            // 個体を動かす
            Vector2 position = Vector2.zero;
            foreach (int direction in gene)
            {
                switch (direction)
                {
                    case 0: position += Vector2.up; break;
                    case 1: position += Vector2.right; break;
                    case 2: position += Vector2.down; break;
                    case 3: position += Vector2.left; break;
                }
            }

            // 目標点との距離を計算する
            float distance = Vector2.Distance(position, targetPoint);

            // 適応度を計算する
            fitness = 1f / distance;
        }
    }
}
}

このコードでは、Individualクラスにint[] geneを追加し、個体の時系列データを表現しています。また、適応度を計算するメソッドCalculateFitnessでは、int[] geneをもとに個体を動かし、目標点との距離を計算しています。遺伝アルゴリズムの本質は変わらないため、前回のコードと同様に、選択、交叉、突然変異のアルゴリズムが実装されています。

ソースコードの大元はできたので,次に可視化できないか聞いてみました.

【入力】
gameobjectを用いて視覚的に表現できませんか?ソースコードをそれに合わせて書き直してください。

【出力】
はい、GameObjectを用いて個体を視覚的に表現することができます。以下は、前回のコードをGameObjectを用いて書き直したものです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GeneticAlgorithm : MonoBehaviour
{
    public GameObject individualPrefab; // 個体のプレハブ
    public Vector2Int geneRange = new Vector2Int(0, 3); // 塩基の値の範囲
    public int populationSize = 100; // 個体群のサイズ
    public float crossoverRate = 0.8f; // 交叉率
    public float mutationRate = 0.02f; // 突然変異率
    public int geneLength = 100; // 塩基配列の長さ
    public float targetRadius = 1f; // 目標点の半径
    public float timeScale = 1f; // タイムスケール

    private List<Individual> population = new List<Individual>(); // 個体群
    private Vector2 targetPoint; // 目標点

    void Start()
    {
        // 目標点をランダムに設定する
        targetPoint = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));

        // 初期個体群を生成する
        for (int i = 0; i < populationSize; i++)
        {
            GameObject individualObj = Instantiate(individualPrefab, Vector3.zero, Quaternion.identity);
            individualObj.transform.parent = transform;
            Individual individual = new Individual(geneLength, geneRange);
            individualObj.GetComponent<IndividualController>().SetIndividual(individual);
            population.Add(individual);
        }

        Time.timeScale = timeScale;
    }

    void Update()
    {
        // 個体群の適応度を計算する
        foreach (Individual individual in population)
        {
            individual.CalculateFitness(targetPoint, targetRadius);
        }

        // 個体群を適応度の高い順にソートする
        population.Sort((a, b) => b.fitness.CompareTo(a.fitness));

        // 次世代の個体群を作る
        List<Individual> offspring = Crossover(population);
        Mutation(offspring);

        // 個体群を更新する
        population.Clear();
        foreach (Individual individual in offspring)
        {
            GameObject individualObj = Instantiate(individualPrefab, Vector3.zero, Quaternion.identity);
            individualObj.transform.parent = transform;
            individualObj.GetComponent<IndividualController>().SetIndividual(individual);
            population.Add(individual);
        }

        // 目標点を更新する
        if (Input.GetKeyDown(KeyCode.Space))
        {
            targetPoint = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));
        }
    }

    // 交叉する
    private List<Individual> Crossover(List<Individual> parents)
    {
        List<Individual> offspring = new List<Individual>();
        while (offspring.Count < populationSize)
        {
            // 交叉する親を選ぶ
            Individual parent1 = parents[Random.Range(0, parents.Count)];

【入力】
続き

【出力】

        Individual parent2 = parents[Random.Range(0, parents.Count)];

        // 交叉するかどうかをランダムに決める
        if (Random.value < crossoverRate)
        {
            // 一点交叉する
            int crossoverPoint = Random.Range(1, geneLength);
            int[] gene1 = parent1.gene;
            int[] gene2 = parent2.gene;
            int[] childGene1 = gene1.Take(crossoverPoint).Concat(gene2.Skip(crossoverPoint)).ToArray();
            int[] childGene2 = gene2.Take(crossoverPoint).Concat(gene1.Skip(crossoverPoint)).ToArray();
            offspring.Add(new Individual(childGene1));
            if (offspring.Count < populationSize)
            {
                offspring.Add(new Individual(childGene2));
            }
        }
        else
        {
            // 交叉しない場合は親をそのまま子供にする
            offspring.Add(new Individual(parent1.gene));
            if (offspring.Count < populationSize)
            {
                offspring.Add(new Individual(parent2.gene));
            }
        }
    }

    return offspring;
}

// 突然変異する
private void Mutation(List<Individual> offspring)
{
    foreach (Individual individual in offspring)
    {
        for (int i = 0; i < geneLength; i++)
        {
            // 突然変異するかどうかをランダムに決める
            if (Random.value < mutationRate)
            {
                // 塩基をランダムに変える
                individual.gene[i] = Random.Range(geneRange.x, geneRange.y + 1);
            }
        }
    }
}
}

また、個体を視覚的に表現するためのスクリプト IndividualController も用意する必要があります。以下は、そのスクリプトの例です。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class IndividualController : MonoBehaviour
{
    private Individual individual; // 個体

    public void SetIndividual(Individual individual)
    {
        this.individual = individual;
    }

    void Update()
    {
        // 塩基配列に従って個体を動かす
        int[] gene = individual.gene;
        for (int i = 0; i < gene.Length; i++)
        {
            Vector2 direction = Vector2.zero;
            switch (gene[i])
            {
                case 0: // 上に移動
                    direction = Vector2.up;
                    break;
                case 1: // 右に移動
                    direction = Vector2.right;
                    break;
                case 2: // 下に移動
                    direction = Vector2.down;
                    break;
                case 3: // 左に移動
                    direction = Vector2.left;
                    break;
            }
            transform.Translate(direction * Time.deltaTime);
        }
    }
}

このスクリプトは、Individual クラスから受け取った個体の塩基配列に従って、GameObjectを移動させるためのものです。また、このスクリプトを個体のプレハブにアタッチすることで、個体を表示することができます。

Unityでスクリプトを動かすときには,ソースコードをモデル上にオブジェクトにアタッチする必要があるので,以下のように聞いてみます.

【入力】
GeneticAlgorithmはどこにアタッチすればいいですか?

GeneticAlgorithmクラスは、Unityのどのオブジェクトにアタッチしても構いません。ただし、通常は適切なオブジェクトにアタッチして、シーン内での個体の生成や進化を制御するために使用されます。

例えば、シーン内に個体を生成し、IndividualControllerスクリプトをアタッチしたオブジェクトに個体の情報を渡すために、GeneticAlgorithmクラスをアタッチすることができます。また、GeneticAlgorithmクラスは、個体の進化を制御するために必要な機能を提供するため、ゲームのメインスクリプトにアタッチすることもできます。

どのオブジェクトにアタッチするかは、プロジェクトの構造や目的に応じて決定する必要があります。

なるほど,とりあえず適当に作ればいいのね.

上記のコードだけだと個体しか視覚化されないので,目標ポイントも視覚化させるようにコード生成させます.

【入力】
ちなみにtargetPointも視覚化できませんか?

はい、targetPointも視覚化することができます。Unityでは、オブジェクトの位置を表すためにTransformコンポーネントを使用します。targetPointもTransformコンポーネントをアタッチしたオブジェクトとして表現することができます。

具体的には、以下の手順でtargetPointを視覚化できます。

Hierarchyビューで、空のオブジェクトを作成します。
作成したオブジェクトにTransformコンポーネントをアタッチします。
targetPointの座標を空のオブジェクトのTransformコンポーネントのPositionプロパティに設定します。
空のオブジェクトに表示用のオブジェクトをアタッチします。例えば、Sphereなどを使用することができます。
表示用のオブジェクトを調整して、適切な大きさや色に設定します。
以上の手順を実行することで、targetPointを視覚化することができます。また、スクリプトから空のオブジェクトを取得し、座標を更新することで、targetPointの移動に合わせて表示用のオブジェクトも移動するようにすることができます。

入力が悪かったようで,ソースコードが出力されませんでした.明示的にソースコードを出力されるように指定してみます.

【入力】
では,スクリプトから空のオブジェクトを取得し、座標を更新することで、targetPointの移動に合わせて表示用のオブジェクトも移動するようにするためのコードを含めて、上記コードを書き直してください。

以下は、targetPointを表示するためのコード例です。空のオブジェクトを作成し、Sphereをアタッチして、targetPointの位置に移動させるようになっています。

using UnityEngine;
using System.Collections;

public class GeneticAlgorithm : MonoBehaviour {

    // 表示用のSphereオブジェクト
    public GameObject targetPointObject;

    // targetPointの座標
    private Vector3 targetPoint = new Vector3(5f, 0f, 5f);

    void Start() {
        // 空のオブジェクトを作成し、Sphereオブジェクトをアタッチする
        GameObject emptyObject = new GameObject("TargetPoint");
        targetPointObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        targetPointObject.transform.parent = emptyObject.transform;
        targetPointObject.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);

        // Sphereオブジェクトを赤色に設定する
        Renderer renderer = targetPointObject.GetComponent<Renderer>();
        renderer.material.color = Color.red;

        // 空のオブジェクトをtargetPointの位置に移動する
        emptyObject.transform.position = targetPoint;
    }

    void Update() {
        // targetPointの座標を更新する
        targetPoint.x = Mathf.Sin(Time.time);
        targetPoint.z = Mathf.Cos(Time.time);

        // 空のオブジェクトをtargetPointの位置に移動する
        targetPointObject.transform.parent.transform.position = targetPoint;
    }
}

Startメソッドで、空のオブジェクトを作成し、Sphereオブジェクトをアタッチしています。また、Sphereオブジェクトを赤色に設定しています。

Updateメソッドでは、targetPointの座標を更新し、空のオブジェクトをtargetPointの位置に移動させるようにしています。targetPointObjectは、空のオブジェクトの子オブジェクトとしてアタッチされているため、parentプロパティを使用して、空のオブジェクトの位置を変更しています。

ここでInvidivualクラスがないことに気がつきました.以下のようにして出力させます.

上記のソースコード内のIndividualクラスはどうやって作るの。

以下は、Individualクラスの例です。この例では、位置座標を表すVector3型の配列を遺伝子として持っています。

public class Individual {

    // 個体の遺伝子
    private Vector3[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength) {
        gene = new Vector3[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector3(Random.Range(-5f, 5f), 0f, Random.Range(-5f, 5f));
        }
    }

    // 遺伝子のgetter/setter
    public Vector3[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }
}

Individualクラスでは、コンストラクタで遺伝子を初期化します。遺伝子は、ランダムな位置座標で初期化されます。遺伝子のgetter/setterと、適応度のgetter/setterも定義されています。遺伝子の型はVector3型となっていますが、必要に応じて他の型に変更することができます。

とりあえずここまでのコードと説明さえあればUnityで動きそうだったので,次にUnity側で操作します.

Unity側での操作(環境作成とコード実装)

Unity HubからNew Projectを作成して,2D(URP)を選択します.
Project nameは適当につけます.
image.png

ロードし終わったら,Assets内にScriptディレクトリを作成します.ChatGPTの指示に従って,GeneticAlgorithmクラス,IndividualControllerクラス,Individualクラスのファイルを作成します.

image.png

ファイルを作成したらChatGPTの出力を基に,以下のようにソースコードをコピペします.

GeneticAlgorithm
GeneticAlgorithm.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GeneticAlgorithm : MonoBehaviour
{
    public GameObject individualPrefab; // 個体のプレハブ
    public Vector2Int geneRange = new Vector2Int(0, 3); // 塩基の値の範囲
    public int populationSize = 100; // 個体群のサイズ
    public float crossoverRate = 0.8f; // 交叉率
    public float mutationRate = 0.02f; // 突然変異率
    public int geneLength = 100; // 塩基配列の長さ
    public float targetRadius = 1f; // 目標点の半径
    public float timeScale = 1f; // タイムスケール

    private List<Individual> population = new List<Individual>(); // 個体群
    private Vector2 targetPoint; // 目標点

    // targetPointの座標
    private Vector3 targetPoint = new Vector3(5f, 0f, 5f);

    void Start()
    {
        // 目標点をランダムに設定する
        targetPoint = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));

         // 空のオブジェクトを作成し、Sphereオブジェクトをアタッチする
        GameObject emptyObject = new GameObject("TargetPoint");
        targetPointObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        targetPointObject.transform.parent = emptyObject.transform;
        targetPointObject.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);

        // Sphereオブジェクトを赤色に設定する
        Renderer renderer = targetPointObject.GetComponent<Renderer>();
        renderer.material.color = Color.red;

        // 空のオブジェクトをtargetPointの位置に移動する
        emptyObject.transform.position = targetPoint;

        // 初期個体群を生成する
        for (int i = 0; i < populationSize; i++)
        {
            GameObject individualObj = Instantiate(individualPrefab, Vector3.zero, Quaternion.identity);
            individualObj.transform.parent = transform;
            Individual individual = new Individual(geneLength, geneRange);
            individualObj.GetComponent<IndividualController>().SetIndividual(individual);
            population.Add(individual);
        }

        Time.timeScale = timeScale;
    }

    void Update()
    {
        // 個体群の適応度を計算する
        foreach (Individual individual in population)
        {
            individual.CalculateFitness(targetPoint, targetRadius);
        }

        // 個体群を適応度の高い順にソートする
        population.Sort((a, b) => b.fitness.CompareTo(a.fitness));

        // 次世代の個体群を作る
        List<Individual> offspring = Crossover(population);
        Mutation(offspring);

        // 個体群を更新する
        population.Clear();
        foreach (Individual individual in offspring)
        {
            GameObject individualObj = Instantiate(individualPrefab, Vector3.zero, Quaternion.identity);
            individualObj.transform.parent = transform;
            individualObj.GetComponent<IndividualController>().SetIndividual(individual);
            population.Add(individual);
        }

        // 目標点を更新する
        if (Input.GetKeyDown(KeyCode.Space))
        {
            targetPoint = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));
        }

        // targetPointの座標を更新する
        targetPoint.x = Mathf.Sin(Time.time);
        targetPoint.z = Mathf.Cos(Time.time);

        // 空のオブジェクトをtargetPointの位置に移動する
        targetPointObject.transform.parent.transform.position = targetPoint;
    }

    // 交叉する
    private List<Individual> Crossover(List<Individual> parents)
    {
        List<Individual> offspring = new List<Individual>();
        while (offspring.Count < populationSize)
        {
            // 交叉する親を選ぶ
            Individual parent1 = parents[Random.Range(0, parents.Count)];
                 Individual parent2 = parents[Random.Range(0, parents.Count)];

        // 交叉するかどうかをランダムに決める
        if (Random.value < crossoverRate)
        {
            // 一点交叉する
            int crossoverPoint = Random.Range(1, geneLength);
            int[] gene1 = parent1.gene;
            int[] gene2 = parent2.gene;
            int[] childGene1 = gene1.Take(crossoverPoint).Concat(gene2.Skip(crossoverPoint)).ToArray();
            int[] childGene2 = gene2.Take(crossoverPoint).Concat(gene1.Skip(crossoverPoint)).ToArray();
            offspring.Add(new Individual(childGene1));
            if (offspring.Count < populationSize)
            {
                offspring.Add(new Individual(childGene2));
            }
        }
        else
        {
            // 交叉しない場合は親をそのまま子供にする
            offspring.Add(new Individual(parent1.gene));
            if (offspring.Count < populationSize)
            {
                offspring.Add(new Individual(parent2.gene));
            }
        }
    }

        return offspring;
    }

    // 突然変異する
    private void Mutation(List<Individual> offspring)
    {
        foreach (Individual individual in offspring)
        {
            for (int i = 0; i < geneLength; i++)
            {
                // 突然変異するかどうかをランダムに決める
                if (Random.value < mutationRate)
                {
                    // 塩基をランダムに変える
                    individual.gene[i] = Random.Range(geneRange.x, geneRange.y + 1);
                }
            }
        }
    }
}

IndividualController
IndividualController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class IndividualController : MonoBehaviour
{
    private Individual individual; // 個体

    public void SetIndividual(Individual individual)
    {
        this.individual = individual;
    }

    void Update()
    {
        // 塩基配列に従って個体を動かす
        int[] gene = individual.gene;
        for (int i = 0; i < gene.Length; i++)
        {
            Vector2 direction = Vector2.zero;
            switch (gene[i])
            {
                case 0: // 上に移動
                    direction = Vector2.up;
                    break;
                case 1: // 右に移動
                    direction = Vector2.right;
                    break;
                case 2: // 下に移動
                    direction = Vector2.down;
                    break;
                case 3: // 左に移動
                    direction = Vector2.left;
                    break;
            }
            transform.Translate(direction * Time.deltaTime);
        }
    }
}
Individual
Individual.cs

public class Individual {

    // 個体の遺伝子
    private Vector3[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength) {
        gene = new Vector3[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector3(Random.Range(-5f, 5f), 0f, Random.Range(-5f, 5f));
        }
    }

    // 遺伝子のgetter/setter
    public Vector3[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }
}

次にシーンを作っていきます.
シーン中には,実態を持たないスクリプト格納用オブジェクトGeneticAlgorithmObjと,プレハブ用のモデルindividualPrefabを作成します.
GeneticAlgorithmObjは,右クリック->Create Emptyより作成します.
GeneticAlogirithm.csをアタッチさせ,さらにIndividual PrefabをindividualPrefabに指定します.
image.png

individualPrefabは,右クリック->2D Object->Sprites->Squareより作成します.
必要に応じて見た目を変えて,IndividualController.csをアタッチします.
image.png

よしこれで実行だ❗❗❗❗...と思っても動きません,駄目です❗❗❗❗デバッグエラー多発です😱
上記のコードを基に修正していきます.👊

ソースコードの修正をChatGPTに投げる

いくつかのソースコードはChatGPTに修正させましょう.
会話の続き(Individualクラスの話)から始めます.

2次元ではありませんか?

おっしゃる通りで、私の前回の回答に誤りがありました。2D空間の遺伝アルゴリズムの例を以下に示します。

public class Individual {

    // 個体の遺伝子
    private Vector2[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength) {
        gene = new Vector2[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));
        }
    }

    // 遺伝子のgetter/setter
    public Vector2[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }
}

2D空間の個体の遺伝子として、位置座標を表すVector2型の配列を使用しています。個体の初期化時に、ランダムな位置座標で遺伝子を初期化しています。遺伝子のgetter/setterと、適応度のgetter/setterも定義されています。必要に応じて、遺伝子の型を変更することができます。

遺伝子情報のint[] geneVector2[] geneになってしまいました...
ただこっちのほうが良さそうなので,そのまま続けてみます.

【入力】
Vector2が見つかりません

おっしゃる通りです、Vector2クラスを使用する場合は、UnityEngineの名前空間をインポートする必要があります。以下のように修正してください。

using UnityEngine;

public class Individual {

    // 個体の遺伝子
    private Vector2[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength) {
        gene = new Vector2[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));
        }
    }

    // 遺伝子のgetter/setter
    public Vector2[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }
}

GeneticAlgorithmクラスでのインスタンス化する際の引数の数が合わないので,コンストラクタを修正させます.

【入力】
上記ではインスタンス化するときにIndividual individual = new Individual(geneLength, geneRange);と宣言しています。これにあわせてIndividualのコンストラクタを修正してください

ご指摘ありがとうございます。以下のように修正してください。


public class Individual {

    // 個体の遺伝子
    private Vector2[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength, float geneRange) {
        gene = new Vector2[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector2(Random.Range(-geneRange, geneRange), Random.Range(-geneRange, geneRange));
        }
    }

    // 遺伝子のgetter/setter
    public Vector2[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }
}

using UnityEngine;が入ってませんね...)
型が違うので修正させます.

【入力】 geneRangeはVector2Int型ではありませんか?

おっしゃる通りです、geneRangeはfloat型であるため、修正が必要です。以下のように修正してください。


public class Individual {

    // 個体の遺伝子
    private Vector2[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength, float geneRange) {
        gene = new Vector2[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector2(Random.Range(-geneRange, geneRange), Random.Range(-geneRange, geneRange));
        }
    }

    // 遺伝子のgetter/setter
    public Vector2[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }
}

【入力】 geneRangeはfloat型ではなく、Vector2Int型です。それに合わせてください

了解しました。以下のように修正してください。

public class Individual {

    // 個体の遺伝子
    private Vector2[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength, Vector2Int geneRange) {
        gene = new Vector2[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector2(Random.Range(-geneRange.x, geneRange.x), Random.Range(-geneRange.y, geneRange.y));
        }
    }

    // 遺伝子のgetter/setter
    public Vector2[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }
}

GeneticAlogrithmクラスで参照しているFitness関数を追加させます.

【入力】
Individualにはfitnessを計算するためのCalculateFitness(Vector2 targetPoint, float targetRadius)関数が含まれているはずです。それに合わせて修正してください

了解しました。以下のように修正してください。


public class Individual {

    // 個体の遺伝子
    private Vector2[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength, Vector2Int geneRange) {
        gene = new Vector2[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector2(Random.Range(-geneRange.x, geneRange.x), Random.Range(-geneRange.y, geneRange.y));
        }
    }

    // 遺伝子のgetter/setter
    public Vector2[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }

    // 適応度を計算する
    public void CalculateFitness(Vector2 targetPoint, float targetRadius) {
        Vector2 position = Vector2.zero;
        float distance = 0f;

        // geneに基づいて移動する
        for (int i = 0; i < gene.Length; i++) {
            position += gene[i];
            distance = Vector2.Distance(position, targetPoint);

            // ゴールに到達したら距離をゼロにする
            if (distance <= targetRadius) {
                distance = 0f;
                break;
            }
        }

        // ゴールに到達していない場合、最終位置とゴールとの距離を適応度とする
        fitness = (distance == 0f) ? 1f : 1f / distance;
    }
}

Individualクラスはコンストラクタも2種類あるので,合わせて記載させます.

【入力】
Individualはnew Individual(Vector2[] gene)のように遺伝子情報のみからインスタンス化するコンストラクタも必要です

了解しました。以下のように、遺伝子情報のみからインスタンス化するコンストラクタを追加してください。


public class Individual {

    // 個体の遺伝子
    private Vector2[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength, Vector2Int geneRange) {
        gene = new Vector2[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector2(Random.Range(-geneRange.x, geneRange.x), Random.Range(-geneRange.y, geneRange.y));
        }
    }

    // 遺伝子情報のみからインスタンス化するコンストラクタ
    public Individual(Vector2[] gene) {
        this.gene = gene;
    }

    // 遺伝子のgetter/setter
    public Vector2[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }

    // 適応度を計算する
    public void CalculateFitness(Vector2 targetPoint, float targetRadius) {
        Vector2 position = Vector2.zero;
        float distance = 0f;

        // geneに基づいて移動する
        for (int i = 0; i < gene.Length; i++) {
            position += gene[i];
            distance = Vector2.Distance(position, targetPoint);

            // ゴールに到達したら距離をゼロにする
            if (distance <= targetRadius) {
                distance = 0f;
                break;
            }
        }

        // ゴールに到達していない場合、最終位置とゴールとの距離を適応度とする
        fitness = (distance == 0f) ? 1f : 1f / distance;
    }
}

GeneticAlogrithmクラスでエラーが発生してるので,修正案を出力させます.

【入力】
一点交叉させるセクションのVector2[] childGene1 = gene1.Take(crossoverPoint).Concat(gene2.Skip(crossoverPoint)).ToArray();でエラーが発生します。修正してください。

申し訳ありませんが、前回の回答で誤りがありました。Geneの型がint[]からVector2[]に変更されたことを考慮せず、誤った回答をしてしまいました。

以下が正しいコードになります。crossoverPointには、int型を指定してください。


Vector2[] childGene1 = gene1.Take(crossoverPoint).Concat(gene2.Skip(crossoverPoint)).ToArray();

【入力】
akeとSkipがVector2の定義に含まれていません。

おっしゃる通りでした。Vector2型にはTake()やSkip()が含まれていません。

以下のようにTake()やSkip()を使用する代わりに、forループを使用して遺伝子の要素をコピーすることができます。


Vector2[] childGene1 = new Vector2[gene1.Length];
for (int i = 0; i < crossoverPoint; i++)
{
    childGene1[i] = gene1[i];
}
for (int i = crossoverPoint; i < gene2.Length; i++)
{
    childGene1[i] = gene2[i];
}

同様に、childGene2の生成も以下のようになります。

Vector2[] childGene2 = new Vector2[gene2.Length];
for (int i = 0; i < crossoverPoint; i++)
{
    childGene2[i] = gene2[i];
}
for (int i = crossoverPoint; i < gene1.Length; i++)
{
    childGene2[i] = gene1[i];
}

IndividualControllerの引数がint[] geneのままなので,これも修正させます.

【入力】
上記のIndividualControllerを修正できますか?geneはVector2型ですよね?

申し訳ありません、確かにgeneはVector2型ですね。

以下は修正したIndividualControllerのコードです。


using UnityEngine;

public class IndividualController : MonoBehaviour
{
    public Individual individual;
    public float speed = 5f;
    public float rotationSpeed = 180f;

    private int currentGeneIndex = 0;

    private void FixedUpdate()
    {
        if (currentGeneIndex >= individual.gene.Length)
        {
            return;
        }

        // 目標地点への方向ベクトルを計算
        Vector2 targetDirection = individual.gene[currentGeneIndex] - (Vector2)transform.position;
        float targetAngle = Vector2.SignedAngle(Vector2.up, targetDirection);

        // 回転と移動
        float angleDiff = targetAngle - transform.eulerAngles.z;
        if (angleDiff > 180f)
        {
            angleDiff -= 360f;
        }
        else if (angleDiff < -180f)
        {
            angleDiff += 360f;
        }

        float rotateAmount = Mathf.Sign(angleDiff) * Mathf.Min(rotationSpeed * Time.fixedDeltaTime, Mathf.Abs(angleDiff));
        transform.Rotate(Vector3.forward, rotateAmount);
        transform.position += transform.up * speed * Time.fixedDeltaTime;

        // 目標地点に到達したら、次の目標地点を設定
        if (Vector2.Distance(transform.position, individual.gene[currentGeneIndex]) < 0.1f)
        {
            currentGeneIndex++;
        }
    }

    public void SetIndividual(Individual individual)
    {
        this.individual = individual;
        currentGeneIndex = 0;
        transform.position = individual.gene[0];
        transform.rotation = Quaternion.identity;
    }
}


また、この修正版のIndividualControllerは、以下のようにして使用できます。

Individual individual = new Individual(gene);
IndividualController controller = Instantiate(individualPrefab, transform.position, Quaternion.identity);
controller.SetIndividual(individual);

流石に会話が長すぎたのか,文脈を読み取れずに出力が破綻していますね...😓
というわけでChatGPTとのやり取りは中断して,この出力に基づいて更にコードを修正します.
IndividualControllerを除いてコードを以下のように修正します.
int[] geneも必要に応じて,Vector2[] geneに変えていきます.

GeneticAlgorithm
GeneticAlgorithm.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GeneticAlgorithm : MonoBehaviour
{
    public GameObject individualPrefab; // 個体のプレハブ
    public Vector2Int geneRange = new Vector2Int(0, 3); // 塩基の値の範囲
    public int populationSize = 100; // 個体群のサイズ
    public float crossoverRate = 0.8f; // 交叉率
    public float mutationRate = 0.02f; // 突然変異率
    public int geneLength = 100; // 塩基配列の長さ
    public float targetRadius = 1f; // 目標点の半径
    public float timeScale = 1f; // タイムスケール

    private List<Individual> population = new List<Individual>(); // 個体群
    private Vector2 targetPoint; // 目標点

    void Start()
    {
        // 目標点をランダムに設定する
        targetPoint = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));

         // 空のオブジェクトを作成し、Sphereオブジェクトをアタッチする
        GameObject emptyObject = new GameObject("TargetPoint");
        targetPointObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        targetPointObject.transform.parent = emptyObject.transform;
        targetPointObject.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);

        // Sphereオブジェクトを赤色に設定する
        Renderer renderer = targetPointObject.GetComponent<Renderer>();
        renderer.material.color = Color.red;

        // 空のオブジェクトをtargetPointの位置に移動する
        emptyObject.transform.position = targetPoint;

        // 初期個体群を生成する
        for (int i = 0; i < populationSize; i++)
        {
            GameObject individualObj = Instantiate(individualPrefab, Vector3.zero, Quaternion.identity);
            individualObj.transform.parent = transform;
            Individual individual = new Individual(geneLength, geneRange);
            individualObj.GetComponent<IndividualController>().SetIndividual(individual);
            population.Add(individual);
        }

        Time.timeScale = timeScale;
    }

    void Update()
    {
        // 個体群の適応度を計算する
        foreach (Individual individual in population)
        {
            individual.CalculateFitness(targetPoint, targetRadius);
        }

        // 個体群を適応度の高い順にソートする
        population.Sort((a, b) => b.fitness.CompareTo(a.fitness));

        // 次世代の個体群を作る
        List<Individual> offspring = Crossover(population);
        Mutation(offspring);

        // 個体群を更新する
        population.Clear();
        foreach (Individual individual in offspring)
        {
            GameObject individualObj = Instantiate(individualPrefab, Vector3.zero, Quaternion.identity);
            individualObj.transform.parent = transform;
            individualObj.GetComponent<IndividualController>().SetIndividual(individual);
            population.Add(individual);
        }

        // 目標点を更新する
        if (Input.GetKeyDown(KeyCode.Space))
        {
            targetPoint = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));
        }

        // targetPointの座標を更新する
        targetPoint.x = Mathf.Sin(Time.time);
        targetPoint.z = Mathf.Cos(Time.time);

        // 空のオブジェクトをtargetPointの位置に移動する
        targetPointObject.transform.parent.transform.position = targetPoint;
    }

    // 交叉する
    private List<Individual> Crossover(List<Individual> parents)
    {
        List<Individual> offspring = new List<Individual>();
        while (offspring.Count < populationSize)
        {
            // 交叉する親を選ぶ
            Individual parent1 = parents[Random.Range(0, parents.Count)];
                 Individual parent2 = parents[Random.Range(0, parents.Count)];

        // 交叉するかどうかをランダムに決める
        if (Random.value < crossoverRate)
        {
            // 一点交叉する
            int crossoverPoint = Random.Range(1, geneLength);
            Vector2[] gene1 = parent1.Gene;
            Vector2[] gene2 = parent2.Gene;
            Vector2[] childGene1 = new Vector2[gene1.Length];
            for (int i = 0; i < crossoverPoint; i++)
            {
                childGene1[i] = gene1[i];
            }
            for (int i = crossoverPoint; i < gene2.Length; i++)
            {
                childGene1[i] = gene2[i];
            }

            Vector2[] childGene2 = new Vector2[gene2.Length];
            for (int i = 0; i < crossoverPoint; i++)
            {
                childGene2[i] = gene2[i];
            }
            for (int i = crossoverPoint; i < gene1.Length; i++)
            {
                childGene2[i] = gene1[i];
            }
            offspring.Add(new Individual(childGene1));
            if (offspring.Count < populationSize)
            {
                offspring.Add(new Individual(childGene2));
            }
        }
        else
        {
            // 交叉しない場合は親をそのまま子供にする
            offspring.Add(new Individual(parent1.gene));
            if (offspring.Count < populationSize)
            {
                offspring.Add(new Individual(parent2.gene));
            }
        }
    }

        return offspring;
    }

    // 突然変異する
    private void Mutation(List<Individual> offspring)
    {
        foreach (Individual individual in offspring)
        {
            for (int i = 0; i < geneLength; i++)
            {
                // 突然変異するかどうかをランダムに決める
                if (Random.value < mutationRate)
                {
                    // 塩基をランダムに変える
                    individual.Gene[i] = new Vector2(Random.Range(-geneRange, geneRange), Random.Range(-geneRange, geneRange));
                }
            }
        }
    }
}

Individual
Individual.cs
using UnityEngine;

public class Individual {

    // 個体の遺伝子
    private Vector2[] gene;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength, Vector2Int geneRange) {
        gene = new Vector2[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++) {
            gene[i] = new Vector2(Random.Range(-geneRange.x, geneRange.x), Random.Range(-geneRange.y, geneRange.y));
        }
    }

    // 遺伝子情報のみからインスタンス化するコンストラクタ
    public Individual(Vector2[] gene) {
        this.gene = gene;
    }

    // 遺伝子のgetter/setter
    public Vector2[] Gene {
        get { return gene; }
        set { gene = value; }
    }

    // 適応度のgetter/setter
    public float Fitness {
        get { return fitness; }
        set { fitness = value; }
    }

    // 適応度を計算する
    public void CalculateFitness(Vector2 targetPoint, float targetRadius) {
        Vector2 position = Vector2.zero;
        float distance = 0f;

        // geneに基づいて移動する
        for (int i = 0; i < gene.Length; i++) {
            position += gene[i];
            distance = Vector2.Distance(position, targetPoint);

            // ゴールに到達したら距離をゼロにする
            if (distance <= targetRadius) {
                distance = 0f;
                break;
            }
        }

        // ゴールに到達していない場合、最終位置とゴールとの距離を適応度とする
        fitness = (distance == 0f) ? 1f : 1f / distance;
    }
}

手動でコードを修正

いよいよChatGPTの出力が破綻してきたので,ここからは手を動かします.🦾

まずChatGPTでは修正できなかった,IndividualControllerを修正します.
単純にVector2[] geneが移動方向の時系列データなので,それに合わせて移動させるように書き直して上げればいいです.
また,個体が動き終わったかどうか(geneの終端に達したかどうか)の変数bool isMoveFinishedも追加しておきます.

IndividualControllerの修正コード
public int geneIndex = 0;
public bool isMoveFinished = false;

void Update()
{
    if(this.individual != null && !(isMoveFinished))
    {
        // 塩基配列に従って個体を動かす
        transform.Translate(individual.Gene[geneIndex].x * Time.deltaTime, individual.Gene[geneIndex].y * Time.deltaTime, 0);
        geneIndex = geneIndex + 1;
        if (geneIndex == individual.Gene.Length)
        {
            isMoveFinished = true;
            //Debug.Log("Move Finished!");
        }
    }
    
}

次にGeneticAlgorithmを修正します.
上記までのコードだとFitness値の順番に個体をソートしていますが,実は自然淘汰の過程が入っていないので,いつまでたっても収束しません
したがって以下のようなコードを追加します.

自然淘汰のコード

public float naturalSelectionRate = 0.1f; // 淘汰で生き残る割合

void Update()
{
    // 自然淘汰
    float selectionNumber = populationSize * naturalSelectionRate;
    population.RemoveRange((int)selectionNumber, population.Count - (int)selectionNumber);
}

また,各個体のIndividuaController.isMoveFinishedに合わせて個体群を更新するタイミングを修正します.
Vector2Int geneRangeint geneRangeに変更します.
デバッグログなども追加していきます.
修正後の全コードは以下のようになります(投げやり).
実際には何回もシーンの実行,デバッグを繰り返しながら修正してきます.

GeneticAlgorithm
GeneticAlgorithm.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GeneticAlgorithm : MonoBehaviour
{
    public List<GameObject> individualObjects = new List<GameObject>();
    public GameObject individualPrefab; // 個体のプレハブ
    public int geneRange = 5; // 塩基の値の範囲
    public int populationSize = 50; // 個体群のサイズ
    public float naturalSelectionRate = 0.1f; // 淘汰で生き残る割合
    public float crossoverRate = 0.2f; // 交叉率
    public float mutationRate = 0.1f; // 突然変異率
    public int geneLength = 100; // 塩基配列の長さ
    public float targetRadius = 1f; // 目標点の半径
    public float timeScale = 5f; // タイムスケール

    private List<Individual> population = new List<Individual>(); // 個体群
    private Vector2 targetPoint; // 目標点

    // 表示用のSphereオブジェクト
    public GameObject targetPointObject;

    

    void Start()
    {
        // 目標点をランダムに設定する
        targetPoint = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));
        // 空のオブジェクトを作成し、Sphereオブジェクトをアタッチする
        GameObject emptyObject = new GameObject("TargetPoint");
        targetPointObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        targetPointObject.transform.parent = emptyObject.transform;
        targetPointObject.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);
        // Sphereオブジェクトを赤色に設定する
        Renderer renderer = targetPointObject.GetComponent<Renderer>();
        renderer.material.color = Color.red;

        // 空のオブジェクトをtargetPointの位置に移動する
        emptyObject.transform.position = targetPoint;

        // 初期個体群を生成する
        for (int i = 0; i < populationSize; i++)
        {
            GameObject individualObj = Instantiate(individualPrefab, Vector3.zero, Quaternion.identity);
            individualObj.transform.parent = transform;
            Individual individual = new Individual(geneLength, geneRange);
            individualObj.GetComponent<IndividualController>().SetIndividual(individual);
            individualObjects.Add(individualObj);
            population.Add(individual);
        }

        Time.timeScale = timeScale;
    }

    void Update()
    {
        bool isMoveFinished = true;
        foreach (GameObject individualObj in individualObjects)
        {
            individualObj.GetComponent<IndividualController>().GetIndividual().Position = individualObj.transform.position;
            isMoveFinished &= individualObj.GetComponent<IndividualController>().isMoveFinished;
        }

        if (isMoveFinished)
        {
            Debug.Log("Generate!");
            // 個体群の適応度を計算する
            foreach (Individual individual in population)
            {
                individual.CalculateFitness(targetPoint, targetRadius);
            }

            // 個体群を適応度を降順でソートする
            population.Sort((a, b) => b.Fitness.CompareTo(a.Fitness));
            Debug.Log("Best Fitness:" + population[0].Fitness);

            // 自然淘汰
            float selectionNumber = populationSize * naturalSelectionRate;
            population.RemoveRange((int)selectionNumber, population.Count - (int)selectionNumber);


            // 次世代の個体群を作る
            List<Individual> offspring = Crossover(population);
            Mutation(offspring);

            // 個体群を更新する
            population.Clear();
            Debug.Log("Destroy!");
            foreach (GameObject individualObj in individualObjects)
            {
                Destroy(individualObj, 0f);
            }
            individualObjects.Clear();

            foreach (Individual individual in offspring)
            {
                GameObject individualObj = Instantiate(individualPrefab, Vector3.zero, Quaternion.identity);
                individualObj.transform.parent = transform;
                individualObj.GetComponent<IndividualController>().SetIndividual(individual);
                individualObjects.Add(individualObj);
                population.Add(individual);
            }

            // 目標点を更新する
            if (Input.GetKeyDown(KeyCode.Space))
            {
                targetPoint = new Vector2(Random.Range(-5f, 5f), Random.Range(-5f, 5f));

                // 空のオブジェクトをtargetPointの位置に移動する
                targetPointObject.transform.parent.transform.position = targetPoint;
            }
        }
    }

    // 交叉する
    private List<Individual> Crossover(List<Individual> parents)
    {
        List<Individual> offspring = new List<Individual>();
        while (offspring.Count < populationSize)
        {
            // 交叉する親を選ぶ
            Individual parent1 = parents[Random.Range(0, parents.Count)];
            Individual parent2 = parents[Random.Range(0, parents.Count)];

            // 交叉するかどうかをランダムに決める
            if (Random.value < crossoverRate)
            {
                // 一点交叉する
                int crossoverPoint = Random.Range(1, geneLength);
                Vector2[] gene1 = parent1.Gene;
                Vector2[] gene2 = parent2.Gene;
                Vector2[] childGene1 = new Vector2[gene1.Length];
                for (int i = 0; i < crossoverPoint; i++)
                {
                    childGene1[i] = gene1[i];
                }
                for (int i = crossoverPoint; i < gene2.Length; i++)
                {
                    childGene1[i] = gene2[i];
                }
                Vector2[] childGene2 = new Vector2[gene2.Length];
                for (int i = 0; i < crossoverPoint; i++)
                {
                    childGene2[i] = gene2[i];
                }
                for (int i = crossoverPoint; i < gene1.Length; i++)
                {
                    childGene2[i] = gene1[i];
                }
                offspring.Add(new Individual(childGene1));
                if (offspring.Count < populationSize)
                {
                    offspring.Add(new Individual(childGene2));
                }
            }
            else
            {
                // 交叉しない場合は親をそのまま子供にする
                offspring.Add(new Individual(parent1.Gene));
                if (offspring.Count < populationSize)
                {
                    offspring.Add(new Individual(parent2.Gene));
                }
            }
        }

        return offspring;
    }

    // 突然変異する
    private void Mutation(List<Individual> offspring)
    {
        foreach (Individual individual in offspring)
        {
            for (int i = 0; i < geneLength; i++)
            {
                // 突然変異するかどうかをランダムに決める
                if (Random.value < mutationRate)
                {
                    // 塩基をランダムに変える
                    individual.Gene[i] = new Vector2(Random.Range(-geneRange, geneRange), Random.Range(-geneRange, geneRange));
                }
            }
        }
    }
}

IndividualController
IndividualController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class IndividualController : MonoBehaviour
{
    private Individual individual; // 個体
    public bool isMoveFinished = false;
    public int geneIndex = 0;

    public void SetIndividual(Individual individual)
    {
        this.individual = individual;
    }

    public Individual GetIndividual()
    {
        return this.individual;
    }

    void Update()
    {
        if(this.individual != null && !(isMoveFinished))
        {
            // 塩基配列に従って個体を動かす
            Debug.Log("x:" + individual.Gene[geneIndex].x);
            Debug.Log("y:" + individual.Gene[geneIndex].y);
            transform.Translate(individual.Gene[geneIndex].x * Time.deltaTime, individual.Gene[geneIndex].y * Time.deltaTime, 0);
            geneIndex = geneIndex + 1;
            if (geneIndex == individual.Gene.Length)
            {
                isMoveFinished = true;
                //Debug.Log("Move Finished!");
            }
        }
        
    }
}
Individual
Individual.cs

using UnityEngine;

public class Individual
{

    // 個体の遺伝子
    private Vector2[] gene;

    private Vector2 position;

    // 適応度
    private float fitness;

    // 個体の初期化
    public Individual(int geneLength, int geneRange)
    {
        gene = new Vector2[geneLength];

        // geneをランダムな値で初期化する
        for (int i = 0; i < geneLength; i++)
        {
            gene[i] = new Vector2(Random.Range(-geneRange, geneRange), Random.Range(-geneRange, geneRange));
        }
    }

    // 遺伝子情報のみからインスタンス化するコンストラクタ
    public Individual(Vector2[] gene)
    {
        this.gene = gene;
    }

    // 遺伝子のgetter/setter
    public Vector2[] Gene
    {
        get { return gene; }
        set { gene = value; }
    }

    public Vector2 Position
    {
        get { return position; }
        set { position = value; }
    }

    // 適応度のgetter/setter
    public float Fitness
    {
        get { return fitness; }
        set { fitness = value; }
    }

    // 適応度を計算する
    public void CalculateFitness(Vector2 targetPoint, float targetRadius)
    {
        float distance = 0f;

        // geneに基づいて移動する
        for (int i = 0; i < gene.Length; i++)
        {
            distance = Vector2.Distance(position, targetPoint);

            // ゴールに到達したら距離をゼロにする
            if (distance <= targetRadius)
            {
                distance = 0f;
                break;
            }
        }

        // ゴールに到達していない場合、最終位置とゴールとの距離の分母を適応度とする
        fitness = (distance == 0f) ? 1f : 1f / distance;
    }
}

実行してみる

シーンに戻って上の再生ボタン▶を押すと開始します.

image.png

最初バラバラだった集団が,世代を経るごとに目標(赤丸⭕)に向かっていってますね🤗

video.gif

完走した感想

やはりソースコードを0から書くよりもフレームワークあたりをChatGPTに書かせるのが楽だと思いました(小並感).👍
今回の場合は体感8割方GPTくん🤖がコード生成・修正して,残りは自分で追記した感じ.👊
ただ,UnityとGAについてある程度知っておかないと完成させるのは無理かなぁ...😓
GPT4なる代物がマルチモーダル📺🎤💬らしいので,Unityのシーン作成とかがもっと楽になるのかな?🤔

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?