Unity-机器学习

遗传算法案例一:基于感官的移动

2018-06-07  本文已影响0人  veinsvx

首先,看一下我们最终训练出来的结果是这样的(这个gif图片我截得非常不满意,后面有一张比较完整的训练动图,大家可以先拉到后方看一下,这篇文章可是非常长的。。。)


遗传算法结果.gif

第一步:在场景里新建一个Cube(黑色)和一个Plane(蓝色),并改变改变他们的缩放和位置,实现如下图所示的场景;然后将Cube命名为Dead,Plane命名为Ground。


基本场景搭建.png
第二步:新建一个胶囊体和一个立方体,将立方体设为胶囊体的子物体,然后将胶囊体命名为People,立方体命名为Eye,并且调整他们的位置和旋转角度成如下图所示:(请务必保持Cube的轴向如图所示(60,0,0),因为到时我们会通过Cube的蓝色轴方向发射射线,来检测地面)
物体的轴向.png
物体的层级关系.png

第三步:
1.选中People,给他添加Rigibody组件,并冻结刚体xyz轴的旋转。
2.修改People的层,将layer改为bot(需要自己先创建)。


物体需要更改的属性参数.png

第四步:
1.新建一个空物体,并拖拽到蓝色地面的正上方,以后他将会作为我们People的出生点。
2.将People变成预制体,并删掉场景里的People。

第五步:新建3个脚本,分别命名为MyDNA,MyBrain,MyPopulationManager

MyDNA脚本编写以下代码

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

public class MyDNA
{
    //这个列表里存储的数值,是为了影响物体的行动
    List<int> genes = new List<int>();
    int dnaLength = 0;
    int maxValues = 0;
        
    /// <summary>
    /// 自己编写构造函数来实现这个类实例化时,进行一些初始的赋值
    /// </summary>
    /// <param name="l">基因列表的长度</param>
    /// <param name="v">基因列表中影响物体行动的随机整数的最大值</param>
    public MyDNA (int l,int v)
    {
        dnaLength = l;
        maxValues = v;
        SetRandom();
    }
        
    /// <summary>
    /// 对基因列表进行随机数赋值(因为我们的目标是实现优胜劣汰,所以要有随机数产生各种
    /// 各样的“性状”,来进行训练)
    /// </summary>
    private void SetRandom()
    {
        genes.Clear();
        for (int i = 0; i < dnaLength; i++)
        {
            genes.Add(Random.Range(0, maxValues));
        }
    }
        
        
    /// <summary>
    /// 这个函数会将传进来的父亲和母亲的DNA序列进行重组(各取一半,取d1的基因列表
    /// 前半部分,取d2的基因列表后半部分)
    /// </summary>
    /// <param name="d1">父亲的DNA</param>
    /// <param name="d2">母亲的DNA</param>
    public void Combine(MyDNA d1,MyDNA d2)
    {
        for (int i = 0; i < dnaLength; i++)
        {
            if (i < dnaLength / 2.0)
            {
                int c = d1.genes[i];
                genes[i] = c;
            }
            else
            {
                int c = d2.genes[i];
                genes[i] = c;
            }
        }
    }
    
    /// <summary>
    /// 对基因列表的某个随机的元素进行赋一个随机的"行为"值(因为列表里的元素的值
    /// 是可以影响物体的运动行为)
    /// </summary>
    public void Mutate()
    {
        genes[Random.Range(0, dnaLength)] = Random.Range(0, maxValues);
    }
    
    /// <summary>
    /// 返回基因列表里的某个元素的值
    /// </summary>
    /// <param name="pos">元素的下标位置</param>
    /// <returns></returns>
    public int GetGene(int pos)
    {
            return genes[pos];
    }
}

MyBrain脚本编写如下代码

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

