一:机器阅读理解简介

1.1 什么是机器阅读理解任务

  • 机器阅读理解(Machine Reading Comprehension,简称MRC)是一项通过让机器回答基于给定上下文的问题来测试机器理解自然语言的程度的任务,简单来说即给定一个或者多个文档P,以及一个问题Q,输出问题Q的答案A。
  • 机器阅读理解任务的形式是较为多样化的,常见的类型包括完形填空式、答案选择式、片段抽取式、自由生成式,本次分享的内容为片段抽取式的机器阅读理解,即问题Q的答案A在文档P中,A是P中的一个连续片段。
    •  完形填空式(Cloze Test)

  • 答案选择式(Multiple Choice)

  • 片段抽取式(Extractive Question Answering):

  • 自由生成式(Generative Question Answering):

总结:

  • 完形填空式:填入空白的词。
  • 答案选择式:从选项中选择正确答案。
  • 片段抽取式:从文本中抽取答案。
  • 自由生成式:生成完整的答案。

1.2 机器阅读理解任务样例

1.3 机器阅读理解数据集格式 

数据集解读: 在机器阅读理解(MRC)任务中,数据集的格式通常包含上下文、问题和答案等信息。以下是对该格式的详细解释:

  1. context:这是提供给模型的上下文信息,通常是一个段落或文本,包含与问题相关的信息。示例:"《战国无双3》是由光荣和ω-force开发的战国无双系列的正统第三续作..."
  2. id:每个样本的唯一标识符,用于区分不同的样本。示例:"id": "DEV_0_QUERY_0"
  3. question:这是模型需要回答的问题,通常与上下文密切相关。示例:"question": "《战国无双3》是由哪两个公司合作开发的?"
  4. answers:这是一个包含答案信息的对象,通常包括以下两个部分:
    answer_start:答案在上下文中的起始位置(索引)。
    text:答案的文本内容。
    示例:这里的answer_start有两个值,是因为出现答案的地方有两个,通常只需要取第一个即可
     "answers": {
             "answer_start": [11, 11],
             "text": ["光荣和ω-force", "光荣和ω-force"]
         }

1.4 评估指标

  1. 精准匹配度(Exact Match,EM):计算预测结果与标准答案是否完全匹配。如果完全匹配,则 EM 为 1,否则为 0。
  2. 模糊匹配度(F1):计算预测结果与标准答案之间字级别的匹配程度。

示例数据:

  • 模型预测结果:北京
  • 真实标准答案:北京天安门

计算结果:

  • EM 计算:
    • 由于模型预测的 北京 与真实答案 北京天安门 不完全匹配,因此 EM = 0。
  • 精确率(P)和召回率(R)计算:
    • 精确率(P):模型预测的正确答案与模型预测的总答案的比率。
      • 预测的正确答案是 北京,总预测答案也是 北京,所以 P = 2/2 = 1。
    • 召回率(R):模型预测的正确答案与真实答案的比率。
      • 真实答案是 北京天安门,其中包含 北京,所以 R = 2/5(北京 在 北京天安门 中的字数为 2,总字数为 5)。
  • F1 计算:

二:基于transformers的解决方案

2.1 数据预处理

  • 数据处理格式:
    • [CLS] 是一个特殊的标记,表示序列的开始,通常用于分类任务。
    •  Question 是用户提出的问题。
    • [SEP] 是分隔符,用于分隔问题和上下文。
    • Context 是提供给模型的上下文信息,包含与问题相关的内容。
  • 如何确定答案位置:
    • start_positions / end_positions:
      • 这两个参数用于指示答案在上下文中的起始和结束位置。模型需要根据这些位置来识别答案。
    • offset_mapping:
      • 这是一个映射,用于指示每个 token 在原始文本中的位置,帮助模型更好地理解答案的具体位置。
  • Context 过长如何解决:

    • 策略 1:直接截断:

      • 这种方法简单易实现,但可能会丢失答案的部分数据,因为无法确定答案的位置。

    • 策略 2:滑动窗口:

      • 这种方法实现较为复杂,但可以减少信息丢失。通过使用滑动窗口技术,可以将长文本分成多个部分进行处理,确保模型能够获取更多上下文信息。

  • 示例:

假设我们有以下问题和上下文:

  • 问题:“北京的首都是哪里?”
  • 上下文:“北京是中国的首都,位于华北地区。”
  1. 数据格式化:

       [CLS] 北京的首都是哪里? [SEP] 北京是中国的首都,位于华北地区。 [SEP]

  2. 确定答案位置: 
  • 假设答案是 “北京”,在上下文中的起始位置为 0,结束位置为 1。
  • start_positions = 0
  • end_positions = 1

      3. 使用滑动窗口:

  • 如果上下文过长,可以将其分成多个部分,例如:
  • 第一部分:“北京是中国的首都,位于华北”
  • 第二部分:“中国的首都,位于华北地区。”

