Многозначная классификация с помощью Keras
Можно ли обучить нейросеть делать не один, а сразу несколько прогнозов? Этот вопрос возникает, когда нам необходимо классифицировать изображения по двум, трём или большему числу меток. Например, чтобы определить сразу тип одежды (рубашка, платье, брюки и так далее), цвет и ткань.
В этом руководстве мы расскажем, как создать многозначную нейронную сеть с помощью Keras.
Многозначная или нечёткая классификация (multi-label classification) позволяет определить, с какой вероятностью объект принадлежит к каждому из классов.
Урок разбит на четыре части.
В первой части вы узнаете о наборе данных для классификации по нескольким меткам, а также о том, как создать собственный датасет.
Далее мы кратко обсудим SmallerVGGNet — архитектуру нашей глубокой свёрточной нейронной сети.
Затем мы обучим сеть с помощью подготовленного набора данных, и, наконец, оценим её на тестовых изображениях.
Cодержание
1. Набор данных
2. Структура проекта
3. Архитектура Keras-модели
4. Создание модели
5. Обучение модели
6. Применение модели к новым изображениям
7. Результаты
8. Загрузки
Набор данных
Набор данных состоит из 2167 изображений в шести категориях:
— Черные джинсы (344 изображений)
— Синее платье (386 изображений)
— Синие джинсы (356 изображений)
— Синяя рубашка (369 изображений)
— Красное платье (380 изображений)
— Красная рубашка (332 изображения)
Цель нейросети — классифицировать как цвет, так и тип одежды. Вы можете использовать этот датасет, а можете создать свой с помощью урока «Как (быстро) собрать набор данных для глубокого обучения».
Процесс загрузки и ручного удаления лишних изображений занимает примерно 30 минут.
Структура проекта
В разделе «Загрузки» вы можете скачать архив с кодом и набором данных. Структура проекта выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
├── classify.py ├── dataset │ ├── black_jeans [344 entries │ ├── blue_dress [386 entries] │ ├── blue_jeans [356 entries] │ ├── blue_shirt [369 entries] │ ├── red_dress [380 entries] │ └── red_shirt [332 entries] ├── examples │ ├── example_01.jpg │ ├── example_02.jpg │ ├── example_03.jpg │ ├── example_04.jpg │ ├── example_05.jpg │ ├── example_06.jpg │ └── example_07.jpg ├── fashion.model ├── mlb.pickle ├── plot.png ├── pyimagesearch │ ├── __init__.py │ └── smallervggnet.py ├── search_bing_api.py └── train.py |
В корне находятся 6 файлов и 3 папки.
Файлы, с которыми мы будем работать в этом уроке:
- search_bing_api.py: этот скрипт позволяет быстро создать набор данных для глубокого обучения. Вам не нужно запускать его, так как датасет уже находится в архиве. Можете использовать его в образовательных целях.
- train.py: скрипт для обучения классификатора.
- fashion.model: файл модели. Скрипт train.py сериализует модель Keras на диск. Мы будем использовать её позже в файле classify.py.
- mlb.pickle: бинаризатор меток MultiLabelBinarizer из scikit-learn, созданный с помощью train.py — этот файл содержит имена классов в виде сериализованных структур данных.
- plot.png: график, который создаёт обучающий скрипт. Если вы используете собственный набор данных, не забудьте проверить этот файл на точность, потери и переобучение.
- classify.py: скрипт для проверки классификатора. Всегда выполняйте проверку в исходном проекте, прежде чем использовать модель для других задач.
И три каталога:
- dataset: папка содержит набор изображений. У каждого класса свой соответствующий подкаталог. Когда датасет упорядочен таким образом, из него проще извлечь метки классов по заданному пути.
- pyimagesearch: это модуль с нашей нейросетью. В него входят файлы __init__.py и smallervggnet.py.
- examples: здесь находятся семь примеров изображений. На каждом из них мы протестируем результаты классификации с помощью скрипта classify.py.
Если список показался вам большим, не пугайтесь: мы рассмотрим файлы в том порядке, в котором они представлены.
Архитектура Keras-модели
Архитектура CNN (свёрточной нейронной сети) SmallerVGGNet, которую мы используем для этого урока — упрощённая версия своего прародителя VGGNet. Модель VGGNet впервые представили Карен Симонян и Эндрю Циссерман в своей статье “Very Deep Convolutional Networks for Large Scale Image Recognition” в 2015 году.
На этом уроке мы не будем вдаваться в подробности архитектуры нейросети. Если у вас возникнут какие-либо вопросы, задавайте их в комментариях — мы с радостью ответим на них или напишем отдельный пост с описанием модели.
Загрузив и распаковав zip-архив, откройте файл smallervggnet.py из каталога pyimagesearch:
1 2 3 4 5 6 7 8 9 10 |
# импортируем необходимые пакеты from keras.models import Sequential from keras.layers.normalization import BatchNormalization from keras.layers.convolutional import Conv2D from keras.layers.convolutional import MaxPooling2D from keras.layers.core import Activation from keras.layers.core import Flatten from keras.layers.core import Dropout from keras.layers.core import Dense from keras import backend as K |
В строках 2-10 мы импортируем необходимые модули. Создадим класс SmallerVGGNet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class SmallerVGGNet: @staticmethod def build(width, height, depth, classes, finalAct="softmax"): # инициализируем модель вместе с формой входных # данных "channels last" и размерностью каналов model = Sequential() inputShape = (height, width, depth) chanDim = -1 # если мы используем "channels first", обновляем форму # входных данных и размерность каналов if K.image_data_format() == "channels_first": inputShape = (depth, height, width) chanDim = 1 |
Наш класс определён в строке 12. Затем мы создаём функцию сборки (build) в строке 14.
Метод требует четырёх параметров: ширина (width), высота (height), глубина (depth) и классы (classes). Глубина определяет количество цветовых каналов в исходном изображении, а классы — число категорий или меток (не их названия). Мы будем использовать эти параметры в обучающем скрипте для модели с формой входных данных 96 x 96 x 3.
Необязательный аргумент finalAct (значение по умолчанию — “softmax”) будет использоваться в конце построения архитектуры. Изменение этого значения на “sigmoid” позволит нам выполнить многозначную классификацию.
Внутри функции мы инициализируем модель (строка 17) и по умолчанию используем архитектуру “channel_last” в строках 18 и 19. Строки 23-25 нужны для бэкэндов с поддержкой “channel_first”.
Создадим первый слой CONV => RELU => POOL:
1 2 3 4 5 6 7 |
# CONV => RELU => POOL model.add(Conv2D(32, (3, 3), padding="same", input_shape=inputShape)) model.add(Activation("relu")) model.add(BatchNormalization(axis=chanDim)) model.add(MaxPooling2D(pool_size=(3, 3))) model.add(Dropout(0.25)) |
Слой CONV имеет 32 фильтра с ядром 3x3 и активацией RELU (Rectified Linear Unit). Мы применяем пакетную нормализацию, max-pooling и 25% исключений.
Исключение (dropout) — это процесс случайного отключения узлов, соединяющих текущий и последующий слои. Он помогает снизить переобучение, поскольку ни один узел не будет ответственным за прогноз определённого класса.
Далее перейдём к двум наборам слоёв (CONV => RELU) * 2 => POOL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# (CONV => RELU) * 2 => POOL model.add(Conv2D(64, (3, 3), padding="same")) model.add(Activation("relu")) model.add(BatchNormalization(axis=chanDim)) model.add(Conv2D(64, (3, 3), padding="same")) model.add(Activation("relu")) model.add(BatchNormalization(axis=chanDim)) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) # (CONV => RELU) * 2 => POOL model.add(Conv2D(128, (3, 3), padding="same")) model.add(Activation("relu")) model.add(BatchNormalization(axis=chanDim)) model.add(Conv2D(128, (3, 3), padding="same")) model.add(Activation("relu")) model.add(BatchNormalization(axis=chanDim)) model.add(MaxPooling2D(pool_size=(2, 2))) |
Обратите внимание на изменения размеров фильтров, ядер и pool_size. Так мы постепенно уменьшаем размер сети, но увеличиваем глубину.
За этим блоком следует набор слоёв FC => RELU:
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# первый (и единственный) набор слоёв FC => RELU model.add(Flatten()) model.add(Dense(1024)) model.add(Activation("relu")) model.add(BatchNormalization()) model.add(Dropout(0.5)) # используйте *softmax* активацию для классификации # по одной метке и *sigmoid* по нескольким model.add(Dense(classes)) model.add(Activation(finalAct)) # возвращаем сетевую архитектуру return model |
Полностью связанные слои размещаются в конце сети (параметр Dense в строках 57 и 64).
В строке 65 мы определяем, использовать ли активацию “softmax” для классификации по одной метке, или активацию “sigmoid” для нескольких меток. Желаемый параметр надо указать в строке 14 этого сценария и в строке 95 файла train.py.
Создание модели
Теперь, когда мы инициализировали сеть SmallerVGGNet, давайте создадим скрипт для её обучения классификации по нескольким меткам.
Откройте файл train.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# импортируем бэкенд Agg из matplotlib для сохранения графиков на диск import matplotlib matplotlib.use("Agg") # подключаем необходимые пакеты from keras.preprocessing.image import ImageDataGenerator from keras.optimizers import Adam from keras.preprocessing.image import img_to_array from sklearn.preprocessing import MultiLabelBinarizer from sklearn.model_selection import train_test_split from pyimagesearch.smallervggnet import SmallerVGGNet import matplotlib.pyplot as plt from imutils import paths import numpy as np import argparse import random import pickle import cv2 import os |
В строках 2-19 импортируются необходимые пакеты и модули. Строка 3 подключает бэкэнд matplotlib для сохранения графиков на диск в фоновом режиме.
Мы исходим из того, что на этом этапе у вас уже установлены Keras, scikit-learn, matplotlib, imutils и OpenCV. Если же это ваш первый проект глубокого обучения, то вы можете получить все необходимые библиотеки двумя способами:
- используйте предварительно настроенную среду (требует 5 минут вашей работы, а обучение нейросети будет стоить дешевле чашки кофе);
- создайте собственное окружение (требует времени, терпения и настойчивости).
Предварительно настроенная среда позволяет быстро загрузить и собрать файлы, завершив работу в считанные минуты. Например, на наших серверах с мощными GPU для глубокого обучения NVIDIA Tesla V100 можно использовать готовый образ с Ubuntu 18.04, CUDA Toolkit и Deepo: в нём уже настроен Jupyter Notebok и установлены популярные библиотеки для машинного обучения.
Если вы хотите создать свою среду (и у вас есть время на отладку и устранение неполадок), то советуем ознакомиться со статьями:
— Настройка Ubuntu для глубокого обучения с Python (только для CPU)
— Настройка Ubuntu 16.04 + CUDA + GPU для глубокого обучения с Python (GPU и CPU)
— Настройка macOS для глубокого обучения с Python, TensorFlow и Keras
Итак, ваша среда настроена и все пакеты успешно импортированы. Давайте разберём аргументы командной строки:
1 2 3 4 5 6 7 8 9 10 11 12 |
# создаём парсер аргументов и передаём их ap = argparse.ArgumentParser() ap.add_argument("-d", "--dataset", required=True, help="path to input dataset (i.e., directory of images)") ap.add_argument("-m", "--model", required=True, help="path to output model") ap.add_argument("-l", "--labelbin", required=True, help="path to output label binarizer") ap.add_argument("-p", "--plot", type=str, default="plot.png", help="path to output accuracy/loss plot") args = vars(ap.parse_args()) |
Аргументы командной строки устанавливают параметры для скрипта подобно аргументам функций.
Мы работаем с четырьмя аргументами (строки 23-30):
--dataset: путь к набору изображений на диске.
--model: путь к сериализованной модели Keras.
--labelbin: путь к выходному бинаризатору с несколькими метками.
--plot: путь к выходному файлу графика обучения.
Перейдём к инициализации важных для процесса обучения переменных:
1 2 3 4 5 6 7 |
# инициализируем число эпох, скорость обучения, # размер пакета и размерность изображения EPOCHS = 75 INIT_LR = 1e-3 BS = 32 IMAGE_DIMS = (96, 96, 3) |
Переменные в строках 35-38 определяют, что:
— сеть будет обучаться в течение 75 эпох, обрабатывая тренировочные шаблоны и улучшая их распознавание методом обратного распространения ошибки;
— начальная скорость обучения равна 10-3 (значение по умолчанию для оптимизатора Adam);
— размер пакета составляет 32. Вы можете отрегулировать это значение в зависимости от возможностей графического процессора;
— как упоминалось выше, изображения имеют размер 96x96 и содержат 3 канала.
Загрузим и предварительно обработаем данные для обучения:
1 2 3 4 5 6 7 8 9 10 |
# берём пути к изображениям и рандомно перемешиваем print("[INFO] loading images...") imagePaths = sorted(list(paths.list_images(args["dataset"]))) random.seed(42) random.shuffle(imagePaths) # инициализируем данные и метки data = [] labels = [] |
Здесь мы берём пути к изображениям imagePaths и случайным образом перемешиваем их. Затем инициализируем данные и метки (строки 47 и 48).
Далее пройдёмся по всем изображением, обработаем их и извлечём метки классов:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# цикл по изображениям for imagePath in imagePaths: # загружаем изображение, обрабатываем и добавляем в список image = cv2.imread(imagePath) image = cv2.resize(image, (IMAGE_DIMS[1], IMAGE_DIMS[0])) image = img_to_array(image) data.append(image) # извлекаем метку класса из пути к изображению и обновляем # список меток l = label = imagePath.split(os.path.sep)[-2].split("_") labels.append(l) |
Сначала мы загружаем каждое изображение в память (строка 53). Затем выполняем его предварительную обработку (строки 54 и 55) и добавляем к списку данных (строка 56).
Строки 60 и 61 разделяют путь к изображению на несколько меток. Строка 60 создаёт список из двух элементов, который добавляется в массив меток в строке 61. Вот пример того, как это выглядит в терминале:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ python >>> import os >>> labels = [] >>> imagePath = "dataset/red_dress/long_dress_from_macys_red.png" >>> l = label = imagePath.split(os.path.sep)[-2].split("_") >>> l ['red', 'dress'] >>> labels.append(l) >>> >>> imagePath = "dataset/blue_jeans/stylish_blue_jeans_from_your_favorite_store.png" >>> l = label = imagePath.split(os.path.sep)[-2].split("_") >>> labels.append(l) >>> >>> imagePath = "dataset/red_shirt/red_shirt_from_target.png" >>> l = label = imagePath.split(os.path.sep)[-2].split("_") >>> labels.append(l) >>> >>> labels [['red', 'dress'], ['blue', 'jeans'], ['red', 'shirt']] |
Как видите, список меток — это «массив массивов», в котором каждая составляющая является двухэлементным списком.
Но мы ещё не закончили с предварительной обработкой:
1 2 3 4 5 |
# масштабируем интенсивности пикселей в диапазон [0, 1] data = np.array(data, dtype="float") / 255.0 labels = np.array(labels) print("[INFO] data matrix: {} images ({:.2f}MB)".format( len(imagePaths), data.nbytes / (1024 * 1000.0))) |
Всего двумя строками кода мы преобразовываем списки данных и меток в массивы NumPy и масштабируем интенсивности пикселей в диапазон [0, 1].
Давайте бинаризуем метки с помощью следующего блока кода:
1 2 3 4 5 6 7 8 9 10 |
# бинаризуем метки с помощью многозначного # бинаризатора scikit-learn print("[INFO] class labels:") mlb = MultiLabelBinarizer() labels = mlb.fit_transform(labels) # цикл по всем меткам for (i, label) in enumerate(mlb.classes_): print("{}. {}".format(i + 1, label)) |
Для этого мы используем класс MultiLabelBinarizer из scikit-learn. Вы не можете использовать стандартный LabelBinarizer для многозначной классификации. Строки 72 и 73 преобразуют метки в вектор, в котором закодирован представленный на изображении класс.
Вот пример, как кортеж ("red","dress") преобразуется в вектор с шестью категориями:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ python >>> from sklearn.preprocessing import MultiLabelBinarizer >>> labels = [ ... ("blue", "jeans"), ... ("blue", "dress"), ... ("red", "dress"), ... ("red", "shirt"), ... ("blue", "shirt"), ... ("black", "jeans") ... ] >>> mlb = MultiLabelBinarizer() >>> mlb.fit(labels) MultiLabelBinarizer(classes=None, sparse_output=False) >>> mlb.classes_ array(['black', 'blue', 'dress', 'jeans', 'red', 'shirt'], dtype=object) >>> mlb.transform([("red", "dress")]) array([[0, 0, 1, 0, 1, 0]]) |
Операция One-hot encoding преобразует категориальные метки в вектор целых чисел. Тот же принцип применяется в последних двух строках, только у нас будет two-hot encoding.
В последней строке две метки являются “hot” (представлены цифрой «1»), что указывает на их наличие. Если какая-либо из меток отсутствует, то в массиве она будет равна нулю.
Создадим обучающую и тестовую выборки, а также инициализируем генератор данных:
1 2 3 4 5 6 7 8 9 10 |
# разбиваем данные на обучающую и тестовую выборки, используя 80% # данных для обучения и оставшиеся 20% для тестирования (trainX, testX, trainY, testY) = train_test_split(data, labels, test_size=0.2, random_state=42) # создаём генератор для добавления изображений aug = ImageDataGenerator(rotation_range=25, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode="nearest") |
В строках 81 и 82 мы выделяем 80% данных для обучения и 20% для тестирования.
Генератор для добавления изображений инициализируется в строках 85-87. Дополнение данных следует выполнять, если в каждом классе содержится не менее 1000 снимков.
Соберём модель и инициализируем оптимизатор Adam:
1 2 3 4 5 6 7 8 9 10 11 |
# инициализируем модель с активацией sigmoid # для многозначной классификации print("[INFO] compiling model...") model = SmallerVGGNet.build( width=IMAGE_DIMS[1], height=IMAGE_DIMS[0], depth=IMAGE_DIMS[2], classes=len(mlb.classes_), finalAct="sigmoid") # инициализируем оптимизатор opt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS) |
В строках 92-95 мы собираем модель с параметром finalAct="sigmoid" для многозначной классификации.
Скомпилируем модель и начнём обучение (это может занять некоторое время в зависимости от характеристик вашего процессора):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# компилируем модель, используя двоичную кросс-энтропию # вместо категориальной. Это может показаться нелогичным # для многозначной классификации, но имейте в виду, что цель -- # обрабатывать каждую выходную метку как независимое # распределение Бернулли model.compile(loss="binary_crossentropy", optimizer=opt, metrics=["accuracy"]) # обучаем нейросеть print("[INFO] training network...") H = model.fit_generator( aug.flow(trainX, trainY, batch_size=BS), validation_data=(testX, testY), steps_per_epoch=len(trainX) // BS, epochs=EPOCHS, verbose=1) |
В строках 105 и 106 мы компилируем модель, используя бинарную кросс-энтропию ("binary_crossentropy"), а не категориальную ("categorical_crossentropy"). Это может показаться нелогичным для многозначной классификации, но цель — обработать каждую выходную метку как независимое распределение Бернулли. Так мы сможем штрафовать каждый выходной узел по отдельности.
В строках 110-114 запускается процесс обучения с ранее созданным генератором.
После этого сохраним нашу модель и бинаризатор меток на диске:
1 2 3 4 5 6 7 8 9 10 |
# сохраняем модель на диск print("[INFO] serializing network...") model.save(args["model"]) # сохраняем бинаризатор меток на диск print("[INFO] serializing label binarizer...") f = open(args["labelbin"], "wb") f.write(pickle.dumps(mlb)) f.close() |
Построим график потерь и точности:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# строим график потерь и точности plt.style.use("ggplot") plt.figure() N = EPOCHS plt.plot(np.arange(0, N), H.history["loss"], label="train_loss") plt.plot(np.arange(0, N), H.history["val_loss"], label="val_loss") plt.plot(np.arange(0, N), H.history["acc"], label="train_acc") plt.plot(np.arange(0, N), H.history["val_acc"], label="val_acc") plt.title("Training Loss and Accuracy") plt.xlabel("Epoch #") plt.ylabel("Loss/Accuracy") plt.legend(loc="upper left") plt.savefig(args["plot"]) |
Процесс построения происходит в строках 127-137, а строка 138 выполняет сохранение файла на диск в виде изображения.
Обучение модели
Откройте терминал, перейдите в каталог проекта и выполните команду:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
$ python train.py --dataset dataset --model fashion.model \ --labelbin mlb.pickle Using TensorFlow backend. [INFO] loading images... [INFO] data matrix: 2165 images (467.64MB) [INFO] class labels: 1. black 2. blue 3. dress 4. jeans 5. red 6. shirt [INFO] compiling model... [INFO] training network... Epoch 1/75 name: GeForce GTX TITAN X 54/54 [==============================] - 4s - loss: 0.3503 - acc: 0.8682 - val_loss: 0.9417 - val_acc: 0.6520 Epoch 2/75 54/54 [==============================] - 2s - loss: 0.1833 - acc: 0.9324 - val_loss: 0.7770 - val_acc: 0.5377 Epoch 3/75 54/54 [==============================] - 2s - loss: 0.1736 - acc: 0.9378 - val_loss: 1.1532 - val_acc: 0.6436 ... Epoch 73/75 54/54 [==============================] - 2s - loss: 0.0534 - acc: 0.9813 - val_loss: 0.0324 - val_acc: 0.9888 Epoch 74/75 54/54 [==============================] - 2s - loss: 0.0518 - acc: 0.9833 - val_loss: 0.0645 - val_acc: 0.9784 Epoch 75/75 54/54 [==============================] - 2s - loss: 0.0405 - acc: 0.9857 - val_loss: 0.0429 - val_acc: 0.9842 [INFO] serializing network... [INFO] serializing label binarizer... |
Как видите, мы обучили сеть и достигли:
— 98.57% точности классификации на обучающей выборке
— 98.42% точности классификации на тестовой выборке.
Применение модели к новым изображениям
Проверим нашу обученную модель на снимках, не входящих в обучающий или тестовый наборы.
Откройте файл classify.py в каталоге проекта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# импортируем необходимые пакеты from keras.preprocessing.image import img_to_array from keras.models import load_model import numpy as np import argparse import imutils import pickle import cv2 import os # создаём парсер аргументов и передаём их ap = argparse.ArgumentParser() ap.add_argument("-m", "--model", required=True, help="path to trained model model") ap.add_argument("-l", "--labelbin", required=True, help="path to label binarizer") ap.add_argument("-i", "--image", required=True, help="path to input image") args = vars(ap.parse_args()) |
В строках 2-9 мы импортируем пакеты Keras и OpenCV.
Затем обрабатываем аргументы командной строки (12-19).
Загрузим и предварительно обработаем входное изображение:
1 2 3 4 5 6 7 8 9 10 |
# загружаем изображение image = cv2.imread(args["image"]) output = imutils.resize(image, width=400) # обрабатываем изображение для классификации image = cv2.resize(image, (96, 96)) image = image.astype("float") / 255.0 image = img_to_array(image) image = np.expand_dims(image, axis=0) |
Обработка происходит таким же образом, как и с обучающими данными.
Загрузим модель с бинаризатором меток и классифицируем изображение:
1 2 3 4 5 6 7 8 9 10 11 12 |
# загружаем обученную нейросеть и бинаризатор # с несколькими метками print("[INFO] loading network...") model = load_model(args["model"]) mlb = pickle.loads(open(args["labelbin"], "rb").read()) # классифицируем входное изображение и находим # индексы наиболее вероятных классов print("[INFO] classifying image...") proba = model.predict(image)[0] idxs = np.argsort(proba)[::-1][:2] |
В строках 34 и 35 происходит загрузка модели и бинаризованных меток. Затем мы классифицируем обработанное изображение (строка 40) и извлекаем индексы двух меток классов (строка 41) следующим образом:
— сортируем индексы массива по их вероятности в порядке убывания;
— получаем два индекса класса.
При желании вы можете изменить этот код для работы с большим числом меток классов. Мы советуем также использовать пороговое значение вероятности и возвращать метки с достоверностью > N%.
Выведем метки класса и из значения на выходное изображение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# цикл по индексам меток классов for (i, j) in enumerate(idxs): # рисуем метку на изображении label = "{}: {:.2f}%".format(mlb.classes_[j], proba[j] * 100) cv2.putText(output, label, (10, (i * 30) + 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) # показываем вероятность для каждой метки for (label, p) in zip(mlb.classes_, proba): print("{}: {:.2f}%".format(label, p * 100)) # показываем выходное изображение cv2.imshow("Output", output) cv2.waitKey(0) |
Цикл в строках 44-48 рисует две метки и соответствующие им значения вероятности на выходном изображении. Также мы отображаем прогнозы в терминале в строках 51 и 52. И, наконец, показываем изображение на экране (строки 55 и 56).
Результаты
Попробуем применить скрипт classify.py для классификации новых изображений. Если вы захотите использовать свои фотографии, то вам не нужно менять код выше — достаточно просто указать путь к ним с помощью аргументов командной строки в терминале.
Запустите команду:
1 2 3 4 5 6 7 8 9 10 11 12 |
$ python classify.py --model fashion.model --labelbin mlb.pickle \ --image examples/example_01.jpg Using TensorFlow backend. [INFO] loading network... [INFO] classifying image... black: 0.00% blue: 3.58% dress: 95.14% jeans: 0.00% red: 100.00% shirt: 64.02% |
Успех! Оба класса обнаружены с высокой вероятностью.
Теперь попробуем с синим платьем:
1 2 3 4 5 6 7 8 9 10 11 12 |
$ python classify.py --model fashion.model --labelbin mlb.pickle \ --image examples/example_02.jpg Using TensorFlow backend. [INFO] loading network... [INFO] classifying image... black: 0.03% blue: 99.98% dress: 98.50% jeans: 0.23% red: 0.00% shirt: 0.74% |
Тоже всё хорошо. Вы можете самостоятельно протестировать остальные примеры из папки examples и поделиться результатами.
Разберём пример с чёрным платьем (example_07.jpg). Раз наша нейросеть может определять чёрные и синие джинсы вместе с синими и красными платьями, сможет ли она классифицировать чёрное платье?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ python classify.py --model fashion.model --labelbin mlb.pickle \ --image examples/example_07.jpg Using TensorFlow backend. [INFO] loading network... [INFO] classifying image... black: 91.28% blue: 7.70% dress: 5.48% jeans: 71.87% red: 0.00% shirt: 5.92% |
О нет, ошибка! Классификатор сообщает, что модель носит чёрные джинсы.
Что же произошло? Почему прогноз неверен?
Ответ прост: обычная нейронная сеть не может прогнозировать классы, которые она никогда не изучала, а многозначная сеть не умеет определять неизвестные ей комбинации классов. Поэтому вам необходимо обучить свою архитектуру на каждой комбинации меток, которую вы собираетесь использовать.
Загрузки
Zip-архив с исходным кодом и набором данных можно скачать по ссылке (размер архива 562 МБ).
⌘⌘⌘
Надеемся, что вам понравился этот пост. Делитесь своими результатами в комментариях, предлагайте темы для новых статей — мы обязательно опубликуем самые интересные. Если что-то осталось для вас непонятным — задавайте вопросы, и мы поможем разобраться.
С оригинальной статьёй можно ознакомиться на портале pyimagesearch.com.