模型微调入门介绍二

上一篇博客中介绍了如何使用YelpReviewFull进行模型微调,其中,重点介绍了数据加载和数据预处理,此篇博客将介绍如何用squad数据集训练一个问答系统模型。

squad数据集

 SQuAD(The Stanford Question Answering Dataset)是一组阅读数据集,该数据集基于群众在维基百科中提出的问题,其中每个问题的答案来自于对应阅读段落的一段文本,共计500 多篇文章中的10 万多个问答配对。具体的数据信息如下图所示,包括title,context,question,answers字段,answer的答案来源于context,answer字段中给出了答案在context中的start字符位置。

模型微调入门介绍二_第1张图片

squad数据集最早设计出来,用于训练模型的阅读理解能力,文本摘要等能力,因为从context中寻找问题答案,本身就涵盖了对context的阅读理解能力,生成答案体现了对context内容的摘要能力。问答系统实际是一个端到端的业务场景,这个场景里面包含了阅读理解能力、文本摘要和内容生成能力。

数据预处理代码

(备注:以下代码来源于极客时间模型微调训练营)

下面的代码编写了大量脚本来对原始数据进行预处理,我们需要了解的是:这些代码究竟进行了哪些操作,为什么要进行这些操作。

from transformers import AutoTokenizer

squad_v2 = False
model_checkpoint = "distilbert-base-uncased"
batch_size = 16
pad_on_right = tokenizer.padding_side == "right"
# The maximum length of a feature (question and context)
max_length = 384 
# The authorized overlap between two part of the context when splitting it is needed.
doc_stride = 128 


tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
import transformers
assert isinstance(tokenizer, transformers.PreTrainedTokenizerFast)



def prepare_train_features(examples):
    # 一些问题的左侧可能有很多空白字符,这对我们没有用,而且会导致上下文的截断失败
    # (标记化的问题将占用大量空间)。因此,我们删除左侧的空白字符。
    examples["question"] = [q.lstrip() for q in examples["question"]]

    # 使用截断和填充对我们的示例进行标记化,但保留溢出部分,使用步幅(stride)。
    # 当上下文很长时,这会导致一个示例可能提供多个特征,其中每个特征的上下文都与前一个特征的上下文有一些重叠。
    tokenized_examples = tokenizer(
        examples["question" if pad_on_right else "context"],
        examples["context" if pad_on_right else "question"],
        truncation="only_second" if pad_on_right else "only_first",
        max_length=max_length,
        stride=doc_stride,
        return_overflowing_tokens=True,
        return_offsets_mapping=True,
        padding="max_length",
    )

    # 由于一个示例可能给我们提供多个特征(如果它具有很长的上下文),我们需要一个从特征到其对应示例的映射。这个键就提供了这个映射关系。
    sample_mapping = tokenized_examples.pop("overflow_to_sample_mapping")
    # 偏移映射将为我们提供从令牌到原始上下文中的字符位置的映射。这将帮助我们计算开始位置和结束位置。
    offset_mapping = tokenized_examples.pop("offset_mapping")

    # 让我们为这些示例进行标记!
    tokenized_examples["start_positions"] = []
    tokenized_examples["end_positions"] = []

    for i, offsets in enumerate(offset_mapping):
        # 我们将使用CLS令牌的索引来标记不可能的答案。
        input_ids = tokenized_examples["input_ids"][i]
        cls_index = input_ids.index(tokenizer.cls_token_id)

        # 获取与该示例对应的序列(以了解上下文和问题是什么)。
        sequence_ids = tokenized_examples.sequence_ids(i)

        # 一个示例可以提供多个跨度,这是包含此文本跨度的示例的索引。
        sample_index = sample_mapping[i]
        answers = examples["answers"][sample_index]
        # 如果没有给出答案,则将cls_index设置为答案。
        if len(answers["answer_start"]) == 0:
            tokenized_examples["start_positions"].append(cls_index)
            tokenized_examples["end_positions"].append(cls_index)
        else:
            # 答案在文本中的开始和结束字符索引。
            start_char = answers["answer_start"][0]
            end_char = start_char + len(answers["text"][0])

            # 当前跨度在文本中的开始令牌索引。
            token_start_index = 0
            while sequence_ids[token_start_index] != (1 if pad_on_right else 0):
                token_start_index += 1

            # 当前跨度在文本中的结束令牌索引。
            token_end_index = len(input_ids) - 1
            while sequence_ids[token_end_index] != (1 if pad_on_right else 0):
                token_end_index -= 1

            # 检测答案是否超出跨度(在这种情况下,该特征的标签将使用CLS索引)。
            if not (offsets[token_start_index][0] <= start_char and offsets[token_end_index][1] >= end_char):
                tokenized_examples["start_positions"].append(cls_index)
                tokenized_examples["end_positions"].append(cls_index)
            else:
                # 否则,将token_start_index和token_end_index移到答案的两端。
                # 注意:如果答案是最后一个单词(边缘情况),我们可以在最后一个偏移之后继续。
                while token_start_index < len(offsets) and offsets[token_start_index][0] <= start_char:
                    token_start_index += 1
                tokenized_examples["start_positions"].append(token_start_index - 1)
                while offsets[token_end_index][1] >= end_char:
                    token_end_index -= 1
                tokenized_examples["end_positions"].append(token_end_index + 1)

    return tokenized_examples

