#
Дообучение модели для классификации музыки

В этом разделе мы представим пошаговое руководство по дообучению модели трансформера, использующей только кодеровщик, для классификации музыки. Для демонстрации мы будем использовать облегченную модель и достаточно небольшой набор данных, что означает, что код может быть запущен на любом GPU потребительского класса, включая GPU T4 16GB, предоставляемый в рамках бесплатного уровня Google Colab. В разделе приведены различные советы, которые можно попробовать при использовании GPU меньшего размера и возникновении проблем с нехваткой памяти.

##
Набор данных

Для обучения нашей модели мы будем использовать набор данных GTZAN, который представляет собой популярный набор данных из 1000 песен для классификации музыкальных жанров. Каждая песня представляет собой 30-секундный клип из одного из 10 музыкальных жанров - от диско до металла. Мы можем получить аудиофайлы и соответствующие им метки из Hugging Face Hub с помощью функции `load_dataset()` из 🤗 Datasets:

    from datasets import load_dataset

    gtzan = load_dataset("marsyas/gtzan", "all")
    gtzan

Output:

    Dataset({
        features: ['file', 'audio', 'genre'],
        num_rows: 999
    })

Одна из записей в GTZAN повреждена, поэтому она была удалена из набора данных. Поэтому мы имеем 999 примеров вместо 1000.

GTZAN не предоставляет предопределенного валидационного набора, поэтому нам придется создать его самостоятельно. Набор данных сбалансирован по жанрам, поэтому мы можем использовать метод `train_test_split()` для быстрого создания разбиения в пропорции 90/10 следующим образом:

    gtzan = gtzan.train_test_split(seed=42, shuffle=True, test_size=0.1)
    gtzan

Output:

    DatasetDict({
        train: Dataset({
            features: ['file', 'audio', 'genre'],
            num_rows: 899
        })
        test: Dataset({
            features: ['file', 'audio', 'genre'],
            num_rows: 100
        })
    })

Отлично, теперь, когда у нас есть обучающий и тестовый наборы, давайте посмотрим на один из аудиофайлов:

    gtzan["train"][0]

Output:

    {
        "file": "~/.cache/huggingface/datasets/downloads/extracted/fa06ce46130d3467683100aca945d6deafb642315765a784456e1d81c94715a8/genres/pop/pop.00098.wav",
        "audio": {
            "path": "~/.cache/huggingface/datasets/downloads/extracted/fa06ce46130d3467683100aca945d6deafb642315765a784456e1d81c94715a8/genres/pop/pop.00098.wav",
            "array": array(
                [
                    0.10720825,
                    0.16122437,
                    0.28585815,
                    ...,
                    -0.22924805,
                    -0.20629883,
                    -0.11334229,
                ],
                dtype=float32,
            ),
            "sampling_rate": 22050,
        },
        "genre": 7,
    }

Как мы видели в Разделе 1, аудиофайлы представлены в виде одномерных массивов NumPy, где значение массива представляет собой амплитуду на заданном временном интервале. Для этих композиций частота дискретизации составляет 22 050 Гц, то есть в секунду дискретизируется 22 050 значений амплитуды. Это необходимо учитывать при использовании предварительно обученной модели с другой частотой дискретизации, самостоятельно преобразуя частоты дискретизации для обеспечения их соответствия. Мы также видим, что жанр представлен в виде целого числа, или class label, то есть в том формате, в котором модель будет делать свои предсказания. Воспользуемся методом `int2str()` функции `genre` для преобразования этих целых чисел в человекочитаемые имена:

    id2label_fn = gtzan["train"].features["genre"].int2str
    id2label_fn(gtzan["train"][0]["genre"])

Output:

    'pop'

Эта метка выглядит корректно, поскольку совпадает с именем аудиофайла. Теперь рассмотрим еще несколько примеров на примере Gradio для создания простого интерфейса с API `Blocks`:

    import gradio as gr

    def generate_audio():
        example = gtzan["train"].shuffle()[0]
        audio = example["audio"]
        return (
            audio["sampling_rate"],
            audio["array"],
        ), id2label_fn(example["genre"])

    with gr.Blocks() as demo:
        with gr.Column():
            for _ in range(4):
                audio, label = generate_audio()
                output = gr.Audio(audio, label=label)

    demo.launch(debug=True)

Из этих образцов мы, конечно, можем услышать разницу между жанрами, но может ли это сделать трансформер? Давайте обучим модель, чтобы выяснить это! Для начала нам нужно найти подходящую для этой задачи предварительно обученную модель. Посмотрим, как это можно сделать.

##
Выбор предварительно обученной модели для классификации звука

