作者:哈工大SCIR碩士生 趙懷鵬
導讀:好的工具能讓人事半功倍。神經網路框架PyTorch具有很強的靈活性,並且程式碼可讀性很高,能夠幫助你快速實現自己的想法,因此在學術圈越來越流行。本文講解如何利用PyTorch搭建一個簡易的單文件抽取式摘要系統。
一:PyTorch簡介
PyTorch是一種非常簡單,優雅的動態圖框架。其介面和模組的設計比較清晰,能夠幫助你快速實現自己的想法。下面借用李飛飛教授公開課cs231n第8講Deep Learning Software的內容介紹下動態圖相比靜態圖的一些優勢。當然靜態圖也有自己的優勢,這裡不加討論,大家可以詳細看下這一講的內容幫助你挑選心儀的框架。下面介紹靜態圖和動態圖的一個重要區別:迴圈和條件判斷。
① 條件判斷
假設我們需要判斷的正負號來執行不同的條件。PyTorch版本
if z > 0:
...
else:
...
Tensorflow版本:
tf.cond(tf.less(z,0),f1,f2)
② 迴圈
PyTorch版本
for t in range(T):
...
Tensorflow版本:
tf.fold1(f,arg1,arg2)
通過上面的虛擬碼我們可以看到PyTorch更加貼近python語法,讓我們寫程式碼更加自然。而Tensorflow需要嚴格遵守其定義的一套API。當然Tensorflow也有自己的優勢。這裡的例子僅展示了動態圖框架的靈活性。
二:抽取式摘要簡介
摘要是對資訊的高度概括,它能夠幫助我們在海量資料中快速獲取自己想要的資訊。在資訊爆炸的時代,只靠人工寫摘要是不現實的,因此我們需要一套自動摘要系統來幫助人們快速獲取想要的資訊。自動摘要按生成摘要的方式可以分為抽取式摘要和生成式摘要,按照文字型別可以分為單文件摘要和多文件摘要。
無監督學習是傳統抽取式摘要的主流方法。基於無監督學習的抽取式摘要可以分為三類[1]:A. 向量空間模型(The Vector-Space Methods). 其思想就是用向量表示句子和文件,然後計算其相似度來決定哪些句子重要。代表的方法有LSA 等。B. 基於圖的模型(The Graph-Based Methods). 圖模型把文章建模成圖,節點表示一個句子,邊表示句子見的相似或相關程度。圖模型的重要理論依據是中心理論,認為如果一個句子和周圍的句子都很相似,那麼這個句子是能夠代表文章資訊的。TextRank[2]就是其中重要的模型。C. 組合優化方法(The Combinational Optimization Methods).組合優化方法就是把抽取式摘要看做一個組合優化問題,代表的演算法有ILP(the integer linear programming method)和次模函式(submodular functions)。
近些年來,隨著深度學習在自然語言處理領域的廣泛應用,基於有監督學習的抽取式摘要的工作逐漸成為主流。其代表的工作有Jianpeng Cheng et.al[3]在2016年提出的基於Seq2Seq的抽取式摘要方法,並達到了當時的state of the art。另外這篇工作基於DailyMail資料集利用無監督學習構造一份抽取式摘要的資料集,本次實驗也是利用的這份公開資料集。今年AAAI上Ramesh et. al.[4]提出SummaRuNNer,並且達到了目前的state of the art。本文實驗就是復現這篇工作。下面簡單介紹一下這個模型。
圖1: SummaRuNNer模型
如圖1所示,該模型是有一個兩層RNN構成。最底層是詞的輸入,第二層是詞級別的雙向GRU,用來建模句子表示。第二層的每個句子的隱層各自做average pooling作為各自句子的表示。第三層是句子級別的雙向GRU,輸入是上一層的句子表示。得到隱層再做average pooling就能夠得到文件的表示。最後利用文件的表示來幫助我們依次對句子做分類。 最後分類層的公式如下:
圖2: 分類層公式
其中是到達第個位置的已經生成的摘要的表示,是文章的表示。 表示第個句子的資訊,計算的是當前的句子和文章表示的相似度,表示的是當前的句子能夠帶來多少“新”的資訊,接下來的三項分別表示絕對位置,相對位置和偏置。可以看出整個公式非常直觀,可解釋性很強。
最後Loss採用的負對數似然。在最終選取摘要的時候並不是簡單的分類,而是根據每個句子的概率高低排序,選擇概率最高的前幾句即可。關於模型再進一步的討論和細節讀者可以參考原文,這裡不再作討論和擴充套件。
三:程式碼實現
下面簡單介紹下用PyTorch實現該模型的關鍵步驟,具體細節可以參考完整程式碼:https://github.com/hpzhao/SummaRuNNer
① 模型訓練
瞭解一個框架最重要的就是看它如何訓練,測試。我們先把SummaRuNNer這個模型看成一個黑盒,看下如何利用這個模型來訓練一個神經網路。
首先我們申請一個模型:
net = SummaRuNNer(config)
net.cuda()
這裡的net就是我們的網路。接下來定義損失函式和優化器:
# Loss and Optimizer
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(net.parameters(), lr=args.lr)
接下來我們將一篇文章的句子作為模型的輸入用來得到每個句子的分類概率,這也就是前向過程:
outputs = net(sents)
接下來是反向過程,當我們得到Loss之後就可以對其求梯度了:
optimizer.zero_grad()
loss = criterion(outputs, labels)
loss.backward()
這裡我們首先要清空上一次計算存留的梯度值,然後計算Loss,最後再用backward()這個函式來自動求梯度,這樣看是不是很簡單直觀呢。我們求過梯度之後就能進行反向傳播了:
# gradient clipping
torch.nn.utils.clip_grad_norm(net.parameters(), 1e-4)
optimizer.step()
這裡做了gradient clipping,為了學習更加的穩定。最核心的就是optimizer.step()這一步利用我們之前定義好的Adam演算法來進行引數更新。
至此我們可以說完成了利用PyTorch來訓練一個神經網路。比起Tensorflow定義了一套自己的語法框架,PyTorch可以說是非常簡單,直觀。最後我們儲存訓練的模型:
torch.save(net.state_dict(), args.model_file)
② 模型測試
模型測試最核心的是載入模型,載入模型之後我們可以通過之前的前向過程得到每個句子的預測概率。
net = SummaRuNNer(config).cuda()
net.load_state_dict(torch.load(args.model_file))
for index, docs in enumerate(test_loader):
doc = docs[0]
x, y = prepare_data(doc, word2id)
sents = Variable(torch.from_numpy(x)).cuda()
outputs = net(sents)
至此我們已經完成了模型的訓練和預測部分的核心程式碼。
③ 網路搭建
在上面的例子中我們把SummaRuNNer模型看成一個黑盒,通過傳入篇章來得到我們想要的結果。接下來我們需要搭建網路。
PyTorch搭建網路一般需要繼承nn.Module這個類,並實現裡面的forward()函式。雖然一開始感覺不太靈活,但nn.Module為我們封裝了一些操作,為我們程式設計帶來便利,例如net.parameters()可得到網路所有需要訓練的引數。另外,這種方式也讓程式碼可讀性更高。下面是利用 nn.Module搭建網路的程式碼框架:
class SummaRuNNer(nn.Module):
def __init__(self, config):
super(SummaRuNNer, self).__init__()
...
def forward():
...
接下來我們來完善前向過程。結合模型圖,我們首先搭建詞級別RNN:
# word level GRU
word_features = self.word_embedding(x)
word_outputs, _ = self.word_GRU(word_features)
接下來我們搭建句子級別RNN:
# sentence level GRU
# 句子級別RNN的輸入是上一層詞級別RNN的隱層做average pooling
sent_features = self._avg_pooling(word_outputs, sequence_length)
sent_outputs, _ = self.sent_GRU(sent_features.view(1, -1, self.sent_input_size))
接下來我們利用句子級的GRU來得到篇章的表示:
# document representation
doc_features = self._avg_pooling(sent_outputs, [[x.size(0)]])
doc = torch.transpose(self.tanh(self.fc1(doc_features)), 0, 1)
最後是分類層,我們根據前面得到的表示來實現上述公式:
# classifier layer
outputs = []
sent_outputs = sent_outputs.view(-1, 2 * self.sent_GRU_hidden_units)
# 初始化當前摘要表示
s = Variable(torch.zeros(100, 1)).cuda()
# 分類層
for position, sent_hidden in enumerate(sent_outputs):
h = torch.transpose(self.tanh(self.fc2(sent_hidden.view(1, -1))), 0, 1)
position_index = Variable(torch.LongTensor([[position]])).cuda()
p = self.position_embedding(position_index).view(-1, 1)
content = torch.mm(self.Wc, h)
salience = torch.mm(torch.mm(h.view(1, -1), self.Ws), doc)
# 這裡用tanh(s)而不是直接用s的原因是讓s的值保持在一定體量
novelty = -1 * torch.mm(torch.mm(h.view(1, -1), self.Wr), self.tanh(s))
position = torch.mm(self.Wp, p)
bias = self.b
Prob = self.sigmoid(content + salience + novelty + position + bias)
s = s + torch.mm(h, Prob)
outputs.append(Prob)
return torch.cat(outputs, dim = 0)
至此我們完成了摘要網路的構建。
四:總結
通過上面的例子我們對PyTorch有了一個比較直觀的理解。初學者可以看一下PyTorch官網的入門教程:Deep Learning with PyTorch: A 60 Minute Blitz.
參考文獻
[1] Chen K Y, Liu S H, Chen B, et al. Extractive broadcast news summarization leveraging recurrent neural network language modeling techniques[J]. IEEE/ACM Transactions on Audio, Speech and Language Processing (TASLP), 2015, 23(8): 1322-1334.[2] Mihalcea R, Tarau P. TextRank: Bringing Order into Text[C]//EMNLP. 2004, 4: 404-411.[3] Cheng J, Lapata M. Neural summarization by extracting sentences and words[J]. arXiv preprint arXiv:1603.07252, 2016.[4] Nallapati R, Zhai F, Zhou B. SummaRuNNer: A recurrent neural network based sequence model for extractive summarization of documents[J]. hiP (yi= 1| hi, si, d), 2017, 1: 1.
本期責任編輯: 張偉男
本期編輯: 劉元興