0


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

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

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

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

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

准备数据

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

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

importpandasaspd
fromtransformersimportGPT2LMHeadModel, GPT2Tokenizer
importnumpyasnp
importrandom
importtorch
fromtorch.utils.dataimportDataset, DataLoader
fromtransformersimportGPT2Tokenizer, GPT2LMHeadModel, AdamW, get_linear_schedule_with_warmup
fromtqdmimporttqdm, trange
importtorch.nn.functionalasF
importcsv

### 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(lambdax: 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类将在训练期间为我们做这些,为我们原始数据帧中的每首歌曲。

classSongLyrics(Dataset):  
    def__init__(self, control_code, truncate=False, gpt2_type="gpt2", max_length=1024):

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

        forrowindf['Lyric']:
          self.lyrics.append(torch.tensor(
                self.tokenizer.encode(f"<|{control_code}|>{row[:max_length]}<|endoftext|>")
            ))               
        iftruncate:
            self.lyrics = self.lyrics[:20000]
        self.lyrics_count = len(self.lyrics)
        
    def__len__(self):
        returnself.lyrics_count

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

模型的训练

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

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

#Get the tokenizer and model
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
model = GPT2LMHeadModel.from_pretrained('gpt2')

#Accumulated batch size (since GPT2 is so big)
defpack_tensor(new_tensor, packed_tensor, max_seq_len):
    ifpacked_tensorisNone:
        returnnew_tensor, True, None
    ifnew_tensor.size()[1] +packed_tensor.size()[1] >max_seq_len:
        returnpacked_tensor, False, new_tensor
    else:
        packed_tensor = torch.cat([new_tensor, packed_tensor[:, 1:]], dim=1)
        returnpacked_tensor, True, None

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

deftrain(
    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

    forepochinrange(epochs):

        print(f"Training epoch {epoch}")
        print(loss)
        foridx, entryintqdm(enumerate(train_dataloader)):
            (input_tensor, carry_on, remainder) = pack_tensor(entry, input_tensor, 768)

            ifcarry_onandidx!= 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
        ifsave_model_on_epoch:
            torch.save(
                model.state_dict(),
                os.path.join(output_dir, f"{output_prefix}-{epoch}.pt"),
            )
    returnmodel

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

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

model = train(dataset, model, tokenizer)

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

生成歌词

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

defgenerate(
    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")

    withtorch.no_grad():

        forentry_idxintrange(entry_count):

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

            foriinrange(entry_length):
                outputs = model(generated, labels=generated)
                loss, logits = outputs[:2]
                logits = logits[:, -1, :] / (temperatureiftemperature>0else1.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)

                ifnext_tokenintokenizer.encode("<|endoftext|>"):
                    entry_finished = True

                ifentry_finished:

                    generated_num = generated_num+1

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

#Function to generate multiple sentences. Test data should be a dataframe
deftext_generation(test_data):
  generated_lyrics = []
  foriinrange(len(test_data)):
    x = generate(model.to('cpu'), tokenizer, test_data['Lyric'][i], entry_count=1)
    generated_lyrics.append(x)
  returngenerated_lyrics

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

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

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

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

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

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

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

#Loop to keep only generated text and add it as a new column in the dataframe
my_generations=[]

foriinrange(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=[]

foriinrange(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评分的代码。

#Using BLEU score to compare the real sentences with the generated ones
importstatistics
fromnltk.translate.bleu_scoreimportsentence_bleu

scores=[]

foriinrange(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翻译组

标签:

“如何微调GPT-2生成高质量的歌词”的评论:

还没有评论