from datasets import load_dataset

data = load_dataset('squad')['train'][:10]
convert_data=prepare_train_features(data)

  为了理解上面的预处理代码究竟对原始数据进行了哪些操作,可以先看一下处理后的数据是怎样的。可以看到,处理后的数据包括input_ids,start_positions,end_positions和attention_mask字段。

模型微调入门介绍二_第2张图片

与直接调用tokenizer进行encode相比较,实际整个数据预处理就是计算答案的start_positions和end_postions信息,并将这个信息按模型训练需要的格式组装到dataset中。那么,为什么需要start_positions和end_positions呢?因为对于问答系统而言,从context中摘要获取问题的答案,得到了start_positions和end_positions,实际就得到了答案本身。在训练过程中,模型通过比较它的预测值(即 start_positionsend_positions)与真实的答案位置来计算损失。这种监督学习的设置有助于模型学习如何从文本中正确地定位答案。

  对input_ids的内容进行decode,并打印得到下面的数据信息。因为总共只读取了10条数据,可以看到,每一段还是从原始数据中获取[CLS]question[SEP]context[SEP]进行组装,后面[PAD]都是填充占位符号,因为设置了max_length=384,当长度低于384时,就会自动进行填充。

模型微调入门介绍二_第3张图片

对start_positions和end_positions进行decode,得到的信息如下图所示,以第一组为例子,答案就是context中第125-132字符内容。

模型微调入门介绍二_第4张图片

  明白了这么长的数据预处理结果,接下来再看看代码中重点部分代码,一起来捋一下整个代码实现流程。首先来看看sample_mapping和offset_mapping的含义。

下面的代码中,在调用tokenizer进行encode过程中设置了最大长度是100,如果超出这个长度就进行截断。例如,数据集第一条数据的question+context,如果长度大于100个字符,那么就要被截取成多段。[0,0,0,1...]表示处理后的数据的前面三段属于原始数据集中第一段内容,list是从0这个下标开始。

模型微调入门介绍二_第5张图片

对encode的数据进行decode还原,如下图所示,可以看到确实是将第一条数据分成了三段来表示。总结而言overflow_to_sample_mapping 是在处理长文本时的一个辅助数据结构,它用于将分割后的文本块(overflow)映射回原始数据集中的样本

模型微调入门介绍二_第6张图片

offset_mapping 是一个用于指示每个 token 在原始文本中的起始和结束字符位置的数据结构。即通过offset_mapping,你可以在处理分词后的文本时,准确地定位每个 token 在原始文本中的位置。注意,第一个位置因为是分割符号CLS,所以开始和结束位置是(0,0)。下面是一个更加简单的例子来说明offset_mapping的值的含义。对一个句子“Hello,how are you doing today?”进行encode后,对应的offset_mappings值如下所示:

模型微调入门介绍二_第7张图片

  理解了上面两个字段含义后,继续阅读上面的代码。可以看到,组装start_positions和end_postions是读取原始数据集中answer中的start的值,计算len(answer)来得到end_positons的信息。当然,过程中还有很多小细节处理,例如判断组装后是否超过最大长度等。

  实际对于这类典型数据的典型应用场景,transformers库已经封装了方法来进行数据的预处理,对于squad数据,transformers库封装了squad_example_convert_features这个方法来进行数据预处理。下面是对这个方法的简要介绍。

squad_example_convert_features方法

squad_convert_examples_to_features 方法用于将 SQuAD 数据集中的示例(examples)转换为模型的特征(features)。这个函数的主要目的是将原始的 SQuAD 数据转换为适合训练和评估的特征格式,以便用于训练和评估问答模型。该函数的参数如下图所示:

transformers.data.processors.squad.squad_convert_examples_to_features(
    examples: Union[List[InputExample], str],
    tokenizer: PreTrainedTokenizer,
    max_seq_length: Optional[int] = 384,
    doc_stride: Optional[int] = 128,
    max_query_length: Optional[int] = 64,
    is_training: bool = True,
    return_dataset: Optional[str] = None,
    threads: Optional[int] = 1,
    tqdm_enabled: Optional[bool] = True,
    squad_v2: Optional[bool] = False
)

对上面参数的含义解释如下所示:

  • tokenizer: 预训练模型的分词器。
  • max_seq_length: 输出特征的最大序列长度。
  • doc_stride: 用于处理长文本的步幅(stride)。
  • max_query_length: 问题的最大长度。
  • is_training: 是否用于训练。如果设置为 True,将返回 PyTorch 数据集。
  • return_dataset: 如果设置为 "pt",将返回 PyTorch 数据集;如果设置为 "tf", 将返回 TensorFlow 数据集。
  • threads: 处理示例的线程数。
  • tqdm_enabled: 是否启用 tqdm 进度条。
  • squad_v2: 是否是 SQuAD 2.0 数据集。

其中,doc_stride 是在处理长文本时的步幅(stride),它决定了模型在上下文中滑动的步长。在处理长文本时,将整个文本输入模型可能会超过模型的最大输入长度限制。为了应对这种情况,我们可以将文本分成多个片段,每个片段包含一部分上下文,并使用滑动窗口的方式在文本中移动。具体来说,doc_stride 的作用是控制两个相邻片段之间的重叠部分,以确保模型在处理每个片段时可以利用相邻片段的上下文信息。max_seq_length(最大序列长度): 该参数限制了输入序列(包括问题和上下文)的总长度。max_query_length(最大问题长度): 该参数用于限制输入中问题的长度。

如果要调用这个方法对squad数据进行处理以及模型微调,官网是给出了完整的code例子。

  在实际项目中,如果是某个领域的特定数据,可能并没有已经封装好的数据预处理方法可以调用,所以,学习掌握数据预处理过程是非常有价值的。具体数据应该转换成什么格式,还需要与选训练的模型和训练的目标有关。

模型训练代码

数据预处理理解后,后面的代码就相对简单了,基本都是固化格式的代码,具体如下图所示:通过dataset.map()方法对下载的所有原始数据进行数据预处理。在TrainingArugments中设置微调的超参数,调用Trainer对象进行训练。下面的代码中用到了data collator。在Hugging Face Transformers库中,default_data_collator是一个数据整合器(data collator)的默认实现。数据整合器用于将一批(batch)的样本组合成适合输入模型的格式,通常用于训练时的数据加载和批处理过程。

from transformers import AutoModelForQuestionAnswering, TrainingArguments, Trainer
from datasets import load_dataset

model = AutoModelForQuestionAnswering.from_pretrained(model_checkpoint)

batch_size=64
model_dir = "models"
model_name = model_checkpoint.split("/")[-1]

datasets = load_dataset("squad_v2")
tokenized_datasets = datasets.map(prepare_train_features,
                                  batched=True,
                                  remove_columns=datasets["train"].column_names)

args = TrainingArguments(
    f"{model_dir}/{model_name}-finetuned-squad",
    evaluation_strategy = "epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=batch_size,
    per_device_eval_batch_size=batch_size,
    num_train_epochs=3,
    weight_decay=0.01,
)

from transformers import default_data_collator

data_collator = default_data_collator

trainer = Trainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
)

调用trainer.train()方法开始模型微调训练,剩下的就是等待训练完成了。

模型微调入门介绍二_第8张图片

以上就是对如何通过squad数据集训练问答模型的介绍。

你可能感兴趣的:(LLM,深度学习,人工智能)