В этой статье я расскажу о том, как создавался проект HuggingArtists и что у него под капотом.
Мне будет очень приятно, если вы поставите сразу звезду в репозитории:
Вступление
В HuggingArtists, мы можем создавать тексты песен на основе конкретного исполнителя. Это было сделано путем fine-tune (точной настройки) предварительно обученного трансформера HuggingFace на собранных данных Genius.
Кроме того, мы используем интеграцию Weights & Biases для автоматического учета производительности и прогнозов модели.
Полный отчет с красивыми и интерактивными графиками можно прочитать тут.
Все запуски сохраняются и визуализируются с помощью Weights & Biases и сохраняются по ссылке:
Disclaimer: Данный проект не предназначен для публикации какой-либо ложной информации или неприятных слов, а предназначен для проведения исследований по Natural Language Generation.
Примеры генераций
Eminem:
I am a bit of a nutter
Like a fucking crazy nut
But I just wanted to break and get a little more serious, so be patient
I dont think I need no medical attention
But I do know that these niggas gonna be in my prayers
And they say Im a nutter
Like a fucking crazy nut
But I love you
I am a nutter
Like a fuckin nutter
But I feel like the greatest that I can be
With your body and your spirits
But I dont know how I will act
When I finally get out
And I get pissed off
Ill be right back at ya with the first one on the floor
But I aint ever gonna get the respect youve been getting
And Im a nutter...
I am bipolar while I’m rhyming, bipolar while I am hate
When I’m rhyming, I may seem a little bit crazy
But the thing’s with these rhymes that I pick
I stick to the syllable ç unless it’s on a syllable
Bread is full of charmin I am thinkin all of the garbage
Hear the bass line and ya hear the bass line
But hip hop is what makes’em songs
I try to describe how I’m gettin’ em all, but I’m lazy
When I’m rapin on a song, I’m just making it
If I don’t got the right, In my mind...
Drake:
I am never livin the life that I never lived
I walk around with my Glock 9
And lay down with my feet up
This is my house, bitch
Welcome to Detroit, this is my city
Ive made it all my people proud
I even sold my house down
To get a possible placement as a burnt pit
My little girl, she should ntroller
Wanna flip sometimes on the upside
But its hard to hold a grudge, I dont bite
I say my wholehearted, prayer is my heart
I pray for you when Im gone, but I gotta leave you in peace
That we did not plan to be seeing each other, well
We did plan to be seeing each other, well
And if you ever need anything, I...
Собираем датасет
Перед тренировкой нашей нейросети, мы должны собрать датасет и привести его в должный вид. Так как чистые данные — залог успеха. Поэтому убираем из него повторяющиеся пробелы и новые строки.
Парсим данные
Все данные были собраны с Genius с помощью этого скрипта.
Здесь мы используем asyncio и aiohttp для парализации сбора данных.
Все наборы данных доступны здесь: ссылка.
Пример использования в Colab:
!python parse.py \
--artist_id=$ARTIST_ID\
--token=$GENIUS_API_TOKEN\
--save_path=$SAVE_PATH
Как использовать датасеты
Как загрузить набор данных непосредственно из библиотеки datasets
(пример — Eminem):
from datasets import load_dataset
dataset = load_dataset("huggingartists/eminem")
Структура датасета
Пример «train» выглядит следующим образом:
Этот пример был слишком длинным и был обрезан:
{
"text": "Look, I was gonna go easy on you\nNot to hurt your feelings\nBut I'm only going to get this one chance\nSomething's wrong, I can feel it..."
}
Поля датасета
Поля данных одинаковы для всех разбиений.
- text: строка.
Разделение данных
Все данные сгруппированы в «train», но можно легко разделить на «train», «validation» и «test» с помощью нескольких строк кода:
from datasets import load_dataset, Dataset, DatasetDict
import numpy as np
datasets = load_dataset("huggingartists/eminem")
train_percentage = 0.9
validation_percentage = 0.07
test_percentage = 0.03
train, validation, test = np.split(datasets['train']['text'], [int(len(datasets['train']['text'])*train_percentage), int(len(datasets['train']['text'])*(train_percentage + validation_percentage))])
datasets = DatasetDict(
{
'train': Dataset.from_dict({'text': list(train)}),
'validation': Dataset.from_dict({'text': list(validation)}),
'test': Dataset.from_dict({'text': list(test)})
}
)
Как использовать модели
Проще всего использовать Colab за пару минут:
Также вы можете использовать любую модель непосредственно с помощью pipeline
для генерации текста:
from transformers import pipeline
generator = pipeline('text-generation',
model='huggingartists/eminem')
generator("I am", num_return_sequences=5)
Или с библиотекой трансформеров:
from transformers import AutoTokenizer, AutoModelWithLMHead
tokenizer = AutoTokenizer.from_pretrained("huggingartists/eminem")
model = AutoModelWithLMHead.from_pretrained("huggingartists/eminem")
Предварительная обработка данных
Мы будем использовать tokenizer
, чтобы наша нейросеть могла понимать текст. Tokenizer переводит текст в цифры.
Например:
encoded_input = tokenizer("Hello, I'm a single sentence!")
print(encoded_input)
Результат:
{
'input_ids': [101, 138, 18696, 155, 1942, 3190, 1144, 1572, 13745, 1104, 159, 9664, 2107, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}
Программа возвращает словарь. input_ids
— это индексы, соответствующие каждому токену в нашем предложении. Про attention_mask
и token_type_ids
можно почитать в документации.
Токенизатор может декодировать список идентификаторов токенов в соответствующем предложении:
tokenizer.decode(encoded_input["input_ids"], add_special_tokens=False)
Результат:
"Hello, I'm a single sentence!"
Теперь нам нужно тожесамое сделать с нашими данными. Мы можем вызвать токенизатор для всех наших текстов. Это очень просто, используя метод map
из библиотеки datasets
. Сначала мы определяем функцию, которая вызывает токенизатор в наших текстах:
def tokenize_function(examples):
return tokenizer(examples["text"])
Затем мы применяем его ко всем разделениям в нашем объекте datasets
, используя batched=True
и 4 процесса для ускорения предварительной обработки. После этого нам не понадобится текстовая колонка, поэтому мы ее отбрасываем.
tokenized_datasets = datasets.map(tokenize_function, batched=True, num_proc=4, remove_columns=["text"])
Если мы теперь посмотрим на элемент наших наборов данных, мы увидим, что текст был заменен на input_ids
, которые понадобятся модели:
tokenized_datasets["train"][1]
Результат:
{
'attention_mask': [1, 1, 1, 1, 1, 1],
'input_ids': [238, 8576, 9441, 2987, 238, 252]
}
Теперь самое сложное: нам нужно объединить все наши тексты вместе, а затем разделить результат на небольшие фрагменты определенного размера блока. Для этого мы снова будем использовать метод map
с параметром batched=True
. Эта опция фактически позволяет нам изменять количество примеров в наборах данных, возвращая другое количество примеров, чем мы получили. Таким образом, мы можем создавать наши новые образцы из серии примеров.
Во-первых, мы берем максимальную длину, с которой наша модель была предварительно обучена. Это может быть слишком большим, чтобы поместиться в оперативной памяти вашего графического процессора, поэтому здесь мы берем немного меньше-всего.
# block_size = tokenizer.model_max_length
block_size = int(tokenizer.model_max_length / 4)
Затем мы пишем функцию предварительной обработки, которая будет группировать наши тексты:
def group_texts(examples):
# Concatenate all texts.
concatenated_examples = {k: sum(examples[k], []) for k in examples.keys()}
total_length = len(concatenated_examples[list(examples.keys())[0]])
# We drop the small remainder, we could add padding if the model supported it instead of this drop, you can
# customize this part to your needs.
total_length = (total_length // block_size) * block_size
# Split by chunks of max_len.
result = {
k: [t[i : i + block_size] for i in range(0, total_length, block_size)]
for k, t in concatenated_examples.items()
}
result["labels"] = result["input_ids"].copy()
return result
Во-первых, обратите внимание, что мы дублируем входные данные для наших меток. Это связано с тем, что модель библиотеки Transformers применяет смещение вправо, поэтому нам не нужно делать это вручную.
Также обратите внимание, что по умолчанию метод map
отправит пакет из 1000 примеров для обработки функцией предварительной обработки. Поэтому здесь мы отбросим оставшуюся часть, чтобы сделать объединенные маркированные тексты кратными block_size
каждые 1000 примеров. Вы можете настроить это, передав больший размер пакета (который также будет обрабатываться медленнее). Вы также можете ускорить предварительную обработку с помощью многопроцессорной обработки:
lm_datasets = tokenized_datasets.map(
group_texts,
batched=True,
batch_size=1000,
num_proc=1,
)
Ура! Наши данные готовы!
Настраиваем тренера
Тренер — это простой, но функциональный цикл обучения и оценки для PyTorch, оптимизированный для трансформеров.
from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
f"output/{model_name}",
overwrite_output_dir=True,
learning_rate=1.372e-4,
weight_decay=0.01,
num_train_epochs=num_train_epochs,
save_total_limit=1,
save_strategy='epoch',
save_steps=1,
seed=seed_data,
logging_steps=5,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=lm_datasets["train"],
)
Тренируем нейросеть
Запускаем тренировку и ждем:
trainer.train()
Ожидаем завершения и сохраняем модель куда удобно.
Анализируем результаты
Полученые результаты оказались очень даже неплохими. В них присутствует хорошая рифма и даже бэки с аирбэками. Если в полученых результатах присутствуют многочисленные повторения, то модель недостаточно натренирована.
Все запуски сохраняются и визуализируются с помощью Weights & Biases и сохраняются по ссылке:
Полный отчет с красивыми и интерактивными графиками можно прочитать тут.
Заключение
Если вы хотите внести свой вклад в этот проект ИЛИ создать что-то классное вместе — свяжитесь со мной: ссылка.