Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Moscow Python Meetup №89 Павел Мамаев (Сбер, Ведущий инженер по разработке). Классификация запросов клиентов. Дёшево и сердито

Moscow Python Meetup №89 Павел Мамаев (Сбер, Ведущий инженер по разработке). Классификация запросов клиентов. Дёшево и сердито

Когда у вас достаточно большой и разношёрстный спектр запросов клиентов, а вам необходимо все это валидировать, и на это нет ресурсов в виде LLM или NN — "Что же делать?". Расскажу, как сделать классификацию быстро и без больших затрат на разметку и обучение.

Видео: https://moscowpython.ru/meetup/89/client-requests-classification/

Moscow Python: http://moscowpython.ru
Курсы Learn Python: http://learn.python.ru
Moscow Python Podcast: http://podcast.python.ru
Заявки на доклады: https://bit.ly/mp-speaker

Moscow Python Meetup

April 11, 2024
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. • Сбер • Ведущий инженер по разработке Июнь 2021 –

    н.в. Виртуальный ассистент. Платежи и Переводы: Переводы клиенту Сбербанка Переводы между своими счетами Оплата сотовой Переводы по СБП … Виртуальный ассистент. Брокерский бизнес: Пополнение брокерского счета Курс акций Московской биржи … • SAS Russia • Аналитик Data Science Декабрь 2018 – Июнь 2021 Мамаев Павел Алексеевич
  2. Архитектура Взаимодействие продуктов с платформой Устройство ввода Платформа Нормализация ввода/вывода

    Маршрутизатор Платформа заказчика 1 Продукт 1 Продукт 2 Продукт 3 Продукт 4 Общий классификатор
  3. Архитектура Взаимодействие продуктов с платформой Устройство ввода Платформа Нормализация ввода/вывода

    Общий классификатор Маршрутизатор Платформа заказчика 1 Продукт 1 Продукт 2 Продукт 3 Продукт 4 Платформа заказчика 2 Продукт 1 Продукт 2 Продукт 3 Платформа заказчика 3 Продукт 1 Продукт 2 Продукт 3 Платформа заказчика 4 Продукт 1 Продукт 2 Продукт 3
  4. Архитектура Взаимодействие продуктов с платформой Устройство ввода Платформа Нормализация ввода/вывода

    Общий классификатор Маршрутизатор Платформа заказчика 1 Продукт 1 Продукт 2 Продукт 3 Продукт 4 Платформа заказчика 2 Продукт 1 Продукт 2 Продукт 3 Платформа заказчика 3 Продукт 1 Продукт 2 Продукт 3 Платформа заказчика 4 Продукт 1 Продукт 2 Продукт 3
  5. Архитектура Свой классификатор Платформа заказчика 1 Продукт 1 Продукт 2

    Продукт 3 Продукт 4 Что-то умное Продукт 5 Продукт 6 Устройство ввода Платформа Нормализация ввода/вывода Общий классификатор Маршрутизатор Платформа заказчика 2 Продукт 1 Продукт 2 Продукт 3 Платформа заказчика 3 Продукт 1 Продукт 2 Продукт 3 Платформа заказчика 4 Продукт 1 Продукт 2 Продукт 3
  6. Почему свой сервис Возможные варианты: • Переиспользовать целевое внутренне решение

    • Переиспользовать OpenSource решение • Создать свое собственное решение
  7. Почему свой сервис Возможные варианты: • Переиспользовать целевое внутренне решение

    • Переиспользовать OpenSource решение • Создать свое собственное решение Но: • Необходимо затратить большой ресурс времени, а у нас сроки • Возникнуть проблемы с безопасностью, и мы в банкинге • Нужны дополнительные ресурсы
  8. Почему свой сервис Возможные варианты: • Переиспользовать целевое внутренне решение

    • Переиспользовать OpenSource решение • Создать свое собственное решение Но: • Необходимо затратить большой ресурс времени, а у нас сроки • Возникнуть проблемы с безопасностью, и мы в банкинге • Нужны дополнительные ресурсы
  9. Почему свой сервис Возможные варианты: • Переиспользовать целевое внутренне решение

    • Переиспользовать OpenSource решение • Создать свое собственное решение Но: • Необходимо затратить большой ресурс времени (после MVP) • Возникнуть проблемы с безопасностью, и мы в банкинге • Нужны дополнительные ресурсы (в меру возможностей – MVP)
  10. Какое качество нам нужно? Рассматриваем MVP (CR – целевая метрика)

    20% 25% 28% 35% 40% 55% 60% 70% 85% 87% 90% 92% 94% 96% 98% 99% 0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%
  11. Какое качество нам нужно? Рассматриваем MVP (план минимум) 20% 25%

    28% 35% 40% 55% 60% 70% 85% 87% 90% 92% 94% 96% 98% 99% 0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%
  12. Какое качество нам нужно? Рассматриваем MVP (план максимум) 20% 25%

    28% 35% 40% 55% 60% 70% 85% 87% 90% 92% 94% 96% 98% 99% 0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%
  13. Какое качество нам нужно? Рассматриваем MVP (технический стек) 20% 25%

    28% 35% 40% 55% 60% 70% 85% 87% 90% 92% 94% 96% 98% 99% 0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100% ??? AI, DL, NN, LLM, BERT Внутренне решение
  14. Какое качество нам нужно? Рассматриваем MVP (технический стек) 20% 25%

    28% 35% 40% 55% 60% 70% 85% 87% 90% 92% 94% 96% 98% 99% 0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100% PyMorphy2, sklearn AI, DL, NN, LLM, BERT If/else Внутренне решение
  15. MVP Классификатора Пример реализации (процесс) Входная фраза Токенизатор (pymorphy2) Извлечение

    сущностей Условия Пополни мой брокерский счет на 500 рублей Нормализация пополнить мой брокерский счет на 500 rur [ {‘text’: …, ‘lemma’: …, …}, … ] { ’action’: ‘refill’, ‘amount’: { ‘value’: 500.0, ‘curr’: ‘RUB’ }, … } if/elif/elif/.../else
  16. MVP Классификатора Пример реализации (процесс) Входная фраза Токенизатор (pymorphy2) Извлечение

    сущностей Условия Пополни мой брокерский счет на 500 рублей Нормализация пополнить мой брокерский счет на 500 rur [ {‘text’: …, ‘lemma’: …, …}, … ] { ’action’: ‘refill’, ‘amount’: { ‘value’: 500.0, ‘curr’: ‘RUB’ }, … } if/elif/elif/.../else
  17. Нормализатор + Токенизатор Пример кода class LocalTextNormalizer: def __init__(self): self.__ready_to_use

    = False self._morph = None @lazy def morph(self): self._morph = Pymorphy2MorphWrapper() return self._morph def __load_everything(self): self.converter_pipeline = { 'Конверсия юникодовых символов’: …, 'Цифры и буквы отдельно’: …, 'Номера телефонов’: …, 'Номера карт’: …, 'Объединение сумм’: …, 'Символы валют’: …, "Претокенизация математических операций": … } self.tokens_processor = { "Synonyms": …, "Text2Num": …, "CurrencyMoney": …, "Grammemes": self.morph, } self.__ready_to_use = True def load_everything(self): if not self.__ready_to_use: self.__load_everything() def __call__(self, text, message_type="text"): self.load_everything() normalized_text = text convert_processor = self.convert_plan[message_type] for converter_name in convert_processor: converter = self.converter_pipeline[converter_name] normalized_text = converter(normalized_text) token_list = self.get_token_list(normalized_text) self.extend_raw_token_list(token_list) for converter_step in self.processor_pipeline: converter_name = converter_step["name"] converter = self.tokens_processor[converter_name] token_list = converter(token_list) return { "original_text": text, "normalized_text": return_lemmas_only(token_list), "tokenized_elements_list": token_list }
  18. Пополни мой брокерский счет на 500 рублей { 'original_text': 'Пополни

    мой брокерский счет на 500 рублей', 'normalized_text': 'пополнить мой брокерский счет на MONEY_TOKEN .', 'tokenized_elements_list': [ {'text': 'Пополни', 'raw_text': 'Пополни', 'grammem_info’: … , …}, {'text': 'мой', 'raw_text': 'мой', 'grammem_info’: …, …}, {'text': 'брокерский', 'raw_text': 'брокерский', 'grammem_info’: …, …}, {'text': 'счет', 'raw_text': 'счет', 'grammem_info’: …, …}, {'text': 'на', 'raw_text': 'на', 'grammem_info’: …, …}, {'text': '500', 'raw_text': '500', 'lemma': '500', 'original_text': '500', 'token_type': 'NUM_TOKEN’, …}, {'text': 'rur', 'raw_text': 'рублей', 'grammem_info’: …, …}, {'raw_text': '.', 'text': '.', 'lemma': '.', 'token_type': 'SENTENCE_ENDPOINT_TOKEN’, …} ], 'entities': { 'NUM_TOKEN': [{'value': 500, 'adjectival_number': False}], 'MONEY_TOKEN': [{'amount': 500, 'currency': 'rur'}], 'CCY_TOKEN': [{'value': 'rur'}] }, 'original_message_name': 'MESSAGE_FROM_USER', 'human_normalized_text': 'пополнить мой брокерский счет на 500 rur', 'asr_normalized_message': None, 'human_normalized_text_with_anaphora': 'пополнить мой брокерский счет на 500 rur' }, Нормализатор + Токенизатор Пример ответа
  19. { 'original_text': 'Пополни мой брокерский счет на 500 рублей', 'normalized_text':

    'пополнить мой брокерский счет на MONEY_TOKEN .', 'tokenized_elements_list': [ {'text': 'Пополни', 'raw_text': 'Пополни', 'grammem_info’: … , …}, {'text': 'мой', 'raw_text': 'мой', 'grammem_info’: …, …}, {'text': 'брокерский', 'raw_text': 'брокерский', 'grammem_info’: …, …}, {'text': 'счет', 'raw_text': 'счет', 'grammem_info’: …, …}, {'text': 'на', 'raw_text': 'на', 'grammem_info’: …, …}, {'text': '500', 'raw_text': '500', 'lemma': '500', 'original_text': '500', 'token_type': 'NUM_TOKEN’, …}, {'text': 'rur', 'raw_text': 'рублей', 'grammem_info’: …, …}, {'raw_text': '.', 'text': '.', 'lemma': '.', 'token_type': 'SENTENCE_ENDPOINT_TOKEN’, …} ], 'entities': { 'NUM_TOKEN': [{'value': 500, 'adjectival_number': False}], 'MONEY_TOKEN': [{'amount': 500, 'currency': 'rur'}], 'CCY_TOKEN': [{'value': 'rur'}] }, 'original_message_name': 'MESSAGE_FROM_USER', 'human_normalized_text': 'пополнить мой брокерский счет на 500 rur', 'asr_normalized_message': None, 'human_normalized_text_with_anaphora': 'пополнить мой брокерский счет на 500 rur' }, Нормализатор + Токенизатор Разбор ответа original_text Оригинальная фраза normalized_text Нормализованный текст tokenized_elements_list Список токенов запроса entities Базовые сущности
  20. { 'text': 'Пополни', 'raw_text': 'Пополни', 'grammem_info': { 'aspect': 'perf', 'mood':

    'imp', 'number': 'sing', 'person': '2', 'transitivity': 'tran','verbform': 'fin', 'voice': 'act', 'raw_gram_info': 'aspect=perf|mood=imp|number=sing|person=2|transitivity=tran|verbform=fin|voice=act', 'part_of_speech': 'VERB' }, 'lemma': 'пополнить', 'is_stop_word': False, 'list_of_dependents': [4, 7], 'dependency_type': 'root', 'head': 0 } { 'raw_text': '.', 'text': '.', 'lemma': '.', 'token_type': 'SENTENCE_ENDPOINT_TOKEN', 'token_value': {'value': '.'}, 'list_of_token_types_data': [ { 'token_type': 'SENTENCE_ENDPOINT_TOKEN', 'token_value': {'value': '.'} } ] } Нормализатор + Токенизатор Пример ответа
  21. { 'text': 'Пополни', 'raw_text': 'Пополни', 'grammem_info': { 'aspect': 'perf', 'mood':

    'imp', 'number': 'sing', 'person': '2', 'transitivity': 'tran','verbform': 'fin', 'voice': 'act', 'raw_gram_info': 'aspect=perf|mood=imp|number=sing|person=2|transitivity=tran|verbform=fin|voice=act', 'part_of_speech': 'VERB' }, 'lemma': 'пополнить', 'is_stop_word': False, 'list_of_dependents': [4, 7], 'dependency_type': 'root', 'head': 0 } Нормализатор + Токенизатор Разбор ответа text Оригинальный текст raw_text Сырой текст, как пришел grammem_info Информация о составе слова (падеж, род, число и пр.) is_stop_word Стоп-слово
  22. { 'text': '500', 'raw_text': '500', 'lemma': '500', 'original_text': '500', 'token_type':

    'NUM_TOKEN', 'token_value': { 'value': 500, 'adjectival_number': False }, 'list_of_token_types_data': [ { 'token_type': 'NUM_TOKEN', 'token_value': { 'value': 500, 'adjectival_number': False } } ], 'grammem_info': { 'numform': 'digit', 'raw_gram_info': 'numform=digit’, 'part_of_speech': 'NUM' }, 'is_stop_word': False, 'list_of_dependents': [5], 'dependency_type': 'nummod', 'head': 4, 'is_beginning_of_composite': True, 'composite_token_type': 'MONEY_TOKEN', 'composite_token_length': 2, 'composite_token_value': { 'amount': 500, 'currency': 'rur' } } Нормализатор + Токенизатор Пример ответа
  23. { 'text': '500', 'raw_text': '500', 'lemma': '500', 'original_text': '500', 'token_type':

    'NUM_TOKEN', 'token_value': { 'value': 500, 'adjectival_number': False }, 'list_of_token_types_data': [ { 'token_type': 'NUM_TOKEN', 'token_value': { 'value': 500, 'adjectival_number': False } } ], 'grammem_info': { 'numform': 'digit', 'raw_gram_info': 'numform=digit’, 'part_of_speech': 'NUM' }, 'is_stop_word': False, 'list_of_dependents': [5], 'dependency_type': 'nummod', 'head': 4, 'is_beginning_of_composite': True, 'composite_token_type': 'MONEY_TOKEN', 'composite_token_length': 2, 'composite_token_value': { 'amount': 500, 'currency': 'rur' } } Нормализатор + Токенизатор Разбор ответа token_type Тип токена (число, валюта и пр.) token_value Значение токена composite_token_type Тип составного токена из нескольких таких (сумма и пр.) composite_token_value Значение составного токена
  24. Извлечение сущности Пример кода @dataclass class Entity: value: Optional[Any] elements_indexes:

    Optional[List[int]] name: Optional[str] = None @property def is_composite(self) -> bool: return isinstance(self.elements_indexes, list) and len(self.elements_indexes) > 1 @property def is_empty(self) -> bool: return not (isinstance(self.elements_indexes, list) and len(self.elements_indexes) > 0)
  25. class EntityExtractor: name: Optional[str] = None do_remove: bool = True

    def get_empty_entity(self) -> Entity: return Entity( name=self.name, value=None, elements_indexes=None ) def get_entity(self, elements_list: ElementsList, external_data: Optional[Dict[str, Any]]) -> Optional[Entity]: raise NotImplementedError def extract(self, data: Dict[str, Union[Dict, List, ElementsList]], external_data: Optional[Dict[str, Any]]) -> Tuple[Entity, bool]: entity = self.get_entity(elements_list=data["elements_list"], external_data=external_data) … return entity, self.do_remove Извлечение сущности Пример кода
  26. Извлечение сущности Пример кода class AmountExtractor(EntityExtractor): name: str = "amount"

    do_remove: bool = True optional_lemmas_before_entity: Set[str] = {"на"} hard_return_lemmas: Set[str] = {"минус"} default_currency: str = "rur" @staticmethod def _is_acceptable_element_after_number_token(element: Element) -> bool: pass def get_empty_entity(self) -> Entity: return Entity( name=self.name, value={ "amount": None, "currency": None }, elements_indexes=None ) def get_entity(self, elements_list: ElementsList, external_data: Optional[Dict[str, Any]]) -> Optional[Entity]: … return entity
  27. { 'original_text': 'Пополни мой брокерский счет на 500 рублей', 'normalized_text':

    'пополнить мой брокерский счет на MONEY_TOKEN .', 'tokenized_elements_list': [ {'text': 'Пополни', 'raw_text': 'Пополни', 'grammem_info’: … , …}, {'text': 'мой', 'raw_text': 'мой', 'grammem_info’: …, …}, {'text': 'брокерский', 'raw_text': 'брокерский', 'grammem_info’: …, …}, {'text': 'счет', 'raw_text': 'счет', 'grammem_info’: …, …}, {'text': 'на', 'raw_text': 'на', 'grammem_info’: …, …}, {'text': '500', 'raw_text': '500', 'lemma': '500', 'original_text': '500', 'token_type': 'NUM_TOKEN’, …}, {'text': 'rur', 'raw_text': 'рублей', 'grammem_info’: …, …}, {'raw_text': '.', 'text': '.', 'lemma': '.', 'token_type': 'SENTENCE_ENDPOINT_TOKEN’, …} ], 'entities': { 'NUM_TOKEN': [{'value': 500, 'adjectival_number': False}], 'MONEY_TOKEN': [{'amount': 500, 'currency': 'rur'}], 'CCY_TOKEN': [{'value': 'rur'}] }, 'original_message_name': 'MESSAGE_FROM_USER', 'human_normalized_text': 'пополнить мой брокерский счет на 500 rur', 'asr_normalized_message': None, 'human_normalized_text_with_anaphora': 'пополнить мой брокерский счет на 500 rur' }, { "action": "refill", "brokerage_contract": "BA", "amount": { "value": 500.0, "curr": "RUB" }, "card": None } Извлечение сущности Пример ответа
  28. { "action": "refill", "brokerage_contract": "BA", "amount": { "value": 500.0, "curr":

    "RUB" }, "card": None } Извлечение сущности Разбор ответа action – class ActionExtractor(EntityExtractor): Тип действия, которое хочет сделать клиент: пополнить, снять, спросить и пр. brokerage_contract – class BrokerageContractExtractor(EntityExtractor): Тип брокерского инструмента: Брокерский счет, ИИС, ИнвестКопилка amount – class AmountExtractor(EntityExtractor): Сумма, с которой клиент хочет что-то сделать card – class PaymentToolsExtractor(EntityExtractor): Платежный инструмент, который назвал клиент: платежная система, номер
  29. Условия Пример кода is_sbp_transfer = entities.get(e.TransferKeywordExtractor.name) \ or entities.get(e.PaymentKeywordExtractor.name) \

    or self.is_main_intents \ or entities.get(e.PTLiteGenericCardFromExtractor.name) \ or entities.get(e.PTLiteGenericCardToExtractor.name) \ or entities.get(e.AccountAmbiguousExtractor.name, {}).get('account', False) \ or entities.get(e.ClassificationRecipientExtractor.name) if entities.get(e.QuickPaymentSystemExtractor.name) or entities.get(e.LightBankExtractor.name) or entities.get( "to_other"): if entities.get(e.CancelRefundExtractor.name): return ['A0532.Cancel_or_refund_payment'] if entities.get(e.DoNotPayExtractor.name): return ['A04.01665.Do_not_pay_and_transfer'] if entities.get(e.SpasiboExtractor.name): return ['A0501.How_to_check_your_SPASIBO_bonus_balance'] if entities.get(e.QuestionMarkerExtractor.name) \ or entities.get(e.LimitsExtractor.name) \ or entities.get(e.PurchaseServiceKeywordExtractor.name) \ or entities.get("is_credit") \ or entities.get(e.TransferAbroadExtractor.name): return ["A04.01589.Quick_payments_system"] if any(entities.get(e.NoComissionExtractor.name).values()): if is_sbp_transfer and self.is_sbp and entities.get("is_without_comission"): if entities.get("to_sber"): return ["p2p"] return ["sbp"] return ["A04.01589.Quick_payments_system"] if is_sbp_transfer and self.is_sbp: if entities.get("to_sber"): return ["p2p"] return ["sbp"] else: return ["A04.01589.Quick_payments_system"] if entities.get(e.NoComissionExtractor.name, {}).get('comission', False) and not entities.get( "is_without_comission", True): return ['A0545.Comission_for_payments_and_transfers'] if entities.get(e.CancelRefundExtractor.name): return ['A0532.Cancel_or_refund_payment'] if entities.get(e.LimitsExtractor.name): return ['A04.01598.How_to_change_daily_limit'] if len(intents) > 1: return full_card
  30. Результат Первая итерация • Средний прирост по метрикам продуктов ~10-15%

    • Клиенты чаще попадают в нужный продукт за счет разветвляющего навыка
  31. MVP Классификатора Пример реализации (процесс) Входная фраза Токенизатор (pymorphy2) Извлечение

    сущностей Пополни мой брокерский счет на 500 рублей Нормализация пополнить мой брокерский счет на 500 rur [ {‘text’: …, ‘lemma’: …, …}, … ] { ’action’: ‘refill’, ‘amount’: { ‘value’: 500.0, ‘curr’: ‘RUB’ }, … }
  32. MVP Классификатора Пример реализации (улучшение) Входная фраза Токенизатор (pymorphy2) Извлечение

    сущностей Бинаризация Модель (sklearn) Пополни мой брокерский счет на 500 рублей Нормализация пополнить мой брокерский счет на 500 rur [ {‘text’: …, ‘lemma’: …, …}, … ] { ’action’: ‘refill’, ‘amount’: { ‘value’: 500.0, ‘curr’: ‘RUB’ }, … } { ’action_refill’: 1, ‘action_is_faq’: 0, ’has_amount’: 1, … } refill_ba
  33. Основные поинты • Иногда нужно создавать MVP в меру своих

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