【GPT】文本生成任务(生成摘要、文本纠错、机器翻译等的模型微调)

note

一、NLG任务

NLG:自然语言生成任务,很多NLP任务可以被描述为NLG任务,如经典的T5模型(text to text transfer transformer模型)就是NLG模型,如文本纠错任务,输出正确的文本描述、智能问答根据一定背景进行推理,然后回答。

# 安装一些必要的包
!pip install openai
# torch install 命令 https://pytorch.org/get-started/locally/
!pip install torch==2.0.0+cpu torchvision==0.15.1+cpu --extra-index-url https://download.pytorch.org/whl/cpu
!pip install tokenizers==0.13.2
!pip install transformers==4.27.4
!pip install --no-binary=protobuf protobuf==3.20.1
!pip install sentencepiece==0.1.97
!pip install shap==2.8.2
# 配置openai api key
import openai
OPENAI_API_KEY = "填入专属的API key"  # TODO
openai.api_key = OPENAI_API_KEY

models = openai.Model.list()
# 有60+个模型,fine-tune模型是以ft-personal开头
print([x.id for x in models.data])

二、NLG之文本摘要

主要分为三种:

  • 抽取式摘要:从原文档中提取现成的句子作为摘要句。
  • 压缩式摘要:对原文档的冗余信息进行过滤,压缩文本作为摘要。
  • 生成式摘要:基于NLG技术,根据源文档内容,由算法模型自己生成自然语言描述。

2.1 基于mT5的文本摘要

这里先使用huggingface进行测试:

''''''
import re
import torch
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

# 载入模型
tokenizer = AutoTokenizer.from_pretrained("csebuetnlp/mT5_multilingual_XLSum")
model = AutoModelForSeq2SeqLM.from_pretrained("csebuetnlp/mT5_multilingual_XLSum")

WHITESPACE_HANDLER = lambda k: re.sub('\s+', ' ', re.sub('\n+', ' ', k.strip()))

text = """全球游戏用户已经达到37亿,占到全球人口的一半,这是一个非常庞大的市场,这个市场完全值得我们去关注和应用,其中休闲游戏用户又占了很大的比例。为什么休闲游戏用户能够占那么大的比例,且还在不断增长呢?我们拿短视频迅猛的发展来举例。现在大家生活节奏很快,压力也很大,用户需要一些轻松快乐的体验来缓解压力、调整情绪。短视频的体验就非常轻松,想看就看,想停就停,看的时候可以给人带来快乐,还可以让你学到一些东西。在大数据的加持下,不断推给用户想看的东西,甚至让用户停不下来。休闲游戏也是类似的,休闲游戏本质上也是轻松,可以带来快乐并帮助用户减压的,而且休闲游戏比短视频有一个优势是其提供的各种各样的核心玩法,具备更多交互元素和不同的体验。"""
text = WHITESPACE_HANDLER(text)
input_ids = tokenizer([text], return_tensors="pt", padding="max_length", truncation=True, max_length=512)["input_ids"]

# 生成结果文本
output_ids = model.generate(input_ids=input_ids, max_length=84, no_repeat_ngram_size=2, num_beams=4)[0]
output_text = tokenizer.decode(output_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)

print("原始文本: ", text)
print("摘要文本: ", output_text)
# 摘要文本:  休闲游戏已经成为人们生活中不可或缺的一部分。

2.2 基于openai接口测试

def summarize_text(text):
    response = openai.Completion.create(
        engine="text-davinci-003",
        prompt=f"请对以下文本进行总结,注意总结的凝炼性,将总结字数控制在20个字以内:\n{
      
      text}",
        temperature=0.7,
        max_tokens=500,
    )

    summarized_text = response.choices[0].text.strip()
    return summarized_text

text = "全球游戏用户已经达到37亿,占到全球人口的一半,这是一个非常庞大的市场,这个市场完全值得我们去关注和应用,其中休闲游戏用户又占了很大的比例。为什么休闲游戏用户能够占那么大的比例,且还在不断增长呢?我们拿短视频迅猛的发展来举例。现在大家生活节奏很快,压力也很大,用户需要一些轻松快乐的体验来缓解压力、调整情绪。短视频的体验就非常轻松,想看就看,想停就停,看的时候可以给人带来快乐,还可以让你学到一些东西。在大数据的加持下,不断推给用户想看的东西,甚至让用户停不下来。休闲游戏也是类似的,休闲游戏本质上也是轻松,可以带来快乐并帮助用户减压的,而且休闲游戏比短视频有一个优势是其提供的各种各样的核心玩法,具备更多交互元素和不同的体验。"""
output_text = summarize_text(text)
print("原始文本: ", text)
print("摘要文本: ", output_text)
print("摘要文本长度: ", len(output_text))
# 摘要文本:  全球游戏用户已达37亿,休闲游戏占比很大且不断增长,因其轻松快乐的体验及大数据推送,可缓解生活压力并学习新知识。
# 摘要文本长度:  56