public class MyBrain : MonoBehaviour
{
    int DNALength = 2;
    //用来记录存活时间,
    public float timeAlive;
    public float timeWalking;
    public MyDNA dna;
    public GameObject eyes;
    bool alive = true;
    bool seeGround = true;
        
    
    /// <summary>
    /// 这个是unity的回调函数,当两个碰撞体/刚体相撞时,unity会自动调用此函数
    /// </summary>
    /// <param name="collision">碰撞到的物体</param>
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.name == "Dead")
        {
            alive = false;
            timeWalking = 0;
            timeAlive = 0;
        }
    }

    /// <summary>
    /// Brain进行初始化赋值的函数,每个挂在这个脚本的物体,都会进行初始化赋值
    /// </summary>
    public void Init()
    {
        //列表长度为2,每个元素存在3种动作状态,实例化后将随机赋予一种
        //实例化后,每个数字的意义:0 向前走 1左转 2右转
        //dna.genes[0]看到地面,dna.genes[1]看不到地面
        dna = new MyDNA(DNALength, 3);
        timeAlive = 0;
        alive = true;
    }
    
    void Update()
    {
        if (!alive) return;
        Debug.DrawRay(eyes.transform.position, eyes.transform.forward * 10, Color.red, 10);
        seeGround = false;
        RaycastHit hit;
        if (Physics.Raycast(eyes.transform.position, eyes.transform.forward * 10, out hit))
        {
            if (hit.collider.gameObject.name == "Ground")
            {
                seeGround = true;
            }
        }
        //不能让他们一直无限制的运行下去,即使是最优秀的基因,他们最多存活时间也不可能超过极限。
        timeAlive = MyPopulationManager.elapsed;

        float turn = 0;
        float move = 0;
        if (seeGround)
        {
            if (dna.GetGene(0) == 0) { move = 1; timeWalking += 1; }
            else if (dna.GetGene(0) == 1) turn = -90;
            else if (dna.GetGene(0) == 2) turn = 90;
        }
        else
        {
            if (dna.GetGene(1) == 0) { move = 1; timeWalking += 1; }
            else if (dna.GetGene(1) == 1) turn = -90;
            else if (dna.GetGene(1) == 2) turn = 90;
        }

        this.transform.Translate(0, 0, move * 0.1f);
        this.transform.Rotate(0, turn, 0);
    }
}

MyPopulationManager编写如下代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
    
public class MyPopulationManager : MonoBehaviour
{
    public GameObject botPrefab;
    public int populationSize = 50;
    //用来储存实例化出来的"人"
    List<GameObject> population = new List<GameObject>();
    //可以看做是人类寿命。
    public static float elapsed = 0;
    public float trailTime = 5;
    int generation = 1;

    GUIStyle guiStyle = new GUIStyle();      
    /// <summary>
    /// 通过UI的方式向屏幕输出一些信息,方便查看当前进化状态
    /// </summary>
    private void OnGUI()
    {
        guiStyle.fontSize = 25;
        guiStyle.normal.textColor = Color.white;
        GUI.BeginGroup(new Rect(10, 10, 250, 150));
        GUI.Box(new Rect(0, 0, 140, 140), "Stats", guiStyle);
        GUI.Label(new Rect(10, 25, 200, 30), "Gen:" + generation, guiStyle);
        GUI.Label(new Rect(10, 50, 200, 30), string.Format("Time:{0:0.00}", elapsed), guiStyle);
        GUI.Label(new Rect(10, 75, 200, 30), "Population:" + population.Count, guiStyle);
        GUI.EndGroup();
    }
    

    void Start()
    {  
        for (int i = 0; i < populationSize; i++)
        {
            Vector3 startingPos = new Vector3(this.transform.position.x + Random.Range(-2, 2),
            this.transform.position.y,
            this.transform.position.z + Random.Range(-2, 2));
            GameObject b = Instantiate(botPrefab, startingPos, this.transform.rotation);
            b.GetComponent<MyBrain>().Init();
            population.Add(b);
        }
    }
    
