问题解答(问答任务)¶
问题解答任务是给定一个问题,返回一个答案。如果你曾经问过像 Alexa、Siri 或 Google 这样的虚拟助手天气如何,那么你已经使用过问题解答模型了。问题解答任务主要有两种类型:
- 抽取式:从给定的上下文中提取答案。
- 抽象式:根据上下文生成一个正确回答问题的答案。
本指南将向你展示如何:
- 在 SQuAD 数据集上微调 DistilBERT 模型,用于抽取式问题解答。
- 使用微调后的模型进行推理。
要查看所有与该任务兼容的架构和检查点,我们建议查看 任务页面。
在开始之前,请确保你已经安装了所有必要的库:
pip install transformers datasets evaluate
我们鼓励你登录你的 Hugging Face 账户,这样你就可以上传并与社区分享你的模型。当提示时,输入你的令牌登录:
from huggingface_hub import notebook_login
notebook_login()
加载 SQuAD 数据集¶
首先从 🤗 Datasets 库中加载 SQuAD 数据集的一个较小的子集。这将让你有机会进行实验,并确保一切正常工作,然后再在完整数据集上花费更多时间进行训练。
from datasets import load_dataset
squad = load_dataset("squad", split="train[:5000]")
使用 train_test_split 方法将数据集的 train 分割成一个训练集和测试集:
squad = squad.train_test_split(test_size=0.2)
然后查看一个示例:
squad["train"][0]
这里有几个重要的字段:
answers:答案的起始位置和答案文本。context:模型需要从中提取答案的背景信息。question:模型应该回答的问题。
预处理¶
下一步是加载 DistilBERT 分词器来处理 question 和 context 字段:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("distilbert/distilbert-base-uncased")
问题解答任务有一些特殊的预处理步骤你应该注意:
- 数据集中的某些示例可能有一个非常长的
context,超过了模型的输入长度限制。为了处理更长的序列,只截断context,设置truncation="only_second"。 - 接下来,通过设置
return_offset_mapping=True,将答案的起始和结束位置映射到原始context。 - 有了映射,现在你可以找到答案的起始和结束标记。使用 sequence_ids 方法来找到偏移量中哪部分对应于
question,哪部分对应于context。
以下是如何创建一个函数来截断并将 answer 的起始和结束标记映射到 context:
def preprocess_function(examples):
questions = [q.strip() for q in examples["question"]]
inputs = tokenizer(
questions,
examples["context"],
max_length=384,
truncation="only_second",
return_offsets_mapping=True,
padding="max_length",
)
offset_mapping = inputs.pop("offset_mapping")
answers = examples["answers"]
start_positions = []
end_positions = []
for i, offset in enumerate(offset_mapping):
answer = answers[i]
start_char = answer["answer_start"][0]
end_char = answer["answer_start"][0] + len(answer["text"][0])
sequence_ids = inputs.sequence_ids(i)
# 找到上下文的开始和结束
idx = 0
while sequence_ids[idx] != 1:
idx += 1
context_start = idx
while sequence_ids[idx] == 1:
idx += 1
context_end = idx - 1
# 如果答案不完全在上下文中,标记为 (0, 0)
if offset[context_start][0] > end_char or offset[context_end][1] < start_char:
start_positions.append(0)
end_positions.append(0)
else:
# 否则它是起始和结束标记的位置
idx = context_start
while idx <= context_end and offset[idx][0] <= start_char:
idx += 1
start_positions.append(idx - 1)
idx = context_end
while idx >= context_start and offset[idx][1] >= end_char:
idx -= 1
end_positions.append(idx + 1)
inputs["start_positions"] = start_positions
inputs["end_positions"] = end_positions
return inputs
要在整个数据集上应用预处理函数,使用 🤗 Datasets 的 map 函数。你可以通过设置 batched=True 来加速 map 函数,以便一次处理数据集中的多个元素。删除你不需要的任何列:
tokenized_squad = squad.map(preprocess_function, batched=True, remove_columns=squad["train"].column_names)
现在使用 DefaultDataCollator 创建一个示例批次。与其他 🤗 Transformers 中的数据整理器不同,DefaultDataCollator 不会应用任何额外的预处理,如填充。
Pytorch
from transformers import DefaultDataCollator
data_collator = DefaultDataCollator()
TensorFlow
from transformers import DefaultDataCollator
data_collator = DefaultDataCollator(return_tensors="tf")
训练¶
Pytorch
如果你不熟悉使用 Trainer 微调模型,请查看这里的基本教程 这里!
现在你准备好开始训练你的模型了!使用 AutoModelForQuestionAnswering 加载 DistilBERT:
from transformers import AutoModelForQuestionAnswering, TrainingArguments, Trainer
model = AutoModelForQuestionAnswering.from_pretrained("distilbert/distilbert-base-uncased")
在这一点上,只剩下三个步骤:
- 在 TrainingArguments 中定义你的训练超参数。唯一需要的参数是
output_dir,它指定了保存模型的位置。通过设置push_to_hub=True将模型推送到 Hub(你需要登录 Hugging Face 才能上传你的模型)。 - 将训练参数传递给 Trainer,以及模型、数据集、分词器和数据整理器。
- 调用 train() 来微调你的模型。
training_args = TrainingArguments(
output_dir="my_awesome_qa_model",
eval_strategy="epoch",
learning_rate=2e-5,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
num_train_epochs=3,
weight_decay=0.01,
push_to_hub=True,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_squad["train"],
eval_dataset=tokenized_squad["test"],
processing_class=tokenizer,
data_collator=data_collator,
)
trainer.train()
训练完成后,使用 push_to_hub() 方法将你的模型分享到 Hub,以便每个人都可以使用你的模型:
trainer.push_to_hub()
from transformers import create_optimizer
batch_size = 16
num_epochs = 2
total_train_steps = (len(tokenized_squad["train"]) // batch_size) * num_epochs
optimizer, schedule = create_optimizer(
init_lr=2e-5,
num_warmup_steps=0,
num_train_steps=total_train_steps,
)
然后使用 TFAutoModelForQuestionAnswering 加载 DistilBERT:
from transformers import TFAutoModelForQuestionAnswering
model = TFAutoModelForQuestionAnswering.from_pretrained("distilbert/distilbert-base-uncased")
使用 prepare_tf_dataset() 将你的数据集转换为 tf.data.Dataset 格式:
tf_train_set = model.prepare_tf_dataset(
tokenized_squad["train"],
shuffle=True,
batch_size=16,
collate_fn=data_collator,
)
tf_validation_set = model.prepare_tf_dataset(
tokenized_squad["test"],
shuffle=False,
batch_size=16,
collate_fn=data_collator,
)
使用 compile 配置模型进行训练:
import tensorflow as tf
model.compile(optimizer=optimizer)
在开始训练之前要设置的最后一件事是提供一种将模型推送到 Hub 的方法。这可以通过在 PushToHubCallback 中指定将模型和分词器推送到哪里来实现:
from transformers.keras_callbacks import PushToHubCallback
callback = PushToHubCallback(
output_dir="my_awesome_qa_model",
tokenizer=tokenizer,
)
最后,你准备好开始训练你的模型了!调用 fit,使用你的训练和验证数据集、epoch 数量和你的回调来微调模型:
model.fit(x=tf_train_set, validation_data=tf_validation_set, epochs=3, callbacks=[callback])
训练完成后,你的模型会自动上传到 Hub,以便每个人都可以使用它!
如果你想要更深入地了解如何为问题解答微调模型,请查看相应的 PyTorch 笔记本 或 TensorFlow 笔记本。
评估¶
问题解答的评估需要大量的后处理。为了不占用你太多时间,本指南跳过了评估步骤。Trainer 仍然在训练过程中计算评估损失,所以你不会完全不知道你的模型的性能。
如果你有更多时间,并且对如何评估你的问题解答模型感兴趣,请查看 🤗 Hugging Face 课程中的 问题解答 章节!
推理¶
太好了,现在你已经微调了一个模型,你可以使用它进行推理了!
想出一个问题和一些你想要模型预测的上下文:
question = "How many programming languages does BLOOM support?"
context = "BLOOM has 176 billion parameters and can generate text in 46 natural languages and 13 programming languages."
尝试你的微调模型进行推理的最简单方法是使用它在一个 pipeline() 中。为问题解答实例化一个 pipeline,并将你的文本传递给它:
from transformers import pipeline
question_answerer = pipeline("question-answering", model="my_awesome_qa_model")
question_answerer(question=question, context=context)
如果你愿意,你也可以手动复制 pipeline 的结果:
Pytorch
对文本进行分词并返回 PyTorch 张量:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("my_awesome_qa_model")
inputs = tokenizer(question, context, return_tensors="pt")
将你的输入传递给模型并返回 logits:
import torch
from transformers import AutoModelForQuestionAnswering
model = AutoModelForQuestionAnswering.from_pretrained("my_awesome_qa_model")
with torch.no_grad():
outputs = model(**inputs)
从模型输出中获取起始和结束位置的最高概率:
answer_start_index = outputs.start_logits.argmax()
answer_end_index = outputs.end_logits.argmax()
解码预测的标记以获得答案:
predict_answer_tokens = inputs.input_ids[0, answer_start_index : answer_end_index + 1]
tokenizer.decode(predict_answer_tokens)
TensorFlow
对文本进行分词并返回 TensorFlow 张量:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("my_awesome_qa_model")
inputs = tokenizer(question, text, return_tensors="tf")
将你的输入传递给模型并返回 logits:
from transformers import TFAutoModelForQuestionAnswering
model = TFAutoModelForQuestionAnswering.from_pretrained("my_awesome_qa_model")
outputs = model(**inputs)
从模型输出中获取起始和结束位置的最高概率:
answer_start_index = int(tf.math.argmax(outputs.start_logits, axis=-1)[0])
answer_end_index = int(tf.math.argmax(outputs.end_logits, axis=-1)[0])
解码预测的标记以获得答案:
predict_answer_tokens = inputs.input_ids[0, answer_start_index : answer_end_index + 1]
tokenizer.decode(predict_answer_tokens)