最近在使用复旦大学预训练好的中文bart模型来生成摘要(参考BART中文摘要生成,(nplcc与LCSTS数据集)_nlpcc2017数据集_道天翁的博客-CSDN博客),期间在复现的时候遇到了几个很有意思的问题,记录一下。
将训练代码封装在类中,初始化的时候加载预训练模型
class BlogSumBartClass:
def __init__(self,config,options):
self.config = config
self.options = options
self.data_path = ""
bart_path = "./data/models/bart/bart-base-chinese"
self.tokenizer = AutoTokenizer.from_pretrained(bart_path)
self.model_config = BertConfig.from_pretrained(bart_path)
self.model = AutoModelForSeq2SeqLM.from_pretrained(bart_path)
self.max_input_length = 512
self.max_target_length = 256
self.output_dir = ""
self.logs_dir = ""
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)
if not os.path.exists(self.logs_dir):
os.makedirs(self.logs_dir)
self.batch_size = 16
self.num_epochs = 10
加载数据的时候使用load_dataset函数报错,可能是数据集格式有问题,只能手动从jsonl文件中逐行读取数据
def load_data(self):
list_all = []
count = 0
with open(self.data_path) as file:
for it in jsonlines.Reader(file):
list_data = []
body = it["body"]
summary = it["summary"]
if len(body)>510:
body = body[:255] + body[255:]
list_data.append(body.strip())
if len(summary)>510:
summary = summary[:510]
list_data.append(summary.strip())
list_all.append(list_data)
count += 1
df = pd.DataFrame(list_all,columns=['body','summary'])
dataset = Dataset.from_pandas(df)
return dataset
由于预训练模型最大输入长度为512,当一篇文章过长时为选择取其头尾各255个字符读取。
再对数据进行分词和处理
def preprocess_function(self,examples):
inputs = [doc for doc in examples["document"]]
model_inputs = self.tokenizer(inputs,max_length=self.max_input_length,truncation=True)
labels = self.tokenizer(examples["summary"],max_length=self.max_target_length,truncation=True)
model_inputs["labels"] = labels["input_ids"]
return model_inputs
然后是计算评估指标rouge的代码
def compute_metrics(self,eval_pred):
predictions,labels = eval_pred
decoded_preds = self.tokenizer.batch_decode(predictions,skip_special_tokens=True)
labels = np.where(labels != -100,labels,self.tokenizer.pad_token_id)
decoded_labels = self.tokenizer.batch_decode(labels,skip_special_tokens=True)
decoded_preds = ["".join(pred.replace(" ","")) for pred in decoded_preds]
decoded_labels = ["".join(label.replace(" ","")) for label in decoded_labels]
labels_lens = [np.count_nonzero(pred != self.tokenizer.pad_token_id) for pred in labels]
rouge = lawrouge.Rouge()
result = rouge.get_scores(decoded_preds,decoded_labels,avg=True)
print(result)
result = {'rouge-1': result['rouge-1']['f'], 'rouge-2': result['rouge-2']['f'], 'rouge-l': result['rouge-l']['f']}
result = {key:value * 100 for key,value in result.items()}
return result
以下是处理数据和训练模型的总流程代码
def train(self):
dataset = self.load_data()
dataset = dataset.map(self.flatten,remove_columns=["body","summary"])
train_data_txt,validation_data_txt = dataset.train_test_split(test_size=0.1,shuffle=True,seed=42).values()
train_data_txt,test_data_txt = train_data_txt.train_test_split(test_size=0.1,shuffle=True,seed=42).values()
dd = datasets.DatasetDict({"train":train_data_txt,"validation":validation_data_txt,"test":test_data_txt})
raw_datasets = dd
print(raw_datasets)
tokenized_datasets = raw_datasets.map(self.preprocess_function,batched=True)
args = Seq2SeqTrainingArguments(
output_dir=self.output_dir,
num_train_epochs=self.num_epochs,
do_train=True,
do_eval=True,
per_device_train_batch_size=self.batch_size,
per_device_eval_batch_size=self.batch_size,
learning_rate=1e-04,
weight_decay=0.001,
label_smoothing_factor=0.1,
predict_with_generate=True,
logging_dir=self.logs_dir,
logging_steps=200,
evaluation_strategy="epoch",
save_total_limit=3,
generation_max_length=256,
generation_num_beams=1,
)
print(self.tokenizer.pad_token_id)
data_colltaot = DataCollatorForSeq2Seq(self.tokenizer,model=self.model)
trainer = Seq2SeqTrainer(
model=self.model,
args=args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_colltaot,
tokenizer=self.tokenizer,
compute_metrics=self.compute_metrics
)
train_result = trainer.train()
trainer.save_model()
metrics = train_result.metrics
trainer.log_metrics("train",metrics)
trainer.save_metrics("train",metrics)
trainer.save_state()
数据文件格式是一个jsonl文件,内部格式如下:
{"body":"文章一","summary":"摘要一"}
{"body":"文章二","summary":"摘要二"}
{"body":"文章三","summary":"摘要三"}
一切准备就绪了,允许代码,不出意外就要出意外了。
在compute_metrics()中报错了,
在decoded_preds = self.tokenizer.batch_decode(predictions,skip_special_tokens=True)这行报错:OverflowError: out of range integral type conversion attempted。
这行代码的作用是在验证过程中将模型生成的摘要ids通过字典解码成单词,报错的意思是解码时整形范围溢出了。
在网上大量查阅资料和翻看源代码,终于找到了原因了。
先说解决方法,很简单,只要在decoded_preds = self.tokenizer.batch_decode(predictions,skip_special_tokens=True)前加入一行
predictions = np.where(predictions != -100,predictions,self.tokenizer.pad_token_id)
self.tokenizer.batch_decode(predictions,skip_special_tokens=True)
就可以了,可以参考[run_translation.py] out of range integral type conversion attempted · Issue #22634 · huggingface/transformers · GitHub的讨论
至于原因,是生成的ids出现了-100,而字典里index没有-100,自然就溢出了。比如生成的摘要ids可能为
[127,155,164,135,199,6642.....,0,0,0,0]
[127,165,1354,4521,242,652.....,0,-100,-100,-100]
可以在compute_metrics()中添加
def compute_metrics(self,eval_pred):
predictions,labels = eval_pred
for i in predictions:
print(i)
decoded_preds = self.tokenizer.batch_decode(predictions,skip_special_tokens=True)
print(decoded_preds)
来查看验证过程中的生成情况
为什么会出现这样的情况呢?
我们在data_colltaot = DataCollatorForSeq2Seq(self.tokenizer,model=self.model)这行代码中将数据集分词处理并转化为data_collator格式,后面长度未到最大长度就会补齐,在DataCollatorForSeq2Seq()函数中如果没有规定label_pad_token_id的值那么默认补齐的值为-100.所以说-100这个值很特殊,很有可能是补齐的值,但是这里的-100是原数据集中评估集的ids值,并不是模型生成的,并且-100这个值会在模型训练中被忽略掉,所以似乎不是这里导致的报错。
但是我们知道了-100极有可能是补齐的值,而且-100的位置出现都是值尾部而且长度不一,甚至有时候并没有出现-100,而且很多时候都是用0在补齐。这就很疑惑了,为什么会有两个补齐值。
通过翻看Trainer的源代码,发现问题出在generation_max_length这个参数上面。
这个参数的意思是生成的摘要最大长度为这个,当超过时会截断,不足时会补齐,补齐就是用的-100.
当评估数据和生成数据长度不一样时,Trainer会自动补齐长度,使用-100来补齐,这就导致了预测生成的摘要尾部可能会出现-100导致整形溢出。所以只要使用numpy的where将-100改成模型的padding值就行了,这个模型的padding值为0。
如果transformer版本较高的话,就不能使用with tokenizer.as_target_tokenizer()来同步分词了,这个函数在高版本被移除了。
测试的时候使用Trainer.get_eval-dataloader()来加载测试集和生成集,然后在使用np.where()时报错无法加载GPU张量,在使用前
outputs = outputs.to('cuda')
outputs = outputs.cpu().numpy()
将GPU张量改为CPU张量就可以了