In [ ]:
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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

Note: Вся информация в этом разделе переведена с помощью русскоговорящего Tensorflow сообщества на общественных началах. Поскольку этот перевод не является официальным, мы не гарантируем что он на 100% аккуратен и соответствует официальной документации на английском языке. Если у вас есть предложение как исправить этот перевод, мы будем очень рады увидеть pull request в tensorflow/docs репозиторий GitHub. Если вы хотите помочь сделать документацию по Tensorflow лучше (сделать сам перевод или проверить перевод подготовленный кем-то другим), напишите нам на docs-ru@tensorflow.org list.

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

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

Setup


In [ ]:
import tensorflow as tf

In [ ]:
AUTOTUNE = tf.data.experimental.AUTOTUNE

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

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

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


In [ ]:
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)

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


In [ ]:
for item in data_root.iterdir():
  print(item)

In [ ]:
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

In [ ]:
all_image_paths[:10]

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

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


In [ ]:
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)

In [ ]:
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])

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

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

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


In [ ]:
label_names = sorted(item.name for item in data_root.glob('*/') if item.is_dir())
label_names

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


In [ ]:
label_to_index = dict((name, index) for index, name in enumerate(label_names))
label_to_index

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


In [ ]:
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])

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

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


In [ ]:
img_path = all_image_paths[0]
img_path

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


In [ ]:
img_raw = tf.io.read_file(img_path)
print(repr(img_raw)[:100]+"...")

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


In [ ]:
img_tensor = tf.image.decode_image(img_raw)

print(img_tensor.shape)
print(img_tensor.dtype)

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


In [ ]:
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())

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


In [ ]:
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

In [ ]:
def load_and_preprocess_image(path):
  image = tf.io.read_file(path)
  return preprocess_image(image)

In [ ]:
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.

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


In [ ]:
path_ds = tf.data.Dataset.from_tensor_slices(all_image_paths)

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


In [ ]:
print(path_ds)

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


In [ ]:
image_ds = path_ds.map(load_and_preprocess_image, num_parallel_calls=AUTOTUNE)

In [ ]:
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()

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

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


In [ ]:
label_ds = tf.data.Dataset.from_tensor_slices(tf.cast(all_image_labels, tf.int64))

In [ ]:
for label in label_ds.take(10):
  print(label_names[label.numpy()])

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


In [ ]:
image_label_ds = tf.data.Dataset.zip((image_ds, label_ds))

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


In [ ]:
print(image_label_ds)

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


In [ ]:
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

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

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

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

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


In [ ]:
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

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

  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:


In [ ]:
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

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

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

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

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


In [ ]:
mobile_net = tf.keras.applications.MobileNetV2(input_shape=(192, 192, 3), include_top=False)
mobile_net.trainable=False

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

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

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


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

keras_ds = ds.map(change_range)

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

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


In [ ]:
# Датасету может понадобиться несколько секунд для старта пока заполняется буфер перемешивания.
image_batch, label_batch = next(iter(keras_ds))

In [ ]:
feature_map_batch = mobile_net(image_batch)
print(feature_map_batch.shape)

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


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

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


In [ ]:
logit_batch = model(image_batch).numpy()

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

print("Shape:", logit_batch.shape)

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


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

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


In [ ]:
len(model.trainable_variables)

In [ ]:
model.summary()

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

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


In [ ]:
steps_per_epoch=tf.math.ceil(len(all_image_paths)/BATCH_SIZE).numpy()
steps_per_epoch

In [ ]:
model.fit(ds, epochs=1, steps_per_epoch=3)

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

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

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

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


In [ ]:
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))

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


In [ ]:
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

In [ ]:
timeit(ds)

Кеш

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

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


In [ ]:
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

In [ ]:
timeit(ds)

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


In [ ]:
timeit(ds)

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


In [ ]:
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

In [ ]:
timeit(ds)

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


In [ ]:
timeit(ds)

Файл TFRecord

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

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

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


In [ ]:
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, которую вы задали ранее:


In [ ]:
image_ds = tf.data.TFRecordDataset('images.tfrec').map(preprocess_image)

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


In [ ]:
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

In [ ]:
timeit(ds)

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

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

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


In [ ]:
paths_ds = tf.data.Dataset.from_tensor_slices(all_image_paths)
image_ds = paths_ds.map(load_and_preprocess_image)
image_ds

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

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


In [ ]:
ds = image_ds.map(tf.io.serialize_tensor)
ds

In [ ]:
tfrec = tf.data.experimental.TFRecordWriter('images.tfrec')
tfrec.write(ds)

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


In [ ]:
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

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


In [ ]:
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

In [ ]:
timeit(ds)