Делаем intent classifier для службы поддержки без доменного датасета на русском

В этой статье я продемонстрирую, как без собственного датасета сделать классификатор намерений пользователя для службы поддержки в сфере e-commerce. И более того, я расскажу, как у меня получилось сделать классфикатор для русского языка без датасета на русском языке.

Меня зовут Елизавета Колмакова, я Data Scientist в компании, которая разрабатывает айти-решения для крупного ритейла.

6226c13ca3d5b8e01f13b32e0030d574.png

Intent classifier — модель, которая способна определить, с какой проблемой или целью обратился пользователь в службу поддержки. Когда мы говорим про систему определения интентов в NLP, классический подход предполагает наличие размеченного датасета и использование широко применяемых архитектур для классификации интентов, таких как BERT. Например, сообщение пользователя «Как вернуть товар?» будет помечено как намерение «Возврат товара». Далее такой датасет используется для обучения модели, которая способна выявлять интенты в тексте. Однако существуют и множество других решений, которые могут пригодиться в более строгих условиях задачи, например, в случае отсутствия размеченных данных. Как один из способов, можно воспользоваться методами обучения без учителя, такими как кластеризация текстовых сообщений по близости, что позволяет группировать сообщения с похожими интентами вместе, не требуя разметки данных. 

Но давайте более подробно рассмотрим этот классический подход.

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

  2. Данные: Традиционно, для создания интент-классификатора необходим размеченный датасет с намерениями и соответствующими им текстовыми примерами. Это список намерений с полным набором примеров для каждого из намерений. Число примеров сильно зависит от задачи, но, в среднем по больнице, 300 примеров на каждое намерение будет минимально достаточным для обучения.

  3. Метрики: Для оценки качества классификатора используются метрики, такие как F1-score на тестовом наборе данных. Это позволяет определить точность и полноту классификации, что дает более полную картину о производительности модели.

То есть, имеем такую структуру для первой пробы решения:

73238e046294bfe29f80d88e6cb0dc12.png


Архитектура — использование Bert классификатора (что является относительно простой задачей).

100226aa4b94cf5409d22dcd09097c22.png


Данные — размеченный набор намерений с 300 примерами на каждый интент (есть вопросики)

9648eb42c4c60cee0157ea828b7189ad.png

Метрика для проверки — f1-score на тестовом наборе данных, что обеспечивает надежное оценивание производительности модели.

Однако, что если мы начнем с того, что у нас нет собственных размеченных данных? Классический подход далеко не всегда оказывается применим на практике. В моем случае данные не были собраны для этой задачи, в моем распоряжении была всего лишь небольшая история сообщений без разметки. И надо понимать, что создание собственного датасета — это процесс, который:

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

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

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

В этот же момент я начала думать над архитектурой другого решения, основанного на принципах активного обучения (Active learning). Одно из применений такого подхода, называемого few-shot learning, предполагает использование большой языковой модели, например, ChatGPT, для определения интентов. В таком подходе значительно упрощается вопрос датасета, так как нам не нужно иметь полный набор примеров для каждого из намерений, необходимо иметь лишь парочку примеров наших кастомные интентов. 

Тогда промпт выглядел бы так:

Определи, к какому интенту относится сообщение: «Как вернуть товар?» (Варианты интентов: Возврат товара, Обмен товара, Уточнение условий возврата). Выбери строго из перечисленных интентов, не меняя формулировки интента.

Затем можно разработать API запрос к модели Chat GPT, который будет передавать такие промпты и запрашивать модель определить интент на основе предложенных вариантов. Код для такого API запроса может выглядеть примерно так:

import openai

def find_intent(question, list_intents):
    prompt = f"Определи, к какому интенту относится сообщение: {question} (Варианты намерений: {', '.join(list_intents)}). Выбери строго из перечисленных интентов, не меняя формулировки интента."    
    answer = openai.Completion.create(
        engine="text-davinci-002",
        prompt=prompt,
        max_tokens=20,
    )
    
    choosen_intent = answer.choices[0].text.strip()
    return choosen_intent

question = "Как вернуть товар?"
list_intents = ["Возврат товара", "Обмен товара", "Уточнение условий возврата"]
intent = find_intent(question, list_intents)
print(f"Определенный интент: {intent}")

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

Чтобы обойти это ограничение и повысить надежность моего решения, я пришла к тому, что мне необходимо использовать мультиязычный encoder. Мультиязычный encoder- это модель, способная обрабатывать текст на нескольких языках. Это позволяет расширить область поиска данных для классификации интентов до просто «датасет интентов для ритейла». Таким образом, это дало мне доступ к пробе классического подхода, так как вопрос датасета решился тем, что мультиязычный энкодер даст необходимые знания классификатору, независимо от языка. Кроме того, при исследовании связки работы encoder с разными архитектурами (опробованные архитектуры перечислены ниже), показатели метрик не вырастают значительно при смене обычной полносвязанной нейронки на Bert, поэтому для оптимизации было решено использовать обычную полносвязанную двухслойную нейронку — количество интентов и простота их определения позволили это сделать.

Получили такую структуру:

73238e046294bfe29f80d88e6cb0dc12.png


Архитектура — использование полносвязанной двухслойной нейронки…

100226aa4b94cf5409d22dcd09097c22.png


Данные — размеченный набор намерений с 300 примерами на каждый интент, что также легко сделать.

9648eb42c4c60cee0157ea828b7189ad.png

Метрика для проверки — f1-score на тестовом наборе данных, что обеспечивает надежное оценивание производительности модели.

А теперь к деталям

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

