实战命名实体识别任务

一、命名实体识别简介

  • 命名实体识别(Named Entity Recognition, 简称NER)是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等。 通常包括两部分:① 实体边界识别 ② 确定实体类别(人名、地名、机构名或其他)。例:“小明在北京上班”:

    实体类别 实体
    地点 北京
    人物 小明
  • 数据标注体系:IOB1、IOB2、IOE1、IOE2、IOBES、BILOU

  • IOB2标注:

    • I表示实体内部,O表示实体外部,B表示实体开始
    • B/I-XXX,XXX表示具体的类别
    • 示例:
    标记 说明
    B-Person 人名开始
    I-Person 人名中间
    B-Organization 组织名开始
    I-Organization 组织名中间
    O 非命名实体
  • IOBES标注:

    • I表示实体内部,O表示实体外部,B表示实体开始,E表示实体结束,S表示一个词单独形成一个命名实体
    • 有时也会用M代替I,但本质是同一含义
  • 简单示例

二、代码实战

模型结构(ModelForTokenClassification):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class BertForTokenClassification(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.num_labels = config.num_labels

# 使用预训练模型BertModel作为基础模型
self.bert = BertModel(config, add_pooling_layer=False)
classifier_dropout = (
config.classifier_dropout if config.classifier_dropout is not None else config.hidden_dropout_prob
)
self.dropout = nn.Dropout(classifier_dropout)
self.classifier = nn.Linear(config.hidden_size, config.num_labels)

# Initialize weights and apply final processing
self.post_init()

@add_start_docstrings_to_model_forward(BERT_INPUTS_DOCSTRING.format("batch_size, sequence_length"))
@add_code_sample_docstrings(
checkpoint=_CHECKPOINT_FOR_TOKEN_CLASSIFICATION,
output_type=TokenClassifierOutput,
config_class=_CONFIG_FOR_DOC,
expected_output=_TOKEN_CLASS_EXPECTED_OUTPUT,
expected_loss=_TOKEN_CLASS_EXPECTED_LOSS,
)
def forward(
self,
input_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
token_type_ids: Optional[torch.Tensor] = None,
position_ids: Optional[torch.Tensor] = None,
head_mask: Optional[torch.Tensor] = None,
inputs_embeds: Optional[torch.Tensor] = None,
labels: Optional[torch.Tensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
) -> Union[Tuple[torch.Tensor], TokenClassifierOutput]:
r"""
labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
Labels for computing the token classification loss. Indices should be in `[0, ..., config.num_labels - 1]`.
"""
return_dict = return_dict if return_dict is not None else self.config.use_return_dict

outputs = self.bert(
input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)

sequence_output = outputs[0]

sequence_output = self.dropout(sequence_output)
logits = self.classifier(sequence_output)

loss = None
if labels is not None:
loss_fct = CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))

if not return_dict:
output = (logits,) + outputs[2:]
return ((loss,) + output) if loss is not None else output

return TokenClassifierOutput(
loss=loss,
logits=logits,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)

2.1 导入相关包

1
2
3
from transformers import AutoTokenizer, AutoModelForTokenClassification, TrainingArguments, Trainer, DataCollatorForTokenClassification
from datasets import load_dataset
import evaluate

2.2 加载数据集

1
2
3
4
5
6
7
# 如果可以联网,直接使用load_dataset进行加载
#ner_datasets = load_dataset("peoples_daily_ner", cache_dir="./data")
# 如果无法联网,则使用下面的方式加载数据集
from datasets import DatasetDict
ner_datasets = DatasetDict.load_from_disk("ner_data")

label_list = ner_datasets["train"].features["ner_tags"].feature.names # ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC']

ner_datasets数据集格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{'id': '0',
'tokens': ['海',
'钓',
'比',
'赛',
'地',
'点',
'在',
'厦',
'门',
'与',
'金',
'门',
'之',
'间',
'的',
'海',
'域',
'。'],
'ner_tags': [0, 0, 0, 0, 0, 0, 0, 5, 6, 0, 5, 6, 0, 0, 0, 0, 0, 0]}

其中的ner_tags表示标注的类别,0表示O,1表示B-PER,2表示I-PER,3表示B-ORG,4表示I-ORG,5表示B-LOC,6表示I-LOC(PER:人名,ORG:组织名,LOC:地点名)
可以通过ner_datasets[“train”].features查看数据集的具体格式:

1
2
3
{'id': Value(dtype='string', id=None),
'tokens': Sequence(feature=Value(dtype='string', id=None), length=-1, id=None),
'ner_tags': Sequence(feature=ClassLabel(names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC'], id=None), length=-1, id=None)}

2.3 数据集预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-macbert-base")

# 对于已经做好tokenize的数据,tokenizer会把每一个字都当做一个token进行编码,所以需要指定is_split_into_words参数为True
# tokenizer(ner_datasets["train"][0]["tokens"], is_split_into_words=True)

# 由于tokenizer中使用的子词机制,可能会将一个字拆分成多个token,所以需要使用word_ids来进行映射
# 借助word_ids 实现标签映射
def process_function(examples):
tokenized_exmaples = tokenizer(examples["tokens"], max_length=128, truncation=True, is_split_into_words=True)
labels = []
for i, label in enumerate(examples["ner_tags"]):
word_ids = tokenized_exmaples.word_ids(batch_index=i)
label_ids = []
for word_id in word_ids:
if word_id is None:
label_ids.append(-100)
else:
label_ids.append(label[word_id])
labels.append(label_ids)
tokenized_exmaples["labels"] = labels
return tokenized_exmaples

tokenized_datasets = ner_datasets.map(process_function, batched=True)

划分好的数据集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DatasetDict({
train: Dataset({
features: ['id', 'tokens', 'ner_tags', 'input_ids', 'token_type_ids', 'attention_mask', 'labels'],
num_rows: 20865
})
validation: Dataset({
features: ['id', 'tokens', 'ner_tags', 'input_ids', 'token_type_ids', 'attention_mask', 'labels'],
num_rows: 2319
})
test: Dataset({
features: ['id', 'tokens', 'ner_tags', 'input_ids', 'token_type_ids', 'attention_mask', 'labels'],
num_rows: 4637
})
})


处理过后的第0条数据:

1
{'id': '0', 'tokens': ['海', '钓', '比', '赛', '地', '点', '在', '厦', '门', '与', '金', '门', '之', '间', '的', '海', '域', '。'], 'ner_tags': [0, 0, 0, 0, 0, 0, 0, 5, 6, 0, 5, 6, 0, 0, 0, 0, 0, 0], 'input_ids': [101, 3862, 7157, 3683, 6612, 1765, 4157, 1762, 1336, 7305, 680, 7032, 7305, 722, 7313, 4638, 3862, 1818, 511, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'labels': [-100, 0, 0, 0, 0, 0, 0, 0, 5, 6, 0, 5, 6, 0, 0, 0, 0, 0, 0, -100]}

2.4 创建模型

1
2
# 对于所有的非二分类任务,切记要指定num_labels,否则就会device错误
model = AutoModelForTokenClassification.from_pretrained("hfl/chinese-macbert-base", num_labels=len(label_list), max_seq_length=128)

2.5 创建评估函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
seqeval = evaluate.load("seqeval")

import numpy as np

def eval_metric(pred):
predictions, labels = pred
predictions = np.argmax(predictions, axis=-1)

# 将id转换为原始的字符串类型的标签
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 = seqeval.compute(predictions=true_predictions, references=true_labels, mode="strict", schema="IOB2")

return {
"f1": results["overall_f1"]
}

2.6 配置训练参数

1
2
3
4
5
6
7
8
9
10
11
12
args = TrainingArguments(
output_dir="models_for_ner",
per_device_train_batch_size=64,
per_device_eval_batch_size=128,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="f1",
logging_steps=50,
num_train_epochs=1,
report_to="none"
)

2.7 创建训练器

1
2
3
4
5
6
7
8
9
trainer = Trainer(
model=model,
args=args,
processing_class=tokenizer,
data_collator=DataCollatorForTokenClassification(tokenizer),
compute_metrics=eval_metric,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
)

2.8 模型训练

1
2
3
trainer.train()

trainer.evaluate(eval_dataset=tokenized_datasets["test"])

2.9 模型预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from transformers import pipeline

# 使用pipeline进行推理时要指定id2label
model.config.id2label = {index: label for idx, label in enumerate(label_list)}

# 如果模型是基于GPU训练的,那么推理时要指定device
# 对于NER任务,可以指定aggregation_strategy为simple,得到具体的实体的结果,而不是token的结果
ner_pipe = pipeline("token-classification", model=model, tokenizer=tokenizer, device=0, aggregation_strategy="simple")

res = ner_pipe("小明在北京上班")

"""
res:
[{'entity_group': 'PER',
'score': 0.949744,
'word': '小 明',
'start': 0,
'end': 2},
{'entity_group': 'LOC',
'score': 0.9895726,
'word': '北 京',
'start': 3,
'end': 5}]
"""

# 根据start和end取实际的结果
# res中使用decode得到的结果中间还会有空格,所以还需要进一步处理
ner_result = {}
x = "小明在北京上班"
for r in res:
if r["entity_group"] not in ner_result:
ner_result[r["entity_group"]] = []
ner_result[r["entity_group"]].append(x[r["start"]: r["end"]])

print(ner_result) # {'PER': ['小明'], 'LOC': ['北京']}

参考资料:
[1] 【手把手带你实战HuggingFace Transformers-入门篇】基础知识与环境安装
[2] 【Github项目地址】