Bert基础(十八)--Bert实战:NER命名实体识别
逐步总结
定义
名义识别(Named Entity Recognition, NER)是自然语言处理中的关键技术,旨在从文本中识别出具有特定意义的实体,并对这些实体进行分类。
技术实现
- 步骤:NER任务分为两步——实体边界识别和命名实体分类。
- 标注方式:
- B-I-O(B-开始、I-内部、O-结束):适用于简单场景。
- B-I-E(Begin-起始、Inside-内部结束标识):适用于复杂场景。
- IOB-2(仅起始和结束标识):常用且高效。
评价指标- 准确率(Accuracy):正确预测数与总预测数之比。
- 召回率(Recall):实际正类数与所有正类数之比。
- F1分数(F1 Score):准确率和召回率的调和平均值。
- BLEU分数:用于多候选生成任务。
实战流程- 数据准备:使用Hugging Face提供的“msra_ner”数据集。
- 模型构建:通过预训练模型进行微调。
- 评估与优化:使用seqeval库计算性能指标,并通过调整超参数优化模型。
代码示例
`python加载数据集
from datasets import load_dataset
nerdatasets = loaddataset("msraner", cachedir="./data")查看数据
print(ner_datasets["train"][0])
定义标记函数
def process_function(examples):
tokenizedexmaples = tokenizer(examples["tokens"], maxlength=128, truncation=True, issplitinto_words=True)
labels = []
for i, label in enumerate(examples["ner_tags"]):
wordids = tokenizedexmaples.wordids(batchindex=i)
label_ids = []
for wordid in wordids:
if word_id is None:
label_ids.append(-100)
else:
labelids.append(label[wordid])
labels.append(label_ids)
tokenized_exmaples["labels"] = labels
return tokenized_exmaples创建处理函数并应用到数据上
tokenizeddatasets = nerdatasets.map(process_function, batched=True)
创建并微调模型
`
总结与展望
本文全面介绍了NER的基本概念和技术实现方法,并提供了完整的代码示例以展示如何在实践中应用这些知识。未来可以进一步探索更复杂的模型或结合其他技术提升NER性能。
1、命名实体识别介绍
1.1 简介
命名实体识别(NER)属于自然语言处理领域的一项核心技术。其主要目标是从文本中识别具有特定意义或指代性强的实体,并进行分类。这些常见类型包括人名、地名、组织机构名等基本类型以及日期时间类others如专有名词等。其应用广泛,在信息提取和文本挖掘等领域有广泛应用。
任务主要分为两个方面:
实体边界识别:本任务的目标在于明确文本中实体的具体范围与界限。
实体类型判定:在完成边界定位后,还需对各实体进行分类工作,如识别人名、地名、机构名称等。
举个例子,在分析或处理一段文本内容时,“马云在杭州创建了阿里巴巴”的情况下(即当输入字符串为“马云在杭州创建了阿里巴巴时),NER系统需要识别出阿里巴巴这一术语属于组织机构名称,“马云”属于人名属性,“杭州”则是地理位置标识。
NER的主要实现依赖于机器学习与深度学习等技术手段。
基于模型训练的方法能够识别并分类文本中的实体信息。
随着深度学习技术的进步,在NER方面取得了显著的提升。
现如今在NLP研究与应用中处于领先地位。
今天我们可以借助transformers库来进行实践操作。
1.2 标注方法
在序列标注方法中存在多种标注方式:包括 BIO 标记法(基于位置)、 BIOSE 标记法(基于位置和实体)、 IOB 标记法(基于实体)、 BILOU 标记法(基于具体信息)以及 BMEWO 标记法(基于特定实体),其中前三者尤为突出。
BIO:标识实体的开始,中间部分和非实体部分
- b标识命名实体的起始位置
- I指代该词在命名实体内的具体位置
- o代表仅属于文本外部的普通字符
BIOSE:增加S单个实体情况的标注和增加E实体的结束标识
b标识为一个实体的起始点
I类型的标记位于实体内部
o类型仅用于表示不属于当前实体范围的部分
e类型指定实体结束的位置
s类型特指单个词构成的一个独立实体
IOB-1(即I-B-O-1):一种三元状态标记法(Begin、Inside、Outside),该方法与标准IOB标注法的主要区别在于其起始标记的位置设定。具体而言,在标准IOB标注法中使用"Begin"标记表示命名实体的起始位置,在该方法中则采用"Inside"标签来标识两个连续的同类型命名实体之间的过渡边界位置。举个例子:
词序列为:[Word1]、[Word2]、[Word3]、[Word4]、[Word5]、[Word6]
其中对应的IOB标注结果依次为:I-loc、I-loc、B-loc、I-loc、o、o
而相应的 BIO 标注结果则为:
(B-Loc) (I-Loc) (B-Loc) (I-Loc) o o
在 BIO 标注方法中,
每个实体都会以一个 'B' 标签开始,
后面跟着零个或多个 'I' 标签来表示内部状态。
与之相比,
在 IOB 标注方法中,
'B' 标签仅用于标识两个连续出现的同类型命名实体之间的边界位置。
具体而言,
如果两个连续出现的实体属于同一类型,
则第二个实体将被标记为 'I' 标签而非 'B' 标签。
这个例子中前两个词属于同一个命名实体,
因此在 IO-B 之下它们都被标记为 I-Loc起始。
当遇到一个新的同类型命名实体时,
则会采用 B-Loc 来进行标定。
总的来说,
虽然 IO-B 和 BIO 在处理连续命名实体时存在细微差别,
但 BIO 因其实现简单且易于理解而成为更为常用的方法。
考虑到IOB整体表现欠佳,在这一背景下演变为一个更为精确的体系——IOB-2版本。特别地,在该框架下统一采用'B'标记形式作为命名实体的标准标签。这样一来,在表示方式上,该框架与传统的I-B-O标记方法实现了等价性。
* I表示实体内部
* B表示实体开始
* O表示实体外部
| 标记 | 说明 |
|---|---|
| B-Person | 人名开始 |
| I- Person | 人名中间 |
| B-Organization | 组织名开始 |
| I-Organization | 组织名中间 |
| O | 非命名实体 |
1.3 评价指标
精确率:表示模型性能的重要指标。
该比率等于真实正例数量与所有被判定为正例数量之间的比值。
其主要反映了在实际应用中被正确识别为实体的数量。
召回率(Recall):衡量模型对真实正类识别能力的重要指标。它表示在所有真实存在的正类中被正确识别的比例。该指标能够量化模型在识别实体方面的准确性。
Recall = ...
F-score:该值是精确率与召回率的函数。
在精确率与召回率之间寻求平衡时会用到它。
F-score = (2 × 精确度 × 召回率) / (精确度 + 召回率)
举例说明
| 词组 | Gold标签 | Predict标签 |
|---|---|---|
| 马 | B-PER | B-PER |
| 云 | I-PER | I-PER |
| 在 | O | O |
| 杭 | B-LOC | B-LOC |
| 州 | I-LOC | I-LOC |
| 创 | O | O |
| 建 | O | O |
| 了 | O | O |
| 阿 | B-ORG | B-ORG |
| 里 | I-ORG | I-ORG |
| 巴 | I-ORG | O |
| 巴 | I-ORG | O |
例子中一共有三个实体,
- 在识别过程中总共发现了三个实体,在此基础之上达成了两个正确的识别结果。
- 样本中真实包含三个实体,在这些情况下正确识别出两个。
- 经计算得出F1值等于两倍的精准率乘以召回率除以精准率与召回率之和。
F1 Score = $$
\frac{2 \times Precision \times Recall}{Precision + Recall}将已知数值代入公式: F1 Score =
\frac{2 \times \frac{2}{3} \times \frac{2}{3}}{\frac{2}{3} + \frac{2}{3}} = \boxed{\dfrac{4}{9}}
基本步骤:
1 加载数据集
2 数据预处理
3 创建模型
4 创建评估函数
5 创建训练器
6 训练模型
7 评估
8 预测
2.1 加载数据集
打开hugging face,

