再谈Seq2Seq和Attention
之前做NLP的时候发过一篇Seq2Seq和Attention的博客,其实他Seq2Seq的代码写的是有些问题的。
真正的Seq2Seq应该怎么写
把我最近做交通流的一个Seq2Seq放上来,输入是[batch,timelags,feature_num]。
转换到NLP里就是[batch,sentence_length,feature_num]。
# 训练模型
# ---层定义---
x = Input(shape=(None, feature_num,))
enc_LSTM1 = CuDNNLSTM(units=base_num, return_sequences=True, return_state=True)
enc_LSTM2 = CuDNNLSTM(units=base_num, return_sequences=True, return_state=True)
y = Input(shape=(None, feature_num,))
dec_LSTM1 = CuDNNLSTM(units=base_num, return_sequences=True, return_state=True)
dec_LSTM2 = CuDNNLSTM(units=base_num, return_sequences=True, return_state=True)
# 输出f/v/s/lf
att_W=Dense(units=base_num,bias_regularizer=l1(0.02))
att_layer=AttLayer()
att_max=Maximum()
outDense = Dense(units=4, bias_regularizer=l1(0.02))
fLambda = Lambda(lambda netOut: netOut[:, :, 0] * params[0][1] + params[0][0])
vLambda = Lambda(lambda netOut: netOut[:, :, 1] * params[1][1] + params[1][0])
sLambda = Lambda(lambda netOut: netOut[:, :, 2] * params[2][1] + params[2][0])
lfLambda = Lambda(lambda netOut: netOut[:, :, 3] * params[3][1] + params[3][0])
# ---encoder---
enc_lstm1, state_h1, state_c1 = enc_LSTM1(x)
encoder_state1 = [state_h1, state_c1]
enc_lstm2, state_h2, state_c2 = enc_LSTM2(enc_lstm1)
encoder_state2 = [state_h2, state_c2]
# ---decoder---
decoder_lstm1, _, _ = dec_LSTM1(y, initial_state=encoder_state1)
decoder_lstm2, _, _ = dec_LSTM2(decoder_lstm1, initial_state=encoder_state2)
att_input=att_W(decoder_lstm2)
decoder_att=att_layer([enc_lstm2,att_input])
decoder_att_res=att_max([decoder_lstm2,decoder_att])
decoder_Dense2 = outDense(decoder_att_res)
f_out = fLambda(decoder_Dense2)
v_out = vLambda(decoder_Dense2)
s_out = sLambda(decoder_Dense2)
lf_out = lfLambda(decoder_Dense2)
model = Model([x, y], [f_out, v_out, s_out, lf_out])
model.compile(loss='mse', optimizer=RMSprop(2e-5))
model.summary()
# 推断模型
# ---encoder---
encoder_model = Model(x, [enc_lstm2] + encoder_state1 + encoder_state2)
# ---decoder---
decInput_enc = Input(shape=(None, base_num,))
decInput_h1 = Input(shape=(base_num,))
decInput_c1 = Input(shape=(base_num,))
decInput_h2 = Input(shape=(base_num,))
decInput_c2 = Input(shape=(base_num,))
decInput_state1 = [decInput_h1, decInput_c1]
decInput_state2 = [decInput_h2, decInput_c2]
dec_lstm1, dec_h1, dec_c1 = dec_LSTM1(y, initial_state=decInput_state1)
dec_lstm2, dec_h2, dec_c2 = dec_LSTM2(dec_lstm1, initial_state=decInput_state2)
dec_state1 = [dec_h1, dec_c1]
dec_state2 = [dec_h2, dec_c2]
att_in=att_W(dec_lstm2)
dec_att=att_layer([decInput_enc,att_in])
dec_att_res=att_max([dec_lstm2,dec_att])
dec_Dense2 = outDense(dec_att_res)
f_out = fLambda(dec_Dense2)
v_out = vLambda(dec_Dense2)
s_out = sLambda(dec_Dense2)
lf_out = lfLambda(dec_Dense2)
decoderModel = Model([y, decInput_enc] + decInput_state1 + decInput_state2,
[f_out, v_out, s_out, lf_out] + dec_state1 + dec_state2)
decoderModel.summary()
首先,为什么训练模型和推断模型要分开写,因为训练的时候是可以知道输出序列的真值的,可以用到一个技巧叫teacher forcing,就是直接在decoder的每个时刻输出上一个时刻的真值。而这在推断阶段的decoder是做不到的,所以decoder只能一个一个时刻的跑。两者的运行方式不太一样。
那么我训练了训练模型,怎么传递给推断模型呢。
这就要用到基本的对象的思想了,我们知道对象其实是一个指针,指向内存中的一块区域,区域里有他的成员。所以整个模型钟需要完成训练和推掉模型间交互的,其实只有带权重的几个LSTM和Dense层,没有必要涉及到输入层、Lambda层之类的。因此我们在最开始就把层对象定义好,在训练的时候他内存中的权重就会被改变,之后我们直接在推断模型钟加入这些对象的指针,就完成了权重的传递。
注意真正需要传递的只有带权重的层,比如LSTM层,这点其实是非常有用的。
Attention
Attention其实就是,decoder每个时刻生成输出之后,和encoder的每个输出做点乘,点乘的结果表示他们的相似度,然后按encoder结果与每个时刻decoder结果的相似度,加权得出一个新的输出,和原来的decoder输出结合形成新的输出。
根据相似度的计算方式Attention分很多种,点乘是Luong Attention中的dot方法,不需要涉及权重矩阵,很简便也很高效。包括Attention输出和原输出怎么拼接也是个学问。我做的时候取最大效果挺好的。Multi-head就是加入矩阵求相似度之后,做很多个Attention,表示不同模式下的注意力。
keras的代码这样写:
#---temporal Attention---
class AttLayer(Layer):
def __init__(self, **kwargs):
super(AttLayer, self).__init__(**kwargs)
def build(self, input_shape):
assert isinstance(input_shape, list)
super(AttLayer, self).build(input_shape) # be sure you call this somewhere!
def call(self, inputs, mask=None):
input_x,input_y=inputs
# 列数==行数
# [m,n]*[n,q]=[m,q]
x_t = K.permute_dimensions(input_x, (0, 2, 1))
e = K.batch_dot(input_y, x_t)
a_exp = K.exp(e)
a_c = K.repeat(K.sum(a_exp, axis=2), K.shape(input_x)[1])
a_c = K.permute_dimensions(a_c, (0, 2, 1))
a = a_exp / a_c
out = K.batch_dot(a, input_x)
return out
def get_output_shape_for(self, input_shape):
assert isinstance(input_shape, list)
x_shape,y_shape=input_shape
return y_shape
class定义keras层,init、build、call是必须的,call是真正的执行起来用到的部分,如果输出形状和输入不一样还要重写get_output_shape_for,还是叫compute_output_shape。一样就不用写,我这里就是输入输出形状一样。