Github下載完整代碼
https://github.com/rockingdingo/deepnlp/tree/master/deepnlp/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艾滋病 反歧視 創意 大賽 思源 焦點 公益 基金 聯手 曲 美 家具 共 獻 愛心 ...訓練模型
#代碼1python headline.py預測
運行PRedict.py, 交互地輸入分好詞的文本, 得到textsum的結果
#代碼2-1python 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 shellfolder_path=`pwd`input_dir=${folder_path}/news/test/content-test.txtreference_dir=${folder_path}/news/test/title-test.txtsummary_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時的輸入。
圖1 Seq2Seq model 定義輸入序列
,由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建立關系的模型。
圖2 Translate Seq2Seq alignment model Soft Attention和Hard Attention區別
Soft Attention通常是指以上我們描述的這種全連接(如MLP計算Attention 權重),對每一層都可以計算梯度和后向傳播的模型;不同于Soft attention那樣每一步都對輸入序列的所有隱藏層hj(j=1….Tx) 計算權重再加權平均的方法,Hard Attention是一種隨機過程,每次以一定概率抽樣,以一定概率選擇某一個隱藏層 hj*,在估計梯度時也采用蒙特卡羅抽樣Monte Carlo sampling的方法。
圖3 Soft Attention 模型 圖4 Hard Attention 模型 模型實現
我們對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 GRUif use_lstm: single_cell = tf.nn.rnn_cell.BasicLSTMCell(size, state_is_tuple=True)cell = single_cellif 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圖5 Attention注意力權重分配的Heatmap 可視化部分代碼參考項目中的 predict_attn.py seq2seq_attn.py seq2seq_model_attn.py 這三個文件。
預測階段Decode策略: 貪心算法和Beam Search
貪心算法 Beam Search拓展閱讀
deepnlp Python包http://www.deepnlp.org/Github源碼textsumhttps://github.com/rockingdingo/deepnlp/tree/master/deepnlp/textsumTensorflow seq2seq.pyhttps://github.com/tensorflow/tensorflow/blob/64edd34ce69b4a8033af5d217cb8894105297d8a/tensorflow/contrib/legacy_seq2seq/python/ops/seq2seq.pyTensorflow Bi-LSTM textsum exampleshttps://github.com/tensorflow/models/tree/master/textsumATTENTION MECHANISMhttps://blog.heuritech.com/2016/01/20/attention-mechanism/
新聞熱點
疑難解答