自然语言生成(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
希望我能帮到你!