Загрузка изображений

Смотрите на TensorFlow.org Запустите в Google Colab Изучайте код на GitHub Скачайте ноутбук

В этом руководстве приведен простой пример загрузки датасета изображений с использованием tf.data.

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

Setup

from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf
AUTOTUNE = tf.data.experimental.AUTOTUNE

Скачайте и проверьте набор данных

Получите изображения

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

import pathlib
data_root_orig = tf.keras.utils.get_file(origin='https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz',
                                         fname='flower_photos', untar=True)
data_root = pathlib.Path(data_root_orig)
print(data_root)
Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz
228818944/228813984 [==============================] - 2s 0us/step
/home/kbuilder/.keras/datasets/flower_photos

После скачивания 218MB, вам теперь доступна копия изображений цветов:

for item in data_root.iterdir():
  print(item)
/home/kbuilder/.keras/datasets/flower_photos/LICENSE.txt
/home/kbuilder/.keras/datasets/flower_photos/daisy
/home/kbuilder/.keras/datasets/flower_photos/roses
/home/kbuilder/.keras/datasets/flower_photos/sunflowers
/home/kbuilder/.keras/datasets/flower_photos/dandelion
/home/kbuilder/.keras/datasets/flower_photos/tulips
import random
all_image_paths = list(data_root.glob('*/*'))
all_image_paths = [str(path) for path in all_image_paths]
random.shuffle(all_image_paths)

image_count = len(all_image_paths)
image_count
3670
all_image_paths[:10]
['/home/kbuilder/.keras/datasets/flower_photos/dandelion/13386618495_3df1f1330d.jpg',
 '/home/kbuilder/.keras/datasets/flower_photos/dandelion/2697283969_c1f9cbb936.jpg',
 '/home/kbuilder/.keras/datasets/flower_photos/sunflowers/7791014076_07a897cb85_n.jpg',
 '/home/kbuilder/.keras/datasets/flower_photos/sunflowers/6141150299_b46a64e4de.jpg',
 '/home/kbuilder/.keras/datasets/flower_photos/dandelion/3372748508_e5a4eacfcb_n.jpg',
 '/home/kbuilder/.keras/datasets/flower_photos/daisy/520752848_4b87fb91a4.jpg',
 '/home/kbuilder/.keras/datasets/flower_photos/daisy/15327813273_06cdf42210.jpg',
 '/home/kbuilder/.keras/datasets/flower_photos/sunflowers/7586498522_4dcab1c8d2_m.jpg',
 '/home/kbuilder/.keras/datasets/flower_photos/sunflowers/253586685_ee5b5f5232.jpg',
 '/home/kbuilder/.keras/datasets/flower_photos/daisy/10172567486_2748826a8b.jpg']

Просмотрите изображения

Сейчас давайте быстро просмотрим парочку изображений, чтобы вы знали с чем имеете дело:

import os
attributions = (data_root/"LICENSE.txt").open(encoding='utf-8').readlines()[4:]
attributions = [line.split(' CC-BY') for line in attributions]
attributions = dict(attributions)
import IPython.display as display

def caption_image(image_path):
    image_rel = pathlib.Path(image_path).relative_to(data_root)
    return "Image (CC BY 2.0) " + ' - '.join(attributions[str(image_rel)].split(' - ')[:-1])

for n in range(3):
  image_path = random.choice(all_image_paths)
  display.display(display.Image(image_path))
  print(caption_image(image_path))
  print()

jpeg

Image (CC BY 2.0)  by Kayla Kandzorra

jpeg

Image (CC BY 2.0)  by Casey Hugelfink

jpeg

Image (CC BY 2.0)  by T.Kiya

Определите метку для каждого изображения

Выведите на экран все доступные метки:

label_names = sorted(item.name for item in data_root.glob('*/') if item.is_dir())
label_names
['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips']

Присвойте индекс каждой метке:

label_to_index = dict((name, index) for index, name in enumerate(label_names))
label_to_index
{'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflowers': 3, 'tulips': 4}

Создайте список всех файлов и индексов их меток:

all_image_labels = [label_to_index[pathlib.Path(path).parent.name]
                    for path in all_image_paths]

print("First 10 labels indices: ", all_image_labels[:10])
First 10 labels indices:  [1, 1, 3, 3, 1, 0, 0, 3, 3, 0]

