Постановка задачи определения интента по фразе клиента, полученной в текстовом виде

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

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

Запросы клиента представляют собой фразы в текстовом формате вида «я хотел бы сменить тариф для интернета».

В связи с этим встает задача разработки автоматизированной системы классификации обращений клиентов для их дальнейшей обработки.

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

Представленная задача решается методом мультиклассовой классификации.

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

  • использование классификаторов поверх мешка слов будет являться неплохим бейзлайном (например, LogReg + CountVectorizer на символьных и/или словесных n-граммах);

  • классификаторы поверх агрегированных предобученных словесных эмбеддингов (Word2Vec, fastText, GloVe);

  • классификаторы поверх предобученных эмбеддингов предложений, полученных из моделей LaBSE, USE, LASER;

  • fine-tuning трансформерных моделей-энкодеров, таких как BERT, RoBERTa или DistilBERT с добавлением последнего линейного слоя в качестве классификатора.

Данные для обучения модели были размечены на 26 классов.

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

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

В качестве метрик оценки качества модели была выбраны F1 Weighted и F1 Macro, применяемые при решение задач классификации с дисбалансом классов.

В ходе проведения экспериментов по обучению различных моделей и оценки
их качества по выбранной метрике, лучшим решением оказался подход с fine-tuning
модели BERT под текущую downstream-задачу.

Необходимость быстрого инференса модели на ЦП

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

Для линейных классификаторов, использующих мешок слов в качестве текстовых признаков данное ограничение таковым не является, чего нельзя сказать про трансформерные модели с десятками миллионов обучаемых параметров и размером, занимаемым моделью на диске более 700 Мб (для полноразмерных моделей BERT и RoBERTa).

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

Подходы для ускорения инференса нейросетевых моделей

В настоящее время существует множество способов ускорения инференса нейронных сетей, которые можно разделить между собой на 3 группы:

  • дистилляция знаний;

  • уменьшение размера модели;

  • изменение формата представления модели.

Дистилляция знаний – это метод при котором маленькая «модель-ученик» обучается имитировать поведение большой «модели-учителя» или целого ансамбля «моделей-учителей» (предсказания «модели-учителя» на обучающей выборке).

Способы уменьшения размера модели [1]:

  • факторизация – замена тензоров высокой размерности на тензоры низкой размерности, чаще всего применяется в ядрах сверточных нейронных сетей;

  • прунинг – уменьшение размера модели с помощью замены части весов модели нулевыми значениями;

  • квантизация – уменьшение размера модели путем изменения численной точности весов модели с FP32 до FP16 или INT8.

Способы сериализации модели в формат, позволяющий повысить скорость инференса:

  • TorchScript – способ создания сериализуемых и оптимизируемых моделей с помощью фреймворка PyTorch для дальнейшего переиспользования;

  • ONNX – открытый стандарт для конвертации моделей машинного обучения из разных фреймворков в единый формат, а также для обмена моделями между фреймворками;

  • ONNX Runtime – библиотека для кроссплатформенного ускорения обучения и инференса моделей;

  • OpenVINO – набор инструментов, позволяющий экспортировать модель в промежуточный формат после обучения и провести оптимизированный инференс на оборудовании Intel.

Формат представления моделей ONNX и ускоритель инференса моделей ONNX Runtime

Для решения проблем, связанных с поддержкой множественных форматов моделей машинного обучения, которые генерируются различными фреймворками, в конце 2017 года был создан формат Open Neural Network eXchange (ONNX) в качестве стандарта с открытым исходным кодом.

Модели, обученные в различных фреймворках, могут быть преобразованы в формат ONNX для упрощения переносимости моделей. В настоящее время поддерживается около 25 фреймворков для экспорта модели в формат ONNX, таких как: PyTorch, Keras, TensorFlow, CatBoost, Scikit-Learn, XGBoost и другие (Рисунок 1) [2].

Рисунок 1 – Поддерживаемые ONNX фреймворки
Рисунок 1 – Поддерживаемые ONNX фреймворки

Формат модели ONNX представляет собой статический вычислительный граф, в котором вершинами являются вычислительные операторы, а ребра отвечают за последовательность передачи данных по вершинам (Рисунок 2).

Рисунок 2 – Часть графа модели «cointegrated/rubert-tiny2» в формате ONNX, изображенная с помощью инструмента Netron
Рисунок 2 – Часть графа модели «cointegrated/rubert-tiny2» в формате ONNX, изображенная с помощью инструмента Netron

При экспорте модели в формат ONNX сохраняется только математическая формула, необходимая для вычисления выходной величины по входной величине (при преобразовании в формат ONNX из модели удаляются данные, которые были необходимы только на этапе обучения, что позволяет снизить объем потребляемых ресурсов). Как правило, обученные веса являются константами в математической формуле [3].

Модель в формате ONNX является файлом в формате Protocol Buffers, который представляет собой формат файла сообщений, разработанный Google и также используемый Tensorflow [4].

В Protocol Buffers указываются типы данных, такие как Float32, и порядок данных, значение каждого из которых зависит от используемого программного обеспечения, концептуально это похоже на JSON.

К преимуществам использования формата ONNX можно отнести следующее:

  • кроссплатформенность: можно создать модель с помощью Python и PyTorch на Linux, а затем развернуть эту модель в десктопном приложении на Windows с помощью C#;

  • уменьшение количества зависимостей в проекте: не нужно иметь полный набор инструментов, как при обучении модели, достаточно использовать только ONNX Runtime в зависимостях проекта;

  • открытый исходный код проекта, широкая поддержка компаний-гигантов.

ONNX Runtime предоставляют собой среду выполнения, разрабатываемую компанией Microsoft, позволяющую осуществлять инференс модели формата ONNX в различных операционных системах, архитектурах, аппаратных ускорителях, на различных языках программирования (Рисунок 3) [5].

Рисунок 3 – Возможные комбинации программных и аппаратных средств 
для инференса ONNX-модели
Рисунок 3 – Возможные комбинации программных и аппаратных средств для инференса ONNX-модели

Какие комбинации компонентов среды выполнения выбрать, зависит от общего варианта использования модели: приложение для смартфонов, десктопное приложение, серверное решение.

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

  1. Обучение модель в одном из распространенных фреймворков, например, PyTorch и Transformers (для задач обработки естественного языка).

  2. Экспорт модели в формат ONNX, фиксирующий математическое ядро модели, независящий от языка программирования.

  3. В производственной среде формула, состоящая из «прямых» расчетов модели, восстанавливается как функция без поддержки состояния с помощью ONNX Runtime.

  4. Функция без поддержки состояния развертывается на базе веб-фреймворка FastAPI, обеспечивающего доступ к модели средствами REST.

Сравнительный анализ скорости инференса моделей на ЦП с помощью нативного PyTorch и связки ONNX - ONNX Runtime

В ходе fine-tuning-а моделей под текущую задачу классификации текста использовались и оценивались следующие модели архитектуры BERT:

  1. Дистиллированный DistilRuBERT-tiny-5k с уменьшенным размером словаря в 5 000 токенов и числом параметров 3,6 миллиона –https://huggingface.co/DeepPavlov/distilrubert-tiny-cased-conversational-5k.

  2. Дистиллированный DistilRuBERT-tiny с уменьшенным размером словаря в 30 000 токенов и числом параметров 10,4 миллиона – https://huggingface.co/DeepPavlov/distilrubert-tiny-cased-conversational-v1.

  3. Дистиллированная версия полноразмерной многоязычной версии BERT с поддержкой только русского и английского языков, с 83828 токенами и числом параметров 29,2 миллиона – https://huggingface.co/cointegrated/rubert-tiny2.

  4. Полноразмерная модель RuBERT, обученная на русскоязычной Wikipedia и новостных статьях, с размером словаря в 100 000 токенов
    и числом параметров 180 миллионов – https://huggingface.co/DeepPavlov/rubert-base-cased.

Перед началом тестов, каждая из 4-х представленных моделей была переведена в формат ONNX c помощью PyTorch (Листинг 1), а затем квантизирована до численной точности весов INT8 средствами ONNX (Листинг 2).

Тестовый стенд имеет следующую конфигурацию:

  • ОС – Windows 10 Корпоративная LTSC, сборка 19044.1889;

  • ЦП – Intel(R) Core(TM) i5-1135G7, 2.40GHz;

  • ОЗУ – 16 Гб;

  • Версия Python – 3.8.10.

Версии фреймворков и библиотек приведены в таблице 1.

Таблица 1 – Версии фреймоворков и библиотек, используемых в тестах

Название фреймворка/библиотеки

Версия

torch

1.12.1

transformers

4.21.1

onnx

