datawhale 8月学习——NLP之Transformers:Transformers解决序列标注任务

前情回顾

  1. attention和transformers
  2. BERT和GPT
  3. 编写BERT模型
  4. BERT的应用、训练和优化
  5. Transformers解决文本分类任务、超参搜索

结论速递

本任务是序列标注任务,即token级的分类任务,这个任务的实现过程可以加深对token的理解。
流程上和上一个任务十分相似。目前尚未解决的问题时微调过程中训练的参数在代码中是如何实现的。

1 序列标注

1.1 问题简介

序列标注,通常也可以看作是token级别的分类问题:对每一个token进行分类。token级别的分类任务通常指的是为为文本中的每一个token预测一个标签结果。

最常见的token级别分类任务有(对应后面数据集的标签):

  • NER (Named-entity recognition 名词-实体识别) 分辨出文本中的名词和实体 (person人名, organization组织机构名, location地点名…).
  • POS (Part-of-speech tagging词性标注) 根据语法对token进行词性标注 (noun名词, verb动词, adjective形容词…)
  • Chunk (Chunking短语组块) 将同一个短语的tokens组块放在一起。

1.2 参数设定及数据解读

我们需要确定任务种类

task = "ner" #需要是"ner", "pos" 或者 "chunk"
model_checkpoint = "distilbert-base-uncased"
batch_size = 16

接下来加载数据,数据加载和评测方式加载只需要简单使用load_datasetload_metric即可。

from datasets import load_dataset, load_metric

这里使用的是CONLL 2003 dataset数据集。

datasets = load_dataset("conll2003")

和上一个任务一样,这个datasets对象本身是一种DatasetDict数据结构. 对于训练集、验证集和测试集,只需要使用对应的key(train,validation,test)即可得到相应的数据。

所有的数据标签labels都已经被编码成了整数,可以直接被预训练transformer模型使用。这些整数的编码所对应的实际类别储存在features中。

