Contextual Data Augmentation


Posted by Mars.Su on 2020-10-27

本篇概要

根據上一篇[NLP Data Augmentaion常見作法]我們都能基本知道一些常見的在NLP的Data Augmentation的做法,但其實大多數都是規則性的方式居多,我也相信很多人看完第一篇會覺得『有沒有更聰明、更符合ML的做法』,答案是『有的』。
所以,接下來我們將進入到關於利用『ML』跟『Contextual(上下文)』的對應作法,所以這篇決大部分會以探討論文的為主軸,也會適時將我自己的認為重要的段落貼出來說明。

今天要探討的論文為以下兩篇:

  1. Contextual Augmentation:Data Augmentation by Words with Paradigmatic Relations(Kobayashi, 2018)
  2. Conditional BERT Contextual Augmentation(Wu et al., 2018)

其實這兩篇的核心精神是一樣的,差別在於採用的模型方式不同。第一篇是用常見的CNN,RNN等基本DL模型;第二篇則是運用當代最火紅的Pretrained model - Bert

一、Contextual Augmentation:Data Augmentation by Words with Paradigmatic Relations(Kobayashi, 2018)

Background & Introduction

一開始作者先介紹NLP目前基於Neural network幾個常見的應用,像是文字分類(Socher et al., 2013;Kim, 2014)、機器翻譯(Sutskever et al., 2014),其中提到Data Augmentaion技術往往主要被應用於電腦視覺(Simard et al., 1998; Krizhevsky et al., 2012;Szegedy et al., 2015)或是語音處理(Jaitly and Hin- ton, 2015;Ko et al., 2015),但是在NLP文字領域上其實有一定的困難度的,作者認為文字無法像圖片一樣能一個通用的轉換或縮放後依舊保持原先物件的含義。

原文敘述
In natural languages, it is very difficult to obtain universal rules for transforma- tions which assure the quality of the produced data and are easy to apply automatically in various do- mains.

因此在這樣的前提下,作者開始提到過往在做文字Data Augmentation的作法,其中有針對先前學者對於文字Data Augmentation做簡單介紹跟提出其中的問題點,如下:

  1. 基於規則跟分群,再加入人工干涉
    簡單來說就是在分類之前先透過unsupervise learning(分群)的方式來做第一部的分類,後續再透過人工的方式去調整參數,或是cluster的刪除與合併。但該方法的缺點就是需要額外太多成本去干預了,而且效果的不確定性也高。

  2. 基於相近詞
    該方法在上一篇有提到過,其實就是利用詞的相近詞做替換,進而增加文字的資料量,作法可以用WordNet(Miller, 1995; Zhang et al., 2015)或Word Similarity(Wang and Yang, 2015)。而該方法的缺點是句子不夠多樣性且能增量的空間有限,甚至會過於拘限於句子本身的語法。舉例如下:

    這個"演員"很有趣 -> 這個"角色"很有趣 (基於相近詞的替換)
    

    但其實會發現有多詞彙都可以用在這個句子語法上,例子如下

    這個"電影"很有趣
    這個"影集"很有趣
    這個"劇本"很有趣
    ...等
    

    所以如果採用『相近詞』的話,對於文字的Augmentation就會少一些多樣性。

    原文出處
    synonyms are very limited and the synonym-based augmentation cannot pro- duce numerous different patterns from the origi- nal texts.

Proposed Method

所以依據前面所敘述的問題點,該論文的作者就是想透過一些方式來除了可以增加文字的資料量之外,同時也可以增加資料量的過程之多樣性(various),因此作者利用了 Bi-direstioal RNN的LM來做實作,其概念架構圖如下:

作者將此作法稱之為『Contextual Augmentation』,因為Bi-direstioal RNN會依序從左至右、從右至左來做順序訓練,所以藉由這個方式來確保word的預測機率是根據上下文出來的,再者對於位置i的word預測必須根據鄰近的word來做推估。

原文敘述
For prediction at position i, the model encodes the surrounding words individually rightward and leftward

這裡額外補充一下,我們也可以將Bi-direstioal RNN想像成一個聯合機率分佈,只是依照不同的順序來取的,公式參考如下

然而作者認為在一定的語法規範上,透過上下文的方式來做詞的預測替換,可以達到多樣性(various)的效果。但其實這樣的思考還會有一個小問題,如果今天替換的詞導致原本整體句子的含義造成偏差,其實這對於Dataset來說就是Noise。所以作者提出了另一個條件來做約束,就是加入『句子本身Label』變成label-conditional architecture