Для начала выберем подходящую предварительно обученную модель для классификации звука. В этой области предварительное обучение обычно проводится на больших объемах немаркированных аудиоданных, используя такие наборы данных, как LibriSpeech и Voxpopuli. Лучший способ найти эти модели на Hugging Face Hub - использовать фильтр “Audio Classification”, как описано в предыдущем разделе. Хотя такие модели, как Wav2Vec2 и HuBERT, очень популярны, мы будем использовать модель под названием DistilHuBERT. Это гораздо более компактная (или дистиллированная) версия модели HuBERT, которая обучается примерно на 73% быстрее, сохраняя при этом большую часть производительности.

##
От аудио к машинному обучению

##
Предварительная обработка данных

Подобно токенизации в NLP, аудио- и речевые модели требуют, чтобы входные данные были закодированы в формате, который модель может обрабатывать. В 🤗 Transformers преобразование звука во входной формат осуществляется с помощью feature extractor модели. Подобно токенизаторам, 🤗 Transformers предоставляет удобный класс `AutoFeatureExtractor`, который может автоматически выбирать нужный экстрактор признаков для заданной модели. Для того чтобы увидеть, как мы можем обрабатывать наши аудиофайлы, давайте начнем с инстанцирования экстрактора признаков для DistilHuBERT из предварительно обученной контрольной точки:

    from transformers import AutoFeatureExtractor

    model_id = "ntu-spml/distilhubert"
    feature_extractor = AutoFeatureExtractor.from_pretrained(
        model_id, do_normalize=True, return_attention_mask=True
    )

Поскольку частота дискретизации модели и набора данных различна, перед передачей аудиофайла в программу извлечения признаков его необходимо передискретизировать до 16 000 Гц. Для этого сначала нужно получить частоту дискретизации модели от экстрактора признаков:

    sampling_rate = feature_extractor.sampling_rate
    sampling_rate

Output:

    16000

Далее мы проводим повторную выборку набора данных, используя метод `cast_column()` и функцию `Audio` из 🤗 Datasets:

    from datasets import Audio

    gtzan = gtzan.cast_column("audio", Audio(sampling_rate=sampling_rate))

Теперь мы можем проверить первый сэмпл train-split нашего набора данных, чтобы убедиться, что он действительно находится на частоте 16 000 Гц. При загрузке каждого сэмпла Datasets производит повторную дискретизацию аудиофайла “на лету”:

    gtzan["train"][0]

Output:

    {
        "file": "~/.cache/huggingface/datasets/downloads/extracted/fa06ce46130d3467683100aca945d6deafb642315765a784456e1d81c94715a8/genres/pop/pop.00098.wav",
        "audio": {
            "path": "~/.cache/huggingface/datasets/downloads/extracted/fa06ce46130d3467683100aca945d6deafb642315765a784456e1d81c94715a8/genres/pop/pop.00098.wav",
            "array": array(
                [
                    0.0873509,
                    0.20183384,
                    0.4790867,
                    ...,
                    -0.18743178,
                    -0.23294401,
                    -0.13517427,
                ],
                dtype=float32,
            ),
            "sampling_rate": 16000,
        },
        "genre": 7,
    }

Отлично! Мы видим, что частота дискретизации была понижена до 16 кГц. Значения массива также отличаются, так как теперь на каждые 1,5 значения амплитуды приходится примерно одно значение, которое мы имели раньше.

Отличительной особенностью моделей типа Wav2Vec2 и HuBERT является то, что они принимают на вход массив float, соответствующий исходной форме речевого сигнала. В отличие от других моделей, например Whisper, в которых мы предварительно обрабатываем исходную форму звукового сигнала до формата спектрограммы.

Мы уже упоминали, что аудиоданные представлены в виде одномерного массива, поэтому они уже имеют правильный формат для чтения моделью (набор непрерывных входов с дискретными временными шагами). Чтож, что именно делает экстрактор признаков?

Итак, аудиоданные имеют правильный формат, но мы не наложили никаких ограничений на значения, которые они могут принимать. Для оптимальной работы нашей модели необходимо, чтобы все входные данные находились в одном и том же динамическом диапазоне. Это позволит получить одинаковый диапазон активаций и градиентов для наших образцов, что поможет обеспечить стабильность и сходимость в процессе обучения.

Для этого мы нормализуем наши аудиоданные, приводя каждую выборку к нулевому среднему и единичной дисперсии - этот процесс называется масштабированием признаков. Именно эту нормализацию и выполняет наш экстрактор признаков!

