Escrevendo bilhões de músicas com C # e Deep Learning

Neste artigo, explicarei como criar um site ASP.NET Core , que usa a IA para gerar letras de músicas exclusivas com um clique de um botão e permite que os usuários votem nas melhores músicas.

A rede neural


Cerca de 2,5 meses atrás, a OpenAI publicou um post no blog , onde eles demonstraram quase impossível: um modelo de aprendizado profundo, que pode escrever artigos, indistinguíveis dos escritos por seres humanos. O texto que ele gerou foi tão impressionante que eu tive que verificar o calendário para garantir que não era uma piada de tolo de abril (lembre-se de que era fevereiro e Seattle estava coberta de neve).

Amostra de texto GPT-2


Eles não lançaram a maior rede neural com mais de 1 bilhão de parâmetros que construíram até hoje (uma decisão muito controversa), mas forneceram uma versão menor de 117 milhões de parâmetros no GitHub sob licença MIT. O modelo tem um nome muito memorável: GPT-2 .


Então, cerca de um mês atrás, quando eu estava tentando pensar em qual projeto legal eu poderia fazer com o TensorFlow, essa rede se tornou o ponto de partida. Se ele já poderia gerar texto em inglês, não seria muito difícil ajustá- lo para gerar letras de músicas, se houver um conjunto de dados suficientemente grande.


Como o GPT-2 funciona?


Existem várias conquistas importantes na pesquisa de aprendizado profundo, que tornaram possível o GPT-2:


Aprendizagem auto-supervisionada


Essa técnica recebeu seu nome finalizado por Yan LeCunn apenas alguns dias depois que eu escrevi a primeira versão deste artigo. É uma técnica muito poderosa, que pode ser aplicada a basicamente qualquer tipo de dados do mundo real. Para treinar o GPT-2, a OpenAI coletou dezenas de gigabytes de artigos de várias fontes, que foram votados no Reddit.


Convencionalmente, seria necessário um ser humano para percorrer todos esses artigos e, por exemplo, marcá-los como "positivos" ou "negativos". Então eles ensinavam uma rede neural de maneira supervisionada a classificar esses artigos da mesma maneira que um humano.


RECAPTCHA: encontre sinais de stree


A nova idéia aqui é que, para criar um modelo de aprendizado profundo, que tenha um alto nível de entendimento de seus dados, você simplesmente corrompe os dados e encarrega o modelo de restaurar o original. Isso faz o modelo entender as conexões entre partes de dados e seus contextos circundantes.


Vamos tomar o texto como um exemplo. O GPT-2 pega uma amostra do texto original, seleciona 15% dos tokens para serem corrompidos e oculta 80% deles (por exemplo, substitui por um token de máscara especial, geralmente ___), substitui 10% por outro token aleatório do dicionário, e mantém os 10% restantes intactos. Pegue eu joguei uma bola, e ela caiu na grama . Depois da corrupção, pode ser assim: eu joguei a bola do carro, e ela ___ na grama . Em termos leigos, para que a rede restaure o original, é preciso aprender que algo jogado provavelmente cairá e que a bola de carro é algo muito incomum no contexto.


Um modelo treinado assim é bom em gerar / completar dados parciais, mas os recursos de alto nível que aprendeu (como saídas das camadas internas) podem ser usados ​​para outros fins, adicionando uma ou duas camadas sobre elas e ajustando-as somente essa nova última camada em um conjunto de dados real, menor e marcado por humanos, de maneira convencional.


Pouca atenção pessoal


O GPT-2 usa algo chamado pouca atenção pessoal. Em essência, é uma técnica que permite que uma grande quantidade de entradas de processamento de redes neurais se concentre mais em algumas partes dela do que em outras. E a rede aprende onde deve "procurar" durante o treinamento. O mecanismo de atenção é melhor explicado nesta postagem do blog .


A parte esparsa no título desta seção refere-se a uma restrição sobre quais segmentos de entrada o mecanismo de atenção pode escolher. A atenção inicial pode escolher entre toda a entrada. Isso fez com que sua matriz de pesos fosse O (input_size ^ 2), que cresce muito rapidamente com o tamanho da entrada. Pouca atenção normalmente restringe isso de alguma maneira. Para mais informações, dê uma olhada em outro post do OpenAI .


A atenção no GPT-2 é múltipla . Imagine que você poderia ter um olho ou dois adicionais para verificar o que estava no último parágrafo sem parar de ler o atual.


Muito mais


Conexões residuais , codificação de pares de bytes , previsão de próxima sentença e muito mais.


