- 原文地址:Streaming RNNs in TensorFlow
- 原文作者:Reuben Morais
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:sisibeloved
- 校對者:lsvih
謀智(Mozilla)研究所的機器學習團隊正在開發一個自動語音識別引擎,它將作為深度語音(DeepSpeech)專案的一部分,致力於向開發人員開放語音識別技術和預訓練模型。我們正在努力提高我們開源的語音轉文字引擎的效能和易用性。即將釋出的 0.2 版本將包括一個大家期待已久的特性:在錄製音訊時實時進行語音識別的能力。這篇部落格文章描述了我們是怎樣修改 STT(即 speech-to-text,語音轉文字)引擎的架構,來達到實現實時轉錄的效能要求。不久之後,等到正式版本釋出,你就可以體驗這一音訊轉換的功能。
當將神經網路應用到諸如音訊或文字的順序資料時,捕獲資料隨著時間推移而出現的模式是很重要的。迴圈神經網路(RNN)是具有『記憶』的神經網路 —— 它們不僅將資料中的下一個元素作為輸入,而且還將隨時間演進的狀態作為輸入,並使用這個狀態來捕獲與時間相關的模式。有時,你可能希望捕獲依賴未來資料的模式。解決這個問題的方法之一是使用兩個 RNN,一個在時序上向前,而另一個按向後的時序(即從資料中的最後一個元素開始,到第一個元素)。你可以在 Chris Olah 的這篇文章中瞭解更多關於 RNN(以及關於 DeepSpeech 中使用的特定型別的 RNN)的知識。
使用雙向 RNN
DeepSpeech 的當前版本(之前在 Hacks 上討論過)使用了用 TensorFlow 實現的雙向 RNN,這意味著它需要在開始工作之前具有整個可用的輸入。一種改善這種情況的方法是通過實現流式模型:在資料到達時以塊為單位進行工作,這樣當輸入結束時,模型已經在處理它,並且可以更快地給出結果。你也可以嘗試在輸入中途檢視部分結果。
這個動畫展示了資料如何在網路間流動。資料通過三個全連線層,從音訊輸入轉變成特徵計算。然後通過了一個雙向 RNN 層,最後通過對單個時間步長進行預測的全連線層。
為了做到這一點,你需要有一個可以分塊處理資料的模型。這是當前模型的圖表,顯示資料如何流過它。
可以看到,在雙向 RNN 中,倒數第二步的計算需要最後一步的資料,倒數第三步的計算需要倒數第二步的資料……如此迴圈往復。這些是圖中從右到左的紅色箭頭。
通過在資料被饋入時進行到第三層的計算,我們可以實現部分流式處理。這種方法的問題是它在延遲方面不會給我們帶來太多好處:第四層和第五層佔用了整個模型幾乎一半的計算成本。
使用單向 RNN 處理串流
因此,我們可以用單向層替換雙向層,單向層不依賴於將來的時間步。只要我們有足夠的音訊輸入,就能一直計算到最後一層。
使用單向模型,你可以分段地提供輸入,而不是在同一時間輸入整個輸入並獲得整個輸出。也就是說,你可以一次輸入 100ms 的音訊,立即獲得這段時間的輸出,並儲存最終狀態,這樣可以將其用作下一個 100ms 的音訊的初始狀態。
一種使用單向 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_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')
複製程式碼
通過以上對模型的更改,我們可以在客戶端採取以下步驟:
- 執行
initialize_state
節點。 - 積累音訊樣本,直到資料足以供給模型(我們使用的是 16 個時間步長,或 320ms)
- 將資料供給模型,在某個地方積累輸出。
- 重複第二步和第三步直到資料結束。
把幾百行的客戶端程式碼扔給讀者是沒有意義的,但是如果你感興趣的話,可以查閱 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 是謀智研究所機器學習小組的一名工程師。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。