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

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

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

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

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

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

准备数据

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

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

importpandasaspdfromtransformersimportGPT2LMHeadModel, GPT2Tokenizerimportnumpyasnpimportrandomimporttorchfromtorch.utils.dataimportDataset, DataLoaderfromtransformersimportGPT2Tokenizer, GPT2LMHeadModel, AdamW, get_linear_schedule_with_warmupfromtqdmimporttqdm, trangeimporttorch.nn.functionalasFimportcsv###Preparedatalyrics=pd.read_csv('lyrics-data.csv')
lyrics=lyrics[lyrics['Idiom']=='ENGLISH']
#Onlykeeppopularartists, withgenreRock/Popandpopularityhighenoughartists=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'])
#Dropthesongswithlyricstoolong (aftermorethan1024tokens, doesnotwork)
df=df[df['Lyric'].apply(lambdax: len(x.split(' ')) <350)]
#Createaverysmalltestsettocomparegeneratedtextwiththerealitytest_set=df.sample(n=200)
df=df.loc[~df.index.isin(test_set.index)]
#Resettheindexestest_set=test_set.reset_index()
df=df.reset_index()
#Forthetestsetonly, keeplast20wordsinanewcolumn, thenremovethemfromoriginalcolumntest_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_countdef__getitem__(self, item):
returnself.lyrics[item]
dataset=SongLyrics(df['Lyric'], truncate=True, gpt2_type="gpt2")

模型的训练

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

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

#Getthetokenizerandmodeltokenizer=GPT2Tokenizer.from_pretrained('gpt2')
model=GPT2LMHeadModel.from_pretrained('gpt2')
#Accumulatedbatchsize (sinceGPT2issobig)
defpack_tensor(new_tensor, packed_tensor, max_seq_len):
ifpacked_tensorisNone:
returnnew_tensor, True, Noneifnew_tensor.size()[1] +packed_tensor.size()[1] >max_seq_len:
returnpacked_tensor, False, new_tensorelse:
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=100device=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=0accumulating_batch_count=0input_tensor=Noneforepochinrange(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:
continueinput_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+=1input_tensor=Noneifsave_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, #maximumnumberofwordstop_p=0.8,
temperature=1.,
):
model.eval()
generated_num=0generated_list= []
filter_value=-float("Inf")
withtorch.no_grad():
forentry_idxintrange(entry_count):
entry_finished=Falsegenerated=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_psorted_indices_to_remove[..., 1:] =sorted_indices_to_remove[
                  ..., :-1              ].clone()
sorted_indices_to_remove[..., 0] =0indices_to_remove=sorted_indices[sorted_indices_to_remove]
logits[:, indices_to_remove] =filter_valuenext_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=Trueifentry_finished:
generated_num=generated_num+1output_list=list(generated.squeeze().numpy())
output_text=tokenizer.decode(output_list)
generated_list.append(output_text)
breakifnotentry_finished:
output_list=list(generated.squeeze().numpy())
output_text=f"{tokenizer.decode(output_list)}<|endoftext|>"generated_list.append(output_text)
returngenerated_list#Functiontogeneratemultiplesentences. Testdatashouldbeadataframedeftext_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#Runthefunctionstogeneratethelyricsgenerated_lyrics=text_generation(test_set)

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

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

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

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

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

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

#Looptokeeponlygeneratedtextandadditasanewcolumninthedataframemy_generations=[]
foriinrange(len(generated_lyrics)):
a=test_set['Lyric'][i].split()[-30:] #Getthematchingstringwewant (30words)
b=' '.join(a)
c=' '.join(generated_lyrics[i]) #Getallthatcomesafterthematchingstringmy_generations.append(c.split(b)[-1])
test_set['Generated_lyrics'] =my_generations#Finishthesentenceswhenthereisapoint, removeafterthatfinal=[]
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评分的代码。

#UsingBLEUscoretocomparetherealsentenceswiththegeneratedonesimportstatisticsfromnltk.translate.bleu_scoreimportsentence_bleuscores=[]
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

希望我能帮到你!

目录
相关文章
|
自然语言处理 数据格式
【DSW Gallery】基于ModelScope的中文GPT-3模型(1.3B)的微调训练
本文基于ModelScope,以GPT-3(1.3B)为例介绍如何使用ModelScope-GPT3进行续写训练与输入输出形式的训练,训练方式不需要额外指定,训练数据集仅包含 src_txt 时会进行续写训练,同时包含 src_txt 和 tgt_txt 时会进行输入输出形式的训练。
【DSW Gallery】基于ModelScope的中文GPT-3模型(1.3B)的微调训练
|
23天前
|
人工智能 自然语言处理 API
【极客技术】真假GPT-4?微调 Llama 2 以替代 GPT-3.5/4 已然可行!
【极客技术】真假GPT-4?微调 Llama 2 以替代 GPT-3.5/4 已然可行!
40 0
|
9月前
|
数据采集 JSON API
使用GPT-4生成训练数据微调GPT-3.5 RAG管道
OpenAI在2023年8月22日宣布,现在可以对GPT-3.5 Turbo进行微调了。也就是说,我们可以自定义自己的模型了。然后LlamaIndex就发布了0.8.7版本,集成了微调OpenAI gpt-3.5 turbo的功能
222 1
|
12月前
|
并行计算 PyTorch 算法框架/工具
GPT3-2.7B 原生支持多机多卡微调
GPT3-2.7B 原生支持多机多卡微调
149 0
|
数据采集 机器学习/深度学习 自然语言处理
首次:微软用GPT-4做大模型指令微调,新任务零样本性能再提升
首次:微软用GPT-4做大模型指令微调,新任务零样本性能再提升
187 0
|
数据采集 存储 JSON
试下微调GPT-3做一个心理问答机器人
试下微调GPT-3做一个心理问答机器人 前言 最近,笔者做的一个小程序还差最后一个心理问答的功能,主要功能基本就完成了。我想偷个懒,那就调用别人的API吧,正好GPT-3非常火,那就试试?
319 0
|
机器学习/深度学习 自然语言处理 Python
GPT-3解数学题准确率升至92.5%!微软提出MathPrompter,无需微调即可打造「理科」语言模型
GPT-3解数学题准确率升至92.5%!微软提出MathPrompter,无需微调即可打造「理科」语言模型
232 0
|
机器学习/深度学习 自然语言处理 算法
清北微软深挖GPT,把上下文学习整明白了!和微调基本一致,只是参数没变而已
清北微软深挖GPT,把上下文学习整明白了!和微调基本一致,只是参数没变而已
165 0
|
机器学习/深度学习 人工智能 自然语言处理
NLP实战:对GPT-2进行微调以生成创意的域名
NLP实战:对GPT-2进行微调以生成创意的域名
222 0
NLP实战:对GPT-2进行微调以生成创意的域名
|
机器学习/深度学习 人工智能 自然语言处理
莆田版GPT-3开源:同等复现预训练模型GPT Neo,可在Colab上完成微调
GPT-3开源了?Eleuther AI推出的名为GPT-Neo的开源项目:公开发布的GPT-3同等复现预训练模型(1.3B & 2.7B),可在Colab上完成微调。
694 0
莆田版GPT-3开源:同等复现预训练模型GPT Neo,可在Colab上完成微调