
關(guān)鍵詞:seq2seq,RNN,LSTM,NER依存關(guān)系
命名實(shí)體識別(Named Entity Recognization, NER)是AI任務(wù)中重要的一類,而且在技術(shù)落地方面已經(jīng)走在各種應(yīng)用的前列,通過命名實(shí)體識別,我們已經(jīng)能夠識別出諸如 “我 去 五道口 吃 肯德基” 這句話中的地址(五道口)和餐館(肯德基),利用這個(gè)信息,我們就可以給用戶展示五道口的導(dǎo)航信息,和肯德基的餐館信息等。目前在各種智能手機(jī)上已經(jīng)廣泛集成了該功能,如小米的傳送門,Oppo/Vivo的智慧識屏等。但是NER識別有個(gè)局限,我們只能識別出獨(dú)立的實(shí)體,實(shí)際上一句話中不同實(shí)體間很多時(shí)候是存在關(guān)聯(lián)的,比如上面的例句中“五道口”這個(gè)地址就限制了“肯德基”餐館的位置,所以我們就知道用戶想搜索的是五道口的那家肯德基,而不是其他地方的肯德基,那我們?nèi)绾握页鲞@些實(shí)體間的關(guān)系,本文將利用seq2seq模型進(jìn)行獲取。
之前讀過很多文章,它們介紹了各種各樣的seq2seq模型,但是始終沒找到一個(gè)從理論到實(shí)踐能完全串聯(lián)起來的文章,總是讓人覺得云里霧里,似懂非懂。本文試圖通過以下三個(gè)部分的講解,提供一個(gè)從理論到實(shí)踐的完整連貫的介紹:
- 首先介紹seq2seq模型的理論基礎(chǔ),包括循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)和長短時(shí)記憶網(wǎng)絡(luò)(LSTM)。
- 講解針對NER依存關(guān)系這個(gè)問題,我們怎么進(jìn)行建模。
- 最后結(jié)合代碼介紹如何實(shí)現(xiàn)seq2seq模型。
seq2seq理論基礎(chǔ)
seq2seq模型是一種機(jī)器學(xué)習(xí)領(lǐng)域常用的模型,適用于將一個(gè)序列轉(zhuǎn)換成另外一種序列的問題,可以是將一種語言翻譯成另一種語言,將一篇文章轉(zhuǎn)換成一段摘要,將一段語音轉(zhuǎn)換成文字,又或者是將一句話的命名實(shí)體序列轉(zhuǎn)換成實(shí)體間的關(guān)系序列。seq2seq模型通過循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)實(shí)現(xiàn),循環(huán)神經(jīng)網(wǎng)絡(luò)可以記錄序列前面幾步的信息,從而推算下一步的輸出。
一個(gè)簡單的RNN Cell可以表示如下:

或者等效展開如下:

如果把神經(jīng)網(wǎng)絡(luò)的內(nèi)部結(jié)構(gòu)畫出來,會(huì)是下面的結(jié)構(gòu):

這里,依次輸入“我 去 五道口 吃 肯德基”每個(gè)單詞的詞嵌入向量,每一步都會(huì)輸出一個(gè)隱藏狀態(tài)(hidden state)。在計(jì)算某一步輸出的隱藏狀態(tài)的時(shí)候,會(huì)結(jié)合前一步的輸出,生成一個(gè)新的隱藏狀態(tài)。這樣,每一步生成的隱藏狀態(tài)相當(dāng)于包含了前面所有步驟的信息,這個(gè)步驟稱為編碼(Encoder),最后一步輸出的隱藏狀態(tài)Ht就可以作為整個(gè)輸入序列的表示,參與下一步的解碼(Decoder)過程。
理論上RNN網(wǎng)絡(luò)結(jié)構(gòu)能夠包含輸入序列的所有信息,但是實(shí)際上它只能記住當(dāng)前附近的幾步輸入的信息,隨著距離的增加,RNN能記住的有效信息越來越少,這個(gè)有點(diǎn)兒類似狗熊掰棒子,記住了最近的信息,忘掉了之前的信息。對于只需要最近幾步的依賴(短距離依賴)就可以完成的工作,RNN可以勝任,比如“下雨天我需要一把雨傘”,根據(jù)這句話猜測粗體的部分的“雨傘”,由于整個(gè)句子比較短,RNN網(wǎng)絡(luò)需要分析的前后文距離比較短,可以解決這種問題。換一句話,“天氣預(yù)報(bào)今天下雨,我要出遠(yuǎn)門,.....,我需要一把雨傘”,在這句話中,由于最后的雨傘需要依賴句子開頭的“下雨”才能分析出來,距離很長,這種情況下RNN網(wǎng)絡(luò)就捉襟見肘了。此時(shí)需要一種能夠長距離記錄信息的網(wǎng)絡(luò),這種網(wǎng)絡(luò)是長短時(shí)記憶網(wǎng)絡(luò)(Long-Short term memory, LSTM)。
相比于上面的RNN內(nèi)部結(jié)構(gòu)包含的單層的神經(jīng)網(wǎng)絡(luò),LSTM結(jié)構(gòu)更加復(fù)雜,共包含四層神經(jīng)網(wǎng)絡(luò):

