基于transformers的零售 NLP 客服自动回答任务

数据预处理

在投入模型进行训练之前,需要对数据进行适当的预处理。同时,也需要调整或“微调”一个预训练好的模型来适应你的特定任务。由于正在进行文本分类,所以当加载预训练模型时,可能需要忽略或舍弃某些不适用的网络参数。

使用Datasets库来下载数据,并且得到我们需要的评测指标(和benchmark基准进行比较)

使用函数完成

from datasets import load_dataset, load_metric

数据包括context这代表一个段落,而对于这个段落会有几个问题和对应的答案,所以还需要question和text以及answer start,text就是question的答案。这个数据集一个question只有一个答案。result中的字段除了id外其余的就是我们训练需要的字段。

下载SQUAD数据集

datasets = load_dataset("squad_v2" if squad_v2 else "squad")

这个datasets对象是DataaetDict结构,训练、验证、测试分别对应这dict的一个key。

无论是训练集、验证集还是测试集,对于每一个问答数据样本都会有context,question和answers

预处理训练数据

导入库
import numpy as np

from datasets import load_dataset, load_metric

from transformers import AutoTokenizer

from transformers import AutoModelForQuestionAnswering, TrainingArguments, Trainer

from transformers import default_data_collator

import torch

import collections

from tqdm.auto import tqdm

数据预处理及转换 

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

在将数据喂入模型之前,需要对数据进行预处理。预处理的工具叫tokenizertokenizer首先对输入进行tokenize,然后将tokens转化为预模型中需要对应的token ID,再转化为模型需要的输入格式。

以下代码要求tokenizer必须是transformers.PreTrainedTokenizerFast类型

import transformers

assert isinstance(tokenizer, transformers.PreTrainedTokenizerFast)

如果想要看到tokenizer预处理之后的文本格式,仅使用tokenizer的tokenize方法,

add special tokens意思是增加预训练模型所要求的特殊token。

预训练模型输入格式要求的输入为token IDs,还需要attetnion mask。可以使用下面的方法得到预训练模型格式所要求的输入。

tokenizer既可以对单个文本进行预处理,也可以对一对文本进行预处理,tokenizer预处理后得到的数据满足预训练模型输入格式

# 对单个文本进行预处理

# 对2个文本进行预处理,可以看到tokenizer在开始添加了101 token ID,中间用102token ID区分两段文本,末尾用102结尾。这些规则都是预训练模型是所设计的。

长文本

把超长的输入切片为多个较短的输入,每个输入都要满足模型最大长度输入要求。由于答案可能存在与切片的地方,因此我们需要允许相邻切片之间有交集,代码中通过doc_stride参数控制。机器问答预训练模型通常将question和context拼接之后作为输入,然后让模型从context里寻找答案。

for循环遍历数据集,寻找一个超长样本,本notebook例子模型所要求的最大输入是38

max_length = 384  # 输入feature的最大长度,question和context拼接之后
doc_stride = 128  # 2个切片之间的重合token数量。

# 使用range(len())代替直接迭代,避免潜在的不兼容问题
for i in range(len(datasets["train"])):
    example = datasets["train"][i]
    tokenized_example = tokenizer(example["question"], example["context"])
    
    if len(tokenized_example["input_ids"]) > max_length:
        break

只对context进行切片,不会对问题进行切片,由于context是拼接在question后面的,对应着第2个文本,所以使用only_second控制.tokenizer使用doc_stride控制切片之间的重合长度。

# 准备训练数据并转换为feature

tokenized_example = tokenizer(

    example["question"], # 问题文本

    example["context"],  # 篇章文本

    max_length=max_length, 

    truncation="only_second", # 截断只发生在第二部分,即篇章

    return_overflowing_tokens=True,  # 超出最大长度的标记,将篇章切成多片

    stride=doc_stride # 设定篇章切片步长

)

由于我们对超长文本进行了切片,我们需要重新寻找答案所在位置机器问答模型将使用答案的位置作为训练标签。所以切片需要和原始输入有一个对应关系,每个token在切片后context的位置和原始超长context里位置的对应关系。在tokenizer里可以使用return_offsets_mapping参数得到这个对应关系的map:

tokenized_example = tokenizer(

    example["question"],

    example["context"],

    max_length=max_length,

    truncation="only_second",

    return_overflowing_tokens=True,

    return_offsets_mapping=True,

    stride=doc_stride

)

# 打印切片前后位置下标的对应关系

print(tokenized_example["offset_mapping"][0][:100])

然后使用offset_mapping参数映射回切片前的token位置,找到原始位置的tokens。由于question拼接在context前面,直接从question里根据下标找。

first_token_id = tokenized_example["input_ids"][0][1]

offsets = tokenized_example["offset_mapping"][0][1]

print(tokenizer.convert_ids_to_tokens([first_token_id])[0], example["question"][offsets[0]:offsets[1]])

得到了切片前后的位置对应关系。我们还需要使用sequence_ids参数来区分question和context。

None对应了special tokens,然后0或者1分表代表第1个文本和第2个文本,由于我们qeustin第1个传入,context第2个传入,所以分别对应question和context。最终我们可以找到标注的答案在预处理之后的features里的位置:

tokenized_example = tokenizer(example["question"], example["context"], return_attention_mask=True, return_token_type_ids=True)
sequence_ids = tokenized_example['token_type_ids']

answers = example["answers"]
start_char = answers["answer_start"][0]
end_char = start_char + len(answers["text"][0])

# 找到当前文本的Start token index.
token_start_index = 0
while sequence_ids[token_start_index] != 1:
    token_start_index += 1

# 找到当前文本的End token idnex.
token_end_index = len(tokenized_example["input_ids"]) - 1
while sequence_ids[token_end_index] != 1:
    token_end_index -= 1

    end_position = token_end_index + 1
    print("start_position: {}, end_position: {}".format(start_position, end_position))
else:
    print("The answer is not in this feature.")

有时question拼接context,而有时候是context拼接question,不同的模型有不同的要求,因此我们需要使用padding_side参数来指定。

pad_on_right = tokenizer.padding_side == "right" #context在右边

加载预训练模型

已经预处理好了训练/微调需要的数据,现在下载预训练的模型。由于要做的是机器问答任务,于是我们使用这个类AutoModelForQuestionAnswering。和tokenizer相似,model也是使用from_pretrained方法进行加载。

from transformers import AutoModelForQuestionAnswering, TrainingArguments, Trainer

model = AutoModelForQuestionAnswering.from_pretrained(model_checkpoint)

设定训练参数

# 训练设定参数

args = TrainingArguments(
    f"test-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,
)

训练模型

训练的时候,只计算loss。根据评测指标评估模型将会放在下一节。需要把模型,训练参数,数据,之前使用的tokenizer,和数据投递工具default_data_collator传入Tranier。

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

调用train方法开始进行训练

trainer.train()

得到模型预测输出结果

在调用 `trainer.train()` 后,模型开始训练过程。完成训练后,可以使用模型进行预测。

模型预测的主要输出是关于答案开始(start)和结束(end)位置的 logits(即,模型认为每个词可能是答案开始或结束位置的概率得分)。

如果您在评估时向模型输入一个批量(batch)的数据,输出结构大致如下:

在这个例子中,我们首先从评估数据加载器 `trainer.get_eval_dataloader()` 中获取一个批量的数据。然后,我们将这些数据移动到模型所在的设备

使用 `torch.no_grad()` 确保在进行前向传播时不会计算梯度,这样可以节省内存并加速计算。

运行模型后,`output` 将包含多个键值对,其中包括 loss、答案的开始和结束位置的 logits 等。在预测阶段,我们通常只关心开始和结束位置的 logits。

模型为输入中的每个 "feature"(在这里通常是文本中的每个单词或 sub-word)生成一个 logit 值,表示该词作为答案开始或结束位置的可能性。

最后,可以通过在 logits 上应用 `argmax` 函数来找出模型预测的答案的开始和结束位置。这将分别给出开始和结束位置的最大概率得分的索引。

模型本身预测的是answer所在start/end位置的logits。如果评估时喂入模型的是一个batch,输出如下:

import torch

for batch in trainer.get_eval_dataloader():

    break

batch = {k: v.to(trainer.args.device) for k, v in batch.items()}

with torch.no_grad():

    output = trainer.model(**batch)

output.keys()

模型的输出是一个像dict的数据结构,包含了loss,answer start和end的logits。在输出预测结果的时候不需要看loss,直接看logits。

output.start_logits.shape, output.end_logits.shape

(torch.Size([16, 384]), torch.Size([16, 384]))

每个feature里的每个token都会有一个logit

预测answer

选择start的logits里最大的下标最为answer其实位置,end的logits里最大下标作为answer的结束位置。

output.start_logits.argmax(dim=-1), output.end_logits.argmax(dim=-1)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值