Часть 2: Объяснение классификации изображений с помощью LIME

Автор Тигран Аветисян

Это ЧАСТЬ 2 нашей серии из 3 ЧАСТЕЙ «Сравнение объяснимых платформ ИИ».

См. ЧАСТЬ 1 здесь.

В ЧАСТИ 1 мы рассмотрели SHAP — популярную структуру для объяснимого ИИ, которая использует значения Шепли для объяснения моделей машинного обучения/глубокого обучения. Мы использовали SHAP для объяснения классификации цифр MNIST с помощью TensorFlow.

В ЧАСТИ 2 мы рассмотрим LIME — еще одну очень популярную структуру объяснимости ИИ. Хотя LIME преследует те же цели, что и SHAP, он сильно отличается от SHAP с точки зрения реализации и возможностей.

Ниже мы собираемся использовать LIME, чтобы еще раз объяснить классификацию цифр MNIST с помощью TensorFlow. Это позволит нам провести прямое сравнение с SHAP.

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

Давайте начнем!

Что такое ЛАЙМ?

LIME (Local Interpretable Model-agnostic Explanations) — это фреймворк объяснимости ИИ, который был представлен в 2016 году Марко Тулио Рибейро, Самиром Сингхом и Карлосом Гестрином в исследовательской статье Почему я должен вам доверять?: Объясняя предсказания любого классификатора».

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

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

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

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

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

Предпосылки для использования LIME

Чтобы использовать LIME для объяснения изображений, вам, прежде всего, понадобится сам LIME и scikit-image (skimage). LIME использует scikit-image для сегментации изображений.

Чтобы установить LIME и scikit-image, выполните следующие команды в терминале, если вы используете диспетчер пакетов pip:

pip install lime

pip install scikit-image

Если вы используете conda, используйте вместо этого следующие команды:

conda install -c conda-forge lime

conda install scikit-image

Вам также понадобится TensorFlow:

pip install tensorflow

Или с кондой:

conda install -c conda-forge tensorflow

Наконец, если у вас их еще нет, обязательно установите Matplotlib и NumPy.

Использование LIME для изучения предсказаний изображений

Теперь, когда мы понимаем, что такое LIME и как он работает, давайте посмотрим на фреймворк в действии!

Импорт зависимостей

Как всегда, мы начинаем с импорта зависимостей для нашего проекта:

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

Обучение сверточной нейронной сети

Далее давайте импортируем и обработаем набор данных MNIST для обучения:

Как и в ЧАСТИ 1, мы добавляем измерение канала к изображениям (строки 5 и 6), получаем уникальные метки в наборе данных (строка 13) и конвертируем метки в категориальные (строки 16 и 17). Мы дополнительно конвертируем изображения из оттенков серого в RGB (строки 9 и 10), потому что LIME по умолчанию ожидает изображения RGB.

Затем мы создаем нашу модель, которая почти идентична модели, которую мы использовали в ЧАСТИ 1:

Единственная разница в этой модели заключается в том, что входной слой ожидает 3-канальные изображения RGB.

Наконец, давайте скомпилируем и обучим нашу модель:

После обучения сохраните модель для последующего повторного использования:

Использование LIME для объяснения предсказаний модели

Теперь мы можем начать использовать LIME для объяснения предсказаний модели!

Как и в ЧАСТИ 1, давайте добавим по одному изображению на этикетку, чтобы мы могли объяснить все цифры. Они понадобятся нам позже.

https://gist.github.com/tavetisyan95/9798b8b91c580e5d6417839e7e944969

Далее, давайте объясним результаты нашей модели. LIME реализует класс LimeImageExplainerдля объяснения моделей изображений. Вы создаете экземпляр этого класса следующим образом:

Далее мы можем определить алгоритм сегментации для использования LIME. explainerбудет использовать этот алгоритм для выделения областей на изображении, которые положительно и отрицательно влияют на прогнозы.

Для обработки сегментации LIME реализует класс SegmentationAlgorithm, который является оболочкой над тремя алгоритмами сегментации scikit-image — quickshift, felzenszwalb и slic. По умолчанию LimeImageExplainer использует quickshift, но его аргументы по умолчанию не работали для объяснения цифр MNIST в нашей модели.

Вот как мы создаем сегментатор с помощью LIME:

В этом блоке кода мы передаем три аргумента SegmentationAlgorithm:

· algo_type=”quickshift” — определяет используемый алгоритм сегментации — в нашем случае quickshift.

· kernel_size=1 — задает ширину (стандартное отклонение) ядра Гаусса, используемого при сглаживании плотности выборки. Более высокие значения приводят к меньшему количеству кластеров и менее подробным объяснениям.

· max_dist=2 — определяет точку отсечки для расстояний данных, чем выше значение, тем меньше кластеров.

Обратите внимание, что только параметр algo_typeпринадлежит SegmentationAlgorithm. Остальные принадлежат quickshift. LIME принимает аргументы после algo_type и передает их алгоритму, определенному в algo_type. Ознакомьтесь с документацией skimage.segmentation, чтобы узнать больше о параметрах, реализованных в каждом из поддерживаемых алгоритмов.

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

Этот фрагмент кода будет хранить пояснения к изображениям в explanation.

Мы передали ряд аргументов explainer.explain_instance:

· image=imgs_to_explain[1] — образец изображения для создания объяснения.

· classifier_fn=model.predict функция, которая выводит вероятности предсказания. В нашем случае нам нужно просто использовать model.predict.

· top_labels=10— количество меток с наивысшей вероятностью, для которых explainerдолжны давать объяснения.

· num_samples=500 — размер окрестности для использования в линейной модели.

· segmentation_fn=segmenter — алгоритм сегментации, используемый для объяснения.

· random_seed=5 — целочисленное значение, используемое в качестве случайного начального значения для алгоритма сегментации.

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

Здесь explanation.get_image_and_mask(строка 2) возвращает:

· Исходное изображение с тепловой картой поверх него.

· Маска пояснения в отношении label=labels_to_explain[1].

Вместо labels_to_explain вы можете использовать прогнозы, хранящиеся в explanation.top_labels, который содержит прогнозы, отсортированные от самой высокой до самой низкой вероятности.

Обратите внимание на следующие аргументы, переданные в explanation.get_image_and_mask:

· positive_only должен ли LIME выделять только те области, которые положительно влияют на прогноз.

· negative_only — должен ли LIME выделять только те области, которые отрицательно влияют на прогноз.

· hide_rest– следует ли скрывать нерелевантные области в объяснении. Если positive_only=True, LIME скроет отрицательные области. Если positive_only=Falseи negative_only=True, LIME скроет положительные области.

В строке 9 мы используем функцию mark_boundariesиз scikit-image, чтобы наложить границы из maskна temp. Затем мы используем plt.imshowдля построения графика результата.

А вот как выглядит объяснение:

На этом изображении зеленый цвет соответствует областям, которые положительно влияют на предсказание цифры 1. Красные области соответствуют областям, которые имеют отрицательное значение для класса.

Мы можем проверить tempи mask, чтобы лучше понять, что они из себя представляют. Начнем с темп.:

temp — это исходное изображение с тепловой картой поверх него. Что касается mask:

maskсостоит из следующих значений, от темного к яркому:

· -1– области, которые не учитываются в классе. Они соответствуют красным областям в пояснении к изображению сверху.

· 0 — области, которые не учитываются в классе. Они соответствуют серым областям в объяснении.

· 1 — области, которые положительно влияют на класс. Они соответствуют зеленым областям в объяснении.

Изучение возможностей объяснения изображений в LIME

Выше мы рассмотрели только основы объяснения изображений LIME. Однако есть много параметров, которые мы можем настроить, чтобы получить более точное объяснение. Давайте теперь посмотрим на некоторые вещи, которые может сделать LIME!

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

Создание сегментатора

Во-первых, давайте настроим функцию, которая поможет нам создавать сегментаторы:

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

Генерация объяснений и построение графиков

Далее давайте создадим класс, который поможет нам быстро генерировать и отображать объяснения. Мы используем класс, чтобы мы могли генерировать объяснения только один раз, а затем создавать различные графики, просто изменяя аргументы.

Код класса выглядит так:

Вот несколько вещей, о которых следует помнить при работе с этим классом:

· В конструкторе класса (строки с 4 по 7) мы определяем пустой список self.explanations. Позже в этом списке будут храниться объяснения для imgs_to_explain, которые являются нашими образцами изображений из десяти цифр. Мы также создаем внутреннюю переменную imgs_to_explain, в которой будут храниться изображения десяти цифр для повторного использования.

· В методе explain_instances(строки с 10 по 29) мы перебираем предоставленные изображения, генерируем объяснения для каждого из них, а затем сохраняем объяснения в self.explanations. Перед созданием новых объяснений мы удаляем объяснения из предыдущих прогонов (строка 19).

· Метод plot_explanations(строки с 32 по 84) принимает ряд аргументов, включая image_indices и top_predictions. image_indicesопределяет индексы объектов объяснения в self.explanations, которые мы хотим использовать. image_indicesпо сути, это цифры, для которых мы хотим дать объяснение. Например, если вы передали top_labels=3в explain_instancesи хотите объяснить все изображения, image_indicesдолжно быть [0, 1, 2]. top_predictionsопределяет количество лучших прогнозов для image_indices, которые мы хотим объяснить.

· Метод plot_explanations_for_single_image(строки с 86 по 126) ожидает такие параметры, как image_indexи labels. Этот метод предназначен для генерации объяснений для одного изображения в отношении каждого из десяти возможных классов. image_indexопределяет индекс объяснителя, который мы хотим использовать, а labelsдолжен быть списком из десяти возможные классы.

Генерация пояснений с помощью quickshift

Давайте задействуем наши служебные инструменты и создадим объяснения для наших изображений в отношении их трех основных предсказанных классов. Для начала нам нужно создать объект сегментатора и объект класса Explainer:

Давайте теперь создадим пояснения для imgs_to_explain, которые будут храниться внутри my_explainer:

При использовании num_samples=1000 LIME может потребоваться некоторое время для создания объяснений. Вы можете уменьшить num_samples, если процесс занимает у вас слишком много времени.

Как только объяснение сделано, мы можем построить объяснения для трех верхних предсказанных классов для каждой из десяти цифр:

https://gist.github.com/tavetisyan95/984f811d9539194d0f3e1a6f0522d7b4

Результирующий график выглядит следующим образом:

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

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

И вот как выглядит получившийся график:

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

В качестве альтернативы мы можем передать positive_only=Falseнашей функции построения графика, что позволит нам увидеть как положительные, так и отрицательные области в объяснениях. Это поможет нам лучше различать их.

Результат выглядит следующим образом:

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

Если вы хотите видеть в пояснениях только отрицательные области, передайте positive_only=Falseи negative_only=Trueфункции построения графика. Вы можете дополнительно передать hide_rest=True, чтобы увидеть только отрицательные области:

Сюжет таков:

Давайте теперь сгенерируем пояснения для одного изображения относительно всех возможных меток. В качестве примера возьмем цифру 8 — должны получиться интересные объяснения, потому что 8похоже на другие цифры, такие как 3или 0.

Вот что мы получаем с positive_only=True(по умолчанию в plot.explanations_for_single_image):

Затем с hide_rest=True:

С positive_only=False:

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

И, наконец, вот что мы получаем с помощью positive_only=False, negative_only=True и hide_rest=True:

Создание объяснений с помощью felzenszwalb и slic

Теперь давайте также быстро рассмотрим два других сегментатора, которые поддерживает LIME — felzenszwalbи slic. Для этих алгоритмов сегментации мы пробовали разные комбинации параметров, чтобы получить четкие объяснения. Параметры, которые вы найдете ниже, должны подойти и вам, но вы можете попробовать поиграть с ними, чтобы увидеть, как они влияют на объяснения.

Чтобы переключить сегментаторы, нам нужно создать новый сегментатор и объяснить наши изображения с нуля. Начнем с felzenszwalb:

Чтобы не усложнять, давайте будем строить пояснения только с помощью positive_only=False. Для десяти цифр объяснения с felzenszwalb будут выглядеть следующим образом:

felzenszwalbвыделяет контуры цифр более плотно, чем quickshift. quickshift имел тенденцию выделять большие области на заднем плане.

А для цифры 8применительно ко всем возможным классам пояснения выглядят так:

И, наконец, попробуем slic:

Для десяти изображений slicобъяснения будут выглядеть следующим образом:

А для цифры 8применительно к возможным меткам пояснения будут следующими:

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

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

LIME против SHAP — как они сравниваются?

Теперь, как и было обещано, давайте кратко рассмотрим различия и сходства между LIME и SHAP. Ниже приведены некоторые области, в которых мы заметили заметные различия между двумя фреймворками. Но обратите внимание, что моменты, которые мы упомянем, относятся к классификации изображений MNIST с TensorFlow 2 — ваш опыт может различаться в зависимости от того, какие данные и модели вы используете.

Простота использования — Победитель: SHAP

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

В LIME нам пришлось поиграть с параметрами сегментаторов, чтобы получить разумные объяснения. Значения по умолчанию, определенные в scikit-image и LIME, не совсем работали с набором данных MNIST и нашей нейронной сетью, поэтому нам пришлось попробовать разные комбинации, чтобы получить графики, которые вы видели ранее.

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

SHAP был более интуитивным, потому что он давал четкие объяснения прямо из коробки. Единственная проблема, с которой мы столкнулись, заключалась в том, что маскеры inpaint_teleaи inpaint_nsне давали хороших объяснений. В итоге мы выбрали blur(128, 128)для ЧАСТИ 1, потому что это сработало для нашего набора данных и модели.

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

SHAP также имеет встроенные функции построения графиков, поэтому отображать пояснения было очень просто. С LIME нужно самому делать участки — это совсем не сложно, но требует времени.

Ясность объяснения — Победитель: Ничья

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

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

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

Поддержка TensorFlow 2 — Победитель: LIME

И SHAP, и LIME работают с TensorFlow 2, но у нас не было проблем с совместимостью при использовании LIME. С SHAP, если вы помните, нам пришлось включить режим совместимости в TensorFlow, чтобы использовать DeepExplainer. Кроме того, многие примеры в документации SHAP были написаны на TensorFlow 1, поэтому нам пришлось внести некоторые изменения в код, чтобы заставить его работать.

Мы считаем, что отчасти причина лучшей поддержки LIME для TensorFlow 2 заключается в том, что серверная часть LIME не слишком полагается на функции TensorFlow/Keras, если вообще полагается.

SHAP, напротив, делает некоторые очень специфичные для TensorFlow вещи под капотом. А начиная с версии 0.40.0 SHAP не был полностью переведен на TF 2, что вызвало у нас некоторые проблемы с совместимостью. Таким образом, хотя вы можете использовать SHAP с TensorFlow 2, LIME работает с ним намного лучше.

Документация — Победитель: SHAP

И у LIME, и у SHAP слабая документация, но в целом с документами SHAP работать было проще. В документации SHAP есть много примеров использования для разных задач, что ускорило для нас начало работы. Хотя у LIME есть примеры пояснений к изображениям в репозитории GitHub, они представляют собой просто записные книжки Jupyter с очень небольшим количеством комментариев.

У обоих фреймворков были довольно хорошие ссылки на API — вы можете найти ссылку на API для SHAP здесь и для LIME здесь. С учетом сказанного ссылка на SHAP кажется неполной. Например, не было ссылки на класс GradientExplainer, когда мы публиковали ЧАСТЬ 1, и метод __call­__(используемый для создания объяснений)не было указано в разделе Explainer.

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

Следующие шаги

LIME сильно отличается от SHAP своей реализацией, использованием и возможностями. Тем не менее, мы думаем, что в сравнении SHAP и LIME нет явных победителей — у любой платформы есть свои преимущества и недостатки. В идеале вы должны использовать оба, чтобы получить более широкий набор объяснений для вашей модели.

На этом ЧАСТЬ 2 ЗАВЕРШАЕТСЯ! В ЧАСТИ 3 этой серии мы собираемся интегрировать LIME в приложение React. Мы выбираем LIME из-за его настраиваемости и лучшей поддержки TensorFlow 2.

Ждите ЧАСТЬ 3!

Код

Вы можете найти весь код для этой статьи в блокноте Jupyter здесь.