当前位置: 首页 > news >正文

Unity中的神经网络遗传算法实战

目录

用Compute Shader实现神经网络

遗传算法

初始化种群

竞争

繁殖与变异

优胜劣汰

实例:赛道小球

1. 创建神经网络

2. 创建遗传个体

3. 遗传算法训练器

尾声


使用遗传算法训练神经网络相比梯度下降法更为便捷。它不需要预先准备输入-输出训练数据,只需通过随机初始化多组权重参数,经过N次"繁衍进化"就能获得性能良好的网络模型。

这种方法的优势在于无需预设目标输出数据,特别适合那些可以通过环境交互直接评估网络表现的AI系统。但需要注意的是,当网络结构较为复杂时,其训练时间可能会显著延长。

用Compute Shader实现神经网络

神经网络的计算通常采用矩阵优化技术。在Python中实现神经网络时,开发者一般会使用NumPy或PyTorch等库来进行矩阵运算,从而显著提升计算效率。

个人曾经尝试过以单个神经元为最小单位实现的神经网络,但其实这种做法并不好。后来尝试过使用C#的MathNet库中的矩阵,但它发现并没有在硬件层面对矩阵运算进行加速。虽说对于小规模网络,即便不加速计算也不会太影响性能,但总觉得得考虑得更长远些。

想到神经网络的预测过程中,其实我们只关心输入层与输出层,而隐藏层的那些计算结果其实根本不在乎。欸~这似乎很适合用Compute Shader来完成!

隐藏层计算的结果完全可以只在留在ComputeBuffer,只有输入层需要将数据写入以及输出层将结果读取,CPU与GPU间数据的传递并不会很多;而且Compute Shader强大的并行计算能力也可以加速我们的运算过程。

但由于本文注意还是像讲遗传算法,就不喧宾夺主了,具体的实现会包含到文末的项目链接中。

遗传算法

在中学生物课本有提到达尔文的自然选择学说四个主要观点:过度繁殖、生存竞争、遗传和变异、适者生存。遗传算法就是借鉴了其中的思想,它的整个流程及其相似:

初始化种群

在本例中,我们想要获取神经网络中各层合适的权重与偏置的值,来使神经网络的输出符合预期,所以我们将整个神经网络的所有权重与偏置视为一个个体

csharp

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;}}}}
}

csharp

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个体,分别应用种群中各个体作为神经网络的权重与偏置,并直接应用神经网络进行决策。由于这些权重与偏置都是随机的,执行的效果几乎都不堪入目。

csharp

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,也就是只有随机值落在这个范围内时,它才会被选中。如果是其它值,就留给其它段了。

可以明显看出,这样的选择更照顾整体,原本大的值会有更大概率被选中,但小的也有机会。代码实现也非常简单:

csharp

//计算轮盘赌概率分布
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;
}

现在还有一个问题,仅仅只是交叉互换,那么最终得到的最优个体也只会囿于初始种群。如果初始种群中无论怎么交叉互换都无法得到优良个体又该怎么办?这时就得靠变异了。

变异的手段并不固定,只要能做到突破就可以。我的做法就是在原本数值的基础上随机增减一个小数值。但变异通常不能太频繁发生,我们要为它规定一个较小的概率,否则大规模的变异反而会破坏优良父本的传承。

变异的发生可以与繁殖放在一起:

csharp

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个个体做为本轮的优胜者,也是下轮的新父本。

csharp

//在父代和子代组成的整体中选出适应度高的新父代
private void GetBest()
{for(int i = 0; i < totalPopulation.Length; ++i){if (i < AllPopulation)totalPopulation[i] = parents[i];elsetotalPopulation[i] = children[i - AllPopulation];}Array.Sort(totalPopulation, (a, b) => b.fitness.CompareTo(a.fitness));
}

