Al-bert利用自己训练数据集预训练以及测试LCQMC语义相似度测试(一)

本文深入解析ALBERT模型的预训练过程,涵盖数据构造、全词mask策略及训练实例生成,旨在帮助理解其独特之处。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

`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.youkuaiyun.com/md?articleId=103567243

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值