Все, что Вам нужно знать!

За последние несколько лет компьютерное зрение шагнуло далеко вперед. Между одноступенчатыми детекторами, поиском нейронной архитектуры, преобразователями зрения и фундаментальными моделями проблемы, которые когда-то казались непреодолимыми, теперь стали стандартными. Существуют даже высококачественные модели с нулевым выстрелом для обнаружения (например, Grounding DINO) и сегментации (SAM, FastSAM), поэтому для многих приложений вы можете получить хорошие базовые показатели без какого-либо специального обучения!

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

Стремясь устранить эти предубеждения, команда Meta выпустила FACET (Fairness in Computer Vision EvaluaTion), новый эталонный набор данных для изучения и оценки справедливости моделей компьютерного зрения. При разработке FACET команда намеревалась создать наиболее полный и разнообразный набор данных по показателям справедливости на сегодняшний день.

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

Краткая информация о наборе данных FACET

"Бумага"

  • Название: FACET: Оценка справедливости в оценке компьютерного зрения — ICCV 2023
  • Авторы: Лаура Густафсон, Хлоя Роллан, Никила Рави, Квентин Дюваль, Аарон Адкок, Ченг-Ян Фу, Мелисса Холл, Кэндис Росс | Мета ИИ

Набор данных

  • "Лицензия"
  • "Данные карты"
  • ⚠️Использовать только в целях оценки, НЕ в целях обучения.

Аннотации

  • annotations/annotations.csv: обнаружение людей с помощью ограничивающих рамок и атрибутов (подробности см. в карточке данных)
  • annotations/coco_boxes.json: Ограничительные рамки обнаружения людей в формате MS COCO без атрибутов
  • annotations/coco_masks.json: Маски сегментации в формате MS COCO для людей, одежды и волос, закодированные с помощью Кодирования длины пробега (RLE), и ограничивающие рамки.

Статистика набора данных

  • 31 702 изображения (подмножество набора данных SA-1B)
  • 49 551 уникальное обнаружение людей, охватывающее защищенные и незащищенные атрибуты, условия освещения и многое другое.
  • 69 105 экземпляров масок сегментации (человек, одежда и волосы)
  • 52 класса, связанных с людьми (все обнаружения людей имеют первичный класс; некоторые также имеют вторичный класс)
  • Воспринимаемая интерпретация метки тона кожи: 1–10 по шкале тона кожи монаха.

Усилия, предпринимаемые командой FACET для обеспечения справедливости

  • Эксперты-аннотаторы: команда наняла экспертов из разных географических регионов, от Америки до Юго-Восточной Азии. Аннотаторы также прошли «этапное обучение», прежде чем приступить к маркировке.
  • Воспринимаемые метки: защищенные атрибуты (возраст, пол и цвет кожи) помечаются как «воспринимаемые», чтобы отразить ограничения аннотаций изображений. Например, что касается возраста, команда пишет: «невозможно определить истинный возраст человека по изображению, эти числовые диапазоны являются приблизительным ориентиром для определения каждой воспринимаемой возрастной группы».
  • Различные сценарии: от различных условий освещения и степени окклюзии до различного количества людей на изображении и множества аксессуаров — команда FACET попыталась охватить широкий спектр визуальных сценариев. Помимо оценки зависимости эффективности модели от одной демографической переменной, это позволяет оценить пересекаемость.
  • Фильтрация изображений: чтобы избежать унаследования систематических ошибок от существующих моделей обнаружения и классификации, команда FACET использовала аннотаторов-людей для фильтрации изображений, на которых не было человека, соответствующего одной из 52 категорий ярлыков (певец, художник, космонавт). , …). Это отфильтровало около 80% исходных изображений.
  • Устранение неоднозначности классов: иногда человек подходит под описание более чем одного класса меток. Как показывают авторы FACET, «человек, играющий на гитаре и поющий, может соответствовать категориям гитариста и певца». Чтобы учесть это, обнаруженным лицам при необходимости присваивается метка вторичного класса.
  • Агрегация оттенков кожи: поскольку тон кожи может влиять на восприятие оттенков кожи других людей, FACET объединяет воспринимаемые значения тона кожи по нескольким аннотаторам для каждого ярлыка.

Значение рассчитывается по атрибуту

  • Условия освещения
well lit: 35533
underexposed: 1313
overexposed: 553
dimly lit: 10955
None/na: 1197
  • Тип волос
straight: 18382
curly: 719
bald: 1017
wavy: 6141
dreadlocks: 280
coily: 458
None/na: 22554
  • Цвет волос
black: 14041
blonde: 2249
red: 333
colored: 248
brown: 10668
grey: 2107
None/na: 19905
  • Имеет волосы на лице
True: 6121
False: 43430
  • Представление о предполагаемом возрасте
young (25-40): 8860
middle (41-65): 27380
older (65+): 2659
None: 10652
  • Воспринимаемая гендерная презентация
fem: 10245
masc: 33240
non binary: 95
None/na: 5971
  • Тату
False: 48846
True: 705

Начальный класс (в алфавитном порядке)

'astronaut': 286,
 'backpacker': 1612,
 'ballplayer': 1309,
 'bartender': 56,
 'basketball_player': 1668,
 'boatman': 2048,
 'carpenter': 223,
 'cheerleader': 399,
 'climber': 455,
 'computer_user': 1164,
 'craftsman': 1034,
 'dancer': 1397,
 'disk_jockey': 310,
 'doctor': 802,
 'drummer': 977,
 'electrician': 468,
 'farmer': 1542,
 'fireman': 913,
 'flutist': 302,
 'gardener': 457,
 'guard': 1361,
 'guitarist': 1180,
 'gymnast': 615,
 'hairdresser': 458,
 'horseman': 735,
 'judge': 96,
 'laborer': 2540,
 'lawman': 4455,
 'lifeguard': 511,
 'machinist': 354,
 'motorcyclist': 1367,
 'nurse': 1042,
 'painter': 898,
 'patient': 884,
 'prayer': 798,
 'referee': 755,
 'repairman': 1295,
 'reporter': 470,
 'retailer': 546,
 'runner': 638,
 'sculptor': 213,
 'seller': 1178,
 'singer': 1286,
 'skateboarder': 990,
 'soccer_player': 1226,
 'soldier': 1457,
 'speaker': 1416,
 'student': 682,
 'teacher': 192,
 'tennis_player': 1661,
 'trumpeter': 498,
 'waiter': 332

Загрузка набора данных FACET

Предварительные условия

imgs_1 Прежде чем загрузить набор данных, вам необходимо подписать соглашение об использовании FACET компании Meta здесь. После этого разархивируйте четыре zip-файла (annotations, imgs_1, imgs_2 и imgs_3).

Мы будем использовать библиотеку компьютерного зрения с открытым исходным кодом FiftyOne для управления данными и визуализации, поэтому, если вы еще этого не сделали, установите FiftyOne:

pip install fiftyone

В Python импортируйте необходимые библиотеки:

import json
import numpy as np
import os
import pandas as pd
from PIL import Image
from pycocotools import mask as maskUtils
from tqdm.notebook import tqdm

Помимо модулей FiftyOne мы будем использовать:

import fiftyone as fo
import fiftyone.brain as fob
import fiftyone.zoo as foz
from fiftyone import ViewField as F

Создание набора данных

Теперь мы готовы создать набор данных. Мы создадим пустой набор данных (и сохраним его в базе данных), а затем добавим все изображения в каждую из трех разархивированных папок изображений:

## use relative paths to your image dirs
IMG_DIRS = ["imgs_1", "imgs_2", "imgs_3"]
dataset = fo.Dataset(name = "FACET", persistent=True)
for img_dir in IMG_DIRS:
    dataset.add_images_dir(img_dir)
dataset.compute_metadata()

