9.5 命名實體識別—BERT

前言

本節介紹一個NLP領域劃時代意義的模型——BERTBidirectional Encoder Representations from Transformers),BERT論文目前的引用超9萬次!(20242月),BERT模型架構是的NLP中各類任務也可以使用預訓練+微調範式(類似CV領域)。

本節將簡牘BERT論文,然後介紹採用BERT進行NERNamed Entity Recognition,命名實體識別)任務的微調,通過論文瞭解BERT的發展,同時通過具體下游任務,瞭解如何對預訓練好的BERT模型進行微調,來適應下游任務。本節不涉及BERT的預訓練。

BERT論文略讀

BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》(https://arxiv.org/abs/1810.04805

摘要:前人優秀工作僅用了單向資訊且不能很好的應用到各類下游任務,本文提出一種基於Transformer的雙向處理預訓練模型——BERT,在預訓練完成後,採用統一的結構進行多個NLP下游任務微調,均達到SOTA

BERT模型關鍵字:預訓練;雙向資訊;MLMMasked Language Model)預訓練任務;NSPNext Sentence Predict)預訓練任務

預訓練相關工作

BERT之前的ELMoGPT都是預訓練機制,並且取得不錯成績,但他們都是基於單向的,存在缺點。

BERT為了解決單向預訓練帶來的不足,引入了MLMNSP兩個預訓練任務,讓模型能夠從雙向來理解語言。

BERT模型結構

<<AI人工智慧 PyTorch自學>> 9.5 命名實體識

BERT的構建與使用分為兩個階段,預訓練和微調。所有下游任務微調時,均採用預訓練好的參數進行全域初始化、全域訓練。

BERT模型結構很簡單,完全基於Transformerencoder,並且有baselarge兩個版本,attention blockhidden size head分別為(L=12, H=768, A=12, Total Parameters=110M) (L=24, H=1024,A=16, Total Parameters=340M)

BERT的輸入設計很巧妙,使得一個結構適應了多個NLP任務。輸入設計為序列形式,將一個句子、兩個句子都組裝成為一個序列,輸入到模型中。輸入上,設計了兩個特殊的tokenclssep

  • cls:可以理解為序列的全域特徵,用於文本分類、情感分析這類的seq2cls的任務。
  • sep:用於將句子1和句子2進行拼接的token

embedding處理上,設計了額外的segment embedding來標記句子是第一句、還是第二句。具體的輸入embedding由三部分組成,如下圖所示:

<<AI人工智慧 PyTorch自學>> 9.5 命名實體識

BERT的預訓練——MLM

BERT的第一種預訓練任務是MLMmasked language model),是對一句話中的一些單詞進行隱藏,然後讓模型根據上下文內容,在該masktoken位置上要求預測該單詞。例如:白切雞經過MLM處理變為mask,輸入到BERT模型,BERT模型的輸出標籤是白切雞

在進行mask是需要一定概率的,文章中對全文的15%token進行遮罩,然後這15%裡,80%真正變為mask10%為隨機token10%為原始token。這麼做的原因是,下游任務中並沒有mask這個特殊token,為了保障微調時的性能,這裡做了這樣的設置。( a downside is that we are creating a mismatch between pre-training and fine-tuning, since the [MASK] token does not appear during fine-tuning.

BERT的預訓練——NSP

BERT的第一種預訓練任務是NSPNext Sentence Prediction),由於NLP任務中有一些是需要理解兩個句子之間的關係,例如QANLI任務。為了讓BERT掌握句子之間的理解能力,設計了NSP

NSP是一個二分類任務。輸入的是兩個句子組成的序列,輸出的是IsText or Not Text。含義是這兩個句子是否是前後兩句。論文舉的例子:

Input = [CLS] the man went to [MASK] store [SEP] he bought a gallon [MASK] milk [SEP]

Label = IsNext

 

Input = [CLS] the man [MASK] to the store [SEP] penguin [MASK] are flight ##less birds [SEP]

Label = NotNext

Copy

預訓練實驗

預訓練採用了2個資料集:BooksCorpus (800M words) (Zhu et al.,2015) and English Wikipedia (2,500M words)

預訓練參數及耗時如下:bs=256, epoch=40, 100step, 1step預熱, lr=1e-4, base:16TPU4, large:64TPU4天。

BERT的微調——下游任務

有了預訓練好的BERT模型,可以快速方便的應用到各類NLP的下游任務,直接看下圖列舉的四種典型的任務:

1seq2cls:輸入是多個句子,用sep拼接,輸出用cls的特徵向量接softmax實現分類。