Portando GPT-2 (e convertendo Python em geral)


O código do modelo original está em Python, mas eu sou um cara de C #. Felizmente, o código fonte é bastante legível e o ponto crucial está em apenas 5 arquivos, talvez 500 linhas no total. Então, criei um novo projeto .NET Standard, instalei o Gradient (uma ligação do TensorFlow para .NET) e converti esses arquivos linha por linha em C #. Isso me levou cerca de 2 horas. A única coisa pitônica que restava no código era o uso do módulo regex Python do pip (o gerenciador de pacotes mais usado para o Python), pois eu não queria perder tempo aprendendo os meandros das expressões regulares do Python ( como se isso não bastasse). para lidar com os .NET já ).


Principalmente, a conversão consistia em definir classes semelhantes, adicionar tipos e reescrever as compreensões da lista Python nas construções LINQ correspondentes. Além do LINQ da biblioteca padrão, usei o MoreLinq , que expande levemente o que o LINQ pode fazer. Por exemplo:


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

se transformou em:


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

Outra coisa com a qual tive que lutar foi com uma discrepância entre a maneira como o Python lida com os intervalos e os novos intervalos e índices no próximo C # 8, que descobri ao depurar minhas execuções iniciais: no C # 8, o final do intervalo é inclusivo , enquanto no Python é exclusivo (para incluir o último elemento no Python, você deve omitir o lado direito da .. expressão).


Há duas coisas difíceis na ciência da computação: invalidação de cache, nomeação de coisas e erros pontuais.

Infelizmente, a queda da fonte original não continha nenhum treinamento ou código de ajuste fino, mas Neil Shepperd forneceu um ajuste fino simples em seu GitHub , que eu também precisei portar. De qualquer forma, o resultado desse esforço é um código C # , que pode ser usado para jogar com o GPT-2 , agora faz parte do repositório Gradient Samples .


O objetivo do exercício de portabilidade é duplo: após a portabilidade, é possível brincar com o código do modelo em seu C # IDE favorito e mostrar que agora é possível obter modelos avançados de aprendizado profundo trabalhando de maneira personalizada. Projetos .NET logo após o lançamento (entre a queda de código do GPT-2 e o primeiro lançamento do Billion Songs - pouco mais de um mês).


Ajuste fino das letras das músicas


Existem várias maneiras de obter um grande conjunto de letras de músicas. Você pode raspar um dos sites da Internet que o hospeda com um analisador HTML, retirá-lo da sua coleção de karaokê ou arquivos mp3. Felizmente, alguém fez isso por nós. Encontrei alguns conjuntos de dados de letras preparadas no Kaggle . " Toda música que você ouviu " parecia ser a maior. Tentando ajustar o GPT-2, enfrentei dois problemas.


Leitura CSV


Sim, você leu corretamente, a análise de CSV foi um problema . Inicialmente, eu queria usar o ML.NET, a nova biblioteca da Microsoft para aprendizado de máquina, para ler o arquivo. No entanto, depois de percorrer a documentação e configurá-la, percebi que ela falhou ao processar quebras de linha nas músicas corretamente. Não importa o que eu fiz, ele lutou após algumas centenas de exemplos e começou a misturar trechos de letras com títulos e artistas.


Então, tive que recorrer a uma biblioteca de nível inferior, com a qual eu já tinha uma experiência melhor: CsvHelper . Ele fornece uma interface semelhante ao DataReader . Você pode ver o código usando-o aqui . Essencialmente, você abre um arquivo, configura um CsvReader e intercala a chamada em .Read () com as chamadas em .GetField (fieldName) .


Músicas curtas


A maioria das músicas é curta em comparação com um artigo médio no conjunto de dados original usado pelo OpenAI. O treinamento do GPT-2 é mais eficiente em grandes partes de texto, então tive que agrupar várias músicas em partes de texto contínuas para alimentá-las ao treinador. O OpenAI também parecia usar essa técnica; portanto, eles tinham um token especial <| endoftext |> , que atua como um separador entre textos completos em um pedaço e funciona como o token inicial. Agrupei músicas até que um certo número de fichas fosse alcançado e, em seguida, retornei toda a parte para incluir nos dados do treinamento. O código relevante está aqui .


Requisitos de hardware para ajuste


