TensorFlow 中的流式 RNN

Mozilla Research 的机器学习团队继续致力于开发自动语音识别引擎,作为 DeepSpeech 项目 的一部分,该项目旨在使语音技术和训练模型对开发者开放。我们一直在努力提高开源语音识别引擎的性能和易用性。即将发布的 0.2 版本将包含一个备受期待的功能:在录制音频时进行实时语音识别。这篇文章介绍了我们如何改变 STT 引擎的架构来实现这一点,从而实现实时转录性能。很快,您将能够以至少与音频输入相同的速度转录音频。

当将神经网络应用于音频或文本等时序数据时,捕捉随着时间推移而出现的模式非常重要。循环神经网络 (RNN) 是一种“记住”的神经网络——它们不仅将数据中的下一个元素作为输入,还将随时间推移而演变的状态作为输入,并使用此状态来捕捉时间相关的模式。有时,您可能还想捕捉依赖于未来数据的模式。解决此问题的其中一种方法是使用两个 RNN,一个向前推进时间,另一个向后推进时间,从数据的最后一个元素开始,一直到第一个元素。您可以在 Chris Olah 的这篇文章 中了解更多有关 RNN(以及 DeepSpeech 中使用的特定 RNN 类型)的信息。

使用双向 RNN

DeepSpeech 的当前版本 (以前在 Hacks 上介绍过) 使用了用 TensorFlow 实现的双向 RNN,这意味着它需要先获得整个输入才能开始执行任何有用的工作。改进这种情况的一种方法是实现流式模型:在数据到达时分块执行工作,这样当输入结束时,模型已经开始处理它,并且可以更快地给出结果。您也可以尝试查看输入过程中途的局部结果。

This animation shows how the data flows through the network. Data flows from the audio input to feature computation, through three fully connected layers. Then it goes through a bidirectional RNN layer, and finally through a final fully connected layer, where a prediction is made for a single time step.

此动画展示了数据如何在网络中流动。数据从音频输入流向特征计算,穿过三个全连接层。然后它穿过双向 RNN 层,最后穿过最终的全连接层,在此对单个时间步进行预测。

要做到这一点,您需要有一个模型,让您能够分块执行工作。以下是当前模型的图表,展示了数据在其中的流动方式。

如您所见,在双向 RNN 层中,需要最后一个步骤的数据才能计算倒数第二个步骤,而倒数第二个步骤又是计算倒数第三个步骤所必需的,依此类推。这些是图中从右到左的红色箭头。

我们可以在此模型中实现部分流式传输,方法是在数据输入时一直计算到第三层。此方法的问题在于,它在延迟方面不会给我们带来太多好处:第四层和第五层占模型计算成本的近一半。

使用单向 RNN 进行流式传输

相反,我们可以用单向层替换双向层,单向层不依赖于未来时间步。这样一来,我们就可以在获得足够的音频输入后,一直计算到最后一层。

使用单向模型,您可以分段输入,而不是一次性输入整个输入并获取整个输出。这意味着,您可以一次输入 100 毫秒的音频,立即获取这些输出,并保存最终状态,以便将其用作下一段 100 毫秒音频的初始状态。

An alternative architecture that uses a unidirectional RNN in which each time step only depends on the input at that time and the state from the previous step.

使用单向 RNN 的替代架构,其中每个时间步只依赖于该时间步的输入和前一步的状态。

以下是创建推理图的代码,该图可以跟踪每个输入窗口之间的状态

import tensorflow as tf

