Skip to content

Latest commit

 

History

History
801 lines (555 loc) · 44.8 KB

File metadata and controls

801 lines (555 loc) · 44.8 KB

五、了解简单的语音命令

如今,语音服务(例如 Apple Siri,Amazon Alexa,Google Assistant 和 Google Translate)已变得越来越流行,因为语音是我们在某些情况下查找信息或完成任务的最自然和有效的方法。 这些语音服务中的许多服务都是基于云的,因为用户语音可能会很长而且很自由,并且自动语音识别ASR)非常复杂,并且需要大量的计算能力。 实际上,得益于深度学习的突破,仅在最近几年,在自然和嘈杂的环境中 ASR 才变得可行。

但是在某些情况下,能够离线识别设备上的简单语音命令是有意义的。 例如,要控制 Raspberry-Pi 驱动的机器人的运动,您不需要复杂的语音命令,不仅设备上的 ASR 比基于云的解决方案还快,而且即使在没有网络访问的环境。 设备上的简单语音命令识别还可以通过仅在发出某些明确的用户命令时才向服务器发送复杂的用户语音来节省网络带宽。

在本章中,我们将首先概述 ASR 技术,涵盖基于最新的深度学习系统和顶级开源项目。 然后,我们将讨论如何训练和重新训练 TensorFlow 模型,以识别简单的语音命令,例如"left", "right", "up", "down", "stop", "go"。 接下来,我们将使用训练有素的模型来构建一个简单的 Android 应用,然后再构建两个完整的 iOS 应用,一个由 Objective-C 实现,另一个由 Swift 实现。 在前两章中我们没有介绍使用 TensorFlow 模型的基于 Swift 的 iOS 应用,而本章是回顾和加强我们对构建基于 Swift 的 TensorFlow iOS 应用的理解的好地方。

总之,本章将涵盖以下主题:

  • 语音识别 -- 快速概述
  • 训练简单的命令识别模型
  • 在 Android 中使用简单的语音识别模型
  • 在带有 Objective-C 的 iOS 中使用简单的语音识别模型
  • 在带有 Swift 的 iOS 中使用简单的语音识别模型

语音识别 -- 快速概述

1990 年代出现了第一个实用的独立于说话者的大词汇量和连续语音识别系统。 在 2000 年代初期,领先的初创公司 Nuance 和 SpeechWorks 提供的语音识别引擎为许多第一代基于 Web 的语音服务提供了支持,例如 TellMe,Phone 的 AOL 和 BeVocal。 当时构建的语音识别系统主要基于传统的隐马尔可夫模型HMM),并且需要手动编写语法和安静环境以帮助识别引擎更准确地工作。

现代语音识别引擎几乎可以理解嘈杂环境下人们的任何说话,并且基于端到端深度学习,尤其是另一种更适合自然语言处理的深度神经网络,称为循环神经网络RNN)。 与传统的基于 HMM 的语音识别不同,传统的基于 HMM 的语音识别需要人的专业知识来构建和微调手工设计的特征以及声学和语言模型,而基于 RNN 的端到端语音识别系统则将音频输入直接转换为文本,而无需将音频输入转换为语音表示以进行进一步处理。

RNN 允许我们处理输入和/或输出的序列,因为根据设计,网络可以存储输入序列中的先前项目或可以生成输出序列。 这使 RNN 更适用于语音识别(输入是用户说出的单词序列),图像标题(输出是由一系列单词组成的自然语言句子),文本生成和时间序列预测 。 如果您不熟悉 RNN,则一定要查看 Andrey Karpathy 的博客,循环神经网络的不合理有效性。 在本书的后面,我们还将介绍一些详细的 RNN 模型。

关于 RNN 端到端语音识别的第一篇研究论文发表于 2014 年,使用的是连接主义的时间分类CTC)层。 2014 年下半年,百度发布了 Deep Speech,这是第一个使用基于 CTC 的端到端 RNN 构建但拥有庞大数据集的商业系统之一 ,并在嘈杂的环境中实现了比传统 ASR 系统更低的错误率。 如果您有兴趣,可以查看深度语音的 TensorFlow 实现,但是由于此类基于 CTC 的系统存在问题,生成的模型需要太多的资源才能在手机上运行。 在部署期间,它需要一个大型语言模型来纠正部分由 RNN 的性质引起的生成的文本错误(如果您想知道为什么,请阅读前面链接的 RNN 博客以获取一些见识)。

在 2015 年和 2016 年,较新的语音识别系统使用了类似的端到端 RNN 方法,但将 CTC 层替换为基于注意力的模型,因此运行模型时不需要大型语言模型,因此可以在内存有限的移动设备上进行部署。 在本书的此版本中,我们将不会探讨这种可能性,而将介绍如何在移动应用中使用最新的高级 ASR 模型。 相反,我们将从一个更简单的语音识别模型开始,我们知道该模型肯定会在移动设备上很好地工作。

要将离线语音识别功能添加到移动应用,您还可以使用以下两个领先的开源语音识别项目之一:

  • CMU Sphinx 大约 20 年前开始,但仍在积极开发中。 要构建具有语音识别功能的 Android 应用,您可以使用其为 Android 构建的 PocketSphinx。 要构建具有语音识别功能的 iOS 应用,您可以使用 OpenEars 框架,这是一个免费的 SDK,在 iOS 应用中使用 CMU PocketSphinx 构建离线语音识别和文本转换。
  • Kaldi,成立于 2009 年,最近非常活跃,截至 2018 年 1 月,已有 165 个参与者。要在 Android 上进行尝试,您可以查看此博客文章。 对于 iOS,请查看在 iOS 上使用 Kaldi 的原型

