Tensorflow 自動文摘: 基於Seq2Seq+Attention模型的Textsum模型
簡介
這篇文章中我們將基於Tensorflow的Seq2Seq+Attention模型,介紹如何訓練一箇中文的自動生成新聞標題的模型。自動總結(Automatic Summarization)型別的模型一直是研究熱點。 直接抽出重要的句子的抽取式方法較為簡單,有如textrank之類的演算法,而生成式(重新生成新句子)較為複雜,效果也不盡如人意。目前比較流行的Seq2Seq模型,由 Sutskever等人提出,基於一個Encoder-Decoder的結構將source句子先Encode成一個固定維度d的向量,然後通過Decoder部分一個字元一個字元生成Target句子。新增入了Attention注意力分配機制後,使得Decoder在生成新的Target Sequence時,能得到之前Encoder編碼階段每個字元的隱藏層的資訊向量Hidden State,使得生成新序列的準確度提高。
資料準備和預處理
我們選擇公開的“搜狐新聞資料(SogouCS)”的語料,包含2012年6月—7月期間的新聞資料,超過1M的語料資料,包含新聞標題和正文的資訊。資料集可以從搜狗lab下載。 http://www.sogou.com/labs/resource/cs.php
資料的預處理階段極為重要,因為在Encoder編碼階段處理那些資訊,直接影響到整個模型的效果。我們主要對下列資訊進行替換和處理:- 特殊字元:去除特殊字元,如:“「,」,¥,…”;
- 括號內的內容:如表情符,【嘻嘻】,【哈哈】
- 日期:替換日期標籤為TAG_DATE,如:***年*月*日,****年*月,等等
- 超連結URL:替換為標籤TAG_URL;
- 刪除全形的英文:替換為標籤TAG_NAME_EN;
- 替換數字:TAG_NUMBER;
在對文字進行了預處理後,準備訓練語料: 我們的Source序列,是新聞的正文,待預測的Target序列是新聞的標題。 我們擷取正文的分詞個數到MAX_LENGTH_ENC=120個詞,是為了訓練的效果正文部分不宜過長。標題部分擷取到MIN_LENGTH_ENC = 30,即生成標題不超過30個詞。
在data_util.py類中,生成訓練資料時做了下列事情:
- create_vocabulary()方法建立詞典;
- data_to_token_ids()方法把訓練資料(content-train.txt)轉化為對應的詞ID的表示;
兩個檔案的格式:
# 資料1 正文 content-train.txt
世間 本 沒有 歧視 TAG_NAME_EN 歧視 源自於 人 的 內心 活動 TAG_NAME_EN “ 以愛 之 名 ” TAG_DATE 中國 艾滋病 反歧視 主題 創意 大賽 開幕 TAG_NAME_EN 讓 “ 愛 ” 在 高校 流動 。 TAG_NAME_EN 詳細 TAG_NAME_EN
濟慈 之 家 小朋友 感受 愛心 椅子 TAG_DATE TAG_NAME_EN 思源 焦點 公益 基金 向 盲童 孤兒院 “ 濟慈 之 家 ” 提供 了 首 筆 物資 捐贈 。 這 筆 價值 近 萬 元 的 物資 為 曲 美 傢俱 向 思源 · 焦點 公益 基金 提供 的 兒童 休閒椅 TAG_NAME_EN 將 用於 濟慈 之 家 的 小孩子們 日常 使用 。
...
# 資料2 標題 title-train.txt
艾滋病 反歧視 創意 大賽
思源 焦點 公益 基金 聯手 曲 美 傢俱 共 獻 愛心
...
訓練模型
#程式碼1
python headline.py
預測
執行predict.py, 互動地輸入分好詞的文字, 得到textsum的結果
#程式碼2-1
python predict.py
# 輸入和輸出
#> 中央 氣象臺 TAG_DATE TAG_NUMBER 時 繼續 釋出 暴雨 藍色 預警 TAG_NAME_EN 預計 TAG_DATE TAG_NUMBER 時至 TAG_DATE TAG_NUMBER 時 TAG_NAME_EN 內蒙古 東北部 、 山西 中 北部 、 河北 中部 和 東北部 、 京津 地區 、 遼寧 西南部 、 吉林 中部 、 黑龍江 中部 偏南 等 地 的 部分 地區 有 大雨 或 暴雨 。
#current bucket id0
#中央 氣象臺 釋出 暴雨 藍色 預警
#>
我們嘗試輸入下列分好詞的新聞正文,一些挑選過的自動生成的中文標題如下:
ID | 新聞正文 | 新聞標題 | textsum自動生成標題 |
---|---|---|---|
469 | 中央 氣象臺 TAG_DATE TAG_NUMBER 時 繼續 釋出 暴雨 藍色 預警 TAG_NAME_EN 預計 TAG_DATE TAG_NUMBER 時至 TAG_DATE TAG_NUMBER 時 TAG_NAME_EN 內蒙古 東北部 、 山西 中 北部 、 河北 中部 和 東北部 、 京津 地區 、 遼寧 西南部 、 吉林 中部 、 黑龍江 中部 偏南 等 地 的 部分 地區 有 大雨 或 暴雨 。 | 中央 氣象臺 繼續 釋出 暴雨 預警 北京 等 地 有 大雨 | 中央 氣象臺 釋出 暴雨 藍色 預警 |
552 | 美國 科羅拉多州 山林 大火 持續 肆虐 TAG_NAME_EN 當地 時間 TAG_DATE 橫掃 州 內 第二 大 城市 科羅拉多斯 普林斯 一 處 居民區 TAG_NAME_EN 迫使 超過 TAG_NUMBER TAG_NAME_EN TAG_NUMBER 萬 人 緊急 撤離 。 美國 正 值 山火 多發 季 TAG_NAME_EN 現有 TAG_NUMBER 場 山火 處於 活躍 狀態 。 | 山火 橫掃 美 西部 TAG_NUMBER 州 奧 巴馬 將 赴 災區 視察 聯邦 調查局 介入 查 原因 | 美國 科羅拉多州 山火 致 TAG_NUMBER 人 死亡 |
917 | 埃及 選舉 委員會 昨天 宣佈 TAG_NAME_EN 穆斯林 兄弟會 下屬 自由 與 正義黨 主席 穆爾西 獲得 TAG_NUMBER TAG_NAME_EN TAG_NUMBER TAG_NAME_EN 的 選票 TAG_NAME_EN 以 微弱 優勢 擊敗 前 總理 沙 菲克 贏得 選舉 TAG_NAME_EN 成為 新任 埃及 總統 。 媒體 稱 其 理念 獲 下層 民眾 支援 。 | 埃及 大選 昨晚 結束 新 總統 穆爾西 被 認為 具有 改革 魄力 | 埃及 總統 選舉 結果 |
920 | 上 周 TAG_NAME_EN 廣東 華興 銀行 在 央行 宣佈 降息 和 調整 存貸款 波幅 的 第二 天 TAG_NAME_EN 立即 宣佈 首 套 房貸 利率 最低 執行 七 折 優惠 。 一 石 激起 千層 浪 TAG_NAME_EN 隨之 而 起 的 “ 房貸 七 折 利率 重 出 江湖 ” 和 “ 房地產 調控 鬆綁 ” 的 謠言 四起 。 | 房貸 “ 七 折 利率 ” 真相 調查 TAG_NAME_EN 符合 條件 的 幾乎 為零 | 銀監會 否認 房貸 房貸 利率 |
預測並計算ROUGE評估
執行predict.py, 同時呼叫eval.py 中的 evaluate(X, Y, method = "rouge_n", n = 2) 方法計算ROUGE分
#程式碼2-2 linux shell
folder_path=`pwd`
input_dir=${folder_path}/news/test/content-test.txt
reference_dir=${folder_path}/news/test/title-test.txt
summary_dir=${folder_path}/news/test/summary.txt
python predict.py $input_dir $reference_dir $summary_dir
# 輸出:
# 中央 氣象臺 釋出 暴雨 藍色 預警
# Evaludated Rouge-2 score is 0.1818
# ...
下面我們將具體介紹tensorflow的seq2seq模型如何實現,首先先簡單回顧模型的結構。
Seq2Seq+Attention模型回顧
Seq2Seq模型有效地建模了基於輸入序列,預測未知輸出序列的問題。模型有兩部分構成,一個編碼階段的”Encoder”和一個解碼階段的”Decoder”。如下圖的簡單結構所示,Encoder的RNN每次輸入一個字元代表的embedding向量,如依次輸入A,B,C, 及終止標誌,將輸入序列編碼成一個固定長度的向量;之後解碼階段的RNN會一個一個字元地解碼, 如預測為X, 之後在訓練階段會強制將前一步解碼的輸出作為下一步解碼的輸入,如X會作為下一步預測Y時的輸入。
定義輸入序列 ,由Tx個固定長度為d的向量構成; 輸出序列為 ,由Ty個固定長度為d的向量構成; 定義輸入序Encoder階段的RNN隱藏層為 hj, Decoder階段的RNN隱藏層為 Si
Attention注意力分配機制
LSTM模型雖然具有記憶性,但是當Encoder階段輸入序列過長時,解碼階段的LSTM也無法很好地針對最早的輸入序列解碼。基於此,Attention注意力分配的機制被提出,就是為了解決這個問題。在Decoder階段每一步解碼,都能夠有一個輸入,對輸入序列所有隱藏層的資訊h_1,h_2,…h_Tx進行加權求和。打個比方就是每次在預測下一個詞時都會把所有輸入序列的隱藏層資訊都看一遍,決定預測當前詞時和輸入序列的那些詞最相關。
Attention機制代表了在解碼Decoder階段,每次都會輸入一個Context上下文的向量Ci, 隱藏層的新狀態Si根據上一步的狀態Si-1, Yi, Ci 三者的一個非線性函式得出。
Context向量在解碼的每一步都會重新計算,根據一個MLP模型計算出輸出序列i對每個輸入序列j的隱含層的對應權重aij,並對所有隱含層加權平均。文章中說的Alignment Model就是代表這種把輸入序列位置j和輸出序列位置i建立關係的模型。
aij 即可以理解為Decoder解碼輸出序列的第i步,對輸入序列第j步分配的注意力權重。
eij為一個簡單的MLP模型啟用的輸出;aij的計算是對eij做softmax歸一化後的結果。
Soft Attention和Hard Attention區別
Soft Attention通常是指以上我們描述的這種全連線(如MLP計算Attention 權重),對每一層都可以計算梯度和後向傳播的模型;不同於Soft attention那樣每一步都對輸入序列的所有隱藏層hj(j=1….Tx) 計算權重再加權平均的方法,Hard Attention是一種隨機過程,每次以一定概率抽樣,以一定概率選擇某一個隱藏層 hj*,在估計梯度時也採用蒙特卡羅抽樣Monte Carlo sampling的方法。
模型實現
我們對Tensorflow基本教程裡的translate英語法語翻譯例子裡的seq2seq_model.py類稍加修改,就能夠符合我們textsum例子使用,另外我們還會分析針對英文的textsum教程中構建雙向Bi-LSTM的Encoder-Decoder的例子。
1.Seq2Seq模型檔案: seq2Seq_model.py
單向LSTM的Encoder-Decoder結構
教程中的例子很長,但是將例項程式碼分解來看不是那麼複雜,下面將分三段來介紹官方tutorial裡的如何構建seq2seq模型。
定義基本單元: 多層LSTM cell
#程式碼3-1
# Create the internal multi-layer cell for our RNN.
single_cell = tf.nn.rnn_cell.GRUCell(size) # default use GRU
if use_lstm:
single_cell = tf.nn.rnn_cell.BasicLSTMCell(size, state_is_tuple=True)
cell = single_cell
if num_layers > 1:
cell = tf.nn.rnn_cell.MultiRNNCell([single_cell] * num_layers, state_is_tuple=True)
定義前向過程的 seq2seq_f 函式,利用tf.nn.seq2seq.embedding_attention_seq2seq 返回output
# 程式碼3-2
# The seq2seq function: we use embedding for the input and attention.
def seq2seq_f(encoder_inputs, decoder_inputs, do_decode):
return tf.nn.seq2seq.embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs,
cell,
num_encoder_symbols=source_vocab_size,
num_decoder_symbols=target_vocab_size,
embedding_size=size,
output_projection=output_projection,
feed_previous=do_decode,
dtype=tf.float32)
桶Bucket的機制: 應用Bucket機制,核心的思想是把輸入序列的句子按照長度的相似程度分到不同的固定長度的Bucket裡面,長度不夠的都新增PAD字元。之所以有Bucket的原因是工程效率:”RNN在數學上是可以處理任意長度的資料的。我們在TensorFlow中使用bucket的原因主要是為了工程實現的效率” (摘自知乎JQY的回答https://www.zhihu.com/question/42057513)
# 程式碼3-3
# Training outputs and losses.
if forward_only:
self.outputs, self.losses = tf.nn.seq2seq.model_with_buckets(
self.encoder_inputs, self.decoder_inputs, targets,
self.target_weights, buckets, lambda x, y: seq2seq_f(x, y, True),
softmax_loss_function=softmax_loss_function)
# If we use output projection, we need to project outputs for decoding.
if output_projection is not None:
for b in xrange(len(buckets)):
self.outputs[b] = [
tf.matmul(output, output_projection[0]) + output_projection[1]
for output in self.outputs[b]
]
else:
self.outputs, self.losses = tf.nn.seq2seq.model_with_buckets(
self.encoder_inputs, self.decoder_inputs, targets,
self.target_weights, buckets, lambda x, y: seq2seq_f(x, y, False),
softmax_loss_function=softmax_loss_function)
tf.nn.seq2seq.model_with_buckets()結果返回output序列;’ buckets = [(120, 30), (200, 35), (300, 40), (400, 40), (500, 40)]
https://github.com/tensorflow/tensorflow/blob/64edd34ce69b4a8033af5d217cb8894105297d8a/tensorflow/contrib/legacy_seq2seq/python/ops/seq2seq.py雙向LSTM的Encoder-Decoder結構
Encoder階段自己定義了Bi-LSTM結構
# 程式碼4-1
# URL: https://github.com/tensorflow/models/tree/master/textsum
# Encoder: Multi-Layer LSTM, Output: encoder_outputs
for layer_i in xrange(hps.enc_layers):
with tf.variable_scope('encoder%d'%layer_i), tf.device(
self._next_device()):
cell_fw = tf.nn.rnn_cell.LSTMCell(
hps.num_hidden,
initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=123),
state_is_tuple=False)
cell_bw = tf.nn.rnn_cell.LSTMCell(
hps.num_hidden,
initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=113),
state_is_tuple=False)
(emb_encoder_inputs, fw_state, _) = tf.nn.bidirectional_rnn(
cell_fw, cell_bw, emb_encoder_inputs, dtype=tf.float32,
sequence_length=article_lens)
encoder_outputs = emb_encoder_inputs
with tf.variable_scope('output_projection'):
w = tf.get_variable(
'w', [hps.num_hidden, vsize], dtype=tf.float32,
initializer=tf.truncated_normal_initializer(stddev=1e-4))
w_t = tf.transpose(w)
v = tf.get_variable(
'v', [vsize], dtype=tf.float32,
initializer=tf.truncated_normal_initializer(stddev=1e-4))
Decoder階段,利用內建的tf.nn.seq2seq.attention_decoder()函式返回output
# 程式碼4-2
with tf.variable_scope('decoder'), tf.device(self._next_device()):
# When decoding, use model output from the previous step
# for the next step.
loop_function = None
if hps.mode == 'decode':
loop_function = _extract_argmax_and_embed(
embedding, (w, v), update_embedding=False)
cell = tf.nn.rnn_cell.LSTMCell(
hps.num_hidden,
initializer=tf.random_uniform_initializer(-0.1, 0.1, seed=113),
state_is_tuple=False)
encoder_outputs = [tf.reshape(x, [hps.batch_size, 1, 2*hps.num_hidden])
for x in encoder_outputs]
self._enc_top_states = tf.concat(1, encoder_outputs)
self._dec_in_state = fw_state
# During decoding, follow up _dec_in_state are fed from beam_search.
# dec_out_state are stored by beam_search for next step feeding.
initial_state_attention = (hps.mode == 'decode')
decoder_outputs, self._dec_out_state = tf.nn.seq2seq.attention_decoder(
emb_decoder_inputs, self._dec_in_state, self._enc_top_states,
cell, num_heads=1, loop_function=loop_function,
initial_state_attention=initial_state_attention)
Attention注意力矩陣的視覺化
Attention 注意力分配機制的權重矩陣[Aij]可以反映在Decoder階段第i個輸出字元對Encoder階段的第j個字元的注意力分配的權重aij。 我們可以通過繪製Heatmap來視覺化seq2seq模型中Decoder的Y對Encoder的X每個字元的權重。
獲取attention_mask的值
attention_mask 即為我們感興趣的注意力權重分配的tensor,我們首先來看tensorflow的原始碼seq2seq.py這個ops的實現, 容易發現,計算attention_mask 變數a的程式碼出現在 attention_decoder()函式內的attention()函式體下, a = nn_ops.softmax(s) 這句。 我們把該變數新增到return語句中的返回值,同時也修改所有呼叫了attention_decoder()的上層的函式,為了最終能夠在主函式中將attn_mask這個變數抽取出。 具體需要修改的指令碼參考textsum專案下的seq2seq_attn.py這個檔案。 之後我們在主函式中利用attn_out = session.run(self.attn_masks[bucket_id], input_feed) ,對變數進行session.run() 就可以獲得當前這個樣本的attention矩陣的值。
# 程式碼5-1
# URL: https://github.com/rockingdingo/deepnlp/blob/master/deepnlp/textsum/seq2seq_attn.py
def attention_decoder():
## some code
def attention(query):
"""Put attention masks on hidden using hidden_features and query."""
ds = []
if nest.is_sequence(query):
query_list = nest.flatten(query)
for q in query_list:
ndims = q.get_shape().ndims
if ndims:
assert ndims == 2
query = array_ops.concat_v2(query_list, 1)
for a in xrange(num_heads):
with variable_scope.variable_scope("Attention_%d" % a):
y = linear(query, attention_vec_size, True)
y = array_ops.reshape(y, [-1, 1, 1, attention_vec_size])
# Attention mask is a softmax of v^T * tanh(...).
s = math_ops.reduce_sum(v[a] * math_ops.tanh(hidden_features[a] + y),
[2, 3])
# Tensor a 即為我們需要抽取的attention_mask
a = nn_ops.softmax(s)
d = math_ops.reduce_sum(
array_ops.reshape(a, [-1, attn_length, 1, 1]) * hidden, [1, 2])
ds.append(array_ops.reshape(d, [-1, attn_size]))
return ds, a
利用matplotlib視覺化
我們利用matplotlib包中繪製heatmap的函式,可以簡單地將上一步抽取出的attn_matrix視覺化。在eval.py模組中我們整合了一個eval.plot_attention(data, X_label=None, Y_label=None) 函式來簡單繪製attention權重矩陣。 執行 predict_attn.py 指令碼,輸入分好詞的待分析新聞文字,然後自動生成的jpg圖片就儲存在./img目錄下。
# 程式碼5-2
# 輸入文字, 檢視Attention的heatmap:
# > 中央 氣象臺 TAG_DATE TAG_NUMBER 時 繼續 釋出 暴雨 藍色 預警 TAG_NAME_EN 預計 TAG_DATE TAG_NUMBER 時至 TAG_DATE TAG_NUMBER 時 TAG_NAME_EN 內蒙古 東北部 、 山西 中 北部 、 河北 中部 和 東北部 、 京津 地區 、 遼寧 西南部 、 吉林 中部 、 黑龍江 中部 偏南 等 地 的 部分 地區 有 大雨 或 暴雨 。
python predict_attn.py