口語理解(Spoken Language Understanding, SLU)做爲語音識別與天然語言處理之間的一個新興領域,其目的是爲了讓計算機從用戶的講話中理解他們的意圖。SLU是口語對話系統(Spoken Dialog Systems)的一個很是關鍵的環節。下圖展現了口語對話系統的主要流程。html
SLU主要經過以下三個子任務來理解用戶的語言:python
例如,用戶輸入「播放周杰倫的稻香」,首先經過領域識別模塊識別爲"music"領域,再經過用戶意圖檢測模塊識別出用戶意圖爲"play_music"(而不是"find_lyrics" ),最後經過槽填充對將每一個詞填充到對應的槽中:"播放[O] / 周杰倫[B-singer] / 的[O] / 稻香[B-song]"。git
從上述例子能夠看出,一般把領域識別和用戶意圖檢測當作文本分類問題,而把槽填充當作序列標註(Sequence Tagging)問題,也就是把連續序列中每一個詞賦予相應的語義類別標籤。本次實驗的任務就是基於ATIS 數據集進行語義槽填充。(完整代碼地址:https://github.com/llhthinker/slot-filling)github
本次實驗基於ATIS(Airline Travel Information Systems )數據集。顧名思義,ATIS數據集的領域爲"Airline Travel"。ATIS數據集採起流行的"in/out/begin(IOB)標註法": "I-xxx"表示該詞屬於槽xxx,但不是槽xxx中第一個詞;"O"表示該詞不屬於任何語義槽;"B-xxx"表示該詞屬於槽xxx,而且位於槽xxx的首位。部分ATIS訓練數據集以下:算法
what O is O the O arrival B-flight_time time I-flight_time in O san B-fromloc.city_name francisco I-fromloc.city_name for O the O DIGITDIGITDIGIT B-depart_time.time am I-depart_time.time flight O leaving O washington B-fromloc.city_name
ATIS數據集一共有83種語義槽,所以序列標註的標籤類別一共有\(83+83+1=167\)個。ATIS數據集分爲訓練集和測試集,數據規模以下表:框架
訓練集 | 測試集 | |
---|---|---|
句子總數 | 4978個 | 893個 |
詞語總數 | 56590個 | 9198個 |
句子平均詞數 | 11.4個 | 10.3個 |
上文中提到,一般把槽填充當作序列標註問題。不少機器學習算法都可以解決序列標註問題,包括HMM/CFG,hidden vector state(HVS)等生成式模型,以及CRF, SVM等判別式模型。本次實驗主要參考論文《Using Recurrent Neural Networks for Slot Filling in Spoken Language Understanding 》 ,基於RNN來實現語義槽填充。機器學習
RNN能夠分爲簡單RNN(Simple RNN)和門控機制RNN(Gated RNN),前者的RNN單元徹底接收上個時刻的輸入;後者基於門控機制,經過學習到的參數自行決定上個時刻的輸入量和當前狀態的保留量。下面將介紹Elman-RNN, Jordan-RNN, Hybrid-RNN(Elman和Jordan結合)這三種簡單RNN,以及經典的門控機制RNN:LSTM。函數
Elman-RNN將當前時刻的輸入\(x_t\)和上個時刻的隱狀態輸出\(h_{(t-1)}\)做爲輸入,具體以下:學習
\[\begin{split}\begin{array}{ll}h_t = \sigma(W_{ih} x_t + b_{ih} + W_{hh} h_{(t-1)} + b_{hh}) \end{array}\end{split}\]測試
須要說明的是,Pytorch
默認的RNN即爲Elman-RNN,可是它只支持\(\tanh\)和ReLU兩種激活函數。本次實驗按照論文設置,激活函數均採起sigmoid函數,使用Pytorch
具體實現以下:
class ElmanRNNCell(nn.Module): def __init__(self, input_size, hidden_size): super(ElmanRNNCell, self).__init__() self.hidden_size = hidden_size self.i2h_fc1 = nn.Linear(input_size, hidden_size) self.i2h_fc2 = nn.Linear(hidden_size, hidden_size) self.h2o_fc = nn.Linear(hidden_size, hidden_size) def forward(self, input, hidden): hidden = F.sigmoid(self.i2h_fc1(input) + self.i2h_fc2(hidden)) output = F.sigmoid(self.h2o_fc(hidden)) return output, hidden
Jordan-RNN將當前時刻的輸入\(x_t\)和上個時刻的輸出層輸出\(y_{(t-1)}\)做爲輸入,具體以下:
\[\begin{split}\begin{array}{ll}h_t = \sigma(W_{ih} x_t + b_{ih} + W_{yh} y_{(t-1)} + b_{yh}) \end{array}\end{split}\]
使用Pytorch
具體實現以下,其中\(y_0\)初始化爲可訓練的參數:
class JordanRNNCell(nn.Module): def __init__(self, input_size, hidden_size): super(JordanRNNCell, self).__init__() self.hidden_size = hidden_size self.i2h_fc1 = nn.Linear(input_size, hidden_size) self.i2h_fc2 = nn.Linear(hidden_size, hidden_size) self.h2o_fc = nn.Linear(hidden_size, hidden_size) self.y_0 = nn.Parameter(nn.init.xavier_uniform(torch.Tensor(1, hidden_size)), requires_grad=True) def forward(self, input, hidden=None): if hidden is None: hidden = self.y_0 hidden = F.sigmoid(self.i2h_fc1(input) + self.i2h_fc2(hidden)) output = F.sigmoid(self.h2o_fc(hidden)) return output, output
Hybrid-RNN將當前時刻的輸入\(x_t\),上個時刻的隱狀態\(h_{(t-1)}\) 以及上個時刻輸出層輸出\(y_{(t-1)}\)做爲輸入,具體以下:
\[\begin{split}\begin{array}{ll}h_t = \sigma(W_{ih} x_t + b_{ih} + W_{hh} h_{(t-1)} + b_{hh} + W_{yh} y_{(t-1)} + b_{yh}) \end{array}\end{split}\] ,而且\(y_0\)初始化爲可訓練的參數。使用Pytorch
具體實現以下:
class HybridRNNCell(nn.Module): def __init__(self, input_size, hidden_size): super(HybridRNNCell, self).__init__() self.hidden_size = hidden_size self.i2h_fc1 = nn.Linear(input_size, hidden_size) self.i2h_fc2 = nn.Linear(hidden_size, hidden_size) self.i2h_fc3 = nn.Linear(hidden_size, hidden_size) self.h2o_fc = nn.Linear(hidden_size, hidden_size) self.y_0 = nn.Parameter(nn.init.xavier_uniform(torch.Tensor(1, hidden_size)), requires_grad=True) def forward(self, input, hidden, output=None): if output is None: output = self.y_0 hidden = F.sigmoid(self.i2h_fc1(input)+self.i2h_fc2(hidden)+self.i2h_fc3(output)) output = F.sigmoid(self.h2o_fc(hidden)) return output, hidden
LSTM引入了記憶單元\(c_t\)和3種控制門,包括輸入門(input gate)\(i_t\),遺忘門(forget gate)\(f_t\),輸出門(output gate)\(o_t\), 首先,輸入層接受當前時刻輸入\(x_t\)和上個時刻隱狀態輸出\(h_{(t-1)}\),經過\(\tanh\)激活函數獲得記憶單元的輸入\(g_t\); 而後遺忘門\(f_t\)決定上個時刻記憶單元\(c_{(t-1)}\)的保留比例,輸入門\(i_t\)決定當前時刻記憶單元的輸入\(g_t\)的保留比例,二者相加獲得當前的記憶單元\(c_t\); 最後記憶單元\(c_t\)經過\(\tanh\)激活函數獲得的值在輸出門\(o_t\)的控制下獲得最終的當前時刻隱狀態\(h_t\), 具體以下:
\[\begin{split}\begin{array}{ll}i_t = \sigma(W_{ii} x_t + b_{ii} + W_{hi} h_{(t-1)} + b_{hi}) \\f_t = \sigma(W_{if} x_t + b_{if} + W_{hf} h_{(t-1)} + b_{hf}) \\g_t = \tanh(W_{ig} x_t + b_{ig} + W_{hg} h_{(t-1)} + b_{hg}) \\o_t = \sigma(W_{io} x_t + b_{io} + W_{ho} h_{(t-1)} + b_{ho}) \\c_t = f_t c_{(t-1)} + i_t g_t \\h_t = o_t \tanh(c_t)\end{array}\end{split}\]
Pytorch
已經實現了LSTM, 只須要調用相應的API便可,調用的代碼片斷以下:
self.rnn = nn.LSTM(input_size=embedding_dim, hidden_size=hidden_size, bidirectional=bidirectional, batch_first=True)
實驗基於Python 3.6
和Pytorch 0.4.0
,爲進行對照實驗,下列設置針對全部RNN模型:
在使用CPU的狀況下,不一樣模型在測試集的\(F_1\)得分以及平均一個epoch訓練時長的結果以下:
\(F_1(\%) / T(s)\) | Elman | Jordan | Hybrid | LSTM |
---|---|---|---|---|
Single | 87.26 / 438 | 87.90 / 487 | 88.46 / 494 | 92.16 / 3721 |
Bi-Directional | 92.88 / 565 | 90.31 / 580 | 91.85 / 613 | 93.75 / 4357 |
從上表中能夠看出:
在使用同一塊GPU的狀況下,不一樣模型在測試集的\(F_1\)得分以及平均一個epoch訓練時長的結果以下:
\(F_1(\%) / T(s)\) | Elman | Jordan | Hybrid | LSTM |
---|---|---|---|---|
Single | 88.89 / 35.2 | 88.36 / 41.3 | 89.65 / 43.5 | 92.44 / 16.8 |
Bi-Directional | 91.78 / 68.0 | 89.82 / 72.2 | 93.61 / 81.6 | 94.26 / 18.7 |
從上表中能夠看出,即便是隨機梯度降低(batch_size=1),GPU的加速效果仍然至關明顯。值得指出的是,雖然LSTM的運算步驟比其餘三種Simple-RNN多,可是用時倒是最少的,這多是因爲LSTM是直接調用Pytorch
的API,針對GPU有優化,而另外三種的都是本身實現的,GPU加速效果沒有Pytorch
好。
總的來講,將槽填充問題當作序列標註問題是一種有效的作法,而RNN可以較好的對序列進行建模,提取相關的上下文特徵。雙向RNN的表現優於單向RNN,而LSTM的表現優於Simple RNN。對於Simple RNN而言,Elman的表現不比Jordan差(甚至更好),而用時更少而且實現更簡單,這多是主流深度學習框架(TensorFlow
/ Pytorch
等)的simple RNN是基於Elman的緣由。而Hybrid做爲Elman和Jordan的混合體,其訓練時間都多餘Elman和Jordan,\(F_1\)得分略有提高,但不是特別明顯(使用CPU時的雙向Elman表現比雙向Hybrid好),須要更多實驗進行驗證。
從實驗設置能夠看出,本次實驗沒有過多的調參。若是想取得更好的結果,能夠進行更細緻的調參,包括 :
此外,能夠考慮在輸入時融入詞性標註和命名實體識別等信息,在輸出時使用Viterbi算法進行解碼,也能夠嘗試不一樣形式的門控RNN(如GRU,LSTM變體等)以及採用多層RNN,並考慮是否使用殘差鏈接等。
Mesnil G, Dauphin Y, Yao K, et al. Using recurrent neural networks for slot filling in spoken language understanding[J]. IEEE/ACM Transactions on Audio, Speech, and Language Processing, 2015, 23(3): 530-539.
Wikipedia. Recurrent neural network. https://en.wikipedia.org/wiki/Recurrent_neural_network
PyTorch documentation. Recurrent layers. http://pytorch.org/docs/stable/nn.html#recurrent-layers
Hung-yi Lee. Machine Learning (2017,Spring). http://speech.ee.ntu.edu.tw/~tlkagk/courses/ML_2017/Lecture/RNN.pdf
YUN-NUNG (VIVIAN) CHEN. Spring 105 - Intelligent Conversational Bot. https://www.csie.ntu.edu.tw/~yvchen/s105-icb/doc/170321_LU.pdf