Keras-9 实现Seq2Seq

A ten-minute introduction to sequence-to-sequence learning in Keras

  • 简单介绍如何用Keras实现Seq2Seq模型
  • 原文链接 https://blog.keras.io/a-ten-minute-introduction-to-sequence-to-sequence-learning-in-keras.html
  • 该博客的完整代码在 这里

Sequence-to-sequence 学习是什么?

Sequence-to-sequnce学习(Seq2Seq)大概就是将一个序列(Sequence)从一个域转换到另一个域,例如将一段英语通过翻译转换到法语

“the cat sat on the mat” -> [Seq2Seq model] -> “le chat etait assis sur le tapis”

Seq2Seq模型可以用于机器翻译,或者问答系统(给定一个问题,自动生成答案),一般情况下,Seq2Seq模型可以实现任意的文本生成

处理Seq2Seq的方法很多种,可以使用RNN或者1D CNN,这里我们将重点讨论RNN

一种特殊的情况:当输入和输出的长度相同

当输入与输出的序列长度相同时,那么事情变得比较简单,我们可以用LSTM或者GRU就能实现Seq2Seq. 这里有个例子展示了如何教RNN去学习数字的加法(数字用字符串表示)
add numbers

一般情况:规范的Seq2Seq

通常情况下,输入序列和输出序列具有不同的长度(例如机器翻译),并且需要整个输入以便开始预测目标。这就需要一些更高级的设置,下面是Seq2Seq的工作原理

  • 用RNN层(一层或者多层)作为编码器(encoder):它处理输入序列并返回输入序列的内部状态。注意一点,我们抛弃了RNN层的输出,只需要状态(state).状态,可以理解为解码器需要的上下文(context),或者条件(conditioning)
  • 用另一个RNN层(一层或者多层)作为解码器(decoder):它被训练用来预测目标序列的下一个字符,当给定目标序列的前一个字符时。特殊的,它被训练成将目标序列转换成相同的序列,但是会有一个时间步长的偏移,在这种情况下称为"teacher forcing"的训练过程。重要的是,解码器使用来自编码器的状态向量作为初始状态,这使得解码器能够知道应该产生什么样的信息。给定targets[…t](前t个),并且在知晓输入的情况下(conditioned on the input sequence)解码器能够学习如何产生targets[t+1…]

seq2seq-teacher

在推断模式下,例如我们希望解码一组未知的序列,我们的流程有一丢丢不同:

  1. 将输入序列编码成状态向量 input->state vector
  2. 目标序列的大小从1开始 1-char
  3. 给定输入序列的状态向量以及1-char的目标序列,解码器生成下一个字符的预测
  4. 对下一个字符进行采样(在这里,简单的用argmax作为采样)
  5. 将采样的字符添加到目标序列中
  6. 重复以上操作直到生成了结束字符或者超过字符数量上限

seq2seq-inference

A Keras example

让我们动手实现下代码

在这里例子中,我们用到的数据集是一组常用英语短语以及其法语的翻译,叫 fra-eng.zip,这个数据集可以从 manythings.org/anki 下载

我们将实现字符级别的Seq2Seq模型,一个字符一个字符地处理输入序列,一个字符一个字符地生成序列。当然,我们也可以训练单词级别的模型,这样的模型在机器翻译中更为常见。在最后我们将说明一些如何用Embedding将我们字符级别的模型转换为单词级别

完整的代码在能够在github中找到

下面是代码的总结:

  1. 将句子转换为3个numpy arrays, encoder_input_data, decoder_input_data, decoder_target_data:
    • encoder_input_data 是一个 3D 数组,大小为 (num_pairs, max_english_sentence_length, num_english_characters),包含英语句子的one-hot向量
    • decoder_input_data 是一个 3D 数组,大小为 (num_pairs, max_fench_sentence_length, num_french_characters) 包含法语句子的one-hot向量
    • decoder_target_datadecoder_input_data 相同,但是有一个时间的偏差。 decoder_target_data[:, t, :]decoder_input_data[:, t+1, :]相同
  2. 训练一个基于LSTM的Seq2Seq模型,在给定 encoder_input_datadecoder_input_data是,预测 decoder_target_data,我们的模型利用了teacher forcing
  3. 解码一些语言用来验证模型事有效的

下面我们还是来看代码吧

