Сегодня в Lifen наши модели машинного обучения (в основном НЛП) ежедневно предоставляют нашим пользователям более 1 миллиона прогнозов. Этот том неизбежно генерирует запросы в службу поддержки, когда наш ИИ ошибается или делает нелогичное предсказание.

Нашей целью было разработать инструмент, который:

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

Конечный результат

В результате появилось веб-приложение lifen-ai-explainer, доступное внутри компании для специалистов по данным и службы поддержки. Учитывая идентификатор документа (предоставленный клиентом, запрашивающим поддержку), это веб-приложение будет отображать анонимный документ и прогнозы, сделанные нашим ИИ. (Предсказания также анонимизируются, но делаются для исходного документа, в противном случае изменения в документе в процессе анонимизации могут привести к другим результатам)

Для этой цели мы создали lifen-ai-explainer и алгоритм псевдонимизации, которые полностью интегрированы в наши повседневные процессы с середины 2020 года.

Объяснение классификации

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

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

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

Объяснение NER

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

Как это работает?

Наши классификаторы созданы с использованием архитектуры Embedding + LSTM (длинные документы, ресурсы и временные ограничения означают отсутствие преобразователей). Однако эта архитектура не является готовой и хорошо подходит для целей объяснимости.

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

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

Слой глобального внимания

Наше решение состоит в том, чтобы добавить глобальный уровень внимания (или слой внимательного объединения) [1], который вычисляет показатель внимания для каждого токена с плотным слоем + softmax, который затем используется для вычисления взвешенной суммы всех скрытых состояний в каждом временной шаг (которые примерно соответствуют информации, содержащейся в текущем слове).

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

Код слоя

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

def MultiGlobalAttention(timesteps: tf.Tensor, n_heads: int = 1) -> tf.Tensor:
    """Global Attention layer. Computes a weighted sum of all the timesteps.
    Input: N timesteps of dim D
    Output: 1 of dim D
    Zhou et al https://www.aclweb.org/anthology/P16-2034  §3.3"""

    attention = Dense(n_heads, use_bias=False, activation="linear")(timesteps)
    attention = Activation(softmax_axis(1), name="attention")(attention)
    weighted_sum = dot([attention, timesteps], axes=1)
    return Flatten()(weighted_sum)

Как получить эти оценки внимания? Мы извлекаем подмодель внимания, которая не используется в типичных сценариях логического вывода, а только для поддержки в lifen-ai-explainer.

def attention_model(model: Model) -> Model:
    """make attention_model from model"""
    return Model(inputs=model.input, outputs=model.get_layer("attention").output)

Архитектура lifen-ai-explainer (+ lifen-ai)

Веб-приложение представляет собой одностраничное приложение React, написанное на TypeScript, которое взаимодействует с бэкэндом REST Python на основе FastAPI (бэкэнд также обслуживает статические файлы для внешнего интерфейса).

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

Веб-приложение должно делать те же прогнозы, что и lifen-ai. Поэтому мы решили создать серверную часть в виде приложения Python, которое может импортировать существующий lifen-ai в виде библиотеки.

В Python отсутствуют явные модификаторы доступа, что может затруднить поддержание хорошей инкапсуляции и обеспечить удобство сопровождения большой взаимосвязанной кодовой базы. Мы используем import-linter для обеспечения соблюдения правил инкапсуляции и разрешаем lifen-ai-explainer импортировать lifen-ai, но не наоборот.

Как синхронизировать наш ИИ-эксплейнер с новыми функциями?

Наш ИИ быстро развивается, и нам нужен lifen-ai-explainer, чтобы всегда быть в курсе событий. Мы никогда не хотим оказаться в ситуации, когда новая функция не может быть отлажена с помощью lifen-ai-explainer.

Моно-репозиторий, mypy и генерация фронт-бэка

Мы решили поместить оба приложения в одно и то же монорепозиторий. Это позволяет вносить атомарные изменения: отправляя запрос на добавление прогноза в lifen-ai, вы можете одновременно добавить реализацию в lifen-ai-explainer. Это предусмотрено дизайном: поскольку 100% нашей кодовой базы имеет аннотацию типа mypy, критическое изменение в lifen-ai нарушит CI для lifen-ai-explainer. , поэтому вы обязаны поддерживать его в актуальном состоянии. Наши модульные и интеграционные тесты сделают все остальное.

Фронтенд-клиент автоматически генерируется Orval с использованием спецификации OpenAPI, которая также автоматически генерируется FastAPI. В результате любые изменения, внесенные в выходные данные модели, приведут к сбою CI и направят вас к точному месту в коде внешнего интерфейса, где требуются обновления 🎉.

Мы используем поэзию для управления зависимостями Python, которые не поддерживают монорепозиторий, но позволяют нам разделять зависимости на группы. Таким образом, lifen-ai-explainer включает все зависимости lifen-ai, но не наоборот.

lifen-ai-explainer был полностью интегрирован в наши повседневные процессы с середины 2020 года, и с тех пор мы расширили его возможности за счет интерфейса маркировки наборов данных и инструмента просмотра ошибок после обучения.