一、模型定義
NJUNMT中的模型分為Sequence2Sequence和Transformer兩種。兩種模型總體上都可以看做是Encoder-Decoder模型,但使用的具體encoder和decoder的類型存在不同。Seq2Seq模型使用RNN,而Transformer不使用RNN,依賴于attention機制。

1. Sequence2Sequence
這一部分介紹NJUNMT中seq2seq模型的主要構(gòu)成部件。
seq2seq模型配置示例:
#njunmt/example_configs/toy_seq2seq.yml
model: njunmt.models.SequenceToSequence
model_params:
'''是否對所有fflayer進行歸一化'''
fflayers.layer_norm: true
'''源語言embedding維度'''
embedding.dim.source: 8
'''目標語言embedding維度'''
embedding.dim.target: 8
modality.params:
multiply_embedding_mode:
'''各層是否共享embedding和softmax權(quán)重?'''
share_embedding_and_softmax_weights: false
'''logits的保留率 1.0為不進行dropout'''
dropout_logit_keep_prob: 1.0
'''損失函數(shù)'''
loss: crossentropy
timing:
'''初始激活值'''
initializer: random_uniform
...
1.1 Embedding
Embedding(詞嵌入)到底是什么
神經(jīng)網(wǎng)絡(luò)中的embedding層的參數(shù)是一個矩陣,跟整個模型一起訓(xùn)練得來。矩陣乘以某個詞的獨熱向量得到這個詞的詞向量表示。
NJUNMT中用的是經(jīng)過n次封裝后的tf.nn.embedding_lookup()
詳解TF中的Embedding操作!
1.1 Encoder
seq2seq模型的encoder配置示例:
model_params:
...
"""指定encoder類型"""
encoder.class: njunmt.encoders.BiUnidirectionalRNNEncoder
encoder.params:
rnn_cell:
"""指定rnn cell類型,可以是LSTMCell/GRUCell"""
cell_class: GRUCell
cell_params:
"""每個cell中神經(jīng)元的數(shù)量"""
num_units: 9
"""input 的保留率 1.0為不進行dropout"""
dropout_input_keep_prob: 0.9
"""input 的保留率 1"""
dropout_state_keep_prob: 1.0
"""RNN層數(shù)"""
num_layers: 2
...
seq2seq模型使用RNN encoder。njunmt/encoders/rnn_encoder.py中定義了三種encoder類型:
UnidirectionalRNNEncoder:單向多層RNN
StackBidirectionalRNNEncoder:雙向多層RNN
BiUnidirectionalRNNEncoder:底層為雙向RNN,上層為雙向多層RNN
*疑問:為什么要使用這樣的RNN架構(gòu)?
Encoder類均包含encode()方法:
def encode(inputs, sequence_length, **kwargs):
outputs, states = tf.nn.dynamic_rnn(...)
return (outputs, final_states)
tf.nn.dynamic_rnn()方法運行已創(chuàng)建RNN cell,dynamic在于不同batch傳入的sequence_length可以不同,每一個batch內(nèi)動態(tài)進行padding.
Sequence2Sequence類中的_encode()方法:
對input進行embedding,然后調(diào)用Encoder的encode()方法
def _encode(self, input_fields):
features = self._input_to_embedding_fn(...)
encoder_output = self._encoder.encode(features, feature_length)
return encoder_output
1.2 Bridge
銜接encoder final state和decoder initial state的東西。
njunmt/utils/bridges.py中定義了4種Bridge:
ZeroBridge:decoder initial state 為全0
PassThroughBridge:直接將encoder final state傳給decoder
InitialStateBridge:用一層全連接網(wǎng)絡(luò)對encoder final state進行映射再傳給decoder
Variable Bridge:通過學(xué)習(xí)決定decoder initial state
1.3 Decoder
seq2seq模型使用RNN decoder。njunmt/decoders/rnn_decoder.py中定義了三種decoder類型:
SimpleDecoder:多層RNN,無Attention機制
AttentionDecoder:多層RNN,使用concat(decoder_input, attention_context)作為輸入
CondAttentionDecoder:底層是使用decoder_input作為輸入的單層RNN,上層是使用attention_context作為輸入的多層RNN
Decoder類的decode()方法主要是調(diào)用了dynamic_decode()方法:
def dynamic_decode(decoder,
encoder_output,
bridge,
helper,
target_to_embedding_fn,
outputs_to_logits_fn,
parallel_iterations=32,
swap_memory=False,
**kwargs):
...
'''一些初始化,包括把target input 變成embedding'''
with tf.variable_scope(decoder.name):
'''調(diào)用decoder.prepare()方法:
1. 用bridge初始化decoder的states,
2. 從encoder_output獲得attention(如果需要)'''
initial_cache = decoder.prepare(encoder_output, bridge, helper)
if decoder.mode == ModeKeys.INFER:
assert "beam_size" in kwargs
beam_size = kwargs["beam_size"]
initial_cache = stack_beam_size(initial_cache, beam_size)
"""while_loop中的循環(huán)體"""
def body_traininfer(time, inputs, cache, outputs_ta,
finished, *args):
with tf.variable_scope(decoder.name):
'''調(diào)用decoder.step()方法'''
outputs, next_cache = decoder.step(inputs, cache)
outputs_ta = nest.map_structure(lambda ta, out: ta.write(time, out),
outputs_ta, decoder_output_remover.apply(outputs))
inner_loop_vars = [time + 1, None, None, outputs_ta, None]
sample_ids = None
if decoder.mode == ModeKeys.INFER:
log_probs, lengths = args[0], args[1]
bs_stat_ta = args[2]
predicted_ids = args[3]
with tf.variable_scope(self.name):
decoder_top_features = self.merge_top_features(ret_val)
logits = outputs_to_logits_fn(decoder_top_features)
# sample next symbols
sample_ids, beam_ids, next_log_probs, next_lengths \
= helper.sample_symbols(logits, log_probs, finished, lengths, time=time)
predicted_ids = gather_states(tf.reshape(predicted_ids, [-1, time + 1]), beam_ids)
next_cache["decoding_states"] = gather_states(next_cache["decoding_states"], beam_ids)
bs_stat = BeamSearchStateSpec(
log_probs=next_log_probs,
beam_ids=beam_ids)
bs_stat_ta = nest.map_structure(lambda ta, out: ta.write(time, out),
bs_stat_ta, bs_stat)
next_predicted_ids = tf.concat([predicted_ids, tf.expand_dims(sample_ids, axis=1)], axis=1)
next_predicted_ids = tf.reshape(next_predicted_ids, [-1])
next_predicted_ids.set_shape([None])
inner_loop_vars.extend([next_log_probs, next_lengths, bs_stat_ta, next_predicted_ids])
next_finished, next_input_symbols = helper.next_symbols(time=time, sample_ids=sample_ids)
next_inputs = target_to_embedding_fn(next_input_symbols, time + 1)
next_finished = tf.logical_or(next_finished, finished)
inner_loop_vars[1] = next_inputs
inner_loop_vars[2] = next_cache
inner_loop_vars[4] = next_finished
return inner_loop_vars
loop_vars = [initial_time, initial_inputs, initial_cache,
initial_outputs_ta, initial_finished]
if decoder.mode == ModeKeys.INFER:
...'''增加一些infer需要的參數(shù)'''
'''while(cond(args)) loop(body(args))
cond: tf.logical_not(tf.reduce_all(initial_finished))
tf.logical_not:布爾值(組成的張量)not運算
tf.reduce_all:計算張量在維度上的邏輯和(按某軸向全部取and)
finished為布爾張量 記錄什么東西結(jié)束了
循環(huán)到所有finished標記為真為止'''
conf = lambda *args: tf.logical_not(tf.reduce_all(args[4]))
res = tf.while_loop(
conf,
body_traininfer,
loop_vars=loop_vars,
parallel_iterations=parallel_iterations,
swap_memory=swap_memory)
...'''從res中取出final_outputs'''
if decoder.mode == ModeKeys.INFER:
...'''return final_outputs和一些state'''
return final_outputs
decoder output-(merge)->logits-(softmax)->
*這部分比較復(fù)雜,還有很多沒看懂
Sequence2Sequence類中的_decode()方法:
def _decode(self, encoder_output, input_fields):
if '''mode == TRAIN or EVAL''':
...'''準備label和helper'''
else: ''' mode == INFER'''
...'''準備helper'''
'''調(diào)用Decoder的decode()方法'''
decoder_output, decoding_res = self._decoder.decode(
encoder_output, bridge, helper,
self._target_to_embedding_fn,
self._outputs_to_logits_fn,
beam_size=...)
return decoder_output, decoding_res
1.3 Attention
如果不使用注意力機制,那么decoder在每一個時間片使用的都是同一個上下文向量(Context),即encoder output。
在有注意力機制的seq2seq模型中,針對decoder的每個時間片,有不同的Context,每個Context分配給encoder隱藏層狀態(tài)(h)的各時間片的權(quán)重不同。

注意力機制使得decoder第i個時間片的輸出不僅僅關(guān)注相對應(yīng)的encoder第j個時間片的隱藏層狀態(tài),還可以將前后的隱藏層狀態(tài)以一定權(quán)重輸入進來。
上圖中的α即attention weight,根據(jù)encoder的隱藏層狀態(tài)(h)和decoder的隱藏層狀態(tài)(H)得到。在不同種類的注意力機制中,attention weight有不同的計算方法。
1.3.1 加法注意力
BahnandauAttention中使用的是加法注意力:用一個全連接層得到attention score(logits,即下圖中的e),然后softmax得到attention weight(下圖中的α)

另一種常見的字母表達方式:把上式中decoder隱藏層狀態(tài)H替換為Q(query),encoder隱藏層狀態(tài)h替換為K(keys),即為:
attention_weight = softmax(tanh(W1*Q+W2*K))
1.3.2 乘法注意力
其他注意力模型中用到的是乘法注意力:乘法注意力不用使用一個全連接層,所以空間復(fù)雜度占優(yōu);另外由于乘法可以使用優(yōu)化的矩陣乘法運算,所以計算上也一般占優(yōu)。

把上式中decoder隱藏層狀態(tài)H替換為Q(query),encoder隱藏層狀態(tài)h替換為K(keys),即為:
attention_weight = softmax(dot_product(Q, K))
Attention的看這里不要看上面
[深度概念]·Attention機制實踐解讀
#in encoder
EO, _ = encoder(...)
attention_values = EO
attention_keys = FC(EO)
#in decoder
attention_query = cell_output
#in attention.build()
query = FC(attention_query)
keys = attention_keys
memory = attention_values
score = att_fn(query, keys)#att_fn可以是加法乘法等
attention_weight = softmax(score)
context = attention_weight * memory
#then use context to infer