2.3 基于chatGPT接口

def summarize_text(text):
    content = f"请对以下文本进行总结,注意总结的凝炼性,将总结字数控制在20个字以内:\n{
      
      text}"
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[{
    
    "role": "user", "content": content}]
    )
    summarized_text = response.get("choices")[0].get("message").get("content")
    return summarized_text

text = """全球游戏用户已经达到37亿,占到全球人口的一半,这是一个非常庞大的市场,这个市场完全值得我们去关注和应用,其中休闲游戏用户又占了很大的比例。为什么休闲游戏用户能够占那么大的比例,且还在不断增长呢?我们拿短视频迅猛的发展来举例。现在大家生活节奏很快,压力也很大,用户需要一些轻松快乐的体验来缓解压力、调整情绪。短视频的体验就非常轻松,想看就看,想停就停,看的时候可以给人带来快乐,还可以让你学到一些东西。在大数据的加持下,不断推给用户想看的东西,甚至让用户停不下来。休闲游戏也是类似的,休闲游戏本质上也是轻松,可以带来快乐并帮助用户减压的,而且休闲游戏比短视频有一个优势是其提供的各种各样的核心玩法,具备更多交互元素和不同的体验。"""
output_text = summarize_text(text)

print("原始文本: ", text)
print("摘要文本: ", output_text)
print("摘要文本长度: ", len(output_text))
# 注意,chatgpt并不能完美限制摘要输出的字数
# 摘要文本:  全球游戏用户超37亿,休闲游戏用户增长迅速,因为轻松解压,且提供多样化体验。
# 摘要文本长度:  38

从上面的三种方法结果对比,可以发现huggingface的预训练模型mT5结果其实还不太好,后面两种结果挺好,还概括出游戏受欢迎的原因。

三、根据自己的数据集进行模型微调

数据集来源:CSL摘要数据集,是计算机领域的论文摘要和标题数据,包含3500条数据,

  • 标题:平均字数 18,字数标准差 4,最大字数41,最小数字 6;
  • 正文:平均字数 200,字数标准差 63,最大字数 631,最小数字 41;

这里我们根据自己的数据集进行模型微调,3500条数据,每个数据都包括titlecontent两个内容。data列表中每个元素是一个含有这两坨的字典。
数据源地址:https://github.com/liucongg/GPT2-NewsTitle 项目中的CSL摘要数据集

import json
with open('/home/andy/torch_rechub_n/hug_llm/content/dataset/csl_data.json', 'r', encoding='utf-8') as f:
    data = json.load(f)
len(data) # 3500

data[0]
{
    
    'title': '保细节的网格刚性变形算法',
 'content': '提出了一种新的保细节的变形算法,可以使网格模型进行尽量刚性的变形,以减少变形中几何细节的扭曲.首先根据网格曲面局部细节的丰富程度,对原始网格进行聚类生成其简化网格;然后对简化网格进行变形,根据其相邻面片变形的相似性,对简化网格作进一步的合并,生成新的变形结果,将该变形传递给原始网格作为初始变形结果.由于对属于同一个类的网格顶点进行相同的刚性变形,可在变形中较好地保持该区域的表面细节,但分属不同类的顶点之间会出现变形的不连续.为此,通过迭代优化一个二次能量函数,对每个网格顶点的变形进行调整来得到最终变形结果.实验结果显示,该算法简单高效,结果令人满意.'}

import pandas as pd
df = pd.DataFrame(data)
df = df[['content', 'title']]
df.columns = ["prompt", "completion"]
df_train = df.iloc[:500]
df_train.head(5)

在这里插入图片描述

file_path = "/home/andy/torch_rechub_n/hug_llm/content/dataset"
df_train.to_json(file_path + "/csl_summarize_finetune.jsonl", \
                 orient='records', lines=True, force_ascii=False)

!openai tools fine_tunes.prepare_data -f /home/andy/torch_rechub_n/hug_llm/content/dataset/csl_summarize_finetune.jsonl -q
file_path = "/home/andy/torch_rechub_n/hug_llm/content/dataset"
!openai api fine_tunes.create \
    -t "/home/andy/torch_rechub_n/hug_llm/content/dataset/csl_summarize_finetune_prepared.jsonl" \
    -m ada\
    --no_check_if_files_exist

huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...
To disable this warning, you can either:
	- Avoid using `tokenizers` before the fork if possible
	- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)
Upload progress: 100%|███████████████████████| 380k/380k [00:00<00:00, 841Mit/s]
Uploaded file from /home/andy/torch_rechub_n/hug_llm/content/dataset/csl_summarize_finetune_prepared.jsonl: file-cNMh8V9gYipAoLlqgoL0upll
Created fine-tune: ft-6IqMLGVl0FXEWifOvBsY0Aoc
Streaming events until fine-tuning is complete...

(Ctrl-C will interrupt the stream, but not cancel the fine-tune)
[2023-04-25 02:48:37] Created fine-tune: ft-6IqMLGVl0FXEWifOvBsY0Aoc
[2023-04-25 02:48:49] Fine-tune costs $0.43
[2023-04-25 02:48:50] Fine-tune enqueued. Queue number: 2

从上面得到fine tune运行所需的key:

!openai api fine_tunes.get -i ft-6IqMLGVl0FXEWifOvBsY0Aoc
# 保存openai fine tune过程的记录
!openai api fine_tunes.results -i ft-6IqMLGVl0FXEWifOvBsY0Aoc > /home/andy/torch_rechub_n/hug_llm/content/dataset/metric.csv

# 后面的执行步骤是和之前一致的
# game text
def summarize_text(text, model_name):
    response = openai.Completion.create(
        engine=model_name,
        prompt=f"请对以下文本进行总结,注意总结的凝炼性,将总结字数控制在20个字以内:\n{
      
      text}",
        temperature=0.7,
        max_tokens=100,
    )

    summarized_text = response.choices[0].text.strip()
    return summarized_text

text = "全球游戏用户已经达到37亿,占到全球人口的一半,这是一个非常庞大的市场,这个市场完全值得我们去关注和应用,其中休闲游戏用户又占了很大的比例。为什么休闲游戏用户能够占那么大的比例,且还在不断增长呢?我们拿短视频迅猛的发展来举例。现在大家生活节奏很快,压力也很大,用户需要一些轻松快乐的体验来缓解压力、调整情绪。短视频的体验就非常轻松,想看就看,想停就停,看的时候可以给人带来快乐,还可以让你学到一些东西。在大数据的加持下,不断推给用户想看的东西,甚至让用户停不下来。休闲游戏也是类似的,休闲游戏本质上也是轻松,可以带来快乐并帮助用户减压的,而且休闲游戏比短视频有一个优势是其提供的各种各样的核心玩法,具备更多交互元素和不同的体验。"""
print("一、原始文本: ", text, "\n")
print("二、ada摘要文本: ", summarize_text(text, model_name='ada'), "\n")
# print("ada fine-tune摘要文本: ", summarize_text(text, model_name='ada:ft-personal-2023-04-15-13-29-50'))
# # ft model: ada:ft-personal-2023-04-25-02-56-14
print("三、ada fine-tune摘要文本: ", summarize_text(text, model_name='ada:ft-personal-2023-04-25-02-56-14'))

二、ada摘要文本:  因此,多达到休闲游戏的最佳解决方式是在推给用户带来快乐的时候,提醒一个人做好改进活动。

三、ada fine-tune摘要文本:  休闲游戏产品的结构和价值出现在游戏的结构上的字数较偏高的地位。有类似的游戏主题非演讲系

四、文本纠错任务

如飞机写错为灰机。常见的文本纠错技术主要有以下几种:

  • 基于规则的文本纠错技术:在数据库中加入常见错误的映射,如金字塔和金子塔,但耗时耗力;
  • 基于语言模型的文本纠错技术:如Kenlm模型
    • 错误检测:使用jieba中文分词器对句子进行切词,然后结合字粒度和词粒度两方面的疑似错误结果,形成疑似错误位置候选集。
    • 错误纠正:遍历所有的候选集并使用音似、形似词典替换错误位置的词,然后通过语言模型计算句子困惑度,最后比较并排序所有候选集结果,得到最优纠正词。
  • 基于MLM的文本纠错技术:bert就使用了Masked Language Model掩码语言模型(MLM)及Next Sentence Prediction下一句预测(NSP)两个任务,其中MLM任务中有15%*10%的Token会被替换为随机的其他词汇,迫使模型更多地依赖于上下文信息去预测Mask词汇,在一定程度上赋予了模型纠错能力。
    • 可以针对MLM, 将输入设计为错误词汇,输出为正确词汇,进fine tune,参考ACL2020的Soft-Masked BERT模型
  • 于NLG的文本纠错技术:mask方法只能用于输入和输出等长的情况,可以在bert后嵌入一层transformer decoder,将文本纠错,转为将错误的文本翻译为正确文本

