熟悉bert+文本摘要的下游任务微调的代码,方便后续增加组件实现idea
代码来自:
https://github.com/jasoncao11/nlp-notebook/tree/master
已跑通,略有修改
BERT模型参数的数量取决于具体实现,在Google发布的BERT模型中,大概有1.1亿个模型参数。
通常情况下,BERT的参数是在训练期间自动优化调整的,因此在使用预训练模型时不需要手动调节模型参数。
如果想微调BERT模型以适应特定任务,可以通过改变学习率、正则化参数和其他超参数来调整模型参数。在这种情况下,需要进行一些实验以找到最佳的参数配置。
论文地址:https://arxiv.org/pdf/1810.04805.pdf
主要包括:
第一part:【bert中文文本摘要代码(1)】https://blog.csdn.net/wtyuong/article/details/130972775
第二part:【bert中文文本摘要代码(2)】https://blog.csdn.net/wtyuong/article/details/130981010
本文主要为第三part
# -*- coding: utf-8 -*-
import torch
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
from transformers import AdamW, get_linear_schedule_with_warmup
from load_data import traindataloader, valdataloader
from model import BertForSeq2Seq
N_EPOCHS = 5
LR = 5e-4
WARMUP_PROPORTION = 0.001
MAX_GRAD_NORM = 1.0
MODEL_PATH = './bert-base-chinese'
SAVE_PATH = './saved_models/pytorch_model.bin'
# device = "cuda" if torch.cuda.is_available() else 'cpu'
device = torch.device('cuda:5')
使用不同的权重衰减值设置了带有分组参数的优化器。
no_decay
列表包含了在优化过程中不应进行权重衰减的参数的名称。optimizer_grouped_parameters
变量定义了两个参数组:一个带有权重衰减,一个没有权重衰减。
no_decay
列表中的参数,weight_decay
值设置为0.01;no_decay
列表中的参数,weight_decay
值设置为0.0。def run():
best_valid_loss = float('inf')
model = BertForSeq2Seq.from_pretrained(MODEL_PATH)
model.to(device)
no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
{'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)],
'weight_decay': 0.01},
{'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
]
设置优化器和学习率调度器的部分。
total_steps
表示总的训练步数,计算方法是训练数据集的批次数*训练轮数(N_EPOCHS
)。
optimizer
使用AdamW
优化器,接受两个参数:optimizer_grouped_parameters
是之前定义的参数组,lr
是学习率,这里设置为LR
。
scheduler
是学习率调度器,使用get_linear_schedule_with_warmup
函数进行设置。学习率在预热阶段逐渐增加,然后保持稳定进行训练。接受三个参数:
optimizer
是之前定义的优化器num_warmup_steps
表示预热步数,这里设置为总步数的一部分(WARMUP_PROPORTION
)num_training_steps
表示总的训练步数。 total_steps = len(traindataloader) * N_EPOCHS
optimizer = AdamW(optimizer_grouped_parameters, lr=LR, eps=1e-8)
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=int(WARMUP_PROPORTION * total_steps), num_training_steps=total_steps)
迭代训练数据集中的批次,并在每个批次上执行训练步骤。
在每个训练轮次(epoch
)开始时,将模型设置为训练模式(model.train()
),并初始化一个空列表epoch_loss
用于存储每个批次的损失值。
然后,通过使用tqdm
库创建一个进度条显示训练进度,使得训练过程更加可视化。在每个批次上,从traindataloader
中获取批次数据,并将数据移动到指定的device
上。
接下来的代码执行以下操作:
model.zero_grad()
)。loss.backward()
)。torch.nn.utils.clip_grad_norm_
函数对梯度进行裁剪,以防止梯度爆炸问题。epoch_loss
列表中。optimizer.step()
)。pbar.set_postfix(loss=loss.item())
)。step()
方法,更新学习率。在每个轮次结束后,计算当前轮次的平均损失值,并将其添加到loss_vals
列表中,用于后续的可视化或记录。
loss_vals = []
loss_vals_eval = []
for epoch in range(N_EPOCHS):
model.train()
epoch_loss = []
pbar = tqdm(traindataloader)
pbar.set_description("[Train Epoch {}]".format(epoch))
for batch_idx, batch_data in enumerate(pbar):
input_ids = batch_data["input_ids"].to(device)
token_type_ids = batch_data["token_type_ids"].to(device)
token_type_ids_for_mask = batch_data["token_type_ids_for_mask"].to(device)
labels = batch_data["labels"].to(device)
model.zero_grad()
predictions, loss = model(input_ids, token_type_ids, token_type_ids_for_mask, labels)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), MAX_GRAD_NORM)
epoch_loss.append(loss.item())
optimizer.step()
pbar.set_postfix(loss=loss.item())
scheduler.step()
loss_vals.append(np.mean(epoch_loss))
模型在验证集上进行评估的部分。它与训练循环类似,但模型处于评估模式(model.eval()
)。
在每个验证轮次(epoch
)开始时,将模型设置为评估模式,并初始化一个空列表epoch_loss_eval
用于存储每个批次的验证损失值。
然后,通过使用tqdm
库创建一个进度条显示评估进度,使得评估过程更加可视化。在每个批次上,你从valdataloader
中获取批次数据,并将数据移动到指定的device
上。
接下来的代码执行以下操作:
torch.no_grad()
上下文管理器,以确保在评估模式下,梯度不会被计算和更新。epoch_loss_eval
列表中。pbar.set_postfix(loss=loss.item())
)。在每个验证轮次结束后,计算当前轮次的平均验证损失值,并将其添加到epoch_loss_eval
列表中。
获得模型在验证集上的损失值,以评估模型的性能。
model.eval()
epoch_loss_eval= []
pbar = tqdm(valdataloader)
pbar.set_description("[Eval Epoch {}]".format(epoch))
with torch.no_grad():
for batch_idx, batch_data in enumerate(pbar):
input_ids = batch_data["input_ids"].to(device)
token_type_ids = batch_data["token_type_ids"].to(device)
token_type_ids_for_mask = batch_data["token_type_ids_for_mask"].to(device)
labels = batch_data["labels"].to(device)
predictions, loss = model.forward(input_ids, token_type_ids, token_type_ids_for_mask, labels)
epoch_loss_eval.append(loss.item())
pbar.set_postfix(loss=loss.item())
这部分代码用于更新验证损失值,并在验证损失达到新的最低值时保存模型。
loss_vals_eval
列表中。best_valid_loss
),确定是否需要更新最佳验证损失值和保存模型。torch.save()
函数保存模型的状态字典到指定的路径(SAVE_PATH
)。这样可以保留当前具有最低验证损失的模型。torch.cuda.empty_cache()
函数清空GPU缓存,以释放不再使用的显存。跟踪最佳验证损失值,并保存在每个验证轮次中具有最佳性能的模型。
valid_loss = np.mean(epoch_loss_eval)
loss_vals_eval.append(valid_loss)
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), SAVE_PATH)
print("best - epoch: %d"%(epoch))
torch.cuda.empty_cache()
绘制训练损失和验证损失随着训练轮次的变化图表,并保存图表为文件。
plt.plot()
函数分别绘制训练损失和验证损失随着训练轮次的变化。np.linspace(1, N_EPOCHS, N_EPOCHS).astype(int)
生成了从1到N_EPOCHS
的整数数组,用作x轴的取值范围。loss_vals
是训练损失的列表,loss_vals_eval
是验证损失的列表。l1
和l2
是对应的绘图线条对象。plt.legend()
函数创建图例,并指定图例的句柄(handles
)和标签(labels
)。这里使用l1
和l2
作为句柄,并指定标签为"Train loss"和"Eval loss"。loc='best'
将图例放置在最佳位置。plt.savefig()
函数保存图表为文件,文件名为’bert-seq2seq 3.png’。plt.show()
函数显示图表。可视化训练损失和验证损失随着训练轮次的变化,以便进行性能分析和比较。
l1, = plt.plot(np.linspace(1, N_EPOCHS, N_EPOCHS).astype(int), loss_vals)
l2, = plt.plot(np.linspace(1, N_EPOCHS, N_EPOCHS).astype(int), loss_vals_eval)
plt.legend(handles=[l1,l2],labels=['Train loss','Eval loss'],loc='best')
plt.savefig('bert-seq2seq 3.png')
plt.show()
if __name__ == '__main__':
run()
saved_models文件夹包含两个文件:
(1)在原有bert-base-chinese基础上fine-tune的pytorch_model.bin
(2)配置文件config.json,和原有bert-base-chinese的配置文件一样
# -*- coding: utf-8 -*-
import torch
import torch.nn.functional as F
import numpy as np
from model import BertForSeq2Seq
from tokenizer import Tokenizer
import pandas as pd
函数top_k_top_p_filtering()
用于对logits进行top-k和top-p采样。
函数接受以下参数:
logits
:logits分布,形状为(vocabulary size)的张量。top_k
:保留概率最高的top_k个标记(token)。top_p
:保留累积概率大于等于top_p的标记(token)。filter_value
:过滤掉的标记(token)所对应的值。函数的实现逻辑如下:
logits
是一个一维张量。top_k
大于0,则将概率小于top_k中最低概率的标记设为filter_value
。top_p
大于0.0,则对logits进行排序,并计算累积概率。然后,将累积概率超过top_p的标记设为filter_value
。用于对生成的概率分布进行过滤,保留top_k个概率最高的标记,或者保留累积概率大于等于top_p的标记。这样可以控制生成结果的多样性和可靠性。
def top_k_top_p_filtering(logits, top_k=0, top_p=0.0, filter_value=-float('Inf')):
""" Filter a distribution of logits using top-k and/or nucleus (top-p) filtering
Args:
logits: logits distribution shape (vocabulary size)
top_k > 0: keep only top k tokens with highest probability (top-k filtering).
top_p > 0.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering).
Nucleus filtering is described in Holtzman et al. (http://arxiv.org/abs/1904.09751)
From: https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317
"""
assert logits.dim() == 1 # batch size 1 for now - could be updated for more but the code would be less clear
top_k = min(top_k, logits.size(-1)) # Safety check
if top_k > 0:
# Remove all tokens with a probability less than the last token of the top-k
indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
logits[indices_to_remove] = filter_value
if top_p > 0.0:
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
# Remove tokens with cumulative probability above the threshold
sorted_indices_to_remove = cumulative_probs > top_p
# Shift the indices to the right to keep also the first token above the threshold
sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
sorted_indices_to_remove[..., 0] = 0
indices_to_remove = sorted_indices[sorted_indices_to_remove]
logits[indices_to_remove] = filter_value
return logits
这段代码定义了一个函数sample_generate()
,用于生成文本。
函数接受以下参数:
text
:输入的文本。out_max_length
:生成文本的最大长度。top_k
:top-k过滤的k值。top_p
:top-p过滤的概率阈值。max_length
:输入文本的最大长度。model.eval()
)。max_length
和out_max_length
计算输入文本的最大长度input_max_length
,然后使用Tokenizer.encode()
函数对输入文本进行编码,生成input_ids
、token_type_ids
、token_type_ids_for_mask
和labels
。torch.tensor
,并将其移动到指定的设备上。output_ids
,用于存储生成的文本。with torch.no_grad()
的上下文中,进行文本生成的循环。在每个步骤中,通过模型预测下一个标记的概率分布。然后使用top_k_top_p_filtering()
函数对概率分布进行过滤,得到过滤后的logits。接着使用torch.multinomial()
函数从过滤后的分布中采样出下一个标记。Tokenizer.sep_id
),则停止生成过程。否则,将采样到的标记添加到output_ids
中,并更新输入的input_ids
、token_type_ids
和token_type_ids_for_mask
,以便下一步的生成。Tokenizer.decode()
函数将生成的标记序列解码为文本,并返回生成的文本。这个函数实现了使用预训练模型生成文本的功能,可以根据指定的输入文本生成相应的输出文本。
通过调整out_max_length
、top_k
和top_p
等参数,可以控制生成文本的长度和多样性。
def sample_generate(text, out_max_length=256, top_k=30, top_p=0.0, max_length=512):
# device = "cuda" if torch.cuda.is_available() else 'cpu'
model.eval()
input_max_length = max_length - out_max_length
input_ids, token_type_ids, token_type_ids_for_mask, labels = Tokenizer.encode(text, max_length=input_max_length)
input_ids = torch.tensor(input_ids, device=device, dtype=torch.long).view(1, -1)
token_type_ids = torch.tensor(token_type_ids, device=device, dtype=torch.long).view(1, -1)
token_type_ids_for_mask = torch.tensor(token_type_ids_for_mask, device=device, dtype=torch.long).view(1, -1)
#print(input_ids, token_type_ids, token_type_ids_for_mask)
output_ids = []
with torch.no_grad():
for step in range(out_max_length):
scores = model(input_ids, token_type_ids, token_type_ids_for_mask)
logit_score = torch.log_softmax(scores[:, -1], dim=-1).squeeze(0)
logit_score[Tokenizer.unk_id] = -float('Inf')
# 对于已生成的结果generated中的每个token添加一个重复惩罚项,降低其生成概率
for id_ in set(output_ids):
logit_score[id_] /= 1.5
filtered_logits = top_k_top_p_filtering(logit_score, top_k=top_k, top_p=top_p)
next_token = torch.multinomial(F.softmax(filtered_logits, dim=-1), num_samples=1)
if Tokenizer.sep_id == next_token.item():
break
output_ids.append(next_token.item())
input_ids = torch.cat((input_ids, next_token.long().unsqueeze(0)), dim=1)
token_type_ids = torch.cat([token_type_ids, torch.ones((1, 1), device=device, dtype=torch.long)], dim=1)
token_type_ids_for_mask = torch.cat([token_type_ids_for_mask, torch.zeros((1, 1), device=device, dtype=torch.long)], dim=1)
#print(input_ids, token_type_ids, token_type_ids_for_mask)
return Tokenizer.decode(np.array(output_ids))
这段代码定义了一个函数generate_file(df)
,用于生成文本文件。
函数接受DataFrame对象df
作为输入参数。
在函数内部
df.copy()
,然后初始化一个空列表generate_diagnosis
用于存储生成的诊断摘要。df
的每一行,获取描述文本(假设在第二列),并调用sample_generate()
函数生成对应的诊断摘要。generate_diagnosis
列表中,并打印输出摘要信息。generate_diagnosis
列表作为新列添加到副本DataFrame df
中,并将结果保存到Excel文件中。这个函数可以根据给定的描述文本生成相应的诊断摘要,并将结果保存为Excel文件。
def generate_file(df):
df = df.copy()
generate_diagnosis = []
i = 1
for description in df.iloc[:,1]:
summary = sample_generate(description, top_k=5, top_p=0.95)
generate_diagnosis.append(summary)
print(i,"摘要:",summary)
i = i + 1
df.loc[:, "generate_diagnosis"] = generate_diagnosis
df.to_excel("bert-seq2seq生成4.xlsx", sheet_name='Sheet1', index=False)
model_path
为"./bert-base-chinese"。torch.cuda.is_available()
判断是否有可用的CUDA设备,并将设备指定为"cuda:5"。BertForSeq2Seq.from_pretrained(model_path)
加载预训练模型,并将其移动到指定的设备上。filepath
为"./data/test.tsv",并使用pd.read_csv()
函数读取该文件内容,以DataFrame的形式存储在变量file
中。generate_file()
函数,将读取的文件数据作为参数传递给该函数,用于生成诊断摘要,并将结果保存为Excel文件。运行脚本时,加载模型并处理指定的文件,生成诊断摘要并保存结果。
if __name__ == '__main__':
model_path = './bert-base-chinese'
print(torch.cuda.is_available())
device = torch.device('cuda:5')
model = BertForSeq2Seq.from_pretrained(model_path).to(device)
filepath = './data/test.tsv'
file = pd.read_csv(filepath, sep='\t')
generate_file(file)