Получение точности SOTA на уровне 88% в наборе данных Food-101 с использованием модели ResNet50 с FastAI в Google Colab.

Давайте прямо в это.

Приведенные ниже команды оболочки устанавливают необходимые пакеты. Мне пришлось понизить версию PyTorch и Torchvision из-за проблем совместимости с Colab.

%reload_ext autoreload
%autoreload 2

!apt-get update
!apt install wget
!pip install pathlib
!pip install fastai
!pip install "torch==1.4" "torchvision==0.5.0"

Ниже приведены мои импорты. Я решил использовать библиотеку FastAI. У Джереми Ховарда была отличная лекция о создании классификаторов. Это простая библиотека, которая позволяет разработчикам использовать PyTorch, и она много думала о многоуровневой классификации.

Прямо под импортом у меня также есть класс Label Smoothing. Сглаживание меток было добавлено только в бета-версию FastAI 2.0 на момент написания этой записной книжки, поэтому мне пришлось включать его вручную.

import torch
import torch.nn as nn
from fastai import *
from fastai.vision import *
from fastai.callbacks.hooks import *
from pathlib import Path
import os, shutil

from torch.distributions.beta import Beta

def lin_comb(a, b, frac_a): return (frac_a * a) + (1 - frac_a) * b

def unsqueeze(input, dims):
    for dim in listify(dims): input = torch.unsqueeze(input, dim)
    return input

def reduce_loss(loss, reduction='mean'):
    return loss.mean() if reduction=='mean' else loss.sum() if reduction=='sum' else loss

class LabelSmoothingCrossEntropy(nn.Module):
    def __init__(self, ε:float=0.1, reduction='mean'):
        super().__init__()
        self.ε,self.reduction = ε,reduction

    def forward(self, output, target):
        c = output.size()[-1]
        log_preds = F.log_softmax(output, dim=-1)
        loss = reduce_loss(-log_preds.sum(dim=-1), self.reduction)
        nll = F.nll_loss(log_preds, target, reduction=self.reduction)
        return lin_comb(loss/c, nll, self.ε)

Сборка набора данных

Поскольку я использую Colab Pro, мне приходится импортировать свой набор данных каждый раз, когда я запускаю блокнот. Я смог найти набор данных Food-101 на AWS благодаря fastAI, потому что ETH Zurich больше не размещала набор данных на своем веб-сайте (https://vision.ee.ethz.ch/datasets_extra/food-101/). Приведенный ниже код загружает tar-файл, извлекает, переименовывает и собирает изображения на основе данных из файлов train.txt и test.txt. Я предпочитаю хранить все изображения поездов в food-101/images/train, а все тестовые изображения — в food-101/images/test. Я также перемещаю все свои метафайлы в food-101/meta.

# Removing the default sample_data directory that comes with every new Colab instance
!rm -rf sample_data

# Removes the existing food-101 dataset for a fresh, complete download
!rm -rf food-101 

# Downloads the food-101 dataset tar file
!wget https://s3.amazonaws.com/fast-ai-imageclas/food-101.tgz 

# Untars the tar file
!tar -xvf food-101.tgz 

# Removes the redundant h5 directory
!rm -rf food-101.tgz food-101/h5/

# Creates the meta, train and test directories inside food-101/images
!mkdir food-101/meta food-101/images/train food-101/images/test

# Moves all .txt and .json files to the meta directory
!mv food-101/*.txt food-101/*.json food-101/meta/ 

# Moves all test images to the food-101/images/test directory and renames them.
with open('food-101/meta/test.txt') as test_file:
  for line in test_file:
    name_of_folder = line.split('/')[0]
    name_of_file = line.split('/')[1].rstrip()
    Path('food-101/images/' + name_of_folder + '/' + name_of_file + '.jpg').rename('food-101/images/test/' + name_of_folder + '_' + name_of_file + '.jpg')

# Moves all training images to the food-101/images/train directory and renames them.
with open('food-101/meta/train.txt') as train_file:
  for line in train_file:
    name_of_folder = line.split('/')[0]
    name_of_file = line.split('/')[1].rstrip()
    Path('food-101/images/' + name_of_folder + '/' + name_of_file + '.jpg').rename('food-101/images/train/' + name_of_folder + '_' + name_of_file + '.jpg')

# Removes empty directories inside Food-101/images.
with open('food-101/meta/train.txt') as train_file:
  for folder in train_file:
    name_of_folder = folder.split('/')[0]
    if os.path.exists('food-101/images/' + name_of_folder):
      shutil.rmtree('food-101/images/' + name_of_folder)

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

%cd /content/food-101/images/train/
!ls -1 | wc -l

%cd ../test
!ls -1 | wc -l

%cd /content/
/content/food-101/images/train
75750
/content/food-101/images/test
25250
/content

Построение модели

Поскольку каждое изображение во всем наборе данных помечено, я решил использовать имена каждого изображения в качестве классов для классификатора с помощью регулярных выражений. Я тренирую модель с 20% тренировочного набора в качестве проверочного набора. Для увеличения данных я использую функцию .get_transforms() fastAI, потому что преобразования выполняются на лету, поэтому резкого увеличения размера набора данных не происходит, а о случайности заботится библиотека fastAI. Я уменьшаю каждое изображение до размера 224 для лучшего обучения. Размер пакета равен 8, потому что я не хочу превышать лимит памяти в CUDA (у меня были проблемы с этим, когда я использовал большие размеры пакетов). Я использую .normalize(imagenet_stats), чтобы легко нормализовать данные на основе статистики каналов RGB из набора данных ImageNet.

np.random.seed(42)
batch_size = 8

path = 'food-101/images/train'
file_parse = r'/([^/]+)_\d+\.(png|jpg|jpeg)$'

data = ImageList.from_folder(path).split_by_rand_pct(valid_pct=0.2).label_from_re(pat=file_parse).transform(get_transforms(), size=224).databunch(bs = batch_size).normalize(imagenet_stats)

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

top_1 = partial(top_k_accuracy, k=1)
learn = cnn_learner(data, models.resnet50, metrics=[accuracy, top_1], loss_func = LabelSmoothingCrossEntropy(), callback_fns=ShowGraph)

Обучение

Это этап обучения моей модели. Прежде чем запускать набор эпох, я начинаю с определения скорости обучения. Я добавил suggestion=True, чтобы приблизить интервал, который я буду выбирать. В приведенном ниже случае оптимальная скорость обучения составляла около 1e-06.

learn.lr_find()
learn.recorder.plot(suggestion=True)

Основываясь на предложении средства поиска скорости обучения, я получаю срез скорости обучения между 1e-06 и 1e-04. Я запускаю 5 эпох на этой скорости обучения и сохраняю веса как этап-1. Точность модели увеличивается в первом прогоне. К концу 5-й эпохи точность модели улучшилась с 40,7% до 58,5%.

learn.fit_one_cycle(5, max_lr=slice(1e-06, 1e-04))
learn.save('stage-1')

Теперь я нахожу новую скорость обучения, чтобы запустить другой набор эпох. Я делаю то же самое, что и выше. Одна вещь, которую я всегда делаю, это то, что в конце каждого цикла из 5 эпох я размораживаю модель. В этом случае моя скорость обучения составила примерно 1e-041.

learn.unfreeze()
learn.lr_find()
learn.recorder.plot(suggestion=True)

Я пробежал еще 5 эпох и сохранил их как stage-2. Точность модели росла так же быстро, как и на этапе 1, но на этот раз она пересекает линию проверки.

learn.fit_one_cycle(5, max_lr=slice(1e-05, 1e-04))
learn.save('stage-2')

Я хотел посмотреть, смогу ли я добиться большей точности. Для этого я воссоздал объект связки данных. Я оставил его таким же, как и в предыдущем объявлении, за исключением того, что здесь я увеличил размер изображения с 224 до 512. Я также хотел загрузить стадию 2, чтобы запустить средство поиска скорости обучения.

data = ImageList.from_folder(path).split_by_rand_pct(valid_pct=0.2).label_from_re(pat=file_parse).transform(get_transforms(), size=512).databunch(bs = batch_size).normalize(imagenet_stats)
learn = cnn_learner(data, models.resnet50, metrics=[accuracy, top_1], loss_func = LabelSmoothingCrossEntropy(), callback_fns=ShowGraph)
learn.load('stage-2')
learn.unfreeze()
learn.lr_find()
learn.recorder.plot(suggestion=True)

Я провел еще один набор из 5 эпох с новой скоростью обучения. Мой последний набор эпох дал мне оценку точности 77,3%, но после увеличения размера изображения я достиг точности 84,2%. Время обучения значительно увеличилось (с 11 минут на эпоху до 30 минут), потому что я увеличил размер изображения.

learn.fit_one_cycle(5, max_lr=slice(1e-05, 1e-04))
learn.save('stage-3')

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

learn.unfreeze()
learn.lr_find()
learn.recorder.plot(suggestion=True)

learn.fit_one_cycle(5, max_lr=slice(1e-06, 1e-04))
learn.save('stage-4')

Не похоже, что во время тренировок я смогу достичь точности выше 84,7%. Если бы я запускал дополнительные эпохи, график просто колебался бы.

Неверные предсказания

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

interp = ClassificationInterpretation.from_learner(learn)
interp.plot_top_losses(12, figsize=(15, 11))

interp.most_confused(min_val=5)
[('filet_mignon', 'steak', 26),
 ('chocolate_mousse', 'chocolate_cake', 24),
 ('steak', 'filet_mignon', 22),
 ('donuts', 'beignets', 19),
 ('prime_rib', 'steak', 19),
 ('beef_tartare', 'tuna_tartare', 15),
 ('dumplings', 'gyoza', 15),
 ('chocolate_mousse', 'tiramisu', 14),
 ('chocolate_cake', 'chocolate_mousse', 13),
 ('pork_chop', 'steak', 13),
 ('apple_pie', 'bread_pudding', 12),
 ('ice_cream', 'frozen_yogurt', 12),
 ('grilled_salmon', 'pork_chop', 11),
 ('sushi', 'sashimi', 11),
 ('baby_back_ribs', 'steak', 10),
 ('chocolate_mousse', 'panna_cotta', 10),
 ('ramen', 'pho', 10),
 ('tiramisu', 'chocolate_mousse', 10),
 ('tuna_tartare', 'beef_tartare', 10),
 ('bread_pudding', 'apple_pie', 9),
 ('breakfast_burrito', 'huevos_rancheros', 9),
 ('lobster_bisque', 'clam_chowder', 9),
 ('apple_pie', 'baklava', 8),
 ('chicken_quesadilla', 'breakfast_burrito', 8),
 ('hamburger', 'pulled_pork_sandwich', 8),
 ('tuna_tartare', 'ceviche', 8),
 ('baby_back_ribs', 'pork_chop', 7),
 ('chocolate_cake', 'tiramisu', 7),
 ('falafel', 'tacos', 7),
 ('filet_mignon', 'pork_chop', 7),
 ('huevos_rancheros', 'nachos', 7),
 ('huevos_rancheros', 'tacos', 7),
 ('ice_cream', 'chocolate_mousse', 7),
 ('pork_chop', 'filet_mignon', 7),
 ('pulled_pork_sandwich', 'hamburger', 7),
 ('steak', 'prime_rib', 7),
 ('beet_salad', 'foie_gras', 6),
 ('bruschetta', 'caprese_salad', 6),
 ('ceviche', 'tuna_tartare', 6),
 ('cheesecake', 'carrot_cake', 6),
 ('cheesecake', 'strawberry_shortcake', 6),
 ('chicken_quesadilla', 'tacos', 6),
 ('escargots', 'french_onion_soup', 6),
 ('falafel', 'crab_cakes', 6),
 ('foie_gras', 'pork_chop', 6),
 ('huevos_rancheros', 'croque_madame', 6),
 ('peking_duck', 'spring_rolls', 6),
 ('ravioli', 'gnocchi', 6),
 ('ravioli', 'lasagna', 6),
 ('ravioli', 'shrimp_and_grits', 6),
 ('shrimp_and_grits', 'risotto', 6),
 ('caprese_salad', 'greek_salad', 5),
 ('cheesecake', 'bread_pudding', 5),
 ('cheesecake', 'panna_cotta', 5),
 ('chicken_curry', 'gnocchi', 5),
 ('chicken_quesadilla', 'nachos', 5),
 ('club_sandwich', 'grilled_cheese_sandwich', 5),
 ('crab_cakes', 'falafel', 5),
 ('croque_madame', 'grilled_cheese_sandwich', 5),
 ('eggs_benedict', 'croque_madame', 5),
 ('french_fries', 'poutine', 5),
 ('garlic_bread', 'pizza', 5),
 ('greek_salad', 'caesar_salad', 5),
 ('grilled_cheese_sandwich', 'club_sandwich', 5),
 ('grilled_salmon', 'foie_gras', 5),
 ('guacamole', 'nachos', 5),
 ('gyoza', 'dumplings', 5),
 ('hummus', 'chicken_quesadilla', 5),
 ('nachos', 'tacos', 5),
 ('onion_rings', 'fried_calamari', 5),
 ('pulled_pork_sandwich', 'grilled_cheese_sandwich', 5),
 ('risotto', 'fried_rice', 5),
 ('risotto', 'shrimp_and_grits', 5),
 ('steak', 'pork_chop', 5),
 ('strawberry_shortcake', 'cheesecake', 5)]

Когда я смотрю на вывод .most_confused(), я вижу, что путаница модели была между едой, у которой практически нет физических различий. Например, самая большая путаница была между филе-миньоном и стейком. Филе-миньон и стейк на самом деле одно и то же. Стейк — это просто широкий выбор говяжьих отрубов, а филе-миньон — это просто вырезка из меньшей части вырезки.

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

Тестирование

Теперь пришло время протестировать обученную модель. Я создал новую переменную группы данных data_test. Это то же самое, что и выше, за исключением того, что в split_by_folder() я добавил тестовые изображения.

Я выбрал learn.validate() в качестве метода точности. Причина в том, что learn.validate() используется, если существует существующий набор проверки, а наборы проверки всегда помечены. Библиотека fastAI берет тестовые наборы вслепую, даже если тестовые изображения имеют метки, поэтому я не могу рассчитать точность только на этом основании. Для меня было логичнее создать новый объект группы данных, в который я включу свой тестовый набор как набор проверки (split_by_folder(train='train', valid='test')) и сравню с ним свою обученную модель.

path = '/content/food-101/images'

data_test = ImageList.from_folder(path).split_by_folder(train='train', valid='test').label_from_re(file_parse).transform(size=512).databunch().normalize(imagenet_stats)
learn = cnn_learner(data, models.resnet50, metrics=[accuracy, top_1],callback_fns=ShowGraph)
learn.load('stage-4')

Проверка дает мне SoTA 88,1% ≈ 88%

learn.validate(data_test.valid_dl)
[0.5012631, tensor(0.8809), tensor(0.8809)]

By:

Онур Андрос Озбек

Монреаль, Квебек