from keras.models import Model
from keras.layers import Input, LSTM, Dense
from keras import callbacks
import numpy as np
Using TensorFlow backend.
# 基本参数
batch_size = 64
epochs = 100
latent_dim = 256 # LSTM 的单元个数
num_samples = 10000 # 训练样本的大小

# 数据集路径
data_path = 'fra-eng/fra.txt'

数据向量化

input_texts = []
target_texts = []
input_characters = set()
target_characters = set()

with open(data_path, 'r', encoding='utf-8') as f:
    lines = f.read().split('\n')
# 显示部分数据
lines[:20]
['Go.\tVa !',
 'Run!\tCours\u202f!',
 'Run!\tCourez\u202f!',
 'Wow!\tÇa alors\u202f!',
 'Fire!\tAu feu !',
 "Help!\tÀ l'aide\u202f!",
 'Jump.\tSaute.',
 'Stop!\tÇa suffit\u202f!',
 'Stop!\tStop\u202f!',
 'Stop!\tArrête-toi !',
 'Wait!\tAttends !',
 'Wait!\tAttendez !',
 'Go on.\tPoursuis.',
 'Go on.\tContinuez.',
 'Go on.\tPoursuivez.',
 'I see.\tJe comprends.',
 "I try.\tJ'essaye.",
 "I won!\tJ'ai gagné !",
 "I won!\tJe l'ai emporté !",
 'Oh no!\tOh non !']
for line in lines[: min(num_samples, len(lines) - 1)]:
    # 分割输入序列和目标序列
    input_text, target_text = line.split('\t')
    
    # 用'tab'作为 一个序列的开始字符
    # 用 '\n' 作为 序列的结束字符
    target_text = '\t' + target_text + '\n'
    
    input_texts.append(input_text)
    target_texts.append(target_text)
    
    # 计算 input_text 中的 tokens
    for char in input_text:
        if char not in input_characters:
            input_characters.add(char)
    
    # 计算 target_text 中的 tokens
    for char in target_text:
        if char not in target_characters:
            target_characters.add(char)
            
input_characters = sorted(list(input_characters))
target_characters = sorted(list(target_characters))
num_encoder_tokens = len(input_characters)
num_decoder_tokens = len(target_characters)
max_encoder_seq_length = max([ len(txt) for txt in input_texts])
max_decoder_seq_length = max([ len(txt) for txt in target_texts])

print('Nunmber of samples:', len(input_texts))
print('Number of unique input tokens:', num_encoder_tokens)
print('Number of unique output tokens:', num_decoder_tokens)
print('Max sequence length of input:', max_encoder_seq_length)
print('Max sequence length of outputs:', max_decoder_seq_length)
Nunmber of samples: 10000
Number of unique input tokens: 71
Number of unique output tokens: 94
Max sequence length of input: 16
Max sequence length of outputs: 59
# 建立 字符->数字 字典,用于字符的向量化
input_token_index = dict( [(char, i)for i, char in enumerate(input_characters)] )
target_token_index = dict( [(char, i) for i, char in enumerate(target_characters)] )
# 创建数组 
encoder_input_data = np.zeros((len(input_texts), max_encoder_seq_length, num_encoder_tokens), dtype=np.float32)
decoder_input_data = np.zeros((len(input_texts), max_decoder_seq_length, num_decoder_tokens), dtype=np.float32)
decoder_target_data = np.zeros((len(input_texts), max_decoder_seq_length, num_decoder_tokens), dtype=np.float32)

# 填充数据, 对每一个字符做one-hot
for i, (input_text, target_text) in enumerate(zip(input_texts, target_texts)):
    # 对编码器的输入序列做one-hot
    for t, char in enumerate(input_text):
        encoder_input_data[i, t, input_token_index[char]] = 1.0
    
    # 对解码器的输入与输出做序列做one-hot
    for t, char in enumerate(target_text):
        decoder_input_data[i, t, target_token_index[char]] = 1.0
        
        if t > 0:
            # decoder_target_data 不包含开始字符,并且比decoder_input_data提前一步
            decoder_target_data[i, t-1, target_token_index[char]] = 1.0

设计模型

编码器
# 定义编码器的输入
# encoder_inputs (None, num_encoder_tokens), None表示可以处理任意长度的序列
encoder_inputs = Input(shape=(None, num_encoder_tokens))

# 编码器,要求其返回状态
encoder = LSTM(latent_dim, return_state=True)

# 调用编码器,得到编码器的输出(输入其实不需要),以及状态信息 state_h 和 state_c
encoder_outpus, state_h, state_c = encoder(encoder_inputs)

# 丢弃encoder_outputs, 我们只需要编码器的状态
encoder_state = [state_h, state_c]
解码器
# 定义解码器的输入
# 同样的,None表示可以处理任意长度的序列
decoder_inputs = Input(shape=(None, num_decoder_tokens))

# 接下来建立解码器,解码器将返回整个输出序列
# 并且返回其中间状态,中间状态在训练阶段不会用到,但是在推理阶段将是有用的
decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True)

# 将编码器输出的状态作为初始解码器的初始状态
decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=encoder_state)

# 添加全连接层
decoder_dense = Dense(num_decoder_tokens, activation='softmax')
decoder_outputs = decoder_dense(decoder_outputs)

训练模型

# 定义整个模型
model = Model([encoder_inputs, decoder_inputs], decoder_outputs)

# 定义回调函数
#callback_list = [callbacks.EarlyStopping(patience=10)]
# 编译模型
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

# 训练
model.fit([encoder_input_data, decoder_input_data], decoder_target_data,
          batch_size=batch_size,
          epochs = epochs,
          validation_split=0.2)

# 保存模型
model.save('s2s_2.h5')
Train on 8000 samples, validate on 2000 samples
Epoch 1/100
8000/8000 [==============================] - 21s 3ms/step - loss: 0.9184 - val_loss: 0.9529
Epoch 2/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.7240 - val_loss: 0.8046
Epoch 3/100
8000/8000 [==============================] - 18s 2ms/step - loss: 0.6161 - val_loss: 0.6959
Epoch 4/100
8000/8000 [==============================] - 18s 2ms/step - loss: 0.5613 - val_loss: 0.6610
Epoch 5/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.5225 - val_loss: 0.6264
Epoch 6/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.4901 - val_loss: 0.5925
Epoch 7/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.4640 - val_loss: 0.5756
Epoch 8/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.4415 - val_loss: 0.5536
Epoch 9/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.4218 - val_loss: 0.5432
Epoch 10/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.4047 - val_loss: 0.5353
Epoch 11/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.3890 - val_loss: 0.5224
Epoch 12/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.3750 - val_loss: 0.5085
Epoch 13/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.3618 - val_loss: 0.5004
Epoch 14/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.3489 - val_loss: 0.4961
.....
Epoch 100/100
8000/8000 [==============================] - 19s 2ms/step - loss: 0.0563 - val_loss: 0.7883


/home/nls3/anaconda2/envs/keras/lib/python3.6/site-packages/keras/engine/topology.py:2344: UserWarning: Layer lstm_2 was passed non-serializable keyword arguments: {'initial_state': [<tf.Tensor 'lstm_1/while/Exit_2:0' shape=(?, 256) dtype=float32>, <tf.Tensor 'lstm_1/while/Exit_3:0' shape=(?, 256) dtype=float32>]}. They will not be included in the serialized model (and thus will be missing at deserialization time).
  str(node.arguments) + '. They will not be included '

建立推断模型

  • 将推断模型与训练模型分开,这样比较清楚,但是他们两内部所用到的结构是一样的
  • 推断的步骤如下
    1. 将输入编码,得到解码器所需要的初始状态
    2. 结合初始状态,对一个size=1的序列(其中只包含开始字符)做模型推断,得到的输出作为下一个size=1序列的内容
    3. 结合当前的输出以及状态,重复以上步骤
# 定义 sampling 模型
# 定义 encoder 模型,得到输出encoder_states
encoder_model = Model(encoder_inputs, encoder_state)

decoder_state_input_h = Input(shape=(latent_dim,))
decoder_state_input_c = Input(shape=(latent_dim,))
decoder_states_inputs = [decoder_state_input_h, decoder_state_input_c]

# 得到解码器的输出以及中间状态
decoder_outputs, state_h, state_c = decoder_lstm(decoder_inputs, initial_state=decoder_states_inputs)
decoder_states = [state_h, state_c]
decoder_outputs = decoder_dense(decoder_outputs)