由于这是一本关于在移动设备上使用 TensorFlow 的书,因此 TensorFlow 可用于为图像处理,语音处理和文本处理以及其他智能任务(本章其余部分的)构建强大的模型。 我们将重点介绍如何使用 TensorFlow 训练简单的语音识别模型并将其在移动应用中使用。

训练简单的命令识别模型

在本节中,我们将总结编写良好的 TensorFlow 简单音频识别教程中使用的步骤。 一些在训练模型时可能对您有帮助的提示。

我们将建立的简单语音命令识别模型将能够识别 10 个单词:"yes", "no", "up", "down", "left", "right", "on", "off", "stop", "go"; 它也可以检测沉默。 如果没有发现沉默,并且没有发现 10 个单词,它将生成“未知”。 稍后运行tensorflow/example/speech_commands/train.py脚本时,我们将下载语音命令数据集并用于训练模型,实际上除了这 10 个单词外,还包含 20 个单词:"zero", "two", "three", ..., "ten"(到目前为止,您已经看到的 20 个词称为核心词)和 10 个辅助词:"bed", "bird", "cat", "dog", "happy", "house", "marvin", "sheila", "tree", "wow"。 核心词比辅助词(约 1750)具有更多的.wav文件记录(约 2350)。

语音命令数据集是从开放语音记录站点收集的。您应该尝试一下,也许自己花些时间来录制自己的录音,以帮助改善录音效果,并在需要时了解如何收集自己的语音命令数据集。 关于使用数据集构建模型,还有一个 Kaggle 竞赛,您可以在此处了解有关语音模型和提示的更多信息。

在移动应用中要训练和使用的模型基于纸质卷积神经网络,用于小大小关键词发现,这与大多数其他基于 RNN 的大规模语音识别模型不同。 基于 CNN 的语音识别模型是可能的,但很有趣,因为对于简单的语音命令识别,我们可以在短时间内将音频信号转换为图像,或更准确地说,将频谱图转换为频率窗口期间音频信号的分布(有关使用wav_to_spectrogram脚本生成的示例频谱图图像,请参见本节开头的 TensorFlow 教程链接)。 换句话说,我们可以将音频信号从其原始时域表示转换为频域表示。 进行此转换的最佳算法是离散傅立叶变换DFT),快速傅立叶变换FFT)只是一种有效的选择 DFT 实现的算法。

作为移动开发人员,您可能不需要了解 DFT 和 FFT。 但是,您最好了解所有这些模型训练在移动应用中使用时是如何工作的,因为我们知道我们将要介绍的 TensorFlow 简单语音命令模型训练的幕后花絮,这是 FFT 的使用,前十大模型之一。当然,除其他事项外,20 世纪的算法使基于 CNN 的语音命令识别模型训练成为可能。 有关 DFT 的有趣且直观的教程,您可以阅读以下文章

现在,让我们执行以下步骤来训练简单语音命令识别模型:

  1. 在终端上,cd到您的 TensorFlow 源根,可能是~/tensorflow-1.4.0
  2. 只需运行以下命令即可下载我们之前讨论的语音命令数据集:
python tensorflow/examples/speech_commands/train.py

您可以使用许多参数:--wanted_words默认为以yes开头的 10 个核心词; 您可以使用此参数添加更多可以被模型识别的单词。 要训​​练自己的语音命令数据集,请使用--data_url --data_dir=<path_to_your_dataset>禁用语音命令数据集的下载并访问您自己的数据集,其中每个命令应命名为自己的文件夹,其中应包含 1000-2000 个音频剪辑,大约需要 1 秒钟的长度; 如果音频片段更长,则可以相应地更改--clip_duration_ms参数值。 有关更多详细信息,请参见train.py源代码和 TensorFlow 简单音频识别教程。

  1. 如果您接受train.py的所有默认参数,则在下载 1.48GB 语音命令数据集之后,在 GTX-1070 GPU 驱动的 Ubuntu 上,完成 18,000 个步骤的整个训练大约需要 90 分钟。 训练完成后,您应该在/tmp/speech_commands_train文件夹内看到检查点文件的列表,以及conv.pbtxt图定义文件和名为conv_labels.txt的标签文件,其中包含命令列表(与命令列表相同)。 --wanted_words参数是默认值或设置为,在文件的开头加上两个附加词_silence_unknown):
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:08 conv.ckpt-18000.meta
-rw-rw-r-- 1 jeff jeff 433 Dec 9 21:08 checkpoint
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:08 conv.ckpt-18000.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:08 conv.ckpt-18000.index
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:08 conv.ckpt-17900.meta
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:08 conv.ckpt-17900.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:08 conv.ckpt-17900.index
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:07 conv.ckpt-17800.meta
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:07 conv.ckpt-17800.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:07 conv.ckpt-17800.index
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:07 conv.ckpt-17700.meta
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:07 conv.ckpt-17700.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:07 conv.ckpt-17700.index
-rw-rw-r-- 1 jeff jeff 75437 Dec 9 21:06 conv.ckpt-17600.meta
-rw-rw-r-- 1 jeff jeff 3707448 Dec 9 21:06 conv.ckpt-17600.data-00000-of-00001
-rw-rw-r-- 1 jeff jeff 315 Dec 9 21:06 conv.ckpt-17600.index
-rw-rw-r-- 1 jeff jeff 60 Dec 9 19:41 conv_labels.txt
-rw-rw-r-- 1 jeff jeff 121649 Dec 9 19:41 conv.pbtxt

