Unity中利用遗传算法训练MLP
Unity中利用遗传算法训练MLP
梯度下降法训练神经网络通常需要我们给定训练的输入-输出数据,而用遗传算法会便捷很多,它不需要我们给定好数据,只需要随机化多个权重进行N次“繁衍进化”,就可以得出效果不错的网络。
这种训练方式的好处就是不需要训练用的预期输出数据,适合那类可以简单通过环境交互判断训练好坏的神经网络AI。当然,坏处就是训练的时间可能需要很长很长,尤其是神经网络比较庞大时。
完整项目gitee链接:点击这里
用Compute Shader实现神经网络
神经网络的计算一般都用矩阵优化,像python语言者学习实现神经网络时,通常会借助numpy的torch进行计算,加速运算过程。
个人曾经尝试过以单个神经元为最小单位实现的神经网络,但其实这种做法并不好。后来尝试过使用C#的MathNet库中的矩阵,但它发现并没有在硬件层面对矩阵运算进行加速。虽说对于小规模网络,即便不加速计算也不会太影响性能,但总觉得得考虑得更长远些。
想到神经网络的预测过程中,其实我们只关心输入层与输出层,而隐藏层的那些计算结果其实根本不在乎。欸~这似乎很适合用Compute Shader来完成!
隐藏层计算的结果完全可以只在留在ComputeBuffer,只有输入层需要将数据写入以及输出层将结果读取,CPU与GPU间数据的传递并不会很多;而且Compute Shader强大的并行计算能力也可以加速我们的运算过程。
但由于本文注意还是像讲遗传算法,就不喧宾夺主了,具体的实现会包含到文末的项目链接中。
遗传算法
在中学生物课本有提到达尔文的自然选择学说四个主要观点:过度繁殖、生存竞争、遗传和变异、适者生存。遗传算法就是借鉴了其中的思想,它的整个流程及其相似:
初始化种群
在本例中,我们想要获取神经网络中各层合适的权重与偏置的值,来使神经网络的输出符合预期,所以我们将整个神经网络的所有权重与偏置视为一个个体。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace JufGame
{
[CreateAssetMenu(menuName = ("JufGame/AI/ANN/WeightBias"), fileName = ("WeightAndBias_"))]
public class WeightBiasMemory : ScriptableObject
{
[Serializable]
public struct LayerWeightAndBias
{
public int inputCount;
public int outputCount;
public float[] weights;
public float[] bias;
}
[Tooltip("各全连层的权重和偏置")]
public LayerWeightAndBias[] WeiBiasArray;
[Tooltip("全连接层的compute shader")]
public ComputeShader affine;
[Tooltip("激活函数的compute shader")]
public ComputeShader activateFunc;
[Tooltip("损失函数的compute shader")]
public ComputeShader lossFunc;
[Tooltip("当前损失函数在反向传播时是否要载入上次输出,用于sigmoid等函数")]
public bool isLoadLastOutput;
[Header("随机初始化权重")]
[Tooltip("是否要随机初始化")]
public bool isRandomWeightAndBias = false;
[Tooltip("当前权重是否是训练成功后的")]
public bool isFinishedWeightAndBias = false;
[Tooltip("随机初始化的最大值和最小值")]
public float minRandValue = -1, maxRandValue = 1;
[Tooltip("是否随机化权重")]
public bool isRandomBias = false;
private void OnValidate()
{
if(isRandomWeightAndBias && !isFinishedWeightAndBias)
{
RandomWeightAndBias(ref WeiBiasArray, minRandValue, maxRandValue, isRandomBias);
isRandomWeightAndBias = false;
}
}
/// <summary>
/// 随机初始化权重和偏置
/// </summary>
/// <param name="WeiBiasArray">被随机化的数层权重和偏置</param>
/// <param name="minRandValue">最小随机值</param>
/// <param name="maxRandValue">最大随机值</param>
/// <param name="isRandomBias">偏置是否也要随机化,如果false则置0</param>
public static void RandomWeightAndBias(ref LayerWeightAndBias[] WeiBiasArray, float minRandValue,
float maxRandValue, bool isRandomBias = false)
{
var rand = new System.Random();
foreach (var wb in WeiBiasArray)
{
float range = maxRandValue - minRandValue;
// 初始化权重
for (int i = 0; i < wb.weights.Length; ++i)
{
wb.weights[i] = (float)(rand.NextDouble() * range + minRandValue); // 使用指定范围生成随机数
}
// 初始化偏置
for (int i = 0; i < wb.bias.Length; ++i)
{
wb.bias[i] = isRandomBias ? (float)(rand.NextDouble() * range + minRandValue) : 0;
}
}
}
/// <summary>
/// 深拷贝所有层的权重与偏置
/// </summary>
/// <param name="source">拷贝源</param>
/// <param name="target">目标处</param>
public static void DeepCopyAllLayerWB(ref LayerWeightAndBias[] source, ref LayerWeightAndBias[] target)
{
for(int i = 0, j; i < source.Length; ++i)
{
var wb = target[i];
for (j = 0; j < wb.weights.Length; ++j)
{
wb.weights[j] = source[i].weights[j];
}
for (j = 0; j < wb.bias.Length; ++j)
{
wb.bias[j] = source[i].bias[j];
}
}
}
/// <summary>
/// 交换所有层的权重与偏置
/// </summary>
public static void DeepSwap(ref LayerWeightAndBias[] a, ref LayerWeightAndBias[] b)
{
float tp;
for(int i = 0, j; i < a.Length; ++i)
{
var wb = b[i];
for (j = 0; j < wb.weights.Length; ++j)
{
tp = wb.weights[j];
wb.weights[j] = a[i].weights[j];
a[i].weights[j] = tp;
}
for (j = 0; j < wb.bias.Length; ++j)
{
tp = wb.bias[j];
wb.bias[j] = a[i].bias[j];
a[i].bias[j] = tp;
}
}
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace JufGame
{
//遗传算法中的个体,具体逻辑需继承该类扩展
public class GAUnit : MonoBehaviour
{
public WeightBiasMemory memory;
public float FitNess;
public bool isOver;
public virtual void ReStart()
{
isOver = false;
FitNess = 0;
}
}
}
然后初始化指定数量的该类个体作为初始种群,担任原始父本,并让个体权重与偏置随机化。这样一来,每个个体就都是不同的了,至于它们中谁具有更好的潜质,就需要通过竞争得知了。
竞争
我们让游戏中的使用神经网络决策的AI个体,分别应用种群中各个体作为神经网络的权重与偏置,并直接应用神经网络进行决策。由于这些权重与偏置都是随机的,执行的效果几乎都不堪入目。
private void FixedUpdate()
{
if(isEndTrain) //如果选择结束训练,则保留当前最好的个体
{
SaveBest();
}
else if(TrainUnit.isOver) //如果当前训练单位的训练结束
{
parents[curIndex].fitness = TrainUnit.FitNess;
TrainUnit.ReStart();
//轮流将当前父本中个体权重与偏置赋给训练单位进行决策
if(++curIndex < AllPopulation)
{
WeightBiasMemory.DeepCopyAllLayerWB(ref parents[curIndex].WB, ref TrainUnit.memory.WeiBiasArray);
}
//……
}
}
但我们需要“矮子里拔高个”,设计一个评估函数计算每个个体的适应度。比如评估一个小车,我们就可以通过它行驶的距离、速度等进行加权和得到一个适应度。总之,要确保评估函数的计算结果能合理表达出决策结果的好坏。
繁殖与变异
现在,我们要随机从原始父本中选出两个不同的个体,进行繁殖得到两个新的个体。
这个繁殖的过程很简单,与染色体互换的过程极其相似。对于新权重和偏置,随机从两个作为父本的个体选择一个,选取其对应部分的值。每个位置都这么做一遍,就得到了两个新个体(子代)。
但值得注意的是,如果是自然界,其实更优秀的个体会拥有更大的繁殖机会。所以,我们可以使用一种叫轮盘赌的随机选择方式,代替之前的纯随机选择。这样,就可以让适应度更高的个体有更大机会变成父本,但也保留弱小个体被选中的可能。
以上图蓝色段被选中的机会为例,原本它应当为0.4,也就是生成一个0~1的随机数,如果随机数的值小于0.4,那么蓝色就被选中。
而转化为轮盘赌后,蓝色段的部分为0.227~0.59,也就是只有随机值落在这个范围内时,它才会被选中。如果是其它值,就留给其它段了。
可以明显看出,这样的选择更照顾整体,原本大的值会有更大概率被选中,但小的也有机会。代码实现也非常简单:
//计算轮盘赌概率分布
private void CalcRouletteWheel()
{
float totalFitness = 0f;
for (int i = 0; i < parents.Length; i++)
{
totalFitness += parents[i].fitness;
}
float cumulativeSum = 0f;
for (int i = 0; i < cumulativeProbabilities.Length; i++)
{
cumulativeSum += (parents[i].fitness / totalFitness);
cumulativeProbabilities[i] = cumulativeSum;
}
}
//轮盘赌随机下标
private int GetRouletteRandom()
{
float rand = Random.value;
// 选择个体
for (int i = 0; i < cumulativeProbabilities.Length; i++)
{
if (rand < cumulativeProbabilities[i])
{
return i;
}
}
// 如果没有找到,返回最后一个个体(通常不会发生)
return cumulativeProbabilities.Length - 1;
}
现在还有一个问题,仅仅只是交叉互换,那么最终得到的最优个体也只会囿于初始种群。如果初始种群中无论怎么交叉互换都无法得到优良个体又该怎么办?这时就得靠变异了。
变异的手段并不固定,只要能做到突破就可以。我的做法就是在原本数值的基础上随机增减一个小数值。但变异通常不能太频繁发生,我们要为它规定一个较小的概率,否则大规模的变异反而会破坏优良父本的传承。
变异的发生可以与繁殖放在一起:
private void GetChild()
{
int p1, p2;
for(int i = 0; i < parents.Length; i += 2)
{
p2 = p1 = GetRouletteRandom();
var curWB = parents[i].WB;
while(p1 == p2 && parents.Length > 1)
{
p2 = GetRouletteRandom();
}
for(int j = 0; j < curWB.Length; ++j)
{
var curW = curWB[j].weights;
for (int k = 0; k < curW.Length; ++k)
{
if(Random.value < 0.5)
{
children[i].WB[j].weights[k] = parents[p2].WB[j].weights[k];
if (i + 1 < children.Length)
{
children[i + 1].WB[j].weights[k] = parents[p1].WB[j].weights[k];
}
}
else
{
children[i].WB[j].weights[k] = parents[p1].WB[j].weights[k];
if (i + 1 < children.Length)
{
children[i + 1].WB[j].weights[k] = parents[p2].WB[j].weights[k];
}
}
if (Random.value < mutationRate) //随机变异,mutationRate为变异率
{
//mutationScale为变异的幅度,即变异带来的数值增减幅度
children[i].WB[j].weights[k] += Random.Range(-mutationScale, mutationScale);
}
if (i + 1 < children.Length && Random.value < mutationRate)
{
children[i + 1].WB[j].weights[k] += Random.Range(-mutationScale, mutationScale);
}
}
var curB = curWB[j].bias;
for (int k = 0; k < curB.Length; ++k)
{
if(Random.value < 0.5)
{
children[i].WB[j].bias[k] = parents[p2].WB[j].bias[k];
if (i + 1 < children.Length)
{
children[i + 1].WB[j].bias[k] = parents[p1].WB[j].bias[k];
}
}
else
{
children[i].WB[j].bias[k] = parents[p1].WB[j].bias[k];
if (i + 1 < children.Length)
{
children[i + 1].WB[j].bias[k] = parents[p2].WB[j].bias[k];
}
}
if (Random.value < mutationRate) //随机变异,mutationRate为变异率
{
//mutationScale为变异的幅度,即变异带来的数值增减幅度
children[i].WB[j].bias[k] += Random.Range(-mutationScale, mutationScale);
}
if (i + 1 < children.Length && Random.value < mutationRate)
{
children[i + 1].WB[j].bias[k] += Random.Range(-mutationScale, mutationScale);
}
}
}
}
}
优胜劣汰
在繁殖得到新的一批子代后,我们将这些子代也进行一次竞争,这样所有的父代、子代就都有各自的适应度了。我们将它们一起根据适应度进行排序,显然,如果父代的数量是N,那么总共就有2N个个体。在排序后我们选择前N个个体做为本轮的优胜者,也是下轮的新父本。
//在父代和子代组成的整体中选出适应度高的新父代
private void GetBest()
{
for(int i = 0; i < totalPopulation.Length; ++i)
{
if (i < AllPopulation)
totalPopulation[i] = parents[i];
else
totalPopulation[i] = children[i - AllPopulation];
}
Array.Sort(totalPopulation, (a, b) => b.fitness.CompareTo(a.fitness));
}
也就是说,有更高适应度的个体能存活下来,其他的就被淘汰。而这些存活下来的个体会不断重复这个过程。在数次 (或是无数次 迭代后,我们就一定可以得到理想中的个体(比如适应度超高的那种)。这时,我们就可以结束算法了。
实例:赛道小球
用一个比较简单的实例,串一遍整个过程。我们将训练一个用来跑赛道的小球。
1. 创建神经网络
在我的实现中,已将网络结构以ScriptObject
形式存储,我们先新建一个,在Project
下右键Create/ANN/WeightAngBias
:
然后设置具体结构,这次要完成的工作比较简单,就是训练一个可以绕圈跑的小球,所以网络结构比较简单。两个隐藏层足矣(对应Wei Bias Array
的两个元素),这个神经网络接受三个输入,输出两个数据。
至于中间其它参数的设计要符合神经网络的结构,具体来说就是:每一层的Weights
数量要等于InputCount * OutputCount
;除了第一层外,其它层的InputCount
要等于上一层的OutputCount
。(如果你对神经网络有所了解,那就能理解这些
Affine
固定使用同名的Compute Shader,至于Activate Func
和Loss Func
其实可以不管,因为遗传算法训练用不着。
2. 创建遗传个体
场景中已有一个球形物体,挂载了继承GAUnit
的Car
脚本 (原本是想做成车的
神经网络的3个输入数据就来自小球的三条射线检测:
private void CheckEnv()
{
totalSensor = 0;
for(int i = 0; i < direactions.Length; ++i)
{
var dir = transform.TransformDirection(direactions[i]);
if(Physics.Raycast(transform.position, dir, out RaycastHit hit,
rayLength[i], hitMask, QueryTriggerInteraction.Ignore))
{
inputVal[i] = hit.distance / rayLength[i];
}
else
{
inputVal[i] = 1;
}
totalSensor += inputVal[i];
}
}
神经网络的两个输出分别用来控制,移动速度以及角位移:
private void RunMLP()
{
myMLP.Predict(inputVal);
moveVel = transform.TransformDirection(new Vector3( 0, 0, myMLP.outputData[0] * 10));
moveVel = Vector3.MoveTowards(rb.velocity, moveVel, 0.02f);
rb.velocity = moveVel;
transform.eulerAngles += new Vector3(0, myMLP.outputData[1] * 90 * Time.fixedDeltaTime, 0);
}
我们还需要设计一个衡量适应度的函数。而因为我们打算训练一个能在赛道正中央前进的小球,所以这里主要考虑「位移距离、 速度、 检测距离」以及「是否有碰到墙」。一旦isOver
为true后,GA
会让小球回到起始点,进行新的训练.
private void CalculateFitness()
{
totalMoveDis += Vector3.Distance(transform.position, lastPos);
avgSpeed = totalMoveDis / runningTime;
//适应度 与 位移距离、 速度、 检测距离 有关
FitNess = (totalMoveDis*distanceMultipler) + (avgSpeed*avgSpeedMultiplier) + ( totalSensor / inputVal.Length *sensorMultiplier);
if (runningTime > 20 && FitNess < 40) //存活足够时间且适应度不低时,结束本轮
{
isOver = true;
}
if(FitNess >= 1000) //适应度很高时,直接算成功,结束
{
isOver = true;
}
}
private void OnCollisionEnter(Collision other)
{
if(!isOver && hitMask.ContainLayer(other.gameObject.layer))
{
isOver = true; //碰到墙上,直接结束
rb.velocity = Vector3.zero;
}
}
这样,个体的设置就搞定了,它将作为训练时的运行个体。
3. 遗传算法训练器
在场景中任意激活的物体上,挂载GA
脚本,并将Car拖拽在指定位置:
这个脚本中All Population
是初始化种群的数量,这里填50。但注意,这并不会让场景中出现50个小球,而是每轮小球得重复50次来逐一尝试种群中的个体。Mutation Rate
是变异率,这里填0.3;Mutation Scale
是变异幅度默认为1即可。
至于绿色框内的,Is End Train
用来结束遗传算法的训练,并将最好的结果保存到先前的ScriptObject中。其余只是用来观察小球当前训练情况而已。
一切就绪后,点击运行即可训练。训练时我们可以调整Project Settings/Time/Time Scale
加速训练。
需要注意的是,当你想测试小球时,一定要关闭GA脚本,或者将Train Unit
置空,否则一运行就会又重新训练Train Unit
中的个体。比如这里,花了4分钟训练出了一个能走圈的小球 (虽说是倒着走的,保存训练结果,就要先勾上Is End Train
,再终止运行,而后取消启用GA
;这时再运行,会发现小球可以自动绕圈走了:
尾声
完整的训练视频在项目中有。这篇写的比较烂捏,如果了解神经网络,或许这篇就好看懂些。大伙感兴趣就把玩下项目吧,也可以尝试更复杂的赛道,更庞大的网络 (估计训练会很久。
Unity中利用遗传算法训练MLP的更多相关文章
- Unity中利用柏林噪声(perlinnoise)制作摇摆效果
perlinnoise是unity中Mathf下的一个函数,需要两个float参数x和y进行采样,返回一个0-1的float型. 项目里经常要随机摇摆某些东西,比如摄像机,某个随机运动的目标等等,都可 ...
- Unity中利用委托与监听解耦合的思路
这篇随笔是一篇记录性的随笔,记录了从http://www.sikiedu.com/my/course/304,这门课程中学到的内容,附带了一些自己的思考. 一.单例模式的应用 首先假想一种情况,现在需 ...
- Unity中利用光线投射实现摄像机拉近追踪对象
项目中要求实现摄像机跟踪人物移动,并且可以水平360°,上下接近180°的自由旋转.这些操作网上已近有很多版本的实现,在此不再赘述.我在此想说的是,当摄像机移动到场景边界,或者触碰到场景中的障碍物的时 ...
- Unity3D题目,Unity中利用GUI输出九九乘法表
网上看到的这题,下面贴出源代码 using UnityEngine;using System.Collections; public class c99 : MonoBehaviour//C#脚本名: ...
- unity中利用纯物理工具制作角色移动跳跃功能
using System.Collections;using System.Collections.Generic;using UnityEngine; public class Player : M ...
- R语言中的遗传算法详细解析
前言 人类总是在生活中摸索规律,把规律总结为经验,再把经验传给后人,让后人发现更多的规规律,每一次知识的传递都是一次进化的过程,最终会形成了人类的智慧.自然界规律,让人类适者生存地活了下来,聪明的科学 ...
- 【原创翻译】初识Unity中的Compute Shader
一直以来都想试着自己翻译一些东西,现在发现翻译真的很不容易,如果你直接把作者的原文按照英文的思维翻译过来,你会发现中国人读起来很是别扭,但是如果你想完全利用中国人的语言方式来翻译,又怕自己理解的不到位 ...
- NGUI研究院之在Unity中使用贝塞尔曲线(六)[转]
鼎鼎大名的贝塞尔曲线相信大家都耳熟能详.这两天因为工作的原因需要将贝塞尔曲线加在工程中,那么MOMO迅速的研究了一下成果就分享给大家了哦.贝塞尔曲线的原理是由两个点构成的任意角度的曲线,这两个点一个是 ...
- 【转】Unity中的协同程序-使用Promise进行封装(三)
原文:http://gad.qq.com/program/translateview/7170967 译者:崔国军(飞扬971) 审校:王磊(未来的未来) 在这个系列的最后一部分文章,我们要通过 ...
- 移植UE4的模型操作到Unity中
最近在Unity上要写一个东东,功能差不多就是在Unity编辑器上的旋转,移动这些,在手机上也能比较容易操作最好,原来用Axiom3D写过一个类似的,有许多位置并不好用,刚好在研究UE4的源码,在模型 ...
随机推荐
- ARP协议介绍与投毒攻击
目录 ARP是什么? ARP协议工作原理 ARP攻击原理 攻击软件 防范 Reference ARP是什么? ARP是通过网络地址(IP)来定位机器MAC地址的协议,它通过解析网络层地址(IP)来找寻 ...
- Docker通信全视角:原理、实践与技术洞察
本文全面深入地探讨了Docker容器通信技术,从基础概念.网络模型.核心组件到实战应用.详细介绍了不同网络模式及其实现,提供了容器通信的技术细节和实用案例,旨在为专业从业者提供深入的技术洞见和实际操作 ...
- Vue禁止用户复制文案 + 兼容 IE
vue必须要加载完才可以操作dom,或者在mounted和created时使用this.$nextTick方法,使dom生成后进行相关操作. created() { this.$nextTick(() ...
- [oeasy]python0021_宝剑镶宝石_爱之石中剑_批量替换_特殊字符_特殊颜色
继续运行 回忆上次内容 上次 运行了 game.py 分析了 game.py 也大致读懂了 game.py 添加图片注释,不超过 140 字(可选) 这个 程序 可以进一步 进行修改吗 ...
- CF858C 题解
洛谷链接&CF 链接 本篇题解为此题较简单做法及较少码量,并且码风优良,请放心阅读. 题目简述 给你一个均为小写字母的字符串,如果它的子串同时满足: 三个连着的辅音字母. 这一段连着的辅音字母 ...
- SQL连续查询问题拓展—记上海拼多多非技术岗面试真题
真巧,昨天刚写了关于数据库连续问题的解决方案,没想到今天下午两点就有朋友在上海拼多多面试非技术岗位中就遇到了相似的问题.下面是原题: 一个最大连续支付失败的次数 有一张支付流水表pay;字段如下 id ...
- 【Mybatis】Bonus01 笔记资料
对原生JDBC程序的问题总结 public void jdbc() { // 声明Connection对象 Connection con; // 驱动程序名 String driver = " ...
- 【JS】03 BOM 浏览器对象模型
BOM :Broswer Object Model 浏览器对象模型 核心对象是window对象,window对象又可以操作以下的常见对象: - frames[] 窗口对象数组? 浏览器可以打开多个窗口 ...
- 【Redis】05 持久化
持久化概述 Redis提供了不同的持久性选项: 1.RDB持久性按指定的时间间隔执行数据集的时间点快照. 2.AOF持久性会记录服务器接收的每个写入操作,这些操作将在服务器启动时再次播放,以重建原始数 ...
- 切记:使用nvidia omniverse时一定要用2T的固态硬盘
最近在用nvidia omniverse时突然发现这样的一个问题,那就是这个软件实在是太太了,一个组件就4个多GB大小,如果安装几个组件后那么几十GB的硬盘就没有了,同时由于这个体积太大,因此再启动和 ...