1. 概况
1.1 任务
口语理解 (Spoken Language Understanding, SLU) 作为语音识别与自然语言处理之间的一个新兴领域, 其目的是为了让计算机从用户的讲话中理解他们的意图. SLU 是口语对话系统 (Spoken Dialog Systems https://en.wikipedia.org/wiki/Spoken_dialog_systems ) 的一个非常关键的环节. 下图展示了口语对话系统的主要流程.
SLU 主要通过如下三个子任务来理解用户的语言:
领域识别(Domain Detection)
用户意图检测(User Intent Determination)
语义槽填充(Semantic Slot Filling)
例如, 用户输入 "播放周杰伦的稻香", 首先通过领域识别模块识别为 "music" 领域, 再通过用户意图检测模块识别出用户意图为 "play_music"(而不是 "find_lyrics" ), 最后通过槽填充对将每个词填充到对应的槽中:"播放[O] / 周杰伦[B-singer] / 的[O] / 稻香[B-song]".
从上述例子可以看出, 通常把领域识别和用户意图检测当做文本分类问题, 而把槽填充当做序列标注 (Sequence Tagging) 问题, 也就是把连续序列中每个词赋予相应的语义类别标签. 本次实验的任务就是基于 ATIS 数据集进行语义槽填充.(完整代码地址: https://github.com/llhthinker/slot-filling )
1.2 数据集
本次实验基于 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 个 |
2. 模型
上文中提到, 通常把槽填充当做序列标注问题. 很多机器学习算法都能够解决序列标注问题, 包括 HMM/CFG,hidden vector state(HVS)等生成式模型, 以及 CRF, SVM 等判别式模型. 本次实验主要参考论文Using Recurrent Neural Networks for Slot Filling in Spoken Language Understanding https://ieeexplore.ieee.org/abstract/document/6998838/ , 基于 RNN 来实现语义槽填充.
RNN 可以分为简单 RNN(Simple RNN)和门控机制 RNN(Gated RNN), 前者的 RNN 单元完全接收上个时刻的输入; 后者基于门控机制, 通过学习到的参数自行决定上个时刻的输入量和当前状态的保留量. 下面将介绍 Elman-RNN, Jordan-RNN, Hybrid-RNN(Elman 和 Jordan 结合)这三种简单 RNN, 以及经典的门控机制 RNN:LSTM.
2.1 Elman-RNN
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 http://pytorch.org/docs/stable/nn.html#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
- 2.2 Jordan-RNN
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
- 2.4 Hybrid-RNN
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
- 2.5 LSTM
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 http://pytorch.org/docs/stable/nn.html#lstm , 只需要调用相应的 API 即可, 调用的代码片段如下:
- self.rnn = nn.LSTM(input_size=embedding_dim,
- hidden_size=hidden_size,
- bidirectional=bidirectional,
- batch_first=True)
3. 实验
3.1 实验设置
实验基于 Python 3.6 和 Pytorch 0.4.0 http://pytorch.org/ , 为进行对照实验, 下列设置针对所有 RNN 模型:
所有 RNN 模型均只使用单层;
词向量维度设置为 100 维, 并且随机初始化, 在训练过程中进行调整;
隐状态维度设置为 75 维;
采用带动量的随机梯度下降 (SGD),batch size 为 1, 学习率(learning rate) 为 0.1, 动量 (momentum) 为 0.9 并保持不变;
epoch=10;
每种 RNN 模型都实现单向 (Single) 和双向(Bi-Directional), 并分别训练.
3.2 实验结果
在使用 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 |
从上表中可以看出:
基于门控机制的 LSTM 由于其参数和运算步骤的增加, 一个 epoch 的训练时长是另外三种 Simple RNN 的 9 倍左右, 而 \(F_1\)得分也比 Simple RNN 高;
双向 (Bi-Directional)RNN 的 \(F_1\) 得分普遍比单向(Single)RNN 高, 而运行时间也多一些.
在使用同一块 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 好.
4. 总结与展望
总的来说, 将槽填充问题当做序列标注问题是一种有效的做法, 而 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 好), 需要更多实验进行验证.
从实验设置可以看出, 本次实验没有过多的调参. 如果想取得更好的结果, 可以进行更细致的调参, 包括 :
改变词向量维度和隐状态维度;
考虑采用预训练词向量, 然后固定或者进行微调;
采用正则化技术, 包括 L1/L2, Dropout, Batch Normalization, Layer Normalization 等;
尝试使用不同的优化器(如 Adam), 使用 mini-batch, 调整学习率;
增加 epoch 次数.
此外, 可以考虑在输入时融入词性标注和命名实体识别等信息, 在输出时使用 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
来源: https://www.cnblogs.com/llhthinker/p/8978029.html