python

Обучение и оценка модели с Keras

  • среда, 5 февраля 2020 г. в 00:24:55
https://habr.com/ru/post/485890/
  • Python
  • Big Data
  • Машинное обучение
  • Искусственный интеллект
  • TensorFlow




Это руководство охватывает обучение, оценку и прогнозирование (выводы) моделей в TensorFlow 2.0 в двух общих ситуациях:

  • При использовании встроенных API для обучения и валидации (таких как model.fit(), model.evaluate(), model.predict()). Этому посвящен раздел «Использование встроенных циклов обучения и оценки»
  • При написании кастомных циклов с нуля с использованием eager execution и объекта GradientTape. Эти вопросы рассматриваются в разделе «Написание собственных циклов обучения и оценки с нуля».

В целом, независимо от того, используете ли вы встроенные циклы или пишете свои собственные, обучение и оценка моделей работает строго одинаково для всех видов моделей Keras: Sequential моделей, созданных с помощью Functional API, и написанных с нуля с использованием субклассирования.

Установка


from __future__ import absolute_import, division, print_function, unicode_literals

import tensorflow as tf

tf.keras.backend.clear_session()  # # Для простого сброса состояния ноутбука.


Часть I: Использование встроенных циклов обучения и оценки


При передаче данных во встроенные циклы обучения модели вы должны использовать массивы Numpy (если ваши данные малы и умещаются в памяти), либо объекты Dataset tf.data. В следующих нескольких параграфах мы будем использовать набор данных MNIST в качестве массива Numpy, чтобы показать, как использовать оптимизаторы, функции потерь и метрики.

Обзор API: первый полный пример


Давайте рассмотрим следующую модель (будем строим ее с помощью Functional API, но она может быть и Sequential или субклассированной моделью):

from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(784,), name='digits')
x = layers.Dense(64, activation='relu', name='dense_1')(inputs)
x = layers.Dense(64, activation='relu', name='dense_2')(x)
outputs = layers.Dense(10, activation='softmax', name='predictions')(x)

model = keras.Model(inputs=inputs, outputs=outputs)

Вот как выглядит типичный полный процесс работы, состоящий из обучения, проверки на отложенных данных, сгенерированных из исходных данных обучения, и, наконец, оценки на тестовых данных:

# Загрузим учебный датасет для этого примера
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()

# Предобработаем данные (это массивы Numpy)
x_train = x_train.reshape(60000, 784).astype('float32') / 255
x_test = x_test.reshape(10000, 784).astype('float32') / 255

y_train = y_train.astype('float32')
y_test = y_test.astype('float32')

# Зарезервируем 10,000 примеров для валидации
x_val = x_train[-10000:]
y_val = y_train[-10000:]
x_train = x_train[:-10000]
y_train = y_train[:-10000]

# Укажем конфигурацию обучения (оптимизатор, функция потерь, метрики)
model.compile(optimizer=keras.optimizers.RMSprop(),  # Optimizer
              # Минимизируемая функция потерь
              loss=keras.losses.SparseCategoricalCrossentropy(),
              # Список метрик для мониторинга
              metrics=[keras.metrics.SparseCategoricalAccuracy()])

# Обучим модель разбив данные на "пакеты"
# размером "batch_size", и последовательно итерируя
# весь датасет заданное количество "эпох"
print('# Обучаем модель на тестовых данных')
history = model.fit(x_train, y_train,
                    batch_size=64,
                    epochs=3,
                    # Мы передаем валидационные данные для
                    # мониторинга потерь и метрик на этих данных
                    # в конце каждой эпохи
                    validation_data=(x_val, y_val))

# Возвращаемый объект "history" содержит записи
# значений потерь и метрик во время обучения
print('\nhistory dict:', history.history)

# Оценим модель на тестовых данных, используя "evaluate"
print('\n# Оцениваем на тестовых данных')
results = model.evaluate(x_test, y_test, batch_size=128)
print('test loss, test acc:', results)

# Сгенерируем прогнозы (вероятности - выходные данные последнего слоя)
# на новых данных с помощью "predict"
print('\n# Генерируем прогнозы для 3 образцов')
predictions = model.predict(x_test[:3])
print('размерность прогнозов:', predictions.shape)

Определение потерь, метрик и оптимизатора


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

Вам нужно передать их в модель в качестве аргументов метода compile():

model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
              loss=keras.losses.SparseCategoricalCrossentropy(),
              metrics=[keras.metrics.SparseCategoricalAccuracy()])

Аргумент metrics задается в виде списка — ваша модель может иметь любое количество метрик.

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

Обратите внимание, что часто функции потерь и метрики задаются с помощью строковых идентификаторов:

model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
              loss='sparse_categorical_crossentropy',
              metrics=['sparse_categorical_accuracy'])

Для последующего переиспользования поместим определение нашей модели и шаг компиляции в функции; мы будем вызывать их несколько раз в разных примерах этого руководства.

