使用C#和深度学习编写十亿首歌曲

在本文中,我将解释如何构建一个ASP.NET Core网站 ,该网站使用AI只需单击一下按钮即可生成独特的歌曲歌词,并允许用户投票选出最佳歌曲。

神经网络


大约2.5个月前, OpenAI发表了一篇博客文章 ,其中展示了几乎不可能的事情:一种深度学习模型,可以撰写文章,与人类撰写的文章没有区别。 它产生的文本令人印象深刻,以至于我不得不检查日历以确保它不是愚人节的玩笑(请注意那是2月,西雅图被雪覆盖了)。

GPT-2文字范例


到目前为止,他们还没有发布拥有超过10亿个参数的最大神经网络(这是一个颇有争议的决定),但是他们在MIT许可下在GitHub上开源了一个较小的117M参数版本。 该模型的名称非常难忘: GPT-2


因此,大约一个月前,当我试图考虑使用TensorFlow可以完成什么样的出色项目时,该网络便成为了起点。 如果它已经可以生成英文文本,那么在有足够大的数据集的情况下,对其进行微调以生成歌曲歌词应该不会太困难。


GPT-2如何工作?


深度学习研究取得了几项重要成就,使GPT-2成为可能:


自我监督学习


在我撰写本文的第一版后几天,这项技术就由Yan LeCunn最终确定。 这是一项非常强大的技术,几乎可以应用于任何类型的现实世界数据。 为了训练GPT-2,OpenAI从各种来源收集了数十GB的文章 ,这些文章在Reddit上得到了好评。


按照惯例,必须要有人来阅读所有这些文章,例如,将它们标记为“阳性”或“阴性”。 然后他们将以监督的方式教一个神经网络,以与人类相同的方式对这些文章进行分类。


RECAPTCHA:找到路牌


这里的新想法是创建一个深度学习模型,该模型对数据有高层次的了解,您只需破坏数据,然后指定模型来恢复原始数据即可。 这使模型能够理解数据片段及其周围上下文之间的联系。


让我们以文本为例。 GPT-2提取了原始文本的样本,选择了要破坏的令牌的15%,然后屏蔽了80%的令牌(例如,用特殊的屏蔽令牌替换,通常是___),用字典中的其他随机令牌替换了10%,并保持剩余的10%完整。 我把一个球扔了,它掉到了草地上 。 腐败之后,它看起来可能像这样: 我把球扔了,然后___扔到草地上 。 用外行人的话来说,要使网络恢复原始状态,需要了解到,抛出的东西很可能会掉下来,而在这种情况下,汽车球是非常不常见的。


像这样训练过的模型可以很好地生成/完成部分数据,但是通过学习模型(作为内层的输出),可以在其上添加一层或两层并进行微调,从而将其用于其他目的。以常规方式在实际的, 较小的 ,人工标记的数据集上添加新的最后一层


自我注意力稀疏


GPT-2使用了一种称为稀疏自我注意的东西。 从本质上讲,它是一种技术,它使神经网络能够处理大量输入,从而比其他部分更专注于某些部分。 网络会在训练过程中了解应该“看”的地方。 注意机制在此博客文章中有更好的解释。


本节标题中的稀疏部分是指对注意力机制可以选择的输入段的限制。 最初的注意力可以从整个输入中选择。 这导致其权重矩阵为O(input_size ^ 2),随输入大小的增长非常快。 稀疏的关注通常以某种方式限制了这一点。 有关更多信息,请参阅一篇OpenAI博客文章


GPT-2中的注意力是多头的 。 想象一下,您可能有另外一两只眼睛可以用来检查上一段的内容,而不必停止阅读当前的内容。


还有更多


剩余连接字节对编码 ,下一句预测等。


移植GPT-2(通常转换为Python)


