seq2seq模型

做个记录

简单的seq2seq

输入一个序列,用一个 RNN (Encoder)编码成一个向量 u,再用另一个 RNN (Decoder)解码成一个序列输出,且输出序列的长度是可变的。

import tensorflow as tf

class Seq2seq(object):
    def __init__(self, config, w2i_target):
        self.seq_inputs = tf.placeholder(shape=(config.batch_size, None), dtype=tf.int32, name='seq_inputs')
        self.seq_inputs_length = tf.placeholder(shape=(config.batch_size,), dtype=tf.int32, name='seq_inputs_length')
        self.seq_targets = tf.placeholder(shape=(config.batch_size, None), dtype=tf.int32, name='seq_targets')
        self.seq_targets_length = tf.placeholder(shape=(config.batch_size,), dtype=tf.int32, name='seq_targets_length')
        
    with tf.variable_scope("encoder"):
        encoder_embedding = tf.Variable(tf.random_uniform([config.source_vocab_size, config.embedding_dim]), dtype=tf.float32, name='encoder_embedding')
        encoder_inputs_embedded = tf.nn.embedding_lookup(encoder_embedding, self.seq_inputs)
        encoder_cell = tf.nn.rnn_cell.GRUCell(config.hidden_dim)
        encoder_outputs, encoder_state = tf.nn.dynamic_rnn(cell=encoder_cell, inputs=encoder_inputs_embedded, sequence_length=self.seq_inputs_length, dtype=tf.float32, time_major=False)
    
    tokens_go = tf.ones([config.batch_size], dtype=tf.int32) * w2i_target["_GO"]
    decoder_inputs = tf.concat([tf.reshape(tokens_go,[-1,1]), self.seq_targets[:,:-1]], 1)

    
    with tf.variable_scope("decoder"):
        decoder_embedding = tf.Variable(tf.random_uniform([config.target_vocab_size, config.embedding_dim]), dtype=tf.float32, name='decoder_embedding')
        decoder_inputs_embedded = tf.nn.embedding_lookup(decoder_embedding, decoder_inputs)
        decoder_cell = tf.nn.rnn_cell.GRUCell(config.hidden_dim)
        decoder_outputs, decoder_state = tf.nn.dynamic_rnn(cell=decoder_cell, inputs=decoder_inputs_embedded, initial_state=encoder_state, sequence_length=self.seq_targets_length, dtype=tf.float32, time_major=False)
    
    decoder_logits = tf.layers.dense(decoder_outputs.rnn_output, config.target_vocab_size)
    self.out = tf.argmax(decoder_logits, 2)

训练阶段使用 teacher forcing这样做是好的,因为:

  • 防止上一时刻的错误传播到这一时刻,decode 出一个序列,要是第一个单词错了,整个序列就跑偏了,这个序列就没啥意义了,计算 loss 更新参数作用都很小了。用了 Teacher Forcing 可以阻断错误积累,斧正模型训练,加快参数收敛(我自己试了一下,用和不用 Teacher Forcing,训练时候的 loss 下降速度和最终结果真的差了不少)

  • 这样就可以提前把 decoder 的整个输入序列提前准备好,直接放到 dynamic_rnn 函数就能出结果,实现起来简单方便

但是,有个最大的问题:模型训练好了,到了测试阶段,你是不能用 Teacher Forcing 的,因为测试阶段你是看不到期望的输出序列的,所以必须乖乖等着上一时刻输出一个单词,下一时刻才能确定该输入什么。不能提前把整个 decoder 的输入序列准备好, 也就不能用 dynamic_rnn 函数了

利用组件来实现

tokens_go = tf.ones([config.batch_size], dtype=tf.int32) * w2i_target["_GO"]

decoder_embedding = tf.Variable(tf.random_uniform([config.target_vocab_size, config.embedding_dim]), dtype=tf.float32, name='decoder_embedding')
decoder_cell = tf.nn.rnn_cell.GRUCell(config.hidden_dim)

if useTeacherForcing:
    decoder_inputs = tf.concat([tf.reshape(tokens_go,[-1,1]), self.seq_targets[:,:-1]], 1)
    #接收的参数主要有一个大小为[batch_size, seqlen, embed_size]的输入inputs;以及每个句子的真实长度sequence_length,是一个[batch_size]的向量;
    #time_major为真则把seqlen作为第一维。注意下sequence_length是一个batch_size大小的数组,指明了每个句子的真实长度(因为有些长度是padding的)。
    helper =tf.contrib.seq2seq.TrainingHelper(tf.nn.embedding_lookup(decoder_embedding, decoder_inputs), self.seq_targets_length)
else:
    helper = tf.contrib.seq2seq.GreedyEmbeddingHelper(decoder_embedding, tokens_go, w2i_target["_EOS"])
"""
helper 这个类来帮你自动地给 decoder rnn 的每个时刻提供不同的输入内容,用或不用 Teacher Forcing 的区别只在于将 helper 定义为 TrainingHelper 
  或是 GreedyEmbeddingHelper。 且这两种方式,从模型变量的角度看是没有区别的,只是数据的流动方式不同,也就是说,在实际应用中,可以在 train 
  阶段新建一个用 TrainingHelper 的模型,训练完了保存模型参数,在 test 阶段再新建另一个用 GreedyEmbeddingHelper 的模型,直接加载训练好的参
  数就可以用
"""
    
#BasicDecoder的作用就是定义一个封装了decoder应该有的功能的实例,根据Helper实例的不同,这个decoder可以实现不同的功能,
#比如在train的阶段,不把输出重新作为输入,而在inference阶段,将输出接到输入。
decoder = tf.contrib.seq2seq.BasicDecoder(decoder_cell, helper, encoder_state, output_layer=tf.layers.Dense(config.target_vocab_size))
#dynamic_decode 函数类似于 dynamic_rnn,帮你自动执行 rnn 的循环,返回完整的输出序列
decoder_outputs, decoder_state, final_sequence_lengths = tf.contrib.seq2seq.dynamic_decode(decoder, maximum_iterations=tf.reduce_max(self.seq_targets_length))

decoder阶段加入attention

# decoder阶段的 attention机制  
decoder_cell = tf.nn.rnn_cell.GRUCell(config.hidden_dim)
if useAttention:
    attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(num_units=config.hidden_dim, memory=encoder_outputs, memory_sequence_length=self.seq_inputs_length)
    # attention_mechanism = tf.contrib.seq2seq.LuongAttention(num_units=config.hidden_dim, memory=encoder_outputs, memory_sequence_length=self.seq_inputs_length)
    decoder_cell = tf.contrib.seq2seq.AttentionWrapper(decoder_cell, attention_mechanism)
    decoder_initial_state = decoder_cell.zero_state(batch_size=config.batch_size, dtype=tf.float32)
    decoder_initial_state = decoder_initial_state.clone(cell_state=encoder_state)

decoder = tf.contrib.seq2seq.BasicDecoder(decoder_cell, helper, decoder_initial_state, output_layer=tf.layers.Dense(config.target_vocab_size))
decoder_outputs, decoder_state, final_sequence_lengths = tf.contrib.seq2seq.dynamic_decode(decoder, maximum_iterations=tf.reduce_max(self.seq_targets_length))
"""
直观上看就是把原来定义的最基础 GRU 单元(decoder_cell)外面套一个 AttentionWrapper,直接替换原来的 decoder_cell 就好,只有两个字,省事。
  全家桶提供了两种可选 attention 策略:BahdanauAttention 和 LuongAttention,具体区别不细说了,主要是 attention score 怎么计算以及“c”
  怎么结合到输入中的问题,实践上效果差异基本不大
"""

预测阶段加入beam search

tokens_go = tf.ones([config.batch_size], dtype=tf.int32) * w2i_target["_GO"]
decoder_cell = tf.nn.rnn_cell.GRUCell(config.hidden_dim)