decoder_model = Model([decoder_inputs] + decoder_states_inputs, [decoder_outputs]+decoder_states)
# 建立 数字->字符 的字典,用于恢复
reverse_input_char_index = dict([(i, char) for char, i in input_token_index.items()])
reverse_target_char_index = dict([(i, char) for char, i in target_token_index.items()])
def decode_sequence(input_seq):
    # 将输入序列进行编码
    states_value = encoder_model.predict(input_seq)
    
    # 生成一个size=1的空序列
    target_seq = np.zeros((1, 1, num_decoder_tokens))
    # 将这个空序列的内容设置为开始字符
    target_seq[0, 0, target_token_index['\t']] = 1.
    
    # 进行字符恢复
    # 简单起见,假设batch_size = 1
    stop_condition = False
    decoded_sentence = ''
    
    while not stop_condition:
        output_tokens, h, c = decoder_model.predict([target_seq] + states_value)
        
        # sample a token
        sampled_token_index = np.argmax(output_tokens[0, -1, :])
        sampled_char = reverse_target_char_index[sampled_token_index]
        decoded_sentence += sampled_char
        
        # 退出条件:生成 \n 或者 超过最大序列长度
        if sampled_char == '\n' or len(decoded_sentence) > max_decoder_seq_length :
            stop_condition = True
            
        # 更新target_seq
        target_seq = np.zeros((1, 1, num_decoder_tokens))
        target_seq[0, 0, sampled_token_index] = 1.
        
        # 更新中间状态
        states_value = [h, c]
        
    return decoded_sentence
# 检验成果的时候到了,从训练集中选取一些句子做测试
# 效果还行(废话,从训练集里挑的数据)
for seq_index in range(1000, 1100):
    # batch_size = 1
    input_seq = encoder_input_data[seq_index:seq_index+1]
    decoded_sentence = decode_sequence(input_seq)
    
    print('-')
    print('Input sentence:', input_texts[seq_index])
    print('Decoded sentence:', decoded_sentence)
-
Input sentence: Come alone.
Decoded sentence: Venez seuls !

-
Input sentence: Come alone.
Decoded sentence: Venez seuls !

-
Input sentence: Come along.
Decoded sentence: Venez seul !

-
Input sentence: Come on in!
Decoded sentence: Arrête de te li peis travant.

-
Input sentence: Come on in.
Decoded sentence: Entre.

-
Input sentence: Come quick!
Decoded sentence: Dépêche-toi de venir !

-
Input sentence: Come quick!
Decoded sentence: Dépêche-toi de venir !

......

-
Input sentence: I am tired.
Decoded sentence: Je suis chez lui.

-
Input sentence: I broke it.
Decoded sentence: Je l'ai cassé.

-
Input sentence: I broke it.
Decoded sentence: Je l'ai cassé.

总结

我们了解了如何用Keras实现Seq2Seq模型,关键在于构建编码器和解码器,并且要认识到训练阶段与推断阶段工作的流程是不一样的

原文最后还提到了如何将我们这个字符级别的模型改成单词级别,有兴趣的同学可以了解了解

以下是一个简单的基于Kerasseq2seq代码实现,用于将英文短语翻译成法语短语: ``` from keras.models import Model from keras.layers import Input, LSTM, Dense # 定义输入序列 encoder_inputs = Input(shape=(None, num_encoder_tokens)) # LSTM编码器 encoder_lstm = LSTM(latent_dim, return_state=True) encoder_outputs, state_h, state_c = encoder_lstm(encoder_inputs) # 保留编码器状态 encoder_states = [state_h, state_c] # 定义解码器输入 decoder_inputs = Input(shape=(None, num_decoder_tokens)) # LSTM解码器 decoder_lstm = LSTM(latent_dim, return_sequences=True, return_state=True) decoder_outputs, _, _ = decoder_lstm(decoder_inputs, initial_state=encoder_states) decoder_dense = Dense(num_decoder_tokens, activation='softmax') decoder_outputs = decoder_dense(decoder_outputs) # 定义模型 model = Model([encoder_inputs, decoder_inputs], decoder_outputs) # 编译模型 model.compile(optimizer='rmsprop', loss='categorical_crossentropy') # 训练模型 model.fit([encoder_input_data, decoder_input_data], decoder_target_data, batch_size=batch_size, epochs=epochs, validation_split=0.2) ``` 在这个例子中,我们使用了一个LSTM编码器和一个LSTM解码器,并将它们连接起来作为一个seq2seq模型。我们还使用了一个Dense层来将解码器的输出转换成概率分布,以便选择下一个预测字符。最后,我们将所有的模型组件组装成一个Keras模型,并使用rmsprop优化器和分类交叉熵损失函数来进行训练。
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值