[IN]: datasets["train"].features[f"ner_tags"]
[OUT]: Sequence(feature=ClassLabel(num_classes=9, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'], names_file=None, id=None), length=-1, id=None)

以NER为例,0对应的标签类别是”O“, 1对应的是”B-PER“等等。”O“的意思是没有特别实体(no special entity)。本例包含4种实体类别分别是(PER、ORG、LOC,MISC),每一种实体类别又分别有B-(实体开始的token)前缀和I-(实体中间的token)前缀。

  • ‘PER’ for person
  • ‘ORG’ for organization
  • ‘LOC’ for location
  • ‘MISC’ for miscellaneous

可以看到所有对应的标签

[IN]:label_list = datasets["train"].features[f"{task}_tags"].feature.names
[IN]:label_list
[OUT]:['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']

下面这个函数可以实现从数据集里随机选择几个例子进行展示。是通过建立数据帧的形式。

from datasets import ClassLabel, Sequence
import random
import pandas as pd
from IPython.display import display, HTML

def show_random_elements(dataset, num_examples=10):
    assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset."
    picks = []
    for _ in range(num_examples):
        pick = random.randint(0, len(dataset)-1)
        while pick in picks:
            pick = random.randint(0, len(dataset)-1)
        picks.append(pick)
    
    df = pd.DataFrame(dataset[picks])
    for column, typ in dataset.features.items():
        if isinstance(typ, ClassLabel):
            df[column] = df[column].transform(lambda i: typ.names[i])
        elif isinstance(typ, Sequence) and isinstance(typ.feature, ClassLabel):
            df[column] = df[column].transform(lambda x: [typ.feature.names[i] for i in x])
    display(HTML(df.to_html()))

训练集长这样。
在这里插入图片描述

2 序列标注任务的实现

2.1 数据预处理

在将数据喂入模型之前,我们需要用Tokenizer对数据进行预处理。

为了达到数据预处理的目的,我们使用AutoTokenizer.from_pretrained方法实例化我们的tokenizer,这样可以确保:

  • 我们得到一个与预训练模型一一对应的tokenizer。
  • 使用指定的模型checkpoint对应的tokenizer的时候,我们也下载了模型需要的词表库vocabulary,准确来说是tokens vocabulary。

这个被下载的tokens vocabulary会被缓存起来,从而再次使用的时候不会重新下载。

from transformers import AutoTokenizer
    
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

后面的代码要求tokenizer是transformers.PreTrainedTokenizerFast类型,几乎所有模型对应的tokenizer都有对应的fast tokenizer。我们可以在模型tokenizer对应表里查看所有预训练模型对应的tokenizer所拥有的特点。

import transformers
assert isinstance(tokenizer, transformers.PreTrainedTokenizerFast)

这里big table of models查看模型是否有fast tokenizer。

tokenizeris_split_into_words参数设置为True时,可以分散的输入词汇,不受语句数量的限制。

注意transformer预训练模型在预训练的时候通常使用的是subword,如果我们的文本输入已经被切分成了word,那么这些word还会被我们的tokenizer继续切分。

由于标注数据通常是在word级别进行标注的,既然word还会被切分成subtokens,那么意味着我们还需要对标注数据进行subtokens的对齐。同时,由于预训练模型输入格式的要求,往往还需要加上一些特殊符号比如: [CLS] 和 a [SEP]

tokenizer有一个word_ids方法可以帮助我们解决这个问题

[IN]:print(tokenized_input.word_ids())
[OUT]:[None, 0, 1, 1, 2, 3, 4, 5, 6, 7, 7, 8, 9, 10, 11, 11, 11, 12, 13, 14, 15, 16, 17, 18, 18, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, None]

我们可以看到,word_ids将每一个subtokens位置都对应了一个word的下标。比如第1个位置对应第0个word,然后第2、3个位置对应第1个word。特殊字符对应了NOne。有了这个list,我们就能将subtokens和words还有标注的labels对齐啦。

word_ids = tokenized_input.word_ids()
aligned_labels = [-100 if i is None else example[f"{task}_tags"][i] for i in word_ids]

我们通常将特殊字符的label设置为-100,在模型中-100通常会被忽略掉不计算loss。

我们有两种对齐label的方式:

  • 多个subtokens对齐一个word,对齐一个label
  • 多个subtokens的第一个subtoken对齐word,对齐一个label,其他subtokens直接赋予-100.

我们提供这两种方式,通过label_all_tokens = True切换。

label_all_tokens = True

可以将所有内容合起来变成我们的预处理函数。is_split_into_words=True在上面已经结束啦。

def tokenize_and_align_labels(examples):
    tokenized_inputs = tokenizer(examples["tokens"], truncation=True, is_split_into_words=True)

    labels = []
    for i, label in enumerate(examples[f"{task}_tags"]):
        word_ids = tokenized_inputs.word_ids(batch_index=i)
        previous_word_idx = None
        label_ids = []
        for word_idx in word_ids:
            # Special tokens have a word id that is None. We set the label to -100 so they are automatically
            # ignored in the loss function.
            if word_idx is None:
                label_ids.append(-100)
            # We set the label for the first token of each word.
            elif word_idx != previous_word_idx:
                label_ids.append(label[word_idx])
            # For the other tokens in a word, we set the label to either the current label or -100, depending on
            # the label_all_tokens flag.
            else:
                label_ids.append(label[word_idx] if label_all_tokens else -100)
            previous_word_idx = word_idx

        labels.append(label_ids)

    tokenized_inputs["labels"] = labels
    return tokenized_inputs

接下来对数据集datasets里面的所有样本进行预处理,处理的方式是使用map函数,将预处理函数prepare_train_features应用到(map)所有样本上。

tokenized_datasets = datasets.map(tokenize_and_align_labels, batched=True)

更好的是,返回的结果会自动被缓存,避免下次处理的时候重新计算(但是也要注意,如果输入有改动,可能会被缓存影响!)。datasets库函数会对输入的参数进行检测,判断是否有变化,如果没有变化就使用缓存数据,如果有变化就重新处理。但如果输入参数不变,想改变输入的时候,最好清理调这个缓存。清理的方式是使用load_from_cache_file=False参数。另外,上面使用到的batched=True这个参数是tokenizer的特点,以为这会使用多线程同时并行对输入进行处理。

2.2 微调预训练模型

2.2.1 加载预训练模型

使用AutoModelForTokenClassification 这个类,from_pretrained方法下载并加载模型.

from transformers import AutoModelForTokenClassification, TrainingArguments, Trainer

model = AutoModelForTokenClassification.from_pretrained(model_checkpoint, num_labels=len(label_list))

2.2.2 设定训练参数

为了能够得到一个Trainer训练工具,我们还需要3个要素,其中最重要的是训练的设定/参数 TrainingArguments。这个训练设定包含了能够定义训练过程的所有属性。

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

上面evaluation_strategy = "epoch"参数告诉训练代码:我们每个epcoh会做一次验证评估。
上面batch_size在这个notebook之前定义好了。

2.2.3 确定数据收集器

最后我们需要一个数据收集器data collator,将我们处理好的输入喂给模型。

from transformers import DataCollatorForTokenClassification

data_collator = DataCollatorForTokenClassification(tokenizer)

2.2.4 确定评估函数

设置好Trainer还剩最后一件事情,那就是我们需要定义好评估方法。这里用的是seqeval metric来完成评估。

metric = load_metric("seqeval")

评估的输入是预测和label的list

labels = [label_list[i] for i in example[f"{task}_tags"]]
metric.compute(predictions=[labels], references=[labels])

输出长这样
在这里插入图片描述

2.2.5 定义后处理

我们可以对模型预测结果做一些后处理:

  • 选择预测分类最大概率的下标
  • 将下标转化为label
  • 忽略-100所在地方
import numpy as np

def compute_metrics(p):
    predictions, labels = p
    predictions = np.argmax(predictions, axis=2)

    # Remove ignored index (special tokens)
    true_predictions = [
        [label_list[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    true_labels = [
        [label_list[l] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]

    results = metric.compute(predictions=true_predictions, references=true_labels)
    return {
        "precision": results["overall_precision"],
        "recall": results["overall_recall"],
        "f1": results["overall_f1"],
        "accuracy": results["overall_accuracy"],
    }

metric.compute可以计算所有单个类别和全体的precision/recall/f1但这里丢掉了单独的。

2.2.6 训练

接下来将数据/模型/参数传入Trainer即可

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

调用train方法开始训练

trainer.train()

在这里插入图片描述
我们可以再次使用evaluate方法评估。

trainer.evaluate()

在这里插入图片描述

3 疑问

AutoModelForTokenClassification这个类到底做了什么?试图从源码当中找到答案,但是从AutoModelForTokenClassification定义
在这里插入图片描述
找到auto_factory.auto_class_ipdate
在这里插入图片描述
也还是没有弄明白,待后续解决。

参考阅读

  1. Datawhale教程
  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SheltonXiao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值