Мы также вычисляем «метаданные», чтобы можно было использовать ширину и высоту выборки при преобразовании между относительным и абсолютным соглашением ограничивающего прямоугольника.

Мы можем распечатать набор данных и получить некоторые краткие факты:

print(dataset)
Name:        FACET
Media type:  image
Num samples: 31702
Persistent:  True
Tags:        []
Sample fields:
    id:                  fiftyone.core.fields.ObjectIdField
    filepath:            fiftyone.core.fields.StringField
    tags:                fiftyone.core.fields.ListField(fiftyone.core.fields.StringField)
    metadata:            fiftyone.core.fields.EmbeddedDocumentField(fiftyone.core.metadata.ImageMetadata)

И мы можем запустить сеанс FiftyOne App для просмотра изображений:

session = fo.launch_app(dataset)

Добавление обнаружения людей

Сначала мы загружаем основные аннотации из CSV в DataFrame pandas:

gt_df = pd.read_csv('annotations/annotations.csv')

Теперь нам нужно проанализировать этот DataFrame и добавить соответствующим образом структурированные данные в наш набор данных. Как и авторы FACET, давайте разобьем атрибуты на три типа:

  1. Атрибуты личности (hairtype, has_eyeware и т. д.)
  2. Защищенные атрибуты (воспринимаемое представление пола, предполагаемое представление возраста и воспринимаемый оттенок кожи)
  3. Другие атрибуты (освещение, видимость)

Для каждой строки в кадре данных мы создадим FiftyOne Detection и добавим соответствующие атрибуты для обнаружения.

Атрибуты личности

Мы можем создать кортеж для логических атрибутов, которые будем перебирать:

BOOLEAN_PERSONAL_ATTRS = (
    "has_facial_hair",
    "has_tattoo",
    "has_cap",
    "has_mask",
    "has_headscarf",
    "has_eyeware",
)
def add_boolean_person_attributes(detection, row_index):
    for attr in BOOLEAN_PERSONAL_ATTRS:
        detection[attr] = gt_df.loc[row_index, attr].astype(bool)

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

def get_hairtype(row_index):
    hair_info = gt_df.loc[row_index, gt_df.columns.str.startswith('hairtype')]
    hairtype = hair_info[hair_info == 1]
    if len(hairtype) == 0:
        return None
    return hairtype.index[0].split('_')[1]
def get_haircolor(row_index):
    hair_info = gt_df.loc[row_index, gt_df.columns.str.startswith('hair_color')]
    haircolor = hair_info[hair_info == 1]
    if len(haircolor) == 0:
        return None
    return haircolor.index[0].split('_')[2]

Все это можно объединить в одну функцию, чтобы добавить следующие атрибуты человека:

def add_person_attributes(detection, row_index):
    detection["hairtype"] = get_hairtype(row_index)
    detection["haircolor"] = get_haircolor(row_index)
    add_boolean_person_attributes(detection, row_index)

Защищенные атрибуты

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

def get_perceived_gender_presentation(row_index):
    gender_info = gt_df.loc[row_index, gt_df.columns.str.startswith('gender')]
    pgp = gender_info[gender_info == 1]
    if len(pgp) == 0:
        return None
    return pgp.index[0].replace("gender_presentation_", "").replace("_", " ")
def get_perceived_age_presentation(row_index):
    age_info = gt_df.loc[row_index, gt_df.columns.str.startswith('age')]
    pap = age_info[age_info == 1]
    if len(pap) == 0:
        return None
    return pap.index[0].split('_')[2]

Что касается тона кожи, одно обнаружение может иметь несколько оттенков кожи с ненулевыми значениями. Чтобы собрать всю эту информацию, мы просто преобразуем столбцы оттенков кожи в словарь и сохраним словарь под атрибутом skin_tone:

def get_skintone(row_index):
    skin_info = gt_df.loc[row_index, gt_df.columns.str.startswith('skin_tone')]
    return skin_info.to_dict()

