搞定NLP領域的“變形金剛”!手把手教你用BERT進行多標籤文字分類

大資料文摘發表於2019-02-19

過去的一年,深度神經網路的應用開啟了自然語言處理的新時代。預訓練模型在研究領域的應用已經令許多NLP專案的最新成果產生了巨大的飛躍,例如文字分類,自然語言推理和問答。

ELMo,ULMFiT 和OpenAI Transformer是其中幾個關鍵的里程碑。所有這些演算法都允許我們在大型資料庫(例如所有維基百科文章)上預先訓練無監督語言模型,然後在下游任務上對這些預先訓練的模型進行微調。

這一年裡,在這一領域中最激動人心的事件恐怕要數BERT的釋出,這是一種基於多語言轉換器的模型,它已經在各種NLP專案中取得了令人矚目的成果。BERT是一種基於transformer架構的雙向模型,它以一種速度更快的基於Attention的方法取代了RNN(LSTM和GRU)的sequential屬性。

該模型還在兩個無監督任務(“遮蔽語言模型”和“下一句預測”)上進行了預訓練。這讓我們可以透過對下游特定任務(例如情緒分類,意圖檢測,問答等)進行微調來使用預先訓練的BERT模型。

本文將手把手教你,用BERT完成一個Kaggle競賽。

在本文中,我們將重點介紹BERT在多標籤文字分類問題中的應用。傳統的分類問題假定每個文件都分配給一個且只分配給一個類別,即標籤。這有時也被稱為多元分類,比如類別數量是2的話,就叫做二元分類。

而多標籤分類假設文件可以同時獨立地分配給多個標籤或類別。多標籤分類具有許多實際應用,例如業務分類或為電影分配多個型別。在客戶服務領域,此技術可用於識別客戶電子郵件的多種意圖。

我們將使用Kaggle的“惡意評論分類挑戰”來衡量BERT在多標籤文字分類中的表現。

在本次競賽中,我們將嘗試構建一個能夠將給文字片段分配給同惡評類別的模型。我們設定了惡意評論類別作為模型的目標標籤,它們包括普通惡評、嚴重惡評、汙言穢語、威脅、侮辱和身份仇視。

比賽連結:https://www.kaggle.com/c/jigsaw-toxic-comment-classification-challenge

從哪開始?

Google Research最近公開了BERT 的tensorflow部署程式碼,併發布了以下預訓練模型:

  • BERT-Base, Uncased: 12層,768個隱藏單元,自注意力的 head數為12,110M引數

  • BERT-Large, Uncased:24層,1024個隱藏單元,自注意力的 head數為16,340M引數

  • BERT-Base, Cased:12層,768個隱藏單元,自注意力的 head數為12,110M引數

  • BERT-Large, Cased:24層,1024個隱藏單元,自注意力的 head數為16,340M引數

  • BERT-Base, Multilingual Cased (最新推薦):104種語言,12層,768個隱藏單元,自注意力的 head數為12,110M引數

  • BERT-Base, Chinese:中文(簡體和繁體),12層,768個隱藏單元,自注意力的 head數為12,110M引數

編者注:這裡cased和uncased的意思是在進行WordPiece分詞之前是否區分大小寫。uncased表示全部會調整成小寫,且剔除所有的重音標記;cased則表示文字的真實情況和重音標記都會保留下來。

我們將使用較小的Bert-Base,uncased模型來完成此任務。Bert-Base模型有12個attention層,所有文字都將由標記器轉換為小寫。我們在亞馬遜雲 p3.8xlarge EC2例項上執行此模型,該例項包含4個Tesla V100 GPU,GPU記憶體總共64 GB。

因為我個人更喜歡在TensorFlow上使用PyTorch,所以我們將使用來自HuggingFace的BERT模型PyTorch埠,這可從https://github.com/huggingface/pytorch-pretrained-BERT下載。我們已經用HuggingFace的repo指令碼將預先訓練的TensorFlow檢查點(checkpoints)轉換為PyTorch權重。

我們的實現很大程度上是以BERT原始實現中提供的run_classifier示例為基礎的。

資料展示

資料用類InputExample來表示。

  • text_a:文字評論

  • text_b:未使用

  • 標籤:來自訓練資料集的評論標籤列表(很明顯,測試資料集的標籤將為空)

class InputExample(object):    """A single training/test example for sequence classification."""    def __init__(self, guid, text_a, text_b=None, labels=None):        """Constructs a InputExample.        Args:            guid: Unique id for the example.            text_a: string. The untokenized text of the first sequence. For single            sequence tasks, only this sequence must be specified.            text_b: (Optional) string. The untokenized text of the second sequence.            Only must be specified for sequence pair tasks.            labels: (Optional) [string]. The label of the example. This should be            specified for train and dev examples, but not for test examples.        """        self.guid = guid        self.text_a = text_a        self.text_b = text_b         self.labels = labels

