bert中文文本摘要代码

  • 写在最前面
  • 关于BERT
  • 使用transformers库进行微调
  • train.py
  • 自定义参数
  • 迭代训练
  • 验证评估
  • 更新损失
  • 绘图
  • 主函数
  • test.py
  • top_k或top_p采样
  • sample_generate函数
  • generate_file函数
  • 主函数



文本摘要 评价指标 文本摘要 bert_深度学习


🌈你好呀!我是 是Yu欸

🌌 2024每日百字篆刻时光,感谢你的陪伴与支持 ~ 🚀 欢迎一起踏上探险之旅,挖掘无限可能,共同成长!


写在最前面

熟悉bert+文本摘要的下游任务微调的代码,方便后续增加组件实现idea

代码来自:
https://github.com/jasoncao11/nlp-notebook/tree/master

已跑通,略有修改

感谢大家的支持和关注。

最近好多人咨询之前博客【bert中文文本摘要代码】的相关代码报错问题,由于报错有一定的相似性,因此这里统一进行答复

相关博客链接,感兴趣的uu可以点击跳转:
bert中文文本摘要代码(1)bert中文文本摘要代码(2)bert中文文本摘要代码(3)【相关问题解答1】bert中文文本摘要代码:import时无法找到包时,几个潜在的原因和解决方法【相关问题解答2】bert中文文本摘要代码:结果输出为一些重复的标点符号和数字

关于BERT

BERT模型参数的数量取决于具体实现,在Google发布的BERT模型中,大概有1.1亿个模型参数。

通常情况下,BERT的参数是在训练期间自动优化调整的,因此在使用预训练模型时不需要手动调节模型参数。
如果想微调BERT模型以适应特定任务,可以通过改变学习率、正则化参数和其他超参数来调整模型参数。在这种情况下,需要进行一些实验以找到最佳的参数配置。

论文地址:https://arxiv.org/pdf/1810.04805.pdf

使用transformers库进行微调

主要包括:

  • Tokenizer:使用提供好的Tokenizer对原始文本处理,得到Token序列;
  • 构建模型:在提供好的模型结构上,增加下游任务所需预测接口,构建所需模型;
  • 微调:将Token序列送入构建的模型,进行训练。

第一part:【bert中文文本摘要代码(1)】
第二part:【bert中文文本摘要代码(2)】

本文主要为第三part

train.py

自定义参数

# -*- 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())

更新损失

这部分代码用于更新验证损失值,并在验证损失达到新的最低值时保存模型。

  1. 计算当前验证轮次的平均验证损失值,并将其添加到loss_vals_eval列表中。
  2. 通过比较当前验证损失值与之前的最佳验证损失值(best_valid_loss),确定是否需要更新最佳验证损失值和保存模型。
  3. 如果当前验证损失值小于最佳验证损失值,则将最佳验证损失值更新为当前值,并使用torch.save()函数保存模型的状态字典到指定的路径(SAVE_PATH)。这样可以保留当前具有最低验证损失的模型。
  4. 在打印出"best - epoch: %d"消息后,使用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()

绘图

绘制训练损失和验证损失随着训练轮次的变化图表,并保存图表为文件。

  1. 使用plt.plot()函数分别绘制训练损失和验证损失随着训练轮次的变化。np.linspace(1, N_EPOCHS, N_EPOCHS).astype(int)生成了从1到N_EPOCHS的整数数组,用作x轴的取值范围。loss_vals是训练损失的列表,loss_vals_eval是验证损失的列表。l1l2是对应的绘图线条对象。
  2. 使用plt.legend()函数创建图例,并指定图例的句柄(handles)和标签(labels)。这里使用l1l2作为句柄,并指定标签为"Train loss"和"Eval loss"。loc='best'将图例放置在最佳位置。
  3. 使用plt.savefig()函数保存图表为文件,文件名为’bert-seq2seq 3.png’。
  4. 使用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()

文本摘要 评价指标 文本摘要 bert_人工智能_02

主函数

if __name__ == '__main__':
    run()

test.py

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采样

函数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
  • 最后,返回经过过滤后的logits。

用于对生成的概率分布进行过滤,保留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函数

这段代码定义了一个函数sample_generate(),用于生成文本。

函数接受以下参数:

  • text:输入的文本。
  • out_max_length:生成文本的最大长度。
  • top_k:top-k过滤的k值。
  • top_p:top-p过滤的概率阈值。
  • max_length:输入文本的最大长度。
  1. 将模型设置为评估模式(model.eval())。
  2. 根据max_lengthout_max_length计算输入文本的最大长度input_max_length,然后使用Tokenizer.encode()函数对输入文本进行编码,生成input_idstoken_type_idstoken_type_ids_for_masklabels
  3. 将编码后的张量转换为torch.tensor,并将其移动到指定的设备上。
  4. 初始化一个空列表output_ids,用于存储生成的文本。
  5. with torch.no_grad()的上下文中,进行文本生成的循环。在每个步骤中,通过模型预测下一个标记的概率分布。然后使用top_k_top_p_filtering()函数对概率分布进行过滤,得到过滤后的logits。接着使用torch.multinomial()函数从过滤后的分布中采样出下一个标记。
  6. 如果采样到的标记是结束标记(Tokenizer.sep_id),则停止生成过程。否则,将采样到的标记添加到output_ids中,并更新输入的input_idstoken_type_idstoken_type_ids_for_mask,以便下一步的生成。
  7. 最后,使用Tokenizer.decode()函数将生成的标记序列解码为文本,并返回生成的文本。

这个函数实现了使用预训练模型生成文本的功能,可以根据指定的输入文本生成相应的输出文本。
通过调整out_max_lengthtop_ktop_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函数

这段代码定义了一个函数generate_file(df),用于生成文本文件。

函数接受DataFrame对象df作为输入参数。

在函数内部

  1. 首先创建一个副本df.copy(),然后初始化一个空列表generate_diagnosis用于存储生成的诊断摘要。
  2. 接下来,使用循环遍历df的每一行,获取描述文本(假设在第二列),并调用sample_generate()函数生成对应的诊断摘要。
  3. 将生成的诊断摘要添加到generate_diagnosis列表中,并打印输出摘要信息。
  4. 循环结束后,将generate_diagnosis列表作为新列添加到副本DataFrame df 中,并将结果保存到Excel文件中。
  5. 最后将生成的DataFrame保存为名为"bert-seq2seq生成4.xlsx"的Excel文件,保存在"Sheet1"工作表中,不包含行索引。

这个函数可以根据给定的描述文本生成相应的诊断摘要,并将结果保存为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)

主函数

  1. 指定了模型路径model_path为"./bert-base-chinese"。
  2. 通过torch.cuda.is_available()判断是否有可用的CUDA设备,并将设备指定为"cuda:5"。
  3. 使用BertForSeq2Seq.from_pretrained(model_path)加载预训练模型,并将其移动到指定的设备上。
  4. 指定了要处理的文件路径filepath为"./data/test.tsv",并使用pd.read_csv()函数读取该文件内容,以DataFrame的形式存储在变量file中。
  5. 调用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)