英文原文:https://thomassimonini.substack.com/p/create-an-ai-robot-npc-using-hugging
文章目录
一个能够理解你的文字命令的 NPC
直接在游戏中使用强大的人工智能模型是游戏开发的新领域。
想象一下这将是多么令人身临其境:
- 使用聊天模型和人工智能语音与 NPC(非玩家角色)进行实时对话。
- 能够直接用声音与他们交谈。
- 通过文字或语音控制你的角色。
尽管借助 API,这已经成为可能,但仍存在两个缺点:
- 依赖互联网连接,存在由于潜在的 API 延迟而导致沉浸式体验中断的风险。
- 与 API 使用相关的潜在高成本,尤其是对于许多玩家而言。
幸运的是,Unity 推出了神经网络推理库 Sentis(前身为 Barracuda),您可以直接在游戏中运行 AI 模型,而无需依赖 API。
所以今天,我们要制造这个智能机器人,它能理解玩家的指令并执行指令。
这就是本教程结束时您将得到的结果:
ai-robot-npc-huggingface-1
我们将使用一个人工智能模型,它能理解任何文本输入,并在其列表中找到最合适的操作。
与传统的游戏开发相反,该系统的有趣之处在于,你不需要对每一次交互都进行硬编码。相反,你可以使用一个语言模型,根据用户的输入选择最合适的机器人动作。
您可以在此处下载 Windows 演示
为了制作这个项目,我们将使用:
- Unity Game Engine (2022.3 and +).
- 由 Mix 和 Jam 制作的 Jammo 机器人资产。
- Unity Sentis 库是一个神经网络推理库,允许我们直接在游戏中运行 AI 模型。
- Hugging Face Sharp Transformers:一个 Unity 实用程序插件,用于在 Unity 游戏中运行 Transformer 模型。
您可以在此处找到完整的 Unity 项目
在项目结束时,您将构建智能机器人游戏演示。然后,您将能够迭代其他想法。
例如,在制作了这款游戏之后,我又用相同的代码库制作了这款《地牢逃生》演示版 ⚔️,游戏的目标是在不被守卫发现的情况下,通过偷窃和黄金逃离这座监狱。
ai-robot-npc-huggingface-2
因此,在本教程的第一部分,我们将了解句子相似性任务及其工作原理。然后,我们将学习如何制作这个演示。
听起来很有趣?让我们开始吧!
句子相似度的力量
在深入实施之前,我们必须了解句子相似度及其工作原理。
这个游戏如何运作?
在这款游戏中,我们希望给予玩家更多的自由。我们希望玩家通过文字与机器人互动,而不是仅仅通过点击按钮向机器人下达命令。
机器人有一个行动列表,并使用句子相似性模型,根据玩家的指令选择最接近的行动(如果有的话)。
例如,如果我写下 “嘿,给我拿红色的盒子”,机器人在编程时并不知道 "嘿,给我拿红色的盒子 "是什么意思。但是句子相似性模型会将这个顺序和 "给我拿红色方块 "的动作联系起来。
因此,有了这项技术,我们就能构建出可信的角色人工智能,而无需手工将每种可能的玩家输入交互映射到机器人响应的繁琐过程。让句子相似性模型来完成这项工作。
什么是句子相似性?
句子相似性是一项任务,它能够在给定源句和句子的情况下,计算出句子与源句的相似程度。
来源:https://huggingface.co/tasks/sentence-similarity
例如,如果我们的源句(播放器顺序)是 “嘿,给我拿红盒子”,那么它就非常接近 "拿红盒子 "这个句子。
句子相似性模型将输入文本 "Hello "转换为捕捉语义信息的向量(嵌入)。我们称这一步为嵌入。然后,我们使用余弦相似度来计算它们的接近(相似)程度。
细节我就不多说了,但由于我们的句子相似性模型会产生向量。我们可以计算两个向量之间的夹角余弦。结果越接近 1,这两个向量就越相似。
如果您想更深入地研究句子相似性任务,请检查此 https://huggingface.co/tasks/sentence-similarity
完整的过程
既然我们已经了解了句子相似性,那就让我们来看看整个过程:从玩家如何输入指令到机器人如何行动。
-
玩家输入命令:“你能给我带来红色立方体吗?”
-
机器人有一个动作列表[“你好”,“快乐”,“带上红色立方体”,“移动到红色立方体”]
-
然后我们想要做的是嵌入此玩家输入文本以与机器人动作列表进行比较,以找到最相似的动作(如果有)。
-
为此,我们对输入进行标记:Transformer 模型不能将字符串作为输入。它需要转化为数字。这是使用 Sharp Transformers 提供的 Tokenizer 代码完成的。
-
然后,输入(标记化)被传递到输出该文本嵌入的模型,该嵌入是一个捕获有关文本的语义信息的向量。这个推理部分是由Unity Sentis完成的。
-
现在,我们可以使用余弦相似度将该向量与其他向量(来自行动列表)进行比较。
-
我们选择相似度最高的动作并获得相似度分数。
a. 如果相似度得分大于 0.2,我们就会要求机器人执行该动作。
b. 否则,我们要求机器人执行“我很困惑”动画。由于给出的顺序与动作列表相差太大(例如,如果玩家写了一些完全不相关的内容,例如“你喜欢兔子吗?”,就可能出现这种情况。
使用的AI模型
- 在本演示中,我们使用的是 all-MiniLM-L6-v2。
- 这是一个 BERT Transformer 模型。它已经训练好了,我们可以直接使用它
- 我已经为您提供了 ONNX 文件,因此您不需要转换它。
让我们构建我们的智能机器人演示
第 0 步:获取项目
您可以在此处找到完整的 Unity 项目
第 1 步:安装 Unity Sentis
Sentis 文档
https://docs.unity3d.com/Packages/com.unity.sentis@latest
-
打开 Jammo 项目
-
单击 Sentis 预发布包或转至Window > Package Manager,单击 + 图标,选择“Add package by name…”并输入“com.unity.sentis”
-
按“添加”按钮安装该软件包。
第 2 步:安装 Sharp Transformers
Sharp Transformers 是一个 Unity 实用程序插件,用于在 Unity 游戏中运行 Transformer 模型。
我们需要在标记化步骤中执行此操作。
- 转到"Window" > “Package Manager” 以打开Package Manager。
- 单击左上角的“+”,然后选择“从 git URL 添加包”。
- 输入此存储库的 URL,然后单击
“Add”: https://github.com/huggingface/sharp-transformers.git
第 3 步:构建推理过程
如 Sentis 文档所述,要在 Unity 中使用 Sentis 运行神经网络,我们需要遵循以下步骤。
- 使用 Unity.Sentis 命名空间。
- 加载神经网络模型文件。
- 为模型创建输入。
- 创建一个推理引擎(一个worker)。
- 使用输入运行模型以推断结果。
- 得到结果。
在我们的例子中,我们在 SentenceSimilarity.cs 文件中完成所有这些操作,该文件将附加到我们的机器人上。
在Awake中,我:
- 加载我们的神经网络
- 创建一个推理引擎(一个worker)。
- 创建一个运算符,它允许我们使用张量执行运算。
public ModelAsset modelAsset;
public Model runtimeModel;
public IWorker worker;
public ITensorAllocator allocator;
public Ops ops;
/// <summary>
/// Load the model on awake
///</summary>
void Awake( )
{
// Load the ONNX model
runtimeModel = ModelLoader.Load(modelAsset);
// Create an engine and set the backend as GPU //GPUcompute
worker = WorkerFactory.CreateWorker(BackendType.CPU, runtimeModel);
// Create an allocator.
allocator = new TensorCachingAllocator();
// Create an operator
ops = WorkerFactory.CreateOps(BackendType.GPUcompute, allocator);
}
我们有三个功能:
- Encode:接收播放器输入(文本),将其标记化并嵌入其中。
public TensorFloat Encode(List<string> input, IWorker worker, Ops ops)
{
// Step 1:对句子进行标记
Dictionary<string,Tensor> inputSentencesTokensTensor = SentenceSimilarityUtils_.TokenizeInput(input);
//Step 2:计算嵌入并获取输出
worker.Execute(inputSentencesTokensTensor);
// Step 3:获取神经网络的输出
TensorFloat outputTensor = worker.PeekOutput("last hidden state") as Tensorfloat;
// Step 4:执行池化
TensorFloat MeanPooledTensor = SentencesimilarityUtils_.MeanPooling(inputsentencesTokensTensor["attention_mask"], outputTensor,ops);
// Step 5:标准化结果
TensorFloat NormedTensor = SentenceSimilarityUtils_.L2Norm(MeanPooledTensor, ops);
return NormedTensor;
}
SentenceSimilarityScores: 计算输入嵌入(用户输入的内容)和对比嵌入(机器人操作列表)之间的相似度分数
public Tensorfloat SentencesimilarityScores(Tensorfloat InputSequence, Tensorfloat comparisonsequences)
{
TensorFloat SentenceSimilarityScores_= ops.MatMul2D(InputSequence, ComparisonSequences, false, true);
return SentencesimilarityScores_;
}
RankSimilarityScores: 获取玩家输入的最相似动作及其指数
public Tuple<int, float> RanksimilarityScores(string inputsentence, string[] comparisonsentences)
{
//Step 1:Transform string and string[] to lists
List<string> InputSentences = new List<string>();
List<string> ComparisonSentences = new List<string>();
InputSentences.Add(inputSentence);
Comparisonsentences=comparisonSentences.ToList();
// Step 2: 对输入句子和比较句子进行编码
TensorFloat NormEmbedSentences = Encode(InputSentences, worker, ops);
TensorFloat NormEmbedComparisonSentences = Encode(ComparisonSentences, worker, ops);
//计算玩家输入与每个动作的相似度得分
TensorFloat scores = SentenceSimilarityScores(NormEmbedSentences, NormEmbedcomparisonsentences);
scores.MakeReadable();// 能够读取这个张量
//Helper只返回最好的分数和索引
TensorInt scoreIndex=ops.ArgMax(scores, 1, true);
scoreIndex.MakeReadable();
int scoreIndexInt = scoreIndex[0];
scores.MakeReadable();
float score = scores[scoreIndexInt];
// 返回相似度得分和动作索引
return Tuple.Create(scoreIndexInt, score);
}
有了这四个功能,我们就能实现句子相似性。现在,我们只需根据动作定义机器人行为。
第 4 步:构建机器人行为
我们需要定义机器人的行为。
我们的想法是,我们的机器人有不同的可能动作,并且动作的选择将取决于最相似的动作。
我们首先需要定义有限状态机,这是一个简单的AI,其中每个状态都定义了某种行为。
然后,我们将创建实用函数来选择状态,从而选择要执行的一系列操作。
状态机
在状态机中,每个状态代表一种行为,例如,移动到一列、打招呼等。根据状态,代理将执行一系列操作。
在我们的例子中,我们有 7 个状态:
我们需要做的第一件事是创建一个名为 State 的枚举,其中包含每种可能的状态:
/// <summary>
/// Enum of the different possible states of our Robot
/// </summary>
private enum State
{
Idle,Hello, // Say hello
Happy, // Be happy
Puzzled,// Be Puzzled
MoveTo,// Move to a pillar
BringObject,//Step one of bring object(move to it and grab it)
BringObjectToPlayer //Step two of bring object (move to player and drop the object )
}
因为我们需要不断检查状态,所以我们在 Update() 方法中使用开关系统定义了状态机,其中每个情况都是一个状态。
private void Update()
{
// Here's the State Machine, where given its current state, the agent will act accordingly switch(state)
{
default:
case State.Idle:
break;
case State.Hello:
agent.SetDestination(playerPosition.position);
if (Vector3.Distance(transform.position, playerPosition.position) < reachedPositionDistance)
{
RotateTo();
anim.SetBool ("hello", true);
state = State.Idle;
}
break;
case State.Happy:
agent.SetDestination(playerPosition.position);
if (Vector3.Distance(transform.position, playerPosition.position) < reachedPositionDistance)
{
RotateTo();
anim.SetBool("happy", true);
state = State.Idle;
}
break;
case State.Puzzled:
agent.SetDestination(playerPosition.position);
if (Vector3.Distance(transform.position, playerPosition.position) < reachedPositionDistance)
{
RotateTo();
anim.SetBool ("puzzled", true);
state = State.Idle;
}
break;
case State.MoveTo:
agent.SetDestination(goalObject.transform.position);
if (Vector3.Distance(transform.position, goalObject.transform.position) < reachedPositionDistance)
{
state = State.Idle;
}
break;
case State. BringObject:
// First move to the object
agent.SetDestination (goalObject.transform.position);
if (Vector3.Distance(transform.position, goalObject.transform.position) < reachedObjectPositionDistance)
{
Grab(goalObject);
state = State.BringObjectToPlayer;
}
break;
case State. BringObjectToPlayer:
agent.SetDestination(playerPosition.transform.position);
if (Vector3.Distance(transform.position, playerPosition.transform.position) < reachedObjectPositionDistance)
{
Drop(goalObject);
state = State.Idle;
}
break;
}
}
对于每个状态情况,我们定义代理的行为,例如在“Hello”状态下,机器人必须向玩家移动,正确面对他,然后启动其“Hello”动画,然后返回到“空闲”状态。
case State.Hello:
agent.SetDestination (playerPosition.position);//Move towards the Player
//When the Robot is close enough
if (Vector3.Distance(transform.position, playerPosition.position) < reachedPositionDistance)
{
RotateTo(); //Robot face correctly the player
anim.SetBool ("hello", true); //Launch Hello Animation
state = State.Idle; //Set State to Idle
}
break;
现在我们已经定义了每个不同状态的行为。这里的神奇之处在于,语言模型将定义最接近玩家输入的状态。在 Utility 函数中,我们将这种状态称为 “状态”。
让我们定义 Utility 函数
我们的行动列表如下所示:
- Sentence 将被嵌入到人工智能模型中。
- Verb 是状态
- Noun(如果有)是与之交互的对象(柱子、立方体等)
该 Utility 功能将选择与玩家输入文本相似度最高的句子相关的动词和名词。
但首先,为了剔除大量奇怪的输入文本,我们需要一个相似度得分阈值。
例如,如果我说 “看看所有的兔子”,我们可能采取的任何行动都与之无关。因此,我们不会选择得分最高的动作,而是将其称为 "困惑 "状态,这样机器人就会做出困惑的动画。
如果得分较高,我们就会得到与状态相对应的动词和名词(goalObject)(如果有的话)。
我们设置与动词相对应的状态。这将激活与之对应的行为。
/// <summary>
/// Utility 函数:给定 HuggingFaceAPI 的结果,选择得分最高的 State /// </summary>
/// <param name="maxValue">Value of the option with the highest score</param>
/// <param name="maxIndex">Index of the option with the highest score</param>
public void Utility(float maxScore, int maxScoreIndex)
{
// 首先我们检查分数是否 > 0.2,否则我们会让我们的智能体感到困惑;
// 这样我们就可以处理奇怪的输入文本(例如,如果我们写“Go see the dog!”代理会感到困惑)。
if (maxScore < 0.20f)
{
state = State.Puzzled;
}
else
{
// 获取动词和名词(如果有的话)
goalObject = GameObject.Find(actionsList[maxScoreIndex].noun);
string verb = actionsList[maxScoreIndex].verb;
// 设置机器人状态 == verb
state = (State)System.Enum.Parse(typeof(State), verb, true);
}
}
就是这样,现在我们准备好与我们的机器人互动了!
第 5 步:让我们与机器人互动
在此步骤中,您只需点击编辑器中的播放按钮即可。您可以写一些提示词并查看结果。
改进游戏:让我们添加一个新动作
举个例子:
- 复制 YellowPillar 游戏对象并移动它
- 将名称更改为 GreenPillar
- 创建一个新材质并将其设置为绿色
- 将材料放在 GreenPillar 上
现在我们已经放置了新的游戏对象,我们需要将这种可能性添加到句子中并单击 Jammo_Player。
在操作列表中,单击加号按钮并填写此新操作项:
- 将 Go to the green 栏目
- GoTo
- GreenColumn
就是这样!这意味着您可以轻松迭代并向游戏添加更多动作。
现在您已经了解了该项目的运作方式。不要犹豫,不断迭代,做出不同的东西。
例如,使用相同的代码库,我正在创建这个地下城逃生游戏,您的目标是通过偷窃钥匙和黄金逃离这座监狱,而不被警卫发现。
ai-robot-npc-huggingface-2
恭喜,通过几个步骤,我们刚刚构建了一个机器人,它能够根据您的命令执行操作,这真是太棒了!