【NLP】使用 SpaCy 通过 LLM 合成数据微调 NER 模型
在我们之前的文章“使用 SpaCy、ollama 创建用于命名实体识别的合成数据集”中,我们探讨了如何使用 Qwen 生成合成数据,以自动化命名实体识别 (NER) 的注释过程。通过利用合成数据,我们能够高效地准备来自 SmoothNLP金融新闻数据集 的高质量数据集。现在,在这篇文章中,我们将更进一步,使用 SpaCy 训练各种 NER 模型,比较它们的性能,并检查它们在我们合成生成的数据集中识别实体的能力。
步骤 1:加载模型并准备管道
import spacy
nlp = spacy.load('zh_core_web_sm')
from spacy.training import Example
from spacy.util import minibatch
from tqdm import tqdm
import random
from spacy.tokens import DocBin
# Get the Named Entity Recognizer (NER) component or add it if not present
if "ner" not in nlp.pipe_names:
ner = nlp.add_pipe("ner")
else:
ner = nlp.get_pipe("ner")
在这里,我们加载了 SpaCy 的预训练中文模型,zh_core_web_sm
其中包括大型词向量以提高准确性。我们检查ner
管道中是否存在命名实体识别器 ( ) 组件。这对于识别文本中的名称、位置和组织等实体至关重要。
步骤 2:加载训练数据
# 加载训练数据(它是 .spacy 格式)
train_doc_bin = DocBin().from_disk(r"train.spacy")
# 将文档箱转换为文档对象
docs = list(train_doc_bin.get_docs(nlp.vocab))
# 将数据分成训练和验证集(例如,80% 训练,20% 验证)
train_ratio = 0.8
split_idx = int(len(docs) * train_ratio)
train_docs = docs[:split_idx]
valid_docs = docs[split_idx:]
我们使用 SpaCy 的类加载以 .spacy
格式存储的训练数据(DocBin)
,这对于大型数据集非常有效。该get_docs()
方法将二进制文件转换为 SpaCy的Doc
对象。然后,我们将数据分成训练集 (80%) 和验证集 (20%),使我们能够在训练期间评估模型性能。
步骤 3:准备训练示例
train_examples = [Example.from_dict(doc, {"entities": [(ent.start_char, ent.end_char, ent.label_) for ent in doc.ents]}) for doc in train_docs]
在这里,我们通过创建对象来准备训练示例Example
。每个示例将原始文本(doc
)与其实体注释(起始和终止字符索引以及实体标签)配对。这些Example
对象对于训练 SpaCy 模型至关重要,因为它们封装了输入(原始文本)和预期输出(实体)。
步骤 4:初始化优化器并设置训练参数
# 初始化优化器以进行微调
optimizer = nlp.resume_training()
# 设置训练参数
n_iter = 20 # 训练周期次数
patient = 4 # 提前停止耐心
batch_size = 8 # 训练批次大小
best_f1 = 0 # 跟踪最佳 F1 分数
no_improvement = 0 # 用于提前停止
我们使用 SpaCy 的内置resume_training()
方法恢复训练,该方法初始化优化器。然后我们定义训练参数:
n_iter
:训练周期数(模型查看整个训练数据的次数)。patience
:在触发早期停止之前,等待未出现改进的时期数。batch_size
:每次训练的示例数。best_f1
:在验证期间跟踪最佳 F1 分数。no_improvement
:计算 F1 分数没有提高的时期数,用于早期停止。
步骤 5:训练循环
# 使用早期停止和验证的训练循环
for epoch in tqdm(range(n_iter)):
print(f"Epoch {epoch + 1}/{n_iter}")
# 打乱训练数据
random.shuffle(train_examples)
# 创建训练数据批次
batches = minibatch(train_examples, size=batch_size)
# 在每个批次上进行训练
for batch in tqdm(batches, leave=False):
nlp.update(batch, sgd=optimizer)
# 在每个 epoch 之后对验证集进行评估
print("Evaluating on validation set...")
valid_examples = []
for doc in valid_docs:
# 创建一个包含原始文本(无注释)的新 Doc 对象用于预测
pred_doc = nlp.make_doc(doc.text)
# 创建一个将预测与原始带注释文档配对的 Example 对象
example = Example(pred_doc, doc)
valid_examples.append(example)
# 将 nlp.evaluate() 与新创建的 Example 对象一起使用
results = nlp.evaluate(valid_examples)
# 提取 F1 分数、精度和召回率
precision = results["ents_p"]
recall = results["ents_r"]
f1 = results["ents_f"]
print(f"Validation Precision: {precision:.4f}, Recall: {recall:.4f}, F1 Score: {f1:.4f}")
# 根据验证 F1 分数的早期停止逻辑
if f1 > best_f1:
best_f1 = f1
no_improvement = 0
# Save the best model
nlp.to_disk("best_model")
else:
no_improvement += 1
print(f"No improvement for {no_improvement} epochs.")
if no_improvement >= patience:
print("Early stopping triggered. Stopping training.")
break
这是主要的训练循环。对于每个周期:
- 我们对训练示例进行打乱,以避免过度拟合。
- 我们用来
minibatch
创建小批量数据进行梯度更新。 nlp.update()
对每一批训练示例执行单步优化,调整模型的权重以最小化损失。- 在每个时期之后,我们都会在验证集上评估模型。对于每个验证示例,我们使用生成预测
nlp.make_doc()
并将它们与原始注释进行比较。我们使用nlp.evaluate()
来计算评估指标,例如精确度、召回率和F1 分数。这些指标可帮助我们评估模型在未见过的数据上的表现。如果 F1 分数在一个时期后有所提高,我们会重置计数器no_improvement
并保存模型。如果模型在一定数量的时期内没有显示出任何改进(patience
),我们会停止训练以避免过度拟合。表现最佳的模型将保存到磁盘。
步骤 6:评估最终模型
为了使用手动注释的测试集有效地评估最终模型,我们可以按如下方式进行设置:
# 从磁盘加载表现最佳的模型
nlp = spacy.load("best_model")
# 测试数据
test_doc_bin = DocBin().from_disk(r"test.spacy")
test_docs = list(test_doc_bin.get_docs(nlp.vocab))
# 将测试文档转换为示例以供评估
test_examples = [Example.from_dict(doc, {"entities": [(ent.start_char, ent.end_char, ent.label_) for ent in doc.ents]}) for doc in test_docs]
# 在测试示例上评估最终模型
test_results = nlp.evaluate(test_examples)
test_precision = test_results["ents_p"]
test_recall = test_results["ents_r"]
test_f1 = test_results["ents_f"]
print(f"Test Precision: {test_precision:.4f}, Recall: {test_recall:.4f}, F1 Score: {test_f1:.4f}")
步骤 7:使用我们微调的模型进行预测
为了使用我们微调的 NER 模型进行预测,我们可以加载保存的模型并使用它来识别新文本数据中的实体:
# 加载微调后的模型
nlp = spacy.load("best_model")
# 定义要分析的文本
text = "贵州:扫黑风暴使在逃多年涉案人员自首\n新华社贵阳2月15日电(记者 汪军)记者从贵州省公安厅了解到,在扫黑除恶专项斗争高压态势下,在逃8年多的涉嫌组织、领导、参加黑社会性质组织的胡某河近日主动到贵州省铜仁市公安机关投案自首。\n2009年8月,贵州省公安厅统一指挥,成功打掉了铜仁市思南县一涉嫌组织、领导、参加黑社会性质的犯罪团伙,当地群众拍手称快,但该案主要犯罪嫌疑人胡某河在逃。\n今年1月,全国扫黑除恶专项斗争开展以来,贵州省市县三级公安机关协调联动,通过媒体发布相关通告,宣讲有关政策,广泛发动群众检举揭发,深入宣传党和政府扫黑除恶的决心,敦促涉黑涉恶人员主动投案自首、争取宽大处理,积极营造全社会参与支持扫黑除恶的氛围。\n近日在公安机关强大宣传攻势下,在逃8年多的涉嫌组织、领导、参加黑社会性质组织的胡某河迫于压力,主动到公安机关投案自首。目前,该案相关工作正在进一步进行中。"
# 使用模型处理文本
doc = nlp(text)
# 打印识别出的带有标签的实体
for ent in doc.ents:
print(f"Entity: {ent.text}, Label: {ent.label_}")
结论
本文详细介绍了如何使用SpaCy库训练和优化中文命名实体识别(NER)模型。首先,我们加载了SpaCy的预训练中文模型,并确保了NER组件的存在。接着,我们从.spacy格式的文件中加载训练数据,并将其分为训练集和验证集。然后,我们准备了训练示例,初始化了优化器,并设置了训练参数。在训练循环中,我们对训练数据进行打乱和分批处理,同时在每个周期后评估模型在验证集上的性能,并根据F1分数进行早期停止判断。最终,我们保存了表现最佳的模型,并在手动注释的测试集上进行了评估。文章最后展示了如何使用微调后的模型对新文本进行实体识别预测,从而验证了模型的实用性和准确性。