虚幻引擎神经网络引擎(NNE)
CG精品CG文章CG世界 38984 0
实名

通过了实名认证的内容创造者

发布于 2023-9-19 16:42:31

您需要 登录 才可以下载或查看,没有账号?注册

x

(1) 概览
在本课程中,你将学习如何使用虚幻引擎的神经网络引擎插件NNE,为你的游戏打造更好的AI。首先,你将了解NNE插件的整体架构和核心类,以及这些类如何在游戏中实现神经网络。
什么是NNE?NNE是一套可以运行预训练神经网络模型的游戏内平台,支持桌面端和部分主机端的CPU和GPU推理。该插件可以作为多种运行时的抽象层,每个运行时都能针对特定的硬件。
NNE的核心资产是 UNNEModelData,用于保存神经网络模型数据。它可以在游戏中传递给实现了INNERuntimeCPU的运行时,从而创建CPU模型IModelCPU;或者传递给实现了INNERuntimeRDG的运行时,以便创建一个可以用于GPU推理的模型IModelRDG。
要使用NNE,你需要启用相应的核心插件,以及各个运行时所需的全部插件。
资产结构UNNEModelData资产会在导入阶段保存神经网络的原始数据。当你把ONNX数据拖入编辑器时,就会生成UNNEModelData。所有注册过的运行时INNERuntime,都会在游戏内容烘焙后,在系统内部对原始数据进行优化和处理,从而生成一套专门用于运行时的优化模型。
在游戏中,模型数据资产可以传递给CPU或RDG运行时。运行时会检索相应的模型数据,并从中创建一个可运行的模型。
运行时NNE插件会提供一套全局函数,用于列出所有注册过的运行时,或者获取实现了特定接口的运行时。

a77378478b25f9e41105fdebbf7ab189.jpg

目前,系统支持在模板参数T中传递的接口是INNERuntimeCPU和INNERuntimeRDG,它们可以用来创建相应的模型IModelCPU或IModelRDG。运行时可以根据相应的功能实现其中的一个或两个接口。
我们有不同的运行时可以支持不同的平台和硬件,还可能提供一套不同的操作符,而哪个运行时和接口更适合哪个平台和相应的用例,则要视具体的游戏逻辑而定。
目前,运行时的支持信息如下:

b4380781957a10cdc68d604ef24589b7.jpg
用标注*号的运行时有窄操作符集
模型这里的模型指的是实际的神经网络。你可以将输入数据喂给它,让它计算输出。输入和输出的内存归调用者所有,并且它会确保在模型评估过程中保持有效。
不同类型的模型可以实现不同的接口:
CPU
模型可以在CPU或类似CPU的硬件上运行并实现IModelCPU接口。将UNNEModelData资产传递给实现INNERuntimeCPU接口的运行时,就会创建这种模型。
CPU模型可以由任何线程运行,但是调用者必须确保输入和输出内存以及并发调用线程的安全。RDG
模型可以在GPU上运行并实现IModelRDG接口。运行时实现INNERuntimeRDG时就会创建这种模型。
RDG模型从某些方面来说比较特殊,因为它们需要入队到渲染线程上的渲染图形构建器,这样神经网络就可以与渲染资源紧密交互。
RDGBufffer会提供RDG模型的输入和输出。如果输入一直留在CPU内存,调用者必须在运行模型前将一定的上传调用入队到RDGBuffer。
极简代码片段以下代码片段假设UNNEModelData已经被导入到内容路径/path/to/asset,然后进一步假设已经合理分配变量InputShapes、Inputs和Outputs。
首先,我们通过调用LoadObject来检索对UNNEModelData资产的引用。使用其它方法来获得一个有效资产的引用(例如,分配给Actor的UPROPERTY类成员的资产)也是可以的。然后我们得到一个名为NNERuntimeORTCpu的运行时,并且实现了INNERuntimeCPU接口。这个特殊的接口让我们可以创建能够在CPU上运行的模型,接着在调用CreateModelCPU时将资产传递给运行时,从而创建一个可运行的模型。我们需要在模型上调用SetInputShapes来分配所需的缓冲区,然后通过调用RunSync来传递调用者的输入和输出缓冲,从而评估该模型。

(2)NNE - 快速入门指南
在本文中,你将编写一个简单的C++类,将它公开给Actor,最终通过蓝图运行一个基于CPU的神经网络。
目标在本教程中,你将具体完成以下内容:创建一个简单的C++项目编写能够从资产中加载神经网络数据的函数向神经网络传递蓝图输入和输出张量添加函数来运行接口设置蓝图Actor来使用该类

完成课程后,你将了解NNE的核心概念,并将所学知识应用到你的游戏中。
前提条件本教程将讲解如何将预训练神经网络整合到蓝图Actor中,所以需要你已经具备神经网络的基础知识。本教程的目标是提供一套可以在游戏中使用任何AI的框架。创建和训练针对特定应用程序的神经网络则可以作为读者的课后练习。
项目配置新建一个项目,选择游戏分类,然后选择第三人称模板和C++。输入你的项目路径和名称,然后点击创建。