if useBeamSearch > 1:
"""
这回就是把 decoder 从 BasicDecoder 换成 BeamSearchDecoder 就完事了,这封装的,流弊

因为使用了 Beam Search,所以 decoder 的输入形状需要做 K 倍的扩展,tile_batch 就是用来干这个。
  如果和之前的 AttentionWrapper 搭配使用的话,还需要把encoder_outputs 和 sequence_length 都用 
  tile_batch 做一下扩展,具体可以看代码,不细说了
"""
    decoder_initial_state = tf.contrib.seq2seq.tile_batch(encoder_state, multiplier=useBeamSearch)
    decoder = tf.contrib.seq2seq.BeamSearchDecoder(decoder_cell, decoder_embedding, tokens_go, w2i_target["_EOS"],  decoder_initial_state , beam_width=useBeamSearch, output_layer=tf.layers.Dense(config.target_vocab_size))
else:
    decoder_initial_state = encoder_state
    decoder = tf.contrib.seq2seq.BasicDecoder(decoder_cell, helper, decoder_initial_state, output_layer=tf.layers.Dense(config.target_vocab_size))

decoder_outputs, decoder_state, final_sequence_lengths = tf.contrib.seq2seq.dynamic_decode(decoder, maximum_iterations=tf.reduce_max(self.seq_targets_length))  
# 如果使用beam search 最终的输出是beam_width个
### Seq2Seq 模型概述 Seq2Seq(Sequence to Sequence)模型是一种神经网络架构,主要用于将一个序列映射到另一个序列的任务。它广泛应用于自然语言处理领域,例如机器翻译、文本摘要生成以及对话系统等任务[^4]。 #### 基本概念 Seq2Seq 模型的核心思想是利用两个主要组件来完成任务:编码器(Encoder)和解码器(Decoder)。编码器负责接收输入序列并将该序列压缩成固定长度的上下文向量;而解码器则基于这个上下文向量生成目标序列[^1]。 #### 工作原理 在实际操作过程中,编码器通常采用循环神经网络(RNN),如 LSTM 或 GRU 来逐词读取输入序列,并将其转换为隐藏状态表示。最终时间步上的隐藏状态作为整个输入序列的上下文表示传递给解码器。随后,解码器同样使用 RNN 结构逐步生成输出序列中的每一个单词。 然而,在标准 Seq2Seq 架构中存在一个问题——即 **信息不对齐现象**。由于所有输入都被压缩到了单一的上下文中,这可能导致重要细节丢失或者被忽略掉,尤其是在面对较长输入时更为明显[^3]。 为了缓解这一缺陷,“注意力机制”(Attention Mechanism) 被引入进来。通过计算每一步之间源句与当前预测位置之间的关联程度权重分布,使得译者能够动态关注不同部分的内容而非仅仅依靠最后一个隐含层输出来进行下一步决策。 #### 实现方式 以 TensorFlow 为例可以实现上述提到的标准版及改进后的带 Attention 的 Seq2Seq 模型。以下是简化版本代码片段展示了如何构建基础框架: ```python import tensorflow as tf class Encoder(tf.keras.Model): def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz): super(Encoder, self).__init__() self.batch_sz = batch_sz self.enc_units = enc_units self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim) self.gru = tf.keras.layers.GRU(self.enc_units, return_sequences=True, return_state=True, recurrent_initializer='glorot_uniform') def call(self, x, hidden): x = self.embedding(x) output, state = self.gru(x, initial_state=hidden) return output, state class Decoder(tf.keras.Model): def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz): super(Decoder, self).__init__() self.batch_sz = batch_sz self.dec_units = dec_units self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim) self.gru = tf.keras.layers.GRU(self.dec_units, return_sequences=True, return_state=True, recurrent_initializer='glorot_uniform') self.fc = tf.keras.layers.Dense(vocab_size) def call(self, x, hidden, enc_output): # 定义具体的前馈逻辑... pass # 初始化参数并调用模型进行训练过程省略... ``` 以上仅提供了一个非常初步的概念性演示,具体还需要根据项目需求调整超参设置以及其他优化措施。 尽管如此,Seq2Seq 模型也面临着诸多挑战,比如对于特别长的序列建模效果不佳、本身属于黑箱性质缺乏透明度等问题亟待解决[^2]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值