Всем привет! Меня зовут Мичил Егоров, и я лидил команду, которая написала модуль на Python, способный на основании анализа строчки на естественном языке выносить предварительный диагноз и маркировать срочность приема. Ниже я расскажу вам, как мы в составе пяти человек занимались разработкой модуля, почему нам показалось это интересным, и что у нас получилось.

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

Зачем понадобился еще один модуль, работающий с текстами?

Когда мы заболеваем и встречаемся с проблемами записи к врачу, коммуникации с клиникой, вообще потребностью во внимании, нам кажется, что это проблема, которая касается только заболевшего человека. Но на самом деле все гораздо сложнее. Здоровье населения - фактор, который оказывает влияние на социальное самочувствие всего общества. К тому же не только заболевший, но и врачи оказываются в стрессе: как уделить максимальное внимание каждому человеку, нуждающемуся в медицинской помощи?
Эти проблемы начинаются с момента первой коммуникации с клиникой: нагрузка на регистратуры и колл-центры оказывается настоящим челенджем для принимающих звонки медицинских работников.
В ноябре и декабре 2021 года мы провели ряд опросов среди жителей Санкт-Петербурга в попытках выяснить, насколько население готово к применению ИКТ для упрощения первичной коммуникации с медицинскими учреждениями. Общий краткий вывод оказался таким: 1) прекрасно, что есть веб-сервисы для записи к врачу; 2) звонки в регистратуры поликлиник видятся как проблемная зона, хотя периодически людям требуются уточнения и оптимизация логистики своего передвижения по клинике; 3) люди из поколений 25-45 лет готовы пользоваться диалоговыми агентами, подключенными к сайтам и приложениям клиник.
Для нас это показало, что чат-боты, принимающие описание состояния пациента и, тем самым, упрощающие работу специалиста в регистратуре, а, значит, и снижающие нагрузку, являются перспективным направлением для прикладных научных исследований.

Формализируем идею

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

  1. пациент должен связаться с медицинским учреждением и объяснить, что его беспокоит в своем самочувствии;

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

  3. происходит фиксация принятого решения (например, дата приема у терапевта);

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

  5. пациент направляется на анализы;

  6. происходит вторичный прием у врача (этому предшествует еще, как минимум, еще одна коммуникация с регистратурой).

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

И так. Мы ставили перед собой цель попробовать дать медикам инструмент способный через автоматизацию сократить рутину в пользу креативных решений и эмпатии по отношению к пациентам.

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

Тут вы, наверное, подумаете: "опять chatGPT!", но вы правы только отчасти. Сфера здравоохранения требует интерпретируемых и воспроизводимых результатов. На данный момент chatGPT – удобный инструмент во многих областях, связанных с текстовыми данными, но возложить на него судьбы пациентов не правильно как с точки зрения закона, так и с позиций этики. Поэтому мы хотели создать интепретируемый, воспроизводимый и понятный всем инструмент для работы с пациентами.

Общий план и основная идея

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

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

Вот пример, как может работать бот, на базе мультиклассовой классификации:

Идеальный сценарий использование модуля. Бот - https://t.me/distool_bot

Итак, перед нами классика жанра – классификатор, обученный на текстовых данных.
Поэтому у нас возникает следующий стандартный план достижения цели:
• сбор данных;
• обучение моделей;
• валидация результатов;
• если нас не устраивают результаты, возвращаемся на первый пункт;
• если все хорошо - в прод!

Разработка

Хотелось разработать модульную систему, чтобы на стороне клиники можно было комбинировать отдельные куски библиотеки и создавать любые сценарии работы диалогового агента. Поэтому мы выделили три главных модуля.

  • подмодуль поиска симптомов: модуль, построенный на основе SpaCy и Negex, который отвечает за распознавание симптомов и обработку отрицаний симптомов во входных текстовых данных;

  • подмодули классификации: были разработаны два разных модуля классификации, один основан на логистической регрессии, а другой на FEDOT AutoML framework, что обеспечивало гибкость и возможность выбора подходящего классификатора в зависимости от конкретной ситуации;

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

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

