浙江耀华建设集团网站,广州网站建设 领航科技,舟山市规划建设局网站,官方网站建设 在线磐石网络本文通过people_daily_ner数据集#xff0c;介绍两段式训练过程#xff0c;第一阶段是训练下游任务模型#xff0c;第二阶段是联合训练下游任务模型和预训练模型#xff0c;来实现中文命名实体识别任务。
一.任务和数据集介绍 1.命名实体识别任务 NER#xff08;Named En…本文通过people_daily_ner数据集介绍两段式训练过程第一阶段是训练下游任务模型第二阶段是联合训练下游任务模型和预训练模型来实现中文命名实体识别任务。
一.任务和数据集介绍 1.命名实体识别任务 NERNamed Entity Recognition和PosPart-of-Speech是2类典型的标记分类问题。NER是信息抽取基础识别文本中的实体比如人名、地点、组织结构名等本质就是预测每个字对应的标记。DL兴起前主要是HMM和CRF等模型现在基本是DL模型。可根据需要设置标注方式常见方式有BIO、BIESO等。NER数据样例如下所示 2.数据集介绍 本文使用中文命名实体识别数据集people_daily_ner样例数据如下所示 people_daily_ner数据集标签对照表如下所示
O表示不属于一个命名实体。B-PER表示人名的开始。I-PER表示人名的中间和结尾部分。B-ORG表示组织机构名的开始。I-ORG表示组织机构名的中间和结尾部分。B-LOC表示地名的开始。I-LOC表示地名的中间和结尾部分。
3.模型架构 本文使用hfl/rbt3模型[2]参数量约3800万。基本思路为使用一个预训练模型从文本中抽取数据特征再对每个字的数据特征做分类任务最终得到和原文一一对应的标签序列BIO。
二.准备数据集 1.使用编码工具 使用hfl/rbt3编码器编码工具如下所示
def load_encode_tool(pretrained_model_name_or_path):加载编码工具tokenizer AutoTokenizer.from_pretrained(Path(f{pretrained_model_name_or_path}))return tokenizer
if __name__ __main__:# 测试编码工具pretrained_model_name_or_path rL:/20230713_HuggingFaceModel/rbt3tokenizer load_encode_tool(pretrained_model_name_or_path)print(tokenizer)# 测试编码句子out tokenizer.batch_encode_plus(batch_text_or_text_pairs[[ 海, 钓, 比, 赛, 地, 点, 在, 厦, 门, 与, 金, 门, 之, 间, 的, 海, 域, 。],[ 这, 座, 依, 山, 傍, 水, 的, 博, 物, 馆, 由, 国, 内, ′, 一, 流, 的, 设, 计, 师, 主, 持, 设, 计, 。]],truncationTrue, # 截断paddingmax_length, # [PAD]max_length20, # 最大长度return_tensorspt, # 返回pytorch张量is_split_into_wordsTrue # 按词切分)# 查看编码输出for k, v in out.items():print(k, v.shape)# 将编码还原为句子print(tokenizer.decode(out[input_ids][0]))print(tokenizer.decode(out[input_ids][1]))输出结果如下所示
BertTokenizerFast(name_or_pathL:\20230713_HuggingFaceModel\rbt3, vocab_size21128, model_max_length1000000000000000019884624838656, is_fastTrue, padding_sideright, truncation_sideright, special_tokens{unk_token: [UNK], sep_token: [SEP], pad_token: [PAD], cls_token: [CLS], mask_token: [MASK]}, clean_up_tokenization_spacesTrue)
input_ids torch.Size([2, 20])
token_type_ids torch.Size([2, 20])
attention_mask torch.Size([2, 20])
[CLS] 海 钓 比 赛 地 点 在 厦 门 与 金 门 之 间 的 海 域 。 [SEP]
[CLS] 这 座 依 山 傍 水 的 博 物 馆 由 国 内 一 流 的 设 计 [SEP]需要说明参数is_split_into_wordsTrue让编码器跳过分词步骤即告诉编码器输入句子是分好词的不用再进行分词。
2.定义数据集 定义数据集代码如下所示
class Dataset(torch.utils.data.Dataset):def __init__(self, split):# 在线加载数据集# dataset load_dataset(pathpeople_daily_ner, splitsplit)# dataset.save_to_disk(dataset_dict_pathL:/20230713_HuggingFaceModel/peoples_daily_ner)# 离线加载数据集dataset load_from_disk(dataset_pathL:/20230713_HuggingFaceModel/peoples_daily_ner)[split]# print(dataset.features[ner_tags].feature.num_classes) #7# print(dataset.features[ner_tags].feature.names) # [O,B-PER,I-PER,B-ORG,I-ORG,B-LOC,I-LOC]self.dataset datasetdef __len__(self):return len(self.dataset)def __getitem__(self, i):tokens self.dataset[i][tokens]labels self.dataset[i][ner_tags]return tokens, labels
if __name__ __main__:# 测试编码工具pretrained_model_name_or_path rL:/20230713_HuggingFaceModel/rbt3tokenizer load_encode_tool(pretrained_model_name_or_path)# 加载数据集dataset Dataset(train)tokens, labels dataset[0]print(tokens, labels, dataset)print(len(dataset))输出结果如下所示
[海, 钓, 比, 赛, 地, 点, 在, 厦, 门, 与, 金, 门, 之, 间, 的, 海, 域, 。] [0, 0, 0, 0, 0, 0, 0, 5, 6, 0, 5, 6, 0, 0, 0, 0, 0, 0] __main__.Dataset object at 0x0000027B01DC3940
20865其中20865表示训练数据集的大小。在people_daily_ner数据集中每条数据包括两个字段即tokens和ner_tags分别代表句子和标签在__getitem__()函数中把这两个字段取出并返回即可。
3.定义计算设备
device cpu
if torch.cuda.is_available():device cuda
print(device)4.定义数据整理函数
def collate_fn(data):tokens [i[0] for i in data]labels [i[1] for i in data]inputs tokenizer.batch_encode_plus(tokens, # 文本列表truncationTrue, # 截断paddingTrue, # [PAD]max_length512, # 最大长度return_tensorspt, # 返回pytorch张量is_split_into_wordsTrue) # 分词完成无需再次分词# 求一批数据中最长的句子长度lens inputs[input_ids].shape[1]# 在labels的头尾补充7把所有的labels补充成统一的长度for i in range(len(labels)):labels[i] [7] labels[i]labels[i] [7] * lenslabels[i] labels[i][:lens]# 把编码结果移动到计算设备上for k, v in inputs.items():inputs[k] v.to(device)# 把统一长度的labels组装成矩阵移动到计算设备上labels torch.tensor(labels).to(device)return inputs, labels形参data表示一批数据主要是对句子和标签进行编码这里会涉及到一个填充的问题。标签的开头和尾部填充7因为0-6都有物理意义而句子开头会被插入[CLS]标签。无论是句子还是标签最终都被转换为矩阵。测试数据整理函数如下所示
data [([海, 钓, 比, 赛, 地, 点, 在, 厦, 门, 与, 金, 门, 之, 间, 的, 海, 域, 。], [0, 0, 0, 0, 0, 0, 0, 5, 6, 0, 5, 6, 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])]
inputs, labels collate_fn(data)
for k, v in inputs.items():print(k, v.shape)
print(labels, labels.shape)输出结果如下所示
input_ids torch.Size([2, 37])
token_type_ids torch.Size([2, 37])
attention_mask torch.Size([2, 37])
labels torch.Size([2, 37])5.定义数据集加载器
loader torch.utils.data.DataLoader(datasetdataset, batch_size16, collate_fncollate_fn, shuffleTrue, drop_lastTrue)通过数据集加载器查看一批样例数据如下所示
for i, (inputs, labels) in enumerate(loader):break
print(tokenizer.decode(inputs[input_ids][0]))
print(labels[0])
for k, v in inputs.items():print(k, v.shape)输出结果如下所示
[CLS] 这 种 输 液 器 不 必 再 悬 吊 药 瓶 改 用 气 压 推 动 液 体 流 动 自 闭 防 回 流 安 全 、 简 便 、 抗 污 染 堪 称 输 液 器 历 史 上 的 一 次 革 命 。 [SEP] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD]
tensor([7, 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, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7], devicecuda:0)
input_ids torch.Size([16, 87])
token_type_ids torch.Size([16, 87])
attention_mask torch.Size([16, 87])三.定义模型 1.加载预训练模型
# 加载预训练模型
pretrained AutoModel.from_pretrained(Path(f{pretrained_model_name_or_path}))
# 统计参数量
# print(sum(i.numel() for i in pretrained.parameters()) / 10000)
# 测试预训练模型
pretrained.to(device)2.定义下游任务模型 先介绍一个两段式训练的概念通常是先单独对下游任务模型进行训练然后再连同预训练模型和下游任务模型一起进行训练的模式。
class Model(torch.nn.Module):def __init__(self):super().__init__()# 标识当前模型是否处于tuning模式self.tuning False# 当处于tuning模式时backbone应该属于当前模型的一部分否则该变量为空self.pretrained None# 当前模型的神经网络层self.rnn torch.nn.GRU(input_size768, hidden_size768, batch_firstTrue)self.fc torch.nn.Linear(in_features768, out_features8)def forward(self, inputs):# 根据当前模型是否处于tuning模式而使用外部backbone或内部backbone计算if self.tuning:out self.pretrained(**inputs).last_hidden_stateelse:with torch.no_grad():out pretrained(**inputs).last_hidden_state# backbone抽取的特征输入RNN网络进一步抽取特征out, _ self.rnn(out)# RNN网络抽取的特征最后输入FC神经网络分类out self.fc(out).softmax(dim2)return out# 切换下游任务模型的tuning模式def fine_tuning(self, tuning):self.tuning tuning# tuning模式时训练backbone的参数if tuning:for i in pretrained.parameters():i.requires_grad Truepretrained.train()self.pretrained pretrained# 非tuning模式时不训练backbone的参数else:for i in pretrained.parameters():i.requires_grad_(False)pretrained.eval()self.pretrained None1tuning表示当前模型是否处于微调模型pretrained表示微调模式时预训练模型属于当前模型。 2在__init__()中定义了下游任务模型的2个层分别为GRU网络和全连接神经网络层GRU作用是进一步抽取特征提高模型预测正确率。 3fine_tuning()用来切换训练模式pretrained.train()和评估模式pretrained.eval()。
四.训练和测试 1.模型训练
def train(epochs):lr 2e-5 if model.tuning else 5e-4 # 根据模型的tuning模式设置学习率optimizer AdamW(model.parameters(), lrlr) # 优化器criterion torch.nn.CrossEntropyLoss() # 损失函数scheduler get_scheduler(namelinear, num_warmup_steps0, num_training_stepslen(loader) * epochs, optimizeroptimizer) # 学习率衰减策略model.train()for epoch in range(epochs):for step, (inputs, labels) in enumerate(loader):# 模型计算# [b,lens] - [b,lens,8]outs model(inputs)# 对outs和labels变形并且移除PAD# outs - [b, lens, 8] - [c, 8]# labels - [b, lens] - [c]outs, labels reshape_and_remove_pad(outs, labels, inputs[attention_mask])# 梯度下降loss criterion(outs, labels) # 计算损失loss.backward() # 反向传播optimizer.step() # 更新参数scheduler.step() # 更新学习率optimizer.zero_grad() # 清空梯度if step % (len(loader) * epochs // 30) 0:counts get_correct_and_total_count(labels, outs)accuracy counts[0] / counts[1]accuracy_content counts[2] / counts[3]lr optimizer.state_dict()[param_groups][0][lr]print(epoch, step, loss.item(), lr, accuracy, accuracy_content)torch.save(model, model/中文命名实体识别.model)训练过程基本步骤如下所示 1从数据集加载器中获取一个批次的数据。 2让模型计算预测结果。 2使用工具函数对预测结果和labels进行变形移除预测结果和labels中的PAD。 4计算loss并执行梯度下降优化模型参数。 5每隔一定的steps输出一次模型当前的各项数据便于观察。 6每训练完一个epoch将模型的参数保存到磁盘。 接下来介绍两段式训练过程第一阶段是训练下游任务模型第二阶段是联合训练下游任务模型和预训练模型如下所示
# 两段式训练第一阶段训练下游任务模型
model.fine_tuning(False)
# print(sum(p.numel() for p in model.parameters() / 10000))
train(1)# 两段式训练第二阶段联合训练下游任务模型和预训练模型
model.fine_tuning(True)
# print(sum(p.numel() for p in model.parameters() / 10000))
train(5)2.模型测试 模型测试基本思路从磁盘加载模型然后切换到评估模式将模型移动到计算设备从测试集中取批次数据输入模型中统计正确率。
def test():# 加载训练完的模型model_load torch.load(model/中文命名实体识别.model)model_load.eval() # 切换到评估模式model_load.to(device)# 测试数据集加载器loader_test torch.utils.data.DataLoader(datasetDataset(validation), batch_size128, collate_fncollate_fn, shuffleTrue, drop_lastTrue)correct 0total 0correct_content 0total_content 0# 遍历测试数据集for step, (inputs, labels) in enumerate(loader_test):# 测试5个批次即可不用全部遍历if step 5:breakprint(step)# 计算with torch.no_grad():# [b, lens] - [b, lens, 8] - [b, lens]outs model_load(inputs)# 对outs和labels变形并且移除PAD# fouts - [b, lens, 8] - [c, 8]# labels - [b, lens] - [c]outs, labels reshape_and_remove_pad(outs, labels, inputs[attention_mask])# 统计正确数量counts get_correct_and_total_count(labels, outs)correct counts[0]total counts[1]correct_content counts[2]total_content counts[3]print(correct / total, correct_content / total_content)3.预测任务
def predict():# 加载模型model_load torch.load(model/中文命名实体识别.model)model_load.eval()model_load.to(device)# 测试数据集加载器loader_test torch.utils.data.DataLoader(datasetDataset(validation), batch_size32, collate_fncollate_fn, shuffleTrue, drop_lastTrue)# 取一个批次的数据for i, (inputs, labels) in enumerate(loader_test):break# 计算with torch.no_grad():# [b, lens] - [b, lens, 8] - [b, lens]outs model_load(inputs).argmax(dim2)for i in range(32):# 移除PADselect inputs[attention_mask][i] 1input_id inputs[input_ids][i, select]out outs[i, select]label labels[i, select]# 输出原句子print(tokenizer.decode(input_id).replace( , ))# 输出tagfor tag in [label, out]:s for j in range(len(tag)):if tag[j] 0:s .continues tokenizer.decode(input_id[j])s str(tag[j].item())print(s)print()参考文献 [1]HuggingFace自然语言处理详解基于BERT中文模型的任务实战 [2]https://huggingface.co/hfl/rbt3 [3]https://huggingface.co/datasets/peoples_daily_ner/tree/main [4]https://github.com/OYE93/Chinese-NLP-Corpus/ [5]https://github.com/ai408/nlp-engineering/blob/main/20230625_HuggingFace自然语言处理详解/第10章中文命名实体识别.py