点击编辑 > 插件来打开插件窗口。



搜索并启用NNE插件。重新启动虚幻引擎编辑器。



C++实现项目设置点击内容菜单并找到C++ Classes > NNETutorial或者你选择的项目名称。在内容浏览器中单击右键并点击新C++类...。



点击所有类并选择Object(对象)来制作继承自UObject的类,这样我们就能把这个类公开给蓝图了。然后点击下一步。



点击公开然后给该类命名,记得标头和源文件的路径要正确,然后点击创建类。


接着会弹出你的开发环境并显示标头和源文件。
首先打开构建文件:
NNETutorial > Source > NNETutorial > NNETutorial.Build.cs,

然后将NNECore模块添加到PublicDependencyModuleNames,这样我们就能进入基础设施了。
请注意,你可能需要在更改.Build.cs文件之后重新生成Visual Studio文件,这样IntelliSense还可以找到你之后添加的标头。你也可以右键点击.uproject文件并选择相应的选项。



打开标头文件:
NNETutorial > Source > NNETutorial > Public > NeuralNetworkModel.h,

并添加结构体FNeuralNetworkTensor。这样它就会用于向我们的神经网络类传递输入和输出张量。
这个结构体需要用到USTRUCT(BlueprintType) 关键字,以便在蓝图中访问,其中包含用于存储张量形状和张量数据本身的字段。蓝图可以为这些结构体分配一个数组,其中包含用于神经网络各个输入的一个张量,还有另一个可以用于输出。因此,这会是属于蓝图的内存,同时也是NNE插件的要求,可以防止不必要的内存拷贝。



接下来我们要给神经网络类添加和上面类似的关键字,以及一些不可或缺的头文件。



我们要添加一个静态函数,用于列出实现INNERuntimeCPU接口的所有运行时。添加一个静态函数,用于根据运行时名称和UNNEModelData资产类来创建实例。我们还需要添加一个静态函数来根据形状创建FNeuralNetworkTensor。这些都是一些便捷函数,方便之后用简单的蓝图节点来创建新的模型。



现在我们要添加一些专门获取张量输入输出数量的函数,以及用于根据索引获取张量形状的函数。这些将用于在蓝图中为张量分配内存。



接下来,我们要添加一个设置输入张量的函数,以及一个运行神经网络并将结果存储到输出张量的函数。请注意,输入和输出都应使用引用值,这样可以避免不必要的内存拷贝。



最后,我们再添加一些私有字段来保存对模型的引用,并添加一些字段来描述输入绑定和形状。张量形状是包含各个数组元素中各个维度大小的数组,而张量绑定则包含平面内存数据,并且只有字节大小。



完整的头文件应该如下所示:






具体实现接下来我们要实现上述类中所有的函数。
运行时枚举首先,我们要实现可以列出所有可用CPU运行时名称的函数。NNE可以提供一个全局函数,能够返回所有已注册的运行时。由于我们想要在CPU上运行模型,所以要尝试将它们都转换为INNERuntimeCPU接口,并返回相应的名称。因此,我们要获取所有运行时的列表并循环遍历,尝试将运行时转换为所需的接口,还要在成功后将其名称添加到结果中。



模型创建我们要将有效的UNNEModelData资产传递给运行时,从而创建模型。因此要检查资产引用的有效性,并通过名称来获取运行时。我们可以使用NNE提供的模板化全局函数。如果没有相应名称的运行时,或者运行时没有实现在模板实参中传递的接口,则返回空引用。



现在我们有了有效的运行时和模型数据,就可以准备创建模型了。请注意,CreateModelCPU会返回一个唯一的指针,表明所有权已经转移给调用者。当我们要将模型分配给蓝图类时,需要将其转换为共享指针,方便未来有需要时与其他线程共享。
完整的模型创建函数如下:



Tensor创建创建张量其实简单直接:需要一个有效的形状,并将形状内的各个维度彼此相乘来计算出内存大小。由于张量必须有一个定义良好的体积,因此我们要捕获由负值表示的动态维度以及维度设为零的空体积。我们重新调整了引用传递的张量大小,并将内存所有权留给调用者。



模型查询Actor需要查询输入和输出张量的数量以及相应的形状,从而分配内存。这些查询函数只调用IModelCPU上的相应函数。此外,形状查询函数会返回单个张量的形状,因此可以使用简单的TArray作为结果。



输入配置神经网络还可以使用动态的输入形状,让你在模型的生命周期内拥有更好的灵活性。由于输入形状可能会决定模型运行所需的中间缓冲区的大小,因此你需要在调用RunSync之前设置输入。
首先我们要确保给定的输入张量匹配神经网络的输入要求。



神经网络会使用输入张量作为张量绑定,所以我们需要将Helper结构体中提供的张量转换为此类绑定。而张量绑定需要有具体且定义良好的形状,所以我们要将符号输入形状转换为具体的张量形状。因此我们要循环遍历所有的输入,设置张量绑定的数据和大小以及转换的形状。