pycorrector是一个文本纠错工具集,内置了KenLM、MacBERT、Transformer等多种文本纠错模型。

  • pycorrector的项目地址:https://github.com/shibing624/pycorrector
  • 一个基于MacBERT的线上Demo:https://huggingface.co/spaces/shibing624/pycorrector

  pycorrector不仅可以通过“import pycorrector”调用,也提供了Huggingface的预训练模型调用方式,以下是一个基于Huggingface的MacBERT4CSC调用样例。

# 1. 预训练模型
from transformers import BertTokenizer, BertForMaskedLM

# 载入模型
tokenizer = BertTokenizer.from_pretrained("shibing624/macbert4csc-base-chinese")
model = BertForMaskedLM.from_pretrained("shibing624/macbert4csc-base-chinese")

# text = "作为学生,一定要学习好学校的课乘!"
text = "重庆是一个好地方,今天天气好,适合除去玩"
input_ids = tokenizer([text], padding=True, return_tensors='pt')

# 生成结果文本
with torch.no_grad():
    outputs = model(**input_ids)
output_ids = torch.argmax(outputs.logits, dim=-1)
output_text = tokenizer.decode(output_ids[0], skip_special_tokens=True).replace(' ', '')

print("原始文本: ", text)
print("纠错文本: ", output_text)
# 原始文本:  重庆是一个好地方,今天天气好,适合除去玩
# 纠错文本:  重庆是一个好地方,今天天气好,适合出去玩

# 查看修改点
import operator
def get_errors(corrected_text, origin_text):
    sub_details = []
    for i, ori_char in enumerate(origin_text):
        if ori_char in [' ', '“', '”', '‘', '’', '琊', '\n', '…', '—', '擤']:
            # add unk word
            corrected_text = corrected_text[:i] + ori_char + corrected_text[i:]
            continue
        if i >= len(corrected_text):
            continue
        if ori_char != corrected_text[i]:
            if ori_char.lower() == corrected_text[i]:
                # pass english upper char
                corrected_text = corrected_text[:i] + ori_char + corrected_text[i + 1:]
                continue
            sub_details.append((ori_char, corrected_text[i], i, i + 1))
    sub_details = sorted(sub_details, key=operator.itemgetter(2))
    return corrected_text, sub_details

correct_text, details = get_errors(output_text[:len(text)], text)
print(details)

# 2. 基于OpenAI接口的文本纠错实验
def correct_text(text):
    content = f"请对以下文本进行文本纠错:\n{
      
      text}"
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo", 
        messages=[{
    
    "role": "user", "content": content}]
    )
    corrected_text = response.get("choices")[0].get("message").get("content")
    return corrected_text

# text = "作为学生,一定要学习好学校的课乘!"
text = "重庆是一个好地方,今天天气好,适合除去玩"
output_text = correct_text(text)
print("原始文本: ", text)
print("纠错文本: ", output_text)

五、机器翻译任务

  • 基于规则的方法
  • 基于统计的方法
  • 基于神经网络的方法
# 1. huggingface的pre-trained model
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

tokenizer = AutoTokenizer.from_pretrained("Helsinki-NLP/opus-mt-zh-en")
model = AutoModelForSeq2SeqLM.from_pretrained("Helsinki-NLP/opus-mt-zh-en")

# text = "作为学生,一定要学习好学校的课乘!"
text = "重庆是一个好地方, 今天适合出去玩"

inputs = tokenizer(text, return_tensors="pt", )
outputs = model.generate(inputs["input_ids"], max_length=40, num_beams=4, early_stopping=True)
translated_sentence = tokenizer.decode(outputs[0], skip_special_tokens=True)
print('原始文本: ', text)
print('翻译文本: ', translated_sentence)
#原始文本:  重庆是一个好地方, 今天适合出去玩
#翻译文本:  Chongqing is a good place to go. It's a good day.

# 2. 基于OpenAI接口的机器翻译实验
def translate_text(text):
    content = f"请将以下中文文本翻译成英文:\n{
      
      text}"
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo", 
        messages=[{
    
    "role": "user", "content": content}]
    )
    translated_text = response.get("choices")[0].get("message").get("content")
    return translated_text

text_to_translate = "重庆是一个好地方,今天天气好,适合出去玩"
translated_text = translate_text(text_to_translate)
print("原始文本: ", text_to_translate)
print("输出文本: ", translated_text)
# 原始文本:  重庆是一个好地方,今天天气好,适合出去玩
# 输出文本:  Chongqing is a great place. Today, the weather is good and it's suitable for going out and having fun.

上面的翻译结果对比:ChatGPT明显比Helsinki-NLP在中翻英上的效果更好,而不是简单的”直译“,用词相对更为丰富。

翻译数据:数据来源 https://github.com/LouisScorpio/datamining/tree/master/tensorflow-program/nlp/word2vec/dataset
下面翻译《哈利波特》并保存结果,第一册共有token数: 119873。GPT3的token限制数是4k,而GPT4是3万+,所以可以将每个段落当成一个文本块,每次翻译一段。

with open(file_path + "/哈利波特1-7英文原版.txt", "r", encoding='gbk') as f:
    text = f.read()
from transformers import GPT2Tokenizer

tokenizer = GPT2Tokenizer.from_pretrained("gpt2")  # GPT-2的tokenizer和GPT-3是一样的
token_counts = len(tokenizer.encode(text))
print('全书token数: ', token_counts)

# chatgpt的api调用价格是 1000 token 0.01美元,因此可以大致计算翻译一本书的价格
translate_cost = 0.01 / 1000 * token_counts
print(f'翻译全书约需{
      
      translate_cost}美元')

# 翻译全书约需115.14 rmb成本,有点贵了,我们试着只翻译第一本
end_idx = text.find('2.Harry Potter and The Chamber Of Secrets.txt')  # 第一册的结束位置
text = text[:end_idx]
print('第一册字符数: ', len(text))

tokenizer = GPT2Tokenizer.from_pretrained("gpt2") 
token_counts = len(tokenizer.encode(text))
print('第一册token数: ', token_counts)

translate_cost = 0.01 / 1000 * token_counts
print(f'翻译第一册约需{
      
      translate_cost}美元')

paragraphs = text.split('\n')
print('段落数: ', len(paragraphs))

ntokens = []
for paragraph in paragraphs:
    ntokens.append(len(tokenizer.encode(paragraph)))
print('最长段落的token数: ', max(ntokens))
# 段落数:  3038
# 最长段落的token数:  275

但是切分文本块太小会导致语境缺失,可以设置阈值为1000,即文本段落达到1k则新开一个,最后将翻译结果拼接:

def group_paragraphs(paragraphs, ntokens, max_len=1000):
    """
    合并短段落为文本块,用于丰富上下文语境,提升文本连贯性,并提升运算效率。
    :param paragraphs: 段落集合
    :param ntokens: token数集合
    :param max_len: 最大文本块token数
    :return: 组合好的文本块
    """
    batches = []
    cur_batch = ""
    cur_tokens = 0

    # 对于每个文本段落做处理
    for paragraph, ntoken in zip(paragraphs, ntokens):
        if ntoken + cur_tokens + 1 > max_len:  # '1' 指的是'\n'
            # 如果加入这段文本,总token数超过阈值,则开启新的文本块
            batches.append(cur_batch)
            cur_batch = paragraph
            cur_tokens = ntoken
        else:
            # 否则将段落插入文本块中
            cur_batch += "\n" + paragraph
            cur_tokens += (1 + ntoken)
    batches.append(cur_batch)  # 记录最后一个文本块
    return batches

batchs = group_paragraphs(paragraphs, ntokens)
print('文本块数: ', len(batchs))

new_tokens = []
for batch in batchs:
    new_tokens.append(len(tokenizer.encode(batch)))
print('最长文本块的token数: ', max(new_tokens))
# 文本块数:  125
# 最长文本块的token数:  1000

def translate_text(text):
    content = f"请将以下英文文本翻译成中文:\n{
      
      text}"
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo", 
        messages=[{
    
    "role": "user", "content": content}]
    )
    translated_text = response.get("choices")[0].get("message").get("content")
    return translated_text

print(translate_text(batchs[0]))
translated_batchs = []
for i in range(len(batchs)):
    batch_context = batchs[i]
    translated_batchs.append(translate_text(batch_context))

在这里插入图片描述

Reference

[1] fine tune tutorial
[2] 中文文本纠错算法整体介绍
[3] 中文文本纠错任务简介
[4] pycorrector介绍
[5] 长文本英翻中tutorial
[6] https://learnprompting.org/docs/intermediate/least_to_most
[7] https://github.com/datawhalechina/hugging-llm

猜你喜欢

转载自blog.csdn.net/qq_35812205/article/details/130372409