Hugging Face имеет полнофункциональный набор инструментов, от функций создания датасетов до развёртывания демо-моделей. В этом туториале мы воспользуемся такими инструментами, поэтому вам полезно будет знать экосистему Hugging Face. К концу туториала вы сможете обучить модель GPT-2 генерации музыки.
Демо проекта можно попробовать здесь.
Источником вдохновения и фундаментом этого туториала стала выдающаяся работа доктора Тристана Беренса.
Введение
Сегодня генеративный ИИ имеет большую популярность в сфере машинного обучения. Впечатляющие модели наподобие ChatGPT и Stable Diffusion благодаря своим потрясающим возможностям захватили внимание технологического сообщества и широкой публики. Крупные компании (Facebook, OpenAI и Stability AI) также присоединились к этому движению, выпустив впечатляющие инструменты для генерации музыки.
Обычно для создания генеративных музыкальных моделей используется два подхода:
Сырое аудио: при таком подходе для обучения модели используется сырое представление звука (.wav, .mp3) . Такую методику применяют для StableAudio и MusicGen.
Символическая музыка: вместо использования сырого представления звука можно использовать команды для генерации аудио. Например, вместо использования записи мелодии на флейте применяется считываемая музыкантом нотная запись. Команды для создания конкретного музыкального фрагмента хранятся в файлах MIDI или MusicXML. Компания OpenAI обучала MuseNet (уже недоступную) на символической музыке.
В этом туториале мы будем применять символические модели. В частности, мы реализуем хитрую идею: если получится преобразовать команды из файлов символической музыки (в туториале используются файлы MIDI) в слова, то мы сможем при обучении модели воспользоваться потрясающими достижениями в сфере NLP!
Сбор датасета и преобразование его в слова
Примечание: учитывая огромный размер необходимых файлов MIDI, я курировал уже готовый к применению датасет, выложенный на Hugging Face. Если же вы предпочитаете датасет меньшего размера, то можете воспользоваться для этого туториала датасетом JS Fake Chorales.
Сбор датасета и его подготовка к обучению — это самая сложная часть проекта. К счастью, в Интернете есть коллекции MIDI, которыми можно воспользоваться. Мы будем использовать одну из таких коллекций, курируемую Колином Рэффелом — Lakh MIDI dataset (LMD), включающую в себя 176581 уникальный файл MIDI. Из LDM мы возьмём подмножество Clean MIDI (14751 файл) с именами файлов, в которых указаны исполнитель и название.
Получаем жанры
Зная исполнителя и название каждой композиции, мы можем определить её жанр. Эту задачу можно решить множеством разных способов. Я воспользуюсь комбинированным: сначала при помощи Spotify API получу жанры по исполнителю, а потом применю ChatGPT для их группировки в готовый набор более-менее сбалансированных жанров.
# Фрагмент кода Spotify API
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.
Зачем нам получать жанры? Как мы увидим ниже, их можно использовать для внедрения во входную последовательность токена "GENRE={NAME_OF_GENRE}"
, что пустит процесс генерации по направлению этого конкретного жанра.
Токенизация датасета
На изображении выше показан один из способов преобразования музыкальных команд в токены, а это именно то, что нужно для обучения языковой модели! В этом разделе мы узнаем, как выполнить переход от файла MIDI в текстовый формат с использованием псевдослов (не относящихся к английскому лексикону) для обучения модели GPT-2.
Разбиение датасета на блоки
В этом туториале мы будем токенизировать каждый файл на восьмитактовые окна, где каждый «такт» (bar) — это сегмент, содержащий указанное количество долей. Вы можете поэкспериментировать с другими числами, например, с 4 или 16, и понаблюдать, как меняется результат. Можно сделать это множеством разных способов, но мы для простоты обойдём в цикле датасет и создадим новые файлы MIDI длиной восемь тактов. Для разбиения на блоки я воспользовался следующим кодом в Colab:
for i, midi_path in enumerate(tqdm(midi_paths, desc="CHUNKING MIDIS")):
try:
# Указываем выходную папку для этого файла
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)
# Проверяем, существуют ли уже блоки
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
# Загружаем MIDI, объединяем и сохраняем его
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): # сортируем ноты, потому что они не всегда отсортированы правильно
track.notes.sort(key=lambda x: x.start)
for midi_short in midis: # очищаем ноты из укороченных MIDI
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)
# Сохраняем MIDI
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 в слова
Сегментировав каждую композицию в восьмитактовые файлы MIDI, мы можем преобразовать их в псевдослова. Исследователи предложили различные методики токенизации музыки. Вот одни из самых популярных:
Потрясающий обзор различных токенизаторов можно найти в документации MidiTok — мощного пакета Python для токенизации музыкальных файлов MIDI.
Таблица совместимости токенизаций и дополнительных токенов.
Токенизация |
Темп |
Тактовый размер |
Аккорды |
Паузы |
Педаль удержания звука |
Модуляция звука |
---|---|---|---|---|---|---|
MIDILike |
✅ |
✅ |
✅ |
✅ |
✅ |
✅ |
REMI |
✅ |
✅ |
✅ |
✅ |
✅ |
✅ |
TSD |
✅ |
✅ |
✅ |
✅ |
✅ |
✅ |
Structured |
❌ |
❌ |
❌ |
❌ |
❌ |
❌ |
CPWord |
✅ |
✅ |
✅ |
✅ |
❌ |
❌ |
Octuple |
✅ |
✅ |
❌ |
❌ |
❌ |
❌ |
MuMIDI |
✅ |
❌ |
✅ |
❌ |
❌ |
❌ |
MMM |
✅ |
✅ |
✅ |
❌ |
❌ |
❌ |
В этом туториале я буду использовать MMM: методику токенизации Multi-Track Music Machine. MMM — это простая, но мощная методика преобразования файлов MIDI в псевдослова. Можете попробовать другие токенизаторы и сравнить результаты.
MMM: Multi-Track Music Machine
Джефф Энс и Филип Паске представили токенизатор MMM в научной статье MMM: Exploring Conditional Multi-Track Music Generation with the Transformer. Чтобы лучше понять эту методику, взгляните на иллюстрацию из статьи:
В MMM числа обозначают высоту нот и инструменты в нотации MIDI. Например, на показанной выше схеме NOTE_ON=60 — это C4, а INST=30 означает гитару с овердрайвом (Overdriven Guitar). Можно использовать NOTE_ON/NOTE_OFF для обозначения того, где нота начинает и заканчивает звучать, и TIME_DELTA для перемещения временной шкалы. Ноты обёрнуты в токены <BAR_START> и <BAR_END>, которые добавлены внутрь псевдослов <TRACK_START> и <TRACK_END>, которые, в свою очередь, группируются внутри <PIECE_START> и <PIECE_END>, обозначающих начало и конец произведения.
Давайте проиллюстрируем это конкретным примером, взятым из JS Fake Chorales:
PIECE_START STYLE=JSFAKES GENRE=JSFAKES TRACK_START INST=48 BAR_START NOTE_ON=68 TIME_DELTA=4 NOTE_OFF=68 NOTE_ON=67 TIME_DELTA=4 NOTE_OFF=67 NOTE_ON=65 TIME_DELTA=4 NOTE_OFF=65 NOTE_ON=63 TIME_DELTA=2 NOTE_OFF=63 NOTE_ON=65 TIME_DELTA=2 NOTE_OFF=65 BAR_END BAR_START NOTE_ON=67 TIME_DELTA=4 NOTE_OFF=67 NOTE_ON=65 TIME_DELTA=4 NOTE_OFF=65 NOTE_ON=58 TIME_DELTA=2 NOTE_OFF=58 NOTE_ON=60 TIME_DELTA=2 NOTE_OFF=60 NOTE_ON=62 TIME_DELTA=4 NOTE_OFF=62 BAR_END BAR_START NOTE_ON=62 TIME_DELTA=4 NOTE_OFF=62 NOTE_ON=63 TIME_DELTA=4 NOTE_OFF=63 NOTE_ON=63 TIME_DELTA=4 NOTE_OFF=63 NOTE_ON=63 TIME_DELTA=4 NOTE_OFF=63 BAR_END BAR_START NOTE_ON=63 TIME_DELTA=4 NOTE_OFF=63 NOTE_ON=63 TIME_DELTA=12 NOTE_OFF=63 BAR_END TRACK_END TRACK_START INST=0 BAR_START NOTE_ON=72 TIME_DELTA=4 NOTE_OFF=72 NOTE_ON=75 TIME_DELTA=4 NOTE_OFF=75 NOTE_ON=70 TIME_DELTA=4 NOTE_OFF=70 NOTE_ON=67 TIME_DELTA=4 NOTE_OFF=67 BAR_END BAR_START NOTE_ON=72 TIME_DELTA=2 NOTE_OFF=72 NOTE_ON=70 TIME_DELTA=2 NOTE_OFF=70 NOTE_ON=68 TIME_DELTA=4 NOTE_OFF=68 NOTE_ON=67 TIME_DELTA=4 NOTE_OFF=67 NOTE_ON=65 TIME_DELTA=4 NOTE_OFF=65 BAR_END BAR_START NOTE_ON=70 TIME_DELTA=4 NOTE_OFF=70 NOTE_ON=68 TIME_DELTA=4 NOTE_OFF=68 NOTE_ON=67 TIME_DELTA=4 NOTE_OFF=67 NOTE_ON=72 TIME_DELTA=4 NOTE_OFF=72 BAR_END BAR_START NOTE_ON=72 TIME_DELTA=4 NOTE_OFF=72 NOTE_ON=70 TIME_DELTA=12 NOTE_OFF=70 BAR_END TRACK_END TRACK_START INST=32 BAR_START NOTE_ON=53 TIME_DELTA=4 NOTE_OFF=53 NOTE_ON=48 TIME_DELTA=4 NOTE_OFF=48 NOTE_ON=50 TIME_DELTA=4 NOTE_OFF=50 NOTE_ON=51 TIME_DELTA=4 NOTE_OFF=51 BAR_END BAR_START NOTE_ON=48 TIME_DELTA=4 NOTE_OFF=48 NOTE_ON=53 TIME_DELTA=4 NOTE_OFF=53 NOTE_ON=55 TIME_DELTA=2 NOTE_OFF=55 NOTE_ON=57 TIME_DELTA=2 NOTE_OFF=57 NOTE_ON=58 TIME_DELTA=4 NOTE_OFF=58 BAR_END BAR_START NOTE_ON=55 TIME_DELTA=4 NOTE_OFF=55 NOTE_ON=48 TIME_DELTA=2 NOTE_OFF=48 NOTE_ON=50 TIME_DELTA=2 NOTE_OFF=50 NOTE_ON=51 TIME_DELTA=4 NOTE_OFF=51 NOTE_ON=44 TIME_DELTA=2 NOTE_OFF=44 NOTE_ON=46 TIME_DELTA=2 NOTE_OFF=46 BAR_END BAR_START NOTE_ON=48 TIME_DELTA=2 NOTE_OFF=48 NOTE_ON=50 TIME_DELTA=2 NOTE_OFF=50 NOTE_ON=51 TIME_DELTA=12 NOTE_OFF=51 BAR_END TRACK_END TRACK_START INST=24 BAR_START NOTE_ON=65 TIME_DELTA=4 NOTE_OFF=65 NOTE_ON=63 TIME_DELTA=4 NOTE_OFF=63 NOTE_ON=65 TIME_DELTA=2 NOTE_OFF=65 NOTE_ON=58 TIME_DELTA=2 NOTE_OFF=58 NOTE_ON=58 TIME_DELTA=4 NOTE_OFF=58 BAR_END BAR_START NOTE_ON=63 TIME_DELTA=2 NOTE_OFF=63 NOTE_ON=62 TIME_DELTA=2 NOTE_OFF=62 NOTE_ON=60 TIME_DELTA=2 NOTE_OFF=60 NOTE_ON=62 TIME_DELTA=2 NOTE_OFF=62 NOTE_ON=63 TIME_DELTA=4 NOTE_OFF=63 NOTE_ON=58 TIME_DELTA=4 NOTE_OFF=58 BAR_END BAR_START NOTE_ON=58 TIME_DELTA=4 NOTE_OFF=58 NOTE_ON=60 TIME_DELTA=4 NOTE_OFF=60 NOTE_ON=58 TIME_DELTA=4 NOTE_OFF=58 NOTE_ON=58 TIME_DELTA=4 NOTE_OFF=58 BAR_END BAR_START NOTE_ON=56 TIME_DELTA=4 NOTE_OFF=56 NOTE_ON=55 TIME_DELTA=12 NOTE_OFF=55 BAR_END TRACK_END PIECE_END
Надеюсь, этот краткий обзор объяснил вам, как работает MMM. А теперь самое интересное! Давайте возьмём датасет LMD Clean и преобразуем его в псевдослова.
Для токенизации датасета можно воспользоваться опенсорсными библиотеками, например, MidiTok (о которой говорилось выше) или Musicaiz. У обеих имеются замечательные возможности по настройке процесса токенизации. Однако в качестве начальной точки я решил взять репозиторий MMM-JSB и адаптировать его под Lakh Midi Dataset, потому что это позволит мне иметь больше контроля над процессом. Адаптированный репозиторий выложен здесь.
Из адаптированного репозитория удалены файлы со множественными тактовыми размерами и тактовыми размерами, отличающимися от 4/4. Кроме того, в нём добавлен токен GENRE= token
, чтобы мы могли управлять в инференсе жанром, который должна генерировать модель. Наконец, я решил не дискретизировать ноты, чтобы звуки были менее роботоподобными.
Для токенизации датасета можно использовать этот ноутбук. Однако стоит помнить, то этот процесс может быть долгим и у вас могут возникать ошибки. При желании можно его пропустить: я выложил токенизированный датасет в Hub, и его можно сразу использовать! Hugging Face позволяет с лёгкостью загружать датасеты. В данном случае я создал кадр данных (data frame) для очистки и исследования данных, а затем загрузил его в Hub как датасет.
# Устанавливаем датасеты
!pip install datasets
# Собираем файлы из нужной папки
import glob
dataset_files = glob.glob("/path/to/tokenized/dataset/*.txt")
# Загружаем файлы как датасет HF
from datasets import load_dataset
dataset = load_dataset("text", data_files=dataset_files)
# Преобразуем датасет в кадр данных
ds = dataset["train"]
df = ds.to_pandas()
# Выполняем очистку и исследование данных...
from datasets import Dataset
# Пребразуем DataFrame в датасет Hugging Face
clean_dataset = Dataset.from_pandas(df)
# Выполняем вход в аккаунт Hugging Face
from huggingface_hub import notebook_login
notebook_login()
# Загружаем датасет в свой аккаунт, заменив juancopi81 на своё имя
clean_dataset.push_to_hub("juancopi81/mmm_track_lmd_8bars_nots")
Полный ноутбук можно изучить здесь.
Обучаем токенизатор и модель
На этом этапе наш датасет уже должен быть отформатирован в виде псевдослов. Помните, что можно создать его, воспользовавшись предыдущей частью туториала, или просто взять готовый из Hub. Если у вас мало ресурсов или вы хотите протестировать эту часть туториала, то можно взять датасет меньшего размера. Например, я рекомендую в качестве более простой альтернативы датасет js-fakes-4bars. В зависимости от выбранного датасета (LMD | JS Fake) я добавлю ссылки на соответствующие ноутбуки.
Так как теперь у нас есть датасет псевдослов, то следующая часть будет очень похожа на обучение языковой модели, но язык будет состоять из музыкальных слов. Эта часть туториала во многом повторяет курс по NLP Hugging Face, в котором после получения датасета нужно обучить токенизатор.
Примечание: если вы незнакомы с токенизацией или обучением моделей, то рекомендую для лучшего понимания нашего туториала просмотреть курс.
Токенизатор
Colab для этой части туториала: LMD | JS Fake
В этом туториале мы будем обучать модель GPT-2. Эта модель имеет превосходные возможности обучения, она опенсорсная и к тому же Hugging Face существенно облегчил её обучение и использование. Однако GPT-2 не обучена на музыкальный язык, поэтому её нужно обучить заново, начав с токенизатора.
Чтобы проиллюстрировать предыдущий абзац, токенизируем несколько слов из нашего датасета при помощи стандартного токенизатора GPT-2:
# Берём выборку из датасета
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
# Стандартный токенизатор GPT-2, применённый к нашему датасету
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
# Необходимо задать токен UNK
new_tokenizer = Tokenizer(model=WordLevel(unk_token="[UNK]"))
# Добавляем предварительный токенизатор
from tokenizers.pre_tokenizers import WhitespaceSplit
new_tokenizer.pre_tokenizer = WhitespaceSplit()
# Протестируем наш 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.
# Получаем батчи по 1000 текстов
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
# Добавляем специальные токены
trainer = WordLevelTrainer(
special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"]
)
# Выполняем постобработку и загружаем его в 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']
Именно то, что мы и хотели! Отличная работа! Теперь у нас есть токенизатор в Hub для обучения модели GPT-2.
Модель
Colab для этой части туториала: LMD | JS Fake
Теперь, когда датасет и токенизатор готовы, настало время обучать модель. В этой части туториала мы выполним следующее:
Подготовим датасет для модели.
Выберем конфигурацию модели.
Обучим модель при помощи специального тренера. Специальный тренер позволит нам логировать результаты модели в процессе обучения в Weights and Biases (для этого нужен аккаунт W&B).
Подготовка датасета
Самое сложное мы уже сделали, так что подготовка датасета будет простым процессом. Нужно взять датасет с Hugging Face и воспользоваться новым токенизатором для создания токенизированного датасета. Именно эту токенизированную версию датасета GPT-2 ожидает в качестве входных данных.
# Импорт библиотек
from datasets import load_dataset
from transformers import AutoTokenizer
# Скачивание датасета, можно заменить на собственный датасет
ds = load_dataset("juancopi81/mmm_track_lmd_8bars_nots", split="train")
# У нас в ds есть только "train", поэтому мы создаём разбиение test и train
raw_datasets = ds.train_test_split(test_size=0.1, shuffle=True)
# Меняем на соответствующий токенизатор
tokenizer = AutoTokenizer.from_pretrained("juancopi81/lmd_8bars_tokenizer")
raw_datasets
raw_datasets
теперь содержит train и test.
DatasetDict({
train: Dataset({
features: ['text'],
num_rows: 159810
})
test: Dataset({
features: ['text'],
num_rows: 17757
})
})
Теперь токенизируем весь датасет. Это можно сделать множеством способов. В этом туториале мы будем обрезать все тексты (композиции) длиннее заданного context_length. В моделях трансформеров context_length
представляет максимальную длину последовательности (токенов), которую может обрабатывать модель. Эта длина часто ограничена из-за памяти или архитектуры модели.
# Значение 2048 неплохо подходит, но при желании его можно заменить
context_length = 2048
# Функция для токенизации датасета
def tokenize(element):
outputs = tokenizer(
element["text"],
truncation=True, #Удаление элемента длиннее, чем размер контекста, не влияет на JSB
max_length=context_length,
padding=False
)
return {"input_ids": outputs["input_ids"]}
# Создаём tokenized_dataset. Мы используем map для передачи каждого элемента датасета для токенизации и удаления ненужных столбцов.
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 (исследования, анализирующего взаимосвязи между размером модели, данными и показателями). Я адаптировал эту часть ноутбука из реализации Карпати.
Примечание: я пока совершенствую эту часть туториала, поэтому работа не завершена. При внесении изменений я соответствующим образом буду дополнять ноутбук. С удовольствием прочитаю ваши отзывы!
Для этого туториала мы используем небольшую версию (с малым количеством параметров), которая позволит нам обучить модель в условиях ограниченных ресурсов, а после обучения генерировать музыку быстрее. Как вы видели, демо из начала туториала не использует GPU и создаёт музыку за разумное время.
# Можно менять это в зависимости от размера данных
n_layer=6 # Количество слоёв трансформера
n_head=8 # Количество multi-head attention heads
n_emb=512 # Размер эмбеддингов
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)
Data Collator
Перед тем как начинать обучение, нужно создать батчи для модели. Кроме того, следует вспомнить, что входные данные используются как метки в каузальной языковой модели (Causal Language Model) (со сдвигом на один элемент), поэтому это тоже нужно учитывать. Но не беспокойтесь, всё это сделает за нас data collator Hugging Face, это определённо облегчит нашу жизнь!
from transformers import DataCollatorForLanguageModeling
# Он поддерживает и masked language modeling (MLM), и causal language modeling (CLM)
# Для CLM необходимо задать mlm=False
data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)
Обучение модели
Мы собрали всё вместе, и настал момент истины: обучение модели! В процессе обучения модели не следует действовать слепо, так что всегда стоит по ходу дела тестировать некоторые поколения. Эта часть работы доставляет удовольствие: вы будете слышать, как ваша ИИ-музыка будет эволюционировать со сменой эпох.
Для этого вам потребуется аккаунт Weights and Biases и настройка тренера так, чтобы он логировал музыку в eval_loop. Подробности можно посмотреть в ноутбуке, а здесь я приведу самый важный фрагмент:
from transformers import Trainer, TrainingArguments
# сначала создаём собственный тренер для логирования распределения прогнозов
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",
):
# вызываем метод класса super, чтобы получить вывод eval
eval_output = super().evaluation_loop(
dataloader,
description,
prediction_loss_only,
ignore_keys,
metric_key_prefix,
)
# логируем распределение прогнозов при помощи метода `wandb.Histogram`.
if wandb.run is not None:
input_ids = tokenizer.encode("PIECE_START STYLE=JSFAKES GENRE=JSFAKES TRACK_START", return_tensors="pt").cuda()
# Генерируем новые токены.
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
Создав собственный тренер, можно начать обучение модели. Для начала я использовал следующие параметры:
# Создаём аргументы для нашего тренера
from argparse import Namespace
# Получаем выходную папку с меткой времени
output_path = "output"
steps = 5000
# Закомментированные параметры
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)
# Используем созданный выше CustomTrainer
trainer = CustomTrainer(
model=model,
tokenizer=tokenizer,
args=train_args,
data_collator=data_collator,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["test"],
)
И запускаем обучение:
# Обучаем модель
trainer.train()
Используем Sweep для поиска улучшенных гиперпараметров
В предыдущем разделе мы обучили модель генерации музыки. И это отлично! А теперь давайте найдём для нашей модели более качественные гиперпараметры. Для этого существуют разные способы; я решил реализовать «sweep» из Weights and Biases из-за его пользовательского интерфейса и простоты использования.
Для задания sweep в W&B нужно сначала упорядочить код. На этом этапе мы разобьём предыдущий ноутбук на последовательность функций, которые можно вsзывать с различными аргументами. Примеры того, как это можно сделать, показаны по следующим ссылкам:
После упорядочивания кода можно задать конфигурацию sweep в файле YAML или в словаре Python. Эта конфигурация объяснит W&B стратегию исследования гиперпараметров, которую вы хотите реализовать. Давайте изучим этот файл:
# Исполняемая программа
program: train.py
# Метод может быть grid, random или bayes
method: random
# Проект, частью которого является этот sweep
project: mlops-001-lmdGPT
# Оптимизируемая метрика
metric:
name: eval/loss
goal: minimize
# Пространство параметров для поиска
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)
Чтобы сделать вывод из анализа, моно изучить результаты sweep в аккаунте W&B:
Теперь можно запустить наш скрипт для обучения модели с наилучшими гиперпараметрами из анализа. Например:
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
И на этом можно завершить часть обучения модели и токенизатора. Далее мы проверим показатели модели.
Демонстрация показателей модели в Hugging Face Space
После завершения обучения модели настало время её продемонстрировать! Можно создать пользовательский интерфейс (UI) модели и хостить своё приложение как Hugging Face Space. В этой части туториала мы просто сделаем это вместе. Помните, что для этого требуется аккаунт Hugging Face.
После создания нового space можно решить, какой SDK использовать. В этом туториале мы будем использовать Docker, чтобы иметь больше контроля над окружением нашего приложения. Ниже представлен Dockerfile, добавленный мной для демо ML:
FROM ubuntu:20.04
WORKDIR /code
# Чтобы пользователи могли делиться результатами с сообществом - настройте в соответствии со своими требованиями
ENV SYSTEM=spaces
ENV SPACE_ID=juancopi81/multitrack-midi-music-generator
COPY ./requirements.txt /code/requirements.txt
# Предварительная конфигурация tzdata
RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
DEBIAN_FRONTEND="noninteractive" apt-get install -y tzdata
# Важные пакеты для воспроизведения сгенерированной музыки
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
# Скачиваем исходники libfluidsynth
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
# Создаём нового пользователя "user"с user ID 1000
RUN useradd -m -u 1000 user
# Переключаемся на пользователя "user"
USER user
# Задаём в качестве home папку home пользователя user
ENV HOME=/home/user \
PATH=/home/user/.local/bin:$PATH
# Задаем в качестве рабочей папки папку home пользователя user
WORKDIR $HOME/app
# Копируем текущее содержимое папки в контейнер в $HOME/app, делая владельцем user
COPY --chown=user . $HOME/app
CMD ["python3", "main.py"]
Мы настраиваем образ из Ubuntu и устанавливаем необходимые пакеты наподобие FluidSynth, который будем использовать для воспроизведения сгенерированной музыки. В файле requirements.txt есть и другие необходимые пакеты Python. Изучить его можно здесь.
Ещё одна критически важная часть приложения — переход от сгенерированных моделью токенов к музыкальным нотам. На протяжении туториала я скрывал эту функцию, а теперь давайте посмотрим, как она работает. По сути, эта функция использует библиотеку note_seq Magenta для создания note_sequence, которую можно использовать для преобразования в MIDI или для воспроизведения. Вот код для этого; его авторство полностью принадлежит доктору Тристану Беренсу.
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:
"""
Преобразует последовательность токенов в последовательность нот.
Аргументы:
token_sequence (str): последовательность преобразуемых токенов.
qpm (float, optional): четвертных нот в минуту. По умолчанию 120.0.
use_program (bool, optional): использовать ли программу. По умолчанию True.
use_drums (bool, optional): использовать ли барабаны. По умолчанию True.
instrument_mapper (Optional[dict], optional): распределитель инструментов. По умолчанию None.
only_piano (bool, optional): использовать ли только пианино. По умолчанию False.
Возвращает:
NoteSequence: получившуюся последовательность нот.
"""
if isinstance(token_sequence, str):
token_sequence = token_sequence.split()
note_sequence = empty_note_sequence(qpm)
# Вычисление длительности нот и тактов на основании заданного QPM
note_length_16th = 0.25 * 60 / qpm
bar_length = 4.0 * 60 / qpm
# Рендеринг всех нот.
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
# Делаем инструменты правильными.
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:
"""
Создаёт пустую последовательность нот.
Аргументы:
qpm (float, optional): четвертных нот в минуту. По умолчанию 120.0.
total_time (float, optional): общее время. По умолчанию 0.0.
Возвращает:
NoteSequence: пустую последовательность нот.
"""
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:
"""
Генерирует новую последовательность инструмента по заданному seed и температуре.
Аргументы:
seed (str): строка порождающего значения для генерации.
temp (float, optional): температура для генерации, управляющая случайностью. По умолчанию 0.75.
Возвращает:
str: сгенерированную последовательность инструмента.
"""
seed_length = len(tokenizer.encode(seed))
while True:
# Кодируем условные токены.
input_ids = tokenizer.encode(seed, return_tensors="pt")
# Переносим тензор input_ids на то же устройство, что и модель
input_ids = input_ids.to(model.device)
# Генерируем новые токены.
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])
# Проверяем, содержится ли в сгенерированной последовательности "NOTE_ON" за пределами seed
new_generated_sequence = tokenizer.decode(generated_ids[0][seed_length:])
if "NOTE_ON" in new_generated_sequence:
# Если NOTE_ON, то выполняем возврат, потому что генерируем по одному инструменту за раз
return generated_sequence
Этот файл utils также содержит, среди прочих процессов, код удаления, изменения и повторной генерации инструмента.
Наконец, в файле main.py мы добавляем кнопки, которые пользователи могут использовать для взаимодействия с моделью.
# Фрагмент кода нажимаемых кнопок
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)
Давайте посмотрим, как кнопки выглядят в интерфейсе:
И теперь вы можете показать свою модель кому угодно. Разумеется, это здорово, но стоит подумать и о её влиянии. Давайте сделаем это вместе в следующем разделе.
Этические аспекты
Во-первых, хочу поблагодарить вас за то, что вы читаете этот туториал, он длинный и, возможно, немного пугает. Я максимально стремлюсь обеспечить его точность и качество, но признаю, что его можно улучшить и в нём, вероятно, есть ошибки. Я постоянно учусь и развиваюсь, поэтому буду признателен за любые отзывы и рекомендации по улучшению контента.
С другой стороны, после начала составления туториала я решил добавить в него размышления об этических аспектах. Я не специалист в этой теме, поэтому рекомендую читать мнения и мысли специалистов. Тем не менее, я бы хотел поделиться своим пониманием и опасениями.
В процессе генерации музыки при помощи ИИ нужно учесть множество аспектов:
Какова роль системы в творческом процессе?
Какое влияние могут оказать эти модели на рынок труда музыкантов?
Соблюдаем ли мы права исполнителей и композиторов, создавших музыку, которая используется для обучения моделей?
Кто становится владельцем сгенерированной музыки?
И этот список можно продолжить. На все эти вопросы ответить невозможно, поэтому мне бы хотелось сосредоточиться на одном особенно волнующем меня аспекте: цифровом барьере.
Цифровой барьер (digital divide) — «это неравный доступ к цифровым технологиям» (источник: Wikipedia), создающий опасный мост между теми, кто имеет доступ к информации и ресурсам, и теми, кто его не имеет.
Как насчёт цифрового барьера в мире музыки?
«Музыка — универсальный язык человечества», — Генри Лонгфеллоу
Музыка — универсальный язык, выходящий за рамки границ, культур и эпох. Она существует во всех цивилизациях. Однако живые традиции затмеваются или даже забываются в угоду мейнстримной музыки, в том числе и потому, что отдельные группы имеют больший доступ к цифровым платформам, инструментам создания и распространения музыки.
Машинное обучение, особенно при его демократизации, может быть инструментом сохранения и интеграции недостаточно представленной музыки. При наличии подходящих датасетов модели машинного обучения могут анализировать, генерировать и классифицировать непопулярную музыку. Но также они могут усиливать и поддерживать искажения, поскольку сегодня большинство из нас обучает модели для генерации рока, джаза или классической европейской музыки. Сообщество даже придумало термин «кран Баха», так как многие модели теперь могут синтезировать музыку, почти идентичную музыке Баха: «Кран Баха — это ситуация, при которой генеративная система создаёт бесконечный поток контента на уровне или выше уровня какого-то культурно ценимого оригинала, но из-за бесконечности потока он перестаёт быть редким, а значит, становится менее ценным» (из Twitter).
Кроме того, внедрение других художественных традиций лишь улучшит готовые модели и обогатит музыкальные творения. Исполнители-новаторы демонстрируют этот потенциал, например, Hexorsismos или Yaboi Hanoi, выигравший 2022 AI Song Contest с мелодиями и саунд-дизайном, вдохновлёнными тайской культурой.
yaboihanoi · อสุระเทวะชุมนุม - Enter Demons & Gods
С более разнообразным представлением музыки в ИИ возникает множество сложностей, в том числе, доступность данных, инвестиции, образование и многие другие. Опенсорс-сообщество имеет уникальные возможности по решению некоторых из этих проблем благодаря совместной работе над созданием более разнообразных датасетов, разработке инклюзивных моделей, обмену свободными туториалами и в целом проектированию инструментов, отдающих должное более широкому спектру традиций.
Машинное обучение формирует совершенно новую реальность — как сказал профессор Эндрю Ын, это новое электричество. Возможно, мы приближаемся к революции ИИ, которая изменит наш мир, и все мы должны иметь право голоса и участвовать в этом развитии, вне зависимости от языка, культуры, национальности, уровня образования и этнической принадлежности. Представьте, какие опасности могут таиться в мощном инструменте, контролируемом несколькими влиятельными людьми или группами.
Я призываю вас активно участвовать в прогрессе более инклюзивного ИИ. Например, вы можете вступить в опенсорс-сообщество, какой бы уровень опыта вы ни имели. Ценен любой вклад, а объединённые усилия мотивированных людей способны творить чудеса. Вы можете найти возможности сотрудничества с любым уровнем знаний в Discord Hugging Face. Кроме того, я родился в Колумбии и хотел бы создать в Hugging Face качественный датасет (MIDI или аудио) с недостаточно широко представленной музыкой Латинской Америки. Напишите мне, если хотите присоединиться.
vassabi
вот кстати да.
причем - весь последний абзац можно жирным выделить. Это полезно всем.