Al-bert利用自己訓練數據集預訓練以及測試LCQMC語義相似度測試(一)

`Al-bert利用自己訓練數據集預訓練以及測試LCQMC語義相似度測試## 標題(一)

數據預處理解析

本次主要是針對al-bert與訓練過程進行解析,希望對大家有所幫助,預訓練過程主要分1、數據構造過程;2、與訓練過程;首先簡述數據構造過程:(1)首先針對文檔的每一行數據進行中文全詞處理,針對同一文檔的句子進行拼接以概率90%爲最大長度target_seq_length左右;(2)然後形成token_a和tokens_b進行拼接形成tokens,再次過程形成50%概率token_a和tokens_b順序顛倒作爲bert訓練第二個任務的數據;(3)然後按照論文中的替代詞語mask的方法構造訓練數據集,體現預測全詞的代碼是只要是連續幾個位置是一個jieba詞語,這幾個位置就作爲是否作爲預測同一結果,具體過程如下解析

1.1 create_pretraining_data.py 解析

    首先,albert還是利用了bert的訓練方法,所以數據處理非常相似,除了利用全詞模式的更改;本文章只針對代碼進行解析;代碼在github搜索就能得到
    作者提供了一個樣例數據,這樣的話,其實是非常讓用戶方便的進行自己數據進行預訓練自己數據的。本次測試是利用了代碼中的有關螞蟻金服的數據測試了albert利用無監督預訓練的過程。本次利用debug模式的順序講解
  提供測試數據news_zh_1.txt結構如下:
由於相關子課題研究還沒有結束,課題組非行政機構只承擔建議義務。
.........
張健坦言,能否在高考語文中出現一個新的形式——政論或申論形式的傳統文化論述題,這一方向應該是研究和創新的改革方向之一。

懸灸技術培訓專家教你艾灸降血糖,爲爸媽收好了!
.........
近年來隨着我國經濟條件的改善和人們生活水平的提高,我國糖尿病的患病率也在逐年上升。

可以看出數據每一行代表一條訓練數據,空格行代表分割一段內容。具體的數據處理操作就知道數據爲什麼這樣操作;

flags.DEFINE_string("input_file", "./data/news_zh_1.txt","訓練數據輸入路徑.")
flags.DEFINE_string(
    "output_file", "./data/tf_news_2016_zh_raw_news2016zh_1.tfrecord",
    "數據輸出路徑,由於訓練可能數據較大,所以採用tfrecords格式")
flags.DEFINE_string("vocab_file", "./albert_config/vocab.txt", "對應的模型詞典")
flags.DEFINE_bool("do_lower_case", True"對應英文是否要去除大寫")
flags.DEFINE_bool( "do_whole_word_mask", True,"是否使用全詞mask的方法,這個是中文的關鍵點")
flags.DEFINE_integer("max_seq_length", 512, "最大句子長度")
flags.DEFINE_integer("max_predictions_per_seq", 51, "最大預測詞語的長度")
flags.DEFINE_integer("random_seed", 12345, "針對隨機mask的概率採用隨機種子")
flags.DEFINE_integer("dupe_factor", 10, "訓練數據集複製的次數,意思是數據集在構造mask數據集時,每一mask後,再次mask一次 (由於每一次數據mask是隨機的,所以dupe_factor每一次得到的mask結果不同).")
flags.DEFINE_float("masked_lm_prob", 0.10, "Masked LM probability.")
flags.DEFINE_float("short_seq_prob", 0.1,"Probability of creating sequences which are shorter than the ""maximum length.")
flags.DEFINE_bool("non_chinese", False, "是針對英語還是中文,默認false是中文")

以上是對參數進行基本解釋;接下來運行在def main(_):函數:

def main(_):
    tf.logging.set_verbosity(tf.logging.INFO)
    tokenizer = tokenization.FullTokenizer(
        vocab_file=FLAGS.vocab_file, do_lower_case=FLAGS.do_lower_case)

這裏的分詞模塊仍然是英文的分詞,仍然是字符級別的分割;main的代碼整個核心函數是create_training_instances()函數。

    instances = create_training_instances(
        input_files, tokenizer, FLAGS.max_seq_length, FLAGS.dupe_factor,
        FLAGS.short_seq_prob, FLAGS.masked_lm_prob, FLAGS.max_predictions_per_seq,
        rng)

其中參數解釋:
input_files:原始訓練數據;
tokenizer:分詞工具;其他的已經在上面進行解釋

def create_training_instances(input_files, tokenizer, max_seq_length,
                              dupe_factor, short_seq_prob, masked_lm_prob,
                              max_predictions_per_seq, rng):
   all_documents = [[]] #所有處理後的數據集

以下代碼是對文件中的每一行數據放入到all_documents中,做了一些空格以及格式轉換處理,把每一行數據形成字符列表例如[“今”,“天”,“天”,“氣”,、、、],並且對數據進行打亂順序:

  for input_file in input_files:
        with open(input_file, "r",encoding='utf-8') as reader:
            lines=reader.readlines()
            for strings in tqdm(lines):
                strings = strings.strip().replace("   ", " ").replace("  ", " ")  # 如果有兩個或三個空格,替換爲一個空格
                line = tokenization.convert_to_unicode(strings)
                if not line:
                    continue
                line = line.strip()
                # Empty lines are used as document delimiters
                if not line:
                    all_documents.append([])
                tokens = tokenizer.tokenize(line)
                if tokens:
                    all_documents[-1].append(tokens)
    # Remove empty documents
  all_documents = [x for x in all_documents if x]
  rng.shuffle(all_documents)#針對數據進行打亂

獲取需要訓練的原始字符列表

 vocab_words = list(tokenizer.vocab.keys())

以下操作是針對每一行數據進行mask具體操作。

  for _ in range(dupe_factor):
        for document_index in range(len(all_documents)):
            instances.extend(
                create_instances_from_document_albert(
                    # change to albert style for sentence order prediction(SOP), 2019-08-28, brightmart
                    all_documents, document_index, max_seq_length, short_seq_prob,
                    masked_lm_prob, max_predictions_per_seq, vocab_words, rng))

第一個 for循環就是針對所有數據重複dupe_factor次,每一次同一條數據mask的位置不同;第二個for循環是多個文件的情況的操作,本例只有一個數據;所以只進行了一次循環。那麼重點就是create_instances_from_document_albert()函數他就是mask的具體操作。具體代碼如下:

 def create_instances_from_document_albert(
    all_documents, document_index, max_seq_length, short_seq_prob,
    masked_lm_prob, max_predictions_per_seq, vocab_words, rng):
"""Creates `TrainingInstance`s for a single document.
   This method is changed to create sentence-order prediction (SOP) followed by idea from paper of ALBERT, 2019-08-28, brightmart
"""
	document = all_documents[document_index]  # 得到一個文檔
    max_num_tokens = max_seq_length - 3 #這個-3就是爲了增加 [CLS], [SEP], [SEP]這三個字符,[CLS]是每一個樣本的類別字符,[SEP]如果輸入是句子對的話就利用這個進行分割

有一定的比例,如10%的概率,使用比較短的序列長度,以緩解預訓練的長序列和調優階段(可能的)短序列的不一致情況,所以增加了這樣的概率,代碼如下:
其實就是爲了使數據具有一定分佈包含所有句子長度的可能,而不是沒一個訓練數據的長度都是max_num_tokens的長度,這裏長度爲509。

    #以下代碼就是利用了利用0.15的概率隨機取出2到max_num_tokens的長度。
  	if rng.random() < short_seq_prob: 
    	target_seq_length = rng.randint(2, max_num_tokens)

設法使用實際的句子,而不是任意的截斷句子,從而更好的構造句子連貫性預測的任務

   instances = []   #處理好的句子訓練數據
   current_chunk = []  # 當前處理的文本段,包含多個句子
   current_length = 0  
   for i in tqdm(range(len(document))):  # 從文檔的第一個位置開始,按個往下看
       	segment = document[i]  # 對於每一個句子,segment是列表,代表的是按字分開的一個完整句子,如 segment=['我', '是', '一', '爺', '們', ',', 
       #'我', '想', '我', '會', '給', '我', '媳', '婦', '最', '好', '的',
       # '幸', '福', '。']

接下來就是中文需要特殊處理的部分代碼,如果是中文就需要進行全部mask,具體使用函數如下,使用 get_new_segment(segment)進行全詞mask:

		if FLAGS.non_chinese == False:  # if non chinese is False, that means it is chinese, then do something to make chinese whole word mask works.
            segment = get_new_segment(segment)  # whole word mask for chinese: 結合分詞的中文的whole mask設置即在需要的地方加上“##”

get_new_segment()的具體實現如下,需要對以下函數進行詳細說明,使用了jieba分詞進行語義分割;分割之後得到seq_cws_dict字典,字典格式
seq_cws_dict= {‘城市化’: 1, ‘最後’: 1, ‘老城’: 1, ‘何處’: 1, ‘時代’: 1, ‘文化’: 1, ‘該往’: 1, ‘自覺’: 1, ‘南京’: 1, ‘的’: 1, ‘呼喚’: 1, ‘去’: 1}

 def get_new_segment(segment):  # 新增的方法 ####
    """
    輸入一句話,返回一句經過處理的話: 爲了支持中文全稱mask,將被分開的詞,將上特
    殊標記("#"),使得後續處理模塊,能夠知道哪些字是屬於同一個詞。
    :param segment: 一句話. e.g.  ['懸', '灸', '技', '術', '培', '訓', '專', '家', '教', '你', '艾', '灸', '降', '血', '糖', ',',
    '爲', '爸', '媽', '收', '好', '了', '!']
    :return: 一句處理過的話 e.g.    ['懸', '##灸', '技', '術', '培', '訓', '專', '##家', '教', '你', '艾', '##灸',
                                 '降', '##血', '##糖', ',', '爲', '爸', '##媽', '收', '##好', '了', '!']
    """
    seq_cws = jieba.lcut("".join(segment))  # 分詞
    seq_cws_dict = {x: 1 for x in seq_cws}  # 分詞後的詞加入到詞典dict
    new_segment = [] #處理中文分詞後的結果。

剩餘的代碼就是針對判斷jieba分詞的詞語位置,形成如果是一個詞語,就進行模型能夠訓練的全詞mask形式,例如: ‘降’, ‘##血’, ‘##糖’, 但是貌似該算法,仍然是每一個字符進行預測的,並且處理不能超過長度爲3的詞語否則做爲單個字符處理,訓練代碼會進行解析。接下來就是while循環,首先判斷是否是中文字符,如果不是中文字符不做任何處理,直接添加,

	 while i < len(segment):  # 從句子的第一個字開始處理,知道處理完整個句子
	        if len(re.findall('[\u4E00-\u9FA5]', segment[i])) == 0:  # 如果找不到中文的,原文加進去即不用特殊處理。
	            new_segment.append(segment[i])
	            i += 1
	            continue

具體添加i從句子開始0位置開始,最大詞語長度限定3,首先判斷詞語位置不能超過segment長度,然後判斷i:i+length 是否在seq_cws_dict字典中,如果在第一個詞語字符添加到new_segment中,剩餘字符添加增加##這樣的格式,用來區分詞語開始和中間結果。然後i變成 i += length從這個位置在開始添加,has_add標誌爲是否還有詞語添加,然後再次while循環,進行同樣的的判斷,依次重複,並且把has_add重新設置爲False,具體結合如下代碼:

			 has_add = False
			        for length in range(3, 0, -1):
			            if i + length > len(segment):
			                continue
			            if ''.join(segment[i:i + length]) in seq_cws_dict:
			                new_segment.append(segment[i])
			                for l in range(1, length):
			                    new_segment.append('##' + segment[i + l])
			                i += length
			                has_add = True
			                break

這樣 segment = get_new_segment(segment) # whole word mask for chinese: 結合分詞的中文的whole mask設置即在需要的地方加上“##”就基本完成了,也算是數據處理的核心代碼。接下來兩行代碼就是把處理好的句子添加到current_chunk列表中,並且 current_length += len(segment) # 累計到爲止位置接觸到句子的總長度,這樣,坐着的意思就是,沒一個文檔是保持同一個話題的,否則會有部分current_chunk是損壞的數據。然後是判斷是否達到預設最大句子長度:

   if i == len(document) - 1 or current_length >= target_seq_length:
            # 如果累計的序列長度達到了目標的長度,或當前走到了文檔結尾==>構造並添加到“A[SEP]B“中的A和B中;這裏解釋比較清楚了,

如果沒有達到一個訓練語句的長度,則i+=1,取出下一個句子進行

 segment = get_new_segment(segment)  # whole word mask for chinese: 結合分詞的中文的whole mask設置即在需要的地方加上“##”

        current_chunk.append(segment)  # 將一個獨立的句子加入到當前的文本塊中
        current_length += len(segment)  # 累計到爲止位置接觸到句子的總長度

這樣的操作直到達到進行A[SEP]B的操作代碼,達到後,首先隨機選出A句子的結束位置a_end位置,然後把A的代碼extend到token_a中,然後把剩餘的句子列表extend到token_b中。然後再利用50%的概率,tokens_a和tokens_b進行交換位置主要原因在於構造第二個任務is_random_next就是實際標示,如果爲true就是tokens_a和tokens_b進行交換爲負樣本。具體代碼如下:

 			    a_end = 1
                if len(current_chunk) >= 2:  # 當前塊,如果包含超過兩個句子,取當前塊的一部分作爲“A[SEP]B“中的A部分
                    a_end = rng.randint(1, len(current_chunk) - 1)
                # 將當前文本段中選取出來的前半部分,賦值給A即tokens_a
                tokens_a = []
                for j in range(a_end):
                    tokens_a.extend(current_chunk[j])

                # 構造“A[SEP]B“中的B部分(有一部分是正常的當前文檔中的後半部;在原BERT的實現中一部分是隨機的從另一個文檔中選取的,)
                tokens_b = []
                for j in range(a_end, len(current_chunk)):
                    tokens_b.extend(current_chunk[j])

                # 有百分之50%的概率交換一下tokens_a和tokens_b的位置
                # print("tokens_a length1:",len(tokens_a))
                # print("tokens_b length1:",len(tokens_b)) # len(tokens_b) = 0

                if len(tokens_a) == 0 or len(tokens_b) == 0: i += 1; continue
                if rng.random() < 0.5:  # 交換一下tokens_a和tokens_b
                    is_random_next = True
                    temp = tokens_a
                    tokens_a = tokens_b
                    tokens_b = temp
                else:
                    is_random_next = False

接下來對本次訓練數據進行截斷,保證最大長度在 max_num_tokens 範圍內, truncate_seq_pair(tokens_a, tokens_b, max_num_tokens, rng),由於 current_chunk.append(segment) # 將一個獨立的句子加入到當前的文本塊中
是增加一個句子,並且判斷是 if i == len(document) - 1 or current_length >= target_seq_length: 大於等於,所以大部分的len(tokens_a)+len(tokens_b)會大於最大長度。所以要進行截斷。
把tokens_a & tokens_b加入到按照bert的風格,即以[CLS]tokens_a[SEP]tokens_b[SEP]的形式,結合到一起,作爲最終的tokens;
也帶上segment_ids,前面部分segment_ids的值是0,後面部分的值是1.代碼如下:

 				tokens = []
                segment_ids = []
                tokens.append("[CLS]")
                segment_ids.append(0)
                for token in tokens_a:
                    tokens.append(token)
                    segment_ids.append(0)

                tokens.append("[SEP]")
                segment_ids.append(0)

                for token in tokens_b:
                    tokens.append(token)
                    segment_ids.append(1)
                tokens.append("[SEP]")
                segment_ids.append(1)

接下來就是 創建masked LM的任務的數據 Creates the predictions for the masked LM objective,這個是創建數據的另一個重點,
(tokens, masked_lm_positions,
masked_lm_labels) = create_masked_lm_predictions(
tokens, masked_lm_prob, max_predictions_per_seq, vocab_words, rng),具體代碼如下:

   def create_masked_lm_predictions(tokens, masked_lm_prob,
                             max_predictions_per_seq, vocab_words, rng):
		"""Creates the predictions for the masked LM objective."""

我們先看一下坐着的解釋,其實也很清楚了
# Whole Word Masking means that if we mask all of the wordpieces
# corresponding to an original word. When a word has been split into
# WordPieces, the first token does not have any marker and any subsequence
# tokens are prefixed with ##. So whenever we see the ## token, we
# append it to the previous set of word indexes.
#
# Note that Whole Word Masking does not change the training code
# at all – we still predict each WordPiece independently, softmaxed
# over the entire vocabulary.
全詞mask意味着如果詞片斷(也就是字符)對應一個原始ci(在這裏表示是jieba分詞結果),當一個詞語被分割詞語,第一個字符不加標記,後邊的子序列加前綴##在前面已經講過了。所以當遇到##,就把當前字符的下標添加到前一個字符的列表裏,這裏的index並不是真個詞彙列表的index而是在當前句子中的位置。後一句話是全詞mask並沒有改變訓練代碼,仍然是每一個字符預測,這說的是整個詞彙列表的softmax預測整個vocabulary。

    cand_indexes = []  #用來記錄每一個詞語的分割index,上面已經解釋清楚,
    #如果是新詞就添加本身在句子中的下標,如果是詞語的子序列,則把當前字符的下標
    #添加到前一個字符的列表裏
	for (i, token) in enumerate(tokens):
	    if token == "[CLS]" or token == "[SEP]":
	        continue
	    if (FLAGS.do_whole_word_mask and len(cand_indexes) >= 1 and
                token.startswith("##")):
            cand_indexes[-1].append(i)
        else:
            cand_indexes.append([i])
rng.shuffle(cand_indexes)然後把真個訓練數據集打亂,數據打亂的原因就是爲了下邊for循環依次把他們作爲預測labels的原因。但句子index其實還是代表了詞語在句子中的順序。以下代碼確定預測的字符個數。
	 num_to_predict = min(max_predictions_per_seq,
	                     max(1, int(round(len(tokens) * masked_lm_prob))))
#output_tokes是爲了獲得mask之後結果,也就是整個模型的輸入,下面的代碼是去掉##
 if FLAGS.non_chinese == False:  # if non chinese is False, that means it is chinese, then try to remove "##" which is added previously
        output_tokens = [t[2:] if len(re.findall('##[\u4E00-\u9FA5]', t)) > 0 else t for t in tokens]  # 去掉"##"

之後的代碼核心就是,80%的概率採用“mASK”字符替代原始字符,10%概率替代爲與原始字符相同,10%概率在vocab_words(21128個數)隨機尋找一個字符替代。那麼構造訓練數據主要包含了構造結果的兩個內容:
masked_lms = [] #形成命名元祖,內容包含對應句子index以及實際字符label
covered_indexes = set() #句子哪些index形成label
具體代碼如下:

	masked_lms = []
    covered_indexes = set() #句子中哪些字符需要在訓練中預測,對應的下標index
    for index_set in cand_indexes:#在打亂的下標中構造num_to_predict個label.
        if len(masked_lms) >= num_to_predict:/#如果大於限定預測個數則終止
            break
        # If adding a whole-word mask would exceed the maximum number of
        # predictions, then just skip this candidate.
        if len(masked_lms) + len(index_set) > num_to_predict:#與上面判斷一樣的作用
            continue
        is_any_index_covered = False
        for index in index_set:
            if index in covered_indexes:
                is_any_index_covered = True
                break
        if is_any_index_covered:
            continue
        for index in index_set:
            covered_indexes.add(index)  #構造訓練label的index添加到covered_indexes列表中
            masked_token = None
            # 80% of the time, replace with [MASK]
            if rng.random() < 0.8: #以80%的概率用mask替代
                masked_token = "[MASK]"
            else:
                # 10% of the time, keep original
                if rng.random() < 0.5: #10%的概率還是原來的字符
                    if FLAGS.non_chinese == False:  # if non chinese is False, that means it is chinese, then try to remove "##" which is added previously
                        masked_token = tokens[index][2:] if len(re.findall('##[\u4E00-\u9FA5]', tokens[index])) > 0 else \
                            tokens[index]  # 去掉"##"
                    else:
                        masked_token = tokens[index]
                # 10% of the time, replace with random word
                else:
                    masked_token = vocab_words[rng.randint(0, len(vocab_words) - 1)]

            output_tokens[index] = masked_token #用mask替代原來的字符

            masked_lms.append(MaskedLmInstance(index=index, label=tokens[index])) #增加label到命名三元組中。

剩餘的代碼就比較簡單了,就是利用最後得到的label分別保存到 masked_lm_positions = [] #這個是保存label在句子中的位置信息
masked_lm_labels = [] #實際label的字符信息
具體代碼如下:

    masked_lm_positions = []
	masked_lm_labels = []
	for p in masked_lms:
	    masked_lm_positions.append(p.index)
	    masked_lm_labels.append(p.label)
return (output_tokens, masked_lm_positions, masked_lm_labels)

最後返回了:
output_tokens, 訓練數據集的輸入,用mask替代的句子
masked_lm_positions, 位置信息
masked_lm_labels , 實際mask的字符信息;
就知道以上就是構造bert訓練方式之一Masked LM的過程,也就是上面這個代碼的作用:

 # 創建masked LM的任務的數據 Creates the predictions for the masked LM objective
                (tokens, masked_lm_positions,
                 masked_lm_labels) = create_masked_lm_predictions(
                    tokens, masked_lm_prob, max_predictions_per_seq, vocab_words, rng)

然後在利用tokens(用mask替代之後的訓練模型輸入)、segment_ids(區分A和B句子的0,1信息)、masked_lm_positions(訓練數據label在句子中的位置信息)、masked_lm_labels(訓練數據集label字符信息)構造TrainingInstance實例,代碼如下:

   instance = TrainingInstance(  # 創建訓練實例的對象
                    tokens=tokens,
                    segment_ids=segment_ids,
                    is_random_next=is_random_next,
                    masked_lm_positions=masked_lm_positions,
                    masked_lm_labels=masked_lm_labels)

然後instances.append(instance)形成最後的訓練數據,函數跳出返回上一級:

#打亂訓練數據並且返回
 rng.shuffle(instances)
 return instances

接下來的工作1、構造Next sentence預測數據;2、把構造的訓練數據寫入tfrecord中。工作在:

def write_instance_to_example_files(instances, tokenizer, max_seq_length,
                                    max_predictions_per_seq, output_files):
                                     writers = []
    for output_file in output_files:
        writers.append(tf.python_io.TFRecordWriter(output_file))

    writer_index = 0

    total_written = 0

以上代碼就是打開一個tf文件,由於並非所有的數據集都是長度max_seq_length=512,所以要對不夠長度的數據集進行0擴充,並且原始字符利用vocablary中字典對應的下標替換 tokenizer.convert_tokens_to_ids(instance.tokens)成id,並且用input_mask記錄句子的實際長度:

	 for (inst_index, instance) in enumerate(instances):
	        input_ids = tokenizer.convert_tokens_to_ids(instance.tokens)
	        input_mask = [1] * len(input_ids) #作用就是記錄訓練數據的實際長度,
	        segment_ids = list(instance.segment_ids)
	        assert len(input_ids) <= max_seq_length
	
	        while len(input_ids) < max_seq_length:#如果句子小於512,則用0擴充
	            input_ids.append(0)
	            input_mask.append(0)
	            segment_ids.append(0)

然後在對label進行處理,獲取label在句子中的index,並且把label也轉化爲vocablary詞典對應的詞id,masked_lm_weights記錄實際需要預測的label長度,用1.0記錄。同時不夠max_predictions_per_seq長度的進行用0擴充。

		masked_lm_positions = list(instance.masked_lm_positions)
        masked_lm_ids = tokenizer.convert_tokens_to_ids(instance.masked_lm_labels)
        masked_lm_weights = [1.0] * len(masked_lm_ids)

        while len(masked_lm_positions) < max_predictions_per_seq:
            masked_lm_positions.append(0)
            masked_lm_ids.append(0)
            masked_lm_weights.append(0.0)

next_sentence_label = 1 if instance.is_random_next else 0 這句話就是判斷B是否是A句子的下一個句子的label ,如果是random則不是下一個句子,在這裏用1表示,有點費解,但是並不影響預測結果,至此基本構造數據講解完了。下一張講解訓練過程https://editor.csdn.net/md?articleId=103567243

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章