conv_labels.txt包含以下命令:

_silence_
_unknown_
yes
no
up
down
left
right
on
off
stop
go

现在运行以下命令,将图定义文件和检查点文件组合成一个我们可以在移动应用中使用的模型文件:

python tensorflow/examples/speech_commands/freeze.py \
--start_checkpoint=/tmp/speech_commands_train/conv.ckpt-18000 \
--output_file=/tmp/speech_commands_graph.pb
  1. (可选)在移动应用中部署speech_commands_graph.pb模型文件之前,可以使用以下命令对其进行快速测试:
python tensorflow/examples/speech_commands/label_wav.py  \
--graph=/tmp/speech_commands_graph.pb \
--labels=/tmp/speech_commands_train/conv_labels.txt \
--wav=/tmp/speech_dataset/go/9d171fee_nohash_1.wav

您将看到类似以下的输出:

go (score = 0.48427)
no (score = 0.17657)
_unknown_ (score = 0.08560)
  1. 使用summarize_graph工具查找输入节点和输出节点的名称:
bazel-bin/tensorflow/tools/graph_transforms/summarize_graph --in_graph=/tmp/speech_commands_graph.pb

输出应如下所示:

Found 1 possible inputs: (name=wav_data, type=string(7), shape=[]) 
No variables spotted.
Found 1 possible outputs: (name=labels_softmax, op=Softmax) 

不幸的是,它仅对于输出名称是正确的,并且不显示其他可能的输入。 使用tensorboard --logdir /tmp/retrain_logs,然后在浏览器中打开http://localhost:6006与图进行交互也无济于事。 但是,前面各章中显示的小代码段可以帮助您了解输入和输出名称,以下内容与 iPython 进行了交互:

In [1]: import tensorflow as tf
In [2]: g=tf.GraphDef()
In [3]: g.ParseFromString(open("/tmp/speech_commands_graph.pb","rb").read())
In [4]: x=[n.name for n in g.node]
In [5]: x
Out[5]: 
[u'wav_data',
 u'decoded_sample_data',
 u'AudioSpectrogram',
 ...
 u'MatMul',
 u'add_2',
 u'labels_softmax']

因此,我们看到wav_datadecoded_sample_data都是可能的输入。 如果在freeze.py文件中看不到注释,我们就必须深入研究模型训练代码,以准确找出应该使用的输入名称:“结果图包含一个名为 WAV 的编码数据输入 wav_data,用于原始 PCM 数据(在 -1.0 到 1.0 范围内浮动)的一种称为decoded_sample_data,输出称为labels_softmax。” 实际上,在该模型的情况下,有一个 TensorFlow Android 示例应用,这是我们在第 1 章,“移动 TensorFlow 入门”中看到的一部分,称为 TF 语音,专门定义了那些输入名称和输出名称。 在本书后面的几章中,您将看到如何在需要时借助或不借助我们的三种方法来查找模型训练的源代码,以找出关键的输入和输出节点名称。 或者希望,当您阅读本书时,TensorFlow summarize_graph工具将得到改进,以为我们提供准确的输入和输出节点名称。

现在是时候在移动应用中使用我们的热门新模型了。

在 Android 中使用简单的语音识别模型

位于tensorflow/example/android的用于简单语音命令识别的 TensorFlow Android 示例应用具有在SpeechActivity.java文件中进行音频记录和识别的代码,假定该应用需要始终准备好接受新的音频命令。 尽管在某些情况下这确实是合理的,但它导致的代码比仅在用户按下按钮后才进行记录和识别的代码要复杂得多,例如 Apple 的 Siri 的工作方式。 在本部分中,我们将向您展示如何创建新的 Android 应用并添加尽可能少的代码来记录用户的语音命令并显示识别结果。 这应该可以帮助您更轻松地将模型集成到自己的 Android 应用中。 但是,如果您需要处理语音命令应始终自动记录和识别的情况,则应查看 TensorFlow 示例 Android 应用。

使用模型构建新应用

执行以下步骤来构建一个完整的新 Android 应用,该应用使用我们在上一节中构建的speech_commands_graph.pb模型:

  1. 通过接受前面几章中的所有默认设置,创建一个名为AudioRecognition的新 Android 应用,然后将compile 'org.tensorflow:tensorflow-android:+'行添加到应用build.gradle文件依赖项的末尾。
  2. <uses-permission android:name="android.permission.RECORD_AUDIO" />添加到应用的AndroidManifest.xml文件中,以便可以允许该应用记录音频。
  3. 创建一个新的资产文件夹,然后将在上一节的步骤 2 和 3 中生成的speech_commands_graph.pbconv_actions_labels.txt文件拖放到assets文件夹中。
  4. 更改activity_main.xml文件以容纳三个 UI 元素。 第一个是用于识别结果显示的TextView
<TextView
    android:id="@+id/textview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text=""
    android:textSize="24sp"
    android:textStyle="bold"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

第二个TextView将显示上一节第 2 步中使用train.py Python 程序训练的 10 个默认命令:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="yes no up down left right on off stop go"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintHorizontal_bias="0.50"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.25" />