原始的模型代码在Python中,但是我是C#家伙。 幸运的是,源代码具有很好的可读性,并且症结仅存在于 5个文件中,总共可能有500行。 因此,我创建了一个新的.NET Standard项目,安装了Gradient (用于.NET的TensorFlow绑定),并将这些文件逐行转换为C#。 我花了大约2个小时。 代码中剩下的唯一pythonic事情是使用了pip(Python最常用的包管理器)中的Python regex模块,因为我不想浪费时间学习Python正则表达式的复杂性( 好像还不够)已经处理.NET了 )。


通常,转换包括定义相似的类,添加类型以及将Python 列表理解重写为相应的LINQ构造。 除了标准库中的LINQ,我还使用了MoreLinq ,它稍微扩展了LINQ的功能,例如:


bs = list(range(ord("!"), ord("~")+1)) + list(range(ord("¡"), ord("¬")+1)) + list(range(ord(""), ord("ÿ")+1)) 

变成:


 var bs = Range('!', '~' - '!' + 1) .Concat(Range('¡', '¬' -'¡' + 1)) .Concat(Range('', 'ÿ' - '' + 1)) .ToList(); 

我必须解决的另一件事是Python处理范围的方式与即将到来的C#8中新的Ranges和indexs功能之间的差异,我在调试初始运行时发现了这一点:在C#8中,范围的末尾是包含端点的 ,而在Python中则是排他的 (要在Python中包含最后一个元素,您必须省略..表达式的右侧)。


计算机科学中有两件难事:缓存无效,命名和一次错误。

不幸的是,原始的源代码降落不包含任何培训甚至微调代码,但Neil Shepperd在他的GitHub上提供了一个简单的微调器,我也必须移植。 无论如何,这种努力的结果是可以用于GPT-2C#代码现在已成为Gradient Samples存储库的一部分。


移植的目的有两个方面:移植后,一个人就可以在他最喜欢的C#IDE中使用模型代码 ,并表明, 现在可以在定制中使用最新的深度学习模型了。 .NET在发布后不久(在GPT-2的代码下降与“十亿首歌曲”的第一发布之间-仅仅一个月的时间)内进行了开发。


微调歌曲歌词


有几种方法可以获取大量的歌词。 您可以使用HTML解析器抓取托管它的Internet网站之一,将其从您的卡拉OK集合中拉出,或mp3文件。 幸运的是,有人为我们做到了。 我在Kaggle上找到了很多准备好的歌词数据集。 “ 您听到的每首歌 ”似乎都是最大的。 在尝试将GPT-2调整为它时,我遇到了两个问题。


CSV阅读


是的,您没有看错, CSV分析是一个问题 。 最初,我想使用ML.NET(用于机器学习的新Microsoft库)读取文件。 但是,浏览完文档并进行设置后,我意识到它无法正确处理歌曲中的换行符。 不管我做什么,在经过数百个示例之后,它还是很挣扎,并开始将歌词与标题和艺术家混在一起。


因此,我不得不求助于一个较低级的库,该库以前具有更好的经验: CsvHelper 。 它提供了类似DataReader的界面。 您可以在此处使用它查看代码。 本质上,您打开文件,配置CsvReader ,然后将对.Read ()的调用与对.GetField(fieldName)的调用交错。


短歌


与OpenAI使用的原始数据集中的普通文章相比,大多数歌曲都简短。 GPT-2训练对大块文本更有效,因此我不得不将几首歌曲捆绑成连续的文本块,以将其输入给训练者。 OpenAI似乎也使用了这种技术,因此它们有一个特殊的标记<| endoftext |> ,它充当块内完整文本之间的分隔符,并兼作起始标记。 我将歌曲捆绑在一起,直到达到一定数量的令牌,然后将整个块返回到训练数据中。 相关代码在这里


调整的硬件要求


甚至较小的GPT-2版本也很大。 使用12GB的GPU RAM,只能将批处理大小设置为2 (例如,一次训练两个块,较大的批处理大小可以提高GPU性能和训练结果)。 3将在CUDA中耗尽内存 。 并且花了半天时间将其调整到我的V100上所需的性能。 好处是您可以看到进度,因为训练代码会不时地输出一些生成的样本,这些样本以简单的纯文本开头,并且随着训练的进行越来越像歌曲的歌词。


我没有尝试过,但是在CPU上的培训可能会非常慢


预先调整的模型


在准备此博客文章时,我意识到最好不要强迫每个人都花数小时来微调歌词 模型 ,因此我在Billion Songs资料库中 发布了 预先调好的 音乐 。 如果您只是尝试运行“十亿首歌曲”,则无需手动下载。 默认情况下,该项目将为您完成。


在我身上玩HAL9000的半训练模型
我发誓,我应该写信给你
我发誓,我发誓
你现在毁了它,我希望你能做到
我希望你的梦想,我希望你的梦想,我希望你的梦想,我希望你的梦想,我希望你的梦想
关于
我要去的 我要走了 我要走了 我要去,我要去,我要去,我要去,我要去,我要去,我要去,
我要去,我要去,我要去...

建立一个网站


好啦 看起来就像一首歌,现在让我们建立一个网站!


由于我不打算提供任何API,因此我选择了Razor Pages模板而不是MVC。 我也启用了授权功能,因为我们将允许用户投票选出最佳歌词并获得前10名的图表。


赶上MVP,我继续创建了Song.cshtml网页,目前它的目标是简单地调用GPT-2并获得随机歌曲。 该页面的布局很简单,基本上由歌曲及其标题组成:


 @page "/song/{id}" @model BillionSongs.Pages.SongModel</p> @{ ViewData["Title"] = @Model.Song.Title ?? "Untitled"; } <article style="text-align: center"> <h3>@(Model.Song.Title ?? "Untitled")</h3> <pre>@Model.Song.Lyrics</pre> </article> 


现在,因为我喜欢代码可重用,所以我创建了一个接口,该接口可让我稍后插入不同的歌词生成器,然后将其通过ASP.NET注入SongModel中。


 interface ILyricsGenerator { Task<string> GenerateLyrics(uint song, CancellationToken cancellation); } 

现在省略歌曲标题,我们要做的就是在Startup中注册Gpt2LyricsGenerator配置Services并从SongModel中调用它。 因此,让我们开始使用生成器。 我们需要确保的第一件事是


可重复的歌词生成


因为我在标题中做了一个大胆的声明,那就是说将会有超过10亿首歌曲,所以甚至不用考虑生成和存储所有歌曲。 首先,没有任何元数据,这将占用超过1TB的磁盘空间。 其次,在我的网络上大约需要3分钟才能生成一首新歌曲,因此要永久生成所有歌曲。 而且我希望能够通过在需要时切换到Int64将这十亿变成一百万分之一! 试想一下,如果每增加一千亿首歌曲,我们就能赚一分钱? 那将超过世界目前每年的GDP!


相反,我们需要做的是确保GPT-2一次又一次生成同一首歌曲,并给出我在路由中指定的id 。 为此,TensorFlow可以随时通过tf.set_random_seed函数(如tf.set_random_seed(songNumber))设置其内部数字生成器的种子。 然后,我只想调用Gpt2Sampler.SampleSequence ,以获取编码的歌曲文本,对其进行解码并返回结果,从而完成Gpt2LyricsGenerator


不幸的是,第一次尝试并没有按预期工作。 每次点击刷新按钮时,页面上都会返回一个新的唯一文本。 经过大量的调试后,我终于发现TensorFlow 1.X在可重复性方面存在重大问题:许多操作具有内部状态,不受set_random_seed的影响,并且很难重置。


重新初始化模型变量有助于解决该问题,但也意味着必须重新创建会话,并且每次调用都必须重新加载模型权重。 重新加载该大小的会话会导致巨大的内存泄漏。 为了避免在TensorFlow C ++源代码中查找其原因,我决定不在进程内进行文本生成,而是决定使用Process.Start生成一个新进程,在此生成文本,然后从标准输出中读取文本。 在稳定TensorFlow中的重置模型状态的方法之前,这就是要走的路。


