本文分享自華為雲社群《大語言模型底層原理你都知道嗎?大語言模型底層架構之二GPT實現》,作者:碼上開花_Lancer 。
受到計算機視覺領域採用ImageNet對模型進行一次預訓練,使得模型可以通過海量影象充分學習如何提取特徵,然後再根據任務目標進行模型微調的正規化影響,自然語言處理領域基於預訓練語言模型的方法也逐漸成為主流。以ELMo為代表的動態詞向量模型開啟了語言模型預訓練的大門,此後以GPT 和BERT為代表的基於Transformer 的大規模預訓練語言模型的出現,使得自然語言處理全面進入了預訓練微調正規化新時代。
利用豐富的訓練語料、自監督的預訓練任務以及Transformer 等深度神經網路結構,預訓練語言模型具備了通用且強大的自然語言表示能力,能夠有效地學習到詞彙、語法和語意資訊。將預訓練模型應用於下游任務時,不需要了解太多的任務細節,不需要設計特定的神經網路結構,只需要「微調」預訓練模型,即使用具體任務的標註資料在預訓練語言模型上進行監督訓練,就可以取得顯著的效能提升。
OpenAI 公司在2018 年提出的生成式預訓練語言模型(Generative Pre-Training,GPT)是典型的生成式預訓練語言模型之一。GPT 模型結構如圖2.3所示,由多層Transformer 組成的單向語言模型,主要分為輸入層,編碼層和輸出層三部分。
接下來我將重點介紹GPT 無監督預訓練、有監督下游任務微調以及基於HuggingFace 的預訓練語言模型實踐。
GPT 採用生成式預訓練方法,單向意味著模型只能從左到右或從右到左對文字序列建模,所採用的Transformer 結構和解碼策略保證了輸入文字每個位置只能依賴過去時刻的資訊。
給定文字序列w = w1w2...wn,GPT 首先在輸入層中將其對映為稠密的向量:
其中,是詞wi 的詞向量, 是詞wi 的位置向量,vi 為第i 個位置的單詞經過模型輸入層(第0層)後的輸出。GPT 模型的輸入層與前文中介紹的神經網路語言模型的不同之處在於其需要新增
圖1.1 GPT 預訓練語言模型結構
位置向量,這是Transformer 結構自身無法感知位置導致的,因此需要來自輸入層的額外位置資訊。經過輸入層編碼,模型得到表示向量序列v = v1...vn,隨後將v 送入模型編碼層。編碼層由L 個Transformer 模組組成,在自注意力機制的作用下,每一層的每個表示向量都會包含之前位置表示向量的資訊,使每個表示向量都具備豐富的上下文資訊,並且經過多層編碼後,GPT 能得到每個單詞層次化的組合式表示,其計算過程表示如下:
其中 表示第L 層的表示向量序列,n 為序列長度,d 為模型隱藏層維度,L 為模型總層數。GPT 模型的輸出層基於最後一層的表示h(L),預測每個位置上的條件概率,其計算過程可以表示為:
其中, 為詞向量矩陣,|V| 為詞表大小。單向語言模型是按照閱讀順序輸入文字序列w,用常規語言模型目標優化w 的最大似然估計,使之能根據輸入歷史序列對當前詞能做出準確的預測:
其中θ 代表模型引數。也可以基於馬爾可夫假設,只使用部分過去詞進行訓練。預訓練時通常使用隨機梯度下降法進行反向傳播優化該負似然函數。
通過無監督語言模型預訓練,使得GPT 模型具備了一定的通用語意表示能力。下游任務微調(Downstream Task Fine-tuning)的目的是在通用語意表示基礎上,根據下游任務的特性進行適配。下游任務通常需要利用有標註資料集進行訓練,資料集合使用D 進行表示,每個樣例由輸入長度為n 的文字序列x = x1x2...xn 和對應的標籤y 構成。
首先將文字序列x 輸入GPT 模型,獲得最後一層的最後一個詞所對應的隱藏層輸出h(L)n ,在此基礎上通過全連線層變換結合Softmax 函數,得到標籤預測結果。
其中為全連線層引數,k 為標籤個數。通過對整個標註資料集D 優化如下目標函數
精調下游任務:
下游任務在微調過程中,針對任務目標進行優化,很容易使得模型遺忘預訓練階段所學習到的通用語意知識表示,從而損失模型的通用性和泛化能力,造成災難性遺忘(Catastrophic Forgetting)問題。因此,通常會採用混合預訓練任務損失和下游微調損失的方法來緩解上述問題。在實際應用中,通常採用如下公式進行下游任務微調:
其中λ 取值為[0,1],用於調節預訓練任務損失佔比。
HuggingFace 是一個開源自然語言處理軟體庫。其的目標是通過提供一套全面的工具、庫和模型,使得自然語言處理技術對開發人員和研究人員更加易於使用。HuggingFace 最著名的貢獻之一是Transformer 庫,基於此研究人員可以快速部署訓練好的模型以及實現新的網路結構。除此之外,HuggingFace 還提供了Dataset 庫,可以非常方便地下載自然語言處理研究中最常使用的基準資料集。本節中,將以構建BERT 模型為例,介紹基於Huggingface 的BERT 模型構建和使用方法。
常見的用於預訓練語言模型的大規模資料集都可以在Dataset 庫中直接下載並載入。例如,如果使用維基百科的英文語料集合,可以直接通過如下程式碼完成資料獲取:
from datasets import concatenate_datasets, load_dataset bookcorpus = load_dataset("bookcorpus", split="train") wiki = load_dataset("wikipedia", "20230601.en", split="train") # 僅保留'text' 列 wiki = wiki.remove_columns([col for col in wiki.column_names if col != "text"]) dataset = concatenate_datasets([bookcorpus, wiki]) # 將資料集合切分為90% 用於訓練,10% 用於測試 d = dataset.train_test_split(test_size=0.1)
def dataset_to_text(dataset, output_filename="data.txt"): """Utility function to save dataset text to disk, useful for using the texts to train the tokenizer (as the tokenizer accepts files)""" with open(output_filename, "w") as f: for t in dataset["text"]: print(t, file=f) # save the training set to train.txt dataset_to_text(d["train"], "train.txt") # save the testing set to test.txt dataset_to_text(d["test"], "test.txt")
如前所述,BERT 採用了WordPiece 分詞,根據訓練語料中的詞頻決定是否將一個完整的詞切分為多個詞元。因此,需要首先訓練詞元分析器(Tokenizer)。可以使用transformers 庫中的BertWordPieceTokenizer 類來完成任務,程式碼如下所示:
special_tokens = [ "[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]", "<S>", "<T>" ]# if you want to train the tokenizer on both sets # files = ["train.txt", "test.txt"] # training the tokenizer on the training set files = ["train.txt"] # 30,522 vocab is BERT's default vocab size, feel free to tweak vocab_size = 30_522 # maximum sequence length, lowering will result to faster training (when increasing batch size) max_length = 512 # whether to truncate truncate_longer_samples = False # initialize the WordPiece tokenizer tokenizer = BertWordPieceTokenizer() # train the tokenizer tokenizer.train(files=files, vocab_size=vocab_size, special_tokens=special_tokens) # enable truncation up to the maximum 512 tokens tokenizer.enable_truncation(max_length=max_length) model_path = "pretrained-bert" # make the directory if not already there if not os.path.isdir(model_path): os.mkdir(model_path) # save the tokenizer tokenizer.save_model(model_path) # dumping some of the tokenizer config to config file, # including special tokens, whether to lower case and the maximum sequence length with open(os.path.join(model_path, "config.json"), "w") as f: tokenizer_cfg = { "do_lower_case": True, "unk_token": "[UNK]", "sep_token": "[SEP]", "pad_token": "[PAD]", "cls_token": "[CLS]", "mask_token": "[MASK]", "model_max_length": max_length, "max_len": max_length, } json.dump(tokenizer_cfg, f) # when the tokenizer is trained and configured, load it as BertTokenizerFast tokenizer = BertTokenizerFast.from_pretrained(model_path)
在啟動整個模型訓練之前,還需要將預訓練語料根據訓練好的Tokenizer 進行處理。如果檔案長度超過512 個詞元(Token),那麼就直接進行截斷。資料處理程式碼如下所示:
def encode_with_truncation(examples): """Mapping function to tokenize the sentences passed with truncation""" return tokenizer(examples["text"], truncation=True, padding="max_length", max_length=max_length, return_special_tokens_mask=True) def encode_without_truncation(examples): """Mapping function to tokenize the sentences passed without truncation""" return tokenizer(examples["text"], return_special_tokens_mask=True) # the encode function will depend on the truncate_longer_samples variable encode = encode_with_truncation if truncate_longer_samples else encode_without_truncation # tokenizing the train dataset train_dataset = d["train"].map(encode, batched=True) # tokenizing the testing dataset test_dataset = d["test"].map(encode, batched=True) if truncate_longer_samples: # remove other columns and set input_ids and attention_mask as PyTorch tensors train_dataset.set_format(type="torch", columns=["input_ids", "attention_mask"]) test_dataset.set_format(type="torch", columns=["input_ids", "attention_mask"]) else: # remove other columns, and remain them as Python lists test_dataset.set_format(columns=["input_ids", "attention_mask", "special_tokens_mask"]) train_dataset.set_format(columns=["input_ids", "attention_mask", "special_tokens_mask"])
truncate_longer_samples 布林變數來控制用於對資料集進行詞元處理的encode() 回撥函數。如果設定為True,則會截斷超過最大序列長度(max_length)的句子。否則,不會截斷。如果設為truncate_longer_samples 為False,需要將沒有截斷的樣本連線起來,並組合成固定長度的向量。
在構建了處理好的預訓練語料之後,就可以開始模型訓練。程式碼如下所示:
# initialize the model with the config model_config = BertConfig(vocab_size=vocab_size, max_position_embeddings=max_length) model = BertForMaskedLM(config=model_config) # initialize the data collator, randomly masking 20% (default is 15%) of the tokens # for the Masked Language Modeling (MLM) task data_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True, mlm_probability=0.2 ) training_args = TrainingArguments( output_dir=model_path, # output directory to where save model checkpoint evaluation_strategy="steps", # evaluate each `logging_steps` steps overwrite_output_dir=True, num_train_epochs=10, # number of training epochs, feel free to tweak per_device_train_batch_size=10, # the training batch size, put it as high as your GPU memory fits gradient_accumulation_steps=8, # accumulating the gradients before updating the weights per_device_eval_batch_size=64, # evaluation batch size logging_steps=1000, # evaluate, log and save model checkpoints every 1000 step save_steps=1000, # load_best_model_at_end=True, # whether to load the best model (in terms of loss) # at the end of training # save_total_limit=3, # whether you don't have much space so you # let only 3 model weights saved in the disk ) trainer = Trainer( model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=test_dataset, ) # train the model trainer.train()
開始訓練後,可以如下輸出結果:
[10135/79670 18:53:08 < 129:35:53, 0.15 it/s, Epoch 1.27/10] Step Training Loss Validation Loss 1000 6.904000 6.558231 2000 6.498800 6.401168 3000 6.362600 6.277831 4000 6.251000 6.172856 5000 6.155800 6.071129 6000 6.052800 5.942584 7000 5.834900 5.546123 8000 5.537200 5.248503 9000 5.272700 4.934949 10000 4.915900 4.549236
基於訓練好的模型,可以針對不同應用需求進行使用。
# load the model checkpoint model = BertForMaskedLM.from_pretrained(os.path.join(model_path, "checkpoint-10000")) # load the tokenizer tokenizer = BertTokenizerFast.from_pretrained(model_path) fill_mask = pipeline("fill-mask", model=model, tokenizer=tokenizer) # perform predictions examples = [ "Today's most trending hashtags on [MASK] is Donald Trump", "The [MASK] was cloudy yesterday, but today it's rainy.", ] for example in examples: for prediction in fill_mask(example): print(f"{prediction['sequence']}, confidence: {prediction['score']}") print("="*50)
today's most trending hashtags on twitter is donald trump, confidence: 0.1027069091796875 today's most trending hashtags on monday is donald trump, confidence: 0.09271949529647827 today's most trending hashtags on tuesday is donald trump, confidence: 0.08099588006734848 today's most trending hashtags on facebook is donald trump, confidence: 0.04266013577580452 today's most trending hashtags on wednesday is donald trump, confidence: 0.04120611026883125 ================================================== the weather was cloudy yesterday, but today it's rainy., confidence: 0.04445931687951088 the day was cloudy yesterday, but today it's rainy., confidence: 0.037249673157930374 the morning was cloudy yesterday, but today it's rainy., confidence: 0.023775646463036537 the weekend was cloudy yesterday, but today it's rainy., confidence: 0.022554103285074234 the storm was cloudy yesterday, but today it's rainy., confidence: 0.019406016916036606 ==================================================
本篇文章詳細重點介紹GPT 無監督預訓練、有監督下游任務微調以及基於HuggingFace 的預訓練語言模型實踐,下一篇文章我將介紹大語言模型網路結構和注意力機制優化以及相關實踐。
參考文章: