一文讀懂最強中文NLP預訓練模型ERNIE

基於飛槳開源的持續學習的語義理解框架ERNIE 2。0,及基於此框架的ERNIE 2。0預訓練模型,在共計16箇中英文任務上超越了BERT和XLNet, 取得了SOTA效果。本文帶你進一步深入瞭解ERNIE的技術細節。

一:ERNIE 簡介

1.1 簡介

Google 最近提出的 BERT 模型,透過隨機遮蔽15%的字或者word,利用 Transformer 的多層 self-attention 雙向建模能力,在各項nlp 下游任務中(如 sentence pair classification task,singe sentence classification task,question answering task) 都取得了很好的成績。但是,BERT 模型主要是聚焦在針對字或者英文word粒度的完形填空學習上面,沒有充分利用訓練資料當中詞法結構,語法結構,以及語義資訊去學習建模。比如 “我要買蘋果手機”,BERT 模型 將 “我”,“要”, “買”,“蘋”, “果”,“手”, “機” 每個字都統一對待,隨機mask,丟失了“蘋果手機” 是一個很火的名詞這一資訊,這個是詞法資訊的缺失。同時 我 + 買 + 名詞 是一個非常明顯的購物意圖的句式,BERT 沒有對此類語法結構進行專門的建模,如果預訓練的語料中只有“我要買蘋果手機”,“我要買華為手機”,哪一天出現了一個新的手機牌子比如栗子手機,而這個手機牌子在預訓練的語料當中並不存在,沒有基於詞法結構以及句法結構的建模,對於這種新出來的詞是很難給出一個很好的向量表示的,而ERNIE 透過對訓練資料中的詞法結構,語法結構,語義資訊進行統一建模,極大地增強了通用語義表示能力,在多項任務中均取得了大幅度超越BERT的效果!!

1。2 下載地址(這麼好用的模型趕緊下載起來吧!)

ERNIE 的Fine-tuning程式碼和英文預訓練模型已透過飛槳開源

Github 地址:

https://github。com/PaddlePaddle/ERNIE

二:ERNIE 詳解

2.1 ERNIE 結構

2.1.1 ERNIE 初探

一文讀懂最強中文NLP預訓練模型ERNIE

2.1.1 ERNIE 結構詳解

一文讀懂最強中文NLP預訓練模型ERNIE

Figure 2:ERNIE 的 encoder 結構詳解

相比transformer,ERNIE 基本上是 transformer 的encoder 部分,並且encoder 在結構上是全部一樣的,但是並不共享權重,具體區別如下:

Transformer: 6 encoder layers, 512 hidden units, 8 attention heads

ERNIE Base: 12 encoder layers, 768 hidden units, 12 attention heads

ERNIE Large: 24 encoder layers,1024 hidden units, 16 attention heads

從輸入上來看第一個輸入是一個特殊的CLS, CLS 表示分類任務就像 transformer 的一般的encoder, ERINE 將一序列的words 輸入到encoder 中。 每層使用self-attention, feed-word network, 然後把結果傳入到下一個encoder。

2.1.2 ERNIE encoder 說明

encoder

encoder 由兩層構成, 首先流入self-attention layer,self-attention layer 輸出流入 feed-forward 神經網路。至於self-attention的結構,我們在這裡不再展開,有興趣的同學可以進入以下連結仔細閱讀http://jalammar。github。io/illustrated-transformer/,來進一步瞭解self-attention的結構!!

一文讀懂最強中文NLP預訓練模型ERNIE

Figure 3: encoder 結構詳解

embedding

最下層的encoder的輸入是embedding的向量, 其他的encoder的輸入,便是更下層的encoder的輸出, 一般設定輸入的vectors 的維度為512,同學們也可以自己設定。

一文讀懂最強中文NLP預訓練模型ERNIE

Figure 4: encoder 結構詳解

2.2 : ERNIE 1.0 介紹

相比於BERT, ERNIE 1。0 改進了兩種 masking 策略,一種是基於phrase (在這裡是短語 比如 a series of, written等)的masking策略,另外一種是基於 entity(在這裡是人名、位置、組織、產品等名詞,比如Apple, J。K。 Rowling)的masking 策略。在ERNIE 當中,將由多個字組成的phrase 或者entity 當成一個統一單元,相比於bert 基於字的mask,這個單元當中的所有字在訓練的時候,統一被mask。對比直接將知識類的query 對映成向量然後直接加起來,ERNIE 透過統一mask的方式可以潛在地學習到知識的依賴以及更長的語義依賴來讓模型更具泛化性。

一文讀懂最強中文NLP預訓練模型ERNIE

Figure 5: ERNIE 1.0 不同的mask 策略說明

2.3: ERNIE 2.0 介紹

傳統的pre-training 模型主要基於文字中words 和 sentences 之間的共現進行學習。 事實上,訓練文字資料中的詞法結構、語法結構、語義資訊也同樣是很重要的。在命名實體識別中人名、機構名、組織名等名詞包含概念資訊對應了詞法結構,句子之間的順序對應了語法結構,文章中的語義相關性對應了語義資訊。為了去發現訓練資料中這些有價值的資訊,在ERNIE 2。0 中,提出了一個預訓練框架,可以在大型資料集合中進行增量訓練。

一文讀懂最強中文NLP預訓練模型ERNIE

Figure 6: ERNIE 2.0 框架

2.3.1 ERNIE 2.0 結構

ERNIE 2。0 中有一個很重要的概念便是連續學習(Continual Learning),連續學習的目的是在一個模型中順序訓練多個不同的任務,以便在學習下個任務當中可以記住前一個學習任務學習到的結果。透過使用連續學習,可以不斷積累新的知識,模型在新任務當中可以用歷史任務學習到引數進行初始化,一般來說比直接開始新任務的學習會獲得更好的效果。

a: 預訓練連續學習

ERNIE 的預訓練連續學習分為兩步,首先,連續用大量的資料與先驗知識連續構建不同的預訓練任務。其次,不斷的用預訓練任務更新ERNIE 模型。

對於第一步,ERNIE 2。0 分別構建了詞法級別,語法級別,語義級別的預訓練任務。所有的這些任務,都是基於無標註或者弱標註的資料。需要注意的是,在連續訓練之前,首先用一個簡單的任務來初始化模型,在後面更新模型的時候,用前一個任務訓練好的引數來作為下一個任務模型初始化的引數。這樣不管什麼時候,一個新的任務加進來的時候,都用上一個模型的引數初始化保證了模型不會忘記之前學習到的知識。透過這種方式,在連續學習的過程中,ERNIE 2。0 框架可以不斷更新並記住以前學習到的知識可以使得模型在新任務上獲得更好的表現。我們在下面的e, f, g 中會具體介紹ERNIE 2。0 構建哪些預訓練任務,並且這些預訓練任務起了什麼作用。

在圖7中,介紹了ERNIE2。0連續學習的架構。這個架構包含了一系列共享文字encoding layers 來 encode 上下文資訊。這些encoder layers 的引數可以被所有的預訓練任務更新。有兩種型別的 loss function,一種是sequence level 的loss, 一種是word level的loss。在ERNIE 2。0 預訓練中,一個或多個sentence level的loss function可以和多個token level的loss functions 結合來共同更新模型。

一文讀懂最強中文NLP預訓練模型ERNIE

Figure 7: ERINE 2.0 連續學習流程

b: encoder

ERNIE 2。0 用了我們前文提到的transformer 結構encoder,結構基本一致,但是權重並不共享。

c: task embedding.

ERNIE 2。0 用了不同的task id 來標示預訓練任務,task id 從1 到N 對應下面的e, f ,g中提到的預訓練任務。對應的token segment position 以及task embedding 被用來作為模型的輸入。

一文讀懂最強中文NLP預訓練模型ERNIE

Figure 8: ERNIE 2.0 連續學習詳解

e: 構建詞法級別的預訓練任務,來獲取訓練資料中的詞法資訊

1: knowledge masking task,即 ERNIE 1。0 中的entity mask 以及 phrase entity mask 來獲取phrase 以及entity的先驗知識,相較於 sub-word masking, 該策略可以更好的捕捉輸入樣本區域性和全域性的語義資訊。

2: Capitalization Prediction Task,大寫的詞比如Apple相比於其他詞通常在句子當中有特定的含義,所以在ERNIE 2。0 加入一個任務來判斷一個詞是否大寫。

3: Token-Document Relation Prediction Task,類似於tf-idf,預測一個詞在文中的A 段落出現,是否會在文中的B 段落出現。如果一個詞在文章當中的許多部分出現一般就說明這個詞經常被用到或者和這個文章的主題相關。透過識別這個文中關鍵的的詞, 這個任務可以增強模型去獲取文章的關鍵詞語的能力。

f: 構建語法級別的預訓練任務,來獲取訓練資料中的語法資訊

1: Sentence Reordering Task,在訓練當中,將paragraph 隨機分成1 到m 段,將所有的組合隨機shuffle。我們讓pre-trained 的模型來識別所有的這些segments正確的順序。這便是一個k 分類任務

一文讀懂最強中文NLP預訓練模型ERNIE

通常來說,這些sentence 重排序任務能夠讓pre-trained 模型學習到document 中不同sentence 的關係。

2: Sentence Distance Task, 構建一個三分類任務來判別句子的距離,0表示兩個句子是同一個文章中相鄰的句子,1表示兩個句子是在同一個文章,但是不相鄰,2表示兩個句子是不同的文章。透過構建這樣一個三分類任務去判斷句對 (sentence pairs) 位置關係 (包含鄰近句子、文件內非鄰近句子、非同文件內句子 3 種類別),更好的建模語義相關性。

g:構建語義級別的預訓練任務,來獲取訓練資料中的語義任務

1: Discourse Relation Task,除了上面的distance task,ERNIE透過判斷句對 (sentence pairs) 間的修辭關係 (semantic & rhetorical relation),更好的學習句間語義。

2: IR Relevance Task,在這裡主要是利用baidu 的日誌來獲取這個關係,將query 作為第一個sentence,title 作為第二個 sentence。0 表示強關係, 1 表示弱關係,2表示無關係,透過類似google-distance 的關係來衡量 兩個query之間的語義相關性,更好的建模句對相關性。

三: 程式碼梳理

3.1 : 預訓練指令碼

set -euxexport FLAGS_eager_delete_tensor_gb=0export FLAGS_sync_nccl_allreduce=1export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7python 。/pretrain_launch。py \ ——nproc_per_node 8 \ ——selected_gpus 0,1,2,3,4,5,6,7 \ ——node_ips $(hostname -i) \ ——node_id 0 \。/train。py ——use_cuda True \ ——is_distributed False\ ——use_fast_executor True \ ——weight_sharing True \ ——in_tokens true \ ——batch_size 8192 \ ——vocab_path 。/config/vocab。txt \ ——train_filelist 。/data/train_filelist \ ——valid_filelist 。/data/valid_filelist \ ——validation_steps 100 \ ——num_train_steps 1000000 \ ——checkpoints 。/checkpoints \ ——save_steps 10000 \ ——ernie_config_path 。/config/ernie_config。json \ ——learning_rate 1e-4 \ ——use_fp16 false \ ——weight_decay 0。01 \ ——max_seq_len 512 \ ——skip_steps 10

ernie_encoder。py 程式碼如下:

from __future__ import absolute_importfrom __future__ import divisionfrom __future__ import print_functionimport osimport argparseimport numpy as npimport multiprocessingimport paddle。fluid as fluidimport reader。task_reader as task_readerfrom model。ernie import ErnieConfig, ErnieModelfrom utils。args import ArgumentGroup, print_argumentsfrom utils。init import init_pretraining_params# yapf: disableparser = argparse。ArgumentParser(__doc__)model_g = ArgumentGroup(parser, “model”, “model configuration and paths。”)model_g。add_arg(“ernie_config_path”, str, None, “Path to the json file for ernie model config。”)model_g。add_arg(“init_pretraining_params”, str, None, “Init pre-training params which preforms fine-tuning from。 If the ” “arg ‘init_checkpoint’ has been set, this argument wouldn‘t be valid。”) model_g。add_arg(“output_dir”, str, “embeddings”, “path to save embeddings extracted by ernie_encoder。”) data_g = ArgumentGroup(parser, “data”, “Data paths, vocab paths and data processing options”) data_g。add_arg(“data_set”, str, None, “Path to data for calculating ernie_embeddings。”) data_g。add_arg(“vocab_path”, str, None, “Vocabulary path。”)data_g。add_arg(“max_seq_len”, int, 512, “Number of words of the longest seqence。”)data_g。add_arg(“batch_size”, int, 32, “Total examples’ number in batch for training。”)data_g。add_arg(“do_lower_case”, bool, True, “Whether to lower case the input text。 Should be True for uncased models and False for cased models。”)run_type_g = ArgumentGroup(parser, “run_type”, “running type options。”)run_type_g。add_arg(“use_cuda”, bool, True, “If set, use GPU for training。”)# yapf: enabledef create_model(args, pyreader_name, ernie_config): pyreader = fluid。layers。py_reader( capacity=50, shapes=[[-1, args。max_seq_len, 1], [-1, args。max_seq_len, 1], [-1, args。max_seq_len, 1], [-1, args。max_seq_len, 1], [-1, args。max_seq_len, 1], [-1, 1]], dtypes=[‘int64’, ‘int64’, ‘int64’, ‘int64’, ‘float’, ‘int64’], lod_levels=[0, 0, 0, 0, 0, 0], name=pyreader_name, use_double_buffer=True) (src_ids, sent_ids, pos_ids, task_ids, input_mask, seq_lens) = fluid。layers。read_file(pyreader) ernie = ErnieModel( src_ids=src_ids, position_ids=pos_ids, sentence_ids=sent_ids, task_ids=task_ids, input_mask=input_mask, config=ernie_config) enc_out = ernie。get_sequence_output() unpad_enc_out = fluid。layers。sequence_unpad(enc_out, length=seq_lens) cls_feats = ernie。get_pooled_output() # set persistable = True to avoid memory opimizing enc_out。persistable = True unpad_enc_out。persistable = True cls_feats。persistable = True graph_vars = { “cls_embeddings”: cls_feats, “top_layer_embeddings”: unpad_enc_out, } return pyreader, graph_varsdef main(args): args = parser。parse_args() ernie_config = ErnieConfig(args。ernie_config_path) ernie_config。print_config() if args。use_cuda: place = fluid。CUDAPlace(int(os。getenv(‘FLAGS_selected_gpus’, ‘0’))) dev_count = fluid。core。get_cuda_device_count() else: place = fluid。CPUPlace() dev_count = int(os。environ。get(‘CPU_NUM’, multiprocessing。cpu_count())) exe = fluid。Executor(place) reader = task_reader。ExtractEmbeddingReader( vocab_path=args。vocab_path, max_seq_len=args。max_seq_len, do_lower_case=args。do_lower_case) startup_prog = fluid。Program() data_generator = reader。data_generator( input_file=args。data_set, batch_size=args。batch_size, epoch=1, shuffle=False) total_examples = reader。get_num_examples(args。data_set) print(“Device count: %d” % dev_count) print(“Total num examples: %d” % total_examples) infer_program = fluid。Program() with fluid。program_guard(infer_program, startup_prog): with fluid。unique_name。guard(): pyreader, graph_vars = create_model( args, pyreader_name=‘reader’, ernie_config=ernie_config) infer_program = infer_program。clone(for_test=True) exe。run(startup_prog) if args。init_pretraining_params: init_pretraining_params( exe, args。init_pretraining_params, main_program=startup_prog) else: raise ValueError( “WARNING: args ‘init_pretraining_params’ must be specified”) exec_strategy = fluid。ExecutionStrategy() exec_strategy。num_threads = dev_count pyreader。decorate_tensor_provider(data_generator) pyreader。start() total_cls_emb = [] total_top_layer_emb = [] total_labels = [] while True: try: cls_emb, unpad_top_layer_emb = exe。run( program=infer_program, fetch_list=[ graph_vars[“cls_embeddings”]。name, graph_vars[“top_layer_embeddings”]。name ], return_numpy=False) # batch_size * embedding_size total_cls_emb。append(np。array(cls_emb)) total_top_layer_emb。append(np。array(unpad_top_layer_emb)) except fluid。core。EOFException: break total_cls_emb = np。concatenate(total_cls_emb) total_top_layer_emb = np。concatenate(total_top_layer_emb) with open(os。path。join(args。output_dir, “cls_emb。npy”), “wb”) as cls_emb_file: np。save(cls_emb_file, total_cls_emb) with open(os。path。join(args。output_dir, “top_layer_emb。npy”), “wb”) as top_layer_emb_file: np。save(top_layer_emb_file, total_top_layer_emb)if __name__ == ‘__main__’: args = parser。parse_args() print_arguments(args) main(args)

3:利用 Fine-tuning 得到的模型對新資料進行批次預測

我們以分類任務為例,給出了分類任務進行批次預測的指令碼, 使用示例如下:

python -u predict_classifier。py \ ——use_cuda true \ ——batch_size 32 \ ——vocab_path ${MODEL_PATH}/vocab。txt \ ——init_checkpoint “。/checkpoints/step_100” \ ——do_lower_case true \ ——max_seq_len 128 \ ——ernie_config_path ${MODEL_PATH}/ernie_config。json \ ——do_predict true \ ——predict_set ${TASK_DATA_PATH}/lcqmc/test。tsv \ ——num_labels 2

predict_classifier。py 程式碼如下:

from __future__ import absolute_importfrom __future__ import divisionfrom __future__ import print_functionimport osimport timeimport argparseimport numpy as npimport multiprocessing# NOTE(paddle-dev): All of these flags should be# set before `import paddle`。 Otherwise, it would# not take any effect。os。environ[‘FLAGS_eager_delete_tensor_gb’] = ‘0’ # enable gcimport paddle。fluid as fluidfrom reader。task_reader import ClassifyReaderfrom model。ernie import ErnieConfigfrom finetune。classifier import create_modelfrom utils。args import ArgumentGroup, print_argumentsfrom utils。init import init_pretraining_paramsfrom finetune_args import parser# yapf: disableparser = argparse。ArgumentParser(__doc__)model_g = ArgumentGroup(parser, “model”, “options to init, resume and save model。”)model_g。add_arg(“ernie_config_path”, str, None, “Path to the json file for ernie model config。”)model_g。add_arg(“init_checkpoint”, str, None, “Init checkpoint to resume training from。”)model_g。add_arg(“save_inference_model_path”, str, “inference_model”, “If set, save the inference model to this path。”)model_g。add_arg(“use_fp16”, bool, False, “Whether to resume parameters from fp16 checkpoint。”)model_g。add_arg(“num_labels”, int, 2, “num labels for classify”)model_g。add_arg(“ernie_version”, str, “1。0”, “ernie_version”)data_g = ArgumentGroup(parser, “data”, “Data paths, vocab paths and data processing options。”)data_g。add_arg(“predict_set”, str, None, “Predict set file”)data_g。add_arg(“vocab_path”, str, None, “Vocabulary path。”)data_g。add_arg(“label_map_config”, str, None, “Label_map_config json file。”)data_g。add_arg(“max_seq_len”, int, 128, “Number of words of the longest seqence。”)data_g。add_arg(“batch_size”, int, 32, “Total examples‘ number in batch for training。 see also ——in_tokens。”)data_g。add_arg(“do_lower_case”, bool, True, “Whether to lower case the input text。 Should be True for uncased models and False for cased models。”)run_type_g = ArgumentGroup(parser, “run_type”, “running type options。”)run_type_g。add_arg(“use_cuda”, bool, True, “If set, use GPU for training。”)run_type_g。add_arg(“do_prediction”, bool, True, “Whether to do prediction on test set。”)args = parser。parse_args()# yapf: enable。def main(args): ernie_config = ErnieConfig(args。ernie_config_path) ernie_config。print_config() reader = ClassifyReader( vocab_path=args。vocab_path, label_map_config=args。label_map_config, max_seq_len=args。max_seq_len, do_lower_case=args。do_lower_case, in_tokens=False, is_inference=True) predict_prog = fluid。Program() predict_startup = fluid。Program() with fluid。program_guard(predict_prog, predict_startup): with fluid。unique_name。guard(): predict_pyreader, probs, feed_target_names = create_model( args, pyreader_name=’predict_reader‘, ernie_config=ernie_config, is_classify=True, is_prediction=True, ernie_version=args。ernie_version) predict_prog = predict_prog。clone(for_test=True) if args。use_cuda: place = fluid。CUDAPlace(0) dev_count = fluid。core。get_cuda_device_count() else: place = fluid。CPUPlace() dev_count = int(os。environ。get(’CPU_NUM‘, multiprocessing。cpu_count())) place = fluid。CUDAPlace(0) if args。use_cuda == True else fluid。CPUPlace() exe = fluid。Executor(place) exe。run(predict_startup) if args。init_checkpoint: init_pretraining_params(exe, args。init_checkpoint, predict_prog) else: raise ValueError(“args ’init_checkpoint‘ should be set for prediction!”) assert args。save_inference_model_path, “args save_inference_model_path should be set for prediction” _, ckpt_dir = os。path。split(args。init_checkpoint。rstrip(’/‘)) dir_name = ckpt_dir + ’_inference_model‘ model_path = os。path。join(args。save_inference_model_path, dir_name) print(“save inference model to %s” % model_path) fluid。io。save_inference_model( model_path, feed_target_names, [probs], exe, main_program=predict_prog) print(“load inference model from %s” % model_path) infer_program, feed_target_names, probs = fluid。io。load_inference_model( model_path, exe) src_ids = feed_target_names[0] sent_ids = feed_target_names[1] pos_ids = feed_target_names[2] input_mask = feed_target_names[3] if args。ernie_version == “2。0”: task_ids = feed_target_names[4] predict_data_generator = reader。data_generator( input_file=args。predict_set, batch_size=args。batch_size, epoch=1, shuffle=False) print(“———————— prediction results ————————”) np。set_printoptions(precision=4, suppress=True) index = 0 for sample in predict_data_generator(): src_ids_data = sample[0] sent_ids_data = sample[1] pos_ids_data = sample[2] task_ids_data = sample[3] input_mask_data = sample[4] if args。ernie_version == “1。0”: output = exe。run( infer_program, feed={src_ids: src_ids_data, sent_ids: sent_ids_data, pos_ids: pos_ids_data, input_mask: input_mask_data}, fetch_list=probs) elif args。ernie_version == “2。0”: output = exe。run( infer_program, feed={src_ids: src_ids_data, sent_ids: sent_ids_data, pos_ids: pos_ids_data, task_ids: task_ids_data, input_mask: input_mask_data}, fetch_list=probs) else: raise ValueError(“ernie_version must be 1。0 or 2。0”) for single_result in output[0]: print(“example_index:{}\t{}”。format(index, single_result)) index += 1if __name__ == ’__main__‘: print_arguments(args) main(args)

四:總結

本次,我們介紹了

ERNIE的基本結構

ERNIE的訓練流程

預訓練任務,獲取輸入句子/詞經過 ERNIE編碼後的 Embedding 表示,以及批次預測的程式碼

希望經過本文的介紹,希望能夠讓大家對ERNIE有一個全面的瞭解。

官網地址: https://www。paddlepaddle。org。cn

專案地址: https://github。com/PaddlePaddle/ERNIE