null

WhoAsked: Классифицируем изображения, используя сверточные нейронные сети

Я бы отнес проблему классификации изображений к подзадаче более широкого понятия - "компьютерное зрение". Что обозначает этот термин?

 

Когда компьютер получает на обработку изображение, так или иначе, он работает с N-мерным массивом чисел. Машина ничего не знает об изображенных на нем предметах или существах. Однако, в наше время, существует множество задач, которые требуют автоматической обработки изображения с учетом предметов, которые на нем изображены. Хорошим примером будут системы автономного управления автомобилем - на основании данных с лидаров и камер, система должна определить положение и характер объектов вокруг, чтобы принять решение о том, какие маневры необходимо совершить.

 

Однако, как можно решить данную проблему? Тут на помощь приходят алгоритмы машинного обучения. В данном конкретном примере мы будем использовать сверточные нейронные сети. Называются они так в честь операции "свертки", или convolution, которая лежит в основе обработки входных данных. Как правило, топология таких сетей представляет собой набор "слоев", которых могут быть десятки, через которые проходят входные данные. В силу математических особенностей операции свертки, на каждом слое размерность изображения уменьшается, пока мы не получим на выходе матрицу, равную по размерности количеству возможных классов объектов. Классы объектов определяются размеченными данными для тренировки, если при тренивке мы использовали изображения с котиками и щеночками, то возможными классами распознанных объектов будут котики и щеночки. Как правило, на выходе мы получаем список вероятностей, что на изображении существует данный класс объекта.

 

Для оценки точности работы алгоритмов обычно используют две метрики: top-1 и top-5. В первом случае, считаются только изображения, для которых правильный класс был самым вероятным прогнозом алгоритма, то есть классом с самой большой вероятностью. top-5 менее строгая величина, в этом случае считаем изображения, для которых правильный класс вошел в пять самых вероятных прогнозов.

 

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

 

Начнем с выбранной модели. В этом примере мы будем использовать Xception. Согласно Keras docs эта модель показывает лучшие результаты по обеим метрикам top-1 и top-5. Будем использовать Xception с весами, заранее натренерованными на датасете imagenet. Для валидации результатов будем использовать официальный `imagenet` ILSVRC датасет (да, все 167Gb).

 

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

 

Обсудим, как именно валидационный датасет размечен. Сами изображения живут в `ILSVRC/Data/CLS-LOC/val` (пути написаны относительно корня архива, ссылка на который приведена выше). Файлы разметки же живут в `ILSVRC/Annotations/CLS-LOC/val` в виде `.xml` файлов с такими же названиями, как изображения. Пример файла разметки:


 

<annotation>

  <folder>val</folder>

  <filename>ILSVRC2012_val_00026567</filename>

  <source>

    <database>ILSVRC_2012</database>

  </source>

  <size>

    <width>500</width>

    <height>375</height>

    <depth>3</depth>

  </size>

  <segmented>0</segmented>

  <object>

    <name>n02256656</name>

    <pose>Unspecified</pose>

    <truncated>0</truncated>

    <difficult>0</difficult>

    <bndbox>

      <xmin>163</xmin>

      <ymin>200</ymin>

      <xmax>246</xmax>

      <ymax>269</ymax>

    </bndbox>

  </object>

  <object>

    <name>n02256656</name>

    <pose>Unspecified</pose>

    <truncated>0</truncated>

    <difficult>0</difficult>

    <bndbox>

      <xmin>147</xmin>

      <ymin>43</ymin>

      <xmax>409</xmax>

      <ymax>329</ymax>

    </bndbox>

  </object>

</annotation

 

Классы объектов хранятся в `object->name` (`n02256656`). То, что мы тут видим - это `ILSVRC2012_ID`. Как нам найти маппинг к "человеческим" названиям классов, как котики или щеночки? На форумах всплывало название файла `map_clsloc.txt`. После гуглинга, мне удалось найти git gist и оказалось, что этот файл подошел для всех 1000 классов. Благодарность этому человеку.

 

Теперь рассмотрим непосредственно код классификатора:

import numpy as np

from keras.applications.xception import Xception, preprocess_input, decode_predictions

from keras.preprocessing import image

import random

import json

import cPickle

import xml.etree.ElementTree as ET

import os

from tabulate import tabulate


os.environ["KERAS_BACKEND"] = "tensorflow"


model = Xception(

    include_top=True,

    weights='imagenet',

    input_tensor=None,

    input_shape=None,

    pooling=None,

    classes=1000

)


ANNOTATIONS_FILE_PATH = '/path/to/your/ILSVRC/Annotations/CLS-LOC/val'

BATCH_FILE_PATH = '/path/to/your/ILSVRC/Data/CLS-LOC/val'


def get_predictions(img_name):

    img = image.load_img("{}/{}".format(BATCH_FILE_PATH, img_name), target_size=(299, 299))

    x = image.img_to_array(img, data_format=None)

    x = np.expand_dims(x, axis=0)

    x = preprocess_input(x)


    preds = model.predict(x)

    decoded_predictions = [pred for pred in decode_predictions(preds, top=5)[0]]

    return decoded_predictions


def parse_classes(map_clasloc_path):

    with open(map_clasloc_path, 'r') as f:

        lines = f.readlines()

   

    res = {}

    for line in lines:

        [ILSVRC2012_ID, _index, class_name] = line.strip().split(' ')

        res[ILSVRC2012_ID] = class_name

    return res


def get_class_for_image(image_name, classes_map):

    tree = ET.parse("{}/{}".format(ANNOTATIONS_FILE_PATH, image_name.replace('.JPEG', '.xml')))

    root = tree.getroot()

    res = {}

    for name in root.iter('name'):

        id = name.text

        res[id] = classes_map[id]

    return res


def process_image(filename, classes):

    target_classes = get_class_for_image(filename, classes)

    target_ids = target_classes.keys()

    predicted_classes = get_predictions(filename)

    predicted_ids = [pc[0] for pc in predicted_classes]

   

    print('Image {}. Actual classes:'.format(filename))

    for (id, name) in target_classes.items():

        print("\t{}: {}".format(id, name))

    print('Predicted classes:')

    print(tabulate(predicted_classes))

   

    top_1, top_5 = False, False

    top_1_id = predicted_ids[0]

    top_1 = top_1_id in target_ids

    for p_class in predicted_ids:

        if p_class in target_ids:

            top_5 = True

   

    print('Top 1: {}, Top 5: {}'.format(top_1, top_5))

    print("\n--------------------------------------------------\n")

    return (top_1, top_5)


BATCH_FILE_PATH = '/path/to/your/ILSVRC/Data/CLS-LOC/val'


classes = parse_classes('map_clsloc.txt')


count, top_1_res, top_5_res = (0, 0, 0)

for filename in os.listdir(BATCH_FILE_PATH):

    (top_1, top_5) = process_image(filename, classes)

    count += 1

    if top_1:

        top_1_res += 1

    if top_5:

        top_5_res += 1


print('Processed {} images. Top 1 predictions: {}, {}%. Top 5 predictions: {}, {}%' \

        .format(count, top_1_res, round(top_1_res / count, 2), top_5_res, round(top_5_res / count, 2)))

 

* `parse_classes` : простая функция. Она загружает `map_clsloc.txt` и создает кортеж с `ILSVRC2012_ID` как ключи, и человеческие названия классов как названия.

* `get_class_for_image` : эта функция принимает название изображения, находит нужный `.xml` с разметкой, парсит его и возвращает пары `ILSVRC2012_ID` и человеческие названия классов для всех объектов, представленных на изображении.

* `get_predictions` : тут происходит магия. Сначала загружается изображение, его размер приводится к 299x299 пикселов, так как это входной размер для `Xception`. Потом мы запускаем модель и получаем top-5 предсказаний с вероятностями. Возвращает список объектов `(ILSVRC2012_ID, class_name, probability)`.

* `process_image` : главная функция. Получает на вход название изображение, вызывает `get_class_for_image`, вызывает `get_predictions` и сравнивает `ILSVRC2012_ID` предсказанных и полученных классов, чтобы понять, попали ли мы в top-1 и top-5. Возвращает пару `(is_top1, is_top5)`.

* Главный скрипт проходит по 50000 изображениям, вызывает `process_image`. Потом мы считаем процентное соотношение top-1 и top-5.

 

Запуск занял примерно 1 час на Ryzen 9 5900X.

 

* `top 1` : 38762, ~77.52%

* `top 5` : 46876, ~93.75%

 

Результаты очень близки к документации Keras, так что считаем результаты валидными.