Pytorch
TensorFlow

#
Внутри конвейера

Это первый раздел, содержание которого будет немного отличаться в зависимости от того, используете ли вы PyTorch или TensorFlow. Нажмите переключатель над заголовком, чтобы выбрать предпочитаемую платформу!

Давайте начнем с готового примера, взглянув на то, что происходило за кулисами, когда мы выполняли следующий код в Главе 1:

    from transformers import pipeline

    classifier = pipeline("sentiment-analysis")
    classifier(
        [
            "I've been waiting for a HuggingFace course my whole life.",
            "I hate this so much!",
        ]
    )

и на выходе получали:

    [{'label': 'POSITIVE', 'score': 0.9598047137260437},
     {'label': 'NEGATIVE', 'score': 0.9994558095932007}]

Как мы уже увидели в Главе 1, данный конвейер включает в себя три шага: предварительная обработка, передача входных данных через модель и постобработка:

Давайте вкратце рассмотрим каждый из этих этапов.

##
Предварительная обработка с помощью токенизатора

Как и другие нейронные сети, модели Transformer не могут обрабатывать необработанный текст напрямую, поэтому первым шагом нашего конвейера является преобразование входных текстовых данных в числа, понятные модели. Для этого мы используем токенизатор, который будет отвечать за:

- Разделение входных данных на слова, подслова или символы (например, знаки препинания), которые называются токенами
- Отображение каждого токена в целое число
- Добавление дополнительных входных данных, которые могут быть полезны для модели

Всю эту предварительную обработку необходимо выполнять точно так же, как и при предварительном обучении модели, поэтому сначала нам нужно загрузить эту информацию из Model Hub. Для этого мы используем класс `AutoTokenizer` и его метод `from_pretrained()`. Используя имя контрольной точки нашей модели, он автоматически извлекает данные, связанные с токенизатором модели, и кэширует их (поэтому они загружаются только при первом запуске кода ниже).

Поскольку контрольной точкой конвейра `sentiment-analysis` по-умолчанию является модель `distilbert-base-uncased-finetuned-sst-2-english` (вы можете увидеть карточку модели здесь), мы выполним следующие команды:

    from transformers import AutoTokenizer

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)

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

Вы можете использовать библиотеку 🤗 Transformers, не беспокоясь о том, какой фреймворк ML используется в качестве бэкэнда; это может быть PyTorch или TensorFlow или Flax для некоторых моделей. Однако модели Transformer принимают только тензоры в качестве входных данных. Если вы впервые слышите о тензорах, вы можете представлять их как массивы NumPy. Массив NumPy может быть скаляром (0D), вектором (1D), матрицой (2D) или иметь больше измерений. Фактически это тензор; тензоры других платформ машинного обучения ведут себя аналогично, и обычно их так же просто создавать, как массивы NumPy.

Чтобы указать тип тензоров, которые мы хотим получить (PyTorch, TensorFlow, или обычный NumPy), мы используем аргумент `return_tensors`:

    raw_inputs = [
        "I've been waiting for a HuggingFace course my whole life.",
        "I hate this so much!",
    ]
    inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
    print(inputs)

Не беспокойтесь пока о параметрах дополнения (padding) и усечения (truncation); мы объясним это позже. Здесь главное помнить, что вы можете передать одно предложение или список предложений, а также указать тип тензоров, которые вы хотите получить обратно (если тип не передан, в результате вы получите список из списков).

Вот как результаты выглядят в виде тензоров PyTorch:

    {
        'input_ids': tensor([
            [  101,  1045,  1005,  2310,  2042,  3403,  2005,  1037, 17662, 12172, 2607,  2026,  2878,  2166,  1012,   102],
            [  101,  1045,  5223,  2023,  2061,  2172,   999,   102,     0,     0,     0,     0,     0,     0,     0,     0]
        ]),
        'attention_mask': tensor([
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        ])
    }

Сам вывод представляет собой словарь, содержащий два ключа, `input_ids` и `attention_mask`. `input_ids` содержит две строки целых чисел (по одному для каждого предложения), которые являются уникальными идентификаторами токенов в каждом предложении. Мы объясним, что такое `attention_mask` позже в этой главе.

##
Проходим через модель

Мы можем загрузить нашу предварительно обученную модель так же, как и наш токенизатор. Библиотека 🤗 Transformers предоставляет класс `AutoModel` который также имеет метод `from_pretrained()`:

    from transformers import AutoModel

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    model = AutoModel.from_pretrained(checkpoint)

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

Эта архитектура содержит только базовый модуль Transformer: при некоторых входных данных он выводит то, что мы будем называть скрытыми состояниями, также известными как параметры. Для каждого входного набора данных модели мы получим многомерный вектор, представляющий контекстное понимание этого входного набора моделью Transformer.

Если вы пока не понимаете в чем смысл, не беспокойтесь об этом. Мы объясним все это позже.