2.2 模型结构

  • ModelForQuestionAnswering


三:代码实战演练1(截断策略版本)

  • 数据集:cmrc2018
  • 预训练模型:hfl/chinese-macbert-base
  • 数据集处理方式:对context进行截断处理

3.1 Step1 导入相关包

from datasets import load_dataset, DatasetDict
from transformers import AutoTokenizer, AutoModelForQuestionAnswering, TrainingArguments, Trainer, DefaultDataCollator

 3.2 Step2 数据集加载

# 如果可以联网,直接使用load_dataset进行加载
#datasets = load_dataset("cmrc2018", cache_dir="data")
# 如果无法联网,则使用下面的方式加载数据集
datasets = DatasetDict.load_from_disk("mrc_data")

3.3 Step3 数据预处理

tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-macbert-base")
def process_func(examples):
    tokenized_examples = tokenizer(text=examples["question"],
                               text_pair=examples["context"],
                               return_offsets_mapping=True,
                               max_length=384, truncation="only_second", padding="max_length")
    offset_mapping = tokenized_examples.pop("offset_mapping")
    start_positions = []
    end_positions = []
    for idx, offset in enumerate(offset_mapping):
        answer = examples["answers"][idx]
        start_char = answer["answer_start"][0]
        end_char = start_char + len(answer["text"][0])
        # 定位答案在token中的起始位置和结束位置
        # 一种策略,我们要拿到context的起始和结束,然后从左右两侧向答案逼近
        context_start = tokenized_examples.sequence_ids(idx).index(1)
        context_end = tokenized_examples.sequence_ids(idx).index(None, context_start) - 1
        # 判断答案是否在context中
        if offset[context_end][1] < start_char or offset[context_start][0] > end_char:
            start_token_pos = 0
            end_token_pos = 0
        else:
            token_id = context_start
            while token_id <= context_end and offset[token_id][0] < start_char:
                token_id += 1
            start_token_pos = token_id
            token_id = context_end
            while token_id >= context_start and offset[token_id][1] > end_char:
                token_id -=1
            end_token_pos = token_id
        start_positions.append(start_token_pos)
        end_positions.append(end_token_pos)
    
    tokenized_examples["start_positions"] = start_positions
    tokenized_examples["end_positions"] = end_positions
    return tokenized_examples
tokenied_datasets = datasets.map(process_func, batched=True, remove_columns=datasets["train"].column_names)

3.4 Step4 加载模型

model = AutoModelForQuestionAnswering.from_pretrained("hfl/chinese-macbert-base")

 3.5 Step5 配置TrainingArguments

args = TrainingArguments(
    output_dir="models_for_qa",
    per_device_train_batch_size=32,
    per_device_eval_batch_size=32,
    eval_strategy="epoch",
    save_strategy="epoch",
    logging_steps=50,
    num_train_epochs=3
)

3.6  Step6 配置Trainer

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

3.7 Step7 模型训练 

trainer.train()

3.8  Step8 模型预测

from transformers import pipeline

pipe = pipeline("question-answering", model=model, tokenizer=tokenizer, device=0)
pipe(question="小明在哪里上班?", context="小明在北京上班。")

四:代码实战演练2

4.1 Step1 导入相关包

from datasets import load_dataset, DatasetDict
from transformers import AutoTokenizer, AutoModelForQuestionAnswering, TrainingArguments, Trainer, DefaultDataCollator

4.2 Step2 数据集加载

# 如果可以联网,直接使用load_dataset进行加载
datasets = load_dataset("cmrc2018", cache_dir="data")
# 如果无法联网,则使用下面的方式加载数据集
#datasets = DatasetDict.load_from_disk("mrc_data")

4.3 Step3 数据预处理

tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-macbert-base")
def process_func(examples):
    tokenized_examples = tokenizer(text=examples["question"],
                               text_pair=examples["context"],
                               return_offsets_mapping=True,
                               return_overflowing_tokens=True,
                               stride=128,
                               max_length=384, truncation="only_second", padding="max_length")
    sample_mapping = tokenized_examples.pop("overflow_to_sample_mapping")
    start_positions = []
    end_positions = []
    example_ids = []
    for idx, _ in enumerate(sample_mapping):
        answer = examples["answers"][sample_mapping[idx]]
        start_char = answer["answer_start"][0]
        end_char = start_char + len(answer["text"][0])
        # 定位答案在token中的起始位置和结束位置
        # 一种策略,我们要拿到context的起始和结束,然后从左右两侧向答案逼近
        context_start = tokenized_examples.sequence_ids(idx).index(1)
        context_end = tokenized_examples.sequence_ids(idx).index(None, context_start) - 1
        offset = tokenized_examples.get("offset_mapping")[idx]
        # 判断答案是否在context中
        if offset[context_end][1] < start_char or offset[context_start][0] > end_char:
            start_token_pos = 0
            end_token_pos = 0
        else:
            token_id = context_start
            while token_id <= context_end and offset[token_id][0] < start_char:
                token_id += 1
            start_token_pos = token_id
            token_id = context_end
            while token_id >= context_start and offset[token_id][1] > end_char:
                token_id -=1
            end_token_pos = token_id
        start_positions.append(start_token_pos)
        end_positions.append(end_token_pos)
        example_ids.append(examples["id"][sample_mapping[idx]])
        tokenized_examples["offset_mapping"][idx] = [
            (o if tokenized_examples.sequence_ids(idx)[k] == 1 else None)
            for k, o in enumerate(tokenized_examples["offset_mapping"][idx])
        ]

    
    tokenized_examples["example_ids"] = example_ids
    tokenized_examples["start_positions"] = start_positions
    tokenized_examples["end_positions"] = end_positions
    return tokenized_examples

4.4 Step4 获取模型输出

import numpy as np
import collections

def get_result(start_logits, end_logits, exmaples, features):

    predictions = {}
    references = {}

    # example 和 feature的映射
    example_to_feature = collections.defaultdict(list)
    for idx, example_id in enumerate(features["example_ids"]):
        example_to_feature[example_id].append(idx)

    # 最优答案候选
    n_best = 20
    # 最大答案长度
    max_answer_length = 30

    for example in exmaples:
        example_id = example["id"]
        context = example["context"]
        answers = []
        for feature_idx in example_to_feature[example_id]:
            start_logit = start_logits[feature_idx]
            end_logit = end_logits[feature_idx]
            offset = features[feature_idx]["offset_mapping"]
            start_indexes = np.argsort(start_logit)[::-1][:n_best].tolist()
            end_indexes = np.argsort(end_logit)[::-1][:n_best].tolist()
            for start_index in start_indexes:
                for end_index in end_indexes:
                    if offset[start_index] is None or offset[end_index] is None:
                        continue
                    if end_index < start_index or end_index - start_index + 1 > max_answer_length:
                        continue
                    answers.append({
                        "text": context[offset[start_index][0]: offset[end_index][1]],
                        "score": start_logit[start_index] + end_logit[end_index]
                    })
        if len(answers) > 0:
            best_answer = max(answers, key=lambda x: x["score"])
            predictions[example_id] = best_answer["text"]
        else:
            predictions[example_id] = ""
        references[example_id] = example["answers"]["text"]

    return predictions, references

4.5 Step5 评估函数

from cmrc_eval import evaluate_cmrc

def metirc(pred):
    start_logits, end_logits = pred[0]
    if start_logits.shape[0] == len(tokenied_datasets["validation"]):
        p, r = get_result(start_logits, end_logits, datasets["validation"], tokenied_datasets["validation"])
    else:
        p, r = get_result(start_logits, end_logits, datasets["test"], tokenied_datasets["test"])
    return evaluate_cmrc(p, r)

4.6 Step6 加载模型

model = AutoModelForQuestionAnswering.from_pretrained("hfl/chinese-macbert-base")

4.7 Step7 配置TrainingArguments

args = TrainingArguments(
    output_dir="models_for_qa",
    per_device_train_batch_size=32,
    per_device_eval_batch_size=32,
    eval_strategy="steps",
    eval_steps=200,
    save_strategy="epoch",
    logging_steps=50,
    num_train_epochs=1
)

4.8 Step8 配置Trainer

trainer = Trainer(
    model=model,
    args=args,
    tokenizer=tokenizer,
    train_dataset=tokenied_datasets["train"],
    eval_dataset=tokenied_datasets["validation"],
    data_collator=DefaultDataCollator(),
    compute_metrics=metirc
)

4.9 Step9 模型训练

trainer.train()

4.10 Step10 模型预测

from transformers import pipeline

pipe = pipeline("question-answering", model=model, tokenizer=tokenizer, device=0)

pipe(question="小明在哪里上班?", context="小明在北京上班")

更多推荐