上面這個靈感是來自於Stanford Sentinment Treebank(SST)(Socher et al., 2013),這出處的dataset是個電影評論的文字資料,其中這資料的語句會帶上一個標記,原文例子如下:

原文敘述
“the actors are fantastic.”, is annotated with a positive label.

作者認為利用這樣的方式,即便將actor更換成非actor的相近詞(ex.movies, stories, songs)對於整體句子的label仍然呈現『positive label』的狀態。

然而在計算contextual augmentation中,作者有提到如何從先驗的機率分佈當中挑選topk的word來做樣本的擴充,概念原文如下:

接著在前面有提到除了依據上下文來做該word的推估之外,還要額外加入label作為Conditional Constraint以避免所生成出來的語句不合乎原先語句的含義,原因舉例來說明

'the actors are fantastic'. -> 原本label 是 'positive'
如果此時沒有加入只是單純預測 'the actors are '的下一個字時,
是有可能會出現像是'bad'或'terrible'的字詞,則會使與原本語意不符
(positive -> negative)

因此作者認為最後應該要把label embedding到Bi-directional的hidden layer來作為條件約束。

Experiment

在這篇論文的實驗當中,作者採用了6種Dataset以及2種類型的Neural Network來做LM,來呈現實驗成果,採用的標準是平均分類的Accuracy,條列如下:

  1. Dataset
  • SST2 -> 資料總共含有2個label (positive, negative)
  • SST5 -> 資料總共含有5個label
  • Subjectivity -> 資料總共含有2個label (subjective, objective)
  • MPQA -> 資料總共含有2個label (short phrases, sentence)
  • RT -> 電影評論資料集 (positive, negative)
  • TREC -> 資料集含有6種問題類型(person, location, etc.)
  1. LM模型的Neural Network (classifier)
  • CNN

    convolutional filter of {3,4,5}, Max-pooling, Word Embedding,中間FNN採用ReLU作為activation function.

  • LSTM-RNN

    single latey LSTM and Word Embedding

以上兩種模型都有透過grid-search來查找最佳化Hyper-parameters,以及運用Dropout,且都採用adam做optimizer與用softmax作為output activation function。

最後,作者沿用相近詞的augmentation、還有純contextuall以及label-contextual來一起做比較,實驗成果如下:

從實驗結果來看,我們可以發現確實有達到最好的performance,甚至我們可以發現其實透過相近詞的效果還有些許退步。

Conclusion

在論文最後我們也可以看到如下的結果:

從這裡我們可以看到作者將原句"the actors are fantastic"分別標記上了positivenegative來做不同augmentation的效果,原則上我們不難發現其實生成出來的都圍繞著電影、演員等主題相關,差別在與每個句子label是positivenegative,藉此可以看到上下文跟label搭配出來的conditional architectures效果。

二、Conditional BERT Contextual Augmentation(Wu et al., 2018)

接下來要介紹這篇,其實核心精神與第一篇論文非常類似,包含用的實驗資料集、模型的架構流程幾乎可以說一模一樣,當然這邊有些重疊的地方就不再多做描述,只著重不一樣改進之處來做探討。

Background & Introduction

一樣先來一下背景介紹,其實這篇是搭上當年BERT這個Pretrained model順風車上來的,所以會發現他的發表時間與BERT的發表時間非常相近。

今天我們都知道BERT在目前的NLP領域當中可說是當紅炸子雞,經過了兩年到現在延伸了許多變型體來解決各式各樣的問題,包含AlBERT、DistillBERT、 KnowBERT、BioBERT、CommemBERT等等許多模型,所以我們不難想像BERT模型所帶來的影響與貢獻。

BERT

既然該篇論文主題都提到了BERT,我們來快速理解一下BERT的原理以及為何強大的原因。BERT全名為『Bidirectional Encoder Representation from Transformer』,他是擷取出Transformer(詳細架構後續在另外寫一篇)模型中的Encoder,對於BERT的重點我們掌握如下三點:

  1. Transfomer Encoder
  2. Unsupervise 學習大量文本
  3. 兩個pretrained目標 - MLM(Masked Language Model), NSP(Next Sentence Prediction)

其就是透過上面3種方式來成為一個可以套用到多個NLP任務的模型。但這邊不會太細談BERT的細節(後續也會獨立寫一篇),想再更細部理解的請點這裏

BERT Pretrained目標

這裡先快速帶一下BERT的Pretrained的目標,分別為MLM跟NSP,詳細運作原理如下:

  1. MLM(Masked Language Model)

    簡單來說就是『克漏字填空』,會隨機挑取句子中的一個word替換成[mask]這個字符來讓模型基於這樣的上下文,該word應該填入哪一個。(原論文是採用15%的word來做這樣的預訓練目標)

    好處:每個詞彙在不同的情境之下會有不同的representation(Contextual word repr)

  2. NSP(Next Sentence Prediction)

    用來判斷第二句『是否』為第一句的下一句

    好處:協助model理解句子之間的關係

誒等等,各位有沒有發現其中的MLM其實有部分概念重疊到前面第一篇的文章,就是根據『上下文』來推估該word是哪一個字,我們都知道BERT應用的是self-attention來取得雙向的representation,進而將上下文的資訊套入到model訓練。再者,該方法比之前的LSTM-RNN模型還要來得好,因為不用記錄太多的hidden-state以至於在整體的訓練跟計算效能都來得好一些。

而本篇論文的作者也發現到這個問題,再加上他也觀察到前一篇應用的Bi-directional LM是淺層連結(sinalge LSTM and Embedding layer),所以對於一些預測的能力會有一些限制。

沒錯這兩篇唯一的差別就是模型的不同,從原本的Bi-directional LM改成套用BERTmodel,接著運用其中的MLMpretrain目標來做data augmentation的效果,作者稱之為Conditional Bert,簡稱C-BERT;同時也把MLM機制改做成Conditional-MLM

原文敘述
We retrofit BERT to conditional BERT by introducing a new conditional masked language model1 task.

Related Work

此時我相信讀者會有另外一個疑問,那他是如何做到Conditional的?他是如何將label-conditional套用進來的呢?

作法其實很簡單,我們先回過頭來看一下原本BERT再輸入時的embedding,如下圖:

原先BERT輸入的Embedding分為3種,分別是token-embedding, segment-embedding, position-embedding,個別用途如下

  1. token-embedding -> 很單純就是每一個對應embedding後的tensor
  2. segmentation-embedding -> 用來代表句子的位置,同一個句子會有相同的embedding(ex.第一句為0、第二句為1)
  3. position-embedding -> 記錄每一個word的位置,因為原先Transformer並不會像RNN的LM來記住順序,所以要額外一個embedding來記錄每一個word的位置。

其中segmentation-embedding在原本是用來做NSP使用的,所以作者認為這個會與原句子之label無關(但作者尚未詳細說明原因),進而決定將segmentation-embedding改變成label embedding,藉此將句子的label套入進去做BERT的訓練。原因也是想利用這個方式來做文字增量時,除了生成word之外,同時也要滿足原先語句的含義。

原文描述
However, the segmen- tation embeddings has no connection to the actual annotated labels of a sentence, like sense, senti- ment or subjectivity, so predicted word is not al- ways compatible with annotated labels

其中,作者在論文中也很好心的將C-BERT的處理與訓練步驟利用pseudo code來做呈現,步驟如下:

這邊簡單白話說明一下

step1. 將segmentation embedding 更改成 label embedding
step2. 利用Conditional特性來做MLM pretrain Dataset D
step3. 將Dataset進行迭代,每次取出樣本時隨機mask k word,接著來預測word進而達到生成之效果
step4. 將新生成的D' 合併到原本的Dataset D
step5. 就可以來做下游要執行的任務

在這樣的步驟下,Conditioal-Bert所帶來的架構與效果如下:

我們可以看到label-embedding的用法,以及如何透過BERT的pretrained-model的MLM(Masked Language Modle)作法,進而去推估[mask]可能的word。

然而,在程式方面,仔細觀察它提供的github source code的運作原理,會發現他其實是把原先segment_id的值抽換成label_id填補的值,而不是直接改成新的變數,我想這樣講大家一定矇了吧,我來快速帶大家看一下,如下:

我們先簡單做一些變數假設,先設定一個句子跟他對應的label,然而我們先運用huggingface的pretrain BERT,先用bert-base-uncased的語言corpus,如下

from transformers import BertTokenizer, BertForMaskedLM

max_seq_length=64 ## 訓練時最長的句子為64 (後續會經過padding)
original_text = 'you will download a dataset of programming questions from Stack Overflow'
label_list = ['10']
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

接著我們先用Pretrained好的BERT tokenizer來做切詞,我們會發現他其實會把較複雜的word切分成wordpiece,如下:

## 會將一些word 切割成 wordpiece
tokens_a = tokenizer._tokenize(original_text) 
print('結果如下 /n', tokens_a)
結果如下
['you',
 'will',
 'download',
 'a',
 'data',
 '##set',
 'of',
 'programming',
 'questions',
 'from',
 'stack',
 'over',
 '##flow']

接下來就是他產生的embedding的input的關鍵function,透過這些function就可以產生BERT要Pretrain的input,請大家跟著我自己寫的註解去閱讀,如下:

def convert_tokens_to_ids(tokens, tokenizer):
    """將文字token根據BERT的tokenizer取得對應的token_id"""
    ids = []
    for token in tokens:
        token_id = tokenizer._convert_token_to_id(token)
        ids.append(token_id)
    return ids

[mask]隨機挑選的function:

def create_masked_lm_predictions(tokens, masked_lm_probs, masked_lm_labels, 
                                 max_predictions_per_seq, rng, tokenizer):
    """用來隨機挑選token來替換成[mask]"""

    #vocab_words = list(tokenizer.vocab.keys())

    cand_indexes = []
    for (i, token) in enumerate(tokens):
        if token == "[CLS]" or token == "[SEP]":
            continue
        cand_indexes.append(i)

    rng.shuffle(cand_indexes)
    len_cand = len(cand_indexes)
    output_tokens = list(tokens)
    num_to_predict = min(max_predictions_per_seq, 
                         max(1, int(round(len(tokens) * masked_lm_probs))))

    masked_lm_positions = []
    covered_indexes = set()
    for index in cand_indexes:
        if len(masked_lm_positions) >= num_to_predict:
            break
        if index in covered_indexes:
            continue
        covered_indexes.add(index)

        masked_token = None
        ## 80%的機率替換成[mask]
        if rng.random() < 0.8:
            masked_token = "[MASK]"
        else:
            ## 10%的機率保持不變
            if rng.random() < 0.5:
                masked_token = tokens[index]
            ## 10% 隨機替換tokens內的一個word
            else:
                masked_token = tokens[cand_indexes[rng.randint(0, len_cand - 1)]]

        """
            masked_lm_positions -> 要被替換成[mask]的位置index
            masked_lm_labels -> 主要會用來記住被替換成[mask]那些token原先的字對應的token_id
            output_tokens -> 後續要丟到model訓練的input (已經替換成[mask])

            ex. 
            input_tokens = ['[CLS]','you','will','download','a','data','##set','['SEP']']
            output_tokens = ['[CLS]','you','will','[mask]','a','[mask]','##set','['SEP']']
            masked_lm_positions = [3,5]
            masked_lm_labels = [-1,-1,-1,999,-1,105,-1,-1]

        """
        masked_lm_labels[index] = convert_tokens_to_ids([tokens[index]], tokenizer)[0]
        output_tokens[index] = masked_token
        masked_lm_positions.append(index)
    return output_tokens, masked_lm_positions, masked_lm_labels

產生features的function,如下