在LSTM網(wǎng)絡(luò)結(jié)構(gòu)中,四層神經(jīng)網(wǎng)絡(luò)分為三個(gè)部分,紅框表示的遺忘門(forget gate),藍(lán)框表示的輸入門(input gate),和綠框表示的輸出門(output gate),它們分別控制如何將之前的記憶刪除一部分,如何加入當(dāng)前的記憶,如何將整合后的記憶和這一步的輸入聯(lián)合起來計(jì)算一個(gè)輸出。圖中兩條水平向右的線,上面的叫CellState,可以認(rèn)為是承載著前面遙遠(yuǎn)記憶的一條傳送帶,下面的叫HiddenState,是結(jié)合了當(dāng)前輸入,前一步輸出,以及遙遠(yuǎn)記憶后的輸出。當(dāng)一句話的所有單詞都經(jīng)過LSTM網(wǎng)絡(luò)處理后,最后輸出的HiddenState Ht就是Encoder編碼過程的輸出,包含了整個(gè)輸入序列的信息。
上面給出的是基本的LSTM網(wǎng)絡(luò)結(jié)構(gòu),針對LSTM還有很多人提出了很多變種,如下圖所示,此處不再一一介紹。



理解了上面的LSTM網(wǎng)絡(luò)結(jié)構(gòu),在看下面的seq2seq整體結(jié)構(gòu)就很容易理解了:

NER依存關(guān)系建模
有了上面的理論知識,我們就可以針對實(shí)際問題進(jìn)行建模。我們的目的是將輸入的一句話中實(shí)體間的關(guān)系提取出來。
輸入:
我(O) 去(O) 惠新西街甲8號(ADDR) 的(O) 星巴克(CATER) 喝(O) 咖啡(O),預(yù)訂(O) 電話(O) 18701500685(PHONE_NUM)
我們在這句話分詞后面給出了每個(gè)單詞的實(shí)體類型,其中ADDR代表地址,CATER代表餐館,O代表未識別的其他類型。實(shí)體的類型作為輸入的特征向量之一,連同每個(gè)單詞的次嵌入向量一并作為LSTM網(wǎng)絡(luò)的輸入。
上面的一句話中實(shí)體關(guān)系表如下:
| 惠新西街甲8號 | 星巴克 | 18701500685 | |
|---|---|---|---|
| 惠新西街甲8號 | - | right_desc | null |
| 星巴克 | - | - | left_desc |
| 星巴18701500685 | - | - | - |
按照順序,每個(gè)實(shí)體依次和其他實(shí)體產(chǎn)生一個(gè)關(guān)系,比如我們認(rèn)為惠新西街甲8號是對星巴克的描述,那我們可以定義這種關(guān)系為right_desc(右側(cè)描述),惠新西街甲8號和18701500685沒有關(guān)系,我們定義為null, 18701500685也是對星巴克的描述,所以星巴克和18701500685的關(guān)系定義為left_desc(左側(cè)描述)。這樣,對于有N個(gè)非O類型的實(shí)體,它們之間的關(guān)系數(shù)是N*(N-1)/2個(gè),我們就可以把兩兩之間的關(guān)系按照順序作為輸出序列:
輸出:
right_desc null left_desc
這樣就轉(zhuǎn)換成了一個(gè)標(biāo)準(zhǔn)的seq2seq問題。
輸入向量我們使用預(yù)訓(xùn)練的word embedding,尺寸是500000行128列,代表500000個(gè)單詞,每個(gè)單詞用128維向量表示。同時(shí),我們將實(shí)體類型也用數(shù)字表示,加入到128維后面,所以每個(gè)單詞用129維的向量表示。
代碼實(shí)現(xiàn)
首先構(gòu)造編碼器:
# 輸入序列第一部分:單詞的embedding (batch_size, 50, 128)
self.sentence_words_emb = tf.nn.embedding_lookup(self.encoder_embedding, self.input_sentence_words_ids)
# 輸入序列第二部分:單詞的ner類型 (batch_size, 50) -> (batch_size, 50, 1)
self.input_sentence_ner_expand = tf.expand_dims(self.input_sentence_ner_ids, 2, name='expand_dims_tag')
# 兩部分合并起來作為輸入序列 (batch_size, 50, 128+1)
self.input_feature = tf.concat([self.sentence_words_emb, self.input_sentence_ner_expand], 2)
# 構(gòu)建單個(gè)的LSTMCell,同時(shí)添加了Dropout信息
self.encode_cell = self.build_encoder_cell()
encode_input_layer = Dense(self.hidden_units, dtype=tf.float32, name='input_projection')
self.encoder_inputs_embedded = encode_input_layer(self.input_feature)
# 將這個(gè)embedding信息作為tf.nn.dynamic_rnn的輸入
# encoder_outputs:[h_0, h_1, ..., h_t] encoder_output_state: LSTMStateTuple(c_t, h_t)
self.encoder_outputs, self.encoder_output_state = tf.nn.dynamic_rnn(cell=self.encode_cell,
inputs=self.encoder_inputs_embedded,
sequence_length=self.encode_inputs_length,
# 存儲(chǔ)每句話的實(shí)際長度
dtype=tf.float32,
time_major=False)
上面的代碼核心是調(diào)用tf.nn.dynamic_rnn函數(shù)進(jìn)行編碼,該函數(shù)的參數(shù)及含義如下:
cell:用于編碼的神經(jīng)網(wǎng)絡(luò)構(gòu)成,可以是單層RNNCell,也可以是多層RNNCell,這里我們使用的是MultiRNNCell,具體實(shí)現(xiàn)如下:
def build_encoder_cell(self):
return MultiRNNCell([self.build_encode_single_cell() for i in range(self.depth)])
def build_decode_single_cell(self):
cell = LSTMCell(self.hidden_units)
cell = DropoutWrapper(cell, dtype=tf.float32, output_keep_prob=self.keep_prob_placeholder)
return cell
inputs:輸入向量,我們將每個(gè)單詞的word embedding(128維)和ner類型(1維)結(jié)合起來,構(gòu)成輸入向量(129維)
sequence_length:batch里面每句話不考慮填充部分的實(shí)際長度矩陣
time_major:inputs和outputs Tensor的格式,如果是true,格式為[max_time, batch_size, depth],如果是false,格式為[batch_size, max_time, depth]。這里我們指定為false
dynamic_rnn函數(shù)返回兩個(gè)變量,第一個(gè)encoder_outputs是一個(gè)包含了編碼過程中每一步輸出的hidden_state的列表[h_0, h_1, ..., h_t] ,第二個(gè)變量是一個(gè)tuple類型,存儲(chǔ)的是編碼過程最后一步輸出的c_t和h_t,encoder_output_state: LSTMStateTuple(c_t, h_t)。其中h_t就是我們在解碼過程中的輸入,如果使用了Attention機(jī)制,還會(huì)用到hidden_state列表[h_0, h_1, ..., h_t] 。
解碼過程:
解碼過程要區(qū)分訓(xùn)練還是預(yù)測,訓(xùn)練的時(shí)候輸出結(jié)果是已知的,預(yù)測的時(shí)候是未知的。下面是訓(xùn)練階段的解碼代碼:
with tf.variable_scope('decoder'):
const = [[0], [1], [2], [3], [4], [5], [6], [7]] # decode embedding目前用的是一維的,回頭試試8維,16維或者64維
initializer = tf.constant_initializer(const)
self.decoder_embedding = tf.get_variable(name='decoder_embeddings', shape=[self.num_classes, 1],
initializer=initializer, dtype=tf.float32)
# 構(gòu)建輸出層全連接網(wǎng)絡(luò),輸出的類別數(shù)目是label的種類8
decoder_output_layer = Dense(self.num_classes, name='decoder_output_projection')
if self.mode == 'train':
decoder_cell, decoder_initial_state = self.build_decoder_cell()
# 將目標(biāo)結(jié)果轉(zhuǎn)換成對應(yīng)的embedding表示 (batch_size, decode_sentence_max_len) -> (batch_size, decode_sentence_max_len, 1)
decoder_results_embedded = tf.nn.embedding_lookup(self.decoder_embedding, self.targets_train) # tf.expand_dims(targets, 2)
# TrainingHelper用于在Decoder過程中自動(dòng)獲取每個(gè)batch的數(shù)據(jù)
training_helper = seq2seq.TrainingHelper(inputs=decoder_results_embedded,
sequence_length=self.train_decoder_results_length,
time_major=False,
name='training_helper')
training_decoder = seq2seq.BasicDecoder(cell=decoder_cell, # 加入Attention的decoder cell
helper=training_helper, # 獲取目標(biāo)輸出數(shù)據(jù)的helper函數(shù)
initial_state=decoder_initial_state,
# Encoder過程輸出的state作為Decoder過程的輸入State
output_layer=decoder_output_layer) # Decoder完成之后經(jīng)過全連接網(wǎng)絡(luò)映射到最終輸出的類別
# 獲取一個(gè)batch里面最長句子的長度
max_decoder_length = tf.reduce_max(self.train_decoder_results_length)
## 使用training_decoder進(jìn)行dynamic_decode操作,輸出decoder結(jié)果
decoder_outputs, _, _ = seq2seq.dynamic_decode(decoder=training_decoder,
impute_finished=True,
maximum_iterations=max_decoder_length)
# decoder_outputs = (rnn_outputs, sample_id)
# 其中:rnn_output: [batch_size, decoder_targets_length, vocab_size],保存decode每個(gè)時(shí)刻每個(gè)單詞的概率,可以用來計(jì)算loss
# sample_id: [batch_size, decode_vocab_size], tf.int32,保存最終的編碼結(jié)果,也就是rnn_output每個(gè)時(shí)刻概率最大值對應(yīng)的類別??梢员硎咀詈蟮拇鸢?
# 生成一個(gè)和decoder_logits.rnn_output結(jié)構(gòu)一樣的tensor,代表一次訓(xùn)練的結(jié)果
decoder_logits_train = tf.identity(decoder_outputs.rnn_output)
# 選擇logits的最大值的位置作為預(yù)測選擇的結(jié)果
self.decoder_pred_train = tf.argmax(decoder_logits_train, axis=-1, name='decoder_pred_train')
# 根據(jù)輸入batch中每句話的長度,和指定處理的最大長度,填充mask數(shù)據(jù),這樣可以提高計(jì)算效率,同時(shí)不影響最終結(jié)果
masks = tf.sequence_mask(lengths=self.train_decoder_results_length,
maxlen=max_decoder_length, dtype=tf.float32, name='masks')
# 計(jì)算loss
self.loss = seq2seq.sequence_loss(logits=decoder_logits_train, # 預(yù)測值
targets=self.targets_train, # 實(shí)際值
weights=masks, # mask值
average_across_timesteps=True,
average_across_batch=True, )
## 接下來手動(dòng)進(jìn)行梯度更新
# 首先獲得trainable variables
trainable_params = tf.trainable_variables()
# 使用gradients函數(shù),計(jì)算loss對trainable_params的導(dǎo)數(shù),trainable_params包含各個(gè)可訓(xùn)練的參數(shù)
gradients = tf.gradients(self.loss, trainable_params)
# 對可訓(xùn)練參數(shù)的梯度進(jìn)行正則化處理,將權(quán)重的更新限定在一個(gè)合理范圍內(nèi),防止權(quán)重更新過于迅猛造成梯度爆炸或梯度消失
clip_gradients, _ = tf.clip_by_global_norm(gradients, self.max_gradient_norm)
# 一次訓(xùn)練結(jié)束后更新參數(shù)權(quán)重
self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate).apply_gradients(
zip(clip_gradients, trainable_params), global_step=self.global_step)
訓(xùn)練過程的解碼通過seq2seq.dynamic_decode進(jìn)行,該函數(shù)的參數(shù)decoder我們使用BasicDecoder,BasicDecoder參數(shù)含義如下:
cell:解碼的網(wǎng)絡(luò)結(jié)構(gòu),該網(wǎng)絡(luò)我們在build_decoder_cell函數(shù)里生成
helper:如何在每一步獲取數(shù)據(jù)
initial_state:編碼過程輸出的h_t
output_layer:解碼數(shù)據(jù)轉(zhuǎn)換成最終識別類別的網(wǎng)絡(luò),這里我們使用Dense構(gòu)建了一個(gè)輸出數(shù)量為num_classes的全連接網(wǎng)絡(luò)
build_decoder_cell函數(shù)代碼如下:
def build_decoder_cell(self):
encoder_outputs = self.encoder_outputs
encoder_last_state = self.encoder_output_state
encoder_inputs_length = self.encode_inputs_length
# Building attention mechanism: Default Bahdanau
# 'Bahdanau' style attention: https://arxiv.org/abs/1409.0473
self.attention_mechanism = attention_wrapper.BahdanauAttention(
num_units=self.hidden_units, memory=encoder_outputs,
memory_sequence_length=encoder_inputs_length, )
# Building decoder_cell
self.decoder_cell_list = [self.build_decode_single_cell() for i in range(self.depth)]
def attn_decoder_input_fn(inputs, attention):
# Essential when use_residual=True
_input_layer = Dense(self.hidden_units, dtype=tf.float32, name='attn_input_feeding')
return _input_layer(tf.concat([inputs, attention], -1))
# AttentionWrapper wraps RNNCell with the attention_mechanism
# Note: We implement Attention mechanism only on the top decoder layer
self.decoder_cell_list[-1] = attention_wrapper.AttentionWrapper(
cell=self.decoder_cell_list[-1],
attention_mechanism=self.attention_mechanism,
attention_layer_size=self.hidden_units,
cell_input_fn=attn_decoder_input_fn,
initial_cell_state=encoder_last_state[-1],
alignment_history=False,
name='Attention_Wrapper')
# To be compatible with AttentionWrapper, the encoder last state
# of the top layer should be converted into the AttentionWrapperState form
# We can easily do this by calling AttentionWrapper.zero_state
# Also if beamsearch decoding is used, the batch_size argument in .zero_state
# should be ${decoder_beam_width} times to the origianl batch_size
batch_size = self.batch_size
initial_state = [state for state in encoder_last_state]
initial_state[-1] = self.decoder_cell_list[-1].zero_state(batch_size=batch_size, dtype=tf.float32)
decoder_initial_state = tuple(initial_state)
return MultiRNNCell(self.decoder_cell_list), decoder_initial_state
這段代碼我們構(gòu)建了解碼的網(wǎng)絡(luò),可以使一個(gè)單一的RNNCell,也可以是多個(gè)RNNCell,我們使用的后者。在最后一個(gè)Cell上,我們添加了Attention機(jī)制,Attention機(jī)制通過AttentionWrapper實(shí)現(xiàn),作用在decode_cell_list的最后一個(gè)Cell上,AttentionWrapper各參數(shù)含義:
cell:需要被Wrapper的網(wǎng)絡(luò)節(jié)點(diǎn)本身,這里是我們節(jié)點(diǎn)列表的最后一個(gè)節(jié)點(diǎn)
attention_mechanism:attention_mechanism我們使用BahdanauAttention,BahdanauAttention的介紹見下面解釋
attention_layer_size:網(wǎng)絡(luò)輸出層尺寸
cell_input_fn:如何整合網(wǎng)絡(luò)的原始輸入和attention,這里我們簡單將兩個(gè)tensor連接起來,通過一個(gè)Dense全連接網(wǎng)絡(luò)
initial_cell_state:編碼過程最后一個(gè)節(jié)點(diǎn)輸出的h_t
BahdanauAttention各參數(shù)的含義:
num_units:Attention機(jī)制覆蓋的距離,整合多大范圍內(nèi)的記憶
memory:encode輸出的hidden_state列表[h_0, h_1, ..., h_t]
memory_sequence_length:輸入句子的不考慮填充部分的實(shí)際長度
上面介紹的是train過程的解碼過程,下面介紹預(yù)測過程的解碼過程。
decoder_cell_2, decoder_initial_state_2 = self.build_decoder_cell()
# Start_tokens: [batch_size,] `int32` vector
start_tokens = tf.ones([self.batch_size, ], tf.int32) * self.output_start_token
decode_input_layer = Dense(self.hidden_units, dtype=tf.float32, name='decode_input_layer')
# 解碼過程中前一步的輸出通過embedding_lookup轉(zhuǎn)換成嵌入向量,并經(jīng)過一個(gè)全連接網(wǎng)絡(luò),輸出的是8個(gè)目標(biāo)類別中每個(gè)類別的概率
def embed_and_input_proj(inputs): # todo: tensor經(jīng)過Dense后變成什么??
return decode_input_layer(tf.nn.embedding_lookup(self.decoder_embedding, inputs))
# Helper to feed inputs for greedy decoding: uses the argmax of the output
predict_decoding_helper = seq2seq.GreedyEmbeddingHelper(start_tokens=start_tokens,
end_token=self.output_end_token,
embedding=embed_and_input_proj)
# Basic decoder performs greedy decoding at each time step
print("building greedy decoder..")
inference_decoder = seq2seq.BasicDecoder(cell=decoder_cell_2,
helper=predict_decoding_helper,
initial_state=decoder_initial_state_2,
output_layer=decoder_output_layer)
predict_logits, final_state, final_sequence_lengths = seq2seq.dynamic_decode(
decoder=inference_decoder,
output_time_major=False,
# impute_finished=True, # error occurs
maximum_iterations=self.decode_sentence_max_len)
# [batch_size, max_time_step, 1]
self.decoder_pred_decode = tf.expand_dims(predict_logits.sample_id, -1)
預(yù)測的過程和訓(xùn)練過程一樣,也是通過dynamic_decode進(jìn)行,主要區(qū)別在于BasicDecoder的helper參數(shù)不同,在訓(xùn)練的時(shí)候用到的是TrainingHelper,而預(yù)測過程用到的是GreedyEmbeddingHelper,區(qū)別在于訓(xùn)練過程不管每一步預(yù)測輸出的是什么結(jié)果,下一步輸入都不會(huì)使用這個(gè)數(shù)據(jù),而是使用標(biāo)記數(shù)據(jù)對應(yīng)的正確結(jié)果作為輸入,這樣防止某個(gè)步驟輸出的結(jié)果錯(cuò)誤傳遞給后續(xù)的步驟,這是TrainingHelper的實(shí)現(xiàn)。而預(yù)測過程需要將某一步的輸出通過argmax獲取概率最大的作為結(jié)果,然后將這個(gè)結(jié)果轉(zhuǎn)換成embedding作為下一步的輸入,這就是GreedyEmbeddingHelper做的事情。GreedyEmbeddingHelper的參數(shù)如下:
start_tokens:輸出序列的開始標(biāo)志
end_token:輸出序列的結(jié)束標(biāo)志
embedding:如何將前一步的輸出轉(zhuǎn)換成下一步的輸入,可以看到,我們的方法是先獲取前一步輸出的embeddings,然后經(jīng)過一個(gè)全連接網(wǎng)絡(luò),再將輸出作為下一步的輸入
dynamic_decode輸出的三個(gè)參數(shù)中的第一個(gè)predict_logits就是最終預(yù)測的結(jié)果,通過predict_logits.sample_id可以獲取到每一步的預(yù)測結(jié)果,這就是我們最終需要的結(jié)果。
這樣,從理論基礎(chǔ),到建模過程,再到最后的代碼實(shí)現(xiàn),我們完整的講解了利用seq2seq模型根據(jù)輸入序列生成輸出序列的全過程,希望能夠讓你系統(tǒng)的了解到seq2seq模型是怎么回事,以及怎樣運(yùn)行的。
參考:
http://colah.github.io/posts/2015-08-Understanding-LSTMs/
https://zhuanlan.zhihu.com/p/28919765