Text Summarization with Pretrained Encoders:如何將 BERT 應用在 Text Summarization Task 上

Abstract

不管要解任何問題,從 BERTELMoGPT 等等,這些大家琅琅上口的各種預訓練模型開始進行,在 NLP 領域基本已經變成起手式。筆者今天要介紹的論文 Text Summarization with Pretrained Encoders 也不例外,論文分析如何有效地將 BERT 應用在 text summarization task 上,以及預訓練模型對 text summarization task 的影響,並且提出一個可以做 extractive 和 abstractive models 的通用框架。

論文的 contribution 為:

  1. 分析 document encoding 對於 summarization task 的重要性
  2. 提出如何有效地應用預訓練模型在不管是 extractive 或 abstractive 下的 summarization task

接下來,筆者將先快速介紹跟 text summarization task 相關的各種名詞,幫助不熟悉此領域的讀者了解讀懂論文需要的知識,以防鴨子聽雷的狀況,之後再進入正題,熟悉的讀者則可直接跳往正題閱讀。

Background

BERT

BERT 這個由 Google 在 2019 所提出的模型相信大家都不陌生,在提出後就刷了各種任務的榜,成了 NLP 領域無人不知無人不曉的模型。論文中對 BERT 的輸入進行了修改,讓其更好的對應 summarization task 的特性,為了對比,筆者先複習一下原版的 BERT 的輸入。

圖(1)Architecture of the original BERT model

一開始要輸入的 document 會經過處理加上兩種特殊符號[CLS]與[SEP]。[CLS]插在整個 document 的頭,代表整個 document 的開始,[SEP]插在 document 中每個 sentence 的尾,用來區分不同的 sentence。經過處理後,document 表示成 Input \, document = [w_1, w_2, \dots , w_n]w_i 代表 document 裡的第 i 個 token。在 BERT 裡,每個 token w_i 總共會有三種 embedding,token embeddings 代表 token 的意思,segmentation embeddings 代表 token 屬於哪個句子,position embeddings 代表 token 在 sentence 中的位置,將這三種 embedding 相加起來就變成了 BERT 的輸入 X = [x_1, x_2, \dots , x_n],之後丟進由幾層 Transformer Layers 疊起來組成的 Encoder:

\widetilde{h}^l = LN(h^{l-1} + MHAtt(h^{l-1}))
h^l = LN(\widetilde{h}^l+ FFN(\widetilde{h}^l))

h 是 hidden states,h^0 = X;LN 是 Layer normalization;MHAtt 是 multi-head attention;l 是疊幾層 Transformer Layers 的層數。最終 BERT 會輸出 T = [t_1, t_2, \dots , t_n],每個 t_i 是擁有語意資訊的 word embedding。

Extractive and Abstractive Summarization

在 text summarization 有兩種不同的系統,分別是 extractive 與 abstractive 兩種。

extractive summarization 為藉由在文章裡找尋重要的句子,並把找出來的句子接在一起,當作這篇文章的 summary。在模型訓練上,則是當做 sentence classification 的任務,藉由分類每個句子是否為重要句子,來得到要做為 summary 的句子。

abstractive summarization 則是將原始文章餵給模型吃,並直接吐出一段 summary 的方法,是一個 sequence to sequence 的問題。abstractive 相較 extractive 困難的點為,abstractive 需要能夠產生原本不存在於文章的詞彙或句子,並且要符合語言的規則。

Fine-tuning BERT for Summarization

在上面筆者簡單複習了 BERT 與 summarization task 的兩種方式,現在就讓我們來看看如何微調 BERT,在任務上做得更好吧。

Summarization Encoder

論文中指出,將 BERT 直接應用於 summarization,顯然是會有問題的。首先在做 summarization 上,我們通常會使用 sentence embedding 來做相關任務,但 BERT 的訓練方式 MLM(克漏字、填空),學習重點是放在 word embedding 上,而不是 sentence embedding ,另一個問題是即便 segmentation embeddings 可以區分不同的句子,在原始 BERT 中只有使用 sentence pair 進行訓練,就是兩個句子組合在一起,在 summarization 上,則通常需要使用大於兩個的多句子接在一起當作輸入。

