使用 🤗 训练用于音乐生成的 GPT-2 模型
在本教程中,我将引导您创建类似以下的 Space:
🤗 提供了从数据集创建到模型演示部署的全面工具集。您将在本教程中用到这些工具。因此,熟悉 Hugging Face 生态系统将很有帮助。在本教程结束时,您将能够训练一个用于音乐生成的 GPT-2 模型。
本教程的灵感和基础来自 Tristan Behrens 博士的杰出工作。
概述
生成式 AI 目前是机器学习领域的热门趋势。ChatGPT 或 Stable Diffusion 等令人印象深刻的模型以其卓越的能力吸引了科技界和公众。Facebook、OpenAI 和 Stability AI 等主要公司也通过发布令人印象深刻的音乐生成工具涉足这一领域。
生成式音乐模型通常有两种常见方法。您可以从以下几个方面来考虑它们:
- 原始音频:在这种方法中,您使用音频的原始表示(.wav、.mp3)来训练模型。StableAudio 和 MusicGen 使用此方法。
- 符号音乐:除了使用原始音频表示外,您还可以利用生成该音频的指令。例如,您不会使用长笛旋律的录音,而是使用音乐家演奏乐曲所读的乐谱。MIDI 或 MusicXML 文件存储了生成特定音乐片段所需的指令。OpenAI 使用符号音乐训练了 MuseNet(不再可用)。
本教程的重点是符号模型。具体来说,您将实现一个巧妙的想法:如果可以将符号音乐文件(本教程为 MIDI 文件)中包含的指令转换为单词,就可以利用 NLP 的巨大进步来训练您的模型!
让我们一起深入探讨。
目录:
您将找到一系列笔记本,用于检查每个部分的完整代码。
收集数据集并将其转换为单词
注意:考虑到所需 MIDI 文件的大小,我已经整理了一个可在 Hugging Face 上使用的即用型数据集。另外,如果您更喜欢小型数据集,可以使用 JS Fake Chorales 数据集 来学习本教程。
收集数据集并准备好进行训练是项目中最困难的部分。幸运的是,人们在互联网上共享了一些 MIDI 集合,您可以使用。您将使用 Colin Raffel 整理的其中一个集合,即 Lakh MIDI 数据集 (LMD),其中包含 176,581 个独特的 MIDI 文件。从 LDM 中,您将使用 Clean MIDI 子集(14,751 个文件),其文件名指示艺术家和标题。
获取流派
了解每个文件的艺术家和标题使您能够确定歌曲的流派。有很多方法可以做到这一点。我使用了一种混合方法,我首先使用 Spotify API 根据艺术家获取流派,然后使用 ChatGPT 将它们分组为最终的一组或多或少平衡的流派。
# Spotify API's code snippet
genres = {}
for i,artist in enumerate(artists):
try:
results = sp.search(q=artist, type='artist',limit=1)
items = results['artists']['items']
genre_list = items[0]['genres'] if len(items) else items['genres']
genres[artist] = (genre_list[0]).replace(" ", "_")
if i < 5:
print("INFO: Preview {}/5".format(i + 1),
artist, genre_list[:5])
except Exception as e:
genres[artist] = "MISC"
print("INFO: ", artist, "not included: ", e)
结果远非完美,但它们足够接近,可以用于控制我们的模型。包含流派的最终 CSV 文件可在 GitHub 上找到。
为什么要获取流派?您可以使用流派在输入序列中加入一个 token,
"GENRE={NAME_OF_GENRE}"
从而将生成过程引导到该特定流派,正如您接下来将看到的。
对数据集进行分词

上图向您展示了将音乐指令转换为词元的一种方法:这正是您要训练语言模型所需的方法!在本节中,您将了解如何使用伪词(不属于英语词汇的术语)从 MIDI 文件转换为基于文本的格式,以训练您的 GPT-2 模型。
分块数据集
在本教程中,您将以 8 小节的窗口对每个文件进行分词,其中每个“小节”是包含指定拍数的部分。尝试使用其他数字,例如 4 或 16,看看结果如何变化。有很多方法可以做到这一点,但为了简单起见,让我们遍历数据集并创建一个 8 小节长的新 MIDI 文件。我在 Colab 中使用以下代码进行分块。
for i, midi_path in enumerate(tqdm(midi_paths, desc="CHUNKING MIDIS")):
try:
# Determine the output directory for this file
relative_path = midi_path.relative_to(Path("path/to/dataset/lmd", dataset))
output_dir = merged_out_dir / relative_path.parent
output_dir.mkdir(parents=True, exist_ok=True)
# Check if chunks already exist
chunk_paths = list(output_dir.glob(f"{midi_path.stem}_*.mid"))
if len(chunk_paths) > 0:
print(f"Chunks for {midi_path} already exist, skipping...")
continue
# Loads MIDI, merges and saves it
midi = MidiFile(midi_path)
ticks_per_cut = MAX_NB_BAR * midi.ticks_per_beat * 4
nb_cuts = ceil(midi.max_tick / ticks_per_cut)
if nb_cuts < 2:
continue
print(f"Processing {midi_path}")
midis = [deepcopy(midi) for _ in range(nb_cuts)]
for j, track in enumerate(midi.instruments): # sort notes as they are not always sorted right
track.notes.sort(key=lambda x: x.start)
for midi_short in midis: # clears notes from shorten MIDIs
midi_short.instruments[j].notes = []
for note in track.notes:
cut_id = note.start // ticks_per_cut
note_copy = deepcopy(note)
note_copy.start -= cut_id * ticks_per_cut
note_copy.end -= cut_id * ticks_per_cut
midis[cut_id].instruments[j].notes.append(note_copy)
# Saving MIDIs
for j, midi_short in enumerate(midis):
if sum(len(track.notes) for track in midi_short.instruments) < MIN_NB_NOTES:
continue
midi_short.dump(output_dir / f"{midi_path.stem}_{j}.mid")
except Exception as e:
print(f"An error occurred while processing {midi_path}: {e}")
我添加了代码的简化版本。您可以查看完整笔记本。
从 MIDI 指令到文字
将每首歌曲分割成 8 小节的 MIDI 文件后,您现在可以将这些文件转换为伪词。研究人员提出了不同的音乐分词方法,其中最流行的包括:
您可以在 MidiTok(一个强大的 Python 包,用于对 MIDI 音乐文件进行分词)的文档中找到不同分词器的优秀概述。
分词 |
速度 |
拍号 |
和弦 |
休止符 |
延音踏板 |
音高弯音 |
---|---|---|---|---|---|---|
MIDILike |
✅ |
✅ |
✅ |
✅ |
✅ |
✅ |
REMI |
✅ |
✅ |
✅ |
✅ |
✅ |
✅ |
TSD |
✅ |
✅ |
✅ |
✅ |
✅ |
✅ |
结构化 |
❌ |
❌ |
❌ |
❌ |
❌ |
❌ |
CPWord |
✅ |
✅ |
✅ |
✅ |
❌ |
❌ |
八度 |
✅ |
✅ |
❌ |
❌ |
❌ |
❌ |
MuMIDI |
✅ |
❌ |
✅ |
❌ |
❌ |
❌ |
MMM |
✅ |
✅ |
✅ |
❌ |
❌ |
❌ |
本教程将使用 MMM:多轨音乐机器分词方法。MMM 是一种简单而强大的方法,可将 MIDI 文件转换为伪词。尝试其他分词器并比较结果。请告诉我您最喜欢哪个分词器 😀。
MMM:多轨音乐机
Jeff Ens 和 Philippe Pasquier 在论文 MMM:探索 Transformer 条件多轨音乐生成中介绍了 MMM 分词器。请看以下论文中的插图,以便更好地理解此方法

