BERT实战——(2)序列标注
BERT实战——(2)序列标注
引言
我们将展示如何使用 🤗 Transformers代码库中的模型来解决序列标注任务。
任务介绍
序列标注,通常也可以看作是token级别的分类问题:对每一个token进行分类。
token级别的分类任务通常指的是为文本中的每一个token预测一个标签结果。比如命名实体识别任务:
输入:我爱北京天安门 |
常见的token级别分类任务:
- NER (Named-entity recognition 名词-实体识别) 分辨出文本中的名词和实体 (person人名, organization组织机构名, location地点名...).
- POS (Part-of-speech tagging词性标注) 根据语法对token进行词性标注 (noun名词, verb动词, adjective形容词...)
- Chunk (Chunking短语组块) 将同一个短语的tokens组块放在一起。
主要分为以下几个部分:
- 数据加载
- 数据预处理
- 微调预训练模型:使用transformer中的
Trainer
接口对预训练模型进行微调。
前期准备
安装以下库:
pip install datasets transformers seqeval |
数据加载
数据集介绍
我们使用的是CONLL 2003 dataset数据集。
无论是在训练集、验证机还是测试集中,datasets都包含了一个名为tokens
的列(一般来说是将文本切分成了多个token),还包含完成三个不同任务的label列(ner_tag,pos_tag和chunk_tag
),对应了不同任务这tokens的标注。
加载数据
该数据的加载方式在transformers库中进行了封装,我们可以通过以下语句进行数据加载:
from datasets import load_dataset |
如果你使用的是自己的数据,参考第一篇实战博客【定位词:加载数据】加载自己的数据。
给定一个数据切分的key(train、validation或者test)和下标即可查看数据。
datasets["train"][0] |
所有的数据标签都已经被编码成了整数,可以直接被预训练transformer模型使用。这些整数的编码所对应的实际类别储存在features
中。
datasets["train"].features[f"ner_tags"] |
以NER任务为例,0对应的标签类别是”O“, 1对应的是”B-PER“等等。
”O“表示没有特别实体(no special entity/other)。本例包含4种有价值实体类别分别是(PER、ORG、LOC,MISC),每一种实体类别又分别有B-(实体开始的token)前缀和I-(实体中间的token)前缀。
- 'PER' for person
- 'ORG' for organization
- 'LOC' for location
- 'MISC' for miscellaneous
label_list = datasets["train"].features[f"{task}_tags"].feature.names |
下面的函数将从数据集里随机选择几个例子进行展示:
from datasets import ClassLabel, Sequence |
show_random_elements(datasets["train"]) |
id | tokens | pos_tags | chunk_tags | ner_tags | |
---|---|---|---|---|---|
0 | 2227 | [Result, of, a, French, first, division, match, on, Friday, .] | [NN, IN, DT, JJ, JJ, NN, NN, IN, NNP, .] | [B-NP, B-PP, B-NP, I-NP, I-NP, I-NP, I-NP, B-PP, B-NP, O] | [O, O, O, B-MISC, O, O, O, O, O, O] |
1 | 2615 | [Mid-tier, golds, up, in, heavy, trading, .] | [NN, NNS, IN, IN, JJ, NN, .] | [B-NP, I-NP, B-PP, B-PP, B-NP, I-NP, O] | [O, O, O, O, O, O, O] |
2 | 10256 | [Neagle, (, 14-6, ), beat, the, Braves, for, the, third, time, this, season, ,, allowing, two, runs, and, six, hits, in, eight, innings, .] | [NNP, (, CD, ), VB, DT, NNPS, IN, DT, JJ, NN, DT, NN, ,, VBG, CD, NNS, CC, CD, NNS, IN, CD, NN, .] | [B-NP, O, B-NP, O, B-VP, B-NP, I-NP, B-PP, B-NP, I-NP, I-NP, B-NP, I-NP, O, B-VP, B-NP, I-NP, O, B-NP, I-NP, B-PP, B-NP, I-NP, O] | [B-PER, O, O, O, O, O, B-ORG, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O] |
3 | 10720 | [Hansa, Rostock, 4, 1, 2, 1, 5, 4, 5] | [NNP, NNP, CD, CD, CD, CD, CD, CD, CD] | [B-NP, I-NP, I-NP, I-NP, I-NP, I-NP, I-NP, I-NP, I-NP] | [B-ORG, I-ORG, O, O, O, O, O, O, O] |
4 | 7125 | [MONTREAL, 70, 59, .543, 11] | [NNP, CD, CD, CD, CD] | [B-NP, I-NP, I-NP, I-NP, I-NP] | [B-ORG, O, O, O, O] |
数据预处理
在将数据喂入模型之前,我们需要对数据进行预处理。
仍然是两个数据预处理的基本流程:
- 分词;
- 转化成对应任务输入模型的格式;
Tokenizer
用于上面两步数据预处理工作:Tokenizer
首先对输入进行tokenize,然后将tokens转化为预模型中需要对应的token ID,再转化为模型需要的输入格式。
初始化Tokenizer
上一篇博客已经介绍了一些Tokenizer的内容,并做了Tokenizer分词的示例,这里不再重复,下面补充一些在序列标注中需要注意的内容。
from transformers import AutoTokenizer |
转化成对应任务输入模型的格式
transformer预训练模型在预训练的时候通常使用的是subword
,如果我们的文本输入已经被切分成了word,这些word还会被tokenizer继续切分为subwords,同时,由于预训练模型输入格式的要求,往往还需要加上一些特殊符号比如: [CLS]
和 [SEP]
。比如:
example = datasets["train"][4] |
可以看到单词"Zwingmann"
继续被切分成了3个subtokens: 'z', '##wing', '##mann'
。
由于标注数据通常是在word级别进行标注的,而word被切分成了subwords,那么意味需要对标注数据进行subtokens的对齐。
tokenizer中word_ids方法可以帮助我们解决这个问题。
print(tokenized_input.word_ids()) |
可以看到,word_ids将每一个subtokens位置都对应了一个word的下标。比如第1个位置对应第0个word,然后第2、3个位置对应第1个word。特殊字符对应了None。
利用这个list,我们就能将subtokens和words还有标注的labels对齐。
word_ids = tokenized_input.word_ids() |
通常将特殊字符的label设置为-100,在模型中-100通常会被忽略掉不计算loss。
有两种对齐label的方式,通过label_all_tokens = True
切换。
- 多个subtokens对齐一个word,对齐一个label;
- 多个subtokens的第一个subtoken对齐word,对齐一个label,其他subtokens直接赋予-100.
所有内容合起来变成我们的预处理函数。is_split_into_words=True
因为输入的数据已经是按空格切分成word的格式了。
label_all_tokens = True |
以上的预处理函数可以处理一个样本,也可以处理多个样本exapmles。如果是处理多个样本,则返回的是多个样本被预处理之后的结果list。
tokenize_and_align_labels(datasets['train'][:5]) |
接下来使用map函数对数据集datasets里面三个样本集合的所有样本进行预处理,将预处理函数prepare_train_features应用到(map)所有样本上。
tokenized_datasets = datasets.map(tokenize_and_align_labels, batched=True) |
微调预训练模型
数据已经准备好了,我们需要下载并加载预训练模型,然后微调预训练模型。
加载预训练模型
做序列标注token classification任务,那么需要一个能解决这个任务的模型类。我们使用AutoModelForTokenClassification
这个类。
和tokenizer相似,from_pretrained
方法同样可以帮助下载并加载模型,同时也会对模型进行缓存,也可以填入一个包括模型相关文件的文件夹(比如自己预训练的模型),这样会从本地直接加载。
from transformers import AutoModelForTokenClassification |
同样会提示我们加载模型的时候扔掉了一些不匹配的神经网络参数,比如:预训练语言模型的神经网络head被扔掉了,同时随机初始化了文本分类的神经网络head。
设定训练参数
为了能够得到一个Trainer
训练工具,我们还需要训练的设定/参数 TrainingArguments
。这个训练设定包含了能够定义训练过程的所有属性。
task='ner' |
数据收集器data collator
接下来需要告诉Trainer
如何从预处理的输入数据中构造batch。我们使用数据收集器data collator,将经预处理的输入分batch再次处理后喂给模型。
from transformers import DataCollatorForTokenClassification |
定义评估方法
使用seqeval
metric来完成评估。
from datasets import load_metric |
评估的输入是预测和label的list:
labels = [label_list[i] for i in example[f"{task}_tags"]] |
将模型预测送入评估之前,还需要做一些数据后处理:
- 选择预测分类最大概率的下标;
- 将下标转化为label;
- 忽略-100所在位置。
下面的函数将上面的步骤合并了起来:
import numpy as np |
开始训练
将数据/模型/参数传入Trainer
即可:
from transformers import Trainer |
调用train
方法开始训练:
trainer.train() |
模型评估
使用evaluate
方法进行评估:
trainer.evaluate() |
如果想要得到单个类别的precision/recall/f1,直接将结果输入相同的评估函数即可:
predictions, labels, _ = trainer.predict(tokenized_datasets["validation"]) |