1.12.0

onnxconverter-common

1.12.1

onnxruntime

1.12.1

onnxruntime-tools

1.7.0

import torch
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification
)


def convert_from_torch_to_onnx(
        onnx_path: str,
        tokenizer: AutoTokenizer,
        model: AutoModelForSequenceClassification
) -> None:
    """Конвертация модели из формата PyTorch в формат ONNX.

    @param onnx_path: путь к модели в формате ONNX
    @param tokenizer: токенизатор
    @param model: модель
    """
    dummy_model_input = tokenizer(
        "текст для конвертации",
        padding="max_length",
        truncation=True,
        max_length=512,
        return_tensors="pt",
    ).to("cpu")
    torch.onnx.export(
        model,
        dummy_model_input["input_ids"],
        onnx_path,
        opset_version=12,
        input_names=["input_ids"],
        output_names=["output"],
        dynamic_axes={
            "input_ids": {
                0: "batch_size",
                1: "sequence_len"
            },
            "output": {
                0: "batch_size"
            }
        }
    )
    

Листинг 1 – Функция для конвертации модели в формат ONNX с помощью PyTorch

from onnxruntime.quantization import (
    quantize_dynamic,
    QuantType
)


def convert_from_onnx_to_quantized_onnx(
        onnx_model_path: str,
        quantized_onnx_model_path: str
) -> None:
    """Квантизация модели в формате ONNX до Int8
    и сохранение кванитизированной модели на диск.

    @param onnx_model_path: путь к модели в формате ONNX
    @param quantized_onnx_model_path: путь к квантизированной модели
    """
    quantize_dynamic(
        onnx_model_path,
        quantized_onnx_model_path,
        weight_type=QuantType.QUInt8
    )
    

Листинг 2 – Функция для квантизации модели в формате ONNX

Код функции для инференса модели с помощью PyTorch представлен на листинге 3. Начиная с версии PyTorch 1.9, разработчики рекомендуют использовать новый контекстный менеджер для инференса – «with torch. inference_mode()».

Перед тем, как использовать модель в формате ONNX для инференса c помощью ONNX Runtime, необходимо создать сессию, код представлен на листинг 4.

Код функции для инференса модели c помощью ONNX Runtime представлен на листинге 5.

Замер времени инференса осуществлялся с помощью декоратора над функциями «pytorch_inference» (листинг 3) и «onnx_inference» (листинг 5), токенизация текста производилась внутри функций для инференса в обоих случаях.

import torch
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification
)


def pytorch_inference(
        text: str,
        max_tokens: int,
        model: AutoModelForSequenceClassification,
        tokenizer: AutoTokenizer,
) -> torch.Tensor:
    """Инференс модели с помощью PyTorch.

    @param text: входной текст для классификации
    @param max_tokens: максимальная длина последовательности в токенах
    @param model: BERT-модель
    @param tokenizer: токенизатор
    @return: логиты на выходе из модели
    """
    inputs = tokenizer(
        text,
        padding="max_length",
        truncation=True,
        max_length=max_tokens,
        return_tensors="pt"
    ).to('cpu')
    with torch.inference_mode():
        outputs = model(**inputs).logits.detach()
    return outputs
  

Листинг 3 – Функция для инференса модели c с помощью PyTorch

import onnxruntime
from onnxruntime import (
    InferenceSession,
    SessionOptions
)


def create_onnx_session(
        model_path: str,
        provider: str = "CPUExecutionProvider"
) -> InferenceSession:
    """Создание сессии для инференса модели с помощью ONNX Runtime.

    @param model_path: путь к модели в формате ONNX
    @param provider: инференс на ЦП
    @return: ONNX Runtime-сессия
    """
    options = SessionOptions()
    options.graph_optimization_level = \
        onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
    session = InferenceSession(model_path, options, providers=[provider])
    session.disable_fallback()
    return session
  

Листинг 4 – Функция для создания сессии ONNX Runtime

import numpy as np
from transformers import AutoTokenizer
from onnxruntime import InferenceSession