def get_uncompiled_model():
  inputs = keras.Input(shape=(784,), name='digits')
  x = layers.Dense(64, activation='relu', name='dense_1')(inputs)
  x = layers.Dense(64, activation='relu', name='dense_2')(x)
  outputs = layers.Dense(10, activation='softmax', name='predictions')(x)
  model = keras.Model(inputs=inputs, outputs=outputs)
  return model

def get_compiled_model():
  model = get_uncompiled_model()
  model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
              loss='sparse_categorical_crossentropy',
              metrics=['sparse_categorical_accuracy'])
  return model

Вам доступно множество встроенных оптимизаторов, функций потерь и метрик


Как правило, вам не нужно создавать с нуля собственные функции потерь, метрики, или оптимизаторы, поскольку то, что вам нужно, скорее всего, уже является частью Keras API:

Оптимизаторы:

  • SGD() (с или без momentum)
  • RMSprop()
  • Adam()
  • и т.д.

Функции потерь:

  • MeanSquaredError()
  • KLDivergence()
  • CosineSimilarity()
  • и т.д.

Метрики:

  • AUC()
  • Precision()
  • Recall()
  • и т.д.

Кастомные функции потерь


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

def basic_loss_function(y_true, y_pred):
    return tf.math.reduce_mean(y_true - y_pred)

model.compile(optimizer=keras.optimizers.Adam(),
              loss=basic_loss_function)

model.fit(x_train, y_train, batch_size=64, epochs=3)

Если вам нужна функция потерь у которой есть иные параметры кроме y_true и y_pred, вы можете субклассировать класс tf.keras.losses.Loss и реализовать следующие два метода:

  • __init__(self) — чтобы принять параметры, передаваемые при вызове вашей функции потерь
  • call(self, y_true, y_pred) — чтобы использовать ответы (y_true) и предсказания модели (y_pred) для вычисления потерь модели

Параметры передаваемые в __init__() могут быть использованы во время call() при вычислении потерь.

Следующий пример показывает как реализовать функцию потерь WeightedCrossEntropy которая вычисляет BinaryCrossEntropy, где потери конкретного класса или всей функции могут быть модифицированы при помощи скаляра.

class WeightedBinaryCrossEntropy(keras.losses.Loss):
    """
    Args:
      pos_weight: Скалярный вес для положительных меток функции потерь.
      weight: Скалярный вес для всей функции потерь.
      from_logits: Вычислять ли потери от логитов или вероятностей.
      reduction: Тип tf.keras.losses.Reduction для применения к функции потерь.
      name: Имя функции потерь.
    """
    def __init__(self, pos_weight, weight, from_logits=False,
                 reduction=keras.losses.Reduction.AUTO,
                 name='weighted_binary_crossentropy'):
        super(WeightedBinaryCrossEntropy, self).__init__(reduction=reduction,
                                                         name=name)
        self.pos_weight = pos_weight
        self.weight = weight
        self.from_logits = from_logits

    def call(self, y_true, y_pred):
        if not self.from_logits:
            # Вручную посчитаем взвешенную кросс-энтропию.
            # Формула следующая qz * -log(sigmoid(x)) + (1 - z) * -log(1 - sigmoid(x))
            # где z - метки, x - логиты, а q - веса.
            # Поскольку переданные значения от сигмоиды (предположим в этом случае)
            # sigmoid(x) будет заменено на y_pred

            # qz * -log(sigmoid(x)) 1e-6 добавляется как эпсилон, чтобы не передать нуль в логарифм
            x_1 = y_true * self.pos_weight * -tf.math.log(y_pred + 1e-6)

            # (1 - z) * -log(1 - sigmoid(x)). Добавляем эпсилон, чтобы не пропустить нуль в логарифм
            x_2 = (1 - y_true) * -tf.math.log(1 - y_pred + 1e-6)

            return tf.add(x_1, x_2) * self.weight 

        # Используем встроенную функцию
        return tf.nn.weighted_cross_entropy_with_logits(y_true, y_pred, self.pos_weight) * self.weight


model.compile(optimizer=keras.optimizers.Adam(),
              loss=WeightedBinaryCrossEntropy(0.5, 2))

model.fit(x_train, y_train, batch_size=64, epochs=3)

Кастомные метрики


Если вам нужны метрики не являющиеся частью API, вы можете легко создать кастомные метрики субклассировав класс Metric. Вам нужно реализовать 4 метода:

  • __init__(self), в котором вы создадите переменные состояния для своей метрики.
  • update_state(self, y_true, y_pred, sample_weight=None), который использует ответы y_true и предсказания модели y_pred для обновления переменных состояния.
  • result(self), использующий переменные состояния для вычисления конечного результата.
  • reset_states(self), который переинициализирует состояние метрики.

Обновление состояния и вычисление результатов хранятся отдельно (в update_state() и result() соответственно), потому что в некоторых случаях вычисление результатов может быть очень дорогим и будет выполняться только периодически.

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

