清水泥沙

---
title: "Transformer 学习之路 - 机器阅读理解任务实现"
date: 2024-04-19T17:35:18+08:00
draft: false
description: "深入解析如何使用 Transformer 实现机器阅读理解任务,涵盖数据预处理、模型训练与预测等关键步骤。"
categories: ["Python", "Transformer"]
---

# Transformer 学习之路 - 机器阅读理解任务实现

在自然语言处理(NLP)领域,机器阅读理解(Machine Reading Comprehension, MRC)是一项核心任务,旨在让模型从给定的文本中提取出与问题相关的答案。Transformer 模型的出现极大地推动了这一领域的发展。本文将带你一步步实现一个基于 Transformer 的机器阅读理解任务。

## 1. 环境准备

首先,我们需要安装必要的库。这里我们使用 `datasets` 库来加载数据集,`transformers` 库来加载预训练模型和分词器。

```python
!pip install datasets

2. 导入相关包

接下来,我们导入所需的包,包括数据集加载、模型加载、分词器、训练参数配置等。

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

3. 加载数据集

我们使用 cmrc2018 数据集,这是一个中文机器阅读理解数据集。通过 load_dataset 函数加载数据集,并查看数据集的结构。

datasets = load_dataset("cmrc2018", cache_dir="data")
datasets

查看训练集中的第一条数据:

datasets["train"][0]

4. 数据预处理

数据预处理是机器阅读理解任务中非常关键的一步。我们需要将文本数据转换为模型可以接受的输入格式。这里我们使用 AutoTokenizer 加载预训练的分词器,并对数据进行编码。

tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-macbert-base")
tokenizer

为了便于理解,我们从训练集中随机选择 10 条数据进行处理。

sample_dataset = datasets["train"].select(range(10))
sample_dataset[0]

对数据进行编码,返回的 tokenized_examples 是一个字典,包含 input_idsattention_masktoken_type_ids 等。

tokenized_examples = tokenizer(
    text=sample_dataset["context"],
    text_pair=sample_dataset["question"],
    return_offsets_mapping=True,
    truncation=True,
    max_length=512,
    padding="max_length",
    truncation_strategy="only_second",
)
tokenized_examples.keys()

5. 答案位置定位

在机器阅读理解任务中,我们需要找到答案在文本中的起始和结束位置。这里我们通过 offset_mapping 来定位答案在 token 中的位置。

offset_mapping = tokenized_examples.pop("offset_mapping")

for idx, offset in enumerate(offset_mapping):
    answer = sample_dataset[idx]["answers"]
    start_char = answer["answer_start"][0]
    end_char = start_char + len(answer["text"][0])

    # 定位答案在token中的起始位置和结束位置
    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

    print(answer, start_char, end_char, context_start, context_end, start_token_pos, end_token_pos)
    print("token answer decode:", tokenizer.decode(tokenized_examples["input_ids"][idx][start_token_pos: end_token_pos + 1]))

6. 数据处理函数

为了方便后续的训练,我们将上述过程封装成一个数据处理函数 process_func

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])

        context_start = tokenized_examples.sequence_ids(idx).index(1)
        context_end = tokenized_examples.sequence_ids(idx).index(None, context_start) - 1

        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

7. 数据集处理

使用 map 函数对整个数据集进行处理,并移除原始列。

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

8. 加载模型

接下来,我们加载预训练的问答模型 AutoModelForQuestionAnswering

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

9. 配置训练参数

我们使用 TrainingArguments 来配置训练参数,包括输出目录、批量大小、评估策略等。

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

10. 创建 Trainer

使用 Trainer 类来管理训练过程,包括模型、训练参数、数据集等。

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

11. 训练模型

开始训练模型,确保模型参数是连续的。

for param in model.parameters():
    if not param.is_contiguous():
        param.data = param.contiguous()
trainer.train()

12. 模型预测

训练完成后,我们可以使用模型进行预测。这里我们使用 pipeline 来简化预测过程。

from transformers import pipeline

qa = pipeline("question-answering", model=model, tokenizer=tokenizer)
qa(question="信宜在哪里", context="广东茂名信宜的特色是三华李炒猪大肠。")

13. 模型保存

最后,我们将训练好的模型保存到 Google Drive 中,以便后续使用。

from google.colab import drive
drive.mount('/content/drive')

model_save_path = "/content/drive/MyDrive/datasets/mrc"
model.save_pretrained(model_save_path)

总结

通过本文的步骤,我们实现了一个基于 Transformer 的机器阅读理解任务。从数据预处理到模型训练与预测,每一步都至关重要。希望这篇文章能帮助你更好地理解 Transformer 在机器阅读理解任务中的应用。