def extract_features(tokens_a, tokens_label, max_seq_length, tokenizer):
    """產生要訓練的features"""

    if len(tokens_a) > max_seq_length - 2:
        tokens_a = tokens_a[0: (max_seq_length - 2)]

    tokens = []
    segment_ids = []
    tokens.append('[CLS]')
    segment_ids.append(tokens_label)
    for token in tokens_a: ## 字串的token
        tokens.append(token)
        segment_ids.append(tokens_label) ## 直接把label放到segment_id 做替換
    tokens.append('[SEP]')
    segment_ids.append(tokens_label)
    """
        句子的有多長(包含了[CLS], [SEP]) 原先的label id 也跟著填補
        ex.    token -> ['[CLS]', 'you', 'will', 'download', '[SEP]'], label_id = 10
        segment_id   -> [10,10,10,10,10]
    """



    init_ids = convert_tokens_to_ids(tokens, tokenizer)

    ## 定義要隨機挑選的seed跟機率
    masked_lm_probs = 0.15
    max_predictions_per_seq = 20
    rng = random.Random(12345)
    original_masked_lm_labels = [-1] * max_seq_length

    """
        output_tokens -> 處理過後的token (如果被選定要mask 則會替換成[mask])
        masked_lm_positions -> 被選到要mask的token對應之index
        masked_lm_labels -> 被mask原本的word tokenize編號
    """
    (output_tokens, masked_lm_positions, 
    masked_lm_labels) = create_masked_lm_predictions(
            tokens, masked_lm_probs, original_masked_lm_labels, max_predictions_per_seq, rng, tokenizer)
    input_ids = convert_tokens_to_ids(output_tokens, tokenizer)

    print('masked_lm_positions', masked_lm_positions)

    # 會跟著token數 來產生對應的tensor -> 想像成句子的位置
    input_mask = [1] * len(input_ids)

    # Zero-pad 到 max_seq_length的長度.
    while len(input_ids) < max_seq_length:
        init_ids.append(0)
        input_ids.append(0)
        input_mask.append(0)
        segment_ids.append(0)

    assert len(init_ids) == max_seq_length
    assert len(input_ids) == max_seq_length
    assert len(input_mask) == max_seq_length
    assert len(segment_ids) == max_seq_length

    return tokens, init_ids, input_ids, input_mask, segment_ids, masked_lm_labels

所以透過上面的source code我們可以發現,其實他大致上的架構很像原本huggingface在BERT底層的code(詳細請點此),然後直接調用原先module的method跟function去做更改,基本上變數名沒有太大的變動,直接把值抽換掉(大家可以比較兩邊的segment_ids處理方式)。相信透過程式跟上面的架構圖去做搭配,大家可以略知一二了。

Experiment

這裡作者一樣舉出他用的Dataset與要比較的model,當然他也有與Kobayashi(2018)論文的模型做比較,包含像是CNN, LSTM-RNN等,其中一些的Hyper-parameters也是用Grid-search做設定(與第一篇論文作法相同);對於BERT model,作者也有提到tensorflow跟pytorch的作法跟github連結供參考。
然而對於採用的實驗Dataset,其實與第一篇的論文差不多,整理的表格呈現如下:

參數介紹
c: Number of target classes.
l: Aver- age sentence length.
N : Dataset size.
|V|: Vocab- ulary size.
Test: Test set size


上圖是作者將實驗比較後的結果狀況,當然就單數值上的呈現我們可以看到確實在C-BERT的成效與performance較好。

Conclusion

作者最後有在簡單說明一下 Conditional Bert的應用場景,像是sentence-classification task、inbalanced dataset等,利用該pretrained model的方式來快速做到data augmentation的效果。

總結

其實我們不難發現這兩篇的重疊性蠻多的,無論是原因甚至中間的作法,差別在於model的不同,所以對於我來說,我認為在Conditional Contextual Data Augmentation的核心精神就是『加入label embedding』來協助model訓練時不單單只填入要被預測的word,更重要的是也要合乎原本樣本句子的語意。

我們可以用一個比較簡單的思維去思考這件事情,如果今天沒有加入label embedding的話,其實句子中要被預測的word的可能性會非常多,很有可能會出現一些過往沒有的句子,此時就要再花額外的成本跟資源再去歸納語句。這也符合現實面的場境,為什麼我們要做Data Augmentation?就是在這樣的條件下資料量不足,所以我們希望可以再多生成同樣條件的資料,所以藉由『label』來做conditional embedding,我們也能確保這樣的資料生成都是圍繞在既有的label當中,就不需要再多花時間做額外的歸納與處理了。

當然這兩篇的論文篇幅不長,概念也都很淺顯易懂,但其實有些細節並沒有說得太清楚,像是替換segmentation-embedding的原因就沒有太多著墨了,還有許多細節是需要再多思考的空間,不過接下來我也會實作看看C-BERT來看看成效,後續有新的發現,會再回來補足這篇文章,且在額外補齊Transformer跟BERT架構理論。

Reference


#AI #NLP #BERT #Data-Augmentation #LM #Bi-Directional #RNN #Paper







Related Posts

Angular 9 SCSS Global Variable

Angular 9 SCSS Global Variable

[Release Notes] 20200918_v1 - Fix blog like/bookmark button

[Release Notes] 20200918_v1 - Fix blog like/bookmark button

自己寫 API

自己寫 API


Comments