指針生成網絡(Pointer-Generator-Network)原理與實戰


指針生成網絡(Pointer-Generator-Network)原理與實戰

 

0 前言

      本文主要內容:介紹Pointer-Generator-Network在文本摘要任務中的背景模型架構與原理在中英文數據集上實戰效果與評估,最后得出結論。參考的《Get To The Point: Summarization with Pointer-Generator Networks》以及多篇博客均在文末給出連接,文中使用數據集已上傳百度網盤代碼已傳至GitHub,讀者可以在文中找到相應連接,實際操作過程中確實遇到很多坑,並未在文中一一指明,有興趣的讀者可以留言一起交流。由於水平有限,請讀者多多指正。

  隨着互聯網飛速發展,產生了越來越多的文本數據,文本信息過載問題日益嚴重,對各類文本進行一個“降 維”處理顯得非常必要,文本摘要便是其中一個重要的手段。文本摘要旨在將文本或文本集合轉換為包含關鍵信息的簡短摘要。按照輸出類型可分為抽取式摘要和生成式摘要。抽取式摘要從源文檔中抽取關鍵句和關鍵詞組成摘要,摘要全部來源於原文生成式摘要根據原文,允許生成新的詞語、原文本中沒有的短語來組成摘要。

  指針生成網絡屬於生成式模型。

  僅用Neural sequence-to-sequence模型可以實現生成式摘要,但存在兩個問題:

    1. 可能不准確地再現細節, 無法處理詞匯不足(OOV)單詞;

    2. 傾向於重復自己

    原文是(they are liable to reproducefactual details inaccurately, and they tendto repeat themselves.)

  指針生成網絡(Pointer-Generator-Network)從兩個方面進行了改進

    1. 該網絡通過指向(pointer)從源文本中復制單詞,有助於准確地復制信息,同時保留通過生成器產生新單詞的能力;

    2. 使用coverage機制來跟蹤已總結的內容,防止重復。 

  接下來從下面幾個部分介紹Pointer-Generator-Network原理:

    1. Baseline sequence-to-sequence;

    2. Pointer-Generator-Network;

    3. Coverage Mechanism。

1 Baseline sequence-to-sequence

  Pointer-Generator Networks是在Baseline sequence-to-sequence模型的基礎上構建的,我們首先Baseline seq2seq+attention。其架構圖如下:

 

  該模型可以 關注原文本中的相關單詞以生成新單詞進行概括。比如:模型可能 注意到原文中的 "victorious"" win"這個兩個單詞,在摘要"Germany beat Argentina 2-0"中 生成了新的單詞beat 。

  Seq2Seq的模型結構是經典的Encoder-Decoder模型,即先用Encoder將原文本編碼成一個中間層的隱藏狀態,然后用Decoder來將該隱藏狀態解碼成為另一個文本。Baseline Seq2Seq在Encoder端是一個雙向的LSTM,這個雙向的LSTM可以捕捉原文本的長距離依賴關系以及位置信息,編碼時詞嵌入經過雙向LSTM后得到編碼狀態 hihi 。在Decoder端解碼器是一個單向的LSTM,訓練階段時參考摘要詞依次輸入(測試階段時是上一步的生成詞),在時間步 tt得到解碼狀態 stst 。使用hihi和stst得到該時間步原文第 ii個詞注意力權重。

eti=vTtanh(Whhi+Wsst+battn)eit=vTtanh(Whhi+Wsst+battn)
at=softmax(et)at=softmax(et)

  得到的注意力權重和 hihi加權求和得到重要的上下文向量 ht(contextvector)ht∗(contextvector):

 

ht=iatihiht∗=∑iaithi

 

  htht∗可以看成是該時間步通讀了原文的固定尺寸的表征。然后將 stst和 htht∗ 經過兩層線性層得到單詞表分布 PvocabPvocab:

 

Pvocab=softmax(V(V[st,ht]+b)+b)Pvocab=softmax(V′(V[st,ht∗]+b)+b′)

 

  其中 [st,ht][st,ht∗]是拼接。這樣再通過sofmaxsofmax得到了一個概率分布,就可以預測需要生成的詞:

 

P(w)=Pvocab(w)P(w)=Pvocab(w)

 

  在訓練階段,時間步 tt 時的損失為: 

 

losst=logP(wt)losst=−logP(wt∗)

 

  那么原輸入序列的整體損失為: 

 

loss=1Tt=0Tlosstloss=1T∑t=0Tlosst

 

 