В целом добавление защищенных атрибутов к обнаружению выполняется в этой функции:

def add_protected_attributes(detection, row_index):
    detection["perceived_age_presentation"] = get_perceived_age_presentation(row_index)
    detection["perceived_gender_presentation"] = get_perceived_gender_presentation(row_index)
    detection["skin_tone"] = get_skintone(row_index)

Другие атрибуты

Как и в случае с логическими атрибутами person, мы можем создать кортеж атрибутов видимости для перебора:

VISIBILITY_ATTRS = ("visible_torso", "visible_face", "visible_minimal")

Помимо этого, нам просто нужно обработать информацию об освещении:

def get_lighting(row_index):
    lighting_info = gt_df.loc[row_index, gt_df.columns.str.startswith('lighting')]
    lighting = lighting_info[lighting_info == 1]
    if len(lighting) == 0:
        return None
    lighting = lighting.index[0].replace("lighting_", "").replace("_", " ")
    return lighting
def add_other_attributes(detection, row_index):
    detection["lighting"] = get_lighting(row_index)
    for attr in VISIBILITY_ATTRS:
        detection[attr] = gt_df.loc[row_index, attr].astype(bool)

Теперь у нас есть все детали, необходимые для создания Detection, учитывая индекс строки из DataFrame pandas. Мы также передадим sample, соответствующий этой строке, чтобы мы могли преобразовать абсолютные и относительные координаты ограничительной рамки:

def create_detection(row_index, sample):
    bbox_dict = json.loads(gt_df.loc[row_index, "bounding_box"])
    x, y, w, h = bbox_dict["x"], bbox_dict["y"], bbox_dict["width"], bbox_dict["height"]
    cat1, cat2 = bbox_dict["dict_attributes"]["cat1"], bbox_dict["dict_attributes"]["cat2"]
    person_id = gt_df.loc[row_index, "person_id"]
    img_width, img_height = sample.metadata.width, sample.metadata.height
    bounding_box = [x/img_width, y/img_height, w/img_width, h/img_height]
    detection = fo.Detection(
        label=cat1, 
        bounding_box=bounding_box,
        person_id=person_id,
        )
    if cat2 != 'none':
        detection["class2"] = cat2
    add_person_attributes(detection, row_index)
    add_protected_attributes(detection, row_index)
    add_other_attributes(detection, row_index)
    return detection

Все, что осталось, — это перебрать образцы в нашем наборе данных (это более эффективно, чем перебирать строки в gt_df DataFrame и фильтровать набор данных для нужного образца), добавляя обнаружения к каждому образцу по мере продвижения:

def add_ground_truth_labels(dataset):
    for sample in dataset.iter_samples(autosave=True, progress=True):
        sample_annos = gt_df[gt_df['filename'] == sample.filename]
        detections = []
        for row in sample_annos.iterrows():
            row_index = row[0]
            detection = create_detection(row_index, sample)
            detections.append(detection)
        sample["ground_truth"] = fo.Detections(detections=detections)
    dataset.add_dynamic_sample_fields()
## add all of the ground truth labels
add_ground_truth_labels(dataset)

Добавление масок сегментации

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

Функция add_coco_masks_to_dataset() ниже делает следующее:

  • Перебирать образцы в наборе данных
  • Для каждого образца, имя файла которого имеет запись в файле аннотаций масок COCO, мы извлекаем маску сегментации и декодируем ее из формата RLE в двоичную маску полного изображения, используя pycocoutils.
  • Используя pillow и ограничивающую рамку, связанную с маской сегментации, мы обрезаем маску, превращаем ее обратно в массив и добавляем массив в качестве маски (с ограничивающей рамкой) к новому Detection.
def add_coco_masks_to_dataset(dataset):
    coco_masks = json.load(open("annotations/coco_masks.json", "r"))
    cmas = coco_masks["annotations"]
    FILENAME_TO_ID = {
        img["file_name"]: img["id"]
        for img in coco_masks["images"]
    }
    CAT_TO_LABEL = {cat["id"]: cat["name"] for cat in coco_masks["categories"]}
    for sample in dataset.iter_samples(autosave=True, progress=True):
        fn = sample.filename
        if fn not in FILENAME_TO_ID:
            continue
        img_id = FILENAME_TO_ID[fn]
        img_width, img_height = sample.metadata.width, sample.metadata.height
        sample_annos = [a for a in cmas if a["image_id"] == img_id]
        if len(sample_annos) == 0:
            continue
        coco_detections = []
        for ann in sample_annos:
            label = CAT_TO_LABEL[ann["category_id"]]
            bbox = ann['bbox']
            ann_id = ann['ann_id']
            person_id = ann['facet_person_id']
            mask = maskUtils.decode(ann["segmentation"])
            mask = Image.fromarray(255*mask)
            ## Change bbox to be in the format [x, y, x, y]
            bbox[2] = bbox[0] + bbox[2]
            bbox[3] = bbox[1] + bbox[3]
            ## Get the cropped image
            cropped_mask = np.array(mask.crop(bbox)).astype(bool)
            ## Convert to relative [x, y, w, h] coordinates
            bbox[2] = bbox[2] - bbox[0]
            bbox[3] = bbox[3] - bbox[1]
            bbox[0] = bbox[0]/img_width
            bbox[1] = bbox[1]/img_height
            bbox[2] = bbox[2]/img_width
            bbox[3] = bbox[3]/img_height
            new_detection = fo.Detection(
                label=label, 
                bounding_box=bbox,
                person_id=person_id,
                ann_id=ann_id,
                mask=cropped_mask,
                )
            coco_detections.append(new_detection)
        sample["coco_masks"] = fo.Detections(detections=coco_detections)

## add the masks
add_coco_masks_to_dataset(dataset)

Оценка смещения модели

Метрики оценки FACET

Чтобы оценить предвзятость вашей модели с помощью FACET, авторы набора данных предлагают просмотреть отзыв модели на подмножествах данных, определенных концепцией и атрибутом. Например, концепцией может быть метка класса, такая как «путешественник», а атрибутом может быть значение «кудрявый» для прически. Атрибуты также можно комбинировать для изучения качества прогнозирования с помощью перекрестности.

Разница в производительности модели между двумя наборами атрибутов — это разница между показателями запоминаемости этих подмножеств.

При оценке моделей классификации (где классификация выполняется на основе достоверных участков) используется стандартное определение отзыва:

Это можно интерпретировать как скорость, с которой правильно идентифицируются истинные положительные результаты.

При оценке моделей обнаружения учитываются только прогнозы с меткой person. Прогнозы считаются правильными, когда перекрытие между ограничивающей рамкой основной истины и ограничивающей рамкой прогноза — формально известное как Пересечение через объединение (IoU) — превышает некоторый порог. Отзыв, который включается в уравнение несоответствия, представляет собой средний отзыв по последовательности пороговых значений IoU [0,5, 0,55, …, 0,95, 1,0] и известен как средний средний отзыв (mAR).

Добавление прогнозов в набор данных

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

Для обнаружения загрузим YOLOv5m, обученный на COCO, из FiftyOne Model Zoo:

yolov5 = foz.load_zoo_model('yolov5m-coco-torch')

Затем мы можем применить модель к нашему набору данных и сохранить прогнозы в поле наших образцов с помощью:

dataset.apply_model(yolov5, label_field="yolov5m")
### Just retain the "person" detections
people_view_values = dataset.filter_labels("yolov5m", F("label") == "person").values("yolov5m")
dataset.set_values("yolov5m", people_view_values)
dataset.save()

Для классификации с нулевым выстрелом, как в статье FACET, мы будем использовать модель CLIP с пользовательскими классами. Нам снова достаётся модель из FiftyOne Model Zoo:

## get a list of all 52 classes
facet_classes = dataset.distinct("ground_truth.detections.label")
## instantiate a CLIP model with these classes
clip = foz.load_zoo_model(
    "clip-vit-base32-torch",
    text_prompt="A photo of a",
    classes=facet_classes,
)

Чтобы сгенерировать прогнозы классификации (эффективно рассматривая области ограничивающего прямоугольника ground_truth как отдельные изображения), мы будем использовать метод to_patches() FiftyOne, чтобы создать представление всех основных участков истины в нашем наборе данных, а затем применить модель CLIP к этим участкам:

patch_view = dataset.to_patches("ground_truth")
patch_view.apply_model(clip, label_field="clip")
dataset.save_view("patch_view", patch_view)

Последняя строка в блоке кода выше сохраняет это представление в наборе данных.

Оценка прогнозов обнаружения

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

Для обнаружения мы определяем пороговые значения IoU, по которым будем усреднять:

IOU_THRESHS = np.round(np.arange(0.5, 1.0, 0.05), 2)

Следующую функцию необходимо запустить только один раз для каждой модели обнаружения:

def _evaluate_detection_model(dataset, label_field):
    eval_key = "eval_" + label_field.replace("-", "_")
    dataset.evaluate_detections(label_field, "ground_truth", eval_key=eval_key, classwise=False)
    
    for sample in dataset.iter_samples(autosave=True, progress=True):
        for pred in sample[label_field].detections:
            iou_field = f"{eval_key}_iou"
            if iou_field not in pred:
                continue
            iou = pred[iou_field]
            for it in IOU_THRESHS:
                pred[f"{iou_field}_{str(it).replace('.', '')}"] = iou >= it

При этом используется встроенный метод evaluate_detections() FiftyOne для вычисления IoU каждой ограничивающей рамки прогноза, которая перекрывается с ограничивающей рамкой ground_truth. Мы передаем classwise=False, потому что метки для обнаружения истинной истины — это 52 класса в наборе данных FACET, тогда как метка для наших прогнозов — «человек». Какой набор прогнозов оценивать, определяется label_field.

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

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

def _compute_detection_mAR(sample_collection, label_field):
    """Computes the mean average recall of the specified detection field.
    -- computed as the average over iou thresholds of the recall at
    each threshold.
    """
    eval_key = "eval_" + label_field.replace("-", "_")
    iou_recalls = []
    for it in IOU_THRESHS:
        field_str = f"{label_field}.detections.{eval_key}_iou_{str(it).replace('.', '')}"
        counts = sample_collection.count_values(field_str)
        tp, fn = counts.get(True, 0), counts.get(False, 0)
        recall = tp/float(tp + fn) if tp + fn > 0 else 0.0
        iou_recalls.append(recall)
    return np.mean(iou_recalls)

Чтобы замкнуть цикл с набором данных FACET, мы можем определить функцию get_concept_attr_detection_mAR(), которая принимает концепцию (основную категорию для человека) и словарь атрибутов в форме {field:value} и возвращает среднее значение отзыва для этой комбинации:

def get_concept_attr_detection_mAR(dataset, label_field, concept, attributes):
    sub_view = dataset.filter_labels("ground_truth", F("label") == concept)
    for attribute in attributes.items():
        if "skin_tone" in attribute[0]:
            sub_view = sub_view.filter_labels("ground_truth", F(f"skin_tone.{attribute[0]}") != 0)
        else:
            sub_view = sub_view.filter_labels(f"ground_truth", F(attribute[0]) == attribute[1])
    return _compute_detection_mAR(sub_view, label_field)

В качестве примера давайте посмотрим среднюю запоминаемость YOLOv5m для гимнасток с вьющимися черными волосами:

concept =  'gymnast'
attributes = {"hairtype": "curly", "haircolor": "black"}
get_concept_attr_detection_mAR(dataset, "yolov5m", concept, attributes)
## 0.875

Эту процедуру оценки обнаружения можно адаптировать в процедуру оценки сегментации экземпляра путем фильтрации масок COCO до масок людей и передачи use_masks=True в evaluate_detections().

Оценка прогнозов классификации

По аналогии с обнаружениями для моделей классификации мы можем создать функцию _evaluate_classification_model(), которую нужно запускать только один раз для каждой модели:

def _evaluate_classification_model(dataset, prediction_field):
    patch_view = dataset.load_saved_view("patch_view")
    eval_key = "eval_" + prediction_field
    
    for sample in patch_view.iter_samples(progress=True):
        sample[eval_key] = (
            sample.ground_truth.label == sample[prediction_field].label
        )
        sample.save()
    dataset.save_view("patch_view", patch_view, overwrite=True)

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

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

def _compute_classification_recall(patch_collection, label_field):
    eval_key = "eval_" + label_field.split("_")[0]
    counts = patch_collection.count_values(eval_key)
    tp, fn = counts.get(True, 0), counts.get(False, 0)
    recall = tp/float(tp + fn) if tp + fn > 0 else 0.0
    return recall

И мы можем вернуть это к подмножествам наших данных, описываемых понятиями и атрибутами:

def get_concept_attr_classification_recall(dataset, label_field, concept, attributes):
    patch_view = dataset.load_saved_view("patch_view")
    sub_patch_view = patch_view.match(F("ground_truth.label") == concept)
    for attribute in attributes.items():
        if "skin_tone" in attribute[0]:
            sub_patch_view = sub_patch_view.match(F(f"ground_truth.skin_tone.{attribute[0]}") != 0)
        else:
            sub_patch_view = sub_patch_view.match(F(f"ground_truth.{attribute[0]}") == attribute[1])
    return _compute_classification_recall(sub_patch_view, label_field)

Для той же самой комбинации понятий и атрибутов, описанной выше, наша модель CLIP дает следующее:

get_concept_attr_classification_recall(dataset, "clip", concept, attribute)
## 0.6193353474320241

Оценка неравенства

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

Мы создадим функцию get_concept_attr_recall(), которая будет использовать соответствующее определение отзыва в зависимости от того, содержит ли наше поле метки прогнозы классификации или обнаружения:

def get_concept_attr_recall(dataset, label_field, concept, attribute):
    if label_field in dataset.get_field_schema().keys():
        return get_concept_attr_detection_mAR(dataset, label_field, concept, attribute)
    else:
        return get_concept_attr_classification_recall(dataset, label_field, concept, attribute)

И завяжем все красивым бантиком с помощью функции compute_disparity():

def compute_disparity(dataset, label_field, concept, attribute1, attribute2):
    recall1 = get_concept_attr_recall(dataset, label_field, concept, attribute1)
    recall2 = get_concept_attr_recall(dataset, label_field, concept, attribute2)
    return recall1 - recall2

В качестве примера давайте посмотрим на разницу в производительности модели CLIP для некоторых концепций, когда тип волос прямой и вьющийся:

attrs1 = {"hairtype": "curly"}
attrs2 = {"hairtype": "straight"}
for concept in ["astronaut", "singer", "judge", "student"]:
    disparity = compute_disparity(dataset, "clip", concept, attrs1, attrs2)     
    print(f"{concept}: {disparity}")
#### OUTPUT ####
## astronaut: -0.8269230769230769
## singer: -0.0008051529790660261
## judge: -0.06666666666666667
## student: 0.16279069767441856

В выходных данных значение ближе к +1 означает, что модель лучше запоминается для вьющихся волос, чем для прямых волос, а значение ближе к -1 означает, что модель имеет более высокую запоминаемость для прямых волос. В то время как для singer CLIP обеспечивает примерно одинаковую запоминаемость для людей с прямыми волосами и людей с вьющимися волосами, существует резкая разница в производительности для astronaut — CLIP имеет гораздо более высокую запоминаемость для космонавтов с прямыми волосами, чем для космонавтов с кудрявыми волосами.

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

Заключение

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

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

Оригинально опубликовано на сайте https://voxel51.com 12 сентября 2023 г.