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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Боты в деле или как все устроено

На платформе Россельхозбанка «Своё Фермерство» существует сервис «Ветеринарный бот» — обучаемый виртуальный помощник, круглосуточно определяющий возможные заболевания животных по описаниям симптомов и предоставляющий типовые рекомендации по лечению. Цель бота — за наименьшее число уточняющих вопросов выявить потенциальную болезнь и предоставить консультацию по лечению.

Одна из текущих наших задач по модификации ветеринарного бота — улучшение алгоритма распознавания болезней, в рамках которой мы выделили две подзадачи: классификация потенциальных болезней по текстовым описаниям заболеваний коров и выделение самих симптомов/признаков заболеваний в текстовых сообщениях. Эти подзадачи и решали на главном агрокодинге страны от Россельхозбанка — AgroCode Data Science Cup 2021. Участникам предлагалось определить потенциальные заболевания коров по реальным жалобам людей из открытых источников, а также научиться выделять из текстов симптомы заболеваний (NER — Named Entity Recognition). Мы хотим поделиться с вами методами решения подобных задач. Эта статья будет интересна не только тем, кто специализируется в NLP (Natural Language Processing), но и начинающим исследователям данных.

Откуда берутся данные?

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

Пример: «Прошу совета в лечении язвочек на вымени у коровы. Появились несколько дней назад. Как лечить?».

Мы показали эти тексты ветеринарам, специализирующимся на заболеваниях крупного рогатого скота, чтобы они разметили тексты с потенциальными болезнями, исходя из их описаний. Соответственно выделенные заболевания послужили разметкой для задачи классификации. Однако изначально в данных существовало более 100 различных болезней, при этом для некоторых из них было совсем мало текстов. Поэтому мы приняли решение отобрать наиболее распространенные из них. Таким образом получилось 10 болезней, которые можно предсказать, и класс «другое», означающий, что для данного примера существуют еще и другие заболевания. После отбора в тренировочный набор попало 293 текста, в тестовый — 100 текстов.

Для задачи выделения симптомов требовалась разметка в виде спанов, которые содержали симптомы. Спаны — это участки текста, которые содержат в себе определенный смысл. К примеру, для текста под номером 10 из тренировочной выборки: «Месячный телёнок вчера вечером не до конца выпил молоко, а утром вообще отказался пить. Что может быть?» итоговая разметка выглядит так:

"10": {
  "span": [[72, 86]],
  "label": [0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1]
}

Это означает, что симптомы болезни начинаются с 72-ой позиции по 86-ую — “отказался пить”, а возможные заболевания: “диспепсия молодняка”, “инфекционный ринотрахеит” и “другое”.

Программа для разметки YEDDA и процесс разметки
Программа для разметки YEDDA и процесс разметки

Какие метрики использовали?

Так как задача является составной, то и метрика состояла из двух компонентов с весом 0.8 для задачи классификации и 0.2 для задачи NER.

score = 0.8 * (1 - logloss) + 0.2 * F1

В задаче классификации использовался logloss, вычисляемый как среднее значение метрики sklearn.metrics.log_loss по классам болезней.

logloss = \frac{1}{N}\Sigma(sklearn.metrics.logloss(y_{{test}_i}, y_{{preds}_i}))

В задаче NER использовался span-based F1-score, рассчитываемый следующим образом: для каждого текста берутся предсказанные индексы начала и конца размеченных признаков болезни, по ним выделяются из текста токены (отдельные слова, разделенные пробелом) и сравниваются с истинной (экспертной) разметкой.