Загрузите и отформатируйте изображения

TensorFlow включает все инструменты которые вам могут понадобиться для загрузки и обработки изображений:

img_path = all_image_paths[0]
img_path
'/home/kbuilder/.keras/datasets/flower_photos/dandelion/13386618495_3df1f1330d.jpg'

Вот сырые данные:

img_raw = tf.io.read_file(img_path)
print(repr(img_raw)[:100]+"...")
<tf.Tensor: id=1, shape=(), dtype=string, numpy=b'\xff\xd8\xff\xe1\x00HExif\x00\x00II*\x00\x08\x00\x...

Преобразуйте ее в тензор изображения:

img_tensor = tf.image.decode_image(img_raw)

print(img_tensor.shape)
print(img_tensor.dtype)
(335, 500, 3)
<dtype: 'uint8'>

Измените ее размер для вашей модели:

img_final = tf.image.resize(img_tensor, [192, 192])
img_final = img_final/255.0
print(img_final.shape)
print(img_final.numpy().min())
print(img_final.numpy().max())

(192, 192, 3)
0.0
0.999596

Оберните их в простые функции для будущего использования.

def preprocess_image(image):
  image = tf.image.decode_jpeg(image, channels=3)
  image = tf.image.resize(image, [192, 192])
  image /= 255.0  # normalize to [0,1] range

  return image
def load_and_preprocess_image(path):
  image = tf.io.read_file(path)
  return preprocess_image(image)
import matplotlib.pyplot as plt

image_path = all_image_paths[0]
label = all_image_labels[0]

plt.imshow(load_and_preprocess_image(img_path))
plt.grid(False)
plt.xlabel(caption_image(img_path))
plt.title(label_names[label].title())
print()

Постройте tf.data.Dataset

Датасет изображений

Простейший способ построения tf.data.Dataset это использование метода from_tensor_slices.

Нарезка массива строк, приводит к датасету строк:

path_ds = tf.data.Dataset.from_tensor_slices(all_image_paths)

Параметры shapes и types описывают содержимое каждого элемента датасета. В этом случае у нас множество скалярных двоичных строк

print(path_ds)
<TensorSliceDataset shapes: (), types: tf.string>

Сейчас создадим новый датасет который загружает и форматирует изображения на лету пройдясь с preprocess_image по датасету с путями к файлам.

image_ds = path_ds.map(load_and_preprocess_image, num_parallel_calls=AUTOTUNE)
import matplotlib.pyplot as plt

plt.figure(figsize=(8,8))
for n, image in enumerate(image_ds.take(4)):
  plt.subplot(2,2,n+1)
  plt.imshow(image)
  plt.grid(False)
  plt.xticks([])
  plt.yticks([])
  plt.xlabel(caption_image(all_image_paths[n]))
  plt.show()

png

png

png

png

Датасет пар (image, label)

Используя тот же метод from_tensor_slices вы можете собрать датасет меток:

label_ds = tf.data.Dataset.from_tensor_slices(tf.cast(all_image_labels, tf.int64))
for label in label_ds.take(10):
  print(label_names[label.numpy()])
dandelion
dandelion
sunflowers
sunflowers
dandelion
daisy
daisy
sunflowers
sunflowers
daisy

Поскольку датасеты следуют в одном и том же порядке, вы можете просто собрать их вместе при помощи функции zip в набор данных пары (image, label):

image_label_ds = tf.data.Dataset.zip((image_ds, label_ds))

Shapes и types нового датасета это кортежи размерностей и типов описывающие каждое поле:

print(image_label_ds)
<ZipDataset shapes: ((192, 192, 3), ()), types: (tf.float32, tf.int64)>

Примечание. Если у вас есть такие массивы, как «all_image_labels» и «all_image_paths», альтернативой «tf.data.dataset.Dataset.zip» является срез (slice) пары массивов.

ds = tf.data.Dataset.from_tensor_slices((all_image_paths, all_image_labels))

# Кортежи распаковываются в позиционные аргументы отображаемой функции
def load_and_preprocess_from_path_label(path, label):
  return load_and_preprocess_image(path), label

image_label_ds = ds.map(load_and_preprocess_from_path_label)
image_label_ds
<MapDataset shapes: ((192, 192, 3), ()), types: (tf.float32, tf.int32)>

Базовые способы обучения

Для обучения модели на этом датасете, вам необходимо, чтобы данные:

  • Были хорошо перемешаны.
  • Были упакованы.
  • Повторялись вечно.
  • Пакеты должны быть доступны как можно скорее.

Эти свойства могут быть легко добавлены с помощью tf.data api.

BATCH_SIZE = 32

# Установка размера буфера перемешивания, равного набору данных, гарантирует
# полное перемешивание данных.
ds = image_label_ds.shuffle(buffer_size=image_count)
ds = ds.repeat()
ds = ds.batch(BATCH_SIZE)
# `prefetch` позволяет датасету извлекать пакеты в фоновом режиме, во время обучения модели.
ds = ds.prefetch(buffer_size=AUTOTUNE)
ds
<PrefetchDataset shapes: ((None, 192, 192, 3), (None,)), types: (tf.float32, tf.int32)>

Здесь необходимо отметить несколько вещей:

  1. Важна последовательность действий.

    • .shuffle после .repeat перемешает элементы вне границ эпох (некоторые элементы будут увидены дважды в то время как другие ни разу).
    • .shuffle после .batch перемешает порядок пакетов, но не перемешает элементы внутри пакета.
  2. Используйте buffer_size такого же размера как и датасет для полного перемешивания. Вплоть до размера набора данных, большие значения обеспечивают лучшую рандомизацию, но используют больше памяти.

  3. Из буфера перемешивания не выбрасываются элементы пока он не заполнится. Поэтому большой размер buffer_size может стать причиной задержки при запуске Dataset.

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

Последний пункт может быть решен использованием метода tf.data.Dataset.apply вместе со слитой функцией tf.data.experimental.shuffle_and_repeat:

ds = image_label_ds.apply(
  tf.data.experimental.shuffle_and_repeat(buffer_size=image_count))
ds = ds.batch(BATCH_SIZE)
ds = ds.prefetch(buffer_size=AUTOTUNE)
ds
WARNING:tensorflow:From <ipython-input-31-4dc713bd4d84>:2: shuffle_and_repeat (from tensorflow.python.data.experimental.ops.shuffle_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use `tf.data.Dataset.shuffle(buffer_size, seed)` followed by `tf.data.Dataset.repeat(count)`. Static tf.data optimizations will take care of using the fused implementation.

<PrefetchDataset shapes: ((None, 192, 192, 3), (None,)), types: (tf.float32, tf.int32)>

Передайте датасет в модель

Получите копию MobileNet v2 из tf.keras.applications.

Она будет использована для простого примера передачи обучения (transfer learning).

Установите веса MobileNet необучаемыми:

mobile_net = tf.keras.applications.MobileNetV2(input_shape=(192, 192, 3), include_top=False)
mobile_net.trainable=False
Downloading data from https://github.com/JonathanCMitchell/mobilenet_v2_keras/releases/download/v1.1/mobilenet_v2_weights_tf_dim_ordering_tf_kernels_1.0_192_no_top.h5
9412608/9406464 [==============================] - 2s 0us/step

Эта модель предполагает нормализацию входных данных в диапазоне [-1,1]:

help(keras_applications.mobilenet_v2.preprocess_input)
...
Эта функция применяет препроцессинг "Inception" который преобразует
RGB значения из [0, 255] в [-1, 1]
...

Перед передачей входных данных в модель MobilNet, вам нужно конвертировать их из диапазона [0,1] в [-1,1]:

def change_range(image,label):
  return 2*image-1, label

keras_ds = ds.map(change_range)

MobileNet возвращает 6x6 сетку признаков для каждого изображения.

Передайте ей пакет изображений чтобы увидеть:

# Датасету может понадобиться несколько секунд для старта пока заполняется буфер перемешивания.
image_batch, label_batch = next(iter(keras_ds))
feature_map_batch = mobile_net(image_batch)
print(feature_map_batch.shape)
(32, 6, 6, 1280)

Постройте модель обернутую вокруг MobileNet и используйте tf.keras.layers.GlobalAveragePooling2D для усреднения по этим размерностям пространства перед выходным слоем tf.keras.layers.Dense:

model = tf.keras.Sequential([
  mobile_net,
  tf.keras.layers.GlobalAveragePooling2D(),
  tf.keras.layers.Dense(len(label_names), activation = 'softmax')])

Сейчас он производит выходные данные ожидаемых размеров:

logit_batch = model(image_batch).numpy()

print("min logit:", logit_batch.min())
print("max logit:", logit_batch.max())
print()

print("Shape:", logit_batch.shape)
min logit: 0.0048472313
max logit: 0.8545553

Shape: (32, 5)

Скомпилируйте модель чтобы описать процесс обучения:

model.compile(optimizer=tf.keras.optimizers.Adam(),
              loss='sparse_categorical_crossentropy',
              metrics=["accuracy"])

Есть две переменные для обучения - Dense weights и bias:

len(model.trainable_variables)
2
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
mobilenetv2_1.00_192 (Model) (None, 6, 6, 1280)        2257984   
_________________________________________________________________
global_average_pooling2d (Gl (None, 1280)              0         
_________________________________________________________________
dense (Dense)                (None, 5)                 6405      
=================================================================
Total params: 2,264,389
Trainable params: 6,405
Non-trainable params: 2,257,984
_________________________________________________________________

Вы готовы обучать модель.

Отметим, что для демонстрационных целей вы запустите только 3 шага на эпоху, но обычно вам нужно указать действительное число шагов, как определено ниже, перед передачей их в model.fit():

steps_per_epoch=tf.math.ceil(len(all_image_paths)/BATCH_SIZE).numpy()
steps_per_epoch
115.0
model.fit(ds, epochs=1, steps_per_epoch=3)
Train for 3 steps
3/3 [==============================] - 10s 3s/step - loss: 1.8022 - accuracy: 0.2188

<tensorflow.python.keras.callbacks.History at 0x7f156edaef60>

Производительность

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

Простые конвейеры, использованные выше, прочитывают каждый файл отдельно во время каждой эпохи. Это подходит для локального обучения на CPU, может быть недостаточно для обучения на GPU и абсолютно неприемлемо для любого вида распределенного обучения.

Чтобы исследовать производительность наших датасетов, сперва постройте простую функцию:

import time
default_timeit_steps = 2*steps_per_epoch+1

def timeit(ds, steps=default_timeit_steps):
  overall_start = time.time()
  # Выберем один пакет для передачи в пайплайн (заполнение буфера перемешивания),
  # перед запуском таймера
  it = iter(ds.take(steps+1))
  next(it)

  start = time.time()
  for i,(images,labels) in enumerate(it):
    if i%10 == 0:
      print('.',end='')
  print()
  end = time.time()

  duration = end-start
  print("{} batches: {} s".format(steps, duration))
  print("{:0.5f} Images/s".format(BATCH_SIZE*steps/duration))
  print("Total time: {}s".format(end-overall_start))

Производительность данного датасета равна:

ds = image_label_ds.apply(
  tf.data.experimental.shuffle_and_repeat(buffer_size=image_count))
ds = ds.batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
ds
<PrefetchDataset shapes: ((None, 192, 192, 3), (None,)), types: (tf.float32, tf.int32)>
timeit(ds)
........................
231.0 batches: 12.871192216873169 s
574.30577 Images/s
Total time: 19.21748924255371s

Кеш

Используйте tf.data.Dataset.cache, чтобы с легкостью кэшировать вычисления от эпохи к эпохе. Это особенно эффективно если данные помещаются в память.

Здесь изображения кэшируются после предварительной обработки (перекодирования и изменения размера):

ds = image_label_ds.cache()
ds = ds.apply(
  tf.data.experimental.shuffle_and_repeat(buffer_size=image_count))
ds = ds.batch(BATCH_SIZE).prefetch(buffer_size=AUTOTUNE)
ds
<PrefetchDataset shapes: ((None, 192, 192, 3), (None,)), types: (tf.float32, tf.int32)>
timeit(ds)
........................
231.0 batches: 0.6887447834014893 s
10732.56768 Images/s
Total time: 6.619239807128906s

Одним из недостатков использования кэша памяти является то, что кэш должен перестраиваться при каждом запуске, давая одинаковую начальную задержку при каждом запуске датасета:

timeit(ds)
........................
231.0 batches: 0.6965987682342529 s
10611.56054 Images/s
Total time: 0.7095608711242676s

Если данные не помещаются в памяти, используйте файл кэша:

ds = image_label_ds.cache(filename='./cache.tf-data')
ds = ds.apply(
  tf.data.experimental.shuffle_and_repeat(buffer_size=image_count))
ds = ds.batch(BATCH_SIZE).prefetch(1)
ds
<PrefetchDataset shapes: ((None, 192, 192, 3), (None,)), types: (tf.float32, tf.int32)>
timeit(ds)
........................
231.0 batches: 3.1032369136810303 s
2382.02890 Images/s
Total time: 11.058222532272339s

Также у файла кеша есть преимущество использования быстрого перезапуска датасета без перестраивания кеша. Обратите внимание, насколько быстрее это происходит во второй раз:

timeit(ds)
........................
231.0 batches: 2.553021192550659 s
2895.39312 Images/s
Total time: 3.9510445594787598s

Файл TFRecord

Необработанные данные - изображения

TFRecord файлы - это простой формат для хранения двоичных блобов (blob). Упаковывая несколько примеров в один файл, TensorFlow может читать несколько элементов за раз, что особенно важно для производительности особенно при использовании удаленного сервиса хранения, такого как GCS.

Сперва построим файл TFRecord из необработанных данных изображений:

image_ds = tf.data.Dataset.from_tensor_slices(all_image_paths).map(tf.io.read_file)
tfrec = tf.data.experimental.TFRecordWriter('images.tfrec')
tfrec.write(image_ds)

Затем построим датасет, который прочитывает файл TFRecord и обрабатывает изображения с использованием функции preprocess_image, которую вы задали ранее:

image_ds = tf.data.TFRecordDataset('images.tfrec').map(preprocess_image)

Объедините этот датасет с датасетом меток, который вы определили ранее, чтобы получить пару из (image,label):

ds = tf.data.Dataset.zip((image_ds, label_ds))
ds = ds.apply(
  tf.data.experimental.shuffle_and_repeat(buffer_size=image_count))
ds=ds.batch(BATCH_SIZE).prefetch(AUTOTUNE)
ds
<PrefetchDataset shapes: ((None, 192, 192, 3), (None,)), types: (tf.float32, tf.int64)>
timeit(ds)
........................
231.0 batches: 13.176032066345215 s
561.01867 Images/s
Total time: 19.49629044532776s

Это медленнее cache версии, поскольку обработанные изображения не кешируются.

Сериализованные тензоры

Чтобы сохранить некоторый препроцессинг в файл TFRecord сперва, как и ранее, создайте датасет обработанных изображений:

paths_ds = tf.data.Dataset.from_tensor_slices(all_image_paths)
image_ds = paths_ds.map(load_and_preprocess_image)
image_ds
<MapDataset shapes: (192, 192, 3), types: tf.float32>

Сейчас вместо датасета строк .jpeg, у вас датасет тензоров.

Чтобы сериализовать это в файл TFRecord сперва сконвертируйте датасет тензоров в датасет строк:

ds = image_ds.map(tf.io.serialize_tensor)
ds
<MapDataset shapes: (), types: tf.string>
tfrec = tf.data.experimental.TFRecordWriter('images.tfrec')
tfrec.write(ds)

С кешированным препроцессингом данные могут быть выгружены из TFrecord файла очень эффективно - не забудьте только десериализовать тензор перед использованием:

ds = tf.data.TFRecordDataset('images.tfrec')

def parse(x):
  result = tf.io.parse_tensor(x, out_type=tf.float32)
  result = tf.reshape(result, [192, 192, 3])
  return result

ds = ds.map(parse, num_parallel_calls=AUTOTUNE)
ds
<ParallelMapDataset shapes: (192, 192, 3), types: tf.float32>

Сейчас добавьте метки и примените те же стандартные операции, что и ранее:

ds = tf.data.Dataset.zip((ds, label_ds))
ds = ds.apply(
  tf.data.experimental.shuffle_and_repeat(buffer_size=image_count))
ds=ds.batch(BATCH_SIZE).prefetch(AUTOTUNE)
ds
<PrefetchDataset shapes: ((None, 192, 192, 3), (None,)), types: (tf.float32, tf.int64)>
timeit(ds)
........................
231.0 batches: 2.124154567718506 s
3479.97274 Images/s
Total time: 8.349011898040771s