口语理解(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