[譯] TensorFlow 中的 RNN 串流

sisibeloved發表於2018-10-20

謀智(Mozilla)研究所的機器學習團隊正在開發一個自動語音識別引擎,它將作為深度語音(DeepSpeech)專案的一部分,致力於向開發人員開放語音識別技術和預訓練模型。我們正在努力提高我們開源的語音轉文字引擎的效能和易用性。即將釋出的 0.2 版本將包括一個大家期待已久的特性:在錄製音訊時實時進行語音識別的能力。這篇部落格文章描述了我們是怎樣修改 STT(即 speech-to-text,語音轉文字)引擎的架構,來達到實現實時轉錄的效能要求。不久之後,等到正式版本釋出,你就可以體驗這一音訊轉換的功能。

當將神經網路應用到諸如音訊或文字的順序資料時,捕獲資料隨著時間推移而出現的模式是很重要的。迴圈神經網路(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 處理串流

因此,我們可以用單向層替換雙向層,單向層不依賴於將來的時間步。只要我們有足夠的音訊輸入,就能一直計算到最後一層。

使用單向模型,你可以分段地提供輸入,而不是在同一時間輸入整個輸入並獲得整個輸出。也就是說,你可以一次輸入 100ms 的音訊,立即獲得這段時間的輸出,並儲存最終狀態,這樣可以將其用作下一個 100ms 的音訊的初始狀態。

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)

    # 從以批次為主轉置成以時間為主
    input_ = tf.transpose(input_ph, [1, 0, 2])

    # 展開以契合前饋層的維度
    input_ = tf.reshape(input_, [batch_size*n_steps, n_features])

    # 三個隱含的 ReLU 層
    layer1 = tf.contrib.layers.fully_connected(input_, width)
    layer2 = tf.contrib.layers.fully_connected(layer1, width)
    layer3 = tf.contrib.layers.fully_connected(layer2, width)

    # 單向 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

    # 最終的隱含層
    layer5 = tf.contrib.layers.fully_connected(rnn, width)

    # 輸出層
    output = tf.contrib.layers.fully_connected(layer5, ALPHABET_SIZE+1, activation_fn=None)

    # 用新的狀態自動更新原先的狀態
    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')

    # 建立初始化狀態
    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,
        }
    }
複製程式碼

上述程式碼建立的圖有兩個輸入和兩個輸出。輸入是序列及其長度。輸出是 logit 和一個需要在一個新序列開始執行的特殊節點 initialize_state。當固化影象時,請確保不固化狀態變數 previous_state_hprevious_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 個時間步長,或 320ms)
  3. 將資料供給模型,在某個地方積累輸出。
  4. 重複第二步和第三步直到資料結束。

把幾百行的客戶端程式碼扔給讀者是沒有意義的,但是如果你感興趣的話,可以查閱 GitHub 中的程式碼,這些程式碼均遵循 MPL 2.0 協議。事實上,我們有兩種不同語言的實現,一個用 Python,用來生成測試報告;另一個用 C++,這是我們官方的客戶端 API。

效能提升

這些架構上的改動對我們的 STT 引擎能造成怎樣的影響?下面有一些與當前穩定版本相比較的數字:

  • 模型大小從 468MB 減小至 180MB
  • 轉錄時間:一個時長 3s 的檔案,執行在筆記本 CPU上,所需時間從 9s 降至 1.5s
  • 堆記憶體的峰值佔用量從 4GB 降至 20MB(模型現在是記憶體對映的)
  • 總的堆記憶體分配從 12GB 降至 264MB

我覺得最重要的一點,我們現在能在不使用 GPU 的情況下滿足實時的速率,這與流式推理一起,開闢了許多新的使用可能性,如無線電節目、Twitch 流和 keynote 演示的實況字幕;家庭自動化;基於語音的 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()
複製程式碼

最後,如果你想為深度語音專案做出貢獻,我們有很多機會。程式碼庫是用 Python 和 C++ 編寫的,並且我們將新增對 iOS 和 Windows 的支援。通過我們的 IRC 頻道或我們的 Discourse 論壇來聯絡我們。

關於 Reuben Morais

Reuben 是謀智研究所機器學習小組的一名工程師。

Reuben Morais 的更多文章…

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章