能走到这里说明你对模型微调有了一个基本的认识。那么开始一段命名实体的任务过程,下面使用huggingface官网的数据。
依赖 | 版本 |
pytorch | 1.5.0+cpu |
transformers | transformers-4.0.0 |
evaluate-0.4.2
pip install seqeval,evaluate
1 准备模型
下面的模型自己选择一个吧,我的内存太第一个模型跑不了。
https://huggingface.co/ckiplab/bert-base-chinese-ner/tree/main
2 准备数据
https://huggingface.co/datasets/peoples_daily_ner
3 训练
评估指标
https://huggingface.co/spaces/evaluate-metric/seqeval
import evaluate
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForTokenClassification, TrainingArguments, Trainer, DataCollatorForTokenClassification
# 如果可以联网,直接使用load_dataset进行加载
#ner_datasets = load_dataset("peoples_daily_ner", cache_dir="./data")
# 如果无法联网,则使用下面的方式加载数据集
from datasets import DatasetDict
ner_datasets = DatasetDict.load_from_disk("../data/ner_data/")
ner_datasets
tokenizer = AutoTokenizer.from_pretrained("/Users/user/studyFile/2024/nlp/bert_base_chinese_ner/")
# 借助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)
tokenized_datasets
# 自己定义数据的类别个数
label_list = ner_datasets["train"].features["ner_tags"].feature.names
#model = AutoModelForTokenClassification.from_pretrained("../bert_base_chinese_ner/", num_labels=len(label_list))
import torch
model = AutoModelForTokenClassification.from_pretrained("../bert_base_chinese_ner/",num_labels=len(label_list),ignore_mismatched_sizes=True)
#model.num_labels = len(label_list)
#num_labels = len(label_list)
#model.classifier.out_proj.weight.data = torch.nn.functional.linear(model.classifier.weight, (model.classifier.weight.shape[0] / num_labels))
#model.classifier.out_proj.bias.data = model.classifier.bias
# 这里方便大家加载,替换成了本地的加载方式,无需额外下载
seqeval = evaluate.load("seqeval_metric.py")
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)
]
result = seqeval.compute(predictions=true_predictions, references=true_labels, mode="strict", scheme="IOB2")
return {
"f1": result["overall_f1"]
}
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
)
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
compute_metrics=eval_metric,
data_collator=DataCollatorForTokenClassification(tokenizer=tokenizer)
)
trainer.train()
训练的过程太慢了。
4 优化
其实在直接对模型的输出然后利用交叉熵损失函数对优化模型,这样可能会损失上下文之间的依赖信息。那么我们通过CRF进行规划。
1 考虑标签间的依赖关系
在命名实体识别任务中,标签之间的顺序依赖性非常重要。例如,如果一个实体被标记为“B-PERSON”(表示人名实体的开始),那么紧随其后的标签更有可能是“I-PERSON”(表示人名实体的一部分),而不是另一个“B-PERSON”。CRF 模型能够显式地建模这种标签间的依赖关系,从而更好地捕捉到序列中的模式。
2. 约束标签的合法性
CRF 可以在解码阶段施加标签的合法约束,确保生成的标签序列是合理的。例如,在 BIO(Begin, Inside, Outside)标签体系中,不允许出现孤立的 “I-PERSON”,即在没有 “B-PERSON” 的情况下,不应该出现 “I-PERSON”。CRF 在解码过程中可以通过转移概率来保证这种约束。
3. 更好的序列解码
CRF 提供了一种全局最优解的解码方式。与传统的局部最优解(例如使用 Softmax 函数)相比,CRF 能够在考虑整个序列的情况下寻找最优标签序列。这意味着 CRF 不仅考虑当前 token 的信息,还考虑了序列中所有 token 的相互关系,从而提高了整体的预测准确性。
4. 引入特征函数
CRF 允许引入丰富的特征函数,这些特征函数可以捕捉到各种上下文信息,从而增强模型的表达能力。例如,可以引入词性特征、上下文窗口特征等,这些特征对于提高 NER 的性能非常有帮助。
5. 处理边界情况
CRF 可以很好地处理边界情况,比如实体的开始和结束边界。在某些情况下,模型可能会错误地标记实体的边界,而 CRF 通过考虑标签之间的转移概率,可以在一定程度上修正这些错误。
那么损失函数怎么表示呢?
条件随机场(Conditional Random Fiedl)是指给定一组输入的随机变量条件下另一组输出随机变量的条件概率分布模型P(Y|X),其特点是假设输出随机变量构成马尔可夫随机场。条件随机场打破了隐马尔可夫模型的俩个假设(观测独立性假设和齐次马尔可夫性假设),使输入向量和输出向量之间的关系更加明显,从而使其在文本处理等问题上的表现更加优越。
损失 = 状态损失 + 转移损失;
如果没有CRF其实状态损失就是原来的交叉熵损失函数;
-
定义 CRF 模型的能量函数:
CRF 模型定义了一个能量函数 E(y∣x)E(y∣x),它衡量了给定输入序列 xx 时,标签序列 yy 的得分。能量函数通常由两部分组成:
- 发射得分(Emission Scores):ϕt(yt,xt),表示第 tt 个位置的标签 ytyt 在输入 xtxt 上的得分。作用就是模型预测当前输出是否正确;
- 转移得分(Transition Scores):ψt(yt−1,yt),表示从标签 yt−1yt−1 转移到标签 ytyt 的得分。作用就是模型预测当前模型输出前后文之间的相关性(约束)是否合理;
下面是一个转移得分:
转移矩阵这些分数将随着训练的迭代过程被更新,换句话说,CRF层可以自己学到这些约束条件。
CRF损失函数由两部分组成,真实路径的分数 和 所有路径的总分数。真实路径的分数应该是所有路径中分数最高的。
1 计算所有的模型可能的输出得分(发射+转移) ,得到很多得分P1.. PN;
2 计算损失,让真实路径得分最大;
TorchCRF
from TorchCRF import CRF # 从 TorchCRF 库导入 CRF
import torch.nn as nn
class BertForTokenClassificationWithCRF(nn.Module):
def __init__(self, num_labels,path):
super(BertForTokenClassificationWithCRF, self).__init__()
# 加载预训练的 BERT 模型
self.bert = AutoModelForTokenClassification.from_pretrained(path,num_labels=len(label_list))
# # 定义一个线性层将 BERT 的输出映射到标签空间
# self.dropout = nn.Dropout(0.1)
# self.classifier = nn.Linear(self.bert.config.hidden_size, num_labels)
# 添加 CRF 层
self.crf = CRF(num_labels)
def forward(self, input_ids, attention_mask,token_type_ids, labels=None):
# 通过 BERT 得到特征表示
outputs = self.bert(input_ids, attention_mask=attention_mask,token_type_ids=token_type_ids)
#print(outputs)
sequence_output = outputs.logits
#print(sequence_output.shape)
# 应用 dropout
#sequence_output = self.dropout(sequence_output)
# 线性层得到标签分数
emissions = sequence_output #self.classifier(sequence_output)
# 如果提供了标签,则计算损失
if labels is not None:
# 将 attention_mask 转换为布尔类型
mask = attention_mask.bool()
# 计算 CRF 损失
loss = -self.crf(emissions, labels, mask=mask)
return loss
# 否则,解码标签序列
else:
# 应用 CRF 层解码标签
decoded_tags = self.crf.viterbi_decode(emissions, attention_mask.bool())
return decoded_tags
model = BertForTokenClassificationWithCRF(len(label_list),path='../bert-base-chinese/')
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",
# save_steps=200,
# eval_steps=200,
metric_for_best_model="f1",
load_best_model_at_end=True,
logging_steps=20,
num_train_epochs=1
)
model.train()
trainer = Trainer(
model=model,
args=args,
tokenizer=tokenizer,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
compute_metrics=eval_metric,
data_collator=DataCollatorForTokenClassification(tokenizer=tokenizer)
)
# if torch.cuda.is_available():
# trainer.model = trainer.model.to("cuda")
trainer.train()
RuntimeError: false INTERNAL ASSERT FAILED at "../c10/cuda/CUDAGraphsC10Utils.h":73, please report a bug to PyTorch. Unknown CUDA graph CaptureStatus22047
报错了,说是版本不兼容!
cuda 12.3
torch: 1.5.0+cpu