64455a79d64e3fdff6ef3dcf7c26c1d0.png

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

import pandas as pd
import numpy as np
import tensorflow as tf
from sentence_transformers import SentenceTransformer
from tensorflow.keras.layers import Dense
from tensorflow.keras.models import Sequential
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder


train_texts, test_texts, train_labels, test_labels = train_test_split(
    train['text'], train['category'], test_size=0.15, random_state=42)


model = SentenceTransformer('LaBSE')


train_embeddings = model.encode(train_texts.to_list()).astype(np.float32)
test_embeddings = model.encode(test_texts.to_list()).astype(np.float32)


label_encoder = LabelEncoder()
train_labels = label_encoder.fit_transform(train_labels.tolist())
test_labels = label_encoder.transform(test_labels.tolist())

Далее взяла обычную нейросеть и обучила ее на данных, полученных от LaBSE encoder

model_nn = Sequential()
model_nn.add(Dense(64, activation='relu', input_shape=(train_embeddings.shape[1],)))
model_nn.add(Dense(len(label_encoder.classes_), activation='softmax'))


model_nn.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy'])


model_nn.fit(train_embeddings, train_labels, validation_data=(test_embeddings, test_labels), epochs=30, batch_size=64)


loss, accuracy = model_nn.evaluate(test_embeddings, test_labels)
print('Test Loss:', loss)
print('Test Accuracy:', accuracy)

И поместила это в бот в телеграме, чтобы предоставить возможность продемонстрировать работу классификатора.

8779ef13ec8842b97748a8d6bc883e57.png

Особенности такого подхода в том, что здесь падает f1-score в сравнении с использованием датасета на английском языке. Это объясняется тем, что мультиязычные модели могут быть менее точными в работе с русским текстом, чем с английским, из-за неравномерного распределения данных и особенностей языка.

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

  1. LaBSE (Language Agnostic BERT Sentence Embedding): Универсальный мультиязычный энкодер, который обучается на текстах на разных языках.

  2. mBERT (Multilingual BERT): Мультиязычная версия BERT, которая предназначена для работы с текстами на разных языках.

  3. XLM-R (Cross-lingual Language Model — RoBERTa): Мультиязычная модель, основанная на архитектуре RoBERTa и способная работать с множеством языков.

  4. mT5 (Multilingual T5): Мультиязычная версия T5 (Text-to-Text Transfer Transformer), которая может обрабатывать тексты на разных языках.

  5. XLM (Cross-lingual Language Model): Модель, разработанная Facebook AI Research (FAIR), способная выполнять перевод и обработку текста на нескольких языках.

и привожу таблицу сравнение метрик для этих разных энкодеров. 

Имя

Accuracy

Precision

Recall

F1-score

LaBSE

0.99506

0.99523

0.99506

0.99506

mBERT

0.40740

0.55772

0.40740

0.39508

XLM-R

0.96172

0.96596

0.96172

0.96140

mT5

0.58888

0.70619

0.58888

0.58579

XLM

0.90123

0.91275

0.90123

0.90032

По таблице видна интересная динамика: mBERT оказалась самой слабой, однако лучше случайного выбора (1/27 или 0.03), не сильно лучше была mT5. Самой успешной оказалась LaBSE.

Вот так выглядит матрица ошибки по классам для LaBSE

LaBSE

LaBSE

Для остальных так

Для остальных так

mBERT

mBERT

mT5

mT5

XLM Roberta

XLM Roberta

XLM

XLM

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

P.S.: Но необходимо понимать, что такая система на надежна для использования в больших продуктах: чем больше сдвиг в областях знаний у данных (а сдвиг между реальными переписками и датасетом есть), тем хуже будет работать классификатор. Простой пример: кто-то здоровается через «здравствуйте», а кто-то игнорирует приветствия, и если датасет, на котором обучали, был «здоровой версией человека» и отражал вежливую речь, то общение в службе поддержки может быть максимально разнообразным, и точность определения «непривычного» общения будет меньше.  Поэтому для полной разработки однозначно необходим будет собрать обширный датасет, включающий в себя все возможные вариации необходимых интентов. 

Тут ссылочка на github репозиторий с кодом. В папке \research находятся ноутбуки, в которых я тестировала и другие подходы (не только связку LaBSE + simple network), но и 

  • токенайзер + CNN

  • LaBSE + simple network

  • Tokenizer + Attention layers with BiLSTM

  • TF-IDF vectorizer + XGBoost

  • Glove + CNN

  • BertTokenizer + BertClassifier

и привожу таблицу сравнение метрик для этих разных архитектур (для английского датасета):  

Имя

Accuracy

Precision

Recall

F1-score

токенайзер + CNN

0.97777

0.97899

0.97777

0.97784

LaBSE + simple network

0.99382

0.99394

0.99382

0.99381

Tokenizer + Attention layers with BiLSTM

0.98024

0.98242

0.98024

0.98026

TF-IDF vectorizer + XGBoost

0.96064

0.96181

0.96064

0.96078

Glove + CNN

0.98271

0.98408

0.98271

0.98279

BertTokenizer + BertClassifier

0.98395

0.98473

0.98395

0.98399

и их confusion матрицы:

и их confusion матрицы:

токенайзер + CNN

токенайзер + CNN

LaBSE + simple network

LaBSE + simple network

Tokenizer + Attention layers with BiLSTM

Tokenizer + Attention layers with BiLSTM

Glove + CNN

Glove + CNN

BertTokenizer + BertClassifier

BertTokenizer + BertClassifier

Спасибо за внимание и хорошего дня!

© Habrahabr.ru