def onnx_inference(
        text: str,
        session: InferenceSession,
        tokenizer: AutoTokenizer,
        max_length: int
) -> np.ndarray:
    """Инференс модели с помощью ONNX Runtime.

    @param text: входной текст для классификации
    @param session: ONNX Runtime-сессия
    @param tokenizer: токенизатор
    @param max_length: максимальная длина последовательности в токенах
    @return: логиты на выходе из модели
    """
    inputs = tokenizer(
        text,
        padding="max_length",
        truncation=True,
        max_length=max_length,
        return_tensors="np",
    )
    input_feed = {
        "input_ids": inputs["input_ids"].astype(np.int64)
    }
    outputs = session.run(
        output_names=["output"],
        input_feed=input_feed
    )[0]
    return outputs
  

Листинг 5 – Функция для инференса модели c помощью ONNX Runtime

Сравнение времени инференса в зависимости от размера моделей

Производительность инференса всех 4-х моделей оценивалась на одной фиксированной фразе клиента вида «как бы мне оператора услышать?».

Инференс каждой модели состоял из 3-х тестов:

  • инференс с помощью PyTorch;

  • инференс полноразмерной модели ONNX с помощью ONNX Runtime;

  • инференс кванитизированной модели ONNX с помощью ONNX Runtime.

Замеры времени каждого типа инференса производились в 10 000 испытаний для оценки среднего значения и СКО.

Результаты замеров времени инференса моделей представлены на рисунке 4.

Для всех моделей, кроме полноразмерной модели RuBERT «DeepPavlov/rubert-base-cased», переход на инференс с помощью ONNX Runtime давал уменьшение времени выполнения в ~ 2,1-2,3 раз по сравнению с PyTorch, а также уменьшение времени инференса кванитизированной модели ONNX по сравнению с полноразмерной моделью ONNX в ~1,4 -1,5 раз.

Для полноразмерной модели RuBERT «rubert-base-cased» аналогичные показатели отличаются и равны: PyTorch-ONNX Runtime – 1,8 раз, полноразмерная модель ONNX – квантизированная модель ONNX – 2,4 раз.

По результатам первого теста скорости инференса для дальнейших испытаний была выбрана модель «cointegrated/rubert-tiny2».

Эта модель имела скорость инференса на уровне моделей DistilRuBERT-tiny-5k и DistilRuBERT-tiny, но качество на 2 процентных пункта выше.

Полноразмерная модель RuBERT «rubert-base-cased» показала лучшее качество по метрикам среди всех моделей (выше на 0.5 п.п.), но была самой медленной на инференсе, что не удовлетворяло условию задачи.

Рисунок 4 – Среднее время инференса 4-х моделей архитектуры BERT на ЦП на одном примере
Рисунок 4 – Среднее время инференса 4-х моделей архитектуры BERT на ЦП на одном примере

Сравнение времени инференса модели в зависимости от размера батча

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

Производительность инференса оценивалась на одной фиксированной фразе клиента вида «как бы мне оператора услышать?».

В тесте размер батча варьировался от 1 до 128.

Замеры времени каждого типа инференса производились в 10 000 испытаний для оценки среднего значения и СКО.

Результаты замеров инференса модели «cointegrated/rubert-tiny2» в зависимости от размера батча представлены на рисунке 5.

Максимальное ускорение скорости инференса полноразмерной модели ONNX по сравнению с инференсом на PyTorch наблюдается на размерах батча 1, 2, 4 и 8 минимальное – на размерах батча 32 и 64.

Максимальное ускорение скорости инференса кванитизированной модели ONNX по сравнению с полноразмерной моделью ONNX наблюдается на размерах батча свыше 32, а минимальное – от 1 до 16.

Рисунок 5 - Среднее время инференса модели «cointegrated/rubert-tiny2» в зависимости от размера батча
Рисунок 5 - Среднее время инференса модели «cointegrated/rubert-tiny2» в зависимости от размера батча

Сравнение инференса модели в зависимости от длины последовательности

Производительность инференса в зависимости от длины последовательности оценивалась на длинном тексте (более 512 токенов), который обрезался токенизатором в зависимости от переданной длины последовательности.

Длина последовательности текста варьировалась от 16 до 512 токенов.

Замеры времени каждого типа инференса производились в 10 000 испытаний для оценки среднего значения и СКО.

Результаты замеров скорости инференса модели «cointegrated/rubert-tiny2» в зависимости от длины последовательности текста представлены на рисунке 6.

При увеличении длины последовательности ускорение времени инференса полноразмерной модели ONNX по сравнению с инференсом на PyTorch также возрастает и достигает максимального значения на длине последовательности в 512 токенов.

