本文根据 Luong 论文 中NMT的的一个注意力例子来进行 中-英 NMT模型的构建训练,并对该训练后的模型在Docker上进行部署。具体的可以参考Tensorflow教程NMT_with_Attenion ,本文也是根据该教程来对Docker部署进一步探索。
NMT_with_Attenion也是NLP中比较经典的值得复现的一个例子。
本文架构
Model
Train
Save model
Deploy
Model model的具体结构如Luong论文中图片所示,我们将蓝色部分的作为Encoder, 红色部分作为Decoder.
下图是Google Tensorflow中对该模型更进一步的描绘。具体部分,可以参考 上面给出的连接,里面有详细的介绍。
Encoder Encoder中使用了简单的使用了Embedding层+GRU。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Encoder (tf.keras.Model): def __init__ (self, vocab_szie, 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_szie, embedding_dim) self.gru = tf.keras.layers.GRU(self.enc_units, return_sequences=True , return_state=True , recurrent_initializer='glorot_uniform' ) def call (self, inputs ): input , hidden = inputs x = self.embedding(input ) output, state = self.gru(x, initial_state=hidden) return output, state def initialize_hidden_state (self ): return tf.zeros((self.batch_sz, self.enc_units))
这里,特别强调,在call中,有许多 例子 直接 将原始的inputs进行了改变,这样操作实际并不好。 (这点在许多教程,中并没有强调)
由call(self, inputs) —> call(self, input, hidden) 随意的进行了变化, 这将会导致使用 build() 以及后续部署操作带来不可预计的Bug, 因此在官方 中给出的建议是,如果要传入多个变量时,请传入tuple,然后进行解包操作 来获取相关变量,就如Encoder中的call 一样。这种操作, 可能会在IDE coding 声明时 带来不方便,需要编程人员了解你的inputs到底包含的是什么。这就需要 对代码注释进行完善。
这一点也有可能,在TensorFlow2.0 后续版本得到改进。
这样一来,可以有效避免部分不可预计的Bug, 而且在build中也可以得到传入参数的shape。
如
1 2 3 4 5 6 7 8 9 10 class MyModel (tf.keras.models.Model): def __init__ (self ): super (MyModel, self).__init__() def build (self, inputs ): a_shape, b_shape =inputs print ("a_shape :" , a_shape) print ("b_shape :" , b_shape) def call (self, inputs ): a, b = inputs return a@b
1 2 3 4 5 6 7 8 9 10 11 12 a = tf.random.uniform((2 ,3 ), dtype=tf.float32) b = tf.random.uniform((3 ,2 ), dtype=tf.float32) m = MyModel() print (m((a, b)))输出为: a_shape: (2 , 3 ) b_shape: (3 , 2 ) tf.Tensor( [[0.21918826 0.5524787 ] [0.80377287 0.7006702 ]], shape=(2 , 2 ), dtype=float32)
BahdanauAttention Google 样例中采用的是BahdanauAttention,后续也可以改成和Transformer中相似的attention。具体的解释 可以参考上面的Google样例。
简单的说就是
将Decoder中gru上个时间t-1 的 状态state 与Encoder 输出enc_output 分别进行Dense全连接。
然后将 1 中两个全连接后,所得进行相加(利用广播机制)。
再通过tanh激活。
激活后的向量 再通过Dense层向1维进行映射,从而压缩矩阵
最后通过softmax得到score,
然后在利用的到的score 对enc_output进行加权求和,从而得到BahdanauAttention后的特征向量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class BahdanauAttention (tf.keras.layers.Layer): def __init__ (self, units ): super (BahdanauAttention, self).__init__() self.W1 = tf.keras.layers.Dense(units) self.W2 = tf.keras.layers.Dense(units) self.V = tf.keras.layers.Dense(1 ) def call (self, inputs ): query, values = inputs hidden_with_time_axis = tf.expand_dims(query, 1 ) score = self.V(tf.nn.tanh(self.W1(values) + self.W2(hidden_with_time_axis))) attention_weights = tf.nn.softmax(score, axis=1 ) contex_vector = attention_weights * values contex_vector = tf.reduce_sum(contex_vector, axis=1 ) return contex_vector, attention_weights
Decoder Decoder中 ,初始输入的hidden, enc_output均是Encoder中的输出,初始输入x 为标记id.
简单来讲,Decoder 就是 将Encoder中的enc_output和 decoder中gru的上一个时间步t-1 的state 进行attention,将得到的特征 与 输入x经过Embedding得到的向量进行 拼接处理,作为这次gru时间步t 时的输入,在将时间t时的输出 进行全连接Dense向vocab_size进行映射,找到argmax id从而就是 预测的单词。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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) self.attention = BahdanauAttention(self.dec_units) def call (self, inputs ): x, hidden, enc_output = inputs context_vector, attention_weights = self.attention((hidden, enc_output)) x = self.embedding(x) x = tf.concat([tf.expand_dims(context_vector, 1 ), x], axis=-1 ) output, state = self.gru(x) output = tf.reshape(output, (-1 , output.shape[2 ])) x = self.fc(output) return x, state, attention_weights
Train 在前期训练过程中,我们引入了Teacher forcing 技巧。就是在Decoder gru中,原本每个时间t时刻,传入的是上个时间t-1时刻的预测输出,改为了传入的是实际真实 所期待的输出字符id 作为输入。这样可以在训练时避免由于上个时间t-1时刻预测错误,导致后面形成滚雪球效应,造成一连串的错误,从而使得模型收敛过慢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @tf.function def train_step (inp, targ, enc_hidden ): loss = 0 with tf.GradientTape() as tape: enc_output, enc_hidden = encoder((inp, enc_hidden)) dec_hidden = enc_hidden dec_input = tf.expand_dims([targ_lang_tokenizer.word_index['<start>' ]] * BATCH_SIZE, 1 ) for t in range (1 , targ.shape[1 ]): predictions, dec_hidden, _ = decoder((dec_input, dec_hidden, enc_output)) loss += loss_function(targ[:, t], predictions) dec_input = tf.expand_dims(targ[:, t], 1 ) batch_loss = (loss / int (targ.shape[1 ])) variables = encoder.trainable_variables + decoder.trainable_variables gradients = tape.gradient(loss, variables) optimizer.apply_gradients(zip (gradients, variables)) train_loss(batch_loss)
Save model 通过训练好后,我们可以分别对Encoder,Decoder进行保存。
1 2 3 version = '1' encoder.save('path/encoder_zh/' +version) decoder.save('path/decoder_zh/' +version)
保存后,可看见产生如下文件,接下来我们就可以开始 部署了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /Users/lollipop/Documents/tf2/learn/encoder_zh └── 1 ├── assets ├── saved_model.pb └── variables ├── variables.data-00000-of-00002 ├── variables.data-00001-of-00002 └── variables.index /Users/lollipop/Documents/tf2/learn/decoder_zh └── 1 ├── assets ├── saved_model.pb └── variables ├── variables.data-00000-of-00002 ├── variables.data-00001-of-00002 └── variables.index
在部署前,在终端输入下面命令,可以查看encoder decoder保存的一些信息。
1 saved_model_cli show --dir /Users/lollipop/Documents/tf2/learn/encoder_zh/1 --tag_set serve --signature_def serving_default
输出如下:
在输出中我们 可以看见一些input_1, input_2, ouput_1, output_2 等信息,这些在我们 通过rest 向服务端进行信息传输时需要用到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 The given SavedModel SignatureDef contains the following input(s): inputs['input_1'] tensor_info: dtype: DT_INT32 shape: (-1, 46) name: serving_default_input_1:0 inputs['input_2'] tensor_info: dtype: DT_FLOAT shape: (-1, 1024) name: serving_default_input_2:0 The given SavedModel SignatureDef contains the following output(s): outputs['output_1'] tensor_info: dtype: DT_FLOAT shape: (-1, 46, 1024) name: StatefulPartitionedCall:0 outputs['output_2'] tensor_info: dtype: DT_FLOAT shape: (-1, 1024) name: StatefulPartitionedCall:1 Method name is: tensorflow/serving/predict
Deploy 部署前默认已经安装的Docker 和tensorflow/serving的docker镜像。
如果不了解,可以查看以前的文章docker+tensorflow/serving
Deploy in docker 打开两个终端,分别输入下面命令,将模型部署在docker上
1 # docker run -p 8501:8501 --name encoder --mount source =path/encoder_zh,type =bind ,target=/models/encoder -e MODEL_NAME=encoder -t tensorflow/servien
1 # docker run -p 8502:8501 --name decoder --mount source =path/decoder_zh,type =bind ,target=/models/decoder -e MODEL_NAME=decoder -t tensorflow/serving
–name 指定 部署项目的名字
-p 指定端口映射
source 模型保存位置
后面对应的参数进行修改
部署成功后
通过
进行查看
1 2 3 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES eea666daa1ba tensorflow/serving "/usr/bin/tf_serving…" 24 hours ago Up 24 hours 8500 /tcp, 0.0 .0 .0 :8502 ->8501 /tcp decoder 5f2c5ee249ef tensorflow/serving "/usr/bin/tf_serving…" 24 hours ago Up 24 hours 8500 /tcp, 0.0 .0 .0 :8501 ->8501 /tcp encoder
Load model 对Docker部署的模型进行通信前,我们先加载已保存的Encoder,Decoder模型。
1 2 3 4 load_en = tf.saved_model.load('/Users/lollipop/Documents/tf2/learn/encoder_zh/1' ) encoder = load_en.signatures['serving_default' ] load_de = tf.saved_model.load('/Users/lollipop/Documents/tf2/learn/decoder_zh/1' ) decoder = load_de.signatures['serving_default' ]
加载后,我们可以通过encoder.inputs, encoder.outputs 查看相应的参数对应关系。
1 2 3 4 5 6 [<tf.Tensor 'input_1:0' shape=(None , 46 ) dtype=int32>, <tf.Tensor 'input_2:0' shape=(None , 1024 ) dtype=float32>, <tf.Tensor 'statefulpartitionedcall_args_2:0' shape=<unknown> dtype=resource>, <tf.Tensor 'statefulpartitionedcall_args_3:0' shape=<unknown> dtype=resource>, <tf.Tensor 'statefulpartitionedcall_args_4:0' shape=<unknown> dtype=resource>, <tf.Tensor 'statefulpartitionedcall_args_5:0' shape=<unknown> dtype=resource>]
1 2 [<tf.Tensor 'Identity:0' shape=(None , 46 , 1024 ) dtype=float32>, <tf.Tensor 'Identity_1:0' shape=(None , 1024 ) dtype=float32>]
Rest client 这里我使用的是Rest进行通信,比较简单。至于grpc,感觉有点复杂,就没有详细接触。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import tensorflow as tfimport jsonimport data_processimport requestsunits = 1024 encoder_ref = 'http://localhost:8501/v1/models/encoder:predict' decoder_ref = 'http://localhost:8502/v1/models/decoder:predict' input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer = data_process.load_dataset('cmn.txt' ) max_length_inp, max_length_tar = data_process.max_length(input_tensor), data_process.max_length(target_tensor) ''' 其中值得注意的 是 instance处,[]里面是每个实例。 也就是说[]里面的 每一个大括号对应一个预测样本,因此 输入的 维度不包括example_size, 如 有2个30维的图片,正常的表示为 (2 30 30 3), 在传入时,就应转换成 "instance":[{"input":( 30 30 3)},{"input":( 30 30 3)}] 每个大括号代表一个样例 instance 中的 input_1, input_2, 以及output_1 output_2,是根具model的输入 输出 顺序来的。 如果不清楚可以 在加载模型后 通过 encoder.inputs encoder.outputs 来进行查看 在终端中 输入以下命令 将模型部署 映射到8501端口 docker run -p 8501:8501 --name encoder --mount source=path/encoder_zh,type=bind,target=/models/encoder -e MODEL_NAME=encoder -t tensorflow/serving encoder 的rest_client ''' def encoder_rest (input , hidden ): data = json.dumps({"instances" : [{"input_1" : input .numpy().tolist(), "input_2" : hidden.numpy().tolist()}]}) json_response = requests.post(encoder_ref, data=data) predictions = json.loads(json_response.text)['predictions' ] en_output = predictions[0 ]['output_1' ] en_state = predictions[0 ]['output_2' ] return en_state, en_output def decoder_rest (x, en_hidden, en_output ): data = json.dumps({"instances" : [{"input_1" : x.numpy().tolist(), "input_2" : en_hidden, "input_3" : en_output}]}) json_response = requests.post(decoder_ref, data=data) predictions = json.loads(json_response.text)['predictions' ] x = predictions[0 ]['output_1' ] state = predictions[0 ]['output_2' ] attention_weights = predictions[0 ]['output_3' ] return x, state, attention_weights def translate (sentence ): sentence = data_process.preprocess_sentence_zh(sentence) inputs = [inp_lang_tokenizer.word_index[i] for i in sentence.split()] inputs = tf.keras.preprocessing.sequence.pad_sequences([inputs], maxlen=max_length_inp, padding='post' ) inputs = tf.convert_to_tensor(inputs) result = '' hidden = tf.zeros((1 , units)) en_state, en_out = encoder_rest(inputs[0 ], hidden[0 ]) de_inputs = tf.expand_dims([targ_lang_tokenizer.word_index['<start>' ]], 0 ) de_input = de_inputs[0 ] decoder_rest(de_input, en_state, en_out) de_hideen = en_state result = '' for t in range (max_length_tar): x, state, _ = decoder_rest(de_input, de_hideen, en_out) prediction_id = tf.argmax(x, axis=-1 ).numpy() if targ_lang_tokenizer.index_word[prediction_id] == '<end>' : break result += targ_lang_tokenizer.index_word[prediction_id] + ' ' de_input = tf.expand_dims([prediction_id], 0 )[0 ] de_hideen = state return result sentence = '我喜欢你!' print (sentence)print (translate(sentence))
输出如下:
[code]
在Google教程代码下,复现的小小起步,后面会接着复现Transformer,Bert , Transformer-XL·········