也就是说,有更高适应度的个体能存活下来,其他的就被淘汰。而这些存活下来的个体会不断重复这个过程。在数次 (或是无数次 迭代后,我们就一定可以得到理想中的个体(比如适应度超高的那种)。这时,我们就可以结束算法了。

实例:赛道小球

用一个比较简单的实例,串一遍整个过程。我们将训练一个用来跑赛道的小球。

1. 创建神经网络

在我的实现中,已将网络结构以ScriptObject形式存储,我们先新建一个,在Project下右键Create/ANN/WeightAngBias

然后设置具体结构,这次要完成的工作比较简单,就是训练一个可以绕圈跑的小球,所以网络结构比较简单。两个隐藏层足矣(对应Wei Bias Array的两个元素),这个神经网络接受三个输入,输出两个数据。

神经网络中间层的参数设计需遵循其结构特点:每层的权重数量应等于输入数乘以输出数(InputCount × OutputCount);除第一层外,各层的输入数必须与上一层的输出数保持一致。(熟悉神经网络的读者对这些原则应该不陌生)

Affine层固定使用同名Compute Shader。至于激活函数(Activate Func)和损失函数(Loss Func),在遗传算法训练过程中无需特别关注。

2. 创建遗传个体

场景中已有一个球形物体,挂载了继承GAUnitCar脚本 (原本是想做成车的

神经网络的3个输入数据就来自小球的三条射线检测:

csharp

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];}
}

神经网络的两个输出分别用来控制,移动速度以及角位移:

csharp

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时,遗传算法会让小球重置到起始位置,开始新一轮训练。

csharp

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"来加速训练进度。

重要注意事项:

  1. 测试小球性能时,必须关闭GA脚本或将"Train Unit"置空,否则每次运行都会重新训练"Train Unit"中的个体
  2. 示例中经过4分钟训练后,成功获得一个能绕圈移动的小球(虽然目前是倒着走)
  3. 保存训练结果的正确步骤:
    • 勾选"Is End Train"
    • 停止运行
    • 禁用GA脚本
  4. 完成上述操作后再次运行,即可看到小球自动绕圈移动的效果

尾声

如果了解神经网络,或许这篇就好看懂些。大伙感兴趣也可以尝试更复杂的赛道,更庞大的网络 (估计训练会很久

http://www.xdnf.cn/news/1295875.html

相关文章:

  • 一篇文章读懂.Net的依赖注入
  • .NET 的 WebApi 项目必要可配置项都有哪些?
  • .Net4.0 WPF中实现下拉框搜索效果
  • 面试题之项目中git如何进行管理
  • 如何启动本机mysql数据库
  • 在mysql> 下怎么运行 .sql脚本
  • XCTF-warmup详细题解(含思考过程)
  • Morph Studio-一站式AI视频创作平台
  • Vue浅学
  • Elasticsearch 中如何配置 RBAC 权限-实现安全的访问控制
  • QT6(创建第一个QT项目)
  • Win10上Qt使用Libcurl库
  • Qt 实现Ymodem协议源码分享
  • MySQL工具包中的其他程序
  • 从概率填充到置信度校准:GPT-5如何从底层重构AI的“诚实”机制
  • 树莓派 4B 上部署 Minecraft PaperMC 1.20.x 的一键部署脚本
  • ASQA: 面向模糊性事实问题的长格式问答数据集与评估框架
  • C#WPF实战出真汁02--登录界面设计
  • 利用 Python 爬虫按图搜索 1688 商品(拍立淘)实战指南
  • Windows批处理脚本自动合并当前目录下由You-get下载的未合并的音视频文件
  • LeetCode 分类刷题:2302. 统计得分小于 K 的子数组数目
  • 我的第一个开源项目-jenkins集成k8s项目
  • 开疆智能Ethernet转ModbusTCP网关连接UR机器人配置案例
  • 区块链 + 域名Web3时代域名投资的新风口(上)
  • 《算法导论》第 25 章:所有结点对的最短路径问题
  • 常见的tls检测的绕过方案
  • Mybatis学习笔记(二)
  • Transformer之多头注意力机制和位置编码(二)
  • vue更改style
  • 双椒派E2000D网络故障排查指南