在这篇文章中,我们将讨论游戏 Emoji Scavenger Hunt 的内部运作方式。我们将向您展示如何使用 TensorFlow 训练用于对象识别的自定义模型以及如何在 Web 前端使用 TensorFlow.js 使用该模型。在使用浏览器 API 进行摄像头访问和文本到语音的转换时,我们还将介绍一些挑战和解决方法。这个游戏的所有代码都是开源的,可以在 Github 上找到(https://github.com/google/emoji-scavenger-hunt)。
介绍游戏 Emoji Scavenger Hunt
Emoji Scavenger Hunt 是一个有趣的游戏,你会看到一个表情符合,并在秒数之内找到真实世界的等效物体。当你在现实世界中发现表情符号时,随后的表情符号显示难度增加。从你可能拥有的物品开始,比如鞋子,书本或者你自己的手以及像香蕉,蜡烛甚至踏板车这样的东西。
我们的目标是以有趣,互动的方式展示机器学习技术。
训练对象识别模型
Emoji Scavenger Hunt 游戏的核心功能是识别您的相机所看到的物体,并将其与游戏要求您找到的物体(表情符号)相匹配。但相机如何知道它看到了什么?我们需要一个可以帮助识别物体的模型。最初我们开始使用名为 MobileNet 的预训练模型。这个模型是轻量级的,并针对移动设备进行了优化,但其中的对象太具体,不适合我们的游戏。例如,确定了像 “金毛猎犬” 这样的犬种,但没有 “狗” 的通用对象类。我们逐渐意识到需要训练自定义的图像识别模型。
这是转移学习可以派上用场的地方。转移学习是一种技术,它通过将其用于另一个目标任务来重用针对特定任务而训练的机器学习模型。我们通过使用此教程中描述的过程重新训练基于 MobileNet 的模型来构建我们自己的自定义模型。我们添加了一个全连接层,它将默认输出 logits 映射到我们想要的表情符号对象,如 “手” 和 “键盘” 等。我们列出了大约400个对象用于物体识别并收集 100-1000 个图像作为每个对象的训练数据。添加的全连接层通过组合来自 MobileNet 输出层的 1000 个信号来推断这 400 个对象。
注:教程 链接
https://www.tensorflow.org/hub/tutorials/image_retraining
训练脚本可在 TensorFlow Github 存储库中找到(https://github.com/tensorflow/hub/blob/master/examples/image_retraining/retrain.py)。我们将训练过程编译为 Dockerfile,以便您可以通过指向自己的图像数据集来训练自己的模型
我们运行脚本将训练图像数据提供给模型。为了简化我们的训练流程,我们在 Google Cloud Platform 上构建了整个管道。所有的训练数据都将存储在 Google Cloud 存储桶中。通过在 Google Functions 设置云存储触发器,一旦在存储桶中检测到任何变化,就会启动计算引擎上的 GPU 实例。GPU 实例以 TensorFlow SavedModel 格式输出再训练模型,并将其保存在云存储上的另一个存储桶中。
模型培训的数据管道
我们如何与 TensorFlow.js 集成
完成上述模型训练中的步骤后,我们最终得到了一个用于对象识别的 TensorFlow SavedModel。为了通过 TensorFlow.js 在浏览器中访问和使用此模型,我们使用 TensorFlow.js 转换器将此 SavedModel 转换为 TensorFlow.js 可以加载的格式。
识别对象的行为可以分为两个子任务。首先,从相机中抓取像素,然后将图像数据发送到 TensorFlow.js,以根据我们之前训练过的模型预测它的想法。
相机和模型设置
在我们开始预测对象之前,我们需要确保相机(通过 MediaDevices.getUserMedia)准备好显示内容,并且我们的机器学习模型已加载并准备好开始预测。在我们开始预测之前,我们使用以下代码段来启动这两个代码并执行一些任务设置。
1 Promise.all([
2 this.emojiScavengerMobileNet.load().then(() => this.warmUpModel()),
3 camera.setupCamera().then((value: CameraDimentions) => {
4 camera.setupVideoDimensions(value[0], value[1]);
5 }),
6 ]).then(values => {
7 // Both the camera and model are loaded, we can start predicting
8 this.predict();
9 }).catch(error => {
10 // Some errors occurred and we need to handle them
11 });
一旦成功完成,相机设置和模型加载都将以 Promise 解析。您会注意到,一旦加载了模型,我们就会调用 this.warmUpModel()。这个函数只是做一个预测调用来编译程序并将权重上传到 GPU,这样当我们想要传递真实数据进行预测时,模型就会准备就绪。
将图像数据发送到 TensorFlow.js
以下代码片段(已删除注释)是我们的预测函数调用,它从相机中获取数据,将其解析为正确的图像大小,将其发送到我们的 TensorFlow.js 并使用生成的识别对象来查看我们是否找到了表情符号。
1 async predict() {
2 if (this.isRunning) {
3 const result = tfc.tidy(() => {
4
5 const pixels = tfc.fromPixels(camera.videoElement);
6 const centerHeight = pixels.shape[0] / 2;
7 const beginHeight = centerHeight - (VIDEO_PIXELS / 2);
8 const centerWidth = pixels.shape[1] / 2;
9 const beginWidth = centerWidth - (VIDEO_PIXELS / 2);
10 const pixelsCropped =
11 pixels.slice([beginHeight, beginWidth, 0],
12 [VIDEO_PIXELS, VIDEO_PIXELS, 3]);
13
14 return this.emojiScavengerMobileNet.predict(pixelsCropped);
15 });
16
17 const topK =
18 await this.emojiScavengerMobileNet.getTopKClasses(result, 10);
19
20 this.checkEmojiMatch(topK[0].label, topK[1].label);
21 }
22 requestAnimationFrame(() => this.predict());
21 }
让我们更详细地看一下这个片段。我们将整个预测代码逻辑包装在 requestAnimationFrame 调用中,以确保浏览器在进行屏幕绘制更新时以最有效的方式执行此逻辑。如果游戏处于运行状态,我们只执行预测逻辑。通过这种方式,我们可以确保在执行屏幕动画(如结束和赢取屏幕)时,我们不会运行任何 GPU 密集型预测代码。
另一个小而重要的性能改进是将 TensorFlow.js 逻辑包装在对 tf.tidy() 的调用中。这将确保在执行该逻辑期间创建的所有 TensorFlow.js 张量都将在之后得到清理,从而确保更好的长期运行性能。请参阅https://js.tensorflow.org/api/latest/#tidy
我们预测逻辑的核心与从相机中提取图像以发送到 TensorFlow.js 有关。我们不是简单地拍摄整个相机图像并将其发送出去,而是从相机中心切出一部分屏幕并将其发送到 TensorFlow.js。在我们的游戏中,我们使用 224 像素x 224 像素的参考图像训练我们的模型。将与我们的参考训练数据具有相同尺寸的图像发送到 TensorFlow.js,从而确保更好的预测性能。我们的相机元素(它只是一个 HTML 视频元素)不是 224 像素的原因是因为我们想要确保用户的全屏体验,这意味着使用 CSS 将相机元素扩展到 100% 的屏幕。
以下参考图像显示左上角的切片,该切片将发送到 TensorFlow.js。
然后,模型使用该图像数据生成前 10 个最可能项目的列表。您会注意到我们获取前 2 个值并将其传递给 checkEmojiMatch 以确定我们是否找到了匹配项。我们选择使用前 2 个匹配而不是最顶级的项目,因为它使游戏更有趣,并允许我们根据模型在匹配中留有一些余地。拥有一个过于准确和严格的模型会导致用户在无法识别对象时感到沮丧。
在上面的图像示例中,您可以看到我们目前的任务是找到 “键盘” 表情符号。在此示例中,我们还显示了一些调试信息,因此您可以根据输入图像查看模型预测的所有 10 个可能项目。这里的前两个匹配是 “键盘” 和 “手”,它们都在图像中,而 “手” 具有稍大的可能性。虽然 “键盘” 在第二个检测到的位置,但游戏在这里检测到匹配,因为我们使用前两个匹配进行检查。
为我们的模型提供文本到语音的转换
作为游戏的一个有趣的补充,我们实施了 SpeechSynthesis API。从而当你在寻找表情符号的时候,大声朗读出模型预测。在 Android 上的 Chrome 中,通过以下代码实现这一点非常简单:
1 speak(msg: string) {
2 if (this.topItemGuess) {
3 if ('speechSynthesis' in window) {
4 let msgSpeak = new SpeechSynthesisUtterance();
5 msgSpeak.voice = this.sleuthVoice['activeVoice'];
6
7 msgSpeak.text = msg;
8 speechSynthesis.speak(msgSpeak);
9 }
10 }
11 }
此 API 在 Android 上即时运行,但 iOS 将任何 SpeechSynthesis 调用限制为直接与用户操作相关的调用(例如点击事件),因此我们需要为该平台找到替代解决方案。我们已经熟悉 iOS 将音频播放事件绑定到用户操作的要求,我们通过启动用户最初单击 “播放” 按钮时播放的所有音频文件来处理我们游戏中的其他声音,然后立即暂停所有这些音频文件。最后,我们最终制作了一个音频精灵,其中包含了所有 “成功” 的语音线(例如,“嘿,你找到了啤酒”)。这种方法的缺点是这个音频精灵文件变得非常大,对话需要更多。
我们尝试过的一种方法是将音频精灵分解为前缀(“嘿,你找到了”,“是那个”)和后缀(“啤酒”,“香蕉” 等),但我们发现 iOS 在播放一个音频文件的片段,暂停,移动播放头,然后播放同一文件的另一个片段之间增加了不可避免的一秒延迟。前缀和后缀之间的差距很长,以至于感觉很刺耳,我们经常发现语音会远远落后于实际的游戏玩法。我们仍在调查 iOS 上语音改进的其他选项。
下面是我们播放音频文件的函数,其中包含通过开始和停止时间戳处理播放音频精灵片段的附加代码:
1 playAudio(audio: string, loop = false, startTime = 0,
2 endTime:number = undefined) {
3 let audioElement = this.audioSources[audio];
4 if (loop) {
5 audioElement.loop = true;
6 }
7 if (!this.audioIsPlaying(audio)) {
8 audioElement.currentTime = startTime;
9 let playPromise = audioElement.play();
10 if (endTime !== undefined) {
11 const timeUpdate = (e: Event) => {
12 if (audioElement.currentTime >= endTime) {
13 audioElement.pause();
14 audioElement.removeEventListener('timeupdate', timeUpdate);
15 }
16 };
17 audioElement.addEventListener('timeupdate', timeUpdate);
18 }
19 if (playPromise !== undefined) {
20 playPromise.catch(error => {
21 console.log('Error in playAudio: ' + error);
22 });
23 }
24 }
25 }
通过 getUserMedia 进行相机访问时可能存在的风险
Emoji Scavenger Hunt 在很大程度上依赖于能够通过浏览器中的 Javascript 访问相机。我们在浏览器中使用 MediaDevices.getUserMedia API 来访问摄像头。并非所有浏览器都支持此 API,但大多数主流浏览器的最新版本都有很好的支持。
要通过此 API 访问相机,我们使用以下代码段:
1 if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
2 const stream = await navigator.mediaDevices.getUserMedia({
3 'audio': false,
4 'video': {facingMode: 'environment'}
5 });
6 (
7 this.videoElement.srcObject = stream;
8 }
此 API 提供了一种通过传入配置对象并指定 facingMode 来访问前置和后置摄像头的方法。
无法通过 UIWebViews 访问
在测试期间,我们意识到 Apple 不支持任何基于 webkit 浏览器的 UIWebView 使用 getUserMedia API,这意味着 iOS 上任何实现自己浏览器的应用程序,如第三方 Twitter 客户端或 iOS 上的 Chrome,都无法访问摄像头。
为解决此问题,我们会检测到相机初始化失败,并提示用户在本机 Safari 浏览器中打开体验。
致谢
通过这个实验,我们想要创建一个有趣和愉快的游戏,利用当今浏览器中提供的惊人的机器学习技术。这只是一个开始,我们希望您能使用 TensorFlow.js 和 TensorFlow.js 转换器实现您所有的想法。如上所述,我们的代码可以在 Github 上找到(https://github.com/google/emoji-scavenger-hunt),所以请用它来开始你自己的想法。
在构建这个实验的过程中,我们要感谢 Takashi Kawashima,Daniel Smilkov,Nikhil Thorat 和 Ping Yu 的帮助。
全部0条评论
快来发表一下你的评论吧 !