Хотя эти скрытые состояния могут быть полезны сами по себе, они обычно являются входными данными для другой части модели, известной как слой head. В Главе 1 разные задачи могли бы выполняться с одной и той же архитектурой, но с каждой из этих задач будет связан отдельный слой “head”.

###
Многомерный вектор, что это?

Вектор, выводимый модулем Transformer, обычно является большим. И как правило, он имеет три параметра:

- Размер пакета: Количество последовательностей, обрабатываемых одновременно (в нашем примере 2).
- Длина последовательности: Длина числового представления последовательности (в нашем примере 16).
- Размер скрытого слоя сети: Количество измерений вектора каждого входного параметра модели.

Его называют “многомерный” из-за последнего значения. Размер скрытого слоя сети может быть очень большим (768 обычно используется для небольших моделей, а в больших моделях он может достигать 3072 или более).

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

    outputs = model(**inputs)
    print(outputs.last_hidden_state.shape)

    torch.Size([2, 16, 768])

Обратите внимание, что выходные данные моделей 🤗 Transformers ведут себя как именованные кортежи или словари. Вы можете получить доступ к элементам по атрибутам (как это сделали мы) или по ключу (`outputs["last_hidden_state"]`), или даже по индексу, если вы точно знаете, где находится то, что вы ищете (`outputs[0]`).

###
Слои “head” модели: Разбираемся в цифрах

Слои “head” модели принимают многомерный вектор скрытых состояний в качестве входных данных и проецируют их в другое измерение. Они обычно состоят из одного или нескольких линейных слоев:

Выходные данные модели Transformer отправляются непосредственно в слой “head” модели для обработки.

На данной диаграмме модель представлена слоем вложений и последующими слоями. Слой вложений преобразует каждый входной идентификатор полученный токенизатором, в вектор, который представляет собой связанный с ним токен. Последующие слои манипулируют этими векторами, используя механизм внимания, чтобы получить окончательное представление предложений.

В 🤗 Transformersдоступно множество различных архитектур, каждая из которых предназначена для решения конкретной задачи. Вот неполный список:

- `*Model` (извлекает скрытые состояния)
- `*ForCausalLM`
- `*ForMaskedLM`
- `*ForMultipleChoice`
- `*ForQuestionAnswering`
- `*ForSequenceClassification`
- `*ForTokenClassification`
- и другие 🤗

Для нашего примера нам понадобится модель со слоем “head” для классификации последовательностей (чтобы иметь возможность классифицировать предложения как положительные или отрицательные). Итак, на самом деле мы будем использовать не класс `AutoModel`, а `AutoModelForSequenceClassification`:

    from transformers import AutoModelForSequenceClassification

    checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
    model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
    outputs = model(**inputs)

Теперь, если мы посмотрим на форму наших входных данных, размерность будет намного ниже: слой “head” модели принимает в качестве входных данных многомерные векторы, которые мы видели ранее, и выводит векторы, содержащие всего два значения (по одному на метку):

    print(outputs.logits.shape)

    torch.Size([2, 2])

Поскольку у нас всего два предложения и две метки, результат, который мы получаем от нашей модели, имеет форму 2 x 2.

##
Постобработка выходных данных

Значения, которые мы получаем в качестве выходных данных нашей модели, не обязательно имеют смысл сами по себе. Давайте посмотрим:

    print(outputs.logits)

    tensor([[-1.5607,  1.6123],
            [ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)

Наша модель предсказала `[-1.5607, 1.6123]` для первого предложения и `[ 4.1692, -3.3464]` для второго. Это не вероятности, а логиты, необработанные, ненормализованные оценки, выводимые последним слоем модели. Для преобразования в вероятности, они должны пройти через слой SoftMax (все модели 🤗 Transformers выводят логиты, поскольку функция потерь для обучения обычно объединяет последнюю функцию активации, такую как SoftMax, с фактической функцией потерь, такой как перекрестная энтропия):

    import torch

    predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
    print(predictions)

    tensor([[4.0195e-02, 9.5980e-01],
            [9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)

Теперь мы видим, что модель предсказала `[0.0402, 0.9598]` для первого предложения и `[0.9995, 0.0005]` для второго. Это легко узнаваемые оценки вероятности.

Чтобы получить метки, соответствующие каждой позиции, мы можем проверить атрибут `id2label` в конфигурации модели (подробнее об этом в следующем разделе):

    model.config.id2label

    {0: 'NEGATIVE', 1: 'POSITIVE'}

Теперь мы можем сделать вывод, что модель предсказала следующее:

- Первое предложение: NEGATIVE: 0.0402, POSITIVE: 0.9598
- Второе предложение: NEGATIVE: 0.9995, POSITIVE: 0.0005

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

✏️ Попробуйте это сделать! Выберите два (или более) собственных текста и пропустите их через конвейер `sentiment-analysis`. Затем повторите шаги, которые вы видели здесь, и убедитесь, что вы получаете такие же результаты!