class CategoricalTruePositives(keras.metrics.Metric):

    def __init__(self, name='categorical_true_positives', **kwargs):
      super(CategoricalTruePositives, self).__init__(name=name, **kwargs)
      self.true_positives = self.add_weight(name='tp', initializer='zeros')

    def update_state(self, y_true, y_pred, sample_weight=None):
      y_pred = tf.reshape(tf.argmax(y_pred, axis=1), shape=(-1, 1))
      values = tf.cast(y_true, 'int32') == tf.cast(y_pred, 'int32')
      values = tf.cast(values, 'float32')
      if sample_weight is not None:
        sample_weight = tf.cast(sample_weight, 'float32')
        values = tf.multiply(values, sample_weight)
      self.true_positives.assign_add(tf.reduce_sum(values))

    def result(self):
      return self.true_positives

    def reset_states(self):
      # Состояние метрики будет сброшено в начале каждой эпохи.
      self.true_positives.assign(0.)


model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
              loss=keras.losses.SparseCategoricalCrossentropy(),
              metrics=[CategoricalTruePositives()])
model.fit(x_train, y_train,
          batch_size=64,
          epochs=3)

Обработка функций потерь и метрик, не соответствующих стандартной сигнатуре


Подавляющее большинство потерь и метрик может быть вычислено из y_true и y_pred, где y_pred — вывод вашей модели. Но не все. Например, потери регуляризации могут требовать только активацию слоя (в этом случае нет целевых значений), и эта активация может не являться выходом модели.

В таких случаях вы можете вызвать self.add_loss(loss_value) из метода call кастомного слоя. Вот простой пример, который добавляет регуляризацию активности (отметим что регуляризация активности встроена во все слои Keras — этот слой используется только для приведения конкретного примера):

class ActivityRegularizationLayer(layers.Layer):

  def call(self, inputs):
    self.add_loss(tf.reduce_sum(inputs) * 0.1)
    return inputs  # Pass-through layer.

inputs = keras.Input(shape=(784,), name='digits')
x = layers.Dense(64, activation='relu', name='dense_1')(inputs)

# Вставим регуляризацию активности в качестве слоя
x = ActivityRegularizationLayer()(x)

x = layers.Dense(64, activation='relu', name='dense_2')(x)
outputs = layers.Dense(10, activation='softmax', name='predictions')(x)

model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
              loss='sparse_categorical_crossentropy')

# Полученные потери будут намного больше чем раньше
# из-за компонента регуляризации.
model.fit(x_train, y_train,
          batch_size=64,
          epochs=1)

Вы можете сделать то же самое для логирования значений метрик:

class MetricLoggingLayer(layers.Layer):

  def call(self, inputs):
    # Аргумент `aggregation` определяет
    # как аггрегировать попакетные значения
    # в каждой эпохе:
    # в этом случае мы просто усредняем их.
    self.add_metric(keras.backend.std(inputs),
                    name='std_of_activation',
                    aggregation='mean')
    return inputs  # Проходной слой.


inputs = keras.Input(shape=(784,), name='digits')
x = layers.Dense(64, activation='relu', name='dense_1')(inputs)

# Вставка логирования std в качестве слоя.
x = MetricLoggingLayer()(x)

x = layers.Dense(64, activation='relu', name='dense_2')(x)
outputs = layers.Dense(10, activation='softmax', name='predictions')(x)

model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
              loss='sparse_categorical_crossentropy')
model.fit(x_train, y_train,
          batch_size=64,
          epochs=1)

В Functional API, вы можете также вызвать model.add_loss(loss_tensor), или model.add_metric(metric_tensor, name, aggregation).

Вот простой пример:

inputs = keras.Input(shape=(784,), name='digits')
x1 = layers.Dense(64, activation='relu', name='dense_1')(inputs)
x2 = layers.Dense(64, activation='relu', name='dense_2')(x1)
outputs = layers.Dense(10, activation='softmax', name='predictions')(x2)
model = keras.Model(inputs=inputs, outputs=outputs)

model.add_loss(tf.reduce_sum(x1) * 0.1)

model.add_metric(keras.backend.std(x1),
                 name='std_of_activation',
                 aggregation='mean')

model.compile(optimizer=keras.optimizers.RMSprop(1e-3),
              loss='sparse_categorical_crossentropy')
model.fit(x_train, y_train,
          batch_size=64,
          epochs=1)

Автоматическое выделение валидационного отложенного множества


В первом полном примере, как вы видели, мы использовали аргумент validation_data для передачи кортежа массивов Numpy (x_val, y_val) в модель для оценки валидационных потерь и метрик в конце каждой эпохи.

Вот другая опция: аргумент validation_split позволяет вам автоматически зарезервировать часть ваших тренировочных данных для валидации. Значением аргумента является доля данных, которые должны быть зарезервированы для валидации, поэтому значение должно быть больше 0 и меньше 1. Например, validation_split=0.2 значит «используйте 20% данных для валидации», а validation_split=0.6 значит «используйте 60% данных для валидации».