def create_inference_graph(batch_size=1, n_steps=16, n_features=26, width=64):
    input_ph = tf.placeholder(dtype=tf.float32,
                              shape=[batch_size, n_steps, n_features],
                              name='input')
    sequence_lengths = tf.placeholder(dtype=tf.int32,
                                      shape=[batch_size],
                                      name='input_lengths')
    previous_state_c = tf.get_variable(dtype=tf.float32,
                                       shape=[batch_size, width],
                                       name='previous_state_c')
    previous_state_h = tf.get_variable(dtype=tf.float32,
                                       shape=[batch_size, width],
                                       name='previous_state_h')
    previous_state = tf.contrib.rnn.LSTMStateTuple(previous_state_c, previous_state_h)

    # Transpose from batch major to time major
    input_ = tf.transpose(input_ph, [1, 0, 2])

    # Flatten time and batch dimensions for feed forward layers
    input_ = tf.reshape(input_, [batch_size*n_steps, n_features])

    # Three ReLU hidden layers
    layer1 = tf.contrib.layers.fully_connected(input_, width)
    layer2 = tf.contrib.layers.fully_connected(layer1, width)
    layer3 = tf.contrib.layers.fully_connected(layer2, width)

    # Unidirectional LSTM
    rnn_cell = tf.contrib.rnn.LSTMBlockFusedCell(width)
    rnn, new_state = rnn_cell(layer3, initial_state=previous_state)
    new_state_c, new_state_h = new_state

    # Final hidden layer
    layer5 = tf.contrib.layers.fully_connected(rnn, width)

    # Output layer
    output = tf.contrib.layers.fully_connected(layer5, ALPHABET_SIZE+1, activation_fn=None)

    # Automatically update previous state with new state
    state_update_ops = [
        tf.assign(previous_state_c, new_state_c),
        tf.assign(previous_state_h, new_state_h)
    ]
    with tf.control_dependencies(state_update_ops):
        logits = tf.identity(logits, name='logits')

    # Create state initialization operations
    zero_state = tf.zeros([batch_size, n_cell_dim], tf.float32)
    initialize_c = tf.assign(previous_state_c, zero_state)
    initialize_h = tf.assign(previous_state_h, zero_state)
    initialize_state = tf.group(initialize_c, initialize_h, name='initialize_state')

    return {
        'inputs': {
            'input': input_ph,
            'input_lengths': sequence_lengths,
        },
        'outputs': {
            'output': logits,
            'initialize_state': initialize_state,
        }
    }

由上述代码创建的图有两个输入和两个输出。输入是序列及其长度。输出是 logits 和一个特殊的“initialize_state”节点,需要在新序列开始时运行。冻结图时,请确保不要冻结状态变量 previous_state_h 和 previous_state_c。

以下是冻结图的代码

from tensorflow.python.tools import freeze_graph

freeze_graph.freeze_graph_with_def_protos(
        input_graph_def=session.graph_def,
        input_saver_def=saver.as_saver_def(),
        input_checkpoint=checkpoint_path,
        output_node_names='logits,initialize_state',
        restore_op_name=None,
        filename_tensor_name=None,
        output_graph=output_graph_path,
        initializer_nodes='',
        variable_names_blacklist='previous_state_c,previous_state_h')

通过对模型进行这些更改,我们可以在客户端使用以下方法

  1. 运行“initialize_state”节点。
  2. 积累音频样本,直到有足够的样本可以输入到模型中(本例中为 16 个时间步,或 320 毫秒)。
  3. 通过模型馈送,将输出积累在某个地方。
  4. 重复步骤 2 和 3,直到数据结束。

在此向读者介绍数百行客户端代码毫无意义,但如果您感兴趣,它们都已获得 MPL 2.0 许可,并已在 GitHub 上提供。实际上,我们有两个不同的实现,一个是用 Python 编写的,我们用它来生成测试报告,另一个是用 C++ 编写的,它位于我们的官方客户端 API 后面。

性能改进

这对我们的 STT 引擎意味着什么?以下是与我们当前的稳定版本相比的一些数字

  • 模型大小从 468MB 减少到 180MB
  • 转录时间:笔记本电脑 CPU 上的 3 秒文件,从 9 秒减少到 1.5 秒
  • 峰值堆使用量从 4GB 减少到 20MB(模型现在是内存映射的)
  • 总堆分配量从 12GB 减少到 264MB

对我来说,最重要的是我们现在比实时速度更快,并且没有使用 GPU,这与流式推理一起,开辟了许多新的使用可能性,例如对广播节目、Twitch 直播和主题演讲进行实时字幕;家庭自动化;基于语音的 UI;等等。如果您想在下一个项目中集成语音识别,请考虑使用我们的引擎!

以下是一个小的 Python 程序,演示了如何使用 libSoX 从麦克风录制音频,并在录制音频时将其馈送到引擎中。

import argparse
import deepspeech as ds
import numpy as np
import shlex
import subprocess
import sys

parser = argparse.ArgumentParser(description='DeepSpeech speech-to-text from microphone')
parser.add_argument('--model', required=True,
                    help='Path to the model (protocol buffer binary file)')
parser.add_argument('--alphabet', required=True,
                    help='Path to the configuration file specifying the alphabet used by the network')
parser.add_argument('--lm', nargs='?',
                    help='Path to the language model binary file')
parser.add_argument('--trie', nargs='?',
                    help='Path to the language model trie file created with native_client/generate_trie')
args = parser.parse_args()

LM_WEIGHT = 1.50
VALID_WORD_COUNT_WEIGHT = 2.25
N_FEATURES = 26
N_CONTEXT = 9
BEAM_WIDTH = 512

print('Initializing model...')

model = ds.Model(args.model, N_FEATURES, N_CONTEXT, args.alphabet, BEAM_WIDTH)
if args.lm and args.trie:
    model.enableDecoderWithLM(args.alphabet,
                              args.lm,
                              args.trie,
                              LM_WEIGHT,
                              VALID_WORD_COUNT_WEIGHT)
sctx = model.setupStream()

subproc = subprocess.Popen(shlex.split('rec -q -V0 -e signed -L -c 1 -b 16 -r 16k -t raw - gain -2'),
                           stdout=subprocess.PIPE,
                           bufsize=0)
print('You can start speaking now. Press Control-C to stop recording.')

try:
    while True:
        data = subproc.stdout.read(512)
        model.feedAudioContent(sctx, np.frombuffer(data, np.int16))
except KeyboardInterrupt:
    print('Transcription:', model.finishStream(sctx))
    subproc.terminate()
    subproc.wait()

最后,如果您想为 DeepSpeech 项目本身做出贡献,我们有很多机会。代码库是用 Python 和 C++ 编写的,我们很乐意添加 iOS 和 Windows 支持,例如。您可以通过我们的 IRC 频道 或我们的 Discourse 论坛 与我们联系。

关于 Reuben Morais

Reuben Morais 是 Mozilla 机器学习团队的高级研究工程师。他目前专注于缩短机器学习研究与现实世界应用之间的差距,为用户带来隐私保护语音技术。

Reuben Morais 的更多文章…


5 条评论

  1. Adam

    尊敬的 Mozilla,我使用您的语音转文本软件编写了这篇文章,我想说它太棒了,非常感谢!

    2018 年 9 月 22 日 07:36

    1. Reuben Morais

      感谢您的评论,Adam,很高兴听到 DeepSpeech 对您有效!:)

      2018 年 9 月 24 日 05:53

  2. danny

    抱歉,我是 DeepSpeech 的新手。您能更详细地解释一下如何实现这个训练模型吗?我需要更改 Deepspeech 中的“create_inference_graph”函数吗?我该怎么做?请帮助我澄清一下,我该如何在客户端执行以下步骤
    – 运行“initialize_state”节点。
    – 积累音频样本,直到有足够的样本可以输入到模型中(本例中为 16 个时间步,或 320 毫秒)。
    – 通过模型馈送,将输出积累在某个地方。
    – 重复步骤 2 和 3,直到数据结束。

    2018 年 10 月 11 日 01:50

    1. Reuben Morais

      所有这些都已由我们的代码处理:http://github.com/mozilla/DeepSpeech/

      我们有训练代码以及多个客户端,因此您无需从头开始。这篇文章主要记录了我们的方法。

      2018 年 10 月 18 日 12:06

  3. Logan

    哇,太棒了。谢谢。

    2018 年 10 月 14 日 18:15

本文的评论已关闭。