Ускорение инференса кванитизированной модели ONNX по сравнению с полноразмерной моделью ONNX на всех длинах последовательностей примерна постоянна и равняется ~ 1,25 раза.

Рисунок 6 – Среднее время инференса модели «cointegrated/rubert-tiny2» в зависимости от длины последовательности
Рисунок 6 – Среднее время инференса модели «cointegrated/rubert-tiny2» в зависимости от длины последовательности

По результатам проведенных тестов можно сделать следующие выводы:

  1. Вне зависимости от используемой модели архитектуры BERT, на реальных текстовых данных скорость инференса значительно возрастает при переходе на формат модели ONNX и ускоритель инференса ONNX Runtime.

  2. Применение квантизации для модели средствами ONNX также дает прирост скорости инференса по сравнению с полноразмерной моделью ONNX, но в данной ситуации необходимо оценивать метрики после квантизации модели и искать компромисс.

  3. На маленьких размерах батча 1,2 и 4 наблюдается максимальный рост скорости инференса модели с помощью ONNX Runtime по сравнению с PyTorch.

  4. При увеличении длины последовательности ускорение времени инференса полноразмерной модели ONNX по сравнению с инференсом на PyTorch также возрастает.

  5. Конвертация модели в формат ONNX поддерживается большинством фреймворков машинного обучения, достаточно проста и быстра при разработке моделей популярных архитектур, в частности, в сфере NLP и моделей архитектуры BERT.

Список использованных источников

  1. Chip Huyen. Designing Machine Learning Systems. Sebastopol: O’Reilly Media, Inc., 2022. – 206 р.

  2. ONNX supported tools, URL: https://onnx.ai/supported-tools.html (дата обращения: 09.12.2022).

  3. В. Лакшманан. Машинное обучение. Паттерны проектирования. – СПб.: БХВ-Петербург, 2022. – 448 с.: ил.

  4. Interoperable AI: High-Performance Inferencing of ML and DNN Models Using Open-Source Tools, URL: https://odsc.medium.com/interoperable-ai-high-performance-inferencing-of-ml-and-dnn-models-using-open-source-tools-6218f5709071 (дата обращения: 09.12.2022).

  5. ONNX Runtime, URL: https://onnxruntime.ai/ (дата обращения: 09.12.2022).

Комментарии (6)


  1. Andriljo
    13.12.2022 00:28
    +2

    Отличная статья. Но хотел бы поправить, что LaBSE - это language agnostic BERT sentence encoder и эта модель не с родни USE или LASER, это трансформер BERT с 3мя тасками (MLM, NSP, paraphrasing sentence representation) вместо только первых 2ух как у BERT.


    1. AntonyZak Автор
      13.12.2022 08:24
      +2

      Спасибо большое за уточнение!


  1. snakers4
    15.12.2022 10:18
    +1

    Хорошая статья, таких мало на Хабре сейчас.

    От себя добавлю чего не хватает / что делает исследование немного неполным:

    • У вас вроде процессор с 4 ядрами и 8 потоками. У обоих фреймворков могут быть разные значения доступных им "threads" по умолчанию. Надо запускать при прочих равных по идее;

    • У PyTorch тоже есть очень простая встроенная динамическая квантизация. По-хорошему с ней тоже стоит сравнивать. Иногда бывает, что с квантизацией разница по скорости несущественная;

    Стандартная оговорка, что некоторые кастомные модели сложнее конвертировать в ONNX, чет просто сделать JIT script и иногда пакеты торча и JIT-пакеты торча (там есть просто условно компилятор, а есть еще прямо пакетирование кода) позволяют удобно упаковать и немного обфусцировать код.

    В целом наблюдал похожие выводы, но у меня было четко видно, что ONNX быстрее работает с короткими последовательностями, но у меня модели были немного другие.


    1. AntonyZak Автор
      15.12.2022 10:59

      Спасибо Вам за развернутые замечания по статье!

      Для PyTorch и ONNX Runtime количество "threads" установлено было в значение 1 (видимо, при переносе кода забыл указать, что для PyTorch - "torch.set_num_threads(1)", для ONNX Runtime - "options.intra_op_num_threads = 1").

      PyTorch-квантизацию и JIT попробую в будущих исследованиях.


    1. AntonyZak Автор
      15.12.2022 11:30

      А какие Вы модели конвертировали в ONNX?


      1. snakers4
        15.12.2022 11:32
        +1

        Например эту - https://github.com/snakers4/silero-vad