2 Pointer-Generator-Network

  原文中的Pointer-Generator Networks是一個混合了 Baseline seq2seq和PointerNetwork的網絡,它具有Baseline seq2seq的生成能力和PointerNetwork的Copy能力。該網絡的結構如下:

如何權衡一個詞應該是生成的還是復制的?

  原文中引入了一個權重 pgenpgen 。

  從Baseline seq2seq的模型結構中得到了stst 和htht∗,和解碼器輸入 xtxt 一起來計算 pgenpgen :

 

pgen=σ(wThht+wTsst+wTxxt+bptr)pgen=σ(wh∗Tht∗+wsTst+wxTxt+bptr)

 

  這時,會擴充單詞表形成一個更大的單詞表--擴充單詞表(將原文當中的單詞也加入到其中),該時間步的預測詞概率為:

 

P(w)=pgenPvocab(w)+(1pgen)i:wi=watiP(w)=pgenPvocab(w)+(1−pgen)∑i:wi=wait

 

  其中 atiait 表示的是原文檔中的詞。我們可以看到解碼器一個詞的輸出概率有其是否拷貝是否生成的概率和決定。當一個詞不出現在常規的單詞表上時 Pvocab(w)Pvocab(w) 為0,當該詞不出現在文檔中i:wi=wati∑i:wi=wait為0。

3  Coverage mechanism

  原文的特色是運用了Coverage Mechanism來解決重復生成文本的問題,下圖反映了前兩個模型與添加了Coverage Mechanism生成摘要的結果:

  藍色的字體表示的是參考摘要,三個模型的生成摘要的結果差別挺大;

  紅色字體表明了不准確的摘要細節生成(UNK未登錄詞,無法解決OOV問題);

  綠色的字體表明了模型生成了重復文本。

  為了解決此問題--Repitition,原文使用了在機器翻譯中解決“過翻譯”和“漏翻譯”的機制--Coverage Mechanism

  具體實現上,就是將先前時間步的注意力權重加到一起得到所謂的覆蓋向量 ct(coveragevector)ct(coveragevector),用先前的注意力權重決策來影響當前注意力權重的決策,這樣就避免在同一位置重復,從而避免重復生成文本。計算上,先計算coverage vector ctct:

 

ct=t=0t1atct=∑t′=0t−1at′

 

  然后添加到注意力權重的計算過程中,ctct用來計算 etieit:

 

eti=vTtanh(Whhi+Wsst+wccti+battn)eit=vTtanh(Whhi+Wsst+wccit+battn)

 

  同時,為coverage vector添加損失是必要的,coverage loss計算方式為:

 

covlosst=imin(ati,cti)covlosst=∑imin(ait,cit)

 

  這樣coverage loss是一個有界的量  covlosstiati=1covlosst≤∑iait=1 。因此最終的LOSS為:

 

losst=logP(wt)+λimin(ati,cti)losst=−logP(wt∗)+λ∑imin(ait,cit)
 

 

4 實戰部分

4.1 DataSet

英文數據集: cnn dailymail數據集,地址:https://github.com/becxer/cnn-dailymail/

中文數據集:新浪微博摘要數據集,這是中文數據集,有679898條文本及摘要。

中英文數據集均可從這里下載,鏈接:https://pan.baidu.com/s/18ykewFUrTLzW8R84bF42pg  密碼:9yqt。

4.2 Experiments

  試驗環境:centos7.4/python3.6/tensorflow1.12.0  GPU:Tesla-K40m-12G*4   代碼參考:python3 tensorflow版本。調試時候各種報錯,所以需要debug。

  改動后的代碼已上傳至GitHub:https://github.com/zingp/NLP/tree/master/P007PytorchPointerGeneratorNetwork

  中文數據集預處理代碼:

  第一部分是對原始數據進行分詞,划分訓練集測試集,並保存文件。

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
import  os
import  sys
import  time
import  jieba
 
ARTICLE_FILE  =  "./data/weibo_news/train_text.txt"
SUMMARRY_FILE  =  "./data/weibo_news/train_label.txt"
 
TRAIN_FILE  =  "./data/weibo_news/train_art_summ_prep.txt"
VAL_FILE  =  "./data/weibo_news/val_art_summ_prep.txt"
 
def  timer(func):
     def  wrapper( * args,  * * kwargs):
         start  =  time.time()
         =  func( * args,  * * kwargs)
         end  =  time.time()
         cost  =  end  -  start
         print (f "Cost time: {cost} s" )
         return  r
     return  wrapper
 
@timer
def  load_data(filename):
     """加載數據文件,對文本進行分詞"""
     data_list  =  []
     with  open (filename,  'r' , encoding =  'utf-8' ) as f:
         for  line  in  f:
             # jieba.enable_parallel()
             words  =  jieba.cut(line.strip())
             word_list  =  list (words)
             # jieba.disable_parallel()
             data_list.append( ' ' .join(word_list).strip())
     return  data_list
 
def  build_train_val(article_data, summary_data, train_num = 600_000 ):
     """划分訓練和驗證數據"""
     train_list  =  []
     val_list  =  []
     =  0
     for  text, summ  in  zip (article_data, summary_data):
         + =  1
         if  n < =  train_num:
             train_list.append(text)
             train_list.append(summ)
         else :
             val_list.append(text)
             val_list.append(summ)
     return  train_list, val_list
 
def  save_file(filename, li):
     """預處理后的數據保存到文件"""
     with  open (filename,  'w+' , encoding = 'utf-8' ) as f:
         for  item  in  li:
             f.write(item  +  '\n' )
     print (f "Save {filename} ok." )
 
if  __name__  = =  '__main__' :
     article_data  =  load_data(ARTICLE_FILE)      # 大概耗時10分鍾
     summary_data  =  load_data(SUMMARRY_FILE)
     TRAIN_SPLIT  =  600_000
     train_list, val_list  =  build_train_val(article_data, summary_data, train_num = TRAIN_SPLIT)
     save_file(TRAIN_FILE, train_list)
     save_file(VAL_FILE, val_list) 

第二部分是將文件打包,生成模型能夠加載的二進制文件。

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import  os
import  struct
import  collections
from  tensorflow.core.example  import  example_pb2
 
# 經過分詞處理后的訓練數據與測試數據文件
TRAIN_FILE  =  "./data/weibo_news/train_art_summ_prep.txt"
VAL_FILE  =  "./data/weibo_news/val_art_summ_prep.txt"
 
# 文本起始與結束標志
SENTENCE_START  =  '<s>'
SENTENCE_END  =  '</s>'
 
VOCAB_SIZE  =  50_000   # 詞匯表大小
CHUNK_SIZE  =  1000     # 每個分塊example的數量,用於分塊的數據
 
# tf模型數據文件存放目錄
FINISHED_FILE_DIR  =  './data/weibo_news/finished_files'
CHUNKS_DIR  =  os.path.join(FINISHED_FILE_DIR,  'chunked' )
 
def  chunk_file(finished_files_dir, chunks_dir, name, chunk_size):
     """構建二進制文件"""
     in_file  =  os.path.join(finished_files_dir,  '%s.bin'  %  name)
     print (in_file)
     reader  =  open (in_file,  "rb" )
     chunk  =  0
     finished  =  False
     while  not  finished:
         chunk_fname  =  os.path.join(chunks_dir,  '%s_%03d.bin'  %  (name, chunk))   # 新的分塊
         with  open (chunk_fname,  'wb' ) as writer:
             for  in  range (chunk_size):
                 len_bytes  =  reader.read( 8 )
                 if  not  len_bytes:
                     finished  =  True
                     break
                 str_len  =  struct.unpack( 'q' , len_bytes)[ 0 ]
                 example_str  =  struct.unpack( '%ds'  %  str_len, reader.read(str_len))[ 0 ]
                 writer.write(struct.pack( 'q' , str_len))
                 writer.write(struct.pack( '%ds'  %  str_len, example_str))
             chunk  + =  1
 
 
def  chunk_all():
     # 創建一個文件夾來保存分塊
     if  not  os.path.isdir(CHUNKS_DIR):
         os.mkdir(CHUNKS_DIR)
     # 將數據分塊
     for  name  in  [ 'train' 'val' ]:
         print ( "Splitting %s data into chunks..."  %  name)
         chunk_file(FINISHED_FILE_DIR, CHUNKS_DIR, name, CHUNK_SIZE)
     print ( "Saved chunked data in %s"  %  CHUNKS_DIR)
 
 
def  read_text_file(text_file):
     """從預處理好的文件中加載數據"""
     lines  =  []
     with  open (text_file,  "r" , encoding = 'utf-8' ) as f:
         for  line  in  f:
             lines.append(line.strip())
     return  lines
 
 
def  write_to_bin(input_file, out_file, makevocab = False ):
     """生成模型需要的文件"""
     if  makevocab:
         vocab_counter  =  collections.Counter()
 
     with  open (out_file,  'wb' ) as writer:
         # 讀取輸入的文本文件,使偶數行成為article,奇數行成為abstract(行號從0開始)
         lines  =  read_text_file(input_file)
         for  i, new_line  in  enumerate (lines):
             if  %  2  = =  0 :
                 article  =  lines[i]
             if  %  2  ! =  0 :
                 abstract  =  "%s %s %s"  %  (SENTENCE_START, lines[i], SENTENCE_END)
 
                 # 寫入tf.Example
                 tf_example  =  example_pb2.Example()
                 tf_example.features.feature[ 'article' ].bytes_list.value.extend([bytes(article, encoding = 'utf-8' )])
                 tf_example.features.feature[ 'abstract' ].bytes_list.value.extend([bytes(abstract, encoding = 'utf-8' )])
                 tf_example_str  =  tf_example.SerializeToString()
                 str_len  =  len (tf_example_str)
                 writer.write(struct.pack( 'q' , str_len))
                 writer.write(struct.pack( '%ds'  %  str_len, tf_example_str))
 
                 # 如果可以,將詞典寫入文件
                 if  makevocab:
                     art_tokens  =  article.split( ' ' )
                     abs_tokens  =  abstract.split( ' ' )
                     abs_tokens  =  [t  for  in  abs_tokens  if
                                   not  in  [SENTENCE_START, SENTENCE_END]]   # 從詞典中刪除這些符號
                     tokens  =  art_tokens  +  abs_tokens
                     tokens  =  [t.strip()  for  in  tokens]      # 去掉句子開頭結尾的空字符
                     tokens  =  [t  for  in  tokens  if  t ! =  ""]   # 刪除空行
                     vocab_counter.update(tokens)
 
     print ( "Finished writing file %s\n"  %  out_file)
 
     # 將詞典寫入文件
     if  makevocab:
         print ( "Writing vocab file..." )
         with  open (os.path.join(FINISHED_FILE_DIR,  "vocab" ),  'w' , encoding = 'utf-8' ) as writer:
             for  word, count  in  vocab_counter.most_common(VOCAB_SIZE):
                 writer.write(word  +  ' '  +  str (count)  +  '\n' )
         print ( "Finished writing vocab file" )
 
if  __name__  = =  '__main__' :
     if  not  os.path.exists(FINISHED_FILE_DIR):
     os.makedirs(FINISHED_FILE_DIR)
     write_to_bin(VAL_FILE, os.path.join(FINISHED_FILE_DIR,  "val.bin" ))
     write_to_bin(TRAIN_FILE, os.path.join(FINISHED_FILE_DIR,  "train.bin" ), makevocab = True )
     chunk_all() 

  在訓練中文數據集的時候,設置的hidden_dim為 256 ,詞向量維度emb_dim為126,詞匯表數目vocab_size為50K,batch_size設為16。這里由於我們的模型有處理OOV能力,因此詞匯表不用設置過大;在batch_size的選擇上,顯存小的同學建議設為8,否則會出現內存不夠,難以訓練。

  在batch_size=16時,訓練了27k step, 出現loss震盪很難收斂的情況,train階段loss如下:

 

val階段loss如下:

  可以看到當step在10k之后,loss在3.0-5.0之間來回劇烈震盪,並沒有下降趨勢。前面我們為了省顯存,將batch_size設置成16,可能有點小了,梯度下降方向不太明確,顯得有點盲目,因此將batch_size設成了32后重新開始訓練。注意:在一定范圍內,batchsize越大,計算得到的梯度下降方向就越准,引起訓練震盪越小。增大batch_size后訓練的loss曲線如下:

val loss曲線如下:

  看起來loss還是比較震盪的,但是相比bathc_size=16時有所改善。一開始的前10K steps里loss下降還是很明顯的基本上能從6降到4左右的區間,10k steps之后開始震盪,但還是能看到在緩慢下降:從4左右,開始在2-4之間震盪下降。這可能是目前的steps還比較少,只要val loss沒有一直升高,可以繼續觀擦,如果500K steps都還是如此,可以考慮在一個合適的實機early stop。