Код для подсчета метрики span-based F1-score
def f1_span_score_per_text(ground_truth_labels, my_labels, text):
    
    span_my_tokens = list()
    span_gt_tokens = list()
    
    if len(my_labels):
        span_my_words = [text[s[0]:s[1]] for s in my_labels]
        span_my_tokens = [re.sub('[^А-Яа-яёЁ ]+', '', item) for sublist in span_my_words for item in sublist.split()]

    if len(ground_truth_labels):
        span_gt_words = [text[s[0]:s[1]] for s in ground_truth_labels]
        span_gt_tokens = [re.sub('[^А-Яа-яёЁ ]+', '', item) for sublist in span_gt_words for item in sublist.split()]

    if len(span_my_tokens) == 0 or len(span_gt_tokens) == 0:
        return int(span_my_tokens == span_gt_tokens)

    tp = np.sum(list((Counter(span_gt_tokens) & \
                      Counter(span_my_tokens)).values()))
    precision = tp/len(span_my_tokens)
    recall = tp/len(span_gt_tokens)

    if precision + recall > 0:
        return 2*precision*recall/(precision+recall)
    else:
        return 0

Базовое решение для непростой задачи

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

model=OneVsRestClassifier(
  estimator=CatBoostClassifier(iterations=100, 
                               text_features=['text'], 
                               verbose=50)
)

model.fit(X_train, y_train)

Решение для задачи распознавания симптомов мы давать не стали, чтобы участники Data Science чемпионата могли покреативить. Данное решение давало результат метрики logloss=0.35и  F1 = 0.

Как работали над классификацией заболеваний?

Обработка данных

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

Во-первых, конкретно для этого соревнования наиболее эффективный подход — это доразметка спанов тренировочных данных для задачи NER. Этот подход позволил лидерам соревнования достаточно сильно оторваться по метрике за задачу выделения симптомов. Во-вторых, участники использовали базовые подходы для NLP-задач: удаление стоп-слов и знаков пунктуации, приведение к нижнему регистру, стемминг и лемматизация.

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

Один из возможных способов аугментации текста — перифраз текста. Когда текст либо сокращается, либо расширяется без изменения смысла. Примером данного решения является использование парафрайзера на основе “rut5-base-paraphraser” из библиотеки huggingface.

MODEL_NAME = 'cointegrated/rut5-base-paraphraser'
model = T5ForConditionalGeneration.from_pretrained(MODEL_NAME)
tokenizer = T5Tokenizer.from_pretrained(MODEL_NAME)

if torch.cuda.is_available():
		model.cuda();

model.eval();

def paraphrase_base(text, beams=5, grams=3, do_sample=False):
    x = tokenizer(text, return_tensors='pt', padding=True).to(model.device)
    max_size = int(x.input_ids.shape[1] * 1.5 + 10)
    out = model.generate(**x, encoder_no_repeat_ngram_size=grams, 
                         do_sample=do_sample, num_beams=beams, 
                         max_length=max_size, no_repeat_ngram_size=4)
    return tokenizer.decode(out[0], skip_special_tokens=True)

Еще одним вариантом является машинный перевод. Заключается он в том, чтобы перевести текст на какой-либо язык, а после обратно. Таким образом для некоторых слов подбираются синонимы и структура текста может несколько измениться, при этом смысл остается прежним. Реализуется данный метод аналогично с предыдущим, как модель можно использовать “LaBSE-en-ru”. Подробнее почитать про аугментацию текстов можно в блоге Александра Дьяконова. 

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

Модели, построенные на эмбеддингах

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

Базовым вариантом эмбеддингов является TF-IDF, который зависит от частоты употребления слова в документе. Подробнее почитать о данной статистической мере можно тут. Однако TF-IDF не учитывает контекст, что естественно не позволяет достичь наилучшего качества. И чтобы его улучшить, можно использовать эмбеддинги предобученных моделей, таких как Word2Vec, FastText и тд. 

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

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

Применение трансформеров

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

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

Если же говорить о выборе моделей, то наилучшие результаты были получены следующими из них: RuBERT-base, RuBERT-large, LaBSE-en-ru. Выбор моделей осуществлялся на основе предыдущего опыта, тестах и вычислительных возможностях. К слову, о последнем: так как предоставляемый нами корпус текстов был не таким большим, то даже тяжелые модели обучались достаточно быстро.

Предположим, что вы и так слышали о моделях семейства BERT (в предыдущей статье мы описывали, как применяем BERT в других задачах), а вот LaBSE — выбор совершенно неочевидный. Данная модель использовалась участником совместно с аугментацией путем машинного перевода, который к тому же осуществлялся этой же моделью. Помимо нее участник пробовал вариации BERT, но именно эта модель дала лучший результат на валидации и лидерборде. Более подробно почитать о модели можно по следующей ссылке. 