Мы можем посмотреть, как работает экстрактор признаков, применив его к нашему первому аудиосэмплу. Сначала вычислим среднее значение и дисперсию наших исходных аудиоданных:

    import numpy as np

    sample = gtzan["train"][0]["audio"]

    print(f"Mean: {np.mean(sample['array']):.3}, Variance: {np.var(sample['array']):.3}")

Output:

    Mean: 0.000185, Variance: 0.0493

Видно, что среднее значение уже близко к нулю, но дисперсия ближе к 0,05. Если бы дисперсия для выборки была больше, это могло бы вызвать проблемы с нашей моделью, так как динамический диапазон аудиоданных был бы очень мал и, следовательно, трудноразделим. Применим экстрактор признаков и посмотрим, что получится на выходе:

    inputs = feature_extractor(sample["array"], sampling_rate=sample["sampling_rate"])

    print(f"inputs keys: {list(inputs.keys())}")

    print(
        f"Mean: {np.mean(inputs['input_values']):.3}, Variance: {np.var(inputs['input_values']):.3}"
    )

Output:

    inputs keys: ['input_values', 'attention_mask']
    Mean: -4.53e-09, Variance: 1.0

Отлично! Наш экстрактор признаков возвращает словарь, состоящий из двух массивов: `input_values` and `attention_mask`. `input_values` это предварительно обработанные входные аудиоданные, которые мы передадим в модель HuBERT. `attention_mask` используется, когда мы обрабатываем batch аудиовходов одновременно - она используется для того, чтобы сообщить модели, где у нас есть входы разной длины.

Мы видим, что среднее значение теперь очень сильно приближается к нулю, а дисперсия - к единице! Именно в таком виде мы хотим получить наши аудиосэмплы перед подачей их в модель HuBERT.

Обратите внимание, как мы передали частоту дискретизации наших аудиоданных нашему экстрактору признаков. Это хорошая практика, так как экстрактор признаков выполняет проверку под капотом, чтобы убедиться, что частота дискретизации наших аудиоданных соответствует частоте дискретизации, ожидаемой моделью. Если частота дискретизации аудиоданных не совпадает с частотой дискретизации нашей модели, то необходимо увеличить или уменьшить частоту дискретизации аудиоданных до нужной.

Отлично, теперь мы знаем, как обрабатывать наши ресэмплированные аудиофайлы, осталось определить функцию, которую мы можем применить ко всем примерам в наборе данных. Поскольку мы ожидаем, что длина аудиоклипов будет составлять 30 секунд, мы также будем обрезать все более длинные клипы, используя аргументы `max_length` и `truncation` в экстракторе признаков следующим образом:

    max_duration = 30.0

    def preprocess_function(examples):
        audio_arrays = [x["array"] for x in examples["audio"]]
        inputs = feature_extractor(
            audio_arrays,
            sampling_rate=feature_extractor.sampling_rate,
            max_length=int(feature_extractor.sampling_rate * max_duration),
            truncation=True,
            return_attention_mask=True,
        )
        return inputs

Определив эту функцию, мы можем применить ее к набору данных с помощью метода `map()`. Метод `.map()` поддерживает работу с пакетами сэмплов, что мы и сделаем, установив `batched=True`. По умолчанию размер пакета составляет 1000, но мы уменьшим его до 100, чтобы пиковая оперативная память оставалась в разумных пределах для бесплатного уровня Google Colab:

    gtzan_encoded = gtzan.map(
        preprocess_function,
        remove_columns=["audio", "file"],
        batched=True,
        batch_size=100,
        num_proc=1,
    )
    gtzan_encoded

Output:

    DatasetDict({
        train: Dataset({
            features: ['genre', 'input_values','attention_mask'],
            num_rows: 899
        })
        test: Dataset({
            features: ['genre', 'input_values','attention_mask'],
            num_rows: 100
        })
    })

Если при выполнении приведенного выше кода оперативная память устройства будет исчерпана, можно настроить параметры пакетной обработки, чтобы уменьшить пиковое потребление оперативной памяти. В частности, можно модифицировать следующие два аргумента: * `batch_size`: по умолчанию 1000, но выше было установлено значение 100. Попробуйте еще раз уменьшить в 2 раза до 50 * `writer_batch_size`: по умолчанию равен 1000. Попробуйте уменьшить его до 500, а если это не сработает, то уменьшите его еще раз в 2 раза до 250

Для упрощения обучения мы удалили из набора данных столбцы `audio` и `file`. Столбец `input_values` содержит закодированные аудиофайлы, `attention_mask` - двоичную маску из значений 0/1, указывающую на то, куда мы добавили входной аудиосигнал, а столбец `genre` - соответствующие метки (или цели). Для того чтобы `Trainer` мог обрабатывать метки классов, необходимо переименовать колонку `genre` в `label`:

    gtzan_encoded = gtzan_encoded.rename_column("genre", "label")

Наконец, нам необходимо получить отображения меток из набора данных. Это отображение позволит нам перейти от целочисленных идентификаторов (например, `7`) к человекочитаемым меткам классов (например, `"поп"`) и обратно. Таким образом, мы можем преобразовать целочисленное предсказание id нашей модели в человекочитаемый формат, что позволит нам использовать модель в любом последующем приложении. Для этого можно использовать метод `int2str()` следующим образом:

    id2label = {
        str(i): id2label_fn(i)
        for i in range(len(gtzan_encoded["train"].features["label"].names))
    }
    label2id = {v: k for k, v in id2label.items()}

    id2label["7"]

    'pop'

Итак, у нас есть набор данных, готовый к обучению! Давайте рассмотрим, как можно обучить модель на этом наборе данных.

##
Дообучение модели

Для дообучения модели мы воспользуемся классом `Trainer` из раздела 🤗 Transformers. Как мы уже видели в других главах, `Trainer` - это высокоуровневый API, предназначенный для работы с наиболее распространенными сценариями обучения. В данном случае мы будем использовать `Trainer` для дообучения модели на GTZAN. Для этого сначала нужно загрузить модель для данной задачи. Для этого мы можем использовать класс `AutoModelForAudioClassification`, который автоматически добавит соответствующую классификационную голову в нашу предварительно обученную модель DistilHuBERT. Давайте перейдем к инстанцированию модели:

    from transformers import AutoModelForAudioClassification

    num_labels = len(id2label)

    model = AutoModelForAudioClassification.from_pretrained(
        model_id,
        num_labels=num_labels,
        label2id=label2id,
        id2label=id2label,
    )

Мы настоятельно рекомендуем во время тренировок загружать контрольные точки моделей непосредственно на Hugging Face Hub. Hugging Face Hub предоставляет:

- Встроенный контроль версий: вы можете быть уверены, что ни одна контрольная точка модели не будет потеряна в процессе обучения.
- Журналы Tensorboard: отслеживание важных показателей в процессе обучения.
- Карты моделей: документирование того, что делает модель, и предполагаемых вариантов ее использования.
- Сообщество: простой способ обмена информацией и сотрудничества с сообществом! 🤗

Связать ноутбук с Hugging Face Hub очень просто - для этого достаточно ввести аутентификационный токен при появлении соответствующего запроса. Найдите свой токен аутентификации здесь:

    from huggingface_hub import notebook_login

    notebook_login()

Output:

    Login successful
    Your token has been saved to /root/.huggingface/token

Следующим шагом является определение аргументов обучения, включая размер пакета, шаги накопления градиента, количество эпох обучения и скорость обучения:

    from transformers import TrainingArguments

    model_name = model_id.split("/")[-1]
    batch_size = 8
    gradient_accumulation_steps = 1
    num_train_epochs = 10

    training_args = TrainingArguments(
        f"{model_name}-finetuned-gtzan",
        evaluation_strategy="epoch",
        save_strategy="epoch",
        learning_rate=5e-5,
        per_device_train_batch_size=batch_size,
        gradient_accumulation_steps=gradient_accumulation_steps,
        per_device_eval_batch_size=batch_size,
        num_train_epochs=num_train_epochs,
        warmup_ratio=0.1,
        logging_steps=5,
        load_best_model_at_end=True,
        metric_for_best_model="accuracy",
        fp16=True,
        push_to_hub=True,
    )

Здесь мы установили значение `push_to_hub=True`, чтобы включить автоматическую загрузку настроенных контрольных точек во время обучения. Если вы не хотите, чтобы ваши контрольные точки загружались на Hugging Face Hub, вы можете установить значение `False`.

Последнее, что нам необходимо сделать, это определить метрики. Поскольку набор данных сбалансирован, в качестве метрики мы будем использовать accuracy и загружать ее с помощью библиотеки 🤗 Evaluate:

    import evaluate
    import numpy as np

    metric = evaluate.load("accuracy")

    def compute_metrics(eval_pred):
        """Computes accuracy on a batch of predictions"""
        predictions = np.argmax(eval_pred.predictions, axis=1)
        return metric.compute(predictions=predictions, references=eval_pred.label_ids)

Теперь у нас есть все необходимые компоненты! Давайте инстанцируем `Trainer` и обучим модель:

    from transformers import Trainer

    trainer = Trainer(
        model,
        training_args,
        train_dataset=gtzan_encoded["train"],
        eval_dataset=gtzan_encoded["test"],
        tokenizer=feature_extractor,
        compute_metrics=compute_metrics,
    )

    trainer.train()