Сбор данных

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

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

Вот тут на помощь пришел ChatGPT.

Генерация анамнезов пациентов

Хотелось получать разнообразные анамнезы пациентов для заданной болезни. Это могло нам позволить чувствовать себя свободно при обучении и валидации моделей. Мы решили научить ChatGPT выдавать нам описание самочувствия, соответствующее определенному заболеванию.

Придумали следующий промпт (запрос в gpt-3.5):

Ты пациент и пришел на первичный прием.
Ты не знаешь своего диагноза, поэтому не можешь о нем напрямую говорить.

Твой ответ обязательно следует следующим пунктам:

ответ должен быть похож на естественную речь человека;

напиши о том, как ты себя чувствуешь;

напиши о том, какие симптомы проявлялись;

сказать каких симптомов не было;

в ответе не должен быть сам диагноз;

А так же в ответе должно быть выполнено хотя бы одно требование:

когда начали проявляться первые симптомы;

как прогрессировало ухудшение твоего состояния;

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

Основная идея промта была в том, чтобы не давать модели готовые ответы в тексте, а, наоборот, генерировать максимально разнообразные анамнезы, похожие на реальные реплики от пациентов (для этого мы еще подкрутили температуру до 2).
В итоге получались следующие ответы:

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

Кстати, сгенерированные данные мы провалидировали, дав их на оценку респондентам-медикам.

Общий вывод из эксперимента: GPT-3.5 генерирует интересные и применимые для дальнейшего использования реплики «пациентов»; есть проблемы для русского языка, впрочем, никогда такого не было, и вот опять.

Выделение симптомов для обучения моделей

Симптомы – это круто, но модели не достаточно знать про имеющиеся симптомы, надо еще видеть отрицания симптомов.

А еще, выделение симптомов и отрицаний – целая отдельная проблема, а, значит, за это отвечает отдельный подмодуль, алгоритм работы которого требует валидации. Где взять данные?

Путь героя и ручная разметка данных были в наших поисках счастья, но... опять же ChatGPT помог увеличить шансы на успех. Вот промт:

Изучай присылаемые тексты и выделяй из них симптомы в виде списка:

Требования к ответу:

Разделяй сипмтомы на подтвержденные и отрицаемые;

Не отвечай на приветствия в тексте;

Не отвечай на вопросы в тексте;

Не давай рекомендации и советы по поводу лечения;

Пример валидации промпта

Так из анамнезов мы собрали еще и упоминаемые в них симптомы.

Модуль для извлечения симптомов

Общая идея выглядит так:

Пайплайн извлечения симптомов

Подход основан на работе с предопределенным перечнем симптомов (сейчас в нашем арсенале 503 различных симптома). Это позволяет нашему модулю обнаруживать и отслеживать широкий спектр проявлений заболеваний.

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

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

А как «поймать» отрицания? Ведь, ошибаясь в этом, мы рискуем неправильно истолковать полученную информацию и, как следствие, ошибиться в диагнозе. Наш выбор пал на инструмент под названием negex, который анализирует контекст и определяет, есть ли в тексте отрицание относительно каждого слова (в нашем случае – симптома). Конечно же, пришлось адаптировать его для работы с русским языком.

На основе собранной информации (симптомы + отрицание симптомов) подмодуль формирует анамнез. Так мы получаем векторное представление симптомов, которое в дальнейшем используется для обучения модели и предсказания болезни.

Валидация результатов

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

  1. VALID – разметчик указал отрицание или наличие симптома, и алгоритм правильно определил;

  2. INVALID – разметчик указал отрицание или наличие симптома, и алгоритм указал обратно;

  3. VALIDATE_MARKER –алгоритм нашел отсутствие или наличие определенного симптома, а разметчик его не нашел;

  4. VALIDATE_EXTRACTOR – разметчик нашел отрицание или наличие симптома, но модель его не нашла.