Валидация вычисляется взятием последних x% записей массивов полученных вызовом fit, перед любым перемешиванием.

Вы можете использовать validation_split только когда обучаете модель данными Numpy.

model = get_compiled_model()
model.fit(x_train, y_train, batch_size=64, validation_split=0.2, epochs=1, steps_per_epoch=1)

Обучение и оценка с tf.data Dataset


В последних нескольких параграфах вы видели, как обрабатывать потери, метрики и оптимизаторы, и посмотрели, как использовать аргументы validation_data и validation_split в fit, когда ваши данные передаются в виде массивов Numpy.

Давайте теперь рассмотрим случай, когда ваши данные поступают в форме Dataset tf.data.

tf.data API это набор утилит в TensorFlow 2.0 для загрузки и предобработки данных быстрым и масштабируемым способом.

Вы можете передать экземпляр Dataset напрямую в методы fit(), evaluate() и predict():

model = get_compiled_model()

# Сперва давайте создадим экземпляр тренировочного Dataset.
# Для нашего примера мы будем использовать те же данные MNIST что и ранее.
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# Перемешаем и нарежем набор данных.
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)

# Сейчас получим тестовый датасет.
test_dataset = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_dataset = test_dataset.batch(64)

# Поскольку датасет уже позаботился о разбивке на пакеты,
# мы не передаем аргумент `batch_size`.
model.fit(train_dataset, epochs=3)

# Вы можете также оценить модель или сделать прогнозы на датасете.
print('\n# Оценка')
model.evaluate(test_dataset)

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

Если вы хотите учиться только на определенном количестве пакетов из этого Dataset, вы можете передать аргумент `steps_per_epoch`, который указывает, сколько шагов обучения должна выполнить модель, используя этот Dataset, прежде чем перейти к следующей эпохе.

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

model = get_compiled_model()

# Подготовка учебного датасета
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)

# Использовать только 100 пакетов за эпоху (это 64 * 100 примеров)
model.fit(train_dataset.take(100), epochs=3)

Использование валидационного датасета


Вы можете передать экземпляр Dataset как аргумент validation_data в fit:

model = get_compiled_model()

# Подготовим учебный датасет
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)

# Подготовим валидационный датасет
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_dataset = val_dataset.batch(64)

model.fit(train_dataset, epochs=3, validation_data=val_dataset)

В конце каждой эпохи модель будет проходить по валидационному Dataset и вычислять потери и метрики на валидации.

Если вы хотите запустить проверку только на определенном количестве пакетов из этого Dataset, вы можете передать аргумент validation_steps, который указывает, сколько шагов валидации должна выполнить модель до прерывания валидации и перехода к следующей эпохе:

model = get_compiled_model()

# Подготовка тренировочных данных
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)

# Подготовка валидационных данных
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_dataset = val_dataset.batch(64)

model.fit(train_dataset, epochs=3,
          # Запускаем валидацию только на первых 10 пакетах датасета
          # используя аргумент `validation_steps`
          validation_data=val_dataset, validation_steps=10)


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

Аргумент validation_split (генерирующий отложенную выборку из тренировочных данных) не поддерживается при обучении на объектах Dataset, поскольку для этого требуется возможность индексирования элементов, что в общем невозможно в Dataset API.

Другие поддерживаемые форматы входных данных


Кроме массивов Numpy и TensorFlow Dataset-ов, можно обучить модель Keras с использованием датафрейма Pandas, или с генераторами Python которые выдают значения пакетами.

В общем, мы рекомендуем вам использовать входные данные Numpy если их количество невелико и помещается в памяти, и Dataset-ы в других случаях.

Использование весов для примеров и классов


Кроме входных данных и меток модели можно передавать веса примеров и веса классов при использовании fit:

  • При обучении на данных Numpy: с помощью аргументов `sample_weight` и `class_weight`.
  • При обучении на Dataset-ах: если Dataset вернет кортеж `(input_batch, target_batch, sample_weight_batch)` .

Массив «sample weights» это массив чисел которые определяют какой вес придать каждому элементу в пакете при вычислении значения потерь. Это свойство обычно используется в несбалансированных задачах классификации (суть в том, чтобы придать больший вес редко встречающимся классам). Когда используемые веса равны единицам и нулям, массив можно использовать в качестве маски для функции потерь (полностью исключая вклад определенных элементов в общее значение потерь).

Словарь «class weights» является более специфичным экземпляром той же концепции: он сопоставляет индексам классов веса которые должны быть использованы для элементов принадлежащих этому классу. Например, если класс «0» представлен втрое меньше чем класс «1» в ваших данных, вы можете использовать class_weight={0: 1., 1: 0.5}.

Вот примеры Numpy весов классов и весов элементов позволяющих придать большее значение корректной классификации класса #5 (соответствующий цифре «5» в датасете MNIST).

import numpy as np

