本文來自OPPO互聯網基礎技術團隊,轉載請註名做者。同時歡迎關注咱們的公衆號:OPPO_tech,與你分享OPPO前沿互聯網技術及活動。
BERT是近幾年NLP領域中具備里程碑意義的存在。由於效果好和應用範圍廣因此被普遍應用於科學研究和工程項目中。本篇從源碼的角度,從總體到局部分析BERT模型中分類器部分的源碼。git
對於機器學習工程師來講,會調包跑程序應該是萬里長征的第一步。這一步主要是幫助咱們迅速將模型應用到實際業務中,而且提高自信心,但這還遠遠不夠。要想根據不一樣的業務場景更好的使用模型,咱們須要深層次的理解模型,讀點源碼才能走的更遠。github
本篇解讀的是BERT開源項目中分類器部分的源碼,從最開始的數據輸入到模型運行整個流程主要能夠分紅數據處理模塊、特徵處理模塊、模型構建模塊和模型運行模塊。具體以下圖所示:app
圖1 BERT分類器總體模塊劃分框架
由於原生態BERT預訓練模型動輒幾百兆甚至上千兆的大小,模型訓練速度很是慢,對於BERT模型線上化很是不友好,因此使用目前比較火的BERT最新派生產品ALBERT來完成BERT線上化服務。ALBERT使用參數減小技術來下降內存消耗從而最終達到提升BERT的訓練速度,而且在主要基準測試中均名列前茅,可謂跑的快,還跑的好。本篇解讀的BERT源碼也是基於ALBERT開源項目。機器學習
項目開源的github工程:github.com/wilsonlsm006函數
主要解讀分類器部分的源碼,代碼及註釋在run_classifier.py文件,歡迎小夥伴們fork。學習
數據處理模塊主要負責數據讀入和預處理功能。測試
數據處理主要由數據處理器DataProcessor來完成。根據不一樣的任務會有不一樣的數據處理器子類,這裏的不一樣表如今數據讀入方式和數據預處理方面。大數據
實際項目中數據讀入的方式多種多樣,好比csv、tsv、txt等。好比有的項目是須要讀取csv文件,而有的則須要tsv或者txt格式。咱們能夠構建自定義的數據處理器來完成不一樣的項目需求。ui
數據預處理是根據不一樣的NLP任務來完成不一樣的操做,好比單句分類任務咱們須要的是text_a和label格式。而句子類似關係判斷任務須要的是text_a,text_b,label格式。其餘任務也是相似的,根據不一樣的NLP任務來完成數據預處理操做。
經過一個類圖來說解源碼中的數據處理器:
圖2 數據處理器類圖
對應到項目源碼中,咱們有一個DataProcessor父類。父類中有五個方法,分別是讀取tsv文件、得到訓練集、得到驗證集、得到測試集和得到標籤。這裏可根據業務需求增刪改獲取文件類型的函數,好比讀取csv能夠添加get_csv(input_file)等等。
class DataProcessor(object): """Base class for data converters for sequence classification data sets.""" def get_train_examples(self, data_dir): """Gets a collection of `InputExample`s for the train set.""" raise NotImplementedError() def get_dev_examples(self, data_dir): """Gets a collection of `InputExample`s for the dev set.""" raise NotImplementedError() def get_test_examples(self, data_dir): """Gets a collection of `InputExample`s for prediction.""" raise NotImplementedError() def get_labels(self): """Gets the list of labels for this data set.""" raise NotImplementedError() @classmethod def _read_tsv(cls, input_file, quotechar=None): """Reads a tab separated value file.""" with tf.gfile.Open(input_file, "r") as f: reader = csv.reader(f, delimiter="\t", quotechar=quotechar) lines = [] for line in reader: lines.append(line) return lines
下面兩個子類,分別是處理句子關係判斷任務的SentencePairClassificationProcessor數據處理器和LCQMCPairClassificationProcessor分類的數據處理器。前面有講過若是須要作單句分類的任務咱們能夠在這裏添加一個SentenceClassifierProcess進行定製化開發。
對應到項目源碼中,由於咱們是句子關係判斷任務,其實就是判斷兩句話是否是有關係,這裏咱們獲得的最終數據格式是列表類型,具體數據格式以下:
[(guid,text_a,text_b,label),(guid,text_a,text_b,label),....]
其中guid做爲惟一識別text_a和text_b句子對的標誌,能夠理解爲該條樣例的惟一id;
text_a和text_b是須要判斷的兩個句子;
label字段就是標籤,若是兩句話類似則置爲1,不然爲0。
上面四個字段guid和text_a是必須的。text_b是可選的,若是爲空則變成單句分類任務,不爲空則是句子關係判斷任務。label在訓練集和驗證集是必須的,在測試集中能夠不提供。
具體代碼在SentencePairClassificationProcessor子類的_create_examples函數:
def _create_examples(self, lines, set_type): """Creates examples for the training and dev sets.""" examples = [] print("length of lines:", len(lines)) for (i, line) in enumerate(lines): # print('#i:',i,line) if i == 0: continue guid = "%s-%s" % (set_type, i) try: label = tokenization.convert_to_unicode(line[2]) text_a = tokenization.convert_to_unicode(line[0]) text_b = tokenization.convert_to_unicode(line[1]) examples.append( InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label)) except Exception: print('###error.i:', i, line) return examples
特徵處理模塊主要的功能是將數據處理模塊獲得的數據轉化成特徵並持久化到TFRecord文件中,由file_based_convert_examples_to_features函數完成。
""" 將數據處理模塊獲得的數據轉化成TFRecord文件 input: examples:數據格式爲[(guid,text_a,text_b,label),(guid,text_a,text_b,label),....] label_list:標籤列表 max_seq_length:容許的句子最大長度 tokenizer:分詞器 output_file:TFRecord文件存儲路徑 output:持久化到TFRecord格式文件 """ def file_based_convert_examples_to_features( examples, label_list, max_seq_length, tokenizer, output_file):
數據轉化成特徵的操做主要由函數convert_single_example完成。傳統的機器學習須要從數據中抽取特徵,NLP任務是對文本進行分詞等操做獲取特徵。BERT模型中默認每一個字字就是一個詞。
""" 將預處理數據加工成模型須要的特徵 input: ex_index:數據條數索引 example:數據格式爲[(guid,text_a,text_b,label),(guid,text_a,text_b,label),....] label_list:標籤列表 max_seq_length:容許的句子最大長度,這裏若是輸入句子長度不足則補0 tokenizer:分詞器 output: feature = InputFeatures( input_ids=input_ids:token embedding:表示詞向量,第一個詞是CLS,分隔詞有SEP,是單詞自己 input_mask=input_mask:position embedding:爲了令transformer感知詞與詞之間的位置關係 segment_ids=segment_ids:segment embedding:text_a與text_b的句子關係 label_id=label_id:標籤 is_real_example=True) """ def convert_single_example(ex_index, example, label_list, max_seq_length,tokenizer): .... feature = InputFeatures( input_ids=input_ids, input_mask=input_mask, segment_ids=segment_ids, label_id=label_id, is_real_example=True) return feature
論文中BERT模型的輸入轉化成特徵以下圖所示:
圖3 句子輸入轉化成三層Embedding
這裏須要注意下對text_a和text_b的預處理操做。首先會進行標記化將text_a和text_b轉化成tokens_a和tokens_b。
若是tokens_b存在,那麼tokens_a和tokens_b的長度就不能超過max_seq_length-3,由於須要加入cls,sep,seq三個符號;若是tokens_b不存在,那麼tokens_a的長度不能超過 max_seq_length -2 ,由於須要加入 cls 和 sep符號。
這裏經過一條具體的數據轉化成特徵說明上述流程。如今咱們的example中有一條數據,分別有三個字段:
通過分詞以後,咱們會獲得:
tokens: [CLS] 這 種 圖 片 是 用 什 麼 軟 件 制 做 的 ? [SEP] 這 種 圖 片 制 做 是 用 什 麼 軟 件 呢 ? [SEP]
其中[CLS]是模型額外增長的開始標誌,說明這是句首位置。[SEP]表明分隔符,咱們會將兩句話拼接成一句話,經過分隔符來識別。第二句話拼接完成後也會加上一個分隔符。這裏須要注意的是BERT對於中文分詞是以每一個字進行切分,並非咱們一般理解的按照中文實際的詞進行切分。
通過特徵提取以後變成了:
input_ids:101 6821 4905 1745 4275 3221 4500 784 720 6763 816 1169 868 46388043 102 6821 4905 1745 4275 1169 868 3221 4500 784 720 6763 816 1450 8043 1020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
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 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0
segment_ids:0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0
label_id:1
這裏詳細說下咱們真正給模型輸入的特徵是什麼。
input_ids表明詞向量編碼。NLP任務中咱們會將文本轉化成詞向量的表徵形式提供給模型。經過BERT源碼中的tokenizer將句子拆分紅字,而且將字映射成id。好比上面例子中第一句話有14個字,第二句話也有14個字,再加上一個開始標誌和兩個分隔符,一種有31個字。而上面例子中的input_ids列表中前31個位置都有每一個字映射的id,而且相同字的映射的id也是同樣的。其餘則經過添加0進行填充;
input_mask表明位置編碼。爲了transformer感知詞與詞之間的位置關係,源碼中會將當前位置有字的設置爲1,其餘用0進行填充;
segment_ids表明句子關係編碼。若是是句子關係判斷任務則會將text_b位置對應的句子關係編碼置爲1。這裏須要注意,只要是句子關係判斷任務,無論兩句話到底有沒有關係,即標籤是否爲1都會將text_b位置對應的句子關係編碼置爲1;
label_id就表明兩句話是否是有關係。若是有關係則標籤置爲1,不然爲0。
當咱們進行模型訓練的時候,會將所有訓練數據加載到內存中。對於小規模數據集來講沒有問題,可是遇到大規模數據集時咱們的內存並不能加載所有的數據,因此涉及到分批加載數據。Tensorflow給開發者提供了TFRecord格式文件。TFRecord內部採用二進制編碼,加載快,對大型數據轉換友好。
小結下,特徵處理模塊主要將預處理獲得的數據轉化成特徵並存儲到TFRecord格式文件。BERT會將句子輸入轉化成三層Embedding編碼,第一層是詞編碼,主要表示詞自己;第二層編碼是位置編碼,主要爲了transformer感知詞與詞之間的位置關係;第三層編碼則表示句與句之間關係。經過這三層編碼咱們就獲得了模型的特徵輸入。爲了方便大數據集下模型訓練加載數據,咱們將特徵持久化到TFRecord格式文件。
模型構建模塊主要分紅模型構建和模型標準輸入。
經過函數model_fn_builder來構建自定義模型估計器。
""" 自定義模型估計器(model_fn_builder) input:bert_config:bert相關的配置 num_labels:標籤的數量 init_checkpoint:預訓練模型 learning_rate:學習率 num_train_steps:模型訓練輪數 = (訓練集總數/batch_size)*epochs num_warmup_steps:線性地增長學習率,num_warmup_steps = num_train_steps * warmup_proportion use_tpu:是否使用TPU output:構建好的模型 """ def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate, num_train_steps, num_warmup_steps, use_tpu, use_one_hot_embeddings): """Returns `model_fn` closure for TPUEstimator.""" ...... return model_fn
這裏模型構建主要有create_model函數完成,主要完成兩件事:第一是調用modeling.py中的BertModel類建立模型;第二是計算交叉熵損失loss。交叉熵的值越小,兩個機率分佈就越接近。
""" 建立模型,主要完成兩件事:第一件事是調用modeling.py中國的BertModel類建立模型; 第二件事事計算交叉熵損失loss。交叉熵的值越小,兩個機率分佈就越接近。 """ def create_model(bert_config, is_training, input_ids, input_mask, segment_ids, labels, num_labels, use_one_hot_embeddings): """Creates a classification model.""" # 創建一個BERT分類模型(create_model) model = modeling.BertModel( config=bert_config, is_training=is_training, input_ids=input_ids, input_mask=input_mask, token_type_ids=segment_ids, use_one_hot_embeddings=use_one_hot_embeddings) ...... return (loss, per_example_loss, logits, probabilities)
由於源項目是基於Tensorflow框架開發,因此須要將前面獲得的特徵轉化成標準的Tensorflow模型輸入格式。這塊主要由函數file_based_input_fn_builder來完成。經過輸入文件的不一樣能夠完成訓練集、驗證集和測試集的輸入。
""" 模型標準輸入 從TFRecord格式文件中讀取特徵並轉化成TensorFlow標準的數據輸入格式 input:input_file: input_file=train_file:輸入文件,能夠是訓練集、驗證集和預測集 seq_length=FLAGS.max_seq_length:句子最大長度 is_training=True:是否訓練標誌 drop_remainder=True:表示在少於batch_size元素的狀況下是否應刪除最後一批 ; 默認是不刪除。 output:TensorFlow標準的格式輸入 """ def file_based_input_fn_builder(input_file, seq_length, is_training, drop_remainder): name_to_features = { "input_ids": tf.FixedLenFeature([seq_length], tf.int64), "input_mask": tf.FixedLenFeature([seq_length], tf.int64), "segment_ids": tf.FixedLenFeature([seq_length], tf.int64), "label_ids": tf.FixedLenFeature([], tf.int64), "is_real_example": tf.FixedLenFeature([], tf.int64), } ...... return input_fn
這裏須要注意的是is_training字段,對於訓練數據,須要大量的並行讀寫和打亂順序;而對於驗證數據,咱們不但願打亂數據,是否並行也不關心。
小結下,模型構建模塊主要由模型構建和模型標準輸入兩部分。模型構建負責建立和配置BERT模型。模型標準輸入則讀取TFRecord格式文件並轉化成標準的模型輸入,根據輸入文件的不一樣完成訓練集、驗證集和測試集的標準輸入。
上面模型構建好了以後便可運行模型。Tensorflow中模型運行須要構建一個Estimator對象。主要經過源碼中tf.contrib.tpu.TPUEstimator()來構建。
""" Estimator對象包裝由model_fn指定的模型 input:給定輸入和其餘一些參數 use_tpu:是否使用TPU model_fn:前面構建好的模型 config:模型運行相關的配置 train_batch_size:訓練batch大小 eval_batch_size:驗證batch大小 predict_batch_size:預測batch大小 output:須要進行訓練、計算,或預測的操做 """ estimator = tf.contrib.tpu.TPUEstimator( use_tpu=FLAGS.use_tpu, model_fn=model_fn, config=run_config, train_batch_size=FLAGS.train_batch_size, eval_batch_size=FLAGS.eval_batch_size, predict_batch_size=FLAGS.predict_batch_size)
模型訓練經過estimator.train便可完成:
if FLAGS.do_train: train_input_fn = file_based_input_fn_builder( input_file=train_file, seq_length=FLAGS.max_seq_length, is_training=True, drop_remainder=True) .... estimator.train(input_fn=train_input_fn, max_steps=num_train_steps)
模型驗證經過estimator.evaluate便可完成:
if FLAGS.do_eval: eval_input_fn = file_based_input_fn_builder( input_file=eval_file, seq_length=FLAGS.max_seq_length, is_training=False, drop_remainder=eval_drop_remainder) .... result = estimator.evaluate(input_fn=eval_input_fn, steps=eval_steps, checkpoint_path=filename)
模型預測經過estimator.predict便可完成:
if FLAGS.do_predict: predict_input_fn = file_based_input_fn_builder( input_file=predict_file, seq_length=FLAGS.max_seq_length, is_training=False, drop_remainder=predict_drop_remainder) .... result = estimator.predict(input_fn=predict_input_fn)
import tensorflow as tf # 日誌的顯示等級 tf.logging.set_verbosity(tf.logging.INFO) # 打印提示日誌 tf.logging.info("***** Runningtraining *****") # 打印傳參日誌 tf.logging.info(" Num examples = %d", len(train_examples))
import tensorflow as tf flags = tf.flags FLAGS = flags.FLAGS flags.DEFINE_string( "data_dir", None, "The input data dir. Should contain the .tsv files (or other datafiles) " "for thetask.") # 設置哪些參數是必需要傳入的 flags.mark_flag_as_required("data_dir")
本篇主要講解BERT中分類器部分的源碼。總體來看主要分紅數據處理模塊、特徵處理模塊、模型構建模塊和模型運行模塊。
數據處理模塊主要負責數據讀入和預處理工做;特徵處理模塊負責將預處理後的數據轉化成特徵並持久化到TFRecord格式文件中;模型構建模塊主要負責構建BERT模型和模型標準輸入數據準備;模型運行模塊主要負責模型訓練、驗證和預測。
經過總體到局部的方式咱們能夠對BERT中的分類器源碼有深刻的瞭解。後面能夠根據實際的業務需求對分類器進行二次開發。