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

2021-05-18 12:24:16 浏览数 (1)

自然语言生成(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翻译组

0 人点赞