class_weight = {0: 1., 1: 1., 2: 1., 3: 1., 4: 1.,
                # Установим вес "2" для класса "5",
                # сделав этот класс в 2x раз важнее
                5: 2.,
                6: 1., 7: 1., 8: 1., 9: 1.}
print('Обучение с весом класса')
model.fit(x_train, y_train,
          class_weight=class_weight,
          batch_size=64,
          epochs=4)

# Вот тот же пример использующий `sample_weight`:
sample_weight = np.ones(shape=(len(y_train),))
sample_weight[y_train == 5] = 2.
print('\nОбучение с весом класса')

model = get_compiled_model()
model.fit(x_train, y_train,
          sample_weight=sample_weight,
          batch_size=64,
          epochs=4)

Вот соответствующий Dataset пример:

sample_weight = np.ones(shape=(len(y_train),))
sample_weight[y_train == 5] = 2.

# Создадим  Dataset включающий веса элементов
# (3-тий элемент в возвращаемом кортеже).
train_dataset = tf.data.Dataset.from_tensor_slices(
    (x_train, y_train, sample_weight))

# Перемешаем и нарежем датасет.
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)

model = get_compiled_model()
model.fit(train_dataset, epochs=3)

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


В предыдущих примерах, мы рассматривали модель с единственным входом (тензор размера (764,)) и одним выходом (тензор прогнозов размера (10,)). Но как насчет моделей, у которых есть несколько входов или выходов?

Рассмотрим следующую модель, в которой на входными данными являются изображения размера (32, 32, 3) (это (height, width, channels)) и временные ряды размера (None, 10) (это (timesteps, features)). У нашей модели будет два выхода вычисленных из комбинации этих входов: «score» (размерности (1,)) и вероятностное распределение по пяти классам (размерности (5,)).

from tensorflow import keras
from tensorflow.keras import layers

image_input = keras.Input(shape=(32, 32, 3), name='img_input')
timeseries_input = keras.Input(shape=(None, 10), name='ts_input')

x1 = layers.Conv2D(3, 3)(image_input)
x1 = layers.GlobalMaxPooling2D()(x1)

x2 = layers.Conv1D(3, 3)(timeseries_input)
x2 = layers.GlobalMaxPooling1D()(x2)

x = layers.concatenate([x1, x2])

score_output = layers.Dense(1, name='score_output')(x)
class_output = layers.Dense(5, activation='softmax', name='class_output')(x)

model = keras.Model(inputs=[image_input, timeseries_input],
                    outputs=[score_output, class_output])

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

keras.utils.plot_model(model, 'multi_input_and_output_model.png', show_shapes=True)



Во время компиляции мы можем указать разные функции потерь для разных выходов, передав функции потерь в виде списка:

model.compile(
    optimizer=keras.optimizers.RMSprop(1e-3),
    loss=[keras.losses.MeanSquaredError(),
          keras.losses.CategoricalCrossentropy()])

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

Аналогично для метрик:

model.compile(
    optimizer=keras.optimizers.RMSprop(1e-3),
    loss=[keras.losses.MeanSquaredError(),
          keras.losses.CategoricalCrossentropy()],
    metrics=[[keras.metrics.MeanAbsolutePercentageError(),
              keras.metrics.MeanAbsoluteError()],
             [keras.metrics.CategoricalAccuracy()]])

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

model.compile(
    optimizer=keras.optimizers.RMSprop(1e-3),
    loss={'score_output': keras.losses.MeanSquaredError(),
          'class_output': keras.losses.CategoricalCrossentropy()},
    metrics={'score_output': [keras.metrics.MeanAbsolutePercentageError(),
                              keras.metrics.MeanAbsoluteError()],
             'class_output': [keras.metrics.CategoricalAccuracy()]})

Мы рекомендуем использовать имена и словари если у вас более 2 выходов.

Имеется возможность присвоить разные веса разным функциям потерь (например, в нашем примере мы можем захотеть отдать предпочтение потере «score», увеличив в 2 раза важность потери класса), используя аргумент loss_weights:

model.compile(
    optimizer=keras.optimizers.RMSprop(1e-3),
    loss={'score_output': keras.losses.MeanSquaredError(),
          'class_output': keras.losses.CategoricalCrossentropy()},
    metrics={'score_output': [keras.metrics.MeanAbsolutePercentageError(),
                              keras.metrics.MeanAbsoluteError()],
             'class_output': [keras.metrics.CategoricalAccuracy()]},
    loss_weights={'score_output': 2., 'class_output': 1.})

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

# Функции потерь списком
model.compile(
    optimizer=keras.optimizers.RMSprop(1e-3),
    loss=[None, keras.losses.CategoricalCrossentropy()])

# Функции потерь словарем
model.compile(
    optimizer=keras.optimizers.RMSprop(1e-3),
    loss={'class_output': keras.losses.CategoricalCrossentropy()})

