Глибоке занурення у виявлення об'єктів з Tensorflow

59 хв. читання

Новий набір даних Open Images надає все необхідне для тренування моделей комп'ютерного зору та просто ідеально підходить для створення демонстрації. Object Detection API у Tensorflow та його здатність обробляти великі обсяги даних роблять її чудовим вибором, тому почнемо!

Open Images — набір даних, створений компанією Google, що має значну кількість вільно ліцензованих анотованих зображень. Спочатку він містив тільки анотації класифікацій, або, простіше кажучи, мав мітки, що описували що, але не де. Після оновлення основної версії до 2.0 було додано більше анотацій — особливе значення мало додавання анотацій розпізнавання об'єктів. Ці нові анотації не тільки описували, що знаходилось на зображенні, але і де воно знаходилось, визначаючи координати обмежувальної рамки (bounding box, bbox) для певних об'єктів на зображенні.

Глибоке занурення у виявлення об'єктів з Tensorflow

Набір даних складається з 545 тренувальних міток. Вони містять все, від бубликів до слонів, — це значний крок у порівнянні з подібними наборами даних, такими як Common Objects in Context, який містить лише 90 міток. Крім того, мітки в Open Images мають ієрархічну структуру. Це означає, що навіть для окремих підрозділів усього набору даних можливо створити спеціальні класифікатори. Ого!

У цьому посібнику будуть детально описані кроки того, як створити свій власний детектор об'єктів, навчений з допомогою набору даних Open Images.

Перш ніж продовжити, ознайомтеся зі застереженнями відносно цього демо.

Застереження

  • Цей посібник передбачає, що ви добре знайомі з git, python, bash та основними операціями linux. Наш приклад приведено для систем debian/linux, однак, з деякими налаштуваннями він повинен працювати й для більшості інших середовищ.

  • Повний набір даних складає приблизно 6.2 ТБ, завантажений та нестиснутий. Можна використовувати спеціальний завантажувач зображень, який змінює розмір зображень при завантаженні.

  • Фреймворк Tensorflow потребує дуже багато пам'яті, в іншому випадку можливі постійні збої. Рекомендується мати принаймні 32 ГБ оперативної пам'яті, хоча замість цього ви можете використовувати місце для зберігання тимчасових файлів на твердому диску (scratch space).

  • Набір даних Open Images є великим та всеосяжним, але багато його класів є незбалансованими, що впливає на точність. Існують більш вдосконалені набори даних, такі як SMOTE, але, оскільки курс є базовим, ми не будемо їх розглядати.

Усі скрипти та файли, описані в цьому посібнику, можна знайти у репозиторії open images на github.

Все ще з нами? Добре, тоді почнімо.

Виявлення об'єктів за допомогою Tensorflow

Проект Tensorflow має ряд досить корисних розширень, один з них ー Object Detection API.

Розширення дозволяє користувачам Tensorflow створювати ефективні моделі виявлення об'єктів, використовуючи інфраструктуру орієнтованого графа (directed compute graph) Tensorflow. Розширення неймовірно потужне, але трохи складне у використанні. У цій статті ми покроково все детально розглянемо.

Крок 1: Форматування ваших даних

Набір даних Open Images поділяється на декілька компонентів:

  • файл з індексом зображення
  • файл анотацій обмежувальної рамки
  • опис класів
  • навчальні класи файлів
#!/usr/bin/env bash
# завантажує та витягує анотації з обмеженими рамками openimages та файли шляху зображень
mkdir data
wget http://storage.googleapis.com/openimages/2017_07/images_2017_07.tar.gz
tar -xf images_2017_07.tar.gz
mv 2017_07 data/images
rm images_2017_07.tar.gz

wget http://storage.googleapis.com/openimages/2017_07/annotations_human_bbox_2017_07.tar.gz
tar -xf annotations_human_bbox_2017_07.tar.gz
mv 2017_07 data/bbox_annotations
rm annotations_human_bbox_2017_07.tar.gz

wget http://storage.googleapis.com/openimages/2017_07/classes_2017_07.tar.gz
tar -xf classes_2017_07.tar.gz
mv 2017_07 data/classes
rm classes_2017_07.tar.gz

В Open Images, всі дані мають CSV формат. Він чудовий завдяки своїй компактності та простоті в обробці. Однак, як формат, він не дуже зручний для читання, існують більш прості альтернативи, з якими легше програмно працювати. З цих причин, ми конвертуватимемо наші анотації й файли зображень у JSON, щоб нам було легше з ними працювати.

Варто також зазначити, що файл анотацій містить 600 різних міток і тільки 545 з них добре навчені. Нам знадобиться перехресне посилання із файлом trainable-classes.txt, щоб відфільтрувати тільки навчені мітки.

Файл індексу зображення містить посилання на саме зображення та ID кожного зображення у всьому наборі даних. Навіть тих зображень, що не містять bbox анотацій!

Джерело

Переклад класових визначень

Файл trainable_classes.txt містить закодовані мітки, які повністю підходять для навчання, але можуть бути головним болем під час обчислення. Використаємо файл class_descriptions.csv, щоб створити перекладений файл класів, яких навчають.

def translate_class_descriptions(trainable_classes_file, descriptions_file):
    with open(trainable_classes_file, 'rb') as file:
        trainable_classes = file.read().replace(' ', '').split('\
')
    description_table = {}
    with open(descriptions_file) as f:
        for row in csv.reader(f):
            if len(row):
                description_table[row[0]] = row[1].replace("\\"", "").replace("'", "").replace('`', '')
    output = []
    for elm in trainable_classes:
        if elm != '':
            output.append(description_table[elm])
    return output


def save_classes(formatted_data, translated_path):
    with open(translated_path, 'w+') as f:
        json.dump(formatted_data, f)

Процедура виконання запитів функції та аналіз аргументів:

parser = argparse.ArgumentParser()
parser.add_argument('--trainable_classes_path', dest='trainable_classes', required=True)
parser.add_argument('--class_description_path', dest='class_description', required=True)
parser.add_argument('--trainable_translated_path', dest='trainable_translated_path', required=True)


if __name__ == '__main__':
    args = parser.parse_args()
    trainable_classes_path = args.trainable_classes
    description_path = args.class_description
    translated_path = args.trainable_translated_path
    translated = translate_class_descriptions(trainable_classes_path, description_path)
    save_classes(translated, translated_path)

Як ви можете бачити, ми виконуємо просту заміну рядка (з фільтром) для кожного елементу, в точно такому ж форматі, що й оригінальний файл trainable_classes.txt. Це значно допоможе нам, коли настане час для обчислення та виводу, так що добре, що ми спершу прибрали його зі шляху.

Джерело

Форматування метаданих

Форматуймо спочатку наш файл анотацій. Ми можемо зробити це, переклавши рядки CSV в елементи JSON, і створивши постійний список ідентифікаторів зображень.

Ми запускаємо простий скрипт дедуплікації по нашому списку id й зберігаємо його, щоб можна було відфільтрувати непотрібні нам зображення, зберігаючи пропускну здатність та дисковий простір.

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

# Дозволяє витягувати не тільки кожну анотацію, але і список ідентифікаторів зображень.
# Цей ідентифікатор буде використовуватися для фільтрації зображень, які не мають дійсних анотацій.
def format_annotations(annotation_path, trainable_classes_path):
    annotations = []
    ids = []
    with open(trainable_classes_path, 'rb') as file:
        trainable_classes = file.read().split('\
')
        
    with open(annotation_path, 'rb') as annofile:
        for row in csv.reader(annofile):
            annotation = {'id': row[0], 'label': row[2], 'confidence': row[3], 'x0': row[4],
                          'x1': row[5], 'y0': row[6], 'y1': row[7]}
            if anno['label'] in trainable_classes:
                annotations.append(annotation)
                ids.append(row[0])
    ids = dedupe(ids)
    return annotations, ids
def dedupe(seq):
    seen = set()
    seen_add = seen.add
    return [x for x in seq if not (x in seen or seen_add(x))]

Ми слідуємо прикладу нашого файлу індексу зображень, знову перекладаючи рядки CSV у JSON елементи. Слід зазначити, що файл індексів зображень містить величезну кількість зображень, пов'язаних з метаданими, однак, в наших обставинах, нас хвилюють лише id та URL зображення.

def format_image_index(images_path):
    images = []
    with open(images_path, 'rb') as f:
        reader = csv.reader(f)
        dataset = list(reader)
        for row in dataset:
            image = {'id': row[0], 'url': row[2]}
            images.append(image)
    return images

Фільтрація виконується шляхом створення вихідного масиву, який складається лише зі зображень, що містять id, які мають анотації обмежувальної рамки, а всі інші елементи видаляються.

# Давайте перевіряти кожне зображення і зберігати його лише в тому випадку, якщо його ідентифікатор містить анотацію обмежувальної рамки, пов'язаної з нею.
def filter_image_index(dataset, ids):
    output_list = []
    for element in dataset:
        if element['id'] in ids:
            output_list.append(element)
    return output_list

Потім ми створюємо більш простий у використанні примітив, роблячи рефакторинг наших анотацій і групуючи їх на основі id зображень. Ми назвемо ці згруповані елементи «точками» для ясності.

# Збирає анотації для кожного ідентифікатора зображення, з якими простіше працювати.
def points_maker(annotations):
    by_id = {}
    for anno in tqdm(annotations, desc="grouping annotations"):
        if anno['id'] in by_id:
            by_id[anno['id']].append(anno)
        else:
            by_id[anno['id']] = []
            by_id[anno['id']].append(anno)
    groups = []
    while len(by_id) >= 1:
        key, value = by_id.popitem()
        groups.append({'id': key, 'annotations': value})
    return groups

Нарешті, функція зберігання й наша процедура:

def save_data(data, out_path):
    with open(out_path, 'w+') as f:
        json.dump(data, f)

parser = argparse.ArgumentParser()
parser.add_argument('--annotations_input_path', dest='anno_path', required=True)
parser.add_argument('--image_index_input_path', dest='index_in_path', required=True)
parser.add_argument('--point_output_path', dest='point_path', required=True)
parser.add_argument('--image_index_output_path', dest='index_out_path', required=True)
parser.add_argument('--trainable_classes_path', dest='trainable_path', required=True)

if __name__ == "__main__":
    args = parser.parse_args()
    anno_input_path = args.anno_path
    image_index_input_path = args.index_in_path
    point_output_path = args.point_path
    image_index_output_path = args.index_out_path
    trainable_classes_path = args.trainable_path
    annotations, valid_image_ids = format_annotations(anno_input_path, trainable_classes_path)
    images = format_images(image_index_input_path)
    points = points_maker(annotations)
    filtered_images = filter_images(images, valid_image_ids)
    save_data(images, image_index_output_path)
    save_data(points, point_output_path)

Тепер у нас є анотації, відформатовані у мітки. Наші індекси зображень відфільтровані, щоб містити тільки використані id, і все це в JSON!

Все ще слідуєте інструкціям? Чудово, тоді час перейти до обробки URL-адрес зображень.

Джерело

Завантаження зображень

Як багато з вас, можливо, зрозуміли, завантаження ~660 тисяч веб-масштабованих зображень ー монструозна задача. На щастя, завантаження зображень — частково асинхронна задача, для вирішення якої можна скористатись перевагою багатонитевості нашого застосунку.

Спершу, погляньмо на нашу функцію паралельної обробки, оскільки, це не зовсім стандартний спосіб multiprocessing.pool.starmap. Мені подобається використовувати цю конкретну версію, оскільки візуалізація продуктивності коду ー те, що важливо для довгочасної підтримки скриптів. По суті, що важливо відзначити, так це те, що параметр array позначає ітерабельність, яку ви плануєте використати для паралельного зіставлення, а function позначає функцію, яку ви плануєте розпаралелити.

# Це хороший інструмент паралельної обробки, який використовує tqdm,
# щоб допомогти візуалізувати час до завершення.
def parallel_process(array, function, n_jobs=16, use_kwargs=False, front_num=3):
    """
        A parallel version of the map function with a progress bar.

        Args:
            array (array-like): An array to iterate over.
            function (function): A python function to apply to the elements of array
            n_jobs (int, default=16): The number of cores to use
            use_kwargs (boolean, default=False): Whether to consider the elements of array as dictionaries of
                keyword arguments to function
            front_num (int, default=3): The number of iterations to run serially before kicking off the parallel job.
                Useful for catching bugs
        Returns:
            [function(array[0]), function(array[1]), ...]
    """
    #Ми запускаємо перші кілька ітерацій почергово, щоб відловити помилки
    if front_num > 0:
        front = [function(**a) if use_kwargs else function(a) for a in array[:front_num]]
    #Якщо ми встановимо n_jobs в 1, то просто запустимо спискове включення. Це корисно для тестування та налагодження.
    if n_jobs==1:
        return front + [function(**a) if use_kwargs else function(a) for a in tqdm(array[front_num:])]
    #Збираємо воркери
    with ProcessPoolExecutor(max_workers=n_jobs) as pool:
        #Передає елементи масиву в функцію
        if use_kwargs:
            futures = [pool.submit(function, **a) for a in array[front_num:]]
        else:
            futures = [pool.submit(function, a) for a in array[front_num:]]
        kwargs = {
            'total': len(futures),
            'unit': 'it',
            'unit_scale': True,
            'leave': True
        }
        #Виведить прогрес, по мірі завершення завдання
        for f in tqdm(as_completed(futures), **kwargs):
            pass
    out = []
    #Отримайте результати від ф'ючерсів.
    for i, future in tqdm(enumerate(futures)):
        try:
            out.append(future.result())
        except Exception as e:
            out.append(e)
    return front + out

Поглянувши на функцію завантаження, ми можемо побачити, що вона використовує глобальний save_directory_path, визначений пізніше в нашій функції. Це вказує на теку, у якій ми плануємо зберігати наші файли. На жаль, у python більшість інструментів паралельного зіставлення не підтримують «постійні» вхідні параметри, і, в цьому випадку, більше сенсу має надати цю змінну як глобальний сценарій.

Наша функція завантажувача, в основному, використовує бібліотеку requests, і намагається завантажити кожне зображення з його URL-адреси. У цьому прикладі, якщо з будь-якою причини формується виключення, ми пропускаємо це зображення. Очевидно, існують ситуації, для яких цей підхід є нестандартним, тому використовуйте його на свій страх та ризик.

Вдало завантажене зображення зберігається як двійковий потік у файл з його ім'ям, визначеним id зображення. Це спрощує та робить більш ефективними пошук та завантаження зображень.

def download(element):
    image_content = None
    dir_path = save_directory_path
    browser_headers = [
        {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704 Safari/537.36"},
        {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743 Safari/537.36"},
        {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:44.0) Gecko/20100101 Firefox/44.0"}
    ]
    try:
        response = requests.get(element['url'],
                                headers=random.choice(browser_headers),
                                verify=False)
        image_content = response.content
    except Exception:
        pass
    if image_content:
        complete_file_path = os.path.join(dir_path, element['id']+'.'+element['url'].split('.')[-1])
        with open(complete_file_path, "wb") as f:
            f.write(image_content)
            f.close()

Нарешті, наша процедура:

parser = argparse.ArgumentParser()
parser.add_argument('--images_path', dest='images_path', required=True)
parser.add_argument('--images_output_directory', dest='images_output_directory', required=True)

if __name__ == "__main__":
    args = parser.parse_args()
    images_path = args.images_path
    save_directory_path = args.images_output_directory
    try:
        os.makedirs(save_directory_path)
    except OSError:
        pass # already exists
    with open(images_path, 'rb') as f:
        image_urls = json.load(f)
    parallel_process(image_urls, download)

Ох, це займе деякий час! Перед завантаженням переконайтесь, чи нема у вас обмежень пропускної здатності. ~660 тисяч зображень — багато зображень, і хочеться порекомендувати вам двічі перевірити, що у вас є достатньо місця для їх зберігання.

Глибоке занурення у виявлення об'єктів з Tensorflow

Джерело

Перевірка зображень та зменшення розмірів

Тепер у нас є тонна зображень, але всі вони різних розмірів, і деякі з них можуть бути пошкоджені! Продовжимо й перевіримо їх, але замість перевірки й зміни розмірів двома командами, зробімо ефективніше й поєднаємо ці операції.

# Коли ми переходимо до списку анотацій, перевірятимемо кожен ідентифікатор зображення, щоб переконатися, що він дійсний.
def process_images(saved_images_path, resized_images_path, points):
    cleaned_points = []
    for point in tqdm(points, desc="checking if images are valid from label index"):
            try:
                stored_path = os.path.join(saved_images_path, point['id'] + '.jpg')
                im = Image.open(stored_path)
                im.verify()
                # Тепер, коли зображення перевірено,
                # перемасштабуємо його і перезапишемо.
                im.thumbnail((256, 256))
                if resized_images_path:
                    resized_path = os.path.join(resized_images_path, point['id'] + '.jpg')
                    im.save(resized_path, 'JPG')
                else:
                    os.remove(stored_path)
                    im.save(stored_path, 'JPG')
                cleaned_points.append(point)
            except:
                pass
    return cleaned_points

Ми перевіряємо на дійсність зображення для кожного елемента мітки. Спочатку ми перевіряємо його та упевняємося, що нічого не пошкоджено. Якщо це так, ми продовжуємо та, при необхідності, повторно масштабуємо, якщо ж вихідна тека не визначена ー перезаписуємо.

Якщо під час обробки зображення щось йде не так, то зображення не було правильно відформатоване, тому ми відфільтровуємо його з нашого списку міток.

Примітка: розміри мініатюр встановлюються так для зменшення витрат на навчання, а не задля того, щоб відповідати конкретному «стандарту». Ми встановлюємо щось невелике, щоб зменшити накладні витрати при створенні TFRecords. Деякі мережі виявлення об'єктів спроектовані для роботи з рядом розмірів зображень та пропорцій, але зміна розмірів тут не є суворо необхідним для навчання. Однак, це допомагає.

І нарешті, наші компонент завантаження/збереження та процедура для скрипту:

def load_dataset(file_path):
    with open(file_path, 'rb') as f:
        annotations = json.load(f)
    return annotations


def save_dataset(data, file_path):
    with open(file_path, 'w+') as f:
        json.dump(data, f)
        
parser = argparse.ArgumentParser()
parser.add_argument('--image_directory', dest='image_directory_path', required=True)
parser.add_argument('--image_saving_directory', dest='resized_directory_path')
parser.add_argument('--datapoints_input_path', dest='datapoints_input_path', required=True)
parser.add_argument('--datapoints_output_path', dest='datapoints_output_path', required=True)

if __name__ == "__main__":
    args = parser.parse_args()
    images_directory = args.image_directory_path
    resized_directory = args.resized_directory_path
    points_input_path = args.datapoints_input_path
    points_save_path = args.datapoints_save_path
    points = load_dataset(points_input_path)
    filtered_points = process_images(images_directory, resized_directory, points)
    save_dataset(filtered_points, points_save_path)

Запустіть цей процес для наборів навчання, тестування та перевірки. Якщо ви хочете зберегти вихідні файли, вкажіть змінну шляху resized_directory, яка визначить, де ми зберігаємо змінені/перевірені зображення.

Джерело

Визначення карти міток

Для обчислення, Tensorflow потребує протобуферний файл label_map. Цей об'єкт, по суті, просто зіставляє індекс мітки (який є цілим числом, використаним при навчанні) з ключовим словом мітки. Якщо ви проводите навчання без кроку обчислення, ви можете уникнути цього, однак це допоможе при виконанні виведення пізніше.

# тепер ми створюємо файл pbtxt, ми повинні зробити це самостійно

def save_label_map(label_map_path, data):
    with open(label_map_path, 'w+') as f:
        for i in range(len(data)):
            line = "item {\
id: " + str(i + 1) + "\
name: '" + data[i] + "'\
}\
"
            f.write(line)

parser = argparse.ArgumentParser()
parser.add_argument('--trainable_classes_path', dest='trainable_classes', required=True)
parser.add_argument('--label_map_path', dest='label_map_path', required=True)

if __name__ == '__main__':
    args = parser.parse_args()
    trainable_classes_file = args.trainable_classes
    class_description_file = args.class_description
    label_map_path = args.label_map_path
    save_label_map(label_map_path, trainable_classes_file)

Доступних інструментів для генерування файлів label_map для Tensorflow немає, і для великих наборів міток, подібних до нашого, написання такого інструменту вручну може бути надто громіздким. Через це, я вирішив створити інструмент автоматичної заміни рядків, що задовольняє вимогам карти міток.

Джерело

Останній крок перед тим як ми почнемо конструювання нашої моделі ー створення файлів TFRecord.

Створення TFRecord

Записи Tensorflow ー цікава конструкція. Вони використовуються практично повсюди в об'єктах Tensoflow як носій для зберігання набору даних і приховують в собі багато складних речей, а документація з використання власного набору даних є неповною.

На щастя, я вже зробив за вас усю складну роботу. Цей розділ проведе вас через все, що вам необхідно для того, щоб розпочати використовувати записи Tensorflow!

Спочатку ми повинні згенерувати «номер класу» або ціле число індексів міток для кожної мітки. Ці цілі числа використовуються безпосередньо функцією перехресної ентропії нейронної мережі, яка використовується для калібрування продуктивності мережі в задачі класифікації. Ми визначаємо номер класу на основі порядку, в якому вони визначені в файлі trainable_classes.

def generate_class_num(points):
    enum_points = []
    with open(trainable_classes_file, 'rb') as file:
        trainable_classes = file.read().split('\
')
    for point in tqdm(points):
            for anno in point['annotations']
                anno['class_num'] = trainable_classes.index(anno['label'])+1
                output.append(anno)
    return output

Щоб створити запис для проекту виявлення об'єктів, нам потрібно кілька компонентів. Деякі з них базуються на одному зображенні, а деякі — на анотації.

На жаль, API для створення «прикладів» або окремих елементів у TFRecord трохи заплутаний. Ви не надаєте масив анотацій, а замість цього — ряд масивів для кожного індивідуального компонента анотації. Для цих компонентів «за анотацією» ми включаємо координати обмежувальних рамок, «текст» мітки або визначення, а також унікальне ціле значення для позначення цього конкретного класу.

Примітка: Якщо ви використовуєте свій власний набір даних, переконайтеся, що координати вашої обмежувальної рамки до координат зображення відносні, а не абсолютні. Якщо дані анотації набору даних визначені в абсолютних координатах, переконайтеся, що ви конвертували їх у відносні координати, перш ніж змінювати розмір зображень. Я ледь не згорів від цього, вчіться на моїх помилках 😀

# Створіть запис для кожного зображення.
# Якщо ми не можемо правильно завантажити файл зображення, пропустимо його
def group_to_tf_record(point, image_directory):
    format = b'jpeg'
    xmins = []
    xmaxs = []
    ymins = []
    ymaxs = []
    class_nums = []
    class_ids = []
    image_id = point[0]['id']
    filename = os.path.join(image_directory, image_id + '.jpg').decode()
    try:
        image = Image.open(filename)
        width, height = image.size
        with tf.gfile.GFile(filename, 'rb') as fid:
            encoded_jpg = bytes(fid.read())
    except:
        return None
    key = hashlib.sha256(encoded_jpg).hexdigest()
    for anno in point['annotations']:
        xmins.append(float(anno['x0']))
        xmaxs.append(float(anno['x1']))
        ymins.append(float(anno['y0']))
        ymaxs.append(float(anno['y1']))
        class_nums.append(anno['class_num'])
        class_ids.append(bytes(anno['label']))
    tf_example = tf.train.Example(features=tf.train.Features(feature={
        'image/height': dataset_util.int64_feature(height),
        'image/width': dataset_util.int64_feature(width),
        'image/key/sha256': dataset_util.bytes_feature(key.encode('utf8')),
        'image/filename': dataset_util.bytes_feature(bytes(filename)),
        'image/source_id': dataset_util.bytes_feature(bytes(image_id)),
        'image/encoded': dataset_util.bytes_feature(encoded_jpg),
        'image/format': dataset_util.bytes_feature(format),
        'image/object/bbox/xmin': dataset_util.float_list_feature(xmins),
        'image/object/bbox/xmax': dataset_util.float_list_feature(xmaxs),
        'image/object/bbox/ymin': dataset_util.float_list_feature(ymins),
        'image/object/bbox/ymax': dataset_util.float_list_feature(ymaxs),
        'image/object/class/text': dataset_util.bytes_list_feature(class_ids),
        'image/object/class/label': dataset_util.int64_list_feature(class_nums)
    }))
    return tf_example

Ого, тут тонна речей. Що робить весь цей код?

Спочатку ми завантажуємо файл зображення для цієї конкретної точки й закодовуємо його як масив даних. Дуже важливо завантажувати зображення таким способом, оскільки в API виявлення об'єктів логіка обробки внутрішнього зображення є крихкою та може не представляти зображення так, як ви цього очікуєте.

Далі ми перебираємо анотації точкового об'єкта й створюємо масиви елементів. Слід зазначити, що якщо який-небудь з масивів відсутній або не має однакову довжину, Tensorflow видасть купу виключень.

Тепер, коли ми створили те, що аналогічне «рядку» бази даних, слід записати дані у файл TFRecord!

Для стислості ми тримаємо логіку запису в головній процедурі сценаріїв, її можна з легкістю розмістити в окремій функції, якщо ви цього хочете.

def load_points(file_path):
    with open(file_path, 'rb') as f:
        points = json.load(f)
    return points


if __name__ == "__main__":
    trainable_classes_file = sys.argv[1]
    record_storage_path = sys.argv[2]
    annotations_file = sys.argv[3]
    saved_images_root_directory = sys.argv[4]
    annotations = load_points(annotations_file)
    with_class_num = generate_class_num(annotations)
    writer = tf.python_io.TFRecordWriter(record_storage_path)
    for group in tqdm(annotations, desc="writing to file"):
        record = group_to_tf_record(group)
        if record:
            serialized = record.SerializeToString()
            writer.write(serialized)
    writer.close()

Чудово, ми майже закінчили з цим. Ми скомпілювали окремі сценарії обробки у серію гістів (gists) для вашого користування. Обов'язково виконайте ці кроки кілька разів, щоб створити файли TFRecord для навчання, тестування та перевірки, оскільки вони знадобляться нам для наступного кроку.

Джерело

Крок 2: налаштування Object Detection API

Отже, всі наші дані правильно відформатовані у файли TFRecords, і ми ось-ось готові розпочати навчання. На цьому етапі слід почати додати елементи з Object Detection API.

API виявлення об'єктів містить кілька корисних сценаріїв, перевагами яких ми можемо скористатись. А саме ー сценарії eval.py та train.py у головній теці. Хоча їх встановлення ー ще той головний біль, тому я проведу вас через швидке встановлення, щоб зрушити справу з місця.

Налаштування середовища

По-перше, вам знадобляться системні залежності, тому завантажте термінал та слідуйте далі!

Встановіть необхідні системні залежності через pypi:

pip install pillow
pip install lxml
pip install jupyter
pip install matplotlib
pip install protobuf>=2.6

І найважливіше (якщо воно ще не встановлене):

# Для CPU
pip install tensorflow
# Для GPU
pip install tensorflow-gpu

Слід зазначити, що tensorflow-gpu скомпільований з дуже специфічними версіями CUDA та CUDNN, тому має сенс скомпілювати tensorflow проект із джерела, якщо ваше середовище відрізняється.

Далі потрібно клонувати репозиторій git-моделей, який можна знайти тут та скомпілювати його компоненти cython:

git clone https://github.com/tensorflow/models.git
# cd спершу до кореневого шляху проекту
cd models/research
protoc object_detection/protos/*.proto --python_out=.

Тепер переконаймося в тому, що встановлені змінні середовища (можливо, ви хочете встановити їх на постійній основі).

# поки в теці моделей/досліджень
export PYTHONPATH=$PYTHONPATH:`pwd`:`pwd`/slim
# і якщо ви використовуєте tensorflow-gpu й ще не встановили шлях cuda::
export LD_LIBRARY_PATH=/usr/local/cuda/lib64

Чудово, тепер все повністю встановлене. Якщо ви хочете провести невеликий тест, щоб впевнитись в тому, що все працює, спробуйте запустити й подивитись, чи все в порядку:

# поки в теці моделей/досліджень
python object_detection/builders/model_builder_test.py

Transfer learning

Виявлення об'єктів ー складна задача, що потребує використання технік глибокого навчання. Звичайно, вона потребує тренування моделі з сотнями шарів та мільйонами параметрів! Як ви, можливо, уявили, навіть наш набір даних у 660 тисяч зображень, скоріш за все буде недостатнім.

На щастя, є рішення! Всі конфігурації моделі виявлення об'єктів у Object Detection API підтримують transfer learning. Це означає, що ми можемо взяти попередньо підготований класифікатор зображень (натренований з допомогою мільйона зображень) й використати його для нашого детектора.

Саме те, як працює transfer learning, знаходиться поза межами цього глибокого занурення, але для того, щоб отримати більш інтуїтивне розуміння, я рекомендую вам ознайомитись з посиланням вище.

Добре, отже ми можемо використовувати попередньо навченні моделі, але звідки ж ми їх візьмемо?

Хороше питання! В глибині репозиторію API ви можете знайти цей зручний посібник, що описує кожну модель класифікації. Всі вони прості для перемикання з допоміжної на реальну пам'ять (swap in), і навпаки (swap out), що дуже зручно для тестування.

Отже, продовжимо. Завантажте один із цих файлів та розпакуйте його у спеціальну теку ー це допоможе нам пізніше.

Налаштування схеми виявлення об'єктів

Ми багато чого тут зробили, і вже майже можемо приступати до навчання, але спершу треба налаштувати конфігурацію буфера графів.

В API виявлення об'єктів, стандартним способом визначення моделі для навчання є створення або налаштування файлу config. Цей файл визначає те, як tensorflow інтерпретуватиме запити, звідки брати дані та де їх зберігати. У цьому файлі міститься купа інформації, тому розіб'ємо її на керовані фрагменти.

Це початок конфігурації моделі. Тут ми використовуємо шаблон виявлення об'єктів faster_rcnn. Він може бути замінений на інші архітектури, на відміну від цієї сторінки, але в цьому демо ми розглянемо лише faster_rcnn.

  • num_classes ー загальна кількість міток класифікації, де 0 позначає фоновий клас.
  • image_resizer важливий. Є два основних типів зміни розміру: fixed_shape_resizer та keep_aspect_ratio_resizer. Розмірність зображення важлива для виявлення об'єктів. Слід зазначити, що замість спотворення або деформації fixed_shape_resizer буде заповнювати незначний розмір, що значно покращить стабільність перед натуральними веб-зображеннями.
model {
  faster_rcnn {
    num_classes: 545
    image_resizer {
      fixed_shape_resizer {
        height: 350
        width: 350
      }
    }

Інша частина класу моделі визначає гіперпараметри різних рівнів. У більшості випадків гіперпараметри за замовчуванням дозволять вам досягти багато чого.

    feature_extractor {
      type: 'faster_rcnn_resnet101'
      first_stage_features_stride: 16
    }
    first_stage_anchor_generator {
      grid_anchor_generator {
        scales: [0.25, 0.5, 1.0, 2.0]
        aspect_ratios: [0.5, 1.0, 2.0]
        height_stride: 16
        width_stride: 16
      }
    }
    first_stage_box_predictor_conv_hyperparams {
      op: CONV
      regularizer {
        l2_regularizer {
          weight: 0.0
        }
      }
      initializer {
        truncated_normal_initializer {
          stddev: 0.01
        }
      }
    }
    first_stage_nms_score_threshold: 0.1
    first_stage_nms_iou_threshold: 0.7
    first_stage_max_proposals: 300
    first_stage_localization_loss_weight: 2.0
    first_stage_objectness_loss_weight: 1.0
    initial_crop_size: 14
    maxpool_kernel_size: 2
    maxpool_stride: 2
    second_stage_box_predictor {
      mask_rcnn_box_predictor {
        use_dropout: true
        dropout_keep_probability: 0.5
        fc_hyperparams {
          op: FC
          regularizer {
            l2_regularizer {
              weight: 0.0
            }
          }
          initializer {
            variance_scaling_initializer {
              factor: 1.0
              uniform: true
              mode: FAN_AVG
            }
          }
        }
      }
    }
    second_stage_post_processing {
      batch_non_max_suppression {
        score_threshold: 0.009999999776482582
        iou_threshold: 0.6000000238418579
        max_detections_per_class: 100
        max_total_detections: 300
      }
      score_converter: SOFTMAX
    }
    second_stage_localization_loss_weight: 2.0
    second_stage_classification_loss_weight: 1.0
  }
}

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

Погляньмо на щось, що не є шаблонним кодом ー train_config.

train_config: {
  batch_size: 20
    optimizer {
        adam_optimizer: {
            learning_rate {
            exponential_decay_learning_rate: {initial_learning_rate:0.00001}
            }
        }
    }
  fine_tune_checkpoint: "/media/deepstorage/model/faster-rcnn/model.ckpt"
  from_detection_checkpoint: True
  batch_queue_capacity: 50
  gradient_clipping_by_norm: 10
  data_augmentation_options {
    random_horizontal_flip {
    }
  }
}

Тут багато важливої інформації, розіб'ємо її на частини:

  • batch_size — визначає кількість елементів, що працюють у пакеті. Tensorflow потребує фіксованої кількості та не враховує пам'ять GPU чи розмір даних. Це число сильно залежить від апаратних якостей вашого GPU й розмірів зображення, та не являється суворою необхідністю для отримання якісних результатів. Tensorflow також потребує, щоб кожний вхідний масив мав однакову розмірність, що означає, що будь-який batch_size > 1 вимагає image_resize для fixed_shape_resizer.

  • optimizer — дуже важливий, бо він визначає, як будуть оновлюватись ваги за допомогою зворотного розповсюдження. Режим за замовчування — стандартний momentum_optimizer, що є гнучкою версією стохастичного градієнтного спуску (stochastic gradient descent). Це відмінно підходить для більшості типів систем, але для великих розріджених масивів, таких як наш вихідний масив, оптимізатор adam підходить більше. Якщо ви хочете продивитись інші параметри, перегляньте цей файл.

  • fine_tune_checkpoint — тут визначається тека та префікс ім'я файлу нашої попередньо навченої моделі. Ось чому ми самостійно зберегли файл у теці. Не турбуйтеся про те, що у вас нема файлу model.ckpt. Tensorflow це з'ясує.

  • from_detection_checkpoint: True — не описується в жодній документації, але потребується для правильної роботи попередньо навченої контрольної точки виявлення об'єктів. Якщо ви використовуєте чисту контрольну точку «класифікації», залиште значення false.

  • batch_queue_capacity — ще один важливий параметр. Tensorflow містить потоковий конвеєр, який дозволяє завантажувати сховище навчальних пакетів у пам'ять, але не встановлюється динамічно доступною пам'яттю хоста. За замовчуванням це число дорівнює 300, що, навіть з сильно зжатими зображеннями, вважалось занадто великою кількістю для нашої високопродуктивної навчальної машини. Відрегулюйте відповідно.

  • gradient_clipping_by_norm — параметр важливий для уникання вибухових градієнтів (exploding gradients). Ми встановлюємо експериментальне значення 10, але його можна відрегулювати.

  • data_augmentation_options — встановлення деяких додаткових опцій може значно збільшити розмір нашого набору даних, одночасно збільшуючи надійність детектора. Для отримання інформації про те, які опції доступні, зверніться до цього файлу.

Отже, тепер у нас є налаштований за нашим бажанням training_config, наш GPU зможе справитись з завданням без неприємних помилок OOM. Подивимось тепер на eval_config:

eval_config: {
  num_examples: 3000
  num_visualizations: 20
}

Набагато компактніше, вірно? Параметри за замовчуванням добре тут підходять, особливо, якщо врахувати, що етап розрахунку в основному призначений для візуалізації узагальнення та надійності. Якщо ваша система починає зависати, коли виконуються обидва етапи ー навчання та розрахунку, можливо, варто зменшити значення num_examples. Якщо ви бажаєте переглянути весь список опцій, зверніться до цього файлу.

Нарешті, подивімося на наші дві конфігурації читача:

train_input_reader: {
  tf_record_input_reader {
    input_path: "PATH_TO_RECORD_FILE/train_545.record"
  }
  label_map_path: "PATH_TO_LABEL_MAP/label_map_545.pbtxt"
}

eval_input_reader: {
  tf_record_input_reader {
    input_path: "PATH_TO_RECORD_FILE/test_545.record"
  }
  label_map_path: "PATH_TO_LABEL_MAP/label_map_545.pbtxt"
  shuffle: false
}

Дуже просто, так? Ми встановлюємо перетасування у значення false, тому що хочемо побачити як мережа вдосконалюється від одного обчислення до іншого, але можете встановити це значення у true, якщо бажаєте отримати більш стохастичний результат.

Добре, досі ми маніпулювали й форматували метадані набору даних, завантажили, перевірили й змінили розмір всіх наших файлів зображень та створили файли записів. Ми завантажили та підготували API виявлення об'єктів, і тепер створили файл конфігурації.

Ми нарешті готові розпочати навчання!

Джерело

Крок 3: Навчання та продакшн

Все необхідне встановлено для того, щоб розпочати навчання, але спершу швидко опишемо процеси навчання та обчислення.

У теці з API є два важливих скрипти: eval.py та train.py. Це правда, що нам не треба запускати скрипт eval.py, оскільки він не сприяє навчанню, однак, він надає неоціненну інформацію про навчання, яка може бути з легкістю відображена й розповсюджена, використовуючи Tensorboard.

Наступні скрипти використовуються в командному рядку, і їх слід запускати в окремих сеансах. Я рекомендую для простоти використовувати інструмент screen.

python object_detection/train.py \\
--logtostderr \\ 
--train_dir=PATH_TO_TRAINING_OUTPUT_DIRECTORY \\ 
--pipeline_config_path=PATH_TO_CONFIG_FILE

Тека training_output міститиме усі важливі файли контрольних точок, необхідних для виведення та обслуговування, коли модель буде достатньо навчена. Підключення до std err означає, що вивід буде докладнішим, що корисно для дебагу.

python object_detection/eval.py \\
--logtostderr \\
--eval_dir=PATH_TO_EVALUATION_OUTPUT_DIRECTORY \\
--pipeline_config_path=PATH_TO_CONFIG_FILE

І, нарешті, на іншому екрані — запустіть tensorboard daemon

# from tensorboard source directory
tensorboard \\
--logdir=training:/PATH_TO_TRAINING_OUTPUT_DIR,testing:/PATH_TO_EVAL_OUTPUT_DIR\\
--port=6006
--host=localhost

Коли всі ці скрипти запущені, ви на шляху навчання вашої нейронної мережі! Навчання може зайняти деякий час, тому обов'язково поверніться до екземпляра Tensorboard, що виконується, щоб перевірити узагальнення вашої моделі. Також слід зазначити, що API не зупиниться, коли «вичерпає дані», тому найкращим способом виявлення завершення одного проходу є момент, коли середня точність стане плоскою лінією.

Глибоке занурення у виявлення об'єктів з Tensorflow

З Tensorboard можна перевірити деякі зразки зображень та побачити як виглядає обчислення на перший погляд.

Глибоке занурення у виявлення об'єктів з Tensorflow

Чудово, виглядає так, наче дійсно навчили щось, що може виявляти речі. Погляньмо тепер як це перевести у продакшн.

Генерація Frozen Graph

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

Tensorflow має концепт, відомий як експортування метаграфа (exporting metagraph). Замороження графу дозволяє нам об'єднати структуру моделі (файл конфігурації) разом із вагами та градієнтом у єдиний файл двійкового протобуфера.

Для більшості методів виведення ми робимо це, виконуючи скрипт під назвою export_inference_graph.py, який, знову ж таки, можна знайти у репозиторії object_detection.

python export_inference_graph.py --input_type image_tensor \\
--pipeline_config_path /PATH/TO/CONFIG/FILE.config \\
--trained_checkpoint_prefix /PATH/TO/TRAINED/OUTPUT/DIRECTORY/model.ckpt \\
--output_directory /PATH/TO/FROZEN/DIRECTORY

Після того, як це зроблено, у вас з'явиться файл frozen_inference_graph.pb у вашій теці frozen. Проігноруйте решту gobbley-gook там й завантажте його у data API разом з нашими попередньо визначеним label_map.pbtxt, щоб ми могли конвертувати наші закодовані класи в такі речі як кішка, собака та яблуко.

Службові виводи з Algorithmia

У нас є все необхідне для створення алгоритму на Algorithmia. Першим кроком буде створення нового алгоритму й визначення його мови як алгоритму python3 для підтримки Tensorflow. Обов'язково вкажіть, що алгоритм потребує доступ до інтернету та GPU для обробки, або вивід забере цілу купу часу.

Подивімося зараза на наш нинішній файл алгоритму. Ми розкладемо його на частини й поговоримо про кожну з них окремо.

import numpy as np
import tensorflow as tf
from PIL import Image
import Algorithmia
import os
import multiprocessing
from . import label_map_util

Нам потрібно декілька додаткових файлів з репозиторію object_detection, щоб все працювало, а саме ー скрипти label_map_util.py та string_int_label_map_pb2.py. Обидва файли надані в репозиторії.

# Це код для більшості алгоритмів виявлення об'єктів tensorflow
# У цьому прикладі він налаштований спеціально для нашого прикладу даних open images.

client = Algorithmia.client()
TEMP_COLLECTION = 'data://.session/'
BOUNDING_BOX_ALGO = 'util/BoundingBoxOnImage/0.1.x'
SIMD_ALGO = "util/SmartImageDownloader/0.2.14"
MODEL_FILE = "data://zeryx/openimagesDemo/ssd.pb"
LABEL_FILE = "data://zeryx/openimagesDemo/label_map.pbtxt"
NUM_CLASSES = 545


class AlgorithmError(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)

Як і у всіх наших стандартних алгоритмах Python — ми заздалегідь визначаємо будь-які константи, параметри, що повторно використовуються, зокрема файли та алгоритми, з якими ми можемо взаємодіяти кілька разів. Визначивши все заздалегідь, ми полегшимо зміну речей пізніше.

Ми також описуємо об'єкт AlgorithmError, це допомагає нам робити більш стислі винятки.

def load_model():
    path_to_labels = client.file(LABEL_FILE).getFile().name
    path_to_model = client.file(MODEL_FILE).getFile().name
    detection_graph = tf.Graph()
    with detection_graph.as_default():
        od_graph_def = tf.GraphDef()
        with tf.gfile.GFile(path_to_model, 'rb') as fid:
            serialized_graph = fid.read()
            od_graph_def.ParseFromString(serialized_graph)
            tf.import_graph_def(od_graph_def, name='')
    label_map = label_map_util.load_labelmap(path_to_labels)
    categories = label_map_util.convert_label_map_to_categories(
        label_map, max_num_classes=NUM_CLASSES, use_display_name=True)
    category_index = label_map_util.create_category_index(categories)
    return detection_graph, category_index
        
def load_labels(label_path):
    label_map = label_map_util.load_labelmap(label_path)
    categories = label_map_util.convert_label_map_to_categories(
        label_map, max_num_classes=NUM_CLASSES, use_display_name=True)
    category_index = label_map_util.create_category_index(categories)
    return category_index

Це наш стандартний попередньо завантажений фрагмент виявлення об'єктів Tensorflow. Зверніть особливу увагу на те, як path_to_model використовується для встановлення об'єкта detection_graph. Як ви можете побачити, він визначений через глобальний об'єкт tf, що робить подальше вдосконалення цього процесу складним.

Наша карта міток перевтілюється в category_index, що корисно для простого пошуку кожної мітки в нашій функції виводу.

def load_image_into_numpy_array(image):
    (im_width, im_height) = image.size
    return np.array(image.getdata()).reshape(
      (im_height, im_width, 3)).astype(np.uint8)


def get_image(url):
    output_url = client.algo(SIMD_ALGO).pipe({'image': str(url)}).result['savePath'][0]
    temp_file = client.file(output_url).getFile().name
    renamed_path = temp_file + '.' + output_url.split('.')[-1]
    os.rename(temp_file, renamed_path)
    return renamed_path

Тут ми зазначаємо, як ми завантажуємо зображення, використовуючи Smart Image Downloader, і як ми завантажуємо його в необхідних розмірів масив Numpy для Tensorflow.

def generate_gpu_config(memory_fraction):
    config = tf.ConfigProto()
    # config.gpu_options.allow_growth = True
    config.gpu_options.per_process_gpu_memory_fraction = memory_fraction
    return config

Це дуже важливий компонент, що оптимізує витрати пам'яті Tensorflow. Він також зменшує вузькі місця (bottlenecks) та помилки OOM при запуску скрипту виводу на algorithmia. Якщо per_process_gpu_memory_fraction не визначений, по замовчуванню використовується значення 1.

Визначення змінної allow_growth означає, що виділяється стільки пам'яті GPU, скільки суворо необхідно.

# Ця функція виконує пропускну операцію над frozen graph,
# витягує найбільш імовірні обмежувальні рамки та ваги.
def infer(graph, image_path, category_index, min_score, output):
    with graph.as_default():
        with tf.Session(graph=graph, config=generate_gpu_config(0.6)) as sess:
            image_np = load_image_into_numpy_array(Image.open(image_path).convert('RGB'))
            height, width, _ = image_np.shape
            image_np_expanded = np.expand_dims(image_np, axis=0)
            image_tensor = graph.get_tensor_by_name('image_tensor:0')
            boxes = graph.get_tensor_by_name('detection_boxes:0')
            scores = graph.get_tensor_by_name('detection_scores:0')
            classes = graph.get_tensor_by_name('detection_classes:0')
            num_detections = graph.get_tensor_by_name('num_detections:0')
            (boxes, scores, classes, num_detections) = sess.run(
                [boxes, scores, classes, num_detections],
                feed_dict={image_tensor: image_np_expanded})
            boxes = np.squeeze(boxes)
            classes = np.squeeze(classes).astype(np.int32)
            scores = np.squeeze(scores)
    for i in range(len(boxes)):
        confidence = float(scores[i])
        if confidence >= min_score:
            ymin, xmin, ymax, xmax = tuple(boxes[i].tolist())
            ymin = int(ymin * height)
            ymax = int(ymax * height)
            xmin = int(xmin * width)
            xmax = int(xmax * width)
            class_name = category_index[classes[i]]['name']
            output.append(
                {
                    'coordinates': {
                                   'y0': ymin,
                                   'y1': ymax,
                                   'x0': xmin,
                                   'x1': xmax
                               },
                    'label': class_name,
                    'confidence': confidence
                }
            )

Це наша основна функція виводу, тому розпакуємо її.

Ми визначаємо долю пам'яті GPU до 0.6, але при необхідності її можна відрегулювати. Також ми форматуємо дані зображень у масив Numpy й витягуємо його розміри для процесу виводу. Потім ми витягаємо дескриптори Tensorflow, що визначені на виході нашого графа. Після цього ми запускаємо крок виведення, використовуючи функцію sess.run.

Крок виведення ー найбільш трудомісткий процес, але після його закінчення ми можемо форматувати результати у корисну форму. Ми відфільтрували рамки зі значенням перехресної ентропії (cross entropy), меншим за min_score та форматували його у простий для аналізу формат JSON.

Як ви могли помітити, замість звичайного повернення, наші результати тут повертаються як оновлення зміненого виведення списку. Я покажу вам пізніше, чому ми робимо це в нашій функції apply.

def draw_boxes_and_save(image, output_path, box_data):
    request = {}
    remote_image = TEMP_COLLECTION + image.split('/')[-1]
    temp_output = TEMP_COLLECTION + '1' + image.split('/')[-1]
    client.file(remote_image).putFile(image)
    request['imageUrl'] = remote_image
    request['imageSaveUrl'] = temp_output
    request['style'] = 'basic'
    boxes = []
    for box in box_data:
        coords = box['coordinates']
        coordinates = {'left': coords['x0'], 'right': coords['x1'],
                       'top': coords['y0'], 'bottom': coords['y1']}
        text_objects = [{'text': box['label'], 'position': 'top'},
                       {'text': 'score: {}%'.format(box['confidence']), 'position': 'bottom'}]
        boxes.append({'coordinates': coordinates, 'textObjects': text_objects})
    request['boundingBoxes'] = boxes
    temp_image = client.algo(BOUNDING_BOX_ALGO).pipe(request).result['output']
    local_image = client.file(temp_image).getFile().name
    client.file(output_path).putFile(local_image)
    return output_path

Якщо користувачу потрібен графічний результат, можна використати алгоритм обмежувальної рамки на зображенні, щоб швидко створити графічне представлення результатів виявлення. Використовуючи цю логіку, ми можемо швидко створити зображення як це:

Глибоке занурення у виявлення об'єктів з Tensorflow

def apply(input):
    output_path = None
    min_score = 0.50
    if isinstance(input, str):
        image = get_image(input)
    elif isinstance(input, dict):
        if 'image' in input and isinstance(input['image'], str):
            image = get_image(input['image'])
        else:
            raise Exception("AlgoError3000: 'image' missing from input")
        if 'output' in input and isinstance(input['output'], str):
            output_path = input['output']
        if 'min_score' in input and isinstance(input['min_score'], float):
            min_score = input['min_score']
    else:
        raise AlgorithmError("AlgoError3000: Invalid input")
    manager = multiprocessing.Manager()
    box_output = manager.list()
    p = multiprocessing.Process(target=infer,
                                args=(GRAPH, image, CAT_INDEX,
                                      min_score, box_output))
    p.start()
    p.join()
    box_output = [x for x in box_output]
    box_output = sorted(box_output, key=lambda k: k['confidence'])
    if output_path:
        path = '/tmp/image.' + output_path.split('.')[-1]
        im = Image.open(image).convert('RGB')
        im.save(path)
        image = draw_boxes_and_save(path, output_path, box_output)
        return {'boxes': box_output, 'image': image}
    else:
        return {'boxes': box_output}

GRAPH, CAT_INDEX = load_model()

Нарешті, подивімося на нашу функцію apply, серце будь-якого алгоритму на Algorithmia. У цій функції нам надається input, який може бути декількох типів. Спочатку ми повинні обробити цей вхід в очікуваний тип схеми, що і робить перша половина функції.

Однак, як ви, можливо, помітили, ми використовуємо багатопроцесорну функціональність, зокрема керований список і Process. Чому б ми коли-небудь захотіли використовувати багатопотокову систему для, по суті, послідовного алгоритму?

Tensorflow сьогодні визначається глобальною змінною tf. Коли inference функції виходить, змінна все ще містить його задані властивості та значення. Одним з цих значень є контекст пам'яті нашого GPU, який звільняється тільки тоді, коли звільняється змінна tf. Через це ми можемо зіткнутися з проблемами, коли Tensorflow не звільняє пам'ять GPU, що може викликати багато ускладнень в майбутньому. Запустивши застосунок Tensorflow в окремому потоці, а потім вбивши потік, ми вбиваємо також контекст пам'яті GPU Tensorflow без впливу на продуктивність!

Після того, як ми витягнемо наші результати з керованого списку, ми зможемо швидко завершити постобробку та повернути їх!

І, нарешті, внизу цього скрипту ви можете побачити, що ми запускаємо скрипт load_model() у глобальному стані. Це означає, що ми попередньо завантажуємо frozen graph у пам'ять хоста, що різко знижує час затримки та змінюваність запитів API.

Ось і все. Ми все зробили! Якщо ви хочете побачити робочий демо-алгоритм детектора об'єктів, можете знайти його тут.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 1.6K
Приєднався: 1 рік тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід