HoloLens(第一代)和 Azure 303:自然语言理解 (LUIS)


注意

混合现实学院教程在制作时考虑到了 HoloLens(第一代)和混合现实沉浸式头戴显示设备。 因此,对于仍在寻求这些设备的开发指导的开发人员而言,我们觉得很有必要保留这些教程。 我们不会在这些教程中更新 HoloLens 2 所用的最新工具集或集成相关的内容。 我们将维护这些教程,使之持续适用于支持的设备。 将来会发布一系列演示如何针对 HoloLens 2 进行开发的新教程。 此通知将在教程发布时通过指向这些教程的链接进行更新。


在本课程中,你将了解如何使用 Azure 认知服务和语言理解 API,将语言理解集成到混合现实应用程序中。

实验室结果

语言理解 (LUIS) 是一项 Microsoft Azure 服务,它使应用程序能够理解用户输入内容的含义,例如通过用自己的表述提取用户可能的意向。 这是通过机器学习来实现的,机器学习可理解和学习输入信息,然后可使用详细的相关信息进行回复。 有关详细信息,请访问 Azure 语言理解 (LUIS) 页面

完成本课程后,你将拥有一个混合现实沉浸式头戴显示设备应用程序,该应用程序将能够执行以下操作:

  1. 使用连接到沉浸式头戴显示设备的麦克风捕获用户输入语音。
  2. 将捕获到的语音听写发送到 Azure 语言理解智能服务 (LUIS)
  3. 让 LUIS 从发送的信息中提取含义(信息将被分析),并尝试确定用户请求的意向。

开发过程包括创建一个应用,用户将能够在此应用中使用语音和/或凝视来更改场景中对象的大小和颜色。 本课程将不介绍对运动控制器的使用。

在应用程序中,由你决定结果与设计的集成方式。 本课程旨在教授如何将 Azure 服务与 Unity 项目集成。 你的任务是运用从本课程中学到的知识来增强混合现实应用程序。

做好训练 LUIS 多次的准备,这将在第 12 章中介绍。 LUIS 训练的次数越多,你得到的结果越好。

设备支持

课程 HoloLens 沉浸式头戴显示设备
MR 和 Azure 303:自然语言理解 (LUIS) ✔️ ✔️

注意

尽管本课程主要重点介绍 Windows Mixed Reality 沉浸式 (VR) 头戴显示设备,但你也可以将本课程中学到的内容应用到 Microsoft HoloLens。 随着课程的进行,你将看到有关支持 HoloLens 可能需要进行的任何更改的说明。 使用 HoloLens 时,你可能会在语音捕获过程中注意到某些回声。

先决条件

注意

本教程专为具有 Unity 和 C# 基本经验的开发人员设计。 另请注意,本文档中的先决条件和书面说明在编写时(2018 年 5 月)已经过测试和验证。 可随意使用最新软件(如安装工具一文所列的软件),但不应假设本课程中的信息会与你在比下列版本更高的软件中找到的内容完全一致。

建议在本课程中使用以下硬件和软件:

开始之前

  1. 为了避免在生成此项目时遇到问题,强烈建议在根文件夹或接近根的文件夹中创建本教程中提到的项目(长文件夹路径会在生成时导致问题)。

  2. 要使计算机能够启用语音听写,请转到“Windows 设置”>“隐私”>“语音、墨迹书写和键入”,然后按“打开语音服务和键盘输入建议”按钮

  3. 通过本教程中的代码,可从在计算机上设置的默认麦克风设备进行录制。 请确保将默认麦克风设备设置为要用于捕获语音的设备。

  4. 如果你的头戴显示设备内置有麦克风,请确保已在“混合现实门户”设置中启用“当我戴上头戴显示设备时,切换到头戴显示设备麦克风”选项

    设置沉浸式头戴显示设备

第 1 章 - 设置 Azure 门户

要在 Azure 中使用语言理解服务,需要配置服务的实例,使其可用于你的应用程序

  1. 登录到 Azure 门户

    注意

    如果你没有 Azure 帐户,需要创建一个。 如果你在课堂或实验室场景中跟着本教程学习,请让讲师或监督人员帮助设置你的新帐户。

  2. 登录后,单击左上角的“新建”,搜索“语言理解”,然后单击 Enter

    创建 LUIS 资源

    注意

    在更新的门户中,“新建”一词可能已替换为“创建资源”

  3. 右侧的新页将提供语言理解服务的说明。 在本页的左下角,选择“创建”按钮,创建此服务的实例

    LUIS 服务创建 - 法律声明

  4. 单击“创建”后:

    1. 插入此服务实例的所需名称

    2. 选择一个“订阅” 。

    3. 选择适合你的定价层;如果这是你第一次创建 LUIS 服务,则会向你提供免费层(名为 F0)。 对于本课程,免费分配已绰绰有余。

    4. 选择一个资源组或创建一个新资源组。 通过资源组,可监视和预配 Azure 资产集合、控制其访问权限并管理其计费。 建议保留与常用资源组下的单个项目(例如这些课程)关联的所有 Azure 服务。

      若要详细了解 Azure 资源组,请访问资源组一文。

    5. 为资源组确定位置(如果正在创建新的资源组)。 理想情况下,此位置在运行应用程序的区域中。 某些 Azure 资产仅在特定区域可用。

    6. 还需要确认了解应用于此服务的条款和条件。

    7. 选择创建

      创建 LUIS 服务 - 用户输入

  5. 单击“创建”后,必须等待服务创建完成,这可能需要一分钟时间

  6. 创建服务实例后,门户中将显示一条通知。

    新 Azure 通知图像

  7. 单击通知可浏览新的服务实例。

    成功资源创建通知

  8. 单击通知中的“转到资源”按钮,浏览新的服务实例。 你将转到新的 LUIS 服务实例。

    访问 LUIS 密钥

  9. 在本教程中,应用程序将需要调用你的服务,这是使用服务的订阅密钥来完成的。

  10. 在 LUIS API 服务的“快速启动”页上,导航到第一步(“获取密钥”),然后单击“密钥”(也可单击服务导航菜单中由钥匙图标表示的蓝色“密钥”超链接来实现此操作)。 这将显示你的服务密钥

  11. 从显示的密钥中复制一个,稍后需要在项目中用到它。

  12. 在“服务”页中单击“语言理解门户”,重定向到在 LUIS 应用中用于创建新服务的网页

第 2 章 - 语言理解门户

本部分将介绍如何在 LUIS 门户上创建 LUIS 应用。

重要

请注意,在本章中设置实体、意向和语句只是构建 LUIS 服务的第一步:要使服务更准确,还需要多次重新训练服务。 本课程的最后一章介绍了如何重新训练服务,因此请务必完成它。

  1. 访问语言理解门户时,可能需要使用与 Azure 门户相同的凭据进行登录(如果尚未登录)

    LUIS 登录页面

  2. 如果这是你首次使用 LUIS,则需要向下滚动到欢迎页面底部,找到并单击“创建 LUIS 应用”按钮

    “创建 LUIS 应用”页

  3. 登录后,单击“我的应用”(如果当前未在该部分)。 然后,可单击“创建新应用”

    LUIS - 我的应用图像

  4. 为你的应用命名。

  5. 如果应用需要理解与英语之外的语言,那么应将“区域性”更改为相应的语言

  6. 在此,还可添加新 LUIS 应用的说明

    LUIS - 创建新应用

  7. 按“完成”后,将进入新 LUIS 应用程序的“生成”页

  8. 需要理解以下几个重要概念:

    • 意向,表示在用户发起查询后将调用的方法。 一个意向可以有一个或多个实体
    • 实体是查询组件,用于描述与意向相关的信息
    • 语句是开发人员提供的查询示例,LUIS 将用它来训练自身

如果还未完全理解这些概念,不用你担心,本课程将在本章中进一步阐述它们。

首先,请创建建立此课程所需的实体

  1. 在页面左侧,单击“实体”,然后单击“创建新实体”

    创建新实体

  2. 调用名为“颜色”的新实体,将其类型设置为“简单”,然后按“完成”

    创建简单实体 - 颜色

  3. 重复此过程,再创建三 (3) 个简单实体,将其命名为:

    • 扩大
    • 缩小
    • 目标

结果应如下图所示:

实体创建的结果

此时,可开始创建意向

警告

不要删除“无”这一意向

  1. 在页面左侧,单击“意向”,然后单击“创建新意向”

    创建新意向

  2. 调用新的 意向 ChangeObjectColor

    重要

    本课程稍后的代码中会用到此意向名称,因此为获得最佳结果,请按原样使用此名称

确认名称后,将定向到“意向”页。

LUIS - 意向页

你会注意到,有一个文本框要求你键入 5 个或更多不同的语句

注意

LUIS 会将所有语句转换为小写。

  1. 将以下语句插入到顶部文本框(当前文本为“键入大约 5 个示例…”),然后按 Enter
The color of the cylinder must be red

你会注意到,新的语句将出现在下面的列表中

按照相同的过程,插入以下六 (6) 个话语:

make the cube black

make the cylinder color white

change the sphere to red

change it to green

make this yellow

change the color of this object to blue

对于创建的每个语句,必须确定 LUIS 应将哪些字词用作实体。 在本示例中,需要将所有颜色都标记为“颜色”实体,并将对目标的所有可能引用都标记为“目标”实体

  1. 为此,请尝试单击第一个语句中的“圆柱体”一词,然后选择“目标”

    标识语句目标

  2. 现在单击第一个语句中的“红色”一词,然后选择“颜色”

    标识语句实体

  3. 还要标记下一行,其中目标应为“立方体”,颜色应为“黑色”。 另请注意“此项”、“它”和“此对象”词的使用,我们提供了这些词,因此也要提供非特定的目标类型

  4. 重复上述过程,直到所有语句都标记有实体。 如需帮助,请查看下图。

    提示

    选择要标记为实体的单词时:

    • 对于单个词,只需单击它们即可。
    • 对于由两个或更多词组成的词组,请单击词组的开头和结尾。

    注意

    可使用“标记视图”切换按钮在实体视图和标记视图之间进行切换

  5. 结果应如下图所示,其中显示了实体视图和令牌视图

    令牌和实体视图

  6. 此时,按页面左上方的“训练”按钮,等待其上的圆形小指示器变为绿色。 这表示 LUIS 训练成功,可识别此意向。

    训练 LUIS

  7. 请进行练习:使用“目标”、“扩大”和“缩小”这三个实体创建名为 ChangeObjectSize 的新意向

  8. 按照与上一意向相同的过程,插入下面八 (8) 个要求更改大小的语句

    increase the dimensions of that
    
    reduce the size of this
    
    i want the sphere smaller
    
    make the cylinder bigger
    
    size down the sphere
    
    size up the cube
    
    decrease the size of that object
    
    increase the size of this object
    
  9. 结果应如下图所示:

    设置 ChangeObjectSize 标记/实体

  10. 在 ChangeObjectColor 和 ChangeObjectSize 这两个意向创建和训练后,单击页面顶部的“发布”按钮

    发布 LUIS 服务

  11. 在“发布”页上,你将完成并发布 LUIS 应用,使代码能够访问它

    1. 将“发布到”下拉列表设置为“生产”

    2. 将“时区”设置为你所在的时区

    3. 选中“包括所有预测意向分数”框

    4. 单击“发布到生产槽”

      发布设置

  12. 在“资源和密钥”部分

    1. 在 Azure 门户中选择为服务实例设置的区域。
    2. 你将注意到下面的 Starter_Key 元素,请忽略它
    3. 单击“添加密钥”,然后插入创建服务实例时在 Azure 门户中获取的密钥。 如果登录 Azure 和 LUIS 门户的是同一用户,则会为你提供你希望使用的租户名称、订阅名称和密钥所对应的下拉菜单(其名称与你之前在 Azure 门户中提供的名称相同)

    重要

    在“终结点”下,复制与已插入的密钥对应的终结点,你很快会在代码中使用它

第 3 章 - 设置 Unity 项目

下面是用于使用混合现实进行开发的典型设置,因此对其他项目来说,这是一个不错的模板。

  1. 打开 Unity,单击“新建”

    启动新的 Unity 项目。

  2. 现在,需要提供 Unity 项目名称,并插入 MR_LUIS。 确保将项目类型设置为“3D”。 将“位置”设置为适合你的位置(请记住,越接近根目录越好)。 然后,单击“创建项目”

    提供新的 Unity 项目的详细信息。

  3. 当 Unity 处于打开状态时,有必要检查默认“脚本编辑器”是否设置为“Visual Studio”。 转到“编辑”>“首选项”,然后在新窗口中导航到“外部工具”。 将外部脚本编辑器更改为 Visual Studio 2017。 关闭“首选项”窗口。

    更新脚本编辑器首选项。

  4. 接下来,转到“文件”>“生成设置”,然后单击“切换平台”按钮将平台切换到“通用 Windows 平台”

    “生成设置”窗口,将平台切换为 UWP。

  5. 转到“文件”>“生成设置”,并确保

    1. 将“目标设备”设置为“任何设备”

      对于 Microsoft HoloLens,请将“目标设备”设置为“HoloLens”

    2. 将“生成类型”设置为“D3D”

    3. 将“SDK”设置为“最新安装的版本”

    4. 将“Visual Studio 版本”设置为“最新安装的版本”

    5. 将“生成并运行”设置为“本地计算机”

    6. 保存场景并将其添加到生成。

      1. 通过选择“添加开放场景”来执行此操作。 将出现一个保存窗口。

        单击“添加开放场景”按钮

      2. 为此创建新文件夹,并为将来的任何场景创建一个新文件夹,然后选择“新建文件夹”按钮以创建新文件夹,将其命名为“场景”

        创建新的 Scripts 文件夹

      3. 打开新创建的“场景”文件夹,然后在“文件名:”文本字段中,键入 MR_LuisScene,再按“保存”

        为新场景提供名称。

    7. 在“生成设置”中,其余设置目前应保留为默认值

  6. 在“生成设置”窗口中,单击“播放器设置”按钮,这会在检查器所在的空间中打开相关面板

    打开播放器设置。

  7. 在此面板中,需要验证一些设置:

    1. 在“其他设置”选项卡中

      1. “脚本运行时版本”应为“稳定”(.NET 3.5 等效版本)

      2. “脚本后端”应为 “.NET”

      3. “API 兼容性级别”应为“.NET 4.6”

        更新其他设置。

    2. 在“发布设置”选项卡的“功能”下,检查以下内容

      1. InternetClient

      2. Microphone

        更新发布设置。

    3. 在面板再靠下部分,在“发布设置”下的“XR 设置”中,勾选“支持虚拟现实”,确保已添加“Windows Mixed Reality SDK”

      更新 XR 设置。

  8. 返回生成设置 Unity C# 项目不再灰显;勾选此框旁边的复选框。

  9. 关闭“生成设置”窗口 。

  10. 保存场景和项目(“文件”>“保存场景/文件”>“保存项目”)

第 4 章 - 创建场景

重要

如果想要跳过本课程的“Unity 设置”部分,直接学习代码,可下载此 .unitypackage,将其作为自定义包导入到项目中,然后从第 5 章继续学习。

  1. 右键单击层次结构面板的空白区域,在“3D 对象”下添加一个平面

    创建平面。

  2. 请注意,在“层次结构”中再次右键单击来创建更多对象时,如果仍然选择了最后一个对象,则所选对象将是新对象的父对象。 请不要在“层次结构”中的空白区域中左键单击再右键单击。

  3. 请重复上述过程来添加以下对象:

    1. 球形
    2. 柱状
    3. Cube
    4. 3D 文本
  4. 生成的场景“层次结构”应如下图所示

    “层次结构”场景设置。

  5. 左键单击“主摄像头”将其选中,查看检查器面板,你将看到摄像头对象及其所有组件

  6. 单击检查器面板最底部的“添加组件”按钮

    添加音频源

  7. 搜索名为“音频源”的组件,如上所示

  8. 另请确保将主摄像头的“变换”组件设置为 (0,0,0),可按摄像头的“变换”组件旁边的齿轮图标,再选择“重置”来完成此操作。 “转换”组件应如下所示

    1. “位置”设置为“0, 0, 0”
    2. “旋转”设置为“0, 0, 0”

    注意

    对于 Microsoft HoloLens,你还需要进行以下更改,这些内容是“主摄像头”上的“摄像头”组件的一部分

    • 清除标志:纯色
    • 背景“黑色,Alpha 0”- 十六进制颜色:#00000000
  9. 左键单击“平面”将其选中。 在检查器面板中,用以下值设置“变换”组件

    X 轴 Y 轴 Z 轴
    0 -1 0
  10. 左键单击“球体”将其选中。 在检查器面板中,用以下值设置“变换”组件

    X 轴 Y 轴 Z 轴
    2 1 2
  11. 左键单击“圆柱体”将其选中。 在检查器面板中,用以下值设置“变换”组件

    X 轴 Y 轴 Z 轴
    -2 1 2
  12. 左键单击“立方体”将其选中。 在检查器面板中,用以下值设置“变换”组件

转换 - 位置

X Y Z
0 1 4

转换 - 旋转

X Y Z
45 45 0
  1. 左键单击“新文本”对象将其选中。 在检查器面板中,用以下值设置“变换”组件

转换 - 位置

X Y Z
-2 6 9

转换 - 缩放

X Y Z
0.1 0.1 0.1
  1. 将“文本网格”组件中的字号更改为 50

  2. 将“文本网格”对象的名称更改为“听写文本”

    创建 3D 文本对象

  3. 层次结构面板结构现在应如下所示:

    场景视图中的文本网格

  4. 最终场景应如下图所示:

    场景视图。

第 5 章 - 创建 MicrophoneManager 类

要创建的第一个脚本是 MicrophoneManager 类。 然后,你将创建 LuisManager 类、Behaviours 类和最后一个类 Gaze(现可随意创建所有这些类,尽管查看每个章节时都会讲到)

MicrophoneManager 类负责

  • 检测连接到头戴显示设备或计算机的录音设备(以默认设备为准)。
  • 捕获音频(语音),并使用听写将其存储为字符串。
  • 暂停语音后,将语音听写提交给 LuisManager 类

若要创建此类,请执行以下操作:

  1. 在“项目面板”中右键单击“创建”>“文件夹”。 将该文件夹命名为“脚本”

    创建“脚本”文件夹。

  2. 创建“脚本”文件夹后,双击打开它。 然后,在该文件夹中,右键单击“创建”>“C# 脚本”。 将脚本命名为“MicrophoneManager”

  3. 双击“MicrophoneManager”,通过 Visual Studio 打开它

  4. 将以下命名空间添加到 文件顶部:

        using UnityEngine;
        using UnityEngine.Windows.Speech;
    
  5. 然后,将以下变量添加到 MicrophoneManager 类中

        public static MicrophoneManager instance; //help to access instance of this object
        private DictationRecognizer dictationRecognizer;  //Component converting speech to text
        public TextMesh dictationText; //a UI object used to debug dictation result
    
  6. 现在需要添加 Awake() 和 Start() 方法的代码。 类初始化时,将调用这些方法:

        private void Awake()
        {
            // allows this class instance to behave like a singleton
            instance = this;
        }
    
        void Start()
        {
            if (Microphone.devices.Length > 0)
            {
                StartCapturingAudio();
                Debug.Log("Mic Detected");
            }
        }
    
  7. 现在,你需要应用用于启动和停止语音捕获的方法,并将其传递给你即将构建的 LuisManager 类

        /// <summary>
        /// Start microphone capture, by providing the microphone as a continual audio source (looping),
        /// then initialise the DictationRecognizer, which will capture spoken words
        /// </summary>
        public void StartCapturingAudio()
        {
            if (dictationRecognizer == null)
            {
                dictationRecognizer = new DictationRecognizer
                {
                    InitialSilenceTimeoutSeconds = 60,
                    AutoSilenceTimeoutSeconds = 5
                };
    
                dictationRecognizer.DictationResult += DictationRecognizer_DictationResult;
                dictationRecognizer.DictationError += DictationRecognizer_DictationError;
            }
            dictationRecognizer.Start();
            Debug.Log("Capturing Audio...");
        }
    
        /// <summary>
        /// Stop microphone capture
        /// </summary>
        public void StopCapturingAudio()
        {
            dictationRecognizer.Stop();
            Debug.Log("Stop Capturing Audio...");
        }
    
  8. 添加语音暂停时要调用的听写处理程序。 此方法会将听写文本传递给 LuisManager 类

        /// <summary>
        /// This handler is called every time the Dictation detects a pause in the speech. 
        /// This method will stop listening for audio, send a request to the LUIS service 
        /// and then start listening again.
        /// </summary>
        private void DictationRecognizer_DictationResult(string dictationCaptured, ConfidenceLevel confidence)
        {
            StopCapturingAudio();
            StartCoroutine(LuisManager.instance.SubmitRequestToLuis(dictationCaptured, StartCapturingAudio));
            Debug.Log("Dictation: " + dictationCaptured);
            dictationText.text = dictationCaptured;
        }
    
        private void DictationRecognizer_DictationError(string error, int hresult)
        {
            Debug.Log("Dictation exception: " + error);
        }
    

    重要

    删除 Update() 方法,因为此类将不会使用它

  9. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

    注意

    此时,你会注意到在 Unity 编辑器控制面板中出现了错误。 这是因为代码引用了将在下一章中创建的 LuisManager 类

第 6 章 - 创建 LUISManager 类

你现在可创建 LuisManager 类,它将调用 Azure LUIS 服务

此类用于接收来自 MicrophoneManager 类的听写文本,并将其发送到 Azure 语言理解 API 进行分析

此类将反序列化 JSON 响应,并调用 Behaviours 类的适当方法来触发操作

