首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >如何微调GPT-2生成高质量的歌词

如何微调GPT-2生成高质量的歌词

作者头像
deephub
发布2021-05-18 12:24:16
发布2021-05-18 12:24:16
1.3K0
举报
文章被收录于专栏:DeepHub IMBADeepHub IMBA

自然语言生成(NLG)近年来取得了令人难以置信的进步。2019年初,OpenAI发布了GPT-2,一个巨大的预训练模型(1.5B参数),能够生成类人质量的文本。

生成预训Transformer2 (GPT-2),顾名思义,是基于Transformer 的。它使用注意力机制,这意味着它学会关注与上下文最相关的前一个单词,以便预测下一个单词。

本文的目的是向您展示如何调整GPT-2以根据提供的数据生成与上下文相关的文本。

作为一个例子,我将生成歌词。我们的想法是使用已经训练过的模型,根据我们的特定数据对其进行微调,然后根据模型观察到的结果,生成任何给定歌曲中应该遵循的内容。

准备数据

GPT-2本身可以生成高质量的文本。但是,如果您希望它对特定的上下文做得更好,则需要对特定的数据进行微调。在我的例子中,因为我想生成歌词,所以我将使用以下Kaggle数据集,它总共包含12500首流行摇滚歌曲的歌词,都是英文的。

让我们首先导入必要的库并准备数据。我建议在这个项目中使用谷歌Colab,因为对GPU的访问会让事情变得更快。

代码语言:javascript
复制
import pandas as pd
from transformers import GPT2LMHeadModel, GPT2Tokenizer
import numpy as np
import random
import torch
from torch.utils.data import Dataset, DataLoader
from transformers import GPT2Tokenizer, GPT2LMHeadModel, AdamW, get_linear_schedule_with_warmup
from tqdm import tqdm, trange
import torch.nn.functional as F
import csv

### Prepare data
lyrics = pd.read_csv('lyrics-data.csv')
lyrics = lyrics[lyrics['Idiom']=='ENGLISH']

#Only keep popular artists, with genre Rock/Pop and popularity high enough
artists = pd.read_csv('artists-data.csv')
artists = artists[(artists['Genre'].isin(['Rock'])) & (artists['Popularity']>5)]
df = lyrics.merge(artists[['Artist', 'Genre', 'Link']], left_on='ALink', right_on='Link', how='inner')
df = df.drop(columns=['ALink','SLink','Idiom','Link'])

#Drop the songs with lyrics too long (after more than 1024 tokens, does not work)
df = df[df['Lyric'].apply(lambda x: len(x.split(' ')) < 350)]

#Create a very small test set to compare generated text with the reality
test_set = df.sample(n = 200)
df = df.loc[~df.index.isin(test_set.index)]

#Reset the indexes
test_set = test_set.reset_index()
df = df.reset_index()

#For the test set only, keep last 20 words in a new column, then remove them from original column
test_set['True_end_lyrics'] = test_set['Lyric'].str.split().str[-20:].apply(' '.join)
test_set['Lyric'] = test_set['Lyric'].str.split().str[:-20].apply(' '.join)

从第26行和34-35行可以看到,我创建了一个小测试集,其中我删除了每首歌的最后20个单词。这将允许我将生成的文本与实际文本进行比较,以查看模型的执行情况。

创建数据集

为了在我们的数据上使用GPT-2,我们仍然需要做一些事情。我们需要对数据进行标记,这是将字符序列转换为标记的过程,即将句子分隔为单词。

我们还需要确保每首歌曲最多1024个令牌。

SongLyrics类将在训练期间为我们做这些,为我们原始数据帧中的每首歌曲。

代码语言:javascript
复制
class SongLyrics(Dataset):  
    def __init__(self, control_code, truncate=False, gpt2_type="gpt2", max_length=1024):

        self.tokenizer = GPT2Tokenizer.from_pretrained(gpt2_type)
        self.lyrics = []

        for row in df['Lyric']:
          self.lyrics.append(torch.tensor(
                self.tokenizer.encode(f"<|{control_code}|>{row[:max_length]}<|endoftext|>")
            ))               
        if truncate:
            self.lyrics = self.lyrics[:20000]
        self.lyrics_count = len(self.lyrics)
        
    def __len__(self):
        return self.lyrics_count

    def __getitem__(self, item):
        return self.lyrics[item]
    
dataset = SongLyrics(df['Lyric'], truncate=True, gpt2_type="gpt2")

模型的训练

我们现在可以导入预训练的GPT-2模型以及标记器。另外,就像我之前提到的,GPT-2是巨大的。很有可能,如果你试图在你的电脑上使用它,你会得到一堆CUDA出内存错误。

另一种方法是累积梯度,这个想法很简单,在调用优化来执行梯度下降的一步之前,它将几个操作的梯度求和。然后,它将总和除以累计的步数,以得到在训练样本上的平均损失。这意味着更少的计算。

代码语言:javascript
复制
#Get the tokenizer and model
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
model = GPT2LMHeadModel.from_pretrained('gpt2')

#Accumulated batch size (since GPT2 is so big)
def pack_tensor(new_tensor, packed_tensor, max_seq_len):
    if packed_tensor is None:
        return new_tensor, True, None
    if new_tensor.size()[1] + packed_tensor.size()[1] > max_seq_len:
        return packed_tensor, False, new_tensor
    else:
        packed_tensor = torch.cat([new_tensor, packed_tensor[:, 1:]], dim=1)
        return packed_tensor, True, None

现在,我们可以创建训练函数,使用我们所有的歌词来微调GPT-2,这样它就可以预测未来高质量的歌词。

代码语言:javascript
复制
def train(
    dataset, model, tokenizer,
    batch_size=16, epochs=5, lr=2e-5,
    max_seq_len=400, warmup_steps=200,
    gpt2_type="gpt2", output_dir=".", output_prefix="wreckgar",
    test_mode=False,save_model_on_epoch=False,
):
    acc_steps = 100
    device=torch.device("cuda")
    model = model.cuda()
    model.train()

    optimizer = AdamW(model.parameters(), lr=lr)
    scheduler = get_linear_schedule_with_warmup(
        optimizer, num_warmup_steps=warmup_steps, num_training_steps=-1
    )

    train_dataloader = DataLoader(dataset, batch_size=1, shuffle=True)
    loss=0
    accumulating_batch_count = 0
    input_tensor = None

    for epoch in range(epochs):

        print(f"Training epoch {epoch}")
        print(loss)
        for idx, entry in tqdm(enumerate(train_dataloader)):
            (input_tensor, carry_on, remainder) = pack_tensor(entry, input_tensor, 768)

            if carry_on and idx != len(train_dataloader) - 1:
                continue

            input_tensor = input_tensor.to(device)
            outputs = model(input_tensor, labels=input_tensor)
            loss = outputs[0]
            loss.backward()

            if (accumulating_batch_count % batch_size) == 0:
                optimizer.step()
                scheduler.step()
                optimizer.zero_grad()
                model.zero_grad()

            accumulating_batch_count += 1
            input_tensor = None
        if save_model_on_epoch:
            torch.save(
                model.state_dict(),
                os.path.join(output_dir, f"{output_prefix}-{epoch}.pt"),
            )
    return model

您可以随意使用各种超参数(批处理大小、学习率、epoch、优化器)。

最后,我们可以训练模型。

代码语言:javascript
复制
model = train(dataset, model, tokenizer)

使用 torch.save 和 torch.load,您还可以保存您训练过的模型以备将来使用。

生成歌词

通过使用以下两个函数,我们可以为测试数据集中的所有歌曲生成歌词。记得吗,我删掉了每首歌的最后20个词。现在,我们的模型将针对给定的一首歌,看看他的歌词,然后想出歌曲的结尾应该是什么。

代码语言:javascript
复制
def generate(
    model,
    tokenizer,
    prompt,
    entry_count=10,
    entry_length=30, #maximum number of words
    top_p=0.8,
    temperature=1.,
):
    model.eval()
    generated_num = 0
    generated_list = []

    filter_value = -float("Inf")

    with torch.no_grad():

        for entry_idx in trange(entry_count):

            entry_finished = False
            generated = torch.tensor(tokenizer.encode(prompt)).unsqueeze(0)

            for i in range(entry_length):
                outputs = model(generated, labels=generated)
                loss, logits = outputs[:2]
                logits = logits[:, -1, :] / (temperature if temperature > 0 else 1.0)

                sorted_logits, sorted_indices = torch.sort(logits, descending=True)
                cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)

                sorted_indices_to_remove = cumulative_probs > top_p
                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

                next_token = torch.multinomial(F.softmax(logits, dim=-1), num_samples=1)
                generated = torch.cat((generated, next_token), dim=1)

                if next_token in tokenizer.encode("<|endoftext|>"):
                    entry_finished = True

                if entry_finished:

                    generated_num = generated_num + 1

                    output_list = list(generated.squeeze().numpy())
                    output_text = tokenizer.decode(output_list)
                    generated_list.append(output_text)
                    break
            
            if not entry_finished:
              output_list = list(generated.squeeze().numpy())
              output_text = f"{tokenizer.decode(output_list)}<|endoftext|>" 
              generated_list.append(output_text)
                
    return generated_list