class InputFeatures(object):    """A single set of features of data."""    def __init__(self, input_ids, input_mask, segment_ids, label_ids):        self.input_ids = input_ids        self.input_mask = input_mask        self.segment_ids = segment_ids         self.label_ids = label_ids

搞定NLP領域的“變形金剛”!手把手教你用BERT進行多標籤文字分類

我們將InputExample轉換為BERT能理解的特徵,該特徵用類InputFeatures來表示。

  • input_ids:標記化文字的數字id列表

  • input_mask:對於真實標記將設定為1,對於填充標記將設定為0

  • segment_ids:對於我們的情況,這將被設定為全1的列表

  • label_ids:文字的one-hot編碼標籤

標記化(Tokenisation)

BERT-Base,uncased模型使用包含30,522個單詞的詞彙表。標記化過程涉及將輸入文字拆分為詞彙表中可用的標記列表。為了處理不在詞彙表中的單詞,BERT使用一種稱為基於雙位元組編碼(BPE,Byte-Pair Encoding)的WordPiece標記化技術。

這種方法將不在詞彙表之中的詞一步步分解成子詞。因為子詞是詞彙表的一部分,模型已經學習了這些子詞在上下文中的表示,並且該詞的上下文僅僅是子詞的上下文的組合,因此這個詞就可以由一組子詞表示。要了解關於此方法的更多詳細資訊,請參閱文章《使用子詞單位的稀有單詞的神經網路機器翻譯》。

文章連結:https://arxiv.org/pdf/1508.07909

在我看來,這與BERT本身一樣都是一種突破。

模型架構

我們將改寫BertForSequenceClassification類以使其滿足多標籤分類的要求。

class BertForMultiLabelSequenceClassification(PreTrainedBertModel):    """BERT model for classification.    This module is composed of the BERT model with a linear layer on top of    the pooled output.    """    def __init__(self, config, num_labels=2):        super(BertForMultiLabelSequenceClassification, self).__init__(config)        self.num_labels = num_labels        self.bert = BertModel(config)        self.dropout = torch.nn.Dropout(config.hidden_dropout_prob)        self.classifier = torch.nn.Linear(config.hidden_size, num_labels)        self.apply(self.init_bert_weights)    def forward(self, input_ids, token_type_ids=None, attention_mask=None, labels=None):        _, pooled_output = self.bert(input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False)        pooled_output = self.dropout(pooled_output)        logits = self.classifier(pooled_output)        if labels is not None:            loss_fct = BCEWithLogitsLoss()            loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1, self.num_labels))            return loss        else:            return logits            def freeze_bert_encoder(self):        for param in self.bert.parameters():            param.requires_grad = False        def unfreeze_bert_encoder(self):        for param in self.bert.parameters():             param.requires_grad = True

這裡主要的改動是用logits作為二進位制交叉熵的損失函式(BCEWithLogitsLoss),取代用於多元分類的vanilla交叉熵損失函式(CrossEntropyLoss)。二進位制交叉熵損失可以讓我們的模型為標籤分配獨立的機率。

下面的模型摘要說明了模型的各個層及其維度。

BertForMultiLabelSequenceClassification(  (bert): BertModel(    (embeddings): BertEmbeddings(      (word_embeddings): Embedding(28996, 768)      (position_embeddings): Embedding(512, 768)      (token_type_embeddings): Embedding(2, 768)      (LayerNorm): FusedLayerNorm(torch.Size([768]), eps=1e-12, elementwise_affine=True)      (dropout): Dropout(p=0.1)    )    (encoder): BertEncoder(      (layer): ModuleList( #       12 BertLayers        (11): BertLayer(          (attention): BertAttention(            (self): BertSelfAttention(              (query): Linear(in_features=768, out_features=768, bias=True)              (key): Linear(in_features=768, out_features=768, bias=True)              (value): Linear(in_features=768, out_features=768, bias=True)              (dropout): Dropout(p=0.1)            )            (output): BertSelfOutput(              (dense): Linear(in_features=768, out_features=768, bias=True)              (LayerNorm): FusedLayerNorm(torch.Size([768]), eps=1e-12, elementwise_affine=True)              (dropout): Dropout(p=0.1)            )          )          (intermediate): BertIntermediate(            (dense): Linear(in_features=768, out_features=3072, bias=True)          )          (output): BertOutput(            (dense): Linear(in_features=3072, out_features=768, bias=True)            (LayerNorm): FusedLayerNorm(torch.Size([768]), eps=1e-12, elementwise_affine=True)            (dropout): Dropout(p=0.1)          )        )      )    )    (pooler): BertPooler(      (dense): Linear(in_features=768, out_features=768, bias=True)      (activation): Tanh()    )  )  (dropout): Dropout(p=0.1)  (classifier): Linear(in_features=768, out_features=6, bias=True) )

  • BertEmbeddings:輸入嵌入層

  • BertEncoder: 12個BERT模型attention層

  • 分類器:我們的多標籤分類器,out_features = 6,每個分類符對應6個標籤

模型訓練

訓練迴圈與原始BERT實現中提供的run_classifier.py裡的迴圈相同。我們的模型訓練了4個epoch(一個完整的資料集透過了神經網路一次並且返回了一次,這個過程稱為一個 epoch),每批資料大小為32,序列長度為512,即預訓練模型的最大可能性。根據原始論文的建議,學習率保持在3e-5。

因為有機會使用多個GPU,所以我們將Pytorch模型封裝在DataParallel模組中,這使我們能夠在所有可用的GPU上進行訓練。

我們沒有使用半精度FP16技術,因為使用logits 損失函式的二進位制交叉熵不支援FP16處理。但這並不會影響最終結果,只是需要更長的時間訓練。

評估指標

def accuracy_thresh(y_pred:Tensor, y_true:Tensor, thresh:float=0.5, sigmoid:bool=True):    "Compute accuracy when `y_pred` and `y_true` are the same size."    if sigmoid: y_pred = y_pred.sigmoid()    return np.mean(((y_pred>thresh)==y_true.byte()).float().cpu().numpy(), axis=1).sum()

from sklearn.metrics import roc_curve, auc # Compute ROC curve and ROC area for each class fpr = dict() tpr = dict() roc_auc = dict() for i in range(num_labels):    fpr[i], tpr[i], _ = roc_curve(all_labels[:, i], all_logits[:, i])    roc_auc[i] = auc(fpr[i], tpr[i]) # Compute micro-average ROC curve and ROC area fpr["micro"], tpr["micro"], _ = roc_curve(all_labels.ravel(), all_logits.ravel()) roc_auc["micro"] = auc(fpr["micro"], tpr["micro"])

我們為精度度量函式增加了一個閾值,預設設定為0.5。

對於多標籤分類,更重要的指標是ROC-AUC曲線。這也是Kaggle比賽的評分指標。我們分別計算每個標籤的ROC-AUC,並對單個標籤的roc-auc分數進行微平均。

如果想深入瞭解roc-auc曲線,這裡有一篇很不錯的部落格。

部落格連結:https://towardsdatascience.com/understanding-auc-roc-curve-68b2303cc9c5

評估分數

我們重複進行了幾次實驗,每次都有一些輸入上的變化,但都得到了類似的結果,如下所示:

訓練損失:0.022,驗證損失:0.018,驗證準確度:99.31%。

各個標籤的ROC-AUC分數:

  • 普通惡評:0.9988

  • 嚴重惡評:0.9935

  • 汙言穢語:0.9988

  • 威脅:0.9989

  • 侮辱:0.9975

  • 身份仇視:0.9988

  • 微觀平均ROC-AUC得分:0.9987

這樣的結果似乎非常令人鼓舞,因為我們看上去已經建立了一個近乎完美的模型來檢測文字評論的惡毒程度。現在看看我們在Kaggle排行榜上的得分。

Kaggle競賽結果

我們在Kaggle提供的測試資料集上執行推理邏輯,並將結果提交給競賽。以下是結果:




我們的roc-auc評分達到了0.9863,在所有競爭者中排名前10%。為了使比賽結果更具說服力,這次Kaggle比賽的獎金為35000美元,而一等獎得分為0.9885。

搞定NLP領域的“變形金剛”!手把手教你用BERT進行多標籤文字分類

最高分的團隊由專業的高技能資料科學家和從業者組成。除了我們所做的工作之外,他們還使用各種技術來進行資料整合,資料增強(data augmentation)和測試時增強(test-time augmentation)。

結論和後續

我們使用強大的BERT預訓練模型實現了多標籤分類模型。正如我們所展示的那樣,模型在已熟知的公開資料集上得到了相當不錯的結果。我們能夠建立一個世界級的模型生產應用於各行業,尤其是客戶服務領域。

對於我們來說,下一步將是使用“遮蔽語言模型”和“下一句預測”對下游任務的文字語料庫來微調預訓練的語言模型。這將是一項無監督的任務,希望該模型能夠學習一些我們自定義的上下文和術語,這和ULMFiT使用的技術類似。

資料連結: 

原始BERT論文:https://arxiv.org/pdf/1810.04805

相關報導:https://medium.com/huggingface/multi-label-text-classification-using-bert-the-mighty-transformer-69714fa3fb3d

相關文章