若要创建此类,请执行以下操作:

  1. 双击“Scripts”文件夹将其打开

  2. 右键单击“脚本”文件夹,然后单击“创建”>“C# 脚本”。 将脚本命名为“LuisManager”

  3. 双击该脚本,通过 Visual Studio 打开它。

  4. 将以下命名空间添加到 文件顶部:

        using System;
        using System.Collections;
        using System.Collections.Generic;
        using System.IO;
        using UnityEngine;
        using UnityEngine.Networking;
    
  5. 首先,在 LuisManager 类中创建 3 个类(在同一脚本文件中,在 Start() 方法上方),这些类将表示来自 Azure 的反序列化 JSON 响应

        [Serializable] //this class represents the LUIS response
        public class AnalysedQuery
        {
            public TopScoringIntentData topScoringIntent;
            public EntityData[] entities;
            public string query;
        }
    
        // This class contains the Intent LUIS determines 
        // to be the most likely
        [Serializable]
        public class TopScoringIntentData
        {
            public string intent;
            public float score;
        }
    
        // This class contains data for an Entity
        [Serializable]
        public class EntityData
        {
            public string entity;
            public string type;
            public int startIndex;
            public int endIndex;
            public float score;
        }
    
  6. 然后,将以下变量添加到 LuisManager 类中

        public static LuisManager instance;
    
        //Substitute the value of luis Endpoint with your own End Point
        string luisEndpoint = "https://westus.api.cognitive... add your endpoint from the Luis Portal";
    
  7. 请确保现在放入 LUIS 终结点(将从 LUIS 门户获得)。

  8. 现在需要添加 Awake() 方法的代码。 类初始化时,将调用此方法:

        private void Awake()
        {
            // allows this class instance to behave like a singleton
            instance = this;
        }
    
  9. 现在,你需要此应用程序用于将从 MicrophoneManager 类收到的听写发送到 LUIS,然后接收和反序列化响应的方法

  10. 意向的值和关联实体被确定后,将传递到 Behaviours 类的实例以触发预期操作

        /// <summary>
        /// Call LUIS to submit a dictation result.
        /// The done Action is called at the completion of the method.
        /// </summary>
        public IEnumerator SubmitRequestToLuis(string dictationResult, Action done)
        {
            string queryString = string.Concat(Uri.EscapeDataString(dictationResult));
    
            using (UnityWebRequest unityWebRequest = UnityWebRequest.Get(luisEndpoint + queryString))
            {
                yield return unityWebRequest.SendWebRequest();
    
                if (unityWebRequest.isNetworkError || unityWebRequest.isHttpError)
                {
                    Debug.Log(unityWebRequest.error);
                }
                else
                {
                    try
                    {
                        AnalysedQuery analysedQuery = JsonUtility.FromJson<AnalysedQuery>(unityWebRequest.downloadHandler.text);
    
                        //analyse the elements of the response 
                        AnalyseResponseElements(analysedQuery);
                    }
                    catch (Exception exception)
                    {
                        Debug.Log("Luis Request Exception Message: " + exception.Message);
                    }
                }
    
                done();
                yield return null;
            }
        }
    
  11. 创建一个名为 AnalyseResponseElements() 的新方法,该方法将读取生成的 AnalysedQuery 并确定实体。 这些实体被确定后,将传递到 Behaviours 类中来实际使用

        private void AnalyseResponseElements(AnalysedQuery aQuery)
        {
            string topIntent = aQuery.topScoringIntent.intent;
    
            // Create a dictionary of entities associated with their type
            Dictionary<string, string> entityDic = new Dictionary<string, string>();
    
            foreach (EntityData ed in aQuery.entities)
            {
                entityDic.Add(ed.type, ed.entity);
            }
    
            // Depending on the topmost recognized intent, read the entities name
            switch (aQuery.topScoringIntent.intent)
            {
                case "ChangeObjectColor":
                    string targetForColor = null;
                    string color = null;
    
                    foreach (var pair in entityDic)
                    {
                        if (pair.Key == "target")
                        {
                            targetForColor = pair.Value;
                        }
                        else if (pair.Key == "color")
                        {
                            color = pair.Value;
                        }
                    }
    
                    Behaviours.instance.ChangeTargetColor(targetForColor, color);
                    break;
    
                case "ChangeObjectSize":
                    string targetForSize = null;
                    foreach (var pair in entityDic)
                    {
                        if (pair.Key == "target")
                        {
                            targetForSize = pair.Value;
                        }
                    }
    
                    if (entityDic.ContainsKey("upsize") == true)
                    {
                        Behaviours.instance.UpSizeTarget(targetForSize);
                    }
                    else if (entityDic.ContainsKey("downsize") == true)
                    {
                        Behaviours.instance.DownSizeTarget(targetForSize);
                    }
                    break;
            }
        }
    

    重要

    删除 Start() 和 Update() 方法,因为此类将不会使用它们

  12. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

注意

此时,你会注意到 Unity 编辑器控制面板中出现了几个错误。 这是因为代码引用了将在下一章中创建的 Behaviours 类

第 7 章 - 创建 Behaviours 类

Behaviours 类将使用 LuisManager 类提供的实体触发操作

若要创建此类,请执行以下操作:

  1. 双击“Scripts”文件夹将其打开

  2. 右键单击“脚本”文件夹,然后单击“创建”>“C# 脚本”。 将脚本命名为“Behaviours”

  3. 双击该脚本,通过 Visual Studio 打开它

  4. 然后,将以下变量添加到 Behaviours 类中

        public static Behaviours instance;
    
        // the following variables are references to possible targets
        public GameObject sphere;
        public GameObject cylinder;
        public GameObject cube;
        internal GameObject gazedTarget;
    
  5. 添加 Awake() 方法代码。 类初始化时,将调用此方法:

        void Awake()
        {
            // allows this class instance to behave like a singleton
            instance = this;
        }
    
  6. 以下方法由你之前创建的 LuisManager 类调用,以确定哪个对象是查询的目标,然后触发相应的操作

        /// <summary>
        /// Changes the color of the target GameObject by providing the name of the object
        /// and the name of the color
        /// </summary>
        public void ChangeTargetColor(string targetName, string colorName)
        {
            GameObject foundTarget = FindTarget(targetName);
            if (foundTarget != null)
            {
                Debug.Log("Changing color " + colorName + " to target: " + foundTarget.name);
    
                switch (colorName)
                {
                    case "blue":
                        foundTarget.GetComponent<Renderer>().material.color = Color.blue;
                        break;
    
                    case "red":
                        foundTarget.GetComponent<Renderer>().material.color = Color.red;
                        break;
    
                    case "yellow":
                        foundTarget.GetComponent<Renderer>().material.color = Color.yellow;
                        break;
    
                    case "green":
                        foundTarget.GetComponent<Renderer>().material.color = Color.green;
                        break;
    
                    case "white":
                        foundTarget.GetComponent<Renderer>().material.color = Color.white;
                        break;
    
                    case "black":
                        foundTarget.GetComponent<Renderer>().material.color = Color.black;
                        break;
                }          
            }
        }
    
        /// <summary>
        /// Reduces the size of the target GameObject by providing its name
        /// </summary>
        public void DownSizeTarget(string targetName)
        {
            GameObject foundTarget = FindTarget(targetName);
            foundTarget.transform.localScale -= new Vector3(0.5F, 0.5F, 0.5F);
        }
    
        /// <summary>
        /// Increases the size of the target GameObject by providing its name
        /// </summary>
        public void UpSizeTarget(string targetName)
        {
            GameObject foundTarget = FindTarget(targetName);
            foundTarget.transform.localScale += new Vector3(0.5F, 0.5F, 0.5F);
        }
    
  7. 添加 FindTarget() 方法,以确定哪一个 GameObject 是当前意向的目标。 如果实体中未定义显式目标,则此方法将目标默认为被“凝视”的 GameObject

        /// <summary>
        /// Determines which object reference is the target GameObject by providing its name
        /// </summary>
        private GameObject FindTarget(string name)
        {
            GameObject targetAsGO = null;
    
            switch (name)
            {
                case "sphere":
                    targetAsGO = sphere;
                    break;
    
                case "cylinder":
                    targetAsGO = cylinder;
                    break;
    
                case "cube":
                    targetAsGO = cube;
                    break;
    
                case "this": // as an example of target words that the user may use when looking at an object
                case "it":  // as this is the default, these are not actually needed in this example
                case "that":
                default: // if the target name is none of those above, check if the user is looking at something
                    if (gazedTarget != null) 
                    {
                        targetAsGO = gazedTarget;
                    }
                    break;
            }
            return targetAsGO;
        }
    

    重要

    删除 Start() 和 Update() 方法,因为此类将不会使用它们

  8. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 8 章 - 创建 Gaze 类

完成此应用所需的最后一个类是 Gaze 类。 此类会更新对当前位于用户视觉焦点中的 GameObject 的引用

若要创建此类,请执行以下操作:

  1. 双击“Scripts”文件夹将其打开

  2. 右键单击“脚本”文件夹,然后单击“创建”>“C# 脚本”。 将脚本命名为“Gaze”

  3. 双击该脚本,通过 Visual Studio 打开它

  4. 为此类插入以下代码:

        using UnityEngine;
    
        public class Gaze : MonoBehaviour
        {        
            internal GameObject gazedObject;
            public float gazeMaxDistance = 300;
    
            void Update()
            {
                // Uses a raycast from the Main Camera to determine which object is gazed upon.
                Vector3 fwd = gameObject.transform.TransformDirection(Vector3.forward);
                Ray ray = new Ray(Camera.main.transform.position, fwd);
                RaycastHit hit;
                Debug.DrawRay(Camera.main.transform.position, fwd);
    
                if (Physics.Raycast(ray, out hit, gazeMaxDistance) && hit.collider != null)
                {
                    if (gazedObject == null)
                    {
                        gazedObject = hit.transform.gameObject;
    
                        // Set the gazedTarget in the Behaviours class
                        Behaviours.instance.gazedTarget = gazedObject;
                    }
                }
                else
                {
                    ResetGaze();
                }         
            }
    
            // Turn the gaze off, reset the gazeObject in the Behaviours class.
            public void ResetGaze()
            {
                if (gazedObject != null)
                {
                    Behaviours.instance.gazedTarget = null;
                    gazedObject = null;
                }
            }
        }
    
  5. 返回到 Unity 之前,请务必在 Visual Studio 中保存所做的更改

第 9 章 - 完成场景设置

  1. 为了完成场景的设置,请将已创建的每个脚本从“脚本”文件夹拖到层次结构面板中的“主摄像头”对象

  2. 选择“主摄像头”并查看检查器面板,你应该能够看到已附加的每个脚本,并且会注意到每个脚本上都有一些参数尚未设置

    设置摄像头引用目标。

  3. 若要正确设置这些参数,请遵循以下说明:

    1. MicrophoneManager

      • 从层次结构面板中,将“听写文本”对象拖到“听写文本”参数值框中
    2. Behaviours,从层级结构面板中

      • 将“球体”对象拖动到“球体”引用目标框中
      • 将“圆柱体”拖到“圆柱体”引用目标框中
      • 将“立方体”拖到“立方体”引用目标框中
    3. Gaze

      • 将“凝视最大距离”设置为 300(如果尚未设置)
  4. 结果应如下图所示:

    显示摄像头引用目标现已设置。

第 10 章 - 在 Unity 编辑器中测试

测试是否已正确实现场景设置。

请确保:

  • 所有脚本已附加到“主摄像头”对象
  • 已正确分配主摄像头检查器面板中的所有字段
  1. 按 Unity 编辑器中的“播放”按钮。 应用应在连接的沉浸式头戴显示设备中运行。

  2. 尝试一些语句,例如:

    make the cylinder red
    
    change the cube to yellow
    
    I want the sphere blue
    
    make this to green
    
    change it to white
    

    注意

    如果在 Unity 控制台中看到一个指示默认音频设备有变化的错误,则场景可能无法按预期方式工作。 这是混合现实门户处理带内置麦克风的头戴显示设备的内置麦克风的方式造成的。 如果看到此错误,只需停止场景并再次启动它,然后一切就会按预期方式工作。

第 11 章 - 生成和旁加载 UWP 解决方案

确保应用程序在 Unity 编辑器中正常工作后,就可进行生成并部署了。

若要生成,请执行以下操作:

  1. 单击“文件”>“保存”,保存当前场景

  2. 转到“文件”>“生成设置”

  3. 勾选名为“Unity C# 项目”的框(有助于与在创建 UWP 项目后查看和调试代码)

  4. 单击“添加开放式场景”,然后单击“生成”

    “生成设置”窗口

  5. 系统将提示你选择要在其中生成解决方案的文件夹。

  6. 创建“BUILDS”文件夹,然后在此文件夹中使用适当的名称创建另一个文件夹

  7. 单击“选择文件夹”,在该位置开始生成

    创建生成文件夹选择“生成”文件夹

  8. Unity 完成生成后(可能需要一些时间),它应在生成的位置打开“文件资源管理器”窗口

若要在本地计算机上部署:

  1. 在 Visual Studio 中,打开已在上一章中创建的解决方案文件

  2. 在“解决方案平台”中,选择“x86”和“本地计算机”

  3. 在“解决方案配置”中,选择“调试”

    对于 Microsoft HoloLens,你可能会发现,将此选项设置为“远程计算机”会更容易,这样你就不会受限于你的计算机。 不过,还需要执行以下操作:

    • 了解 HoloLens 的 IP 地址,该地址可通过“设置”>“网络和 Internet”>“Wi-Fi”>“高级选项”找到;应使用 IPv4 地址
    • 确保将“开发人员模式”设置为“开”,可通过“设置”>“更新和安全”>“适用于开发人员”找到它

    部署应用

  4. 转到“生成”菜单,并单击“部署解决方案”,将应用程序旁加载到计算机

  5. 应用现在应显示在已安装的应用列表中,可以启动了!

  6. 启动后,应用会提示你授权访问麦克风。 使用运动控制器、语音输入或键盘按“是”按钮

第 12 章 - 改进 LUIS 服务

重要

本章非常重要,可能需要多次查看,因为它将有助于提高 LUIS 服务的准确性:请务必看完本章内容。

若要提升 LUIS 提供的理解水平,需要捕获新的语句,并使用它们重新训练 LUIS 应用。

例如,你可能已经训练了 LUIS 来理解“增加”和“扩大”,但难道你不希望应用还能理解“放大”等词吗?

使用过几次应用程序后,你说过的所有语句将被 LUIS 收集,并在 LUIS 门户中提供。

  1. 根据此链接转到门户应用程序,然后登录。

  2. 用 MS 凭据登录后,单击你的应用名称

  3. 单击页面左侧的“查看终结点语句”按钮

    查看语句

  4. 此时将显示一个列表,其中包含已由混合现实应用程序发送到 LUIS 的语句。

    语句列表

你会注意到一些实体高亮显示

通过将鼠标悬停在每个高亮的字词上,你可查看每个语句,并确定已正确识别哪一个实体、哪些实体出现错误,以及漏掉了哪些实体。

在上面的示例中,发现“标枪”这个词高亮显示作为目标,因此需要将鼠标悬停在这个词上,再单击“删除标签”来更正此错误

检查言语删除标签图像

  1. 如果发现完全错误的语句,可使用屏幕右侧的“删除”按钮将其删除

    删除错误语句

  2. 如果你认为 LUIS 已正确解释语句,可使用“添加到匹配的意向”按钮来验证它的理解

    添加到匹配的意向

  3. 对显示的所有语句进行排序后,请尝试重新加载页面,查看是否有更多语句可用。

  4. 尽可能多地重复此过程来提高应用程序理解水平,这非常重要。

尽情使用吧!

你已完成的 LUIS 集成应用程序

恭喜,你构建了一个混合现实应用,它使用 Azure 语言理解智能服务来理解用户说的话并处理该信息。

实验室结果

额外练习

练习 1

使用此应用程序时,你可能会注意到,如果你凝视地面对象并要求更改其颜色,该应用会照你说的操作。 你能想到如何阻止应用程序更改地面颜色吗?

练习 2

尝试扩展 LUIS 和应用功能,为场景中的对象添加其他功能;例如,在凝视命中点处创建新对象(由用户说的话决定),然后可将这些对象和当前场景对象与现有命令一起使用。