鑒於上面提到的問題,論文修改了原始 BERT 的輸入方式,讓 BERT 可以更好的處理 summarization,並提出針對 summarization task 的 BERTSUM

圖(2)Architecture of the BERTSUM

在 BERTSUM 中,document 裡的每一段 sentence 前面都會額外插入一個[CLS]符號,這裡這麼做有兩個目的,一個是為了凸顯個別句子(individual sentences),另一個是,作者會將這些[CLS]符號的輸出當作每段句子的 sentence embedding,用於後續的預測任務。segmentation embeddings 也被修改為 interval segment embeddings,用於更好的區分多句子,規則是 sent_i 會根據它是奇數句或偶數句來決定是 E_AE_B。舉個例子,一個 document 包含 [sent_1, sent_2, sent_3, sent_4, sent_5],那麼這個 document 的 segment embeddings 為 [E_A, E_B, E_A, E_B, E_A]。經過這樣的設計,模型就能學習到不管是句子表示,相鄰的句子,或是整個 document 的意思。

論文中分別提出如何將 BERTSUM 應用於 Extractive 和 Abstractive Summarization 上,接下來就讓我們來一個一個看看是怎麼做的。

BERTSUMEXT:BERTSUM of Extractive Summarization

在 background 中,筆者介紹過 Extractive Summarization 其實就是對每個句子做二元分類,分類是否要拿來作 summary,所以我們要做的就是把每段句子的 sentence embedding 取出來並丟進分類器進行預測。論文中介紹了在 BERTSUM 上,再加入了幾層 Transformer layers 來做 Extractive Summarization 的模型,並命名為 BERTSUMEXT。整個模型構造很簡單,以下開始介紹:

在 BERTSUM 中,剛剛筆者說過每段句子的 sentence embedding 由[CLS]符號的輸出而來。因此,我們可以將 BERTSUM 的輸入看做是很多個句子,d 代表一個 document 包含句子 [sent_1, sent_2, \dots ,sent_m]sent_i 是在 document 裡的第 i 個句子,第 i 個[CLS]符號的輸出寫作 vector t_i ,是 sent_i 的 sentence embedding。通過 BERTSUM 我們有了每個句子的 sentence embedding,接下來就是將它們丟進額外加入的 l 層 Transformer layers,獲取 document-level 的特徵:

\widetilde{h}^l = LN(h^{l-1} + MHAtt(h^{l-1}))
h^l = LN(\widetilde{h}^l+ FFN(\widetilde{h}^l))

h 是 hidden states,h^0 = PosEmb(T)T 代表 BERTSUM 輸出的 sentence embeddings。

模型的最後就是一個簡單的 sigmoid classifier:

\hat{y}_i = \sigma(W_0h_i^L+b_o)

h^L_i 就是 Transformer第 Lsent_i 的輸出。

BERTSUMABS、BERTSUMEXTABS:BERTSUM of Abstractive Summarization

在 Abstractive Summarization 中,這篇論文使用的模型是普通的 encoder-decoder 架構。encoder 是 pretrained 過的 BERTSUM,decoder 則是隨機初始化的 6 層 Transformer layers。這裡有個小重點,encoder 與 decoder 必須使用不同的 optimizer 來訓練,原因是,如果直接一起訓練的話,當 decoder 還在 underfitting 時,經過 pre-trained 的 encoder 可能已經 overfitting 了。因此,訓練這個網路時,encoder 會使用較小的 learning rate,decoder 則使用較大的 learning rate,讓整個訓練更穩定。這種架構與訓練方式的網路,論文稱為 BERTSUMABS

論文中也同時提出一個兩階段式 fine-tune 方法,稱為 BERTSUMEXTABS。根據先前的經驗(Gehrmann et al., 2018; Li et al., 2018),先做 extractive summarization 可以提升模型在 abstractive summarization task 的表現,所以 BERTSUMEXTABS 會先在 extractive summarization task 上 fine-tuned encoder ,然後再在 abstractive summarization task 上 fine-tuned 第二次。這樣訓練出來的模型稱為 BERTSUMEXTABS