В зависимости от используемого графического процессора, при запуске обучения возможно возникновение ошибки CUDA `"out-of-memory"`. В этом случае можно уменьшать `batch_size` постепенно в 2 раза, а для компенсации использовать [`gradient_accumulation_steps`](https://huggingface.co/docs/transformers/main_classes/trainer#transformers.TrainingArguments.gradient_accumulation_steps)

Output:

    | Training Loss | Epoch | Step | Validation Loss | Accuracy |
    |:-------------:|:-----:|:----:|:---------------:|:--------:|
    | 1.7297        | 1.0   | 113  | 1.8011          | 0.44     |
    | 1.24          | 2.0   | 226  | 1.3045          | 0.64     |
    | 0.9805        | 3.0   | 339  | 0.9888          | 0.7      |
    | 0.6853        | 4.0   | 452  | 0.7508          | 0.79     |
    | 0.4502        | 5.0   | 565  | 0.6224          | 0.81     |
    | 0.3015        | 6.0   | 678  | 0.5411          | 0.83     |
    | 0.2244        | 7.0   | 791  | 0.6293          | 0.78     |
    | 0.3108        | 8.0   | 904  | 0.5857          | 0.81     |
    | 0.1644        | 9.0   | 1017 | 0.5355          | 0.83     |
    | 0.1198        | 10.0  | 1130 | 0.5716          | 0.82     |

Обучение займет примерно 1 час в зависимости от вашего GPU или выделенного для Google Colab. Наша лучшая доля верных ответов составляет 83% - неплохо для 10 эпох с 899 примерами обучающих данных! Конечно, мы могли бы улучшить этот результат, тренируясь на большем количестве эпох, используя методы регуляризации, такие как dropout, или разбивая каждый аудиопример на сегменты по 30 и 15 секунд, чтобы использовать более эффективную стратегию предварительной обработки данных.

Большой вопрос, как это соотносится с другими системами классификации музыки 🤔 Для этого мы можем просмотреть autoevaluate leaderboard, таблицу лидеров, которая классифицирует модели по языку и набору данных, а затем ранжирует их по accuracy.

Мы можем автоматически отправить нашу контрольную точку в таблицу лидеров при передаче результатов обучения в Hugging Face Hub - для этого достаточно задать соответствующие аргументы ключевых слов (kwargs). Вы можете изменить эти значения в соответствии с набором данных, языком и названием модели:

    kwargs = {
        "dataset_tags": "marsyas/gtzan",
        "dataset": "GTZAN",
        "model_name": f"{model_name}-finetuned-gtzan",
        "finetuned_from": model_id,
        "tasks": "audio-classification",
    }

Теперь результаты обучения можно загрузить в Hugging Face Hub. Для этого выполните команду `.push_to_hub`:

    trainer.push_to_hub(**kwargs)

При этом журналы обучения и веса моделей будут сохранены под именем `"your-username/distilhubert-finetuned-gtzan"`. Для примера посмотрите загрузку по адресу `"sanchit-gandhi/distilhubert-finetuned-gtzan"`.

##
Поделиться моделью

Теперь вы можете поделиться этой моделью со всеми желающими, воспользовавшись ссылкой на Hub. Они могут загрузить его с идентификатором `"your-username/distilhubert-finetuned-gtzan"` непосредственно в `pipeline()`. Например, для загрузки точно настроенной контрольной точки `"sanchit-gandhi/distilhubert-finetuned-gtzan"`:

    from transformers import pipeline

    pipe = pipeline(
        "audio-classification", model="sanchit-gandhi/distilhubert-finetuned-gtzan"
    )

##
Заключение

В этом разделе мы рассмотрели пошаговое руководство по тонкой настройке модели DistilHuBERT для классификации музыки. Хотя мы сосредоточились на задаче классификации музыки и на наборе данных GTZAN, представленные здесь шаги применимы в более общем случае к любой задаче классификации звука - тот же сценарий может быть использован для задач классификации звука разговорного языка, таких как выделение ключевых слов или идентификация языка. Вам просто нужно поменять набор данных на тот, который соответствует интересующей вас задаче! Если вы заинтересованы в тонкой настройке других моделей Hugging Face Hub для классификации звука, мы рекомендуем вам ознакомиться с другими примерами в 🤗 репозитории Transformers.

В следующем разделе мы возьмем модель, которую вы только что отладили, и создадим демонстрационный образец классификации музыки, который вы сможете опубликовать на Hugging Face Hub.