使用bart生成中文摘要时遇到的一个问题(已解决)

最近在使用复旦大学预训练好的中文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张量就可以了

你可能感兴趣的:(深度学习,人工智能,自然语言处理)