2seq2cls:輸入是單個句子,處理同上。

3seq2seq:輸入是兩段話構成的序列,輸出是第二段話中要求各token輸出3個類別,用於標記哪些是答案的開始、答案的結束和無關內容。可以理解為詞表為3的序列生成任務。

4seq2seq:輸入是一句話,輸出是每個token的分類類別,類別數根據任務而定,例如NER任務中,類別數是(實體種類*2 + 1),一個實體需要兩個類別,實體開始和實體結束兩個標記,1表示無關類別。

<<AI人工智慧 PyTorch自學>> 9.5 命名實體識

下游任務微調實驗

超參數基本固定,可以套用大部分任務:

  • Learning rate (Adam): 5e-5, 3e-5, 2e-5
  • Number of epochs: 2, 3, 4
  • Batch size: 16, 32

且採用的計算資源也很少,單個TPU一小時,單GPU幾個小時即可,真實親民的好模型。

論文小結

BERT是在ELMoGPT之後提出來的一種基於TransformerEncoder實現雙向資訊交互的預訓練架構,並且可在一個模型結構上實現多種下游任務的微調,具有統一結構。BERT對於NLP的預訓練-微調,算得上開創性的作品,為NLP微調範式打開了大門,後續的NLP任務大多基於BERT範式。

本論文需重點學習的幾點如下:

  1. BERT最大亮點是雙向資訊交互和MLM+NSP的預訓練-微調範式;
  2. MLM執行時,15%選中為mask候選token,再根據8:1:1比例進行真mask、隨機token、原始token的設置
  3. NSP任務採用sep特殊的token來拼接句子,實現一個序列輸入,包含多個句子,sep的引入為模型統一多個任務提供了可能
  4. 預訓練耗時4天,微調僅需數小時,此方案非常親民。

NER任務簡介

命名實體識別(Named Entity Recognition,簡稱 NER)是自然語言處理(NLP)中的一個基礎任務,旨在識別文本中具有特定意義的實體,如人名、地名、組織名、時間、金額等。NER 通常用於資訊提取、知識圖譜構建、文本分類等任務的前置任務,屬於基礎任務。

NER資料的標注通常採用BIO體系來對文本中的每個token進行逐token的打標籤(三分類),Begin表示實體開頭,Inside表示實體中間/結尾,Outside表示非實體。簡而言之,NER是逐token的多分類問題。分類類別等於(實體種類*2 + 1),一個實體需要BI兩個類別標記,所有實體共用一個無關類別O

具體地,看一個文本:蘋果公司計畫在2023年在中國深圳建立一家新工廠,假設有三種實體類別,分別是組織、地名、時間。那麼對應的NER訓練標籤為B-ORG, I-ORG, O, O, B-TIME, O, B-LOC, I-LOC, O, O, O, O。對應關係是

蘋果 公司 計畫 2023 中國 深圳 建立 一家 工廠,由此可知蘋果公司是組織, “2023是時間,中國深圳是地名。

接下來將會採用CLUENER 細細微性命名實體識別資料進行實驗。

資料集構建

資料集下載

根據BERT論文的微調示意圖和上文對NER的介紹,已經知道NER的資料製作目標是做一個token級別的多分類任務,下面就來觀察資料。

首先下載CLUENER2020(https://github.com/CLUEbenchmark/CLUENER2020), 得到cluener_public.zip,解壓後得到3個主要的json,分別是訓練集、驗證集和測試集。其中測試集沒有標注標籤。

以一條具體的資料介紹,一條資料包括原文本text,標籤labellabel中存在多個實體的標注,以字典形式,key是實體類別,value是具體內容以及字元的開始與結束字元。

{"text": "虛幻引擎3動作遊戲《黑光》新作公佈", "label": {"game": {"《黑光》": [[9, 12]]}}}

Copy

資料分為10個標籤類別,分別為:

地址(address),書名(book),公司(company),遊戲(game),政府(goverment),電影(movie),姓名(name),組織機構(organization),職位(position),景點(scene)。

在代碼中對應的模型標籤為(B表示Begin, I表示inside, O表示outside)

["X", "B-address", "B-book", "B-company", 'B-game', 'B-government', 'B-movie', 'B-name',

        'B-organization', 'B-position','B-scene',"I-address",

        "I-book", "I-company", 'I-game', 'I-government', 'I-movie', 'I-name',

        'I-organization', 'I-position','I-scene',

        "S-address", "S-book", "S-company", 'S-game', 'S-government', 'S-movie',

        'S-name', 'S-organization', 'S-position',

        'S-scene','O',"[START]", "[END]"]

Copy

整套代碼借鑒https://github.com/lonePatient/BERT-NER-Pytorch,因此直接分析資料集編寫部分,是如何將原始json解析為模型需要的形式。

資料集解析

根據utils_ner.py中的read_json函數,可以看出對json中的資料是如何做處理的。

最終資料被解析為:

words = ['', '', '', '', '', '', '', '', '', '', '', '']

labels = ['B-company', 'I-company', 'I-company', 'I-company', 'B-position', 'I-position', 'I-position', 'I-position', 'B-name', 'I-name', 'I-name', 'O']

def _read_json(self,input_file):

    lines = []

    with open(input_file, 'r', encoding='utf-8') as f:

        for line in f:

            line = json.loads(line.strip())

            text = line['text']

            label_entities = line.get('label',None)

            words = list(text)

            labels = ['O'] * len(words)

            if label_entities is not None:

                for key,value in label_entities.items():

                    for sub_name,sub_index in value.items():

                        for start_index,end_index in sub_index:

                            assert  ''.join(words[start_index:end_index+1]) == sub_name

                            if start_index == end_index:

                                labels[start_index] = 'S-'+key

                            else:

                                labels[start_index] = 'B-'+key

                                labels[start_index+1:end_index+1] = ['I-'+key]*(len(sub_name)-1)

            lines.append({"words": words, "labels": labels})

    return lines

Copy

DataSet構建

對於BERT模型輸入需要有3個內容,分別是tokenindex maskindex segmentindex,下面是一條具體資料的情況。

02/22/2024 23:05:43 - INFO - processors.ner_seq -   tokens: [CLS] [SEP]

02/22/2024 23:05:43 - INFO - processors.ner_seq -   input_ids: 101 3851 1555 7213 6121 821 689 928 6587 6956 1383 5439 3424 1300 1894 1156 794 1369 671 702 6235 2428 2190 758 6887 7305 3546 6822 6121 749 6237 6438 511 1383 5439 3424 6371 711 8024 2190 4680 1184 1744 1079 1555 689 7213 6121 5445 6241 8024 102 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

02/22/2024 23:05:44 - INFO - processors.ner_seq -   input_mask: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

02/22/2024 23:05:44 - INFO - processors.ner_seq -   segment_ids: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

02/22/2024 23:05:44 - INFO - processors.ner_seq -   label_ids: 31 3 13 13 13 31 31 31 31 31 7 17 17 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Copy

在這裡會用到預訓練模型的字典來做tokensinput_ids的映射,例如[CLS]就是101類,[PAD]0類,浙是3851類。

以上功能在以下兩個地方體現。

  • run_ner_softmax.py train_dataset = load_and_cache_examples(args, args.task_name,tokenizer, data_type='train')
  • processors/ner_seq.py features = convert_examples_to_features(examples=examples,

在此份代碼中,採用了 dataset = TensorDataset(all_input_ids, all_input_mask, all_segment_ids, all_lens,all_label_ids) 的方式構建dataset

模型構建

BERT模型已成為NLP任務中常用的模型,因此已得到很好的集成,目前大多採用HuggingFacetransformers庫進行構建BERT

構建BERT模型有三要素,分別是設定檔,模型本體,tokenizer

設定檔BertConfig

設定檔用class transformers.BertConfig封裝,包含以下主要內容,從參數大概就明白,BERT模型的大小、向量大小、層數等都在這裡面配置。

通常這個配置內容是跟著預訓練模型的資訊走的,需要從磁片中載入config.json檔,例如本案例使用的預訓練模型是從https://huggingface.co/google-bert/bert-base-chinese/tree/main下載,裡邊就包含了設定檔。

def __init__(

    self,

    vocab_size=30522,

    hidden_size=768,

    num_hidden_layers=12,

    num_attention_heads=12,

    intermediate_size=3072,

    hidden_act="gelu",

    hidden_dropout_prob=0.1,

    attention_probs_dropout_prob=0.1,

    max_position_embeddings=512,

    type_vocab_size=2,

    initializer_range=0.02,

    layer_norm_eps=1e-12,

    pad_token_id=0,

    position_embedding_type="absolute",

    use_cache=True,

    classifier_dropout=None,

    **kwargs

):

Copy

模型本體

BERT本身這個nn.Module就複雜一些,除了實現BERT運算功能,還涉及huggingface的協議、功能,所以最終應用的模型類BertSoftmaxForNer是繼承了官方BertPreTrainedModel,並且內部定義了官方BertModel模型,用於BERT的運算;同時在BertSoftmaxForNer內部,進行適當的代碼修改,用於實現下游任務以處理邏輯。

類代碼具體的定義與關係可參考如下UML示意圖:BertSoftmaxForNer是核心,往左邊是BertModel模型的核心,BertModel與上節Transformer一樣,拆分了多個子模組來構建,因此第一列整體是BERT的構建,值得注意的是三個embedding的構建。

<<AI人工智慧 PyTorch自學>> 9.5 命名實體識

捋清楚模型類定義後,再看模型初始化就會清楚真正用的模型是怎麼來的:

model = model_class.from_pretrained(args.model_name_or_path, config=config)

Copy

根據config配置,模型權重檔,借助PreTrainedModel類的from_pretrained()函數,實現模型定義、權重載入。

模型訓練

模型訓練採用run_ner_softmax.py,主要修改預訓練檔路徑、資料集路徑、輸出路徑即可,具體的運行參數如下所示

--model_type=bert

--model_name_or_path=./bert-base-pretrained

--task_name=cluener

--do_lower_case

--loss_type=ce

--do_train

--do_eval

--data_dir=G:\deep_learning_data\cluener_public

--train_max_seq_length=128

--eval_max_seq_length=512

--per_gpu_train_batch_size=64

--per_gpu_eval_batch_size=64

--learning_rate=3e-5

--num_train_epochs=4.0

--logging_steps=20

--save_steps=224

--output_dir=D:\github_desktop\BERT-NER-Pytorch\outputs

--overwrite_output_dir

--seed=42

Copy

BERT的微調大多是4epoch,很快就見效,筆記本上訓練大約10分即可完成,接下來觀察一下訓練效果。

首先來看loss曲線,運行01_loss_curve_plot.py,可以查看loss曲線變化,如下圖所示:

<<AI人工智慧 PyTorch自學>> 9.5 命名實體識

從曲線看,loss下降很快,並且迅速到達了平臺期,F1快速達到了0.79,訓練效果較好。

補充說明:本節的訓練代碼寫得不是很優雅,就不過多討論,後續應該不會再遇到類似結構的代碼。

模型推理

模型訓練好之後,來看看在測試集上的推理效果。

首先,run_ner_softmax.py代碼已經實現推理預測功能,把--do_train --do_eval去掉,保留 --do_predict即可得到test_prediction.json

然後,運行02_predict_parse.py,可以看到解析結果。例如:

從文本"四川敦煌學。近年來,丹棱縣等地一些不知名的石窟迎來了海內外的遊客,他們隨身攜帶著胡文和的著作。"中,提取到實體 organization "四川敦煌學" address "丹棱縣" name "胡文和"

從文本"奈及利亞海軍發言人當天在阿布加向奈及利亞通訊社證實了這一消息。"中,提取到實體 government "奈及利亞海軍" address "阿布加" company "奈及利亞通訊社"

從文本"銷售冠軍:輻射3-Bethesda"中,提取到實體 game "輻射3"

從文本"所以大多數人都是從巴厘島南部開始環島之旅。"中,提取到實體 scene "巴厘島"

從文本"備受矚目的動作及冒險類大作《迷失》在其英文版上市之初就受到了全球玩家的大力追捧。"中,提取到實體 game "《迷失》"

從文本"filippagowski14歲時我感覺自己像梵古"中,提取到實體 name "filippagowski" name "梵古"

Copy

小結

本節通過論文+代碼的形式來介紹BERT模型的應用,通過實驗發現BERT的出現,為NLP各類任務提供了方便。為此,有必要再次回顧一下BERT模型的特性:

1)基於Transformerencoder,實現雙向資訊的編碼融合,提高上下文理解能力,並設計預訓練+微調範式,實現多個NLP任務的統一架構——BERT

2BERT採用的無監督預訓練方法為MLMmasked language model)和NSPnext sentence predict),可以實現token級和句子級的資訊學習;

3BERT的各任務微調,通常指需要4epoch,學習率1e-5級別,gpu幾個小時即可,非常親民;

4BERT的輸入embedding新增了一個segment embedding來標記句子當前token屬於第幾個句子;

5BERT的統一架構實現多個任務,得益於輸入-輸出的設計,有必要再次回顧這幅圖

<<AI人工智慧 PyTorch自學>> 9.5 命名實體識

 

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 HCHUNGW 的頭像
    HCHUNGW

    HCHUNGW的部落格

    HCHUNGW 發表在 痞客邦 留言(0) 人氣()