Как решать задачу NER?

О поиске ближайших слов через эмбеддинги

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

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

Алгоритм поиска схожих слов / синонимов
Алгоритм поиска схожих слов / синонимов

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

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

Про нейросети LSTM и RNN 

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

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

Пример разметки спана
Пример разметки спана

Далее на преобразованных данных обучается нейросеть. Архитектура в свою очередь может содержать LSTM, BiLSTM, RNN или GRU слои. Из интересных решений один из участников представил BiLSTM-сеть с CRF слоем. Подробнее о таком подходе можно почитать здесь.  

И снова о трансформерах

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

Для задачи NER тексты преобразовываются с помощью токенизатора и теггинга. Сначала тексты при помощи токенизатора переводятся в вектора — это то, на чем обучается модель. Далее создаются таргеты при помощи теггинга. Самым распространенным алгоритмом теггинга является “Inside–outside–beginning”. ТегI указывает на то, что слово находится внутри спана. ТегOуказывает — слово находится вне спана. ТегBна то, что тег является началом спана.

{
  "tags": ["O","O","O","O","O","O","O","O","O","O","O","O","O","B-B",
           "O","O","O","O","O","O","O","O","O","O","O","O","O","O","O",
           "O","O","O","O","O","O","O","O","O","O","O","O","O","O"],
  "tokens": ["Здравствуйте",",","опять","нужна","помощь",",","у","моей",
             "коровы","под","левым","глазом","появилась","шишка",".","Вет",
             "не","идет","посмотреть",",","сказала","наверное","укусил","кто",
             ",","велела","мазать","левомеколью",".","Ничего","не","проходит",
             "даже","больше","стала",".","Что","делать","?","Помогите","подскажите","пожалуйста","."]
}

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

Подытожим: напряженная конкуренция до последнего дня

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

График изменения результатов участников во времени
График изменения результатов участников во времени

Итоги после питч-сессии

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

  1. Предобработка данных

  2. Оригинальность подхода

  3. Применимость подхода на практике

  4. Сработавшие методы

  5. Идеи по дальнейшему развитию

После питч-сессии результаты и правда изменились: за счет интересного подхода к аугментации данных участник с места 2 переместился на 1, а также поменялись местами 3 и 4 опять же из-за оригинального подхода к решению задачи.

Планы на будущий год

AgroCode — ежегодная серия мероприятий в области AgroTech от Россельхозбанка для всех желающих поучаствовать в создании агротехнологий, а также просто неравнодушных к сельскому хозяйству. Как и в 2020, мероприятие собрало большое количество участников, для которых были проведены DS-чемпионат, хакатон и конференция. В 2022 году мы планируем продолжить AgroCode, но с достаточно серьезными преобразованиями: будет принципиально новый формат конкурса и интересные задачи, решениями которых обязательно поделимся на Хабре.

Источник: https://habr.com/ru/company/rshb/blog/646179/


Интересные статьи

Интересные статьи

Исходно нейросети были введены в практику как попытка смоделировать для решения прикладных задач работу нейронов в нервной системе животных. По всей видимости аналогичные...
Нейронные сети – это статистические вычислительные модели, применяемые к множеству практических задач, в том числе обработка изображений, машинный перевод и поиск шаблоно...
Устраивать конкурсы в инстаграме сейчас модно. И удобно. Инстаграм предоставляет достаточно обширный API, который позволяет делать практически всё, что может сделать обычный пользователь ручками.
Однажды, в понедельник, мне пришла в голову мысль — "а покопаюсь ка я в новом ядре" (новым относительно, но об этом позже). Мысль не появилась на ровном месте, а предпосылками для нее стали: ...
Если Вы используете в своих проектах инфоблоки 2.0 и таблицы InnoDB, то есть шанс в один прекрасный момент столкнуться с ошибкой MySQL «SQL Error (1118): Row size too large. The maximum row si...