本文原作者:梁源
BERT (Bidirectional Encoder Representations from Transformers) 官方代碼庫 包含了BERT的實現代碼與使用BERT進行文本分類和問題回答兩個demo。本文對官方代碼庫的結構進行整理和分析,並在此基礎上介紹本地數據集使用 BERT 進行 finetune 的操作流程。BERT的原理介紹見參考文獻[3]。
BERT是一種能夠生成句子中詞向量表示以及句子向量表示的深度學習模型,其生成的向量表示可以用於詞級別的自然語言處理任務(如序列標注)和句子級別的任務(如文本分類)。
從頭開始訓練BERT模型所需要的計算量很大,但Google公開了在多種語言(包括中文)上預訓練好的BERT模型參數,因此可以在此基礎上,對自定義的任務進行finetune。相比於從頭訓練BERT模型的參數,對自定義任務進 行finetune所需的計算量要小得多。
本文的第一部分對BERT的官方代碼結構進行介紹。第二部分以文本分類任務為例,介紹在自己的數據集上對BERT模型進行 finetune 的操作流程。
1. BERT實現代碼
BERT官方項目的目錄結構如下圖所示:

下文中將分別介紹項目中各模塊的結構和功能。
1.1 modeling.py
如下圖所示,modeling.py定義了BERT模型的主體結構,即從input_ids(句子中詞語id組成的tensor)
到sequence_output(句子中每個詞語的向量表示)
以及pooled_output(句子的向量表示)
的計算過程,是其它所有后續的任務的基礎。如文本分類任務就是得到輸入的input_ids后,用BertModel得到句子的向量表示,並將其作為分類層的輸入,得到分類結果。

