注:此文章内容均节选自充电了么创始人,CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》(人工智能科学与技术丛书)【陈敬雷编著】【清华大学出版社】
自然语言处理系列六十四
神经网络算法》Seq2Seq端到端神经网络算法
Seq2Seq技术,全称Sequence to Sequence,该技术突破了传统的固定大小输入问题框架,开通了将经典深度神经网络模型(DNNs)运用于翻译与智能问答这一类序列型(Sequence Based,项目间有固定的先后关系)任务的先河,并被证实在机器翻译、对话机器人、语音辨识的应用中有着不俗的表现。下面就详细讲一下其原理和实现。
1. Seq2Seq原理介绍
传统的Seq2Seq是使用两个循环神经网络,将一个语言序列直接转换到另一个语言序列。是循环神经网络的升级版,其联合了两个循环神经网络。一个神经网络负责接收源句子;另一个循环神经网络负责将句子输出成翻译的语言。这两个过程分别称为编码和解码的过程,如图7.38所示。
图7.38 Seq2Seq模型
1)编码过程
编码过程实际上使用了循环神经网络记忆的功能,通过上下文的序列关系,将词向量依次输入网络。对于循环神经网络,每一次网络都会输出一个结果,但是编码的不同之处在于,其只保留最后一个隐藏状态,相当于将整句话浓缩在一起,将其存为一个内容向量(context)供后面的解码器(decoder)使用。
2)解码过程
解码和编码网络结构几乎是一样的,唯一不同的是在解码过程中,是根据前面的结果来得到后面的结果。编码过程中输入一句话,这一句话就是一个序列,而且这个序列中的每个词都是已知的,而解码过程相当于什么也不知道,首先需要一个标识符表示一句话的开始,然后接着将其输入网络得到第一个输出作为这句话的第一个词,接着通过得到的第一个词作为网络的下一个输入,得到的输出作为第二个词,不断循环,通过这种方式来得到最后网络输出的一句话。
3)使用序列到序列网络结构的原因
翻译的每句话的输入长度和输出长度一般来讲都是不同的,而序列到序列的网络结构的优势在于不同长度的输入序列能够能到任意长度的输出序列。使用序列到序列的模型,首先将一句话的所有内容压缩成一个内容向量然后通过一个循环网络不断地将内容提取出来,形成一句新的话。
2. Seq2Seq代码实战
了解了Seq2Seq原理和介绍,我们来做一个实践应用,做一个单词的字母排序,比如输入单词是’acbd’,输出单词是’abcd’,要让机器学会这种排序算法,就可以使用seq2seq的模型来完成,接下来我们分析一下核心步骤,最后会给予一个能直接运行的完整代码给大家学习
1)数据集的准备
这里有两个文件分别是source.txt和target.txt,对应的分别是输入文件和输出文件,代码如下所示。
#读取输入文件
with open('data/letters_source.txt', 'r', encoding='utf-8') as f:
source_data = f.read()
#读取输出文件
with open('data/letters_target.txt', 'r', encoding='utf-8') as f:
target_data = f.read()
2)数据集的预处理
填充序列,序列字符和ID的转换,代码如下所示。
```python
#数据预处理
def extract_character_vocab(data):
#使用特定的字符进行序列的填充
special_words = ['<PAD>', '<UNK>', '<GO>', '<EOS>']
set_words = list(set([character for line in data.split('\n') for character in line]))
#这里要把四个特殊字符添加进词典
int_to_vocab = {
idx: word for idx, word in enumerate(special_words + set_words)}
vocab_to_int = {
word: idx for idx, word in int_to_vocab.items()}
return int_to_vocab, vocab_to_int
source_int_to_letter, source_letter_to_int = extract_character_vocab(source_data)
target_int_to_letter, target_letter_to_int = extract_character_vocab(target_data)
#对字母进行转换
source_int = [[source_letter_to_int.get(letter, source_letter_to_int['<UNK>'])
for letter in line] for line in source_data.split('\n')]
target_int = [[target_letter_to_int.get(letter, target_letter_to_int['<UNK>'])
for letter in line] + [target_letter_to_int['<EOS>']] for line in target_data.split('\n')]
print('source_int_head',source_int[:10])
填充字符含义:
<PAD>: 补全字符。
<EOS>: 解码器端的句子结束标识符。
<UNK>: 低频词或者一些未遇到过的词等。
<GO>: 解码器端的句子起始标识符。
**3) 创建编码层**
创建编码层代码如下所示。
```python
```python
#创建编码层
def get_encoder_layer(input_data, rnn_size, num_layers,source_sequence_length, source_vocab_size,encoding_embedding_size):
#Encoder embedding
encoder_embed_input = layer.embed_sequence(ids=input_data, vocab_size=source_vocab_size,embed_dim=encoding_embedding_size)
#RNN cell
def get_lstm_cell(rnn_size):
lstm_cell = rnn.LSTMCell(rnn_size, initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=2))
return lstm_cell
#指定多个lstm
cell = rnn.MultiRNNCell([get_lstm_cell(rnn_size) for _ in range(num_layers)])
#返回output,state
encoder_output, encoder_state = tf.nn.dynamic_rnn(cell=cell, inputs=encoder_embed_input,sequence_length=source_sequence_length, dtype=tf.float32)
return encoder_output, encoder_state
参数变量含义:
- input_data: 输入tensor
- rnn_size: rnn隐层结点数量
- num_layers: 堆叠的rnn cell数量
- source_sequence_length: 源数据的序列长度
- source_vocab_size: 源数据的词典大小
- encoding_embedding_size: embedding的大小
**4) 创建解码层**
对编码之后的字符串进行处理,去除最后一个没用的字符串,代码如下所示。
```python
#对编码数据进行处理,移除最后一个字符
def process_decoder_input(data, vocab_to_int, batch_size):
'''
补充<GO>,并移除最后一个字符
'''
#cut掉最后一个字符
ending = tf.strided_slice(data, [0, 0], [batch_size, -1], [1, 1])
decoder_input = tf.concat([tf.fill([batch_size, 1], vocab_to_int['<GO>']), ending], 1)
return decoder_input
创建解码层代码如下所示。
#创建解码层
def decoding_layer(target_letter_to_int,
decoding_embedding_size,
num_layers, rnn_size,
target_sequence_length,
max_target_sequence_length,
encoder_state, decoder_input):
#1 构建向量
#目标词汇的长度
target_vocab_size = len(target_letter_to_int)
#定义解码向量的维度大小
decoder_embeddings = tf.Variable(tf.random_uniform([target_vocab_size, decoding_embedding_size]))
#解码之后向量的输出
decoder_embed_input = tf.nn.embedding_lookup(decoder_embeddings, decoder_input)
#2. 构造Decoder中的RNN单元
def get_decoder_cell(rnn_size):
decoder_cell = rnn.LSTMCell(num_units=rnn_size,initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=2))
return decoder_cell
cell = tf.contrib.rnn.MultiRNNCell([get_decoder_cell(rnn_size) for _ in range(num_layers)])
#3. Output全连接层
output_layer = Dense(units=target_vocab_size,kernel_initializer=tf.truncated_normal_initializer(mean=0.0, stddev=0.1))
#4. Training decoder
with tf.variable_scope("decode"):
#得到help对象
training_helper = seq2seq.TrainingHelper(inputs=decoder_embed_input,sequence_length=target_sequence_length,time_major=False)
#构造decoder
training_decoder = seq2seq.BasicDecoder(cell=cell,helper=training_helper,initial_state=encoder_state,output_layer=output_layer)
training_decoder_output, _ ,_= seq2seq.dynamic_decode(decoder=training_decoder,impute_finished=True,maximum_iterations=max_target_sequence_length)
#5. Predicting decoder
#与training共享参数
with tf.variable_scope("decode", reuse=True):
#创建一个常量tensor并复制为batch_size的大小
start_tokens = tf.tile(tf.constant([target_letter_to_int['<GO>']], dtype=tf.int32), [batch_size],name='start_tokens')
predicting_helper = seq2seq.GreedyEmbeddingHelper(decoder_embeddings,start_tokens,target_letter_to_int['<EOS>'])
predicting_decoder = seq2seq.BasicDecoder(cell=cell,helper=predicting_helper,initial_state=encoder_state,output_layer=output_layer)
predicting_decoder_output, _ ,_= seq2seq.dynamic_decode(decoder=predicting_decoder,impute_finished=True,maximum_iterations=max_target_sequence_length)
return training_decoder_output, predicting_decoder_output
在构建解码这一块使用到了,参数共享机制tf.variable_scope(“”),方法参数含义:
- target_letter_to_int: target数据的映射表 <