Mesmo a versão menor do GPT-2 é grande. Com 12 GB de RAM da GPU, eu podia definir o tamanho do lote para 2 (por exemplo, treinar em dois blocos ao mesmo tempo, tamanhos maiores de lote melhoram o desempenho da GPU e os resultados do treinamento). 3 perderia memória no CUDA. E demorou meio dia para ajustá-lo ao desempenho desejado no meu V100. O bônus é que você pode ver o progresso, pois de vez em quando o código de treinamento gera alguns exemplos gerados, que começam como texto simples e simples e se parecem cada vez mais com letras de músicas à medida que o treinamento avança.


Eu não tentei, mas o treinamento na CPU provavelmente será muito lento .


Modelo pré-sintonizado


Enquanto eu preparava este post, percebi que seria melhor não forçar todo mundo a passar horas ajustando o modelo das letras , então eu lancei um pré-ajustado no repositório do Billion Songs . Se você está apenas tentando executar bilhões de músicas, nem precisa fazer o download manualmente. O projeto fará isso por você por padrão.


modelo semi-treinado jogando HAL9000 em mim
Eu juro que devo escrever para você
E eu juro, juro
Você estragou tudo agora, espero que você faça
E eu espero que o seu sonho, eu espero que você sonhe, eu espero que você sonhe Eu espero que você sonhe Eu espero que você sonhe com
Sobre
o que eu vou Eu vou Eu vou Eu vou, vou, vou, vou, vou, vou, vou,
Eu vou, eu vou, eu vou ...

Fazendo um site


OK! Parece uma música (mais ou menos), agora vamos criar um site!


Como não pretendo fornecer APIs, escolho o modelo Razor Pages em oposição ao MVC. Também ativei a autorização, pois permitiremos que os usuários votem nas melhores letras e tenham um gráfico das 10 melhores.


Apressando o MVP, fui em frente e criei uma página da Web Song.cshtml, cujo objetivo no momento será simplesmente chamar o GPT-2 e obter uma música aleatória. O layout da página é trivial e basicamente consiste na música e seu título:


 @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> 


Agora, como gosto do meu código reutilizável, criei uma interface que me permitirá conectar diferentes geradores de letras posteriormente, que serão injetados pelo ASP.NET no SongModel.


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

Omitindo o título da música por enquanto, tudo o que precisamos fazer é registrar o Gpt2LyricsGenerator na Inicialização, configurar os Serviços e chamá-lo no SongModel . Então, vamos começar o gerador. E a primeira coisa que precisamos garantir é que temos


Geração de letras repetíveis


Porque eu fiz uma declaração ousada no título, que será mais de 1 bilhão de músicas, nem pense em gerar e armazenar todas elas. Primeiro, sem nenhum metadado, isso levaria sozinho mais de 1 TB de espaço em disco. Segundo, leva ~ 3 minutos no meu nettop para gerar uma nova música, por isso levará uma eternidade para gerar todas elas. E eu quero poder transformar esse bilhão em um quintilhão, mudando para Int64, se necessário! Imagine que poderíamos fazer 1 centavo por música, em 1 quintilhão de músicas? Isso seria mais do que o atual PIB anual do mundo!


Em vez disso, o que precisamos fazer é garantir que o GPT-2 gere a mesma música repetidas vezes, devido ao seu ID , que eu especifico na rota. Para esse propósito, o TensorFlow permite definir a semente de seu gerador de números interno a qualquer momento, através da função tf.set_random_seed , da seguinte forma: tf.set_random_seed (songNumber) . Então, eu queria apenas chamar Gpt2Sampler.SampleSequence , para obter o texto da música codificada, decodificá-lo e retornar o resultado, concluindo assim Gpt2LyricsGenerator .


Infelizmente, na primeira tentativa, isso não funcionou como esperado. Toda vez que eu pressionava o botão de atualização, um novo texto exclusivo era retornado na página. Após bastante depuração, finalmente descobri que o TensorFlow 1.X tem problemas significativos com a reprodutibilidade: muitas operações têm estados internos, que não são afetados pelo set_random_seed e são difíceis de serem redefinidos.


A reinicialização das variáveis ​​do modelo ajudou a compensar esse problema, mas também significou que a sessão deve ser recriada e os pesos do modelo precisam ser recarregados a cada chamada. Recarregar uma sessão desse tamanho causou um vazamento de memória gigante. Para evitar procurar sua causa no código-fonte TensorFlow C ++, em vez de gerar geração de texto em processo, decidi iniciar um novo processo com Process.Start , gerar texto lá e ler a partir da saída padrão. Até que uma maneira de redefinir o estado do modelo no TensorFlow seja estabilizada, esse seria o caminho a seguir.