Передача данных в модель с несколькими входами и выходами в fit происходит аналогично определению функции потерь в compile: вы можете передать списки массивов Numpy (совпадающие 1:1 с выходами на которых есть функции потерь) или словари сопоставляющие имена выходов массивам Numpy тренировочных данных.

model.compile(
    optimizer=keras.optimizers.RMSprop(1e-3),
    loss=[keras.losses.MeanSquaredError(),
          keras.losses.CategoricalCrossentropy()])

# Сгенерируем случайные Numpy данные
img_data = np.random.random_sample(size=(100, 32, 32, 3))
ts_data = np.random.random_sample(size=(100, 20, 10))
score_targets = np.random.random_sample(size=(100, 1))
class_targets = np.random.random_sample(size=(100, 5))

# Передаем данные в модель в виде списка
model.fit([img_data, ts_data], [score_targets, class_targets],
          batch_size=32,
          epochs=3)

# Передаем данные в модель в виде словаря
model.fit({'img_input': img_data, 'ts_input': ts_data},
          {'score_output': score_targets, 'class_output': class_targets},
          batch_size=32,
          epochs=3)

Ниже пример для Dataset: аналогично массивам Numpy, Dataset должен возвращать кортеж словарей.

train_dataset = tf.data.Dataset.from_tensor_slices(
    ({'img_input': img_data, 'ts_input': ts_data},
     {'score_output': score_targets, 'class_output': class_targets}))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)

model.fit(train_dataset, epochs=3)

Использование колбеков


Колбеки в Keras это объекты которые вызываются в разных местах во время обучения (в начале эпохи, в конце пакета, в конце эпохи, и т.д.) и которые могут быть использованы для реализации такого поведения, как:

Выполнение валидации в различных точках во время обучения (кроме встроенной валидации в конце каждой эпохи)

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

Колбеки могут переданы списком при вызове fit:

model = get_compiled_model()

callbacks = [
    keras.callbacks.EarlyStopping(
        # Прекратить обучение если `val_loss` больше не улучшается
        monitor='val_loss',
        # "больше не улучшается" определим как "не лучше чем 1e-2 и меньше"
        min_delta=1e-2,
        # "больше не улучшается" далее определим как "как минимум в течение 2 эпох"
        patience=2,
        verbose=1)
]
model.fit(x_train, y_train,
          epochs=20,
          batch_size=64,
          callbacks=callbacks,
          validation_split=0.2)

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


  • ModelCheckpoint: Периодических сохраняет модель.
  • EarlyStopping: Останавливает обучение, в том случае когда валидационная метрика прекращает улучшаться.
  • TensorBoard: периодически пишет логи модели которые могут быть визуализированы в TensorBoard (больше деталей в разделе \«Визуализация\»).
  • CSVLogger: стримит значения потерь и метрик в файл CSV.
  • и т.д.

Написание собственного колбека


Вы можете создать собственный колбек расширив базовый класс keras.callbacks.Callback. Колбек имеет доступ к ассоциированной модели посредством свойства класса self.model.

Вот простой пример сохранения списка значений попакетных потерь во время обучения:

class LossHistory(keras.callbacks.Callback):

    def on_train_begin(self, logs):
        self.losses = []

    def on_batch_end(self, batch, logs):
        self.losses.append(logs.get('loss'))

Сохранение контрольных точек моделей


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

Проще всего сделать это с помощью колбека ModelCheckpoint:

model = get_compiled_model()

callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath='mymodel_{epoch}.h5',
        # Путь по которому нужно сохранить модель
        # Два параметра ниже значат что мы перезапишем
        # текущий чекпоинт в том и только в том случае, когда
        # улучится значение `val_loss`.
        save_best_only=True,
        monitor='val_loss',
        verbose=1)
]
model.fit(x_train, y_train,
          epochs=3,
          batch_size=64,
          callbacks=callbacks,
          validation_split=0.2)

Вы также можете написать собственный колбек для сохранения и восстановления моделей.

Использование графиков скорости обучения


Обычным паттерном при тренировке моделей глубокого обучения является постепенное сокращение скорости обучения по мере тренировки модели. Это общеизвестно как «снижение скорости обучения».

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

Передача расписания оптимизатору


Вы можете легко использовать график статического снижения скорости обучения передав объект расписания в качестве аргумента learning_rate вашему оптимизатору:

initial_learning_rate = 0.1
lr_schedule = keras.optimizers.schedules.ExponentialDecay(
    initial_learning_rate,
    decay_steps=100000,
    decay_rate=0.96,
    staircase=True)

optimizer = keras.optimizers.RMSprop(learning_rate=lr_schedule)

Доступно несколько встроенных схем снижения скорости обучения: ExponentialDecay, PiecewiseConstantDecay, PolynomialDecay и InverseTimeDecay.

Использование колбеков для реализации графика динамического изменения скорости обучения


Расписание динамического изменения скорости обучения (например, уменьшение скорости обучения, когда потери при валидации более не улучшаются) не может быть достигнуто с этими объектами расписания, поскольку оптимизатор не имеет доступа к показателям валидации.

Однако колбеки имеют доступ ко всем метрикам, включая метрики валидации! Поэтому, вы можете достичь этого паттерна, используя колбек, который изменяет текущую скорость обучения на оптимизаторе. Фактически, есть и встроенный колбек ReduceLROnPlateau.

Визуализация потерь и метрик во время обучения


Лучший способ следить за вашей моделью во время обучения — это использовать TensorBoard — приложение на основе браузера, которое вы можете запустить локально и которое предоставляет вам:

  • Живые графики функции потерь и метрик для обучения и оценки
  • (опционально) Визуализации гистограмм активаций ваших слоев
  • (опционально) 3D-визуализации пространств вложения, изученных вашими Embedding слоями

Если вы установили TensorFlow с помощью pip, вы можете запустить TensorBoard из командной строки:

tensorboard --logdir=/full_path_to_your_logs

Использование колбека TensorBoard


Самый легкий способ использовать TensorBoard с моделью Keras и методом fit — это колбек TensorBoard.

В простейшем случае просто укажите, куда вы хотите, чтобы колбек писал логи, и все готово:

tensorboard_cbk = keras.callbacks.TensorBoard(log_dir='/full_path_to_your_logs')
model.fit(dataset, epochs=10, callbacks=[tensorboard_cbk])

Колбек TensorBoard имеет много полезных опций, в том числе, писать ли лог вложений, гистограмм и как часто писать логи:

keras.callbacks.TensorBoard(
  log_dir='/full_path_to_your_logs',
  histogram_freq=0,  # Как часто писать лог визуализаций гистограмм
  embeddings_freq=0,  # Как часто писать лог визуализаций вложений
  update_freq='epoch')  # Как часто писать логи (по умолчанию: однажды за эпоху)

Часть II: Написание собственных циклов обучения и оценки с нуля


Если вам нужен более низкий уровень для ваших циклов обучения и оценки, чем тот что дают fit() и evaluate(), вы должны написать свои собственные. Это на самом деле довольно просто! Но вы должны быть готовы к большему количеству отладки.

Использование GradientTape: первый полный пример


Вызов модели внутри области видимости GradientTape позволяет получить градиенты обучаемых весов слоя относительно значения потерь. Используя экземпляр оптимизатора, вы можете использовать эти градиенты для обновления переменных (которые можно получить с помощью model.trainable_weights).

Давайте переиспользуем нашу первоначальную модель MNIST из первой части и обучим ее, используя мини-пакетный градиентный спуск с кастомным циклом обучения.

# Получим модель.
inputs = keras.Input(shape=(784,), name='digits')
x = layers.Dense(64, activation='relu', name='dense_1')(inputs)
x = layers.Dense(64, activation='relu', name='dense_2')(x)
outputs = layers.Dense(10, name='predictions')(x)
model = keras.Model(inputs=inputs, outputs=outputs)

# Создадим экземпляр оптимизатора.
optimizer = keras.optimizers.SGD(learning_rate=1e-3)
# Instantiate a loss function.
loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

# Подготовим тренировочный датасет.
batch_size = 64
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(batch_size)

# Итерируем по эпохам.
epochs = 3
for epoch in range(epochs):
  print('Начинаем эпоху %d' % (epoch,))

  # Итерируем по пакетам в датасете.
  for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):

    # Откроем GradientTape чтобы записать операции
    # выполняемые во время прямого прохода, включающего автодифференцирование.
    with tf.GradientTape() as tape:

      # Запустим прямой проход слоя.
      # Операции применяемые слоем к своим
      # входным данным будут записаны
      # на GradientTape.
      logits = model(x_batch_train, training=True)  # Logits for this minibatch

      # Вычислим значение потерь для этого минибатча.
      loss_value = loss_fn(y_batch_train, logits)

    # Используем gradient tape для автоматического извлечения градиентов
    # обучаемых переменных относительно потерь.
    grads = tape.gradient(loss_value, model.trainable_weights)

    # Выполним один шаг градиентного спуска обновив
    # значение переменных минимизирующих потери.
    optimizer.apply_gradients(zip(grads, model.trainable_weights))

    # Пишем лог каждые 200 пакетов.
    if step % 200 == 0:
        print('Потери на обучении (для одного пакета) на шаге %s: %s' % (step, float(loss_value)))
        print('Уже увидено: %s примеров' % ((step + 1) * 64))

Низкоуровневая обработка метрик


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

  • Создайте экземпляр метрики в начале цикла,
  • Вызовите metric.update_state() после каждого пакета
  • Вызовите metric.result() когда вам нужно показать текущее значение метрики
  • Вызовите metric.reset_states() когда вам нужно очистить состояние метрики (обычно в конце каждой эпохи)

Давайте используем это знание, чтобы посчитать SparseCategoricalAccuracy на валидационных данных в конце каждой эпохи:

# Получим модель
inputs = keras.Input(shape=(784,), name='digits')
x = layers.Dense(64, activation='relu', name='dense_1')(inputs)
x = layers.Dense(64, activation='relu', name='dense_2')(x)
outputs = layers.Dense(10, activation='softmax', name='predictions')(x)
model = keras.Model(inputs=inputs, outputs=outputs)

# Создадим экземпляр оптимизатора для обучения модели.
optimizer = keras.optimizers.SGD(learning_rate=1e-3)
# Создадим экземпляр функции потерь.
loss_fn = keras.losses.SparseCategoricalCrossentropy()

# Подготовим метрику.
train_acc_metric = keras.metrics.SparseCategoricalAccuracy()
val_acc_metric = keras.metrics.SparseCategoricalAccuracy()

# Подготовим тренировочный датасет.
batch_size = 64
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(batch_size)

# Подготовим валидационный датасет.
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_dataset = val_dataset.batch(64)


# Итерируем по эпохам.
epochs = 3
for epoch in range(epochs):
  print('Начало эпохи %d' % (epoch,))

  # Итерируем по пакетам в датасете.
  for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
    with tf.GradientTape() as tape:
      logits = model(x_batch_train)
      loss_value = loss_fn(y_batch_train, logits)
    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))

    # Обновляем метрику на обучении.
    train_acc_metric(y_batch_train, logits)

    # Пишем лог каждые 200 пакетов.
    if step % 200 == 0:
        print('Потери на обучении (за один пакет) на шаге %s: %s' % (step, float(loss_value)))
        print('Уже просмотрено: %s примеров' % ((step + 1) * 64))

  # Покажем метрики в конце каждой эпохи.
  train_acc = train_acc_metric.result()
  print('Accuracy на обучении за эпоху: %s' % (float(train_acc),))
  # Сбросим тренировочные метрики в конце каждой эпохи
  train_acc_metric.reset_states()

  # Запустим валидационный цикл в конце эпохи.
  for x_batch_val, y_batch_val in val_dataset:
    val_logits = model(x_batch_val)
    # Обновим валидационные метрики
    val_acc_metric(y_batch_val, val_logits)
  val_acc = val_acc_metric.result()
  val_acc_metric.reset_states()
  print('Accuracy на валидации: %s' % (float(val_acc),))

Низкоуровневая обработка дополнительных потерь


В предыдущем разделе вы видели, что для слоя можно добавить потери регуляризации, вызвав self.add_loss(value) в методе call.

В общем вы можете захотеть учесть эти потери в своих пользовательских циклах обучения (если только вы сами не написали модель и уже знаете, что она не создает таких потерь).

Вспомните пример из предыдущего раздела, где есть слой, который создает потери регуляризации:

class ActivityRegularizationLayer(layers.Layer):

  def call(self, inputs):
    self.add_loss(1e-2 * tf.reduce_sum(inputs))
    return inputs

inputs = keras.Input(shape=(784,), name='digits')
x = layers.Dense(64, activation='relu', name='dense_1')(inputs)
# Insert activity regularization as a layer
x = ActivityRegularizationLayer()(x)
x = layers.Dense(64, activation='relu', name='dense_2')(x)
outputs = layers.Dense(10, activation='softmax', name='predictions')(x)

model = keras.Model(inputs=inputs, outputs=outputs)

Когда вы вызываете модель так:

logits = model(x_train)

потери которые она создает во время прямого прохода добавляются в атрибут model.losses:

logits = model(x_train[:64])
print(model.losses)

Отслеживаемые потери сначала очищаются в начале модели __call__, поэтому вы увидите только потери, созданные во время текущего одного прямого прохода. Например, при повторном вызове модели и последующем запросе к losses отображаются только последние потери, создано во время последнего вызова:

logits = model(x_train[:64])
logits = model(x_train[64: 128])
logits = model(x_train[128: 192])
print(model.losses)

Чтобы учесть эти потери во время обучения, все, что вам нужно сделать, это модифицировать цикл обучения, добавив к полному значению потерь sum(model.losses):

optimizer = keras.optimizers.SGD(learning_rate=1e-3)

epochs = 3
for epoch in range(epochs):
  print('Начало эпохи %d' % (epoch,))

  for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
    with tf.GradientTape() as tape:
      logits = model(x_batch_train)
      loss_value = loss_fn(y_batch_train, logits)

      # Добавляем дополнительные потери, созданные во время прямого прохода:
      loss_value += sum(model.losses)

    grads = tape.gradient(loss_value, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))

    # Пишем лог каждые 200 пакетов.
    if step % 200 == 0:
        print('Ошибка на обучении (за один пакет) на шаге %s: %s' % (step, float(loss_value)))
        print('Просмотрено: %s примеров' % ((step + 1) * 64))

Это была последняя часть паззла! Вы достигли конца руководства.

Сейчас вы знаете все, что нужно об использовании встроенных циклов обучения и написании своих собственных с нуля.