    /// <summary>
    /// 这个函数的核心就是调用了MyDNA脚本里的Combine函数,实现父母基因的重组功能。
    /// 此外还有百分之一的概率会调用MyDNA脚本里的Mutate函数,实现自然界中的基因突变功能。
    /// </summary>
    /// <param name="parent1">父亲</param>
    /// <param name="parent2">母亲</param>
    /// <returns></returns>
    GameObject Breed(GameObject parent1, GameObject parent2)
    {
        Vector3 startingPos = new Vector3(this.transform.position.x + Random.Range(-2, 2),this.transform.position.y,this.transform.position.z + Random.Range(-2, 2));
        GameObject offspring = Instantiate(botPrefab, startingPos, this.transform.rotation);
        MyBrain b = offspring.GetComponent<MyBrain>();
        //模仿自然界中的变异,有百分之一的概率,在DNA序列中的随机位置产生随机的数字。
        //否则的话,会将父亲的基因和母亲的基因进行分离重组。
        //在MyDNA脚本里有注释解释了Combine函数的作用。
        if (Random.Range(0, 100) == 1)
        {
            b.Init();
            b.dna.Mutate();
        }
        else
        {
            b.Init();
            b.dna.Combine(parent1.GetComponent<MyBrain>().dna,parent2.GetComponent<MyBrain>().dna);
        }
        return offspring;
    }
    
    /// <summary>
    /// 这个脚本的核心
    /// </summary>
    void BreedNewPopulation()
    {
        //这条语句的作用就是实现:根据物体的存活时间和行走时间的长短,来对元素重新排序(升序)
        //因为我们认为行走比存活更重要一些,所以对行走进行加权。
        //查询表达式 
        //List<GameObject> sortedList = (from o in population
        //                               orderby o.GetComponent<MyBrain>().timeAlive+
        //                                       o.GetComponent<MyBrain>().timeWalking*5
        //                               select o).ToList();
            
        //用Lambda表达式
        List<GameObject> sortedList = population.OrderBy(o => (o.GetComponent<MyBrain>().timeWalking * 5 +o.GetComponent<MyBrain>().timeAlive)).ToList();
    
        //把上一代的物体清除掉,用来储存新的一代的物体。
        population.Clear();
        for (int i = (int)(sortedList.Count / 2.0f) - 1; i < sortedList.Count - 1; i++)
        {
            //假设有两个基因良好的物体1,2。
            //首先让1当父亲,2当母亲来组成一个新的基因
            //然后让2当父亲,1当母亲来组成一个新的基因
            //目的1.是加快产生优良基因的速度2.是为了能让这些存活下来排名较高的基因能填满人口列表
            population.Add(Breed(sortedList[i], sortedList[i + 1]));
            population.Add(Breed(sortedList[i + 1], sortedList[i]));
        }
        //销毁掉存放在列表的排序好的所有的上一代物体。
        for (int i = 0; i < sortedList.Count; i++)
        {
            Destroy(sortedList[i]);
        }
        generation++;
    }
    
    
    void Update()
    {
        elapsed += Time.deltaTime;
        //当场景里的物体存活时间超过极限时间,强制结束这一代,重新开始产生新人
        if (elapsed >= trailTime)
        {
            BreedNewPopulation();
            elapsed = 0;
        }
    }
}

第六步:将MyBrain脚本添加到People预制体上,并将Eye拖到空开的字段上。


场景环境配置1.png

第七步:将MyPopulationManager脚本添加到PopulationManager物体上并将People预制体拖动到空开的字段上。


场景环境配置2.png

第八步:点击菜单栏Edit—>ProjectSettings—>Physics,关闭bot层碰撞。(因为People上面有刚体,我们的People相互碰撞,会影响我们的一些数据,所以我们要关闭掉它)


场景环境配置3.png

第九步:运行,等待结果。


遗传算法训练过程和结果.gif

第十步:因为篇幅所限,我没有教大家如何调试及优化,直接把项目最终形式的代码都写好了。其实做项目不要想着一上来就做的完美,要先完成一个小功能,在一点点的进行调试完善。

最后也希望大家水平越来越高。

上一篇下一篇

猜你喜欢

热点阅读