Result

表(1)資料集 summary,最後一欄 percent of novel bi-grams in gold summary,代表答案(gold summary)是否不曾出現在原文中,數字越高代表越少重複。

論文總共實驗了三種不同的資料集,它們的 summary 風格各不相同,可能是文章的重點摘要、一句總結,或由原文剪貼而成,這樣我們就可以評估模型在不同 summary 形式下的表現。

表(2)ROUGE F1 在 CNN/DailyMail test set 的表現 (R1 and R2 are shorthands for unigram and bigram overlap; RL is the longest common subsequence)

表 (1)第一行的 ORACLE 是 extractive ORACLE system,在這裡用來當作表現的 upper bound。LEAD-3 則是直接選文章的前三句當作 summary 的表現。TransformerEXT 代表架構與 BERTSUMEXT 一樣,但是沒有經過 pre-trained,直接在 summarization task 上做訓練,TransformerABS 同理。由表中可以觀察到,BERSUM 相比之前的模型表現都有提升,但也沒有提升太多,原因讓我們繼續看看別的資料集的實驗結果就會知道。

ROUGE Recall 在 NYT test set 的表現
表(3)ROUGE F1 在 XSum test set 的表現

當我們一次把 BERTSUM 在 CNN/DailyMail(表 1)、NYT(表 2)跟 XSum(表 3)的表現拿出來比較,原因就很明顯了。有發現嗎?在 percent of novel bi-grams in gold summary 低的資料集,BERTSUM 的表現就顯得普通,沒有驚人的表現,跟 LEAD-3 的表現也差距不大,並且 BERTSUMEXT 會是表現較好的模型,這就顯示了因為 gold summary 可以大幅地從原文中拼貼出來,所以模型之間的表現就拉不開差距。所以在這種發現下,如果去看 BERTSUM 在 XSum 的表現,BERTSUM 的厲害就徹底展現出來了,不僅贏過了 ORACLE system,表現相比其他模型也大幅上升。

另一個值得關注的點則是 pre-trained 的重要性,對比有 pre-trained 與沒有 pre-trained 的模型,可以發現表現差距很大,尤其在 abstractive summarization 上,最多可以有超過 10% 的差距,再一次呼應論文標題的 with Pretrained Encoders 這句話。

Conclusion

本文帶讀者了解了 Text Summarization with Pretrained Encoders 這篇論文,其基於 BERT 進行改良,改造成適合用於 summarization task 的架構,並提出 BERTSUMEXTBERTSUMABSBERTSUMEXTABS 三種模型,用簡單的方式,大幅提升了 BERT 在 summarization task 上的表現。

SimCSE:Contrastive Learning 在 NLP 的應用,Sentence Embedding 的新 SOTA

Abstract:

去年(2020)在 Computer Vision 領域,Contrastive Learning(CL,對比學習)顯得風風火火,許多用上 Contrastive Learning 概念的模型相繼被提出,最出名的例如:Google 提出 SimCLR,Facebook 提出 MoCoMoCo V2。Contrastive Learning 可套用於不同的學習上,不管是 supervised 或 unsupervised,如:SimCLR 藉由 Self-Supervised Learning(SSL,自監督學習),不只不需要人工標註的資料集即可訓練,表現也相當出色,在 ImageNet 上甚至能跟使用 Supervised Learning 的模型表現相當。那麼既然 Contrastive Learning 這麼厲害,在 NLP 的領域一定也有用上的地方對吧?答案是是的,今天筆者就要來介紹 SimCSE: Simple Contrastive Learning of Sentence Embeddings 這篇論文。

Introduction

圖(1)SimCSE 成為新 SOTA

SimCSE 關注的問題是 universal sentence embeddings,這在 NLP 領域是個基礎且重要的問題。SimCSE 提出一個應用 Contrastive Learning 的訓練方法,來訓練預訓練語言模型(如:BERT 或 RoBERTa),可以使用不管是 unlabeled 或 labeled data 來訓練 sentence embeddings。在使用 unlabeled data 進行 unsupervised learning 的情況下,SimCSE 在 STS(Semantic textual similarity)任務的表現上,不只是遠超其它之前提出的 unsupervised models,甚至能超越 supervised models,使用 labeled data 進行 supervised learning 則更不用說了,穩穩的成為 STS 任務的新 SOTA。如果將 SimCSE 訓練出的模型,遷移到其它任務進行微調,則表現也能比之前提出的其它方法有相當抑或好上一點的表現。

接下來筆者將慢慢講解 SimCSE,包括:

  • Contrastive Learning
  • Unsupervised SimCSE
  • Supervised SimCSE
  • Result
  • Conclusion

Contrastive Learning

圖(2)CL key idea(figure from blog

左:憑記憶畫出來的美元鈔票。右:在有美元鈔票的情況下畫出來的圖。

圖(2)說明了一件事,即使我們見過很多次鈔票,也無法重現出完美的鈔票,但是我們仍然能夠藉由某些關鍵特徵,就能輕易地辨別是否為鈔票。藉此,也就表示其實模型並不需要記住每一個細節,只要能擷取出關鍵特徵,就可以區分不同的東西,這就是 Contrastive Learning 的核心思想。

圖(3)GIF for SimCLR (from Google AI Blog

這裡藉由 SimCLR 圖(3)是怎麼做的,來簡單了解 CL。(1)先 sample 一些 data(batch);(2)對 batch 裡的 data 做不同的 augmentation;(3)一樣的 data 的 representation 要互吸,不一樣的要互斥,簡易流程就是這樣。一樣要拉近,不一樣要拉遠,這就是 CL 的目標。

接下來我們就來看看 SimCSE 是怎麼設計 CL 的訓練。

Unsupervised SimCSE

圖(4)Unsupervised SimCSE 的 positive pair 來自對自己做不同的 dropout,negative pair 則來自 batch 裡其他的 data。

Unsupervised SimCSE 最關鍵的地方就是對如何建構 positive pair 的回答,對於 CV 領域有很多方法,如:cropping、flipping、distortion、rotation 這些 augmentation method,NLP 在近年也有許多方法被提出,如:word deletion、reordering、substitution。但作者發現這些方法反而會傷害到模型的表現,對此作者最後給出的答案非常簡單,那就是 dropout,dropout 可以作為 minimal data augmentation,並達到比之前其它的 augmentation 方法更好的表現。

所以要建構 positive pair 就非常簡單,作者直接利用原始 Transformer 裡的 dropout,讓一個句子分別通過模型兩次,因為 dropout 的關係,產出的兩個 sequence embedding 就會有些許的不同,從而變成一對 positive pair。接下來就是照上面所敘述的 CL 概念進行訓練就好。

Supervised SimCSE

圖(5)Supervised SimCSE 使用 NLI datasets 並利用 entailment pairs 當作 positives,contradiction pairs 為 hard negative,batch 裡的其它 data 為 normal negative。

剛剛我們提到作者提出使用 dropout 來建構 positive pairs,並進行 unsupervised learning。接下來作者也研究了,如果我們使用 supervised datasets 來進行 supervised learning 能不能得到更好的表現?

圖(6)使用不同的 supervised dataset 訓練 SimCSE

圖(6)回答了上述的問題,作者實驗了各種不同的 supervised dataset ,結果顯示使用 supervised dataset 訓練的確有效提高了模型的表現。並且作者提出利用 NLI datasets 的 contradiction pairs 當作 hard negative pairs,可以更進一步提高表現。作者也發現之前的方法(如:Sentence-BERTStackSeq2Seq)常使用的 dual encoder framework 反而會降低模型的表現。

Results

最後我們來看看 SimCSE 的各項表現。

圖(7)STS Dataset 的表現

圖(7)顯示我們同時評估 supervised 和 unsupervised SimCSE 在每一個 STS 任務的表現。不管是哪一種 learning,SimCSE 在每個任務上表現相較之前都有所提升,綜合表現遠超之前的模型。SimCSE-RoBERTa_large 在只使用 unsupervised learning 下,表現甚至已經跟之前的 supervised model 旗鼓相當。

圖(9)Transfer task 的表現

圖(9)顯示當我們遷移 SimCSE 去做其它 NLP 的下游任務,SimCSE 都能有相當或是更好一點的表現。作者也發現,如果在訓練時加入 MLM(Masked Language Model,克漏字)一起訓練,根據下游任務的不同,可能會有幫助或是傷害。

論文還有很多有趣的實驗,像是 pooler(如何產出 sequence embedding)的選擇、batch size、其它 unsupervised 方法在訓練的表現等等,因為篇幅關係,筆者就只取一部分講解,有興趣的人可以去看看原始論文。

Conclusion

SimCSE 提出利用 Contrastive Learning 進行訓練,可以使用不管是 Supervised 或 unsupervised learning 進行訓練,方法簡單暴力,在 STS 任務上成為新 SOTA。

Mixed Precision Training(Pytorch 範例程式)

Mixed precision training 是由百度與 NVIDIA 發表於 ICLR 2018 的論文,其提出一種訓練方法,使得在使用 half-precision(FP16) 訓練模型的情況下,也能達到與使用 single-precision(FP32) 訓練相當的表現,且得到加快訓練速度與降低顯存需求的效果。

Motivation

在當今我們使用的模型變得越來越大,資料也越來越多,結果就是需要更高的算力與更多的顯存來訓練模型。
但當我們的資源不敷使用,銀彈也不夠直接買新設備時該怎麼辦呢?
這個時候就需要使用一些技法來設法降低資源需求,從而達到我們的目標,而 mixed precision training 就是其中一種方法。

影響 deep learning model performance 之因子有以下三種:

  • arithmetic bandwidth
  • memory bandwidth
  • latency

降低使用的精度,可以降低其中兩種因子的影響。
memory bandwidth 的壓力會降低,因為我們只需要更少的 bits 來存儲同樣的參數。
計算時間也會跟著降低,因為降低了計算的精度,從而得到了更高的 throughput。
不過降低精度也不是沒有副作用的,因為能夠表達的數值比原本更小。
由下圖所見,以 FP16 為例,它就只包含 FP32 的一半範圍,當我們需要使用到超過 FP16 的表達範圍時,便會產生問題,所以才需要提出 mixed precision training。

source: https://on-demand.gputechconf.com/gtc/2017/presentation/s7218-training-with-mixed-precision-boris-ginsburg.pdf

Implementation

論文將使用 half-precision(FP16) 來訓練,而為了避免 FP16 可表達數值範圍較小產生的問題,利用了三種方法來避免模型表現下降:

  • fp32 master copy of weights
  • loss scaling
  • arithmetic precision

FP32 Master Copy of Weights

在 mix precision training 中,weights、activations、gradients 都使用 FP16 儲存,並且有一份使用 FP32 儲存的 weight,我們稱它為 FP32 master weights。
訓練流程為,在每一次疊代開始時, FP32 的 weight 會被縮成 FP16,在經過 forward 跟 backward 之後,會得到 weight gradient,利用它去更新 FP32 的 weight,如此循環。
這樣我們可以保持相近於使用 FP32 網路的精確度,並對比使用 FP32 訓練,只需一半的儲存跟算力需求。

source: https://arxiv.org/abs/1710.03740

有兩個理由使我們需要 FP32 master weights。
第一,當我們的 weight gradient 乘上 learning rate 太小了,使之無法表示於 FP16,那就會變成零,weight 就無法更新了,由下圖可以看到約有 5% 的 weight gradient 小於 FP16 可表示的範圍而變成 0。
第二,當要更新的 weight 的值太大,即使要更新的 weight 值可以用 FP16 表示,當它更新的時候也會因為加法在進位的情況下,同樣使值變成 0。使用 FP32 weight 來更新則可避免遇到這些問題。
論文中也做了個實驗,訓練了一個模型,在使用與不使用 FP32 master weights 的情況下,對比使用 FP32 的收斂情況,由下圖可以看到使用 FP32 master weights 相比直接使用 FP32 有很相近的效果,而不使用 FP32 master weights 訓練效果就差非常多了。

source: https://arxiv.org/abs/1710.03740

Loss Scaling

論文中指出大多數的 gradient 是負指數。並且做了個實驗來驗證,作者訓練了一個網路並且撈出每一層的 activation gradient 畫成圖,由下圖可以看到,大部分的 gradient 都是負的,並且很多小於 FP16 的表示範圍而變成了 0,另一個重要的部分則是,有一大部分 FP16 的可表示範圍沒有任何 gradient 的,這個就成了可以使用 loss scaling 的地方。

source: https://arxiv.org/abs/1710.03740

loss scaling 的想法很簡單,既然大多數 gradient 都很小,超過了 FP16 的表示範圍,那就乘上一個值讓它先變大,在更新時再縮回來不就好了。
具體來說,就是當 forward 計算出 loss 之後,把 loss 乘上一個值,這樣在 backward 時,就可以確保所有 gradient 都被一致的縮放。
然後在最後要更新 weight 時,我們在把 gradient unscaled 回來。如此就很簡單的減緩了上述的問題。

Arithmetic Precision

大多數的 deep learning network 主要使用以下三種計算:vector dot-products、reductions 與 point-wise operations。
作者提到,他們發現有些網路為了保持模型的準確度,需要將 FP16 相乘時累加到 FP32,然後在寫進內存前縮回 FP16。
在現在新的 NVIDIA GPU 上,NVIDIA 新增了 Tensor Cores,原生支援了這種計算。

Conclusion:

讓我們總結一下 mixed precision training 的流程:

  1. 使用 FP32 存 master copy of weights
  2. 在每次疊代
    1. copy 一個 FP16 的 weights
    2. Forward propagation(FP16 weights and activations)
    3. 把 loss 乘上 scaling factor S
    4. Backward propagation(FP16 weights, activations, and their gradients)
    5. 把 weight gradient 乘上 1/S
    6. 完成 weight 的更新

這裡就不細講實驗結果了,只要知道作者實驗了很多不同的網路,並且都達到與使用 FP32 訓練差不多的準確度就好,有興趣的人可以去察看原始論文。

Code

最後在 pytorch 1.6 終於也原生支援了 mixed precision training,而不用再使用 Nvidia 出的 apex 來做,簡單了許多。
這裡也放上最基礎的在 pytorch 使用 mixed precision training 的方法。

import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as dset
from torchvision import datasets, transforms, models
from torch.cuda.amp import autocast, GradScaler

# create transform
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5,), (0.5,)),]
)
# Data
trainSet = datasets.MNIST(root='MNIST', download=True, train=True, transform=transform)
trainLoader = dset.DataLoader(trainSet, batch_size=64, shuffle=True)
# Model
class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.base = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
            nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2),
        )
        self.fc = nn.Linear(32*7*7, 10)

    def forward(self, input):
        feature = self.base(input)
        feature = feature.reshape(feature.size(0),-1)
        output = self.fc(feature)
        output = nn.functional.log_softmax(output, dim=1)
        return output
model = Model().to('cuda:0')

optimizer = optim.SGD(model.parameters(), lr=1e-3)
criterion = nn.NLLLoss()
scaler = GradScaler()

for epoch in range(10):
    for input, target in trainLoader:
        input, target = input.to('cuda:0'), target.to('cuda:0')
        optimizer.zero_grad()

        with autocast():
            output = model(input)
            loss = criterion(output, target)

        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()

我們簡單宣告了一個模型並使用 MNIST 資料集進行訓練,基本上對比原本的寫法,只需要加上幾行 code 就可以自動完成,是不是很方便呢。
如果想知道更多,可以去看看官方的 文件