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

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

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

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

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

Предложение было в книге, но не было исполнения. Вот как я применяю решение на практике. Исходный код проекта доступен на GitHub, а приложение на базе Gradio доступно на Huggingface Spaces. Пожалуйста, не стесняйтесь просматривать приложение и код, поскольку я открыт для идей.

Следующий фрагмент кода содержит все импорты, необходимые для проекта:

from fastdownload import download_url
from duckduckgo_search import ddg_images
from fastcore.all import *
from fastai.vision.all import *
import timm
from time import sleep

Следующим шагом будет получение данных с помощью duckduckgo_search:

def search_images(term, max_images=40):
    print(f"Searching for '{term}'")
    return L(ddg_images(term,max_results=max_images)).itemgot('image')

searches = ['grizzly',"black",'teddy','panda','polar']
path = Path("Bears")
from time import sleep

for o in searches:
    dest = (path/o)
    dest.mkdir(exist_ok=True,parents = True)
    download_images(dest, urls=search_images(f'{o} bear'))
    sleep(10)
    download_images(dest, urls=search_images(f'{o} bear photo'))
    sleep(10)
    download_images(dest, urls=search_images(f'{o} bear forest'))
    sleep(10)
    resize_image(path/o, max_size=400, dest=path/o)

Возможно, некоторые из загруженных нами фотографий были ошибочными. Таким образом, мы будем использовать код для устранения неудачных фотографий, чтобы уменьшить проблему. В Fastai есть две функции, которые могут нам в этом помочь. Это опция unlink, которая удалит ошибочные изображения, и verify_images, которая будет использовать загруженные изображения для проверки их точности.

fns = get_image_files(path)
failed = verify_images(fns)
failed.map(Path.unlink)

Наиболее важным шагом в обучении модели с использованием Fastai является создание DataBlock, который будет изменять размер изображений и выполнять увеличение данных. Здесь мы определим блок MultiCategoryBlock и ImageBlock, который сообщит fastai, что ввод будет изображением, а метки будут в Multi-Labelled. Чтобы сделать это проблемой классификации с одной меткой, просто измените MultiCategoryBlock на CategoryBlock.

bears = DataBlock(
    blocks = (ImageBlock,MultiCategoryBlock),
    get_items=get_image_files,
    get_y = parent_label,
    splitter = RandomSplitter(valid_pct=0.3,seed=20),
    item_tfms = RandomResizedCrop(460, min_scale = 0.5),
    batch_tfms=aug_transforms(size=224)
)

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

dls = bears.dataloaders(path,bs= 10)
dls.train.show_batch(max_n=6,unique=True)

Как мы видим, из-за того, что мы указали вывод как Multi-label, метка белого медведя — «a;l;o;p;r», которая является подмножеством полярного.

Таким образом, теоретически у каждого изображения есть несколько меток, в этом случае вместо «полярного» в качестве единственной метки у нас есть «a; l; o; p; r» в виде 5 меток. Или «д», «е», «т», «у» для плюшевого мишки. Это означает, что модель должна предсказать все метки для одного изображения. Вот как мы меняем метки данных с одной меткой, чтобы они работали как данные с несколькими метками.

Теперь мы создадим модель и обучим ее. Изначально модель обучалась на resnet18 из-за быстрого обучения. Но затем переключился на convnext_small для большей точности. Мы использовали метод lr_find, чтобы получить наилучшую скорость обучения для обучения модели. Мы создали пользовательскую функцию precision_multi, чтобы использовать ее для получения точности в настройках с несколькими метками.

def accuracy_multi(inp, targ, thresh=0.5, sigmoid=True):
    "Compute accuracy when `inp` and `targ` are the same size."
    if sigmoid: inp = inp.sigmoid()
    return ((inp>thresh)==targ.bool()).float().mean()

learn = vision_learner(dls, 'convnext_small', metrics = partial(accuracy_multi,thresh=0.6))
learn.lr_find(suggest_funcs=(minimum, steep))

Мы будем обучать данные с помощью функции Fine_tune() от fastai. Эта функция имеет 4 эпохи, в которых обучался только верхний слой, а в следующие 5 эпох веса всех слоев корректировались. Используемая здесь потеря — это nn.BCEWithLogtisLoss, которая здесь не упоминается, поскольку она автоматически устанавливается fastai для классификации по нескольким меткам.

learn.fine_tune(5, base_lr=3e-3, freeze_epochs=4)

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

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

def show_predict(img):
    l = learn.predict(img)
    if l[0] == ['a','d','n','p']:
        res = 'Panda bear'
    elif l[0] == ['g','i','l','r','y','z']:
        res = 'Grizzly Bear'
    elif l[0] == ['d','e','t','y']:
        res = 'Teddy Bear'
    elif l[0] == ['a','l','o','p','r']:
        res = 'Polar Bear'
    elif l[0] == ['a','b','c','k','l']:
        res = 'Black Bear'
    else:
        res = "Others"
    return res

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