在 MMM 中,数字表示 MIDI 记谱法中的音高和乐器。例如,在上图中,NOTE_ON=60 是 C4,INST=30 表示过载吉他。您使用 NOTE_ON/NOTE_OFF 来指示音符何时开始和停止发声,并使用 TIME_DELTA 来移动时间线。音符被包裹在 <BAR_START> 和 <BAR_END> 标记中,这些标记又被添加在 <TRACK_START> 和 <TRACK_END> 伪词中,最后您将它们分组在 <PIECE_START> 和 <PIECE_END> 中:多轨音乐机!
让我们用一个来自 JS Fake Chorales 的具体例子来说明这一点
我希望这个简洁的概述能让您清楚地了解 MMM 的运作方式。现在,有趣的部分来了!让我们获取 LMD Clean 并将其转换为伪词。
为了对数据集进行分词,您可以利用开源库,如 MidiTok(前面提到过)或 Musicaiz。两者都提供了出色的功能来自定义您的分词过程。但是,我决定使用 MMM-JSB 仓库作为起点,并将其适配到 Lakh Midi 数据集,因为这样我可以在过程中获得更多的控制权。您可以在这里找到适配后的仓库。
适配后的仓库删除了包含多个拍号或拍号不是 4/4 的文件。此外,它还添加了一个 GENRE=
token,以便您可以在推理时控制模型生成的流派。最后,我决定不对音符进行量化,这样声音就不会那么机械。
您可以利用此笔记本进行数据集分词。但是,请注意,此过程可能非常耗时,并且您可能会遇到错误。如果您想跳过此过程,我已将分词后的数据集上传到 Hub,您可以直接使用!Hugging Face 允许您轻松上传数据集。在我的例子中,我创建了一个数据框来进行一些清理和基本数据探索,并将最终的数据框作为数据集上传到 Hub。
# Install datasets
!pip install datasets
# Collect files from the right folder
import glob
dataset_files = glob.glob("/path/to/tokenized/dataset/*.txt")
# Load files as HF dataset
from datasets import load_dataset
dataset = load_dataset("text", data_files=dataset_files)
# Convert dataset to dataframe
ds = dataset["train"]
df = ds.to_pandas()
# Some data cleaning and exploration...
from datasets import Dataset
# Convert the DataFrame to a Hugging Face dataset
clean_dataset = Dataset.from_pandas(df)
# Log in to your Hugging Face account
from huggingface_hub import notebook_login
notebook_login()
# Push dataset to your account, replace juancopi81 with your user
clean_dataset.push_to_hub("juancopi81/mmm_track_lmd_8bars_nots")
欢迎查看完整笔记本。
训练分词器和模型。
此时,您的数据集应该已格式化为伪词。请记住,您可以按照本教程的上一部分收集一个,或者使用Hub 上准备好的。您也可以使用较小的数据集来测试本教程的这一部分,或者如果您资源有限。例如,我推荐 js-fakes-4bars 数据集作为一种更简单的替代方案,它也能很好地工作。我将根据您选择的数据集(LMD | JS Fake)添加相应笔记本的链接。
既然您已经有了一个伪词数据集,接下来的部分将与训练语言模型非常相似,但语言是由音乐词汇组成的。实际上,本教程的这一部分严格遵循 Hugging Face 的 NLP 课程,其中您需要在拥有数据集后训练一个分词器。
注意:如果您不熟悉分词或模型训练,我鼓励您回顾该课程以更好地理解本教程。
分词器
Colab 笔记本,用于完成本教程的这一部分:LMD | JS Fake
在本教程中,您将训练一个 GPT-2 模型。这个模型具有出色的学习能力,是开源的,并且 Hugging Face 在促进其训练和使用方面做得非常出色。但是 GPT-2 并没有在音乐语言上进行训练,因此您必须从头开始重新训练,从分词器开始。
为了说明前一点,让我们使用默认的 GPT-2 分词器对数据集中的一些单词进行分词
# Take some sample from the dataset
sample_10 = raw_datasets["train"]["text"][10]
sample = sample_10[:242]
sample
PIECE_START GENRE=POP TRACK_START INST=35 DENSITY=1 BAR_START TIME_DELTA=6.0 NOTE_ON=40 TIME_DELTA=4.0 NOTE_ON=32 TIME_DELTA=0.10833333333333428 NOTE_OFF=40 TIME_DELTA=5.533333333333331 NOTE_OFF=32 BAR_END BAR_START NOTE_ON=31 TIME_DELTA=6.0
# Default GPT-2 tokenizer applied to our dataset
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2")
print(tokenizer(sample).tokens())
['PI', 'EC', 'E', '_', 'ST', 'ART', 'Ġ', 'ĠGEN', 'RE', '=', 'P', 'OP', 'ĠTR', 'ACK', '_', 'ST', 'ART', 'ĠINST', '=', '35', 'ĠD', 'ENS', 'ITY', '=', '1', 'ĠBAR', '_', 'ST', 'ART', 'ĠTIME', '_', 'D', 'EL', 'TA', '=', '6', '.', '0', 'ĠNOTE', '_', 'ON', '=', '40', 'ĠTIME', '_', 'D', 'EL', 'TA', '=', '4', '.', '0', 'ĠNOTE', '_', 'ON', '=', '32', 'ĠTIME', '_', 'D', 'EL', 'TA', '=', '0', '.', '108', '3333', '3333', '33', '34', '28', 'ĠNOTE', '_', 'OFF', '=', '40', 'ĠTIME', '_', 'D', 'EL', 'TA', '=', '5', '.', '5', '3333', '3333', '3333', '31', 'ĠNOTE', '_', 'OFF', '=', '32', 'ĠBAR', '_', 'END', 'ĠBAR', '_', 'ST', 'ART', 'ĠNOTE', '_', 'ON', '=', '31', 'ĠTIME', '_', 'D', 'EL', 'TA', '=', '6', '.', '0']
正如所见,默认的 GPT-2 分词器在处理音乐词元时遇到了困难。我们需要一种自定义方法以获得更好的结果。
为了训练分词器,您通常会首先对单词进行归一化。此步骤包括删除不必要的空格、将单词小写以及删除重音。此步骤对于自然语言是必不可少的,但对于您拥有的音乐词元则不需要。
下一步是预分词,您将输入拆分成更小的实体,例如单词。在我们的例子中,根据空白拆分输入就足够了
from tokenizers import Tokenizer
from tokenizers.models import WordLevel
# We need to specify the UNK token
new_tokenizer = Tokenizer(model=WordLevel(unk_token="[UNK]"))
# Add pretokenizer
from tokenizers.pre_tokenizers import WhitespaceSplit
new_tokenizer.pre_tokenizer = WhitespaceSplit()
# Let's test our pre_tokenizer
new_tokenizer.pre_tokenizer.pre_tokenize_str(sample)
[('PIECE_START', (0, 11)),
('GENRE=POP', (13, 22)),
('TRACK_START', (23, 34)),
('INST=35', (35, 42)),
('DENSITY=1', (43, 52)),
('BAR_START', (53, 62)),
('TIME_DELTA=6.0', (63, 77)),
('NOTE_ON=40', (78, 88)),
('TIME_DELTA=4.0', (89, 103)),
('NOTE_ON=32', (104, 114)),
('TIME_DELTA=0.10833333333333428', (115, 145)),
('NOTE_OFF=40', (146, 157)),
('TIME_DELTA=5.533333333333331', (158, 186)),
('NOTE_OFF=32', (187, 198)),
('BAR_END', (199, 206)),
('BAR_START', (207, 216)),
('NOTE_ON=31', (217, 227)),
('TIME_DELTA=6.0', (228, 242))]
最后,您训练您的分词器,进行任何后处理,并(可选但强烈建议)将其上传到 Hub。
# Yield batches of 1,000 texts
def get_training_corpus():
dataset = raw_datasets["train"]
for i in range(0, len(dataset), 1000):
yield dataset[i : i + 1000]["text"]
from tokenizers.trainers import WordLevelTrainer
# Add special tokens
trainer = WordLevelTrainer(
special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"]
)
# Post-processing and updloading it to the Hub
from transformers import PreTrainedTokenizerFast
new_tokenizer.save("tokenizer.json")
new_tokenizer = PreTrainedTokenizerFast(tokenizer_file="tokenizer.json")
new_tokenizer.add_special_tokens({'pad_token': '[PAD]'})
new_tokenizer.push_to_hub("lmd_8bars_tokenizer")
让我们看看训练后你的分词器表现如何
['PIECE_START', 'GENRE=POP', 'TRACK_START', 'INST=35', 'DENSITY=1', 'BAR_START', 'TIME_DELTA=6.0', 'NOTE_ON=40', 'TIME_DELTA=4.0', 'NOTE_ON=32', 'TIME_DELTA=0.10833333333333428', 'NOTE_OFF=40', 'TIME_DELTA=5.533333333333331', 'NOTE_OFF=32', 'BAR_END', 'BAR_START', 'NOTE_ON=31', 'TIME_DELTA=6.0']
正是我们想要的!太棒了!您现在有一个用于训练 GPT-2 模型的 Hub 中的分词器。
模型
Colab 笔记本,用于本教程的这一部分:LMD | JS Fake
现在您的数据集和分词器都已准备就绪,是时候训练模型了。在本教程的这一部分中,您将
- 为模型准备数据集。
- 选择模型的配置。
- 使用自定义训练器训练模型。自定义训练器将允许您在训练期间将模型的结果记录到 Weights and Biases 中(您需要一个 W&B 帐户才能实现此功能)。
准备数据集
您已经完成了艰苦的工作,因此准备数据集很简单。您需要从 Hugging Face 获取数据集,并使用新的分词器创建分词后的数据集。这个分词后的数据集正是 GPT-2 所期望的输入。
# Import libraries
from datasets import load_dataset
from transformers import AutoTokenizer
# Download dataset - you can change it for your own dataset
ds = load_dataset("juancopi81/mmm_track_lmd_8bars_nots", split="train")
# We had only "train" in the ds, so we can create a test and train split
raw_datasets = ds.train_test_split(test_size=0.1, shuffle=True)
# Change for respective tokenizer
tokenizer = AutoTokenizer.from_pretrained("juancopi81/lmd_8bars_tokenizer")
raw_datasets
raw_datasets
现在包含训练和测试拆分。
DatasetDict({
train: Dataset({
features: ['text'],
num_rows: 159810
})
test: Dataset({
features: ['text'],
num_rows: 17757
})
})
现在让我们对整个数据集进行分词。有很多方法可以做到这一点。在本教程中,您将截断任何长度超过定义 context_length
的文本(歌曲)。在 transformer 模型中,context_length
表示模型可以处理的最大序列长度(词元)。此长度通常受内存限制和模型架构的约束。
# You can replace this, 2048 seems a good number here
context_length = 2048
# Function for tokenizing the dataset
def tokenize(element):
outputs = tokenizer(
element["text"],
truncation=True, #Removing element longer that context size, no effect in JSB
max_length=context_length,
padding=False
)
return {"input_ids": outputs["input_ids"]}
# Create tokenized_dataset. We use map to pass each element of our dataset to tokenize and remove unnecessary columns.
tokenized_datasets = raw_datasets.map(
tokenize, batched=True, remove_columns=raw_datasets["train"].column_names
)
tokenized_datasets
DatasetDict({
train: Dataset({
features: ['input_ids'],
num_rows: 159810
})
test: Dataset({
features: ['input_ids'],
num_rows: 17757
})
})
tokenized_dataset
包含您训练模型所需的 input_ids
。
选择模型配置
在本教程中,您将训练一个 GPT-2 模型。您可以配置不同大小的 GPT-2 模型,这是设置模型时的关键决策。我在笔记本中添加了一些代码,用于使用 Chinchilla 论文(一项分析模型大小、数据和性能之间关系的研究)的一些缩放定律结果来确定模型的大小。我从 Karpathy 的实现中改编了笔记本的这一部分。
注意:我目前正在完善本教程的这一部分,因此仍在进行中。随着我进行更新,我将相应地刷新笔记本。欢迎提出任何反馈!
在本教程中,让我们使用一个小型版本(参数较少),这将允许您以更受限的资源训练模型,并且在训练后,更快地生成音乐。实际上,您在本教程开头看到的演示并没有使用 GPU,但仍然以合理的速度创建音乐。
# Change this based on size of the data
n_layer=6 # Number of transformer layers
n_head=8 # Number of multi-head attention heads
n_emb=512 # Embedding size
from transformers import AutoConfig, GPT2LMHeadModel
config = AutoConfig.from_pretrained(
"gpt2",
vocab_size=len(tokenizer),
n_positions=context_length,
n_layer=n_layer,
n_head=n_head,
pad_token_id=tokenizer.pad_token_id,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
n_embd=n_emb
)
model = GPT2LMHeadModel(config)
数据整理器
在开始训练之前,您需要为模型创建批次。此外,请记住,在因果语言模型中,输入充当标签(通过一个元素进行移位),因此您也必须处理这一点。但别担心,Hugging Face 的数据收集器将为我们完成这项工作:🤗 确实让我们的生活更轻松!
from transformers import DataCollatorForLanguageModeling
# It supports both masked language modeling (MLM) and causal language modeling (CLM)
# We need to set mlm=False for CLM
data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)
训练模型
您已经将所有部分组合在一起,现在是关键时刻:训练模型!在模型训练期间,您不会想蒙着眼睛,因此在训练过程中测试一些生成总是好的。这部分非常令人满意:您将听到您的 AI 音乐随着训练轮次的推移而演变。
为此,您需要一个 Weights and Biases 账户并自定义训练器,以便在 eval_loop 中记录音乐。请参阅笔记本获取详细信息,这里您可以看到关键代码片段:
from transformers import Trainer, TrainingArguments
# first create a custom trainer to log prediction distribution
SAMPLE_RATE=44100
class CustomTrainer(Trainer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def evaluation_loop(
self,
dataloader,
description,
prediction_loss_only=None,
ignore_keys=None,
metric_key_prefix="eval",
):
# call super class method to get the eval outputs
eval_output = super().evaluation_loop(
dataloader,
description,
prediction_loss_only,
ignore_keys,
metric_key_prefix,
)
# log the prediction distribution using `wandb.Histogram` method.
if wandb.run is not None:
input_ids = tokenizer.encode("PIECE_START STYLE=JSFAKES GENRE=JSFAKES TRACK_START", return_tensors="pt").cuda()
# Generate more tokens.
voice1_generated_ids = model.generate(
input_ids,
max_length=512,
do_sample=True,
temperature=0.75,
eos_token_id=tokenizer.encode("TRACK_END")[0]
)
voice2_generated_ids = model.generate(
voice1_generated_ids,
max_length=512,
do_sample=True,
temperature=0.75,
eos_token_id=tokenizer.encode("TRACK_END")[0]
)
voice3_generated_ids = model.generate(
voice2_generated_ids,
max_length=512,
do_sample=True,
temperature=0.75,
eos_token_id=tokenizer.encode("TRACK_END")[0]
)
voice4_generated_ids = model.generate(
voice3_generated_ids,
max_length=512,
do_sample=True,
temperature=0.75,
eos_token_id=tokenizer.encode("TRACK_END")[0]
)
token_sequence = tokenizer.decode(voice4_generated_ids[0])
note_sequence = token_sequence_to_note_sequence(token_sequence)
synth = note_seq.fluidsynth
array_of_floats = synth(note_sequence, sample_rate=SAMPLE_RATE)
int16_data = note_seq.audio_io.float_samples_to_int16(array_of_floats)
wandb.log({"Generated_audio": wandb.Audio(int16_data, SAMPLE_RATE)})
return eval_output
在设置好自定义训练器后,您可以开始训练模型。作为初学者,我使用了以下参数:
# Create the args for out trainer
from argparse import Namespace
# Get the output directory with timestamp.
output_path = "output"
steps = 5000
# Commented parameters
config = {"output_dir": output_path,
"num_train_epochs": 1,
"per_device_train_batch_size": 8,
"per_device_eval_batch_size": 4,
"evaluation_strategy": "steps",
"save_strategy": "steps",
"eval_steps": steps,
"logging_steps":steps,
"logging_first_step": True,
"save_total_limit": 5,
"save_steps": steps,
"lr_scheduler_type": "cosine",
"learning_rate":5e-4,
"warmup_ratio": 0.01,
"weight_decay": 0.01,
"seed": 1,
"load_best_model_at_end": True,
"report_to": "wandb"}
args = Namespace(**config)
让我们在 CustomTrainer 中使用它们
train_args = TrainingArguments(**config)
# Use the CustomTrainer created above
trainer = CustomTrainer(
model=model,
tokenizer=tokenizer,
args=train_args,
data_collator=data_collator,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["test"],
)
并开始你的训练
# Train the model.
trainer.train()
使用 Sweeps 查找更好的超参数
在上一节中,您训练了您的音乐生成模型。这很棒!现在让我们为您的模型寻找更好的超参数。有不同的方法可以做到这一点;我决定使用 Weights and Biases 的“sweeps”,因为它具有用户界面和易用性。
在 W&B 中设置您的 sweeps 首先要求您组织您的代码。在此步骤中,您将把前面的笔记本分解成一系列可以与各种可能参数一起调用的函数。您可以在以下链接中看到一个示例:
组织好代码后,您可以在 YAML 文件或 Python 字典中定义您的 sweeps 配置。此配置将向 W&B 解释您希望用于探索超参数的策略。让我们探索此文件:
# The program to run
program: train.py
# Method can be grid, random or bayes
method: random
# Project this sweep is part of
project: mlops-001-lmdGPT
# Metric to optimize
metric:
name: eval/loss
goal: minimize
# Parameters space to search
parameters:
learning_rate:
distribution: log_uniform_values
min: 5e-4
max: 3e-3
gradient_accumulation_steps:
values: [1, 2, 4]
对于本教程,YAML 文件配置为探索 learning_rate 和 gradient_accumulation_steps 的超参数,这两个是影响训练过程性能最重要的参数。欢迎您进行实验并分享您的结果!
要运行您的 sweep,请按照以下步骤操作
1. 初始化您的 sweep
wandb sweep sweep.yaml
2. 启动您的 sweep 代理: {wandb agent} 的值可以从上一步的输出中获取。{runs for this agent} 代表代理为找到最佳超参数而应进行的最多尝试次数
wandb agent {wandb agent} --count {runs for this agent}
我准备了一个笔记本,用于在您的组织代码位于 GitHub 后运行您的代理(LMD | JS Fake)
然后,您可以查看 W&B 帐户中的 sweep 结果,以得出您的分析结论

现在您可以启动您的脚本,用分析中找到的最佳超参数来训练您的模型。例如:
python train.py --learning_rate=0.0005 --per_device_train_batch_size=8 --per_device_eval_batch_size=4 --num_train_epochs=10 --push_to_hub=True --eval_steps=4994 --logging_steps=4994 --save_steps=4994 --output_dir="lmd-8bars-2048-epochs10" --gradient_accumulation_steps=2
至此,我们完成了模型和分词器的训练部分。我希望您和我一样对接下来要发生的事情感到兴奋:展示您的模型 💪🏾。
在 🤗 Space 中展示模型
模型训练完毕并准备就绪,是时候展示它了!您可以使用 Gradio 创建模型的用户界面 (UI),并将您的应用程序托管为 Hugging Face Space。在本教程的这一部分中,我们将一起完成这项工作。请记住,您需要一个 Hugging Face 账户才能完成此操作。
创建新空间后,您可以决定使用哪个 SDK。本教程将使用 Docker 来更好地控制应用程序的环境。以下是我为 ML 演示添加的 Dockerfile:
FROM ubuntu:20.04
WORKDIR /code
# So users can share results with the community - Adjust to your specific use case
ENV SYSTEM=spaces
ENV SPACE_ID=juancopi81/multitrack-midi-music-generator
COPY ./requirements.txt /code/requirements.txt
# Preconfigure tzdata
RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
DEBIAN_FRONTEND="noninteractive" apt-get install -y tzdata
# Some important packages for playing the generated music
RUN apt-get update -qq && \
apt-get install -qq python3-pip build-essential libasound2-dev libjack-dev wget cmake pkg-config libglib2.0-dev ffmpeg
# Download libfluidsynth source
RUN wget https://github.com/FluidSynth/fluidsynth/archive/refs/tags/v2.3.3.tar.gz && \
tar xzf v2.3.3.tar.gz && \
cd fluidsynth-2.3.3 && \
mkdir build && \
cd build && \
cmake .. && \
make && \
make install && \
cd ../../ && \
rm -rf fluidsynth-2.3.3 v2.3.3.tar.gz
ENV LD_LIBRARY_PATH=/usr/local/lib:${LD_LIBRARY_PATH}
RUN ldconfig
RUN pip3 install --no-cache-dir --upgrade -r /code/requirements.txt
# Set up a new user named "user" with user ID 1000
RUN useradd -m -u 1000 user
# Switch to the "user" user
USER user
# Set home to the user's home directory
ENV HOME=/home/user \
PATH=/home/user/.local/bin:$PATH
# Set the working directory to the user's home directory
WORKDIR $HOME/app
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
COPY --chown=user . $HOME/app
CMD ["python3", "main.py"]
您正在设置一个 Ubuntu 镜像并安装必要的软件包,例如 FluidSynth,您将使用它来播放生成的音乐的声音。requirements.txt 文件中还有其他重要的 Python 软件包。欢迎查看。
您应用程序的另一个关键部分是如何将模型生成的标记转换为音乐音符。我在整个教程中都隐藏了这个函数,但现在让我们看看它是如何工作的。本质上,该函数使用 Magenta 的 note_seq 库来创建一个 note_sequence,您可以将其转换为 MIDI 或播放。这是相关代码,完全归功于 Tristan Behrens 博士。
from typing import Optional
from note_seq.protobuf.music_pb2 import NoteSequence
from note_seq.constants import STANDARD_PPQ
def token_sequence_to_note_sequence(
token_sequence: str,
qpm: float = 120.0,
use_program: bool = True,
use_drums: bool = True,
instrument_mapper: Optional[dict] = None,
only_piano: bool = False,
) -> NoteSequence:
"""
Converts a sequence of tokens into a sequence of notes.
Args:
token_sequence (str): The sequence of tokens to convert.
qpm (float, optional): The quarter notes per minute. Defaults to 120.0.
use_program (bool, optional): Whether to use program. Defaults to True.
use_drums (bool, optional): Whether to use drums. Defaults to True.
instrument_mapper (Optional[dict], optional): The instrument mapper. Defaults to None.
only_piano (bool, optional): Whether to only use piano. Defaults to False.
Returns:
NoteSequence: The resulting sequence of notes.
"""
if isinstance(token_sequence, str):
token_sequence = token_sequence.split()
note_sequence = empty_note_sequence(qpm)
# Compute note and bar lengths based on the provided QPM
note_length_16th = 0.25 * 60 / qpm
bar_length = 4.0 * 60 / qpm
# Render all notes.
current_program = 1
current_is_drum = False
current_instrument = 0
track_count = 0
for _, token in enumerate(token_sequence):
if token == "PIECE_START":
pass
elif token == "PIECE_END":
break
elif token == "TRACK_START":
current_bar_index = 0
track_count += 1
pass
elif token == "TRACK_END":
pass
elif token == "KEYS_START":
pass
elif token == "KEYS_END":
pass
elif token.startswith("KEY="):
pass
elif token.startswith("INST"):
instrument = token.split("=")[-1]
if instrument != "DRUMS" and use_program:
if instrument_mapper is not None:
if instrument in instrument_mapper:
instrument = instrument_mapper[instrument]
current_program = int(instrument)
current_instrument = track_count
current_is_drum = False
if instrument == "DRUMS" and use_drums:
current_instrument = 0
current_program = 0
current_is_drum = True
elif token == "BAR_START":
current_time = current_bar_index * bar_length
current_notes = {}
elif token == "BAR_END":
current_bar_index += 1
pass
elif token.startswith("NOTE_ON"):
pitch = int(token.split("=")[-1])
note = note_sequence.notes.add()
note.start_time = current_time
note.end_time = current_time + 4 * note_length_16th
note.pitch = pitch
note.instrument = current_instrument
note.program = current_program
note.velocity = 80
note.is_drum = current_is_drum
current_notes[pitch] = note
elif token.startswith("NOTE_OFF"):
pitch = int(token.split("=")[-1])
if pitch in current_notes:
note = current_notes[pitch]
note.end_time = current_time
elif token.startswith("TIME_DELTA"):
delta = float(token.split("=")[-1]) * note_length_16th
current_time += delta
elif token.startswith("DENSITY="):
pass
elif token == "[PAD]":
pass
else:
pass
# Make the instruments right.
instruments_drums = []
for note in note_sequence.notes:
pair = [note.program, note.is_drum]
if pair not in instruments_drums:
instruments_drums += [pair]
note.instrument = instruments_drums.index(pair)
if only_piano:
for note in note_sequence.notes:
if not note.is_drum:
note.instrument = 0
note.program = 0
return note_sequence
def empty_note_sequence(qpm: float = 120.0, total_time: float = 0.0) -> NoteSequence:
"""
Creates an empty note sequence.
Args:
qpm (float, optional): The quarter notes per minute. Defaults to 120.0.
total_time (float, optional): The total time. Defaults to 0.0.
Returns:
NoteSequence: The empty note sequence.
"""
note_sequence = NoteSequence()
note_sequence.tempos.add().qpm = qpm
note_sequence.ticks_per_quarter = STANDARD_PPQ
note_sequence.total_time = total_time
return note_sequence
在 utils.py 文件中,您可以找到处理模型生成的函数。我决定一次生成一个乐器,这样用户就可以更好地控制音乐合成
def generate_new_instrument(seed: str, temp: float = 0.75) -> str:
"""
Generates a new instrument sequence from a given seed and temperature.
Args:
seed (str): The seed string for the generation.
temp (float, optional): The temperature for the generation, which controls the randomness. Defaults to 0.75.
Returns:
str: The generated instrument sequence.
"""
seed_length = len(tokenizer.encode(seed))
while True:
# Encode the conditioning tokens.
input_ids = tokenizer.encode(seed, return_tensors="pt")
# Move the input_ids tensor to the same device as the model
input_ids = input_ids.to(model.device)
# Generate more tokens.
eos_token_id = tokenizer.encode("TRACK_END")[0]
generated_ids = model.generate(
input_ids,
max_new_tokens=2048,
do_sample=True,
temperature=temp,
eos_token_id=eos_token_id,
)
generated_sequence = tokenizer.decode(generated_ids[0])
# Check if the generated sequence contains "NOTE_ON" beyond the seed
new_generated_sequence = tokenizer.decode(generated_ids[0][seed_length:])
if "NOTE_ON" in new_generated_sequence:
# If NOTE_ON we return it, we are generating one instrument at a time
return generated_sequence
此工具文件还包含删除、更改或重新生成乐器等关键过程的代码。
最后,在 main.py 文件中,您添加了用户可以单击以与模型交互的按钮。
# Code snippet of clickable buttons
def run():
with demo:
gr.HTML(DESCRIPTION)
gr.DuplicateButton(value="Duplicate Space for private use")
with gr.Row():
with gr.Column():
temp = gr.Slider(
minimum=0, maximum=1, step=0.05, value=0.85, label="Temperature"
)
genre = gr.Dropdown(
choices=genres, value="POP", label="Select the genre"
)
with gr.Row():
btn_from_scratch = gr.Button("🧹 Start from scratch")
btn_continue = gr.Button("➡️ Continue Generation")
btn_remove_last = gr.Button("↩️ Remove last instrument")
btn_regenerate_last = gr.Button("🔄 Regenerate last instrument")
with gr.Column():
with gr.Box():
audio_output = gr.Video(show_share_button=True)
midi_file = gr.File()
with gr.Row():
qpm = gr.Slider(
minimum=60, maximum=140, step=10, value=120, label="Tempo"
)
btn_qpm = gr.Button("Change Tempo")
with gr.Row():
with gr.Column():
plot_output = gr.Plot()
with gr.Column():
instruments_output = gr.Markdown("# List of generated instruments")
with gr.Row():
text_sequence = gr.Text()
empty_sequence = gr.Text(visible=False)
with gr.Row():
num_tokens = gr.Text(visible=False)
btn_from_scratch.click(
fn=generate_song,
inputs=[genre, temp, empty_sequence, qpm],
outputs=[
audio_output,
midi_file,
plot_output,
instruments_output,
text_sequence,
num_tokens,
],
)
btn_continue.click(
fn=generate_song,
inputs=[genre, temp, text_sequence, qpm],
outputs=[
audio_output,
midi_file,
plot_output,
instruments_output,
text_sequence,
num_tokens,
],
)
btn_remove_last.click(
fn=remove_last_instrument,
inputs=[text_sequence, qpm],
outputs=[
audio_output,
midi_file,
plot_output,
instruments_output,
text_sequence,
num_tokens,
],
)
btn_regenerate_last.click(
fn=regenerate_last_instrument,
inputs=[text_sequence, qpm],
outputs=[
audio_output,
midi_file,
plot_output,
instruments_output,
text_sequence,
num_tokens,
],
)
btn_qpm.click(
fn=change_tempo,
inputs=[text_sequence, qpm],
outputs=[
audio_output,
midi_file,
plot_output,
instruments_output,
text_sequence,
num_tokens,
],
)
demo.launch(server_name="0.0.0.0", server_port=7860)
让我们看看按钮在界面中的样子:

现在您可以与所有人分享您的模型了。拥有这个出色的模型并与世界分享是很酷的,但如果考虑到更广泛的影响,那就更酷了。让我们在下一节中一起思考这一点。
考虑伦理影响
首先,感谢您坚持到本教程的这一步。这是一个冗长且可能令人望而生畏的教程。虽然我已尽力确保本教程的准确性和质量,但我承认可能存在改进空间或潜在错误。我一直在不断学习和成长,感谢任何能改进内容的反馈或建议。您的见解将使未来的读者受益,并有助于我的学习之旅。如果本教程的任何一部分能帮助您的学习过程,我将不胜荣幸。
另一方面,自从我开始本教程以来,我决定加入一些关于伦理影响的思考。我不是这个话题的专家,我鼓励您寻求该领域专家的见解和观点。尽管如此,我还是想分享一些我的理解和担忧。
关于用 AI 生成音乐,有很多事情需要考虑:
- 系统在创作过程中的作用是什么?
- 这些模型对音乐家劳动力市场可能产生什么影响?
- 我们是否尊重了我们用来训练模型的音乐艺术家的权利?
- 生成音乐的著作权归谁所有?
这样的问题不胜枚举。不可能涵盖所有这些问题,所以我想把重点放在一个特别让我担忧的方面:数字鸿沟。
数字鸿沟是指“对数字技术的不平等访问”(来源:维基百科),这在那些能够获取信息和资源的人与那些不能获取信息和资源的人之间建立了一座危险的桥梁。
音乐领域的数字鸿沟呢?
音乐是人类的通用语言——亨利·沃兹沃思·朗费罗
音乐是一种超越国界、文化和时代的通用语言。它存在于每种文明中。然而,充满活力的传统正在被主流音乐所掩盖甚至遗忘,部分原因是某些群体更能接触到数字平台和音乐创作与发行工具。
机器学习,尤其是在民主化之后,可以成为一种工具,用于保护和整合我们这个时代被忽视的音乐。事实上,通过合适的数据集,机器学习模型可以分析、生成和分类边缘化音乐,以及其他任务。但它也可能放大和延续偏见,就像现在我们大多数人训练模型来生成摇滚、爵士或欧洲古典音乐的情况一样。实际上,社区已经创造了“巴赫水龙头”这个术语,因为许多模型现在可以合成几乎与巴赫一模一样的音乐:“巴赫水龙头是指生成系统无限量地制作出某种内容,其质量达到或超过某些具有文化价值的原创作品,但无限量的供应使其不再稀有,从而降低了价值”(通过 Twitter)。
此外,融合其他艺术传统只会增强最终模型并丰富音乐创作。创新艺术家正在展示这种潜力,例如 Hexorsismos 或 Yaboi Hanoi,后者凭借受泰国文化启发的旋律和声音设计赢得了 2022 年 AI 歌曲大赛。
在人工智能中实现更多样化的音乐表现面临诸多挑战,包括数据可用性、投资、教育等。开源社区具有独特的优势来应对其中一些挑战,可以通过协作创建更多样化的数据集、开发包容性模型、分享免费教程或通常情况下,设计尊重更广泛传统的工具。
除了音乐之外,机器学习正在塑造一个全新的现实——它就是“新的电力”,正如吴恩达教授所说。我们可能正在接近一场人工智能革命,它可能会改变我们的世界,我们所有人,无论我们的语言、文化、种族、教育或国籍如何,都必须拥有发言权并参与这一发展轨迹。想想一个被少数有影响力的人或群体控制的强大工具所带来的风险。
我邀请您积极参与更具包容性的人工智能进步。作为一个具体的例子,您可以加入开源社区,无论您的专业水平如何。每一次贡献都很重要,而有动力的人们齐心协力可以创造奇迹。您可以在 Hugging Face Discord 中找到许多不同知识水平的合作机会。最后,作为一名哥伦比亚人,我希望在 Hugging Face 上创建一个包含拉丁美洲被忽视音乐的优质数据集(MIDI 或音频)。如果您想加入我们的力量,请告诉我 💪🏾。