modeling.py的31-106行定義了一個BertConfig類,即BertModel的配置,在新建一個BertModel類時,必須配置其對應的BertConfig。BertConfig類包含了一個BertModel所需的超參數,除詞表大小vocab_size外,均定義了其默認取值。BertConfig類中還定義了從python dict和json中生成BertConfig的方法以及將BertConfig轉換為python dict 或者json字符串的方法。
107-263行定義了一個BertModel類。BertModel類初始化時,需要填寫三個沒有默認值的參數:
- config:即31-106行定義的BertConfig類的一個對象;
- is_training:如果訓練則填true,否則填false,該參數會決定是否執行dropout。
- input_ids:一個
[batch_size, seq_length]
的tensor,包含了一個batch的輸入句子中的詞語id。
另外還有input_mask,token_type_ids和use_one_hot_embeddings,scope四個可選參數,scope參數會影響計算圖中tensor的名字前綴,如不填寫,則前綴為”bert”。在下文中,其余參數會在使用時進行說明。
BertModel的計算都在__init__
函數中完成。計算流程如下:
- 為了不影響原config對象,對config進行deepcopy,然后對is_training進行判斷,如果為False,則將config中dropout的概率均設為0。
- 定義input_mask和token_type_ids的默認取值(前者為全1,后者為全0),shape均和input_ids相同。二者的用途會在下文中提及。
- 使用embedding_lookup函數,將input_ids轉化為向量,形狀為
[batch_size, seq_length, embedding_size]
,這里的embedding_table使用tf.get_variable,因此第一次調用時會生成,后續都是直接獲取現有的。此處use_one_hot_embedding的取值只影響embedding_lookup函數的內部實現,不影響結果。 - 調用embedding_postprocessor對輸入句子的向量進行處理。這個函數分為兩部分,先按照token_type_id(即輸入的句子中各個詞語的type,如對兩個句子的分類任務,用type_id區分第一個句子還是第二個句子),lookup出各個詞語的type向量,然后加到各個詞語的向量表示中。如果token_type_id不存在(即不使用額外的type信息),則跳過這一步。其次,這個函數計算position_embedding:即初始化一個shape為
[max_positition_embeddings, width]
的position_embedding矩陣,再按照對應的position加到輸入句子的向量表示中。如果不使用position_embedding,則跳過這一步。最后對輸入句子的向量進行layer_norm和dropout,如果不是訓練階段,此處dropout概率為0.0,相當於跳過這一步。 - 根據輸入的input_mask(即與句子真實長度匹配的mask,如batch_size為2,句子實際長度分別為2,3,則mask為
[[1, 1, 0], [1, 1, 1]]
),計算shape為[batch_size, seq_length, seq_length]
的mask,並將輸入句子的向量表示和mask共同傳給transformer_model函數,即encoder部分。 - transformer_model函數的行為是先將輸入的句子向量表示reshape成
[batch_size * seq_length, width]
的矩陣,然后循環調用transformer的前向過程,次數為隱藏層個數。每次前向過程都包含self_attention_layer、add_and_norm、feed_forward和add_and_norm四個步驟,具體信息可參考transformer的論文。 - 獲取transformer_model最后一層的輸出,此時shape為
[batch_size, seq_length, hidden_size]
。如果要進行句子級別的任務,如句子分類,需要將其轉化為[batch_size, hidden_size]
的tensor,這一步通過取第一個token的向量表示完成。這一層在代碼中稱為pooling層。 - BertModel類提供了接口來獲取不同層的輸出,包括:
- embedding層的輸出,shape為
[batch_size, seq_length, embedding_size]
- pooling層的輸出,shape為
[batch_size, hidden_size]
- sequence層的輸出,shape為
[batch_size, seq_length, hidden_size]
- encoder各層的輸出
- embedding_table
- embedding層的輸出,shape為
modeling.py的其余部分定義了上面的步驟用到的函數,以及激活函數等。
1.2 run_classifier.py
這個模塊可以用於配置和啟動基於BERT的文本分類任務,包括輸入樣本為句子對的(如MRPC)和輸入樣本為單個句子的(如CoLA)。
模塊中的內容包括:
- InputExample類。一個輸入樣本包含id,text_a,text_b和label四個屬性,text_a和text_b分別表示第一個句子和第二個句子,因此text_b是可選的。
- PaddingInputExample類。定義這個類是因為TPU只支持固定大小的batch,在eval和predict的時候需要對batch做padding。如不使用TPU,則無需使用這個類。
- InputFeatures類,定義了輸入到estimator的model_fn中的feature,包括input_ids,input_mask,segment_ids(即0或1,表明詞語屬於第一個句子還是第二個句子,在BertModel中被看作token_type_id),label_id以及is_real_example。
- DataProcessor類以及四個公開數據集對應的子類。一個數據集對應一個DataProcessor子類,需要繼承四個函數:分別從文件目錄中獲得train,eval和predict樣本的三個函數以及一個獲取label集合的函數。如果需要在自己的數據集上進行finetune,則需要實現一個DataProcessor的子類,按照自己數據集的格式從目錄中獲取樣本。注意!在這一步驟中,對沒有label的predict樣本,要指定一個label的默認值供統一的model_fn使用。
- convert_single_example函數。可以對一個InputExample轉換為InputFeatures,里面調用了tokenizer進行一些句子清洗和預處理工作,同時截斷了長度超過最大值的句子。
- file_based_convert_example_to_features函數:將一批InputExample轉換為InputFeatures,並寫入到tfrecord文件中,相當於實現了從原始數據集文件到tfrecord文件的轉換。
- file_based_input_fn_builder函數:這個函數用於根據tfrecord文件,構建estimator的input_fn,即先建立一個TFRecordDataset,然后進行shuffle,repeat,decode和batch操作。
- create_model函數:用於構建從input_ids到prediction和loss的計算過程,包括建立BertModel,獲取BertModel的pooled_output,即句子向量表示,然后構建隱藏層和bias,並計算logits和softmax,最終用cross_entropy計算出loss。
- model_fn_builder:根據create_model函數,構建estimator的model_fn。由於model_fn需要labels輸入,為簡化代碼減少判斷,當要進行predict時也要求傳入label,因此DataProcessor中為每個predict樣本生成了一個默認label(其取值並無意義)。這里構建的是TPUEstimator,但沒有TPU時,它也可以像普通estimator一樣工作。
- input_fn_builder和convert_examples_to_features目前並沒有被使用,應為開放供開發者使用的功能。
- main函數:
- 首先定義任務名稱和processor的對應關系,因此如果定義了自己的processor,需要將其加入到processors字典中。
- 其次從FLAGS中,即啟動命令中讀取相關參數,構建model_fn和estimator,並根據參數中的do_train,do_eval和do_predict的取值決定要進行estimator的哪些操作。
1.3 run_pretraining.py
這個模塊用於BERT模型的預訓練,即使用masked language model和next sentence的方法,對BERT模型本身的參數進行訓練。如果使用現有的預訓練BERT模型在文本分類/問題回答等任務上進行fine_tune,則無需使用run_pretraining.py。
1.4 create_pretraining_data.py
此處定義了如何將普通文本轉換成可用於預訓練BERT模型的tfrecord文件的方法。如果使用現有的預訓練BERT模型在文本分類/問題回答等任務上進行fine_tune,則無需使用create_pretraining_data.py。
1.5 tokenization.py
此處定義了對輸入的句子進行預處理的操作,預處理的內容包括:
- 轉換為Unicode
- 切分成數組
- 去除控制字符
- 統一空格格式
- 切分中文字符(即給連續的中文字符之間加上空格)
- 將英文單詞切分成小片段(如[“unaffable”]切分為[“un”, “##aff”, “##able”])
- 大小寫和特殊形式字母轉換
- 分離標點符號(如 [“hello?”]轉換為 [“hello”, “?”])
1.6 run_squad.py
這個模塊可以配置和啟動基於BERT在squad數據集上的問題回答任務。
1.7 extract_features.py
這個模塊可以使用預訓練的BERT模型,生成輸入句子的向量表示和輸入句子中各個詞語的向量表示(類似ELMo)。這個模塊不包含訓練的過程,只是執行BERT的前向過程,使用固定的參數對輸入句子進行轉換。
1.8 optimization.py
這個模塊配置了用於BERT的optimizer,即加入weight decay功能和learning_rate warmup功能的AdamOptimizer。
2. 在自己的數據集上finetune
BERT官方項目搭建了文本分類模型的model_fn,因此只需定義自己的DataProcessor,即可在自己的文本分類數據集上進行訓練。
訓練自己的文本分類數據集所需步驟如下:
- 下載預訓練的BERT模型參數文件,如(https://storage.googleapis.com/bert_models/2018_10_18/uncased_L-12_H-768_A-12.zip ),解壓后的目錄應包含
bert_config.json
,bert_model.ckpt.data-00000-of-00001
,bert_model.ckpt.index
,bert_model_ckpt.meta
和vocab.txt
五個文件。 - 將自己的數據集統一放到一個目錄下。為簡便起見,事先將其划分成
train.txt
,eval.txt
和predict.txt
三個文件,每個文件中每行為一個樣本,格式如下(可以使用任何自定義格式,只需要編寫符合要求的DataProcessor子類即可): simplistic , silly and tedious . __label__0 即句子和標簽之間用__label__划分,句子中的詞語之間用空格划分。 - 修改
run_classifier.py
,或者復制一個副本,命名為run_custom_classifier.py
或類似文件名后進行修改。 - 新建一個DataProcessor的子類,並繼承三個get_examples方法和一個get_labels方法。三個get_examples方法需要從數據集目錄中獲得各自對應的InputExample列表。以get_train_examples方法為例,該方法需要傳入唯一的一個參數data_dir,即數據集所在目錄,然后根據該目錄讀取訓練數據,將所有用於訓練的句子轉換為InputExample,並返回所有InputExample組成的列表。get_dev_examples和get_test_examples方法同理。get_labels方法僅需返回一個所有label的集合組成的列表即可。本例中get_train_examples方法和get_labels方法的實現如下(此處省略get_dev_examples和get_test_examples): class RtPolarityProcessor(DataProcessor): """Processor of the rt-polarity data set""" @staticmethod def read_raw_text(input_file): with tf.gfile.Open(input_file, "r") as f: lines = f.readlines() return lines def get_train_examples(self, data_dir): """See base class""" lines = self.read_raw_text(os.path.join(data_dir, "train.txt")) examples = [] for i, line in enumerate(lines): guid = "train-%d" % (i + 1) line = line.strip().split("__label__") text_a = tokenization.convert_to_unicode(line[0]) label = line[1] examples.append( InputExample(guid=guid, text_a=text_a, label=label) ) return examples def get_labels(self): return ["0", "1"]
- 在main函數中,向main函數開頭的processors字典增加一項,key為自己的數據集的名稱,value為上一步中定義的DataProcessor的類名: processors = { "cola": ColaProcessor, "mnli": MnliProcessor, "mrpc": MrpcProcessor, "xnli": XnliProcessor, "rt_polarity": RtPolarityProcessor, }
- 執行python run_custom_classifier.py,啟動命令中包含必填參數data_dir,task_name,vocab_file,bert_config_file,output_dir。參數do_train,do_eval和do_predict分別控制了是否進行訓練,評估和預測,可以按需將其設置為True或者False,但至少要有一項設為True。
- 為了從預訓練的checkpoint開始finetune,啟動命令中還需要配置init_checkpoint參數。假設BERT模型參數文件解壓后的路徑為
/uncased_L-12_H-768_A-12
,則將init_checkpoint參數配置為/uncased_L-12_H-768_A-12/bert_model.ckpt
。其它可選參數,如learning_rate等,可參考文件中FLAGS的定義自行配置或使用默認值。 - 在沒有TPU的情況下,即使使用了GPU,這一步有可能會在日志中看到
Running train on CPU
字樣。對此,官方項目的readme中做出了解釋:”Note: You might see a messageRunning train on CPU
. This really just means that it’s running on something other than a Cloud TPU, which includes a GPU. “,因此無需在意。
如果需要訓練文本分類之外的模型,如命名實體識別,BERT的官方項目中沒有完整的demo,因此需要設計和實現自己的model_fn和input_fn。以命名實體識別為例,model_fn的基本思路是,根據輸入句子的input_ids生成一個BertModel,獲得BertModel的sequence_output(shape為[batch_size,max_length,hidden_size]
),再結合全連接層和crf等函數進行序列標注。
這是BERT介紹的第一篇文章。后續我們會將BERT整合進智能鈦機器學習平台,並基於智能鈦機器學習平台,講解BERT用於文本分類、序列化標注、問答等任務的細節,並對比其他方法,給出benchmark。