指針生成網絡(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

Seq2Seq的模型結構是經典的Encoder-Decoder模型,即先用Encoder將原文本編碼成一個中間層的隱藏狀態,然后用Decoder來將該隱藏狀態解碼成為另一個文本。Baseline Seq2Seq在Encoder端是一個雙向的LSTM,這個雙向的LSTM可以捕捉原文本的長距離依賴關系以及位置信息,編碼時詞嵌入經過雙向LSTM后得到編碼狀態 hihi 。在Decoder端,解碼器是一個單向的LSTM,訓練階段時參考摘要詞依次輸入(測試階段時是上一步的生成詞),在時間步 tt得到解碼狀態 stst 。使用hihi和stst得到該時間步原文第 ii個詞注意力權重。
得到的注意力權重和 hihi加權求和得到重要的上下文向量 h∗t(contextvector)ht∗(contextvector):
h∗tht∗可以看成是該時間步通讀了原文的固定尺寸的表征。然后將 stst和 h∗tht∗ 經過兩層線性層得到單詞表分布 PvocabPvocab:
其中 [st,h∗t][st,ht∗]是拼接。這樣再通過sofmaxsofmax得到了一個概率分布,就可以預測需要生成的詞:
在訓練階段,時間步 tt 時的損失為:
那么原輸入序列的整體損失為:
2 Pointer-Generator-Network
原文中的Pointer-Generator Networks是一個混合了 Baseline seq2seq和PointerNetwork的網絡,它具有Baseline seq2seq的生成能力和PointerNetwork的Copy能力。該網絡的結構如下:
如何權衡一個詞應該是生成的還是復制的?
原文中引入了一個權重 pgenpgen 。
從Baseline seq2seq的模型結構中得到了stst 和h∗tht∗,和解碼器輸入 xtxt 一起來計算 pgenpgen :
這時,會擴充單詞表形成一個更大的單詞表--擴充單詞表(將原文當中的單詞也加入到其中),該時間步的預測詞概率為:
其中 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:
然后添加到注意力權重的計算過程中,ctct用來計算 etieit:
同時,為coverage vector添加損失是必要的,coverage loss計算方式為:
這樣coverage loss是一個有界的量 covlosst≤∑iati=1covlosst≤∑iait=1 。因此最終的LOSS為:
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()
r
=
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
=
[]
n
=
0
for
text, summ
in
zip
(article_data, summary_data):
n
+
=
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
i
%
2
=
=
0
:
article
=
lines[i]
if
i
%
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
t
in
abs_tokens
if
t
not
in
[SENTENCE_START, SENTENCE_END]]
# 從詞典中刪除這些符號
tokens
=
art_tokens
+
abs_tokens
tokens
=
[t.strip()
for
t
in
tokens]
# 去掉句子開頭結尾的空字符
tokens
=
[t
for
t
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如下:
可以看到當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方法。公式如下:
其中,n−gramn−gram表示n元詞,RefSummariesRefSummaries表示參考摘要(標准摘要),Countmatch(n−gram)Countmatch(n−gram)表示生成摘要和參考摘要中同時出現n−gramn−gram的個數,Count(n−gram)Count(n−gram)則表示參考摘要中出現的n−gramn−gram個數。ROUGE公式是由召回率的計算公式演變而來的,分子可以看作“檢出的相關文檔數目”,即系統生成摘要與標准摘要相匹配的n−gramn−gram個數,分母可以看作“相關文檔數目”,即參考摘要中所有的n−gramn−gram個數。
來看原文試驗結果:
在上表中,上半部分是模型生成的的摘要評估,而下半部分的是提取摘要評估。可以看出抽象生成的效果接近了抽取效果。再來看重復情況:

例子二:
直觀上效果還是不錯的。可以看出,預測的摘要中已經基本沒有不斷重復自身的現象;像“[話筒] [思考] [吃驚] ”這種文本,應該是原文本中的表情,在對文本的處理中我們並沒有將這些清洗掉,因此依然出現在預測摘要中。不過例子二還是出現了句子不是很通順的情況,在輸出句子的語序連貫上還有待改進。
4.4 Results
1. 在復現原論文的基礎上,將模型方法應用在中文數據集上,取得了一定效果。
2. 可以看出指針生成網絡通過指針復制原文中的單詞,可以生成新的單詞,解決oov問題;其次使用了coverage機制,能夠避免生成的詞語不斷重復。
3. 在語句的通順和連貫上還有待加強。