前情回顾
- attention和transformers
- BERT和GPT
- 编写BERT模型
- BERT的应用、训练和优化
- 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"
model_checkpoint = "distilbert-base-uncased"
batch_size = 16
接下来加载数据,数据加载和评测方式加载只需要简单使用load_dataset 和load_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。
当tokenizer 的is_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:
if word_idx is None:
label_ids.append(-100)
elif word_idx != previous_word_idx:
label_ids.append(label[word_idx])
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)
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 也还是没有弄明白,待后续解决。
参考阅读
- Datawhale教程
|