Então, acabei com duas classes: Gpt2LyricsGenerator , que implementa ILyricsGenerator de cima, gerando uma nova instância do BillionSongs.exe com parâmetros de linha de comando, que incluem a identificação da música, e eventualmente instancia o Gpt2TextGenerator , que na verdade chama GPT-2 para gerar letras, e simplesmente imprime.


Agora, atualizar a página sempre me dava o mesmo texto.


Lidando com tempo de 3 minutos para gerar uma música


Que experiência horrível para o usuário seria! Você acessa um site, clica em "Criar nova música" e absolutamente nada acontece por 3 (!) Minutos enquanto meu nettop leva tempo para gerar as letras das músicas solicitadas.


Eu resolvi esse problema em vários níveis:


Pregenerando músicas


Como mencionado acima, você não pode gerar todas as músicas e servi-las a partir de um banco de dados. E você não pode simplesmente gerar sob demanda, porque isso é lento. Então o que você pode fazer?


Simples! Como a maneira principal para os usuários verem uma nova música é clicar no botão "Tornar aleatório", vamos gerar muitas músicas com antecedência, colocá-las em um ConcurrentQueue e deixar que "Torne aleatórias" pop. Embora o número de visitantes seja baixo, o servidor levará algum tempo entre eles para gerar algumas músicas, que serão prontamente acessíveis.


Outro truque que usei é fazer um loop nessa fila várias vezes, para que muitos usuários possam ver a mesma música pré-gerada. Basta manter um equilíbrio entre o uso da RAM e quantas vezes um usuário precisa clicar em "Tornar aleatório" para ver algo que já viu antes. Simplesmente escolhi 50.000 músicas como um número razoável, o que consumiria apenas 50 MB de RAM, além de fornecer um número bastante grande de cliques.


Eu implementei essa funcionalidade na classe PregeneratedSongProvider : IRandomSongProvider (a interface é injetada no código, responsável por manipular o botão "Tornar aleatório").


Armazenamento em cache


Músicas pré-geradas são armazenadas em cache na memória, mas também defino o cabeçalho do cache HTTP como público para permitir o navegador, e a CDN (eu uso o CloudFlare) armazena em cache para evitar ser atingido por um influxo de usuário.


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

Retornando músicas populares


A maioria das músicas geradas pelo GPT-2 afinado dessa maneira é bastante monótona, se não rudimentar. Para tornar os cliques em "Tornar aleatório" mais atraentes, adicionei uma probabilidade de 25% de que, em vez de uma música completamente aleatória, você obterá uma música que foi previamente votada por outros usuários. Além de aumentar o envolvimento, aumenta a chance de você solicitar uma música, armazenada em cache na CDN ou na memória.


Todos os truques acima são conectados usando a injeção de dependência do ASP.NET na classe Startup .


Votação


Não há muito especial sobre a implementação da votação. Existe o SongVoteCache , que mantém as contagens atualizadas. E um iframe que hospeda o botão de votação s na página da música, que permite que a parte essencial da página - o título e a letra sejam armazenados em cache, enquanto a contagem de votos e o status do login são carregados posteriormente.


Os resultados finais


Amostra de música gerada


Uma versão demo em execução no meu nettop, liderada pelo CloudFlare (com folga, seu Core i3) agora congelada e movida para o nível gratuito do Serviço de Aplicativo do Azure.


O repositório GitHub , contendo código fonte e instruções para executar o site e ajustar o modelo.


Planos para o futuro / exercícios


Gere títulos


O GPT-2 é muito fácil de ajustar. Pode-se gerar títulos de músicas anexando ou sufixando todas as amostras de letras do conjunto de dados com um token artificial como <| startoftitle |> , seguido pelo título do mesmo conjunto de dados.


Como alternativa, os usuários podem sugerir e / ou votar em títulos.


Gere música


No meio do desenvolvimento do Billion Songs, achei que seria legal baixar um monte de arquivos MIDI (que é um formato de música antigo, muito mais próximo do texto do que mp3s) e treinar o GPT-2 para gerar mais . Alguns desses arquivos ainda tinham texto incorporado, para que você pudesse obter a geração de karaokê .


Eu sei que a geração de músicas dessa maneira é muito possível, porque ontem a OpenAI realmente publicou uma implementação dessa ideia em seu blog . Mas, hooray, eles não fizeram o karaokê! Eu descobri que é possível raspar http://www.midi-karaoke.info para esse fim.


Gradiente, também conhecido como TensorFlow for .NET


Por favor, consulte o nosso blog para obter atualizações.

Source: https://habr.com/ru/post/pt453232/


All Articles