最后,我们可以将输入形状设置到模型上,从而调用形状推理并让模型可以分配内存。请注意,我们定义的绑定会在之后运行模型时使用。
完整函数如下所示。它需要在运行模型之前和输入内存发生变化时调用。



模型执行运行模型其实相对简单。这里和配置输入类似,输出张量会被转换为张量绑定,并且会与上面定义的输入绑定一起被传递给模型的执行函数。
请注意,这是一个阻塞请求,因此只适用于小型网络。大型模型执行应该在异步任务中运行,以免阻塞游戏线程。不过这是另一套教程的课题了。



现在你应该可以编译项目并且不会出现任何错误了。请注意,根据你实时编码的设置,你可能需要在开发环境中编译时关闭编辑器。

蓝图实现项目设置返回编辑器,点击内容菜单并找到Content > ThirdPerson > Blueprints,然后在内容浏览器中单击右键,点击蓝图类。



点击Actor并给新资产命名(例如NeuralNetworkModelActor),然后按下Ctrl + S保存资产。



双击打开Actor并点击事件图表。



在左下方的我的蓝图选项卡中,点击变量旁边的+号,添加一个新变量。
添加变量ModelData,类型为NNEModelData对象引用。点击闭眼的图标打开它,这样就能在蓝图之外进入这个变量了,方便我们之后选择这个模型数据资产。
添加另一个变量Model,作为之前创建的C++类的对象引用。
最后添加两个输入和输出数列,类型为NeuralNetworkTensor,你可以右键单击这个类型图标,把它从单条表示的单个结构体改为3x3正方形网格表示的结构体数组。



然后在我的蓝图选项卡中,点击函数旁边的+号来新建函数。添加两个名为LoadModel和CreateInputsOutputs的函数。



具体实现接下来我们要实现这些蓝图函数。
模型加载LoadModel函数负责获取运行时,传递变量ModelData中引用的资产,并将神经网格结果存储到Model。
双击我的蓝图中的函数名称来打开函数图表,然后选择Load Model节点并点击Inputs旁边的+号,添加一个新的输入变量。把它命名为RuntimeIndex并把类型更改为整数。我们会使用它来获得运行时的索引。



该函数的实现如下所示。只要调用我们在上面实现的函数GetRuntimeNames,并且获得索引中作为输入函数传递的名称,然后传递资产ModelData来调用CreateModel,并将结果存储到Model。




输入和输出创建函数createinputtsoutputs会在加载模型后分配所有输入和输出张量。
双击我的蓝图中的函数名称来打开函数图表。而对于输入和输出,我们要获取所需张量的数量并调整数组的大小。接着循环遍历各个张量,并根据模型查询返回的形状来创建它。



Actor配置再次点击事件图表标签,我们需要将实现的函数放在一起,这样Actor开始运行时就会加载模型,并且每Tick都会运行模型。请确保先测试模型是否有效,再调用模型上的函数。
简单起见,我们将0传递给LoadModel,因此我们总会使用NNE返回的第一个运行时。假如结果有效,我们会在使用SetInputs之前调用CreateInputsOutputs来分配内存。假如存在有效的模型,我们就会每Tick测试,然后调用RunSync。



点击右上方的相应按钮来保存和编译蓝图。接着关闭资产并返回,然后返回编辑器。




关卡配置接下来你要给项目添加一个神经网络。要导入模型只需要将ONNX文件拖入你的内容浏览器,然后按下Ctrl + S保存。

最后要做的就是将Actor添加到关卡,并确保它会使用你刚才导入的神经网络。打开ThirdPersonMap关卡,将ActorNeuralNetworkModelActor拖入主窗口,它就会被系统自动选中,并出现在右边的世界大纲视图。
在所有 > 默认下面的细节面板中,你可以找到我们之前在自定义Actor中设置的公开字段ModelData。点击None会打开一个下拉菜单,你可以选择任何已经导入的模型。



将模型分配给Actor后,你就可以运行游戏了。在BeginPlay中,Actor会加载模型并设置适当大小的输入和输出张量,并且会每Tick评估神经网络。
当然了,在运行模型之前,你还需要添加一些游戏逻辑来设置输入数据,并处理存储在输出张量中的每Tick结果。不过这就需要你根据自己的游戏逻辑来应用具体的代码了。
请注意,如果你的模型非常复杂,神经网络会在游戏线程的每tick中都进行评估,从而拖慢你的游戏运行速度。如果模型执行花费的时间超过了一帧,并且你不需要每tick更新,那就可以从异步任务完成模型推理,不过这也涉及到另外一套课程了。
恭喜,你已经成功在你的游戏中运行了神经网络!我们还将推出更多教程,敬请期待!

全文完

评分

参与人数 1元素币 +6 活跃度 +5 展开 理由
马丁先生... + 6 + 5 都这么卷了吗?

查看全部评分

内容主要涵盖影视特效,CG动国,前沿CG技术,作品欣賞
使用道具 <
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表