Таким образом была выделена метрика

Итак, разработанная модель показала accuracy = 84%, и это хорошее значение, но всегда есть, куда расти.

Модуль для предсказания болезни

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

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

Мы приняли решение протестировать два подхода: 1) классическую логистическую регрессию; 2) FEDOT, созданный коллегами из ИТМО. С регрессией – все понятно, а FEDOT позволяет автоматически оптимизировать структуру и параметры модели, что идейно подходило для наших целей. Однако эта модель бывает сложной для интерпретации, а это - проблема в медицинском контексте.

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

В результате обучения модели мы получили следующие метрики:

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

Из таблицы видно, что все модели достаточно много ошибаются, но если сравнить наш SymptomExtractor с базовым Tf-Idf, который переобучился на n-gram-ах, то становится очевидным, что подход с извлечением симптомов себя оправдывает, хоть и требует дальнейшего апгрейта за счет расширения обучающих данных.

Отмечу, что при малой уверенности модели в своем решении, предполагается уточнение у пациента дополнительной информации, и затем повторная классификация. При достаточно уверенных прогнозах (>0.6) модель показывает F1-меру = 0.89.

И самое интересное, мы можем оценить вклад каждого симптома в определение болезни:

Вклад симптома на наличие или отрицание болезни

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

Интерфейс

В итоге мы получаем следующий интерфейс библиотеки:

from distool.feature_extraction import SmartSymptomExtractor
from distool.interpretation.explainer import SymptomBasedExplainer
from distool.estimators import DiseaseClassifier

texts = [
    "У меня болит живот, но нет температуры",
    "У меня температура, но нет недомогания",
    "Я завтра иду домой",
]
diseases = ["гастрит", "отит", "-1"]

symptom_vectorizer = SmartSymptomExtractor()
features = symptom_vectorizer.transform(texts)

classifier = DiseaseClassifier()
classifier.fit(features, diseases)
predicted_diseases = classifier.predict(features)
print("Predicted diseases:", predicted_diseases)
# Predicted diseases: ['гастрит' 'отит' '-1']

explainer = SymptomBasedExplainer(symptom_vectorizer, classifier)

print(explainer.explain(features[1]))
# Наблюдается отит с вероятностью 59%.
# Это потому что у вас наблюдаются следующие симптомы: температура
# И отрицаются следующие: недомогание

Заключение

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

Разработанные модели, зашитые в контур библиотеки, успешно прошли тесты на большом наборе данных. На наш взгляд, библиотека готова к использованию. В планах использовать больше данных для обучения системы, что даст возможность более точных прогнозов. К тому же, как я сказал в самом начале, одна из наших целей – классифицировать срочность приема пациентов. На данный момент кейс реализован для раздела болезней, связанных с кардиологией. Мы даже получили одобрение специалистов. Однако очень хочется продолжить работу над этим подмодулем, а для этого требуются консультации медиков по остальным классам заболеваний.

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

Осталось дать ссылочки. В течение проекта мы выложили несколько датасетов в публичный доступ: https://www.kaggle.com/datasets/egorovm/patient-disease, https://github.com/NIRMA-PATIENT-INTAKE/data. Библиотеку можно установить через PyPI, контрибьют приветствуется: https://github.com/niRMA-PATIENT-INTAKE/disease/. Юзер-френдли демонстратор возможностей библиотеки доступен в виде telegram-бота (https://t.me/distool_bot), его исходный код, кстати, тоже доступен: https://github.com/NIRMA-PATIENT-INTAKE/telegram-interface.

Хочу сказать спасибо своей команде: Дмитрию Погребному, Маше Якубовой, Айталине Кривошапкиной и Анне Чижик за нашу плодотворную работу, и, конечно же, ИТМО, который позволил нам собраться и заниматься интересным проектом.

Всем до новых встреч!