關(guān)于注意力的計算詳細:
提出Transformer的《Attention is all you need》論文原文
《attention is all you need》解讀
2. Transformer

Transformer模型簡介
*以下來自:Transformer for NMT
總的來說,模型由encoder&decoder兩部分組成,左側(cè)的Stage 2,3構(gòu)成一層的encoder,右側(cè)的Stage 2,3,4構(gòu)成一層decoder, 在每一層中的每個Stage稱為一個子層(sublayer). 在transformer中,encoder和decoder同樣可以堆疊N個來構(gòu)建deeper model,論文中使用了6個encoders和6個decoders. 除此之外再加入輸入embedding,位置編碼(positional encoding)和輸出的dense layer,就是完整的transformer. 下面我們具體地分析各部分結(jié)構(gòu)。
*Transformer很復(fù)雜,還沒仔細看
二、運行過程
2.1 一些TF基礎(chǔ)知識
tensorflow中的Graph(圖)和Session(會話)的關(guān)系(大盤雞與紅燒肉)
TensorFlow 中的幾個關(guān)鍵概念:Tensor,Operation,Graph,Session
tensorflow中的Session()和run()
Hook? tf.train.SessionRunHook()介紹
*一個相似完整流程參考
[tf]使用attention機制進行NMT
NJUNMT
bin/train.py中實例化了一個TrainingExperiment對象,帶入模型配置運行訓(xùn)練。
training_runner = TrainingExperiment(
model_configs=model_configs)
training_runner.run()
TrainingExperiment的run()方法定義:
#njunmt/nmt_experiment.py
def run(self):
""" 建立源語言與目標語言詞匯表"""
vocab_source = Vocab(...)
vocab_target = Vocab(...)
eval_dataset = {
"vocab_source": vocab_source,
"vocab_target": vocab_target,
"features_file": self._model_configs["data"]["eval_features_file"],
"labels_file": self._model_configs["data"]["eval_labels_file"]}
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
config.allow_soft_placement = True
"""model_fn()建立模型"""
estimator_spec = model_fn(...)
train_ops = estimator_spec.train_ops
hooks = estimator_spec.training_hooks
"""build training session"""
sess = tf.train.MonitoredSession(
session_creator=tf.train.ChiefSessionCreator(
scaffold=tf.train.Scaffold(),
checkpoint_dir=None,
master="",
config=config),
hooks=tuple(hooks) + tuple(build_eval_metrics
(self._model_configs, eval_dataset,
model_name=estimator_spec.name)))
...
"""somehow 得到處理好的train data"""
train_data = ...
eidx = [0, 0]
update_cycle = [self._model_configs["train"]["update_cycle"], 1]#[目標cycle數(shù),當(dāng)前cycle數(shù)]
def step_fn(step_context):
step_context.session.run(train_ops["zeros_op"])
try:
while update_cycle[0] != update_cycle[1]:
data = train_data.next()
step_context.session.run(
train_ops["collect_op"], feed_dict=data["feed_dict"])
update_cycle[1] += 1
data = train_data.next()
update_cycle[1] = 1
return step_context.run_with_hooks(
train_ops["train_op"], feed_dict=data["feed_dict"])
except StopIteration:
eidx[1] += 1
while not sess.should_stop():
if eidx[0] != eidx[1]:
tf.logging.info("STARTUP Epoch {}".format(eidx[1]))
eidx[0] = eidx[1]
sess.run_step_fn(step_fn)
*疑問:這里的sess.run_step_fn(step_fn)是怎么運行的?tf.train.MonitoredSession()類中沒有查到run_step_fn()方法
關(guān)于tf.train.MonitoredSession()類
官方文檔給的定義是:
Session-like object that handles initialization, recovery and hooks.
是一個處理初始化,模型恢復(fù),和處理Hooks的類似于Session的類。
關(guān)于這里的Experiment,training_hooks都還缺乏了解
關(guān)于tf.estimator
Estimator對象包裝由model_fn指定的模型,其中,給定輸入和其他一些參數(shù),返回需要進行訓(xùn)練、計算,或預(yù)測的操作.
EstimatorSpec是定義model_fn返回的類(繼承自namedtuple),用于初始化Estimator
#njunmt/model/model_builder.py
class EstimatorSpec(
namedtuple('EstimatorSpec', ['name', 'input_fields',
'predictions', 'loss', 'train_ops',
'training_chief_hooks', 'training_hooks'])):
"""創(chuàng)建NMT模型,根據(jù)train,evaluation,inference三種mode具體有不同的返回內(nèi)容"""
def model_fn(...):
...
"""創(chuàng)建模型實例"""
model = eval(model_str)(
params=model_configs["model_params"],...)
...
"""調(diào)用各Model類的build()方法"""
def _build_model():
...
if mode == ModeKeys.INFER:
# model_output is prediction
return _input_fields, _model_output
elif mode == ModeKeys.EVAL:
# model_output = (loss_sum, weight_sum), attention
return _input_fields, _model_output[0], _model_output[1]
elif mode == ModeKeys.TRAIN: # mode == TRAIN
..."""something for train"""
return _input_fields, _loss, grads
...
model_returns = parallelism(_build_model)
input_fields = model_returns[0]
if mode == ModeKeys.INFER:
..."""something for inference"""
return EstimatorSpec(...)
if mode == ModeKeys.EVAL:
... """something for evaluation"""
return EstimatorSpec(...)
assert mode == ModeKeys.TRAIN
..."""something for train"""
return EstimatorSpec(...)
三、調(diào)參基礎(chǔ)
機器學(xué)習(xí)中的正則化(Regularization)
Normalization(歸一化)
《理解Dropout》分享
Seq2Seq中的beam search算法
Tensorflow中優(yōu)化器--AdamOptimizer詳解

TensorFlow之RNN:堆疊RNN、LSTM、GRU及雙向LSTM
[深度概念]·Attention機制實踐解讀
變形金剛”為何強大:從模型到代碼全面解析Google Tensor2Tensor系統(tǒng)