Время прочтения: 4 мин.

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

Рис.1 – Общий принцип работы

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

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

Рис. 2 – Классификатор текстов

При этом первый алгоритм при прогнозе ориентируется на значимые слова, относящиеся к рассматриваемой теме, а второй на «синтаксический мусор» — общие местоимения, предлоги и т.п. В случае же сложных алгоритмов, таких как нейросети, LIME можно использовать в качестве дополнительной проверки модели на адекватность. Пример показан на Рис. 3, где выделены области, повышающие вероятность отнесения к определенному классу).

Рис. 3 – Мультиклассификатор модели Inception

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

import torch
import cv2
from lime import lime_image
from torch.nn import Softmax
from PIL import Image

def load_checkpoint(filepath):
    checkpoint = torch.load(filepath)
    model = checkpoint['model'].to(device)
    model.load_state_dict(checkpoint['state_dict'])
    for parameter in model.parameters():
        parameter.requires_grad = False
    return model.eval()

filepath = "torch_classifier.pth" #Классификатор лица в маске или нет.
model = load_checkpoint(filepath)

def read_image(image_path):
    image = cv2.imread(image_path)
    # проверяем, что изображение прочиталось
    if image is None:
        raise OSError('Не смогли прочитать файл изображения')
    return image

def isFaceMasked(erasedFaces, logits):
    out = []
    for i in range(erasedFaces.shape[0]):
        pil_image = Image.fromarray(erasedFaces[i, ...], mode = "RGB")
        pil_image = train_transforms(pil_image)
        image = pil_image.unsqueeze(0).to(device)
        predictor = model(image).data.to('cpu').numpy()
        if logits:
            out.append(predictor[1])
        else:
            softmax = Softmax(dim=1)
            softmax_pred = softmax(torch.tensor(predictor)).numpy()
            out.append(softmax_pred[:,1])
    return out

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

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

def explain_example(filename, do_explain=True):
    explainer = lime_image.LimeImageExplainer()
    orig_image_array = read_image(filename)
    fface = detectFaces(orig_image_array)
    if fface is None:
        print('Нет лиц')
    else:
        model_pred = isFaceMasked(fface)
        mask, no_mask = model_pred[0]
        if mask > no_mask:
            print('Предсказание модели: маска есть, вероятность %.2f' % mask)
        else:
            print('Предсказание модели: маски нет, вероятность %.2f' % no_mask)
        print('Лицо')

        pil_image = Image.fromarray(fface, mode = "RGB")
        pil_image.show()

        if do_explain:
            explanation = explainer.explain_instance(fface
                                                    ,classifier_fn=isFaceMasked
                                                    ,num_samples=1000
                                                    ,top_labels=1)
            return explanation

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

Для получения одного объяснения потребовалось получить около 1000 прогнозов в режиме инференс (использовался серверный GPU). Для каждой итерации LIME незначительно изменяет значения пикселей входного изображения и фиксировал изменения предсказания модели. В результате алгоритм выделил области, при изменении которых наиболее значительно изменялась вероятность отнесения к определенному классу. На картинках зеленым выделены области, повышающие вероятность отнесения к правильному классу, а красным – понижающие (т.е. слева зеленый показывает, где «маски нет», справа наоборот — красным где «маски нет», зеленым где «маска есть»). Также можно выделять только области, повышающие вероятность отнесения к классу.