因此,我最终得到了两个类: Gpt2LyricsGenerator ,它通过使用命令行参数(包括歌曲ID)产生BillionSongs.exe的新实例,从上面实现ILyricsGenerator ,并最终实例化了Gpt2TextGenerator ,该实例实际上调用了GPT-2来生成歌词,以及简单地打印出来。


现在刷新页面总是给我相同的文本。


处理3分钟的时间来生成歌曲


这将是多么可怕的用户体验! 您访问一个网站,单击“制作新歌曲”,并且3分钟内完全没有任何反应,而我的nettop花时间来生成您请求的歌曲歌词。


我从多个层面解决了这个问题:


预先生成歌曲


如上所述,您无法预先生成所有歌曲,也无法从数据库中提供它们。 而且您不能只按需生成,因为那样会很慢。 那你该怎么办?


很简单! 由于用户观看新歌曲的主要方式是单击“随机”按钮,因此我们预先预先生成很多歌曲,将它们放入ConcurrentQueue中 ,然后让“随机”从中弹出歌曲。 当访问者数量很少时,服务器将在他们之间花费一些时间来生成一些歌曲,然后可以方便地访问它们。


我使用的另一个技巧是使该队列循环几次,以便许多用户可以看到同一首预生成的歌曲。 只需在RAM使用情况和用户单击“随机”以查看他之前看到的内容之间保持平衡即可。 我只是选择了50,000首歌曲作为一个合理的数量,这只占用了50MB的RAM,同时提供了大量的点击次数。


我在类PregenicSongProvider中实现了该功能: IRandomSongProvider (接口已插入代码中,负责处理“随机”按钮)。


快取


预生成的歌曲被缓存到内存中,但是我还将HTTP 缓存标头设置为public,以允许浏览器使用,而CDN(我使用CloudFlare)则将其缓存,以避免受到用户涌入的打击。


 [ResponseCache(VaryByHeader = "User-Agent", Duration = 3*60*60)] public class SongModel: PageModel { … } 

返回流行歌曲


以这种方式微调的GPT-2生成的大多数歌曲,即使不是基本的,也相当乏味。 为了使“随机”的点击更具吸引力,我添加了25%的概率,即您会获得一些歌曲,而不是完全随机的歌曲,该歌曲先前已被其他用户赞成。 除了增加参与度之外,它还增加了您请求存储在CDN或内存中的歌曲的机会。


上面的所有技巧都使用Startup类中的ASP.NET依赖项注入连接在一起。


投票


投票实施没有什么特别之处。 有SongVoteCache ,可以使计数保持最新。 并在歌曲页面上托管一个带投票按钮iframe ,该页面允许页面的基本部分-标题和歌词被缓存,而投票计数和登录状态将在以后加载。


最终结果


生成的歌曲样本


现在已冻结在我的网上桌面上运行的演示版本 ,并以CloudFlare为首(给它一点放松,它的Core i3),并将其转移到Azure应用服务免费层。


GitHub存储库 ,包含源代码以及运行网站和调整模型的说明。


未来计划/练习


产生标题


GPT-2非常易于微调。 可以通过在数据集的每个歌词样本前添加前缀或后缀,例如<| startoftitle |>以及同一数据集的标题,使它生成歌曲标题。


或者,可以允许用户对标题进行建议和/或投票。


产生音乐


在开发十亿首歌曲的过程中,我认为下载一堆MIDI文件(这是一种老式的音乐格式,比mp3格式更接近文本,并训练它们上的GPT-2)会很酷。 。 其中一些文件甚至嵌入了文本,因此可以产生卡拉OK


我知道以这种方式进行音乐创作是很有可能的,因为昨天OpenAI实际上 在他们的博客中 发布了该想法的实现 。 但是,万岁, 他们没有卡拉OK! 我发现,可以出于此目的刮取http://www.midi-karaoke.info


NET的梯度aka TensorFlow


请查看我们的博客以获取任何更新。

Source: https://habr.com/ru/post/zh-CN453232/


All Articles