我们这里使用msra_ner数据集
ner_datasets = load_dataset("msra_ner", cache_dir="./data")
ner_datasets
DatasetDict({
train: Dataset({
features: ['id', 'tokens', 'ner_tags'],
num_rows: 45001
})
test: Dataset({
features: ['id', 'tokens', 'ner_tags'],
num_rows: 3443
})
})
查看数据
print(ner_datasets["train"][0])
{'id': '0', 'tokens': ['当', '希', '望', '工', '程', '救', '助', '的', '百', '万', '儿', '童', '成', '长', '起', '来', ',', '科', '教', '兴', '国', '蔚', '然', '成', '风', '时', ',', '今', '天', '有', '收', '藏', '价', '值', '的', '书', '你', '没', '买', ',', '明', '日', '就', '叫', '你', '悔', '不', '当', '初', '!'], 'ner_tags': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}
观察一下这个数据集的第一条样本是否缺失了?我们可以了解该数据集的具体标注类型。
ner_datasets["train"].features
{'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)}
label_list = ner_datasets["train"].features["ner_tags"].feature.names
label_list
['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC']
这个是IOB-2类型
2.2 数据预处理
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
tokenizer(ner_datasets["train"][0]["tokens"], is_split_into_words=True)
{'input_ids': [101, 2496, 2361, 3307, 2339, 4923, 3131, 1221, 4638, 4636, 674, 1036, 4997, 2768, 7270, 6629, 3341, 8024, 4906, 3136, 1069, 1744, 5917, 4197, 2768, 7599, 3198, 8024, 791, 1921, 3300, 3119, 5966, 817, 966, 4638, 741, 872, 3766, 743, 8024, 3209, 3189, 2218, 1373, 872, 2637, 679, 2496, 1159, 8013, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
在查看数据的过程中时,在分析原始文本时发现其已经被分词处理。对于那些经过分词处理的数据而言,在构建模型时应将is_split_into_words参数设置为True。
# 借助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
这段代码的功能是实现某种特定的数据处理逻辑,在机器学习模型训练过程中具有重要作用。该代码模块主要包括两个主要部分:首先是对输入数据进行预处理阶段的操作;其次是在特征提取阶段的关键操作步骤。整个模块的设计理念体现了对数据质量与处理效率的高度关注;通过一系列严格的算法流程确保输出结果的质量与可靠性得到有效保障
tokenizer:这是一个转换函数,在接收文本、最大长度、截断标志以及是否按单词分割四个参数后生成标记结果。tokenized_exmaples = tokenizer(examples["tokens"], max_length=128, truncation=True, is_split_into_words=True):这行代码调用该转换函数对输入文本进行标记化处理,并将结果存储于tokenized_exmaples变量中。labels = []:这是一个空列表类型变量。for i, label in enumerate(examples["ner_tags"])::这行代码执行循环遍历操作,在此过程中给每个标签分配一个索引值。word_ids = tokenized_exmaples.word_ids(batch_index=i):这行代码调用方法获取标记化后的单词对应id序列。label_ids = []:这是一个空列表类型变量。for word_id in word_ids::这行代码执行逐一处理每个单词id的操作循环。if word_id is None::如果当前id值为None,则执行特殊操作设置为-100。else::否则执行另一种操作将对应的标签id加入结果列表中。labels.append(label_ids):将当前处理得到的结果加入到主标签列表中。tokenized_exmaples["labels"] = labels:将生成的结果赋值给标记后的示例数据字段。return tokenized_exmaples:最后返回整个处理后的结果数据对象对象结构体。
总体而言,该处理函数通过标记器对输入文本实施标记化处理,并同时将输入的标签映射至对应的标记化文本中。映射完成后生成的标签随后将被应用于后续的命名实体识别过程。
tokenized_datasets = ner_datasets.map(process_function, batched=True)
tokenized_datasets
DatasetDict({
train: Dataset({
features: ['id', 'tokens', 'ner_tags', 'input_ids', 'token_type_ids', 'attention_mask', 'labels'],
num_rows: 45001
})
test: Dataset({
features: ['id', 'tokens', 'ner_tags', 'input_ids', 'token_type_ids', 'attention_mask', 'labels'],
num_rows: 3443
})
})
找一个数据看一下
print(tokenized_datasets["train"][5])
{'id': '5', 'tokens': ['我', '们', '是', '受', '到', '郑', '振', '铎', '先', '生', '、', '阿', '英', '先', '生', '著', '作', '的', '启', '示', ',', '从', '个', '人', '条', '件', '出', '发', ',', '瞄', '准', '现', '代', '出', '版', '史', '研', '究', '的', '空', '白', ',', '重', '点', '集', '藏', '解', '放', '区', '、', '国', '民', '党', '毁', '禁', '出', '版', '物', '。'], 'ner_tags': [0, 0, 0, 0, 0, 1, 2, 2, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 0, 0, 0, 0, 0, 0], 'input_ids': [101, 2769, 812, 3221, 1358, 1168, 6948, 2920, 7195, 1044, 4495, 510, 7350, 5739, 1044, 4495, 5865, 868, 4638, 1423, 4850, 8024, 794, 702, 782, 3340, 816, 1139, 1355, 8024, 4730, 1114, 4385, 807, 1139, 4276, 1380, 4777, 4955, 4638, 4958, 4635, 8024, 7028, 4157, 7415, 5966, 6237, 3123, 1277, 510, 1744, 3696, 1054, 3673, 4881, 1139, 4276, 4289, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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, 1, 2, 2, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 0, 0, 0, 0, 0, 0, -100]}
2.3 创建模型
# 对于所有的非二分类任务,切记要指定num_labels,否则就会device错误
model = AutoModelForTokenClassification.from_pretrained("bert-base-chinese", num_labels=len(label_list))
2.4 创建评估函数
seqeval = evaluate.load("seqeval")
seqeval
这里使用seqeval进行计算,我们使用开头那个例子来看一下
| 词组 | Gold标签 | Predict标签 |
|---|---|---|
| 马 | B-PER | B-PER |
| 云 | I-PER | I-PER |
| 在 | O | O |
| 杭 | B-LOC | B-LOC |
| 州 | I-LOC | I-LOC |
| 创 | O | O |
| 建 | O | O |
| 了 | O | O |
| 阿 | B-ORG | B-ORG |
| 里 | I-ORG | I-ORG |
| 巴 | I-ORG | O |
| 巴 | I-ORG | O |
references = [["B-PER", "I-PER", "O", "B-LOC", "I-LOC", "O", "O", "O", "B-ORG", "I-ORG", "I-ORG", "I-ORG"]]
predictions = [["B-PER", "I-PER", "O", "B-LOC", "I-LOC", "O", "O", "O", "B-ORG", "I-ORG", "O", "O"]]
results = seqeval.compute(predictions=predictions, references=references)
{'LOC': {'precision': 1.0, 'recall': 1.0, 'f1': 1.0, 'number': 1},
'ORG': {'precision': 0.0, 'recall': 0.0, 'f1': 0.0, 'number': 1},
'PER': {'precision': 1.0, 'recall': 1.0, 'f1': 1.0, 'number': 1},
'overall_precision': 0.6666666666666666,
'overall_recall': 0.6666666666666666,
'overall_f1': 0.6666666666666666,
'overall_accuracy': 0.8333333333333334}
创建评估函数
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)
]
result = seqeval.compute(predictions=true_predictions, references=true_labels, mode="strict", scheme="IOB2")
return {
"f1": result["overall_f1"]
}
该代码构建了一个用于评估性能的指标函数eval\_metric。该函数旨在衡量序列标注模型在命名实体识别(NER)任务中的表现能力。具体而言:该函数接受一个名为pred的元组作为输入;该元组中包含模型预测得分以及真实标签的信息;此外还涉及对这些数据进行处理以计算最终结果所需的必要步骤。
具体而言:
-
该函数利用numpy库进行数组运算
-
同时调用seqeval库计算相应的性能指标
-
引入numpy库(...),该库专为高效执行数学运算而设计。
-
定义函数名为eval_metric(...),该函数接受一个参数pred。
-
将输入参数pred解包为两个变量(...),分别存储模型预测的分数和真实的标签。
-
通过沿最后一个轴取最大值索引来获取预测标签ID($...\text{axis}=-1\text{)}。
-
通过以下列表推导式生成转换后的标签列表:
true_predictions = [
[
[label_list[p] for p in seq if label != -100]
for seq, label in zip(pred_sequences, labels)
]
for pred_sequences in pred
] -
同理生成真实标签列表:
true_labels = [
[
[label_list[label] for label in seq]
for seq in labels
]
for labels in true_labels
] -
计算完成后返回评估结果字典:
return {"f1": result["overall_f1"]} -
总之此函数用于评估模型在命名实体识别任务上的性能,
它将模型输出的分数转换为具体标签,
并利用seqeval库计算F1分数作为评估指标。
2.5 创建训练器
args = TrainingArguments(
output_dir="models_for_ner",
per_device_train_batch_size=64,
per_device_eval_batch_size=128,
evaluation_strategy="epoch",
save_strategy="epoch",
metric_for_best_model="f1",
load_best_model_at_end=True,
logging_steps=50,
num_train_epochs=1,
report_to=['tensorboard']
)
为了节省时间我们只训练一遍num_train_epochs=1
2.6 模型训练
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["test"],
compute_metrics=eval_metric,
data_collator=DataCollatorForTokenClassification(tokenizer=tokenizer)
)
trainer.train()

F1达到了0.946
2.7 评估
trainer.evaluate(eval_dataset=tokenized_datasets["test"])
{'eval_loss': 0.02092777192592621,
'eval_f1': 0.9463030643800956,
'eval_runtime': 16.7905,
'eval_samples_per_second': 205.056,
'eval_steps_per_second': 1.608,
'epoch': 1.0}
2.8 预测
# 如果模型是基于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.9968899,
'word': '马 云',
'start': 0,
'end': 2},
{'entity_group': 'LOC',
'score': 0.99767697,
'word': '杭 州',
'start': 3,
'end': 5},
{'entity_group': 'ORG',
'score': 0.98138344,
'word': '阿 里 巴 巴',
'start': 8,
'end': 12}]