#Function to generate multiple sentences. Test data should be a dataframe
def text_generation(test_data):
  generated_lyrics = []
  for i in range(len(test_data)):
    x = generate(model.to('cpu'), tokenizer, test_data['Lyric'][i], entry_count=1)
    generated_lyrics.append(x)
  return generated_lyrics

#Run the functions to generate the lyrics
generated_lyrics = text_generation(test_set)

text_generation函数为整个测试做预处理工作

在第6行中,我们指定了一段的最大长度。我把它保留在30,但那是因为标点符号很重要,稍后我将删除最后几个单词,以确保生成结束于句末。

另外两个超参数值得一提:

Temperature (第8行)。它用于衡量生成给定单词的概率。因此,高温迫使模型做出更原始的预测,而较小的温度使模型不致偏离主题。

Top p过滤(第7行)。该模型将按降序对单词概率进行排序。然后,它会把这些概率加到p,同时去掉其他的词。这意味着模型只保留最相关的单词概率,但不只是保持最好的一个,因为多个单词可以适当给定一个序列。

在下面的代码中,我只是清理生成的文本,确保它在句子的末尾结束(而不是在句子中间),并将其存储在测试数据集中的新列中。

代码语言:javascript
复制
#Loop to keep only generated text and add it as a new column in the dataframe
my_generations=[]

for i in range(len(generated_lyrics)):
  a = test_set['Lyric'][i].split()[-30:] #Get the matching string we want (30 words)
  b = ' '.join(a)
  c = ' '.join(generated_lyrics[i]) #Get all that comes after the matching string
  my_generations.append(c.split(b)[-1])

test_set['Generated_lyrics'] = my_generations


#Finish the sentences when there is a point, remove after that
final=[]

for i in range(len(test_set)):
  to_remove = test_set['Generated_lyrics'][i].split('.')[-1]
  final.append(test_set['Generated_lyrics'][i].replace(to_remove,''))

test_set['Generated_lyrics'] = final

评估

有很多方法可以评估生成文本的质量。最流行的度量标准是BLEU。该算法根据生成的文本与现实的相似程度,输出0到1之间的分数。得分为1表示所有生成的单词都出现在真实文本中。

下面是对生成的歌词进行BLEU评分的代码。

代码语言:javascript
复制
#Using BLEU score to compare the real sentences with the generated ones
import statistics
from nltk.translate.bleu_score import sentence_bleu

scores=[]

for i in range(len(test_set)):
  reference = test_set['True_end_lyrics'][i]
  candidate = test_set['Generated_lyrics'][i]
  scores.append(sentence_bleu(reference, candidate))

statistics.mean(scores)

我们得到的BLEU平均分数是0.685,这已经很不错了。相比之下,未进行任何微调的GPT-2模型的BLEU得分为0.288。

然而,BLEU也有它的局限性。它最初是为机器翻译而创建的,只查看用于确定生成文本质量的词汇表。

这就是为什么我会对模型的性能做一个主观的评价。为了做到这一点,我创建了一个小型的web界面(使用Dash)。该代码可在我的Github仓库。

这个界面的工作方式是为应用程序提供一些输入字。然后,模型会用它来预测接下来的几段经文应该是什么。以下是一些结果示例。

红色的是GPT-2模型的预测,给定黑色的输入序列。你看,它已经成功地产生了有意义的诗句,并且尊重了之前的上下文!此外,它还能生成类似长度的句子,这对于保持歌曲的节奏非常重要。在这方面,输入文本中的标点符号在生成歌词时是绝对必要的。

结论

正如本文所示,通过将GPT-2微调到特定的数据,可以非常容易地生成与上下文相关的文本。

对于歌词生成,该模型可以生成既尊重上下文又尊重句子期望长度的歌词。当然,可以对模型进行改进。例如,我们可以强迫它产生押韵的诗句,这在写歌词时经常是必要的。

非常感谢你的阅读,所有代码可以在这找到:https://github.com/francoisstamant/lyrics-generation-with-GPT2

希望我能帮到你!

作者:François St-Amant

原文地址:https://towardsdatascience.com/how-to-fine-tune-gpt-2-for-text-generation-ae2ea53bc272

deephub翻译组

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DeepHub IMBA 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 自然语言生成(NLG)近年来取得了令人难以置信的进步。2019年初,OpenAI发布了GPT-2,一个巨大的预训练模型(1.5B参数),能够生成类人质量的文本。
    • 准备数据
    • 创建数据集
    • 模型的训练
    • 生成歌词
    • 评估
    • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档