1. 程式人生 > >[譯] TensorFlow 中的 RNN 串流

[譯] TensorFlow 中的 RNN 串流

謀智(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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