赞
踩
在上一节中介绍了基于tensorflow的Bert源码,以及基于MRPC任务实现的英文文本对相似度分类,本节同样是实现Bert微调,不同的是不再依托于源码工程,而是引入高度集成的**Huggingface
的transformers
框架快速实现一个基于Bert微调的中文实体链接的分类。
本节的中文实体链接针对的是一个中文实体的全称和简称的链接,即输入一对候选的中文名称预测他们是否是一对全简称,比如“奥林匹克运动会”和“奥运会”是正样本**,而**“奥林匹克运动会”和“亚运会”则是负样本。该任务本质上是文本二分类,输入是一对中文文本,而Bert的输入也是[CLS]...[SEP]...[SEP]
**形式的一对文本,因此引入Bert预训练模型再结合本任务特定的全简称正负样本进行微调。预训练模型类似一个人已经学习了语言的基础语法和语义,微调相当于在这个基础上再单独学习全称简称命名这一领域的语言知识。
本节基于Huggingface的transformers和Pytorch进行开发,依赖包版本如下
transformers 4.24.0
torch 1.12.1+cu113
transformers框架提供了基于预训练模型进行算法开发的标准流程范式,提供了统一的API,包括调用各种预训练模型,文本编码,数据转换抽取,模型搭建,训练测试评价等,使得代码开发更加高效和标准化。
本节中使用transformers框架调用bert-base-chinese预训练模型,登陆Huggingface官网手动下载到本地
预训练模型下载
分别下载五个文件,每个文件各自的作用如下
下载之后放到一个文件目录下比如./model/bert_base_chinese,工程目录如下
. ├── bin ├── data ├── etc │ ├── config.yml ├── logs ├── model │ ├── bert_base_chinese │ │ ├── config.json │ │ ├── pytorch_model.bin │ │ ├── tokenizer_config.json │ │ ├── tokenizer.json │ │ └── vocab.txt ├── README.md ├── requirements.txt ├── src │ ├── main │ │ ├── model_pth_hugging_face.py │ └── utils
首先定义数据,继承PyTorch的Dataset类,从而方便完成后续的数据tensor转换,数据迭代器生成等操作。
class TrainData(Dataset): def __init__(self, path, upper_sample=False): self.data = pickle.load(open(os.path.join(ROOT_PATH, path), "rb")) if upper_sample: self.data = self.sample() def sample(self): # 上采样 positive = [x for x in self.data if x.label[1] == 1] negative = [x for x in self.data if x.label[1] == 0] if len(positive) > len(negative): total_num = len(positive) * 2 upper = negative else: total_num = len(negative) * 2 upper = positive while len(self.data) < total_num: self.data.extend(upper) self.data = self.data[:total_num] return self.data def __len__(self): return len(self.data) def __getitem__(self, item): return self.data[item].fullname, self.data[item].shortname, self.data[item].co_vector, self.data[item].label
TrainData类在实例化之后通过索引输出一对全简称,统计特征和label样本,例如
train_data = TrainData("./data/train.pkl", upper_sample=True)
>>> train_data[0]
('奥林匹克运动会', '奥运会', [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 1])
下一步定义数据整理函数,目的是将Dataset的一行特征转化为每个列的特征组,以及完成Bert的分词编码输出PyTorch的tensor格式。
from transformers import BertModel, BertTokenizer TOKENIZER = BertTokenizer.from_pretrained(os.path.join(ROOT_PATH, "./model/bert_base_chinese")) def collate_fn(data): name_pairs = [] co_vectors = [] labels = [] for d in data: name_pairs.append((d[0], d[1])) co_vectors.append(d[2]) labels.append(0 if d[3][0] == 1 else 1) data = TOKENIZER.batch_encode_plus(batch_text_or_text_pairs=name_pairs, truncation=True, padding="max_length", max_length=50, return_tensors="pt") input_ids = data["input_ids"].to(DEVICE) attention_mask = data["attention_mask"].to(DEVICE) token_type_ids = data["token_type_ids"].to(DEVICE) co_vectors = torch.tensor(co_vectors).to(DEVICE) labels = torch.LongTensor(labels).to(DEVICE) return input_ids, attention_mask, token_type_ids, co_vectors, labels
在整理函数collate_fn中使用transformers导入预训练模型将样本分词编码的方法和预训练Bert一样,在batch_encode_plus中输入**[(A, B),(A, B)…]**的一对对样本数据,自定义句子的最大长度截取逻辑,以tensor的形式进行输出,例如将奥林匹克运动会,奥运会这一对进行编码如下
>>> tmp = TOKENIZER.batch_encode_plus(batch_text_or_text_pairs=[("奥林匹克运动会", "奥运会")], truncation=True, padding="max_length", max_length=50, return_tensors="pt")
{'input_ids': tensor([[ 101, 1952, 3360, 1276, 1046, 6817, 1220, 833, 102, 1952, 6817, 833,
102, 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]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 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': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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]])}
分词器返回编码后的input_ids,token_type_ids,attention_mask,可以对input_ids进行反编码查看编码过程,编码器会自动将一对文本添加特殊字符处理成Bert的输入格式,然后返回每个字符在vocab.txt中的id位置
>>> TOKENIZER.decode(tmp['input_ids'][0])
'[CLS] 奥 林 匹 克 运 动 会 [SEP] 奥 运 会 [SEP] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] ...
下一步将数据和整理函数打包为一个PyTorch的DataLoader,定义batch_size和shuffle直接喂给模型训练。
train_loader = DataLoader(train_data, shuffle=True, batch_size=512, drop_last=False, collate_fn=collate_fn)
查看单个批次的输出数据如下,原始的字符串已经转化为分词id,一个批次为512个样本,每个输入最长50个字符。
for i, (input_ids, attention_mask, token_type_ids, co_vectors, labels) in enumerate(train_loader):
break
>>> input_ids
tensor([[ 101, 2813, 704, ..., 0, 0, 0],
[ 101, 4403, 3862, ..., 0, 0, 0],
[ 101, 704, 1744, ..., 0, 0, 0],
...,
[ 101, 704, 1744, ..., 0, 0, 0],
[ 101, 1744, 6084, ..., 0, 0, 0],
[ 101, 7270, 4510, ..., 0, 0, 0]], device='cuda:1')
>>> input_ids.shape
torch.Size([512, 50])
使用PyTorch搭建模型继承nn.Module,在网络层引入本地下载好的预训练BertModel,同时在微调中加入其他的实体统计特征co_vectors和Bert的输出进行拼接,一起输入nn.ModuleList定义多层全连接层得到最终的模型输出
from transformers import BertModel, BertTokenizer PRE_TRAIN = BertModel.from_pretrained(os.path.join(ROOT_PATH, "./model/bert_base_chinese")) class Model(nn.Module): def __init__(self, bert_hidden_size=768, co_vector_size=55, fc_hidden_size="256,64", dropout_rate=0.1): super(Model, self).__init__() self.pre_train = PRE_TRAIN self.fc_hidden_size = [bert_hidden_size + co_vector_size] + list(map(int, fc_hidden_size.split(","))) self.fc_layers = nn.ModuleList([nn.Linear(self.fc_hidden_size[i], self.fc_hidden_size[i + 1]) for i in range(len(self.fc_hidden_size) - 1)]) self.linear = nn.Linear(self.fc_hidden_size[-1], 2) self.relu = nn.ReLU() self.drop = nn.Dropout(p=dropout_rate) def forward(self, input_ids, attention_mask, token_type_ids, co_vectors): # [None, 768] pre_train_out = self.pre_train(input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids).pooler_output # [None, 768 + 55] concat = torch.concat([pre_train_out, co_vectors], dim=1) # fc => [None, 64] fc_out = concat for i in range(len(self.fc_layers)): fc_out = self.fc_layers[i](fc_out) fc_out = self.relu(fc_out) fc_out = self.drop(fc_out) out = self.linear(fc_out) prob = nn.Softmax(dim=1)(out) # [None, 2] return out, prob
transformers通过一行代码引入Bert模型,将input_ids,attention_mask,token_type_ids直接输入,即可一行代码得到Bert的CLS池化输出,相比于上一节从Bert的源码进行微调方便的太多,可以将更多的精力聚焦在微调的网络结构本身还不是去探究Bert环节的实现。
下一步进行模型训练,设置最大样本复制15轮,在验证集10次不出现AUC上升则早停,通过transformers的get_scheduler设置学习衰减和学习率预热
model = Model().to(DEVICE) epochs = 15 optimizer = Adam(model.parameters(), lr=0.0001, weight_decay=0.0) scheduler = get_scheduler("linear", num_warmup_steps=10, num_training_steps=epochs * len(train_loader), optimizer=optimizer) criterion = nn.CrossEntropyLoss(reduction="mean") step = 0 val_auc_list = [] early_stop_flag = False for epoch in range(epochs): for i, (input_ids, attention_mask, token_type_ids, co_vectors, labels) in enumerate(train_loader): model.train() optimizer.zero_grad() output, prob = model(input_ids, attention_mask, token_type_ids, co_vectors) loss = criterion(output, labels) loss.backward() optimizer.step() scheduler.step() step += 1 if step % 1 == 0: auc = roc_auc_score(labels.cpu().detach().numpy(), output.cpu().detach().numpy()[:, 1]) print("epoch: {} step: {} loss: {} auc: {}".format(epoch + 1, step, loss.item(), auc)) if step % 10 == 0: auc, good_recall, bad_recall, acc, loss = model_metrics(val_loader, model) print("[evaluation] loss: {} 正确全简称的召回率: {} 错误全简称的召回率: {} 正确率: {} AUC: {}" .format(loss, good_recall, bad_recall, acc, auc)) diff_auc = (auc - max(val_auc_list)) if len(val_auc_list) else 0 val_auc_list.append(auc) print("本轮auc比之前最大auc{}:{}, 当前最大auc: {}\n".format("上升" if diff_auc > 0 else "下降", abs(diff_auc), max(val_auc_list))) if diff_auc > 0: torch.save(model.state_dict(), os.path.join(ROOT_PATH, "./model/pth_model/model.pth")) print("[save checkpoint]") if early_stop_auc(val_auc_list, windows=10): print("{:-^30}".format("early stop!")) early_stop_flag = True break if early_stop_flag: break
训练早停日志如下
epoch: 3 step: 246 loss: 0.330115407705307 auc: 0.9397805484762006
epoch: 3 step: 247 loss: 0.2885383367538452 auc: 0.9506531204644413
epoch: 3 step: 248 loss: 0.2339172214269638 auc: 0.9690625620352131
epoch: 3 step: 249 loss: 0.22491329908370972 auc: 0.9756562881562881
epoch: 3 step: 250 loss: 0.2437824159860611 auc: 0.9659361819182102
[evaluation] loss: 0.5039 正确全简称的召回率: 0.8096 错误全简称的召回率: 0.8154 正确率: 0.8125 AUC: 0.8902
本轮auc比之前最大auc下降:0.00990000000000002, 当前最大auc: 0.9001
---------early stop!----------
对了三种模型策略,分别是
三种算法策略的测试集模型指标如下
策略 | 正确链接识别率 | 错误连接识别率 | AUC |
---|---|---|---|
统计特征Xgboost | 0.774 | 0.560 | 0.757 |
统计特征 + 2层8头Bert | 0.751 | 0.838 | 0.886 |
统计特征 + 12层12头Bert + 预训练模型 | 0.899 | 0.733 | 0.903 |
仅通过统计相似度特征进行预测能达到一定的分类水平AUC为0.757,而加入Bert表征的字符特征后AUC上升13个点说明全简称有语义规律,再加入预训练模型AUC提升到0.903说明在外部大样本上预训练在特征任务上微调策略的有效性。
感谢你们的阅读和喜欢,我收藏了很多技术干货,可以共享给喜欢我文章的朋友们,如果你肯花时间沉下心去学习,它们一定能帮到你。
因为这个行业不同于其他行业,知识体系实在是过于庞大,知识更新也非常快。作为一个普通人,无法全部学完,所以我们在提升技术的时候,首先需要明确一个目标,然后制定好完整的计划,同时找到好的学习方法,这样才能更快的提升自己。
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
】
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。