LSTM實現語音識別

序言:語音識別作爲人工智能領域重要研究方向,近幾年發展迅猛,其中RNN的貢獻尤爲突出。RNN設計的目的就是讓神經網絡可以處理序列化的數據。本文筆者將陪同小夥伴們一塊兒踏上語音識別之夢幻旅途,相信此處風景獨好。

內容目錄

 

環境準備

RNN與LSTM介紹RNNLSTM語音識別介紹聲學特徵提取聲學特徵轉換成音素(聲學模型)音素轉文本(語言模型+解碼)語音識別簡單實現提取WAV文件中特徵將WAV文件對應的文本文件轉換成音素分類定義雙向LSTM

模型訓練和測試

 

環境準備

1、win10
2、python3.6.4
3、pip3
4、tensorflow1.12.0
(在運行代碼的時候如果顯示缺少python模塊,直接用pip3安裝即可)

RNN與LSTM介紹

循環神經網絡(RNN)是神經網絡模型中的一種,其中部分神經元的連接組成了有向環,有向環使得RNN中出現了內部狀態或帶記憶的結構,賦予了RNN對動態序列進行建模的能力。在接下來的兩小節中筆者將詳細的介紹一下RNN,以及RNN的變種長短期記憶(Long Short Term Memory,LSTM)網絡。

RNN

圖1中描繪了一個簡單循環神經網絡,叫做Elman網絡,一共包含三層:輸入層(input)、隱藏層(hidden)以及輸出層(output)。context unit用來存儲上一次的隱藏層的值,與下一次的輸入一起輸入到隱藏層(圖中實線表示直接複製,虛線表示需要通過學習獲得)。Elman網絡和喬丹網絡是循環神經網絡中最簡單的形態,本文只介紹Elman網絡,感興趣的讀者可自行查閱喬丹網絡。

圖1 Elman網絡

Elman網絡的數學表達式如下:
假設:

  • x(t):在t時間點的輸入向量;

  • h(t):在t時間點的隱藏向量;

  • y(t):在t時間點的輸出向量;

  • W、U和b:參數矩陣;

  • sigma(h)和sigma(y):激活函數。

隱藏層向量和輸出層向量可以表示爲:


LSTM

長短期記憶(Long Short Term Memory,LSTM)是RNN的一種,最早由Hochreiter和Schmidhuber(1977)年提出,該模型克服了一下RNN的不足,通過刻意的設計來避免長期依賴的問題。現在很多大公司的翻譯和語音識別技術核心都以LSTM爲主。下邊就詳細的介紹一下LSTM的構成。圖2描繪了LSTM單元的結構。

圖2  LSTM單元結構

爲了避免RNN中梯度消失和梯度爆炸的問題,LSTM相對於普通RNN單元有比較大的區別,主要的核心思想是:
①採用叫“細胞狀態”(state)的通道貫穿整個時間序列,如圖3中從C(t-1)到C(t),這條線上只有乘法操作和加法操作。
②通過設計“門”的結構來去除或者增加信息到細胞狀態,LSML中有三個門,分別是“忘記門”、“輸入門”和“輸出門”。

圖3 “細胞狀態”通道示意圖

下邊詳細闡述一下LSML中的三個門。
(1)忘記門
       圖4中紅色加粗部分爲LSML單元中“忘記門”的位置,“忘記門”決定之前狀態中的信息有多少應該捨棄。它會輸出一個0和1之間的數,代表C(t-1)中保留的部分。“忘記門”的計算公式如下:

“忘記門”的輸入是x(t),上一時刻的隱藏層輸出h(t-1)、W(f)和U(f)是“忘記門的參數”,需要通過訓練獲取。

圖4 “忘記門”

(2)輸入門

圖5中紅色加粗部分爲“輸入門”,輸入門決定什麼樣的輸入信息應該保留在“細胞狀態”C(t)中。它會讀取h(t-1)和x(t)的內容,其計算公式爲:


其中輸入是h(t-1)和x(t),W(i)、U(i)、W(c)、U(c)是要訓練的參數。

圖5 “輸入門”

接下來,研究一下“細胞狀態”的值是如何更新的。首先經過忘記門,算出舊的細胞狀態中有多少被遺棄,接着輸入門將所得的結果加入到細胞狀態,表示新的輸入信息中有多少加入到細胞狀態中。計算公式如下:

細胞狀態的更新過程如圖6所示:

圖6 “細胞狀態”更新

(3) 輸出門

       在細胞狀態更新之後,將會基於細胞狀態計算輸出。首先輸入數據h(t-1)和x(t),通過sigmoid激活函數得到“輸出門”的值。然後,把細胞狀態經過tanh處理,並與輸出門的值相乘得到細胞的輸出結果。輸出門的公式如下:


       輸出門的計算流程如圖7紅色加粗部分所示:

圖7 “輸出門”

語音識別介紹

語音識別的最主要過程是:(1)從聲音波形中提取聲學特徵;(2)將聲學特徵轉換成發音的因素;(3)使用語言模型等解碼技術轉變成我們能讀懂的文本。語音識別系統的典型結構如圖8所示:

圖8 語音識別結構

聲學特徵提取

聲音實際上一種波,原始的音頻文件叫WAV文件,WAV文件中存儲的除了一個文件頭以外,就是聲音波形的一個個點。如圖9所示:

圖9 聲音波形示意圖

要對聲音進行分析,首先對聲音進行分幀,把聲音切分成很多小的片段,幀與幀之間有一定的交疊,如圖10,每一幀長度是25ms,幀移是10ms,兩幀之間有25-10=15ms的交疊。

圖10 幀切割圖

分幀後,音頻數據就變成了很多小的片段,然後針對小片段進行特徵提取,常見的提取特徵的方法有:線性預測編碼(Linear Predictive Coding,LPC),梅爾頻率倒譜系數(Mel-frequency Cepstrum),把一幀波形變成一個多維向量的過程就是聲學特徵提取。

聲學特徵轉換成音素(聲學模型)

音素是人發音的基本單位。對於英文,常用的音素是一套39個音素組成的集合。對於漢語,基本就是漢語拼音的生母和韻母組成的音素集合。本文例子中LSTM+CTC神經網絡就是聲學特徵轉換成音素這個階段,該階段的模型被稱爲聲學模型。

音素轉文本(語言模型+解碼)

得到聲音的音素序列後,就可以使用語言模型等解碼技術將音素序列轉換成我們可以讀懂的文本。解碼過程對給定的音素序列和若干假設詞序列計算聲學模型和語言模型分數,將總體輸出分數最高的序列作爲識別的結果(這部分是比較複雜的,感興趣的讀者可以查閱相關資料)。

語音識別簡單實現

本文通過一個簡單的例子演示如何用tensorflow的LSTM+CTC完成一個端到端的語音識別,爲了簡化操作,本例子中的語音識別只訓練一句話,這句話中的音素分類也簡化成對應的字母(與真實因素的訓練過程原理一致)。計算過程如下圖所示:

提取WAV文件中特徵

首先讀者肯定會有疑問?什麼是WAV文件?筆者在此簡單的介紹一下,WAV格式是微軟公司開發的一種聲音文件格式,也叫波形聲音文件,是最早的數字音頻格式,被Windows平臺及其應用程序廣泛支持,是一種無損的音頻數據存放格式。

本文在讀取WAV的特徵數據後,採用python_speech_features包中的方法來讀取文件的MFCC特徵,詳細代碼如下:

def get_audio_feature():
  '''
  獲取wav文件提取mfcc特徵之後的數據
  '''
  audio_filename = "audio.wav"
  #讀取wav文件內容,fs爲採樣率, audio爲數據
  fs, audio = wav.read(audio_filename)

  #提取mfcc特徵
  inputs = mfcc(audio, samplerate=fs)
  # 對特徵數據進行歸一化,減去均值除以方差
  feature_inputs = np.asarray(inputs[np.newaxis, :])
  feature_inputs = (feature_inputs - np.mean(feature_inputs))/np.std(feature_inputs)  

  #特徵數據的序列長度
  feature_seq_len = [feature_inputs.shape[1]]
  return feature_inputs, feature_seq_len

函數的返回值feature_seq_len表示這段語音被分割成了多少幀,一幀數據計算出一個13維長度的特徵值。返回值feature_inputs是一個二維矩陣,矩陣行數是feature_seq_len長度,列數是13。

將WAV文件對應的文本文件轉換成音素分類

本文音素的數量是28,分別對應26個英文字母、空白符和沒有分到類情況。WAV文件對應的文本文件的內容是she had your dark suit in greasy wash water all year。現在把這句話轉換成整數表示的序列,空白用0表示,a-z分別用數字1-26表示,則轉換的結果爲:[19 8 5 0 8 1 4 0 25 15 21 18 0 4 1 18 110 19 21 9 20 0 9 14 0 7 18 5 1 19 25 0 231 19 8 0 23 1 20 5 18 0 1 12 12 0 25 5 118],最後將整個序列轉換成稀疏三元組結構,這樣就可以直接用在tensorflow的tf.sparse_placeholder上。轉換代碼如下:

with open(target_filename, 'r') as f:
    #原始文本爲“she had your dark suit in greasy wash water all year”
    line = f.readlines()[0].strip()
    targets = line.replace(' ', '  ')
    targets = targets.split(' ')
    targets = np.hstack([SPACE_TOKEN if x == '' else list(x) for x in targets])
    targets = np.asarray([SPACE_INDEX if x == SPACE_TOKEN else ord(x) - FIRST_INDEX
                      for x in targets])
    # 將列表轉換成稀疏三元組
    train_targets = sparse_tuple_from([targets])
    print(train_targets)
  return train_targets

定義雙向LSTM

定義雙向LSTM及LSTM之後的特徵映射的代碼如下:

 def inference(inputs, seq_len):
  #定義一個向前計算的LSTM單元,40個隱藏單元
  cell_fw = tf.contrib.rnn.LSTMCell(num_hidden, 
                        initializer=tf.random_normal_initializer(
                                        mean=0.0, stddev=0.1),
                        state_is_tuple=True)

  # 組成一個有2個cell的list
  cells_fw = [cell_fw] * num_layers
  # 定義一個向後計算的LSTM單元,40個隱藏單元
  cell_bw = tf.contrib.rnn.LSTMCell(num_hidden, 
                        initializer=tf.random_normal_initializer(
                                        mean=0.0, stddev=0.1),
                        state_is_tuple=True)
  # 組成一個有2個cell的list
  cells_bw = [cell_bw] * num_layers
  outputs, _, _ = tf.contrib.rnn.stack_bidirectional_dynamic_rnn(cells_fw,
                                                                 cells_bw,
                                                                 inputs,
                                                               dtype=tf.float32,
                                                        sequence_length=seq_len)

  shape = tf.shape(inputs)
  batch_s, max_timesteps = shape[0], shape[1]
  outputs = tf.reshape(outputs, [-1, num_hidden])

  W = tf.Variable(tf.truncated_normal([num_hidden,
                                         num_classes],
                                        stddev=0.1))

  b = tf.Variable(tf.constant(0., shape=[num_classes]))
  # 進行全連接線性計算
  logits = tf.matmul(outputs, W) + b
  # 將全連接計算的結果,由寬度40變成寬度80,
  # 即最後的輸入給CTC的數據寬度必須是26+2的寬度
  logits = tf.reshape(logits, [batch_s, -1, num_classes])
  # 轉置,將第一維和第二維交換。
  # 變成序列的長度放第一維,batch_size放第二維。
  # 也是爲了適應Tensorflow的CTC的輸入格式
  logits = tf.transpose(logits, (1, 0, 2))
  return logits

模型訓練和測試

最後將讀取數據、構建LSTM+CTC網絡及訓練過程結合起來,在完成500次迭代訓練後,進行測試,並將結果輸出,部分代碼如下(完整代碼,請讀者關注本公衆號“大數據技術宅”,輸入“語音識別demo”獲取):

 def main():
  # 輸入特徵數據,形狀爲:[batch_size, 序列長度,一幀特徵數]
  inputs = tf.placeholder(tf.float32, [None, None, num_features])

  # 輸入數據的label,定義成稀疏sparse_placeholder會生成稀疏的tensor:SparseTensor
  # 這個結構可以直接輸入給ctc求loss
  targets = tf.sparse_placeholder(tf.int32)

  # 序列的長度,大小是[batch_size]大小
  # 表示的是batch中每個樣本的有效序列長度是多少
  seq_len = tf.placeholder(tf.int32, [None])

  # 向前計算網絡,定義網絡結構,輸入是特徵數據,輸出提供給ctc計算損失值。
  logits = inference(inputs, seq_len)

  # ctc計算損失
  # 參數targets必須是一個值爲int32的稀疏tensor的結構:tf.SparseTensor
  # 參數logits是前面lstm網絡的輸出
  # 參數seq_len是這個batch的樣本中,每個樣本的序列長度。
  loss = tf.nn.ctc_loss(targets, logits, seq_len)
  # 計算損失的平均值
  cost = tf.reduce_mean(loss)

訓練過程及結果如下圖:

從上圖訓練結果可以清洗的看出經過500次的迭代訓練,語音文件基本已經可以完全識別,本例只演示了一個簡單的LSTM+CTC的端到端的訓練,實際的語音識別系統還需要大量訓練樣本以及將音素轉換成文本的解碼過程。後續文章中,筆者會繼續深入語音識別。
       最後,在2020開年之際,筆者祝各位愛學習的小哥哥,小姐姐,發大財。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章