4.3 Evaluation

  摘要質量評價需要考慮一下三點:

    (1) 決定原始文本最重要的、需要保留的部分;

    (2) 在自動文本摘要中識別出1中的部分;

    (3) 基於語法和連貫性(coherence)評價摘要的可讀性(readability)。

  從這三點出發有人工評價和自動評價,本文只討論一下更值得關注的自動評價。自動文檔摘要評價方法分為兩類:

    內部評價方法(Intrinsic Methods):提供參考摘要,以參考摘要為基准評價系統摘要的質量。系統摘要與參考摘要越吻合, 質量越高。

    外部評價方法(Extrinsic Methods):不提供參考摘要,利用文檔摘要代替原文檔執行某個文檔相關的應用。

  內部評價方法是最常使用的文摘評價方法,將系統生成的自動摘要與參考摘要采用一定的方法進行比較是目前最為常見的文摘評價模式。下面介紹內部評價方法是ROUGE(Recall-Oriented Understudy for Gisting Evaluation)。

  ROUGE是2004年由ISI的Chin-Yew Lin提出的一種自動摘要評價方法,現被廣泛應用於DUC(Document Understanding Conference)的摘要評測任務中。ROUGE基於摘要中n元詞(n-gram)的共現信息來評價摘要,是一種面向n元詞召回率的評價方法。基本思想為由多個專家分別生成人工摘要,構成標准摘要集,將系統生成的自動摘要與人工生成的標准摘要相對比,通過統計二者之間重疊的基本單元(n元語法、詞序列和詞對)的數目,來評價摘要的質量。通過與專家人工摘要的對比,提高評價系統的穩定性和健壯性。該方法現已成為摘要評價技術的通用標注之一。 ROUGE准則由一系列的評價方法組成,包括ROUGE-N(N=1、2、3、4,分別代表基於1元詞到4元詞的模型),ROUGE-L,ROUGE-S, ROUGE-W,ROUGE-SU等。在自動文摘相關研究中,一般根據自己的具體研究內容選擇合適的ROUGE方法。公式如下: 

 

ROUGEN=S{ReferenceSummaries}gramnSCountmatch(gramn)S{ReferenceSummaries}gramnSCount(gramn)ROUGE−N=∑S∈{ReferenceSummaries}∑gramn∈SCountmatch(gramn)∑S∈{ReferenceSummaries}∑gramn∈SCount(gramn)

 

  其中,ngramn−gram表示n元詞RefSummariesRefSummaries表示參考摘要(標准摘要)Countmatch(ngram)Countmatch(n−gram)表示生成摘要和參考摘要中同時出現ngramn−gram的個數Count(ngram)Count(n−gram)則表示參考摘要中出現的ngramn−gram個數ROUGE公式是由召回率的計算公式演變而來的,分子可以看作“檢出的相關文檔數目”,即系統生成摘要與標准摘要相匹配的ngramn−gram個數,分母可以看作“相關文檔數目”,即參考摘要中所有的ngramn−gram個數。

來看原文試驗結果:

   在上表中,上半部分是模型生成的的摘要評估,而下半部分的是提取摘要評估。可以看出抽象生成的效果接近了抽取效果。再來看重復情況:

  可以看出我們的no coverage的模型生成的摘要在n-gram上是要比reference摘要要多的,而使用了coverage之后,重復數目和reference相當。
看一下我們的中文結果:
例子一:

例子二:

 

  直觀上效果還是不錯的。可以看出,預測的摘要中已經基本沒有不斷重復自身的現象;像“[話筒] [思考] [吃驚] ”這種文本,應該是原文本中的表情,在對文本的處理中我們並沒有將這些清洗掉,因此依然出現在預測摘要中。不過例子二還是出現了句子不是很通順的情況,在輸出句子的語序連貫上還有待改進。

4.4 Results

  1. 在復現原論文的基礎上,將模型方法應用在中文數據集上,取得了一定效果。

  2. 可以看出指針生成網絡通過指針復制原文中的單詞,可以生成新的單詞,解決oov問題;其次使用了coverage機制,能夠避免生成的詞語不斷重復。

  3. 在語句的通順和連貫上還有待加強。

5 References

  1. https://arxiv.org/pdf/1704.04368.pdf
  2. https://www.jiqizhixin.com/articles/2019-03-25-7
  3. https://zhuanlan.zhihu.com/p/53821581
  4. https://www.aclweb.org/anthology/W04-1013
  5. https://blog.csdn.net/mr2zhang/article/details/90754134
  6. https://zhuanlan.zhihu.com/p/68253473


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM