基於飛槳開源的持續學習的語義理解框架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 初探
2.1.1 ERNIE 結構詳解
Figure 2:ERNIE 的encoder 結構詳解
相比transformer , ERNIE 基本上是 transformer 的encoder 部分,並且encoder 在結構上是全部一樣的,但是並不共享權重,具體區別如下:
Transformer: 6 encoder layers, 512 hidden units, 8 attention headsERNIE Base: 12 encoder layers, 768 hidden units, 12 attention headsERNIE 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的結構!!
Figure 3: encoder 結構詳解
embedding
最下層的encoder的輸入是embedding的向量, 其他的encoder的輸入,便是更下層的encoder的輸出, 一般設置輸入的vectors 的維度為512, 同學們也可以自己設置。
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的方式可以潛在的學習到知識的依賴以及更長的語義依賴來讓模型更具泛化性。
Figure 5: ERNIE 1.0 不同的mask 策略說明
2.3: ERNIE 2.0 介紹
傳統的pre-training 模型主要基於文本中words 和 sentences 之間的共現進行學習, 事實上,訓練文本數據中的詞法結構,語法結構,語義信息也同樣是很重要的。在命名實體識別中人名,機構名,組織名等名詞包含概念信息對應了詞法結構。句子之間的順序對應了語法結構,文章中的語義相關性對應了語義信息。為了去發現訓練數據中這些有價值的信息,在ERNIE 2.0 中,提出了一個預訓練框架,可以在大型數據集合中進行增量訓練。
Figure 6: ERNIE 2.0 框架
2.3.1 ERNIE2.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 結合來共同更新模型。
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 被用來作為模型的輸入。
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 相比於其他詞通常在句子當中有特定的含義,所以在Erine 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 分類任務
通常來說,這些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 作為第二個second sentence. 0 表示強關係, 1 表示弱關係,2表示無關係,通過類似google-distance 的關係來衡量 兩個query之間的語義相關性,更好的建模句對相關性。
三: 代碼梳理
3.1 : 預訓練腳本
set -eux
export FLAGS_eager_delete_tensor_gb=0
export 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
腳本初始化代碼 pretrain_launch.py
from __future__ import absolute_importfrom __future__ import divisionfrom __future__ import print_functionfrom __future__ import unicode_literalsfrom __future__ import absolute_importfrom __future__ import divisionimport sysimport subprocessimport osimport siximport copyimport argparseimport timeimport loggingfrom utils.args import ArgumentGroup, print_arguments, prepare_loggerfrom pretrain_args import parser as worker_parser
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