最后一个 UI 元素是一个按钮,在点击该按钮时,它会开始录音一秒钟,然后将录音发送到我们的模型以进行识别:

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Start"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintHorizontal_bias="0.50"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.8" />
  1. 打开MainActivity.java,首先创建MainActivity implements Runnable类。 然后添加以下常量,以定义模型名称,标签名称,输入名称和输出名称:
private static final String MODEL_FILENAME = "file:///android_asset/speech_commands_graph.pb";
private static final String LABEL_FILENAME = "file:///android_asset/conv_actions_labels.txt";
private static final String INPUT_DATA_NAME = "decoded_sample_data:0";
private static final String INPUT_SAMPLE_RATE_NAME = "decoded_sample_data:1";
private static final String OUTPUT_NODE_NAME = "labels_softmax";
  1. 声明四个实例变量:
private TensorFlowInferenceInterface mInferenceInterface;
private List<String> mLabels = new ArrayList<String>();
private Button mButton;
private TextView mTextView;
  1. onCreate方法中,我们首先实例化mButtonmTextView,然后设置按钮单击事件处理器,该事件处理器首先更改按钮标题,然后启动线程进行记录和识别:
mButton = findViewById(R.id.button);
mTextView = findViewById(R.id.textview);
mButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mButton.setText("Listening...");
        Thread thread = new Thread(MainActivity.this);
        thread.start();
    }
});

onCreate方法的末尾,我们逐行读取标签文件的内容,并将每一行保存在mLabels数组列表中。

  1. public void run()方法的开头(单击“开始”按钮时开始),添加代码,该代码首先获得用于创建 Android AudioRecord对象的最小缓冲区大小,然后使用buffersize创建新的AudioRecord实例具有 16,000 SAMPLE_RATE和 16 位单声道格式,这是我们模型所期望的原始音频的类型,并最终从AudioRecord实例开始记录:
int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
AudioRecord record = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);

if (record.getState() != AudioRecord.STATE_INITIALIZED) return;
record.startRecording();

Android 中有两个用于记录音频的类:MediaRecorderAudioRecordMediaRecorderAudioRecord更易于使用,但是它会保存压缩的音频文件,直到 Android API Level 24(Android 7.0)为止,该 API 支持录制未经处理的原始音频。 根据这里,截至 2018 年 1 月,市场上有 70% 以上的 Android 设备仍在运行 7.0 或更早的 Android 版本。 您可能不希望将应用定位到 Android 7.0 或更高版本。 另外,要解码由MediaRecorder录制的压缩音频,您必须使用MediaCodec,使用起来非常复杂。 AudioRecord尽管是一个低级的 API,但实际上非常适合记录未处理的原始数据,然后将其发送到语音命令识别模型进行处理。

  1. 创建两个由 16 位短整数组成的数组audioBufferrecordingBuffer,对于 1 秒记录,每次AudioRecord对象读取并填充audioBuffer数组后,实际读取的数据都会附加到 recordingBuffer
long shortsRead = 0;
int recordingOffset = 0;
short[] audioBuffer = new short[bufferSize / 2];
short[] recordingBuffer = new short[RECORDING_LENGTH];
while (shortsRead < RECORDING_LENGTH) { // 1 second of recording
    int numberOfShort = record.read(audioBuffer, 0, audioBuffer.length);
    shortsRead += numberOfShort;
    System.arraycopy(audioBuffer, 0, recordingBuffer, recordingOffset, numberOfShort);
    recordingOffset += numberOfShort;
}
record.stop();
record.release();
  1. 录制完成后,我们首先将按钮标题更改为Recognizing
runOnUiThread(new Runnable() {
    @Override
    public void run() {
        mButton.setText("Recognizing...");
    }
});

然后将recordingBuffer短数组转换​​为float数组,同时使float数组的每个元素都在 -1.0 和 1.0 的范围内,因为我们的模型期望在-之间浮动 1.0 和 1.0:

float[] floatInputBuffer = new float[RECORDING_LENGTH];
for (int i = 0; i < RECORDING_LENGTH; ++i) {
    floatInputBuffer[i] = recordingBuffer[i] / 32767.0f;
}
  1. 如前几章所述,创建一个新的TensorFlowInferenceInterface,然后使用两个输入节点的名称和值调用其feed方法,其中一个是采样率,另一个是存储在floatInputBuffer中的原始音频数据 ]数组:
AssetManager assetManager = getAssets();
mInferenceInterface = new TensorFlowInferenceInterface(assetManager, MODEL_FILENAME);

int[] sampleRate = new int[] {SAMPLE_RATE};
mInferenceInterface.feed(INPUT_SAMPLE_RATE_NAME, sampleRate);

mInferenceInterface.feed(INPUT_DATA_NAME, floatInputBuffer, RECORDING_LENGTH, 1);

之后,我们调用run方法在模型上运行识别推理,然后fetch输出 10 个语音命令中每个命令的输出分数以及“未知”和“沉默”输出:

String[] outputScoresNames = new String[] {OUTPUT_NODE_NAME};
mInferenceInterface.run(outputScoresNames);

float[] outputScores = new float[mLabels.size()];
mInferenceInterface.fetch(OUTPUT_NODE_NAME, outputScores);
  1. outputScores数组与mLabels列表匹配,因此我们可以轻松找到最高得分并获取其命令名称:
float max = outputScores[0];
int idx = 0;
for (int i=1; i<outputScores.length; i++) {
    if (outputScores[i] > max) {
        max = outputScores[i];
        idx = i;
    }
}
final String result = mLabels.get(idx);

最后,我们在TextView中显示结果,并将按钮标题更改回"Start",以便用户可以再次开始记录和识别语音命令:

runOnUiThread(new Runnable() {
    @Override
    public void run() {
        mButton.setText("Start");
        mTextView.setText(result);
    }
});

显示模型驱动的识别结果

现在,在您的 Android 设备上运行该应用。 您将看到如图 5.1 所示的初始屏幕:

图 5.1:应用启动后显示初始屏幕

点击START按钮,然后开始说上面显示的 10 个命令之一。 您将看到按钮标题更改为“监听...”,然后是“识别...”,如图 5.2 所示:

图 5.2:监听录制的音频并识别录制的音频

识别结果几乎实时显示在屏幕中间,如图 5.3 所示:

图 5.3:显示识别的语音命令

整个识别过程几乎立即完成,用于识别的speech_commands_graph.pb模型仅为 3.7MB。 当然,它仅支持 10 条语音命令,但是即使使用train.py脚本的 --wanted_words参数或您自己的数据集支持数十个命令,大小也不会发生太大变化,正如我们在训练部分中讨论的那样。

诚然,此处的应用屏幕截图并不像上一章中那样生动有趣(一张图片价值一千个单词),但是语音识别当然可以做艺术家不能做的事情,例如发出语音命令来控制机器人的运动。

该应用的完整源代码位于 Github 上该书的源代码存储库的Ch5/android文件夹中。 现在让我们看看如何使用该模型构建 iOS 应用,其中涉及一些复杂的 TensorFlow iOS 库构建和音频数据准备步骤,以使模型正确运行。

通过 Objective-C 在 iOS 中使用简单的语音识别模型

如果您已经阅读了前三章中的 iOS 应用,那么您可能更喜欢使用手动构建的 TensorFlow iOS 库而不是 TensorFlow 实验窗格,就像使用手动库方法一样,您可以更好地控制可以添加哪些 TensorFlow 操作来使您的模型满意,这也是我们决定专注于 TensorFlow Mobile 而不是第 1 章,“移动 TensorFlow”的 TensorFlow Lite 的原因之一。

因此,尽管您可以在阅读本书时尝试使用 TensorFlow Pod,以查看 Pod 是否已更新以支持模型中使用的所有操作,但从现在开始,我们将始终使用手动构建的 TensorFlow 库( 请参见 iOS 应用中第 3 章,“检测对象及其位置”的“在 iOS 中使用对象检测模型的”部分的步骤 1 和 2)。

使用模型构建新应用

现在执行以下步骤来创建一个新的 iOS 应用以使用语音命令识别模型:

  1. 在 Xcode 中创建一个名为 AudioRecognition 的新 Objective-C 应用,并将项目设置为使用 TensorFlow 手动构建的库,如“以惊人的艺术样式迁移图片”的步骤 1 中所述。 还将AudioToolbox.frameworkAVFoundation.frameworkAccelerate.framework添加到目标的带库的链接二进制文件。

  2. speech_commands_graph.pb模型文件拖放到项目中。

  3. ViewController.m的扩展名更改为mm,然后添加音频记录和处理所使用的以下标头:

#import <AVFoundation/AVAudioRecorder.h>
#import <AVFoundation/AVAudioSettings.h>
#import <AVFoundation/AVAudioSession.h>
#import <AudioToolbox/AudioToolbox.h>

还添加 TensorFlow 的标头:

#include <fstream>
#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/framework/tensor.h"
#include "tensorflow/core/public/session.h"

现在,定义一个音频SAMPLE_RATE常量,一个指向浮点数组的 C 指针,该数组保存将要发送到模型的音频数据,我们的关键audioRecognition函数签名以及两个属性,其中包含记录的文件路径和一个 iOS AVAudioRecorder实例。 我们还需要让ViewController实现AudioRecorderDelegate,以便它知道录制何时结束:

const int SAMPLE_RATE = 16000;
float *floatInputBuffer;
std::string audioRecognition(float* floatInputBuffer, int length);
@interface ViewController () <AVAudioRecorderDelegate>
@property (nonatomic, strong) NSString *recorderFilePath;
@property (nonatomic, strong) AVAudioRecorder *recorder;
@end

在此,我们不会显示以编程方式创建两个 UI 元素的代码段:一个按钮,当您点击该按钮时,它将开始录制 1 秒钟的音频,然后将音频发送到我们的模型以进行识别,以及一个显示识别结果的标签。 但是,我们将在下一部分中的 Swift 中展示一些 UI 代码以供复习。

  1. 在按钮的UIControlEventTouchUpInside处理器内,我们首先创建一个AVAudioSession实例,并将其类别设置为记录并将其激活:
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *err = nil;
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&err];
if(err){
    NSLog(@"audioSession: %@", [[err userInfo] description]);
    return;
}

[audioSession setActive:YES error:&err];
if(err){
    NSLog(@"audioSession: %@", [[err userInfo] description]);
    return;
}

然后创建一个记录设置字典:

NSMutableDictionary *recordSetting = [[NSMutableDictionary alloc] init];
[recordSetting setValue:[NSNumber numberWithInt:kAudioFormatLinearPCM] forKey:AVFormatIDKey];
[recordSetting setValue:[NSNumber numberWithFloat:SAMPLE_RATE] forKey:AVSampleRateKey];
[recordSetting setValue:[NSNumber numberWithInt: 1] forKey:AVNumberOfChannelsKey];
[recordSetting setValue :[NSNumber numberWithInt:16] forKey:AVLinearPCMBitDepthKey];
[recordSetting setValue :[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsBigEndianKey];
[recordSetting setValue :[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsFloatKey];
[recordSetting setValue:[NSNumber numberWithInt:AVAudioQualityMax] forKey:AVEncoderAudioQualityKey];

最后,在按钮点击处理器中,我们定义保存录制的音频的位置,创建AVAudioRecorder实例,设置其委托并开始录制 1 秒钟:

self.recorderFilePath = [NSString stringWithFormat:@"%@/recorded_file.wav", [NSHomeDirectory() stringByAppendingPathComponent:@"tmp"]];
NSURL *url = [NSURL fileURLWithPath:_recorderFilePath];
err = nil;
_recorder = [[ AVAudioRecorder alloc] initWithURL:url settings:recordSetting error:&err];
if(!_recorder){
    NSLog(@"recorder: %@", [[err userInfo] description]);
    return;
}
[_recorder setDelegate:self];
[_recorder prepareToRecord];
[_recorder recordForDuration:1];
  1. AVAudioRecorderDelegateaudioRecorderDidFinishRecording的委托方法中,我们使用 Apple 的扩展音频文件服务,该服务用于读写压缩和线性 PCM 音频文件,以加载记录的音频,并将其转换为模型所需的格式, 并将音频数据读入存储器。 我们在这里不会显示这部分代码,它主要基于此博客。 在此处理之后,floatInputBuffer指向原始音频样本。 现在,我们可以将数据传递到工作线程中的audioRecognition方法中,并在 UI 线程中显示结果:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    std::string command = audioRecognition(floatInputBuffer, totalRead);
    delete [] floatInputBuffer;
    dispatch_async(dispatch_get_main_queue(), ^{
        NSString *cmd = [NSString stringWithCString:command.c_str() encoding:[NSString defaultCStringEncoding]];
        [_lbl setText:cmd];
        [_btn setTitle:@"Start" forState:UIControlStateNormal];
    });
});
  1. audioRecognition方法内部,我们首先定义一个 C++ string数组,其中包含要识别的 10 个命令以及两个特殊值"_silence_""_unknown_"
std::string commands[] = {"_silence_", "_unknown_", "yes", "no", "up", "down", "left", "right", "on", "off", "stop", "go"};

在完成标准 TensorFlow SessionStatusGraphDef设置后(如我们在前几章的 iOS 应用中所做的那样),我们读出了模型文件,并尝试使用它创建 TensorFlow Session

NSString* network_path = FilePathForResourceName(@"speech_commands_graph", @"pb");

PortableReadFileToProto([network_path UTF8String], &tensorflow_graph);

tensorflow::Status s = session->Create(tensorflow_graph);
if (!s.ok()) {
    LOG(ERROR) << "Could not create TensorFlow Graph: " << s;
    return "";
}

如果成功创建了会话,则为模型定义两个输入节点名称和一个输出节点名称:

std::string input_name1 = "decoded_sample_data:0";
std::string input_name2 = "decoded_sample_data:1";
std::string output_name = "labels_softmax";
  1. 对于"decoded_sample_data:0",我们需要将采样率值作为标量发送(否则在调用 TensorFlow Sessionrun方法时会出错),并且在 TensorFlow C++ API 中定义了张量,如下所示:
tensorflow::Tensor samplerate_tensor(tensorflow::DT_INT32, tensorflow::TensorShape());
samplerate_tensor.scalar<int>()() = SAMPLE_RATE;

对于 "decoded_sample_data:1",需要将浮点数中的音频数据从floatInputBuffer数组转换为 TensorFlow audio_tensor张量,其方式类似于前几章的image_tensor的定义和设置方式:

tensorflow::Tensor audio_tensor(tensorflow::DT_FLOAT, tensorflow::TensorShape({length, 1}));
auto audio_tensor_mapped = audio_tensor.tensor<float, 2>();
float* out = audio_tensor_mapped.data();
for (int i = 0; i < length; i++) {
    out[i] = floatInputBuffer[i];
}

现在我们可以像以前一样使用输入来运行模型并获取输出:

std::vector<tensorflow::Tensor> outputScores;
tensorflow::Status run_status = session->Run({{input_name1, audio_tensor}, {input_name2, samplerate_tensor}},{output_name}, {}, &outputScores);
if (!run_status.ok()) {
    LOG(ERROR) << "Running model failed: " << run_status;
    return "";
}
  1. 我们对模型的outputScores输出进行简单的解析,然后返回最高分。 outputScores是 TensorFlow 张量的向量,其第一个元素包含 12 个可能的识别结果的 12 个得分值。 可以通过flat方法访问这 12 个得分值,并检查最大得分:
tensorflow::Tensor* output = &outputScores[0];
const Eigen::TensorMap<Eigen::Tensor<float, 1, Eigen::RowMajor>, Eigen::Aligned>& prediction = output->flat<float>();
const long count = prediction.size();
int idx = 0;
float max = prediction(0);
for (int i = 1; i < count; i++) {
    const float value = prediction(i);
    printf("%d: %f", i, value);
    if (value > max) {
        max = value;
        idx = i;
    }
}

return commands[idx];

在应用可以录制任何音频之前,您需要做的另一件事是在应用的Info.plist文件中创建一个新的隐私-麦克风使用说明属性,并将该属性的值设置为诸如“听到并识别” 您的语音命令”。

现在,在 iOS 模拟器上运行该应用(如果您的 Xcode 版本早于 9.2,而 iOS 模拟器版本早于 10.0,则您可能必须在实际的 iOS 设备上运行该应用,因为您可能无法在 iOS 或 iPhone 模拟器(10.0 之前的版本)中录制音频,您将首先看到带有 Start 按钮位于中间的初始屏幕,然后点击该按钮并说出 10 个命令之一,识别结果应出现在顶部 ,如图 5.4 所示:

图 5.4:显示初始画面和识别结果

是的,应该会出现识别结果,但实际上不会出现,因为在 Xcode 输出窗格中会出现错误:

Could not create TensorFlow Graph: Not found: Op type not registered 'DecodeWav' in binary running on XXX's-MacBook-Pro.local. Make sure the Op and Kernel are registered in the binary running in this process.

使用tf_op_files.txt修复模型加载错误

我们已经在前面的章节中看到了这种臭名昭著的错误,除非您知道它的真正含义,否则弄清楚该修复程序可能要花很多时间。 TensorFlow 操作由两部分组成:位于 tensorflow/core/ops文件夹中的称为ops的定义(这有点令人困惑,因为操作既可以表示其定义,其实现,也可以表示其定义)。 和位于 tensorflow/core/kernels文件夹中的实现(称为内核)。 tensorflow/contrib/makefile文件夹中有一个名为tf_op_files.txt的文件,其中列出了在手动构建库时需要内置到 TensorFlow iOS 库中的操作的定义和实现。 tf_op_files.txt文件应该包含所有操作定义文件,如为 TensorFlow 移动部署准备模型,因为它们占用的空间很小。 但从 TensorFlow 1.4 或 1.5 开始,tf_op_files.txt文件中并未包含所有操作的操作定义。 因此,当我们看到“未注册操作类型”错误时,我们需要找出哪个操作定义和实现文件负责该操作。 在我们的情况下,操作类型名为DecodeWav。 我们可以运行以下两个 Shell 命令来获取信息:

$ grep 'REGISTER.*"DecodeWav"' tensorflow/core/ops/*.cc
tensorflow/core/ops/audio_ops.cc:REGISTER_OP("DecodeWav")

$ grep 'REGISTER.*"DecodeWav"' tensorflow/core/kernels/*.cc
tensorflow/core/kernels/decode_wav_op.cc:REGISTER_KERNEL_BUILDER(Name("DecodeWav").Device(DEVICE_CPU), DecodeWavOp);

在 TensorFlow 1.4 的 tf_op_files.txt文件中,已经有一行文本tensorflow/core/kernels/decode_wav_op.cc,但可以肯定的是tensorflow/core/ops/audio_ops.cc丢失了。 我们需要做的就是在tf_op_files.txt文件中的任意位置添加一行tensorflow/core/ops/audio_ops.cc,并像在第 3 章,“检测对象及其位置”中一样运行tensorflow/contrib/makefile/build_all_ios.sh,以重建 TensorFlow iOS 库。 然后再次运行 iOS 应用,并继续轻按启动按钮,然后说出语音命令以识别或误解,直到您无聊为止。

本章将重点介绍如何解决Not found: Op type not registered错误的过程,因为将来在其他 TensorFlow 模型上工作时,可以节省大量时间。

但是,在继续学习下一章中将介绍和使用另一种新的 TensorFlow AI 模型之前,让我们给其他喜欢使用更新的且至少对他们更凉快的 Swift 语言的 iOS 开发人员一些考虑。

通过 Swift 在 iOS 中使用简单的语音识别模型

我们在第 2 章中使用 TensorFlow 窗格创建了一个基于 Swift 的 iOS 应用。 现在让我们创建一个新的 Swift 应用,该应用使用我们在上一节中手动构建的 TensorFlow iOS 库,并在我们的 Swift 应用中使用语音命令模型:

  1. 通过 Xcode 创建一个新的“Single View iOS”项目,并按照与上一节中的步骤 1 和 2 相同的方式设置该项目,除了将语言设置为 Swift。
  2. 选择 Xcode “文件 | 新增 | 文件 ...”,然后选择 Objective-C 文件。 输入名称RunInference。 您将看到一个消息框,询问您“您是否要配置一个 Objective-C 桥接头?” 单击创建桥接标题。 将文件RunInference.m重命名为RunInfence.mm,因为我们将混合使用 C,C++ 和 Objective-C 代码来进行后期录音音频处理和识别。 我们仍在 Swift 应用中使用 Objective-C,因为要从 Swift 调用 TensorFlow C++ 代码,我们需要一个 Objective-C 类作为 C++ 代码的包装。
  3. 创建一个名为RunInference.h的头文件,并添加以下代码:
@interface RunInference_Wrapper : NSObject
- (NSString *)run_inference_wrapper:(NSString*)recorderFilePath;
@end

现在,您在 Xcode 中的应用应类似于图 5.5:

图 5.5:基于 Swift 的 iOS 应用项目

  1. 打开ViewController.swift。 在import UIKit之后的顶部添加以下代码:
import AVFoundation

let _lbl = UILabel()
let _btn = UIButton(type: .system)
var _recorderFilePath: String!

然后使ViewController看起来像这样(未显示为_btn_lbl定义NSLayoutConstraint并调用addConstraint的代码段):

class ViewController: UIViewController, AVAudioRecorderDelegate {
    var audioRecorder: AVAudioRecorder!
override func viewDidLoad() {
    super.viewDidLoad()

    _btn.translatesAutoresizingMaskIntoConstraints = false
    _btn.titleLabel?.font = UIFont.systemFont(ofSize:32)
    _btn.setTitle("Start", for: .normal)
    self.view.addSubview(_btn)

    _btn.addTarget(self, action:#selector(btnTapped), for: .touchUpInside)

    _lbl.translatesAutoresizingMaskIntoConstraints = false
    self.view.addSubview(_lbl)
  1. 添加一个按钮点击处理器,并在其内部,首先请求用户的录制许可:
@objc func btnTapped() {
    _lbl.text = "..."
    _btn.setTitle("Listening...", for: .normal)

    AVAudioSession.sharedInstance().requestRecordPermission () {
        [unowned self] allowed in
        if allowed {
            print("mic allowed")
        } else {
            print("denied by user")
            return
        }
    }

然后创建一个AudioSession实例,并将其类别设置为记录,并将状态设置为活动,就像我们在 Objective-C 版本中所做的一样:

let audioSession = AVAudioSession.sharedInstance()

do {
    try audioSession.setCategory(AVAudioSessionCategoryRecord)
    try audioSession.setActive(true)
} catch {
    print("recording exception")
    return
}

现在定义AVAudioRecorder要使用的设置:

let settings = [
    AVFormatIDKey: Int(kAudioFormatLinearPCM),
    AVSampleRateKey: 16000,
    AVNumberOfChannelsKey: 1,
    AVLinearPCMBitDepthKey: 16,
    AVLinearPCMIsBigEndianKey: false,
    AVLinearPCMIsFloatKey: false,
    AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
    ] as [String : Any]

设置文件路径以保存录制的音频,创建AVAudioRecorder实例,设置其委托并开始录制 1 秒钟:

do {
    _recorderFilePath = NSHomeDirectory().stringByAppendingPathComponent(path: "tmp").stringByAppendingPathComponent(path: "recorded_file.wav")
    audioRecorder = try AVAudioRecorder(url: NSURL.fileURL(withPath: _recorderFilePath), settings: settings)
    audioRecorder.delegate = self
    audioRecorder.record(forDuration: 1)
} catch let error {
    print("error:" + error.localizedDescription)
}
  1. ViewController.swift的末尾,添加具有以下实现的AVAudioRecorderDelegate方法audioRecorderDidFinishRecording,该实现主要调用run_inference_wrapper进行音频后处理和识别:
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
    _btn.setTitle("Recognizing...", for: .normal)
    if flag {
        let result = RunInference_Wrapper().run_inference_wrapper(_recorderFilePath)
        _lbl.text = result
    }
    else {
        _lbl.text = "Recording error"
    }
    _btn.setTitle("Start", for: .normal)
}

AudioRecognition_Swift-Bridging-Header.h文件中,添加#include "RunInference.h",以便前面的 Swift 代码RunInference_Wrapper().run_inference_wrapper(_recorderFilePath)起作用。

  1. run_inference_wrapper方法内的RunInference.mm中,从 Objective-C AudioRecognition应用中的ViewController.mm复制代码,如上一节的步骤 5-8 所述,该代码将保存的录制音频转换为格式 TensorFlow 模型接受模型,然后将其与采样率一起发送给模型以获取识别结果:
@implementation RunInference_Wrapper
- (NSString *)run_inference_wrapper:(NSString*)recorderFilePath {
...
}

如果您确实想将尽可能多的代码移植到 Swift,则可以用 Swift 替换 C 中的音频文件转换代码。 还有一些非官方的开源项目提供了官方 TensorFlow C++ API 的 Swift 包装器。 但是为了简单起见和达到适当的平衡,我们将保持 TensorFlow 模型的推论,在本示例中,还将保持音频文件的读取和转换,以及在 C++ 和 Objective-C 中与控制 UI 和录音,并启动调用来进行音频处理和识别。

这就是构建使用语音命令识别模型的 Swift iOS 应用所需的全部内容。 现在,您可以在 iOS 模拟器或实际设备上运行它,并看到与 Objective-C 版本完全相同的结果。

总结

在本章中,我们首先快速概述了语音识别以及如何使用端到端深度学习方法构建现代 ASR 系统。 然后,我们介绍了如何训练 TensorFlow 模型以识别简单的语音命令,并介绍了如何在 Android 应用以及基于 Objective-C 和 Swift 的 iOS 应用中使用该模型的分步教程。 我们还讨论了如何通过找出丢失的 TensorFlow 操作或内核文件,添加它并重建 TensorFlow iOS 库来修复 iOS 中常见的模型加载错误。

ASR 用于将语音转换为文本。 在下一章中,我们将探讨另一个将文本作为输出的模型,并且文本中将包含完整的自然语言句子,而不是本章中的简单命令。 我们将介绍如何构建模型以将图像,我们的老朋友转换为文本,以及如何在移动应用中使用该模型。 观察和描述您在自然语言中看到的内容需要真正的人类智慧。 福尔摩斯是完成这项任务的最佳人选之一。 我们当然还不如福尔摩斯,但是让我们看看如何开始。