|
| 1 | +RNN 配置 |
| 2 | +======== |
| 3 | + |
| 4 | +本教程将指导你如何在 PaddlePaddle |
| 5 | +中配置循环神经网络(RNN)。PaddlePaddle |
| 6 | +高度支持灵活和高效的循环神经网络配置。 在本教程中,您将了解如何: |
| 7 | + |
| 8 | +- 准备用来学习循环神经网络的序列数据。 |
| 9 | +- 配置循环神经网络架构。 |
| 10 | +- 使用学习完成的循环神经网络模型生成序列。 |
| 11 | + |
| 12 | +我们将使用 vanilla 循环神经网络和 sequence to sequence |
| 13 | +模型来指导你完成这些步骤。sequence to sequence |
| 14 | +模型的代码可以在\ ``demo / seqToseq``\ 找到。 |
| 15 | + |
| 16 | +准备序列数据 |
| 17 | +------------ |
| 18 | + |
| 19 | +PaddlePaddle |
| 20 | +不需要对序列数据进行任何预处理,例如填充。唯一需要做的是将相应类型设置为输入。例如,以下代码段定义了三个输入。 |
| 21 | +它们都是序列,它们的大小是\ ``src_dict``\ ,\ ``trg_dict``\ 和\ ``trg_dict``\ : |
| 22 | + |
| 23 | +.. code:: sourcecode |
| 24 | +
|
| 25 | + settings.input_types = [ |
| 26 | + integer_value_sequence(len(settings.src_dict)), |
| 27 | + integer_value_sequence(len(settings.trg_dict)), |
| 28 | + integer_value_sequence(len(settings.trg_dict))] |
| 29 | +
|
| 30 | +在\ ``process``\ 函数中,每个\ ``yield``\ 函数将返回三个整数列表。每个整数列表被视为一个整数序列: |
| 31 | + |
| 32 | +.. code:: sourcecode |
| 33 | +
|
| 34 | + yield src_ids, trg_ids, trg_ids_next |
| 35 | +
|
| 36 | +有关如何编写数据提供程序的更多细节描述,请参考 |
| 37 | +`PyDataProvider2 <../../ui/data_provider/index.html>`__\ 。完整的数据提供文件在 |
| 38 | +``demo/seqToseq/dataprovider.py``\ 。 |
| 39 | + |
| 40 | +配置循环神经网络架构 |
| 41 | +-------------------- |
| 42 | + |
| 43 | +简单门控循环神经网络(Gated Recurrent Neural Network) |
| 44 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 45 | + |
| 46 | +循环神经网络在每个时间步骤顺序地处理序列。下面列出了 LSTM 的架构的示例。 |
| 47 | + |
| 48 | +.. figure:: ../../../tutorials/sentiment_analysis/bi_lstm.jpg |
| 49 | + :alt: image |
| 50 | + |
| 51 | + image |
| 52 | + |
| 53 | +一般来说,循环网络从 *t* = 1 到 *t* = *T* 或者反向地从 *t* = *T* 到 *t* |
| 54 | += 1 执行以下操作。 |
| 55 | + |
| 56 | +*x*\ \ *t* + 1 = *f*\ \ *x*\ (*x*\ \ *t*\ ),\ *y*\ \ *t*\ = *f*\ \ *y*\ (*x*\ \ *t*\ ) |
| 57 | + |
| 58 | +其中 *f*\ \ *x*\ (.) 称为\ **单步函数**\ (即单时间步执行的函数,step |
| 59 | +function),而 *f*\ \ *y*\ (.) 称为\ **输出函数**\ 。在 vanilla |
| 60 | +循环神经网络中,单步函数和输出函数都非常简单。然而,PaddlePaddle |
| 61 | +可以通过修改这两个函数来实现复杂的网络配置。我们将使用 sequence to |
| 62 | +sequence |
| 63 | +模型演示如何配置复杂的循环神经网络模型。在本节中,我们将使用简单的 |
| 64 | +vanilla |
| 65 | +循环神经网络作为使用\ ``recurrent_group``\ 配置简单循环神经网络的例子。 |
| 66 | +注意,如果你只需要使用简单的RNN,GRU或LSTM,那么推荐使用\ ``grumemory``\ 和\ ``lstmemory``\ ,因为它们的计算效率比\ ``recurrent_group``\ 更高。 |
| 67 | + |
| 68 | +对于 vanilla RNN,在每个时间步长,\ **单步函数**\ 为: |
| 69 | + |
| 70 | +*x*\ \ *t* + 1 = *W*\ \ *x*\ \ *x*\ \ *t*\ + *W*\ \ *i*\ \ *I*\ \ *t*\ + *b* |
| 71 | + |
| 72 | +其中 *x*\ \ *t*\ 是RNN状态,并且 *I*\ \ *t*\ 是输入,\ *W*\ \ *x*\ 和 |
| 73 | +*W*\ \ *i*\ 分别是RNN状态和输入的变换矩阵。\ *b* |
| 74 | +是偏差。它的\ **输出函数**\ 只需要\ *x*\ \ *t*\ 作为输出。 |
| 75 | + |
| 76 | +``recurrent_group``\ 是构建循环神经网络的最重要的工具。 |
| 77 | +它定义了\ **单步函数**\ ,\ **输出函数**\ 和循环神经网络的输入。注意,这个函数的\ ``step``\ 参数需要实现\ ``step function``\ (单步函数)和\ ``output function``\ (输出函数): |
| 78 | + |
| 79 | +.. code:: sourcecode |
| 80 | +
|
| 81 | + def simple_rnn(input, |
| 82 | + size=None, |
| 83 | + name=None, |
| 84 | + reverse=False, |
| 85 | + rnn_bias_attr=None, |
| 86 | + act=None, |
| 87 | + rnn_layer_attr=None): |
| 88 | + def __rnn_step__(ipt): |
| 89 | + out_mem = memory(name=name, size=size) |
| 90 | + rnn_out = mixed_layer(input = [full_matrix_projection(ipt), |
| 91 | + full_matrix_projection(out_mem)], |
| 92 | + name = name, |
| 93 | + bias_attr = rnn_bias_attr, |
| 94 | + act = act, |
| 95 | + layer_attr = rnn_layer_attr, |
| 96 | + size = size) |
| 97 | + return rnn_out |
| 98 | + return recurrent_group(name='%s_recurrent_group' % name, |
| 99 | + step=__rnn_step__, |
| 100 | + reverse=reverse, |
| 101 | + input=input) |
| 102 | +
|
| 103 | +PaddlePaddle |
| 104 | +使用“Memory”(记忆模块)实现单步函数。\ **Memory**\ 是在PaddlePaddle中构造循环神经网络时最重要的概念。 |
| 105 | +Memory是在单步函数中循环使用的状态,例如\ *x*\ \ *t* + 1 = *f*\ \ *x*\ (*x*\ \ *t*\ )。 |
| 106 | +一个Memory包含\ **输出**\ 和\ **输入**\ 。当前时间步处的Memory的输出作为下一时间步Memory的输入。Memory也可以具有\ **boot |
| 107 | +layer(引导层)**\ ,其输出被用作Memory的初始值。 |
| 108 | +在我们的例子中,门控循环单元的输出被用作输出Memory。请注意,\ ``rnn_out``\ 层的名称与\ ``out_mem``\ 的名称相同。这意味着\ ``rnn_out`` |
| 109 | +(*x*\ \ *t* + 1)的输出被用作\ ``out_mem``\ Memory的\ **输出**\ 。 |
| 110 | + |
| 111 | +Memory也可以是序列。在这种情况下,在每个时间步中,我们有一个序列作为循环神经网络的状态。这在构造非常复杂的循环神经网络时是有用的。 |
| 112 | +其他高级功能包括定义多个Memory,以及使用子序列来定义分级循环神经网络架构。 |
| 113 | + |
| 114 | +我们在函数的结尾返回\ ``rnn_out``\ 。 这意味着 ``rnn_out`` |
| 115 | +层的输出被用作门控循环神经网络的\ **输出**\ 函数。 |
| 116 | + |
| 117 | +Sequence to Sequence Model with Attention |
| 118 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 119 | + |
| 120 | +我们将使用 sequence to sequence model with attention |
| 121 | +作为例子演示如何配置复杂的循环神经网络模型。该模型的说明如下图所示。 |
| 122 | + |
| 123 | +.. figure:: ../../../tutorials/text_generation/encoder-decoder-attention-model.png |
| 124 | + :alt: image |
| 125 | + |
| 126 | + image |
| 127 | + |
| 128 | +在这个模型中,源序列 *S* = {*s*\ 1, …, \ *s*\ \ *T*\ } |
| 129 | +用双向门控循环神经网络编码。双向门控循环神经网络的隐藏状态 |
| 130 | +*H*\ \ *S*\ = {*H*\ 1, …, \ *H*\ \ *T*\ } 被称为 |
| 131 | +*编码向量*\ 。解码器是门控循环神经网络。当解读每一个\ *y*\ \ *t*\ 时, |
| 132 | +这个门控循环神经网络生成一系列权重 |
| 133 | +*W*\ \ *S*\ \ *t*\ = {*W*\ 1\ *t*\ , …, \ *W*\ \ *T*\ \ *t*\ }, |
| 134 | +用于计算编码向量的加权和。加权和用来生成\ *y*\ \ *t*\ 。 |
| 135 | + |
| 136 | +模型的编码器部分如下所示。它叫做\ ``grumemory``\ 来表示门控循环神经网络。如果网络架构简单,那么推荐使用循环神经网络的方法,因为它比 |
| 137 | +``recurrent_group`` |
| 138 | +更快。我们已经实现了大多数常用的循环神经网络架构,可以参考 |
| 139 | +`Layers <../../ui/api/trainer_config_helpers/layers_index.html>`__ |
| 140 | +了解更多细节。 |
| 141 | + |
| 142 | +我们还将编码向量投射到 ``decoder_size`` |
| 143 | +维空间。这通过获得反向循环网络的第一个实例,并将其投射到 |
| 144 | +``decoder_size`` 维空间完成: |
| 145 | + |
| 146 | +.. code:: sourcecode |
| 147 | +
|
| 148 | + # 定义源语句的数据层 |
| 149 | + src_word_id = data_layer(name='source_language_word', size=source_dict_dim) |
| 150 | + # 计算每个词的词向量 |
| 151 | + src_embedding = embedding_layer( |
| 152 | + input=src_word_id, |
| 153 | + size=word_vector_dim, |
| 154 | + param_attr=ParamAttr(name='_source_language_embedding')) |
| 155 | + # 应用前向循环神经网络 |
| 156 | + src_forward = grumemory(input=src_embedding, size=encoder_size) |
| 157 | + # 应用反向递归神经网络(reverse=True表示反向循环神经网络) |
| 158 | + src_backward = grumemory(input=src_embedding, |
| 159 | + size=encoder_size, |
| 160 | + reverse=True) |
| 161 | + # 将循环神经网络的前向和反向部分混合在一起 |
| 162 | + encoded_vector = concat_layer(input=[src_forward, src_backward]) |
| 163 | +
|
| 164 | + # 投射编码向量到 decoder_size |
| 165 | + encoder_proj = mixed_layer(input = [full_matrix_projection(encoded_vector)], |
| 166 | + size = decoder_size) |
| 167 | +
|
| 168 | + # 计算反向RNN的第一个实例 |
| 169 | + backward_first = first_seq(input=src_backward) |
| 170 | +
|
| 171 | + # 投射反向RNN的第一个实例到 decoder size |
| 172 | + decoder_boot = mixed_layer(input=[full_matrix_projection(backward_first)], size=decoder_size, act=TanhActivation()) |
| 173 | +
|
| 174 | +解码器使用 ``recurrent_group`` 来定义循环神经网络。单步函数和输出函数在 |
| 175 | +``gru_decoder_with_attention`` 中定义: |
| 176 | + |
| 177 | +.. code:: sourcecode |
| 178 | +
|
| 179 | + group_inputs=[StaticInput(input=encoded_vector,is_seq=True), |
| 180 | + StaticInput(input=encoded_proj,is_seq=True)] |
| 181 | + trg_embedding = embedding_layer( |
| 182 | + input=data_layer(name='target_language_word', |
| 183 | + size=target_dict_dim), |
| 184 | + size=word_vector_dim, |
| 185 | + param_attr=ParamAttr(name='_target_language_embedding')) |
| 186 | + group_inputs.append(trg_embedding) |
| 187 | +
|
| 188 | + # 对于配备有注意力机制的解码器,在训练中, |
| 189 | + # 目标向量(groudtruth)是数据输入, |
| 190 | + # 而源序列的编码向量可以被无边界的memory访问 |
| 191 | + # StaticInput 意味着不同时间步的输入都是相同的值, |
| 192 | + # 否则它以一个序列输入,不同时间步的输入是不同的。 |
| 193 | + # 所有输入序列应该有相同的长度。 |
| 194 | + decoder = recurrent_group(name=decoder_group_name, |
| 195 | + step=gru_decoder_with_attention, |
| 196 | + input=group_inputs) |
| 197 | +
|
| 198 | +单步函数的实现如下所示。首先,它定义解码网络的\ **Memory**\ 。然后定义 |
| 199 | +attention,门控循环单元单步函数和输出函数: |
| 200 | + |
| 201 | +.. code:: sourcecode |
| 202 | +
|
| 203 | + def gru_decoder_with_attention(enc_vec, enc_proj, current_word): |
| 204 | + # 定义解码器的Memory |
| 205 | + # Memory的输出定义在 gru_step 内 |
| 206 | + # 注意 gru_step 应该与它的Memory名字相同 |
| 207 | + decoder_mem = memory(name='gru_decoder', |
| 208 | + size=decoder_size, |
| 209 | + boot_layer=decoder_boot) |
| 210 | + # 计算 attention 加权编码向量 |
| 211 | + context = simple_attention(encoded_sequence=enc_vec, |
| 212 | + encoded_proj=enc_proj, |
| 213 | + decoder_state=decoder_mem) |
| 214 | + # 混合当前词向量和attention加权编码向量 |
| 215 | + decoder_inputs = mixed_layer(inputs = [full_matrix_projection(context), |
| 216 | + full_matrix_projection(current_word)], |
| 217 | + size = decoder_size * 3) |
| 218 | + # 定义门控循环单元循环神经网络单步函数 |
| 219 | + gru_step = gru_step_layer(name='gru_decoder', |
| 220 | + input=decoder_inputs, |
| 221 | + output_mem=decoder_mem, |
| 222 | + size=decoder_size) |
| 223 | + # 定义输出函数 |
| 224 | + out = mixed_layer(input=[full_matrix_projection(input=gru_step)], |
| 225 | + size=target_dict_dim, |
| 226 | + bias_attr=True, |
| 227 | + act=SoftmaxActivation()) |
| 228 | + return out |
| 229 | +
|
| 230 | +生成序列 |
| 231 | +-------- |
| 232 | + |
| 233 | +训练模型后,我们可以使用它来生成序列。通常的做法是使用\ **beam search** |
| 234 | +生成序列。以下代码片段定义 beam search 算法。注意,\ ``beam_search`` |
| 235 | +函数假设 ``step`` 的输出函数返回的是下一个时刻输出词的 softmax |
| 236 | +归一化概率向量。我们对模型进行了以下更改。 |
| 237 | + |
| 238 | +- 使用 ``GeneratedInput`` 来表示 trg\_embedding。 ``GeneratedInput`` |
| 239 | + 将上一时间步所生成的词的向量来作为当前时间步的输入。 |
| 240 | +- 使用 ``beam_search`` 函数。这个函数需要设置: |
| 241 | + |
| 242 | + - ``bos_id``: 开始标记。每个句子都以开始标记开头。 |
| 243 | + - ``eos_id``: 结束标记。每个句子都以结束标记结尾。 |
| 244 | + - ``beam_size``: beam search 算法中的beam大小。 |
| 245 | + - ``max_length``: 生成序列的最大长度。 |
| 246 | + |
| 247 | +- 使用 ``seqtext_printer_evaluator`` |
| 248 | + 根据索引矩阵和字典打印文本。这个函数需要设置: |
| 249 | + |
| 250 | + - ``id_input``: 数据的整数ID,用于标识生成的文件中的相应输出。 |
| 251 | + - ``dict_file``: 用于将词ID转换为词的字典文件。 |
| 252 | + - ``result_file``: 生成结果文件的路径。 |
| 253 | + |
| 254 | +代码如下: |
| 255 | + |
| 256 | +.. code:: sourcecode |
| 257 | +
|
| 258 | + group_inputs=[StaticInput(input=encoded_vector,is_seq=True), |
| 259 | + StaticInput(input=encoded_proj,is_seq=True)] |
| 260 | + # 在生成时,解码器基于编码源序列和最后生成的目标词预测下一目标词。 |
| 261 | + # 编码源序列(编码器输出)必须由只读Memory的 StaticInput 指定。 |
| 262 | + # 这里, GeneratedInputs 自动获取上一个生成的词,并在最开始初始化为起始词,如 <s>。 |
| 263 | + trg_embedding = GeneratedInput( |
| 264 | + size=target_dict_dim, |
| 265 | + embedding_name='_target_language_embedding', |
| 266 | + embedding_size=word_vector_dim) |
| 267 | + group_inputs.append(trg_embedding) |
| 268 | + beam_gen = beam_search(name=decoder_group_name, |
| 269 | + step=gru_decoder_with_attention, |
| 270 | + input=group_inputs, |
| 271 | + bos_id=0, # Beginnning token. |
| 272 | + eos_id=1, # End of sentence token. |
| 273 | + beam_size=beam_size, |
| 274 | + max_length=max_length) |
| 275 | +
|
| 276 | + seqtext_printer_evaluator(input=beam_gen, |
| 277 | + id_input=data_layer(name="sent_id", size=1), |
| 278 | + dict_file=trg_dict_path, |
| 279 | + result_file=gen_trans_file) |
| 280 | + outputs(beam_gen) |
| 281 | +
|
| 282 | +注意,这种生成技术只用于类似解码器的生成过程。如果你正在处理序列标记任务,请参阅 |
| 283 | +`Semantic Role Labeling |
| 284 | +Demo <../../demo/semantic_role_labeling/index.html>`__ |
| 285 | +了解更多详细信息。 |
| 286 | + |
| 287 | +完整的配置文件在\ ``demo/seqToseq/seqToseq_net.py``\ 。 |
0 commit comments