Введение

Отказ от ответственности

Keras отлично справляется с абстрагированием низкоуровневых деталей создания нейронной сети, чтобы вы могли сосредоточиться на выполнении работы. Но если вы читаете это, вы, вероятно, обнаружили, что стандартные методы Keras не всегда могут быть использованы для изучения параметров вашей модели. Возможно, ваша модель имеет градиент, который нельзя рассчитать с помощью магии автодиффа, или ваша функция потерь не соответствует сигнатуре my_loss_fn(y_true, y_pred), упомянутой в документации Keras. Если вы нашли онлайн-документацию совершенно бесполезной, читайте дальше! У меня [надеюсь] есть все ответы, которые вы не смогли найти больше нигде.

Цели обучения

Я не обещаю прибить все мои объяснения; Я не считаю себя гуру TensorFlow/Keras. Все, что я здесь написал, основано на многих разных страницах документации TensorFlow/Keras и небольшой проверке исходного кода. Если у вас есть исправления или предложения по улучшению, я рекомендую вам оставить их в комментариях, чтобы все могли извлечь из этого пользу! Наконец, TensorFlow является товарным знаком Google LLC, и эта статья никоим образом не одобрена и не связана с Google LLC.

Что демонстрирует это руководство?

Следуя этому руководству, вы поймете, как создать собственный подкласс Keras Model, который использует пользовательский подкласс Keras Layer в Python. Вы сможете написать свои собственные функции потерь, которые http://archive.ics.uci.edu/ml не соответствуют сигнатуре, my_loss_fn(y_true, y_pred) описанной в документации Кераса. Вы также сможете использовать пользовательские градиенты в сочетании с алгоритмом автодифференциации (autodiff) для оптимизации обучаемых параметров модели.

Загрузить (или создать) набор данных

Это руководство в конечном итоге продемонстрирует, как вы все еще можете использовать пользовательские потери и пользовательские градиенты, не обязательно отказываясь от удобного keras.Model.fit метода обучения вашей нейронной сети.

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

Давайте начнем.

Создание пользовательских слоев Keras

Мы собираемся использовать набор данных Open Access german_credit_numeric, который можно загрузить с библиотекой tensorflow_datasets. Он состоит из 1000 примеров 24 характеристик, связанных с бинарной оценкой кредитного риска «хорошо» (1) или «плохо» (0).

import tensorflow as tf
import tensorflow_datasets as tfds

from typing import Optional


@tf.function
def sigmoid(x: tf.Tensor) -> tf.Tensor:
    return 1 / (1 + tf.exp(-x))


if __name__ == "__main__":

    ds = tfds.load("german_credit_numeric", split="train", as_supervised=True)
    ds = ds.shuffle(1000).batch(100).prefetch(tf.data.AUTOTUNE)

Создайте модель

Прежде чем мы построим модель, мы должны построить ее компоненты. Напомним, что модель будет состоять из плотного слоя, который преобразует наблюдаемые функции в нашем наборе данных в скрытое представление, которое будет служить входными данными для выходного слоя логистической регрессии. Чтобы продемонстрировать, как вы можете смешивать и сочетать пользовательские и предварительно созданные Keras Layers, мы будем использовать встроенный класс Keras keras.layers.Dense для построения первого слоя модели и создадим собственный пользовательский класс слоя для логистической регрессии путем создания подкласса keras.layers.Layer.

Давайте разберем этот пользовательский уровень логистики.

Метод get_config

Метод get_config не является строго обязательным для работы слоя, но он необходим, если вы хотите создать так называемый сериализуемый слой, который можно использовать с Функциональным API. Вы можете использовать метод get_config, чтобы получить атрибуты, необходимые для воссоздания слоя. См. документацию.

Метод build

Как и метод get_config, метод build не требуется для работы слоя, но это рекомендуемый способ определения обучаемых параметров слоя (т. е. весов, смещений). При использовании метода build создание параметра откладывается до первого вызова слоя с некоторыми входными данными. Метод build получает форму ввода (которая может быть неизвестна заранее) через аргумент input_shape и создает параметры слоя в соответствии с input_shape. Например, если аргумент inputs метода call слоя был кортежем тензоров, Keras присвоил бы input_shape массив из TensorShape объектов (по одному для каждого элемента входного тензора). Рекомендуется использовать метод построения, поскольку он позволяет создавать веса на лету, даже если вы заранее не знаете исходные формы.

Метод call

Это вычисляет прямой проход входных данных через слой. Здесь он возвращает
вероятностный вывод логистической регрессии.

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

features_spec, labels_spec = ds.element_spec
del labels_spec  # Not used

feature_inputs = tf.keras.Input(type_spec=features_spec, name="feature_inputs")
dense = tf.keras.layers.Dense(units=4, name="dense_layer")

Обратите внимание, что мы создали объект Input, feature_inputs, который ожидает векторный тензор с пятью компонентами (т. е. обучающий пример с 24 функциями). Слой dense будет вычислять скрытое представление входных данных всего с четырьмя функциями. Поскольку мы используем скрытое представление в качестве входных данных для логистической регрессии, слой Logistic будет иметь четыре обучаемых веса (по одному на входной объект), а также унитарное смещение. Также обратите внимание, что мы не создавали экземпляр слоя Logistic. Мы оставим это пользовательскому объекту Model.

Обучите модель

В этом суть руководства. Мы собираемся создать подкласс keras.Model с пользовательским циклом обучения, функцией потерь и градиентами.

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

Использование отрицательной логарифмической вероятности в качестве нашей пользовательской функции потерь создает некоторые проблемы в Keras. Во-первых, потеря вероятности отрицательного логарифма не обязательно соответствует сигнатуре my_loss_fn(y_true, y_pred), предложенной для пользовательских функций потерь в документации Keras; в нашем случае это функция входных признаков и целевых меток. Таким образом, мы не можем просто определить пользовательскую функцию потерь для отрицательного логарифмического правдоподобия и передать ее в качестве аргумента параметру loss функции Model.fit при обучении модели. Во-вторых, не все функции потерь, основанные на правдоподобии, будут иметь градиенты, которые можно «волшебным образом» решить с помощью алгоритма автодиффа, и в этом случае вам нужно будет указать TensorFlow, как именно вычислять градиенты. Когда функция потерь не соответствует предложенной Керасом подписи, все становится немного сложнее. Наконец, обычно есть некоторые ранние слои, которые изучают скрытое представление наблюдаемых функций ваших обучающих примеров, и если автодифф можно использовать для решения градиентов этих слоев, мы хотели бы использовать его, определяя пользовательские градиенты только при необходимости ( это большой труд!).

Давайте определим подкласс keras.Model, который преодолевает эти проблемы. Сначала я представлю код, а затем объясню его части.

Разбираем модель.

Метод __init__

Модель предназначена для приема раннего блока слоев nn_block, который изучает скрытое представление необработанных входных данных. В этом руководстве nn_block будет экземпляром keras.layers.Dense, но это может быть любой объект keras.layers.Layer. Его также можно полностью опустить, если вы просто хотите выполнить логистическую регрессию на необработанных входных данных.

Модель автоматически инициализирует слой Logistic, который выводит одно значение (units=1), выражающее вероятность данной метки. входные характеристики.

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

Последние мысли

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

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

Теперь вы готовы тренироваться от начала до конца! Полный код приведен в конце этого руководства.

model = CustomModel(nn_block=dense, name="nn_logistic_model")
model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=1e-4))
model.fit(ds, epochs=5, callbacks=[ReportWeightsCallback()])

Вам может быть интересно, почему мы создали подкласс keras.Model для достижения нашей цели. Вам это может показаться сложным. Почему бы, например, не определить функцию потерь с пользовательским градиентом в слое Logistic и вообще отказаться от пользовательского объекта Model?

Полный код

Сначала я пытался это сделать, но понял, что это не сработает. Указав пользовательскому Layer ожидать как функции, так и целевые метки в качестве входных данных, можно реализовать функцию потери отрицательного логарифмического правдоподобия с пользовательским градиентом внутри слоя Logistic. Это работает во время обучения, когда у вас есть доступ к меткам, но плохо работает, когда вы используете модель для вывода, потому что предсказание невидимых данных не включает в себя целевые метки в качестве входных данных: входные данные для логического вывода не будут иметь той же формы, что и входные данные для обучение.

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

Источники

Наконец, средние потери на выборку отслеживаются с помощью loss_tracker.

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

Целью здесь является определение функции потерь, которая позволяет нам использовать autodiff для расчета градиентов потерь по отношению к обучаемым параметрам слоя Dense, но использовать настраиваемый расчет градиента для параметров слоя Logistic. Для этого мы должны изолировать часть расчета потерь, в которой используются параметры пользовательского градиента, от части, в которой используются параметры автодифференциала. Изоляция достигается за счет вложения функции для пользовательского компонента в более широкую функцию потерь, которая обеспечивает полный путь от входных данных модели до выходных данных модели. С помощью этого пути можно оптимизировать обучаемые параметры всех слоев.

Давайте сначала сосредоточимся на внутренней функции, logistic_loss(x, y). Первый аргумент, x, представляет собой входной тензор для слоя Logistic (то есть выход из более раннего слоя Dense). Второй аргумент, y, представляет истинную метку обучающего примера. Эта функция изолирует часть расчета потерь, включающую параметры, которые мы хотим изучить с помощью пользовательского градиента. Декоратор @tf.custom_gradient сигнализирует TensorFlow использовать пользовательские формулы вместо автодифференциала для расчета градиентов потерь по отношению к обучаемым параметрам в области действия декоратора. Поэтому важно, чтобы для всех обучаемых параметров в области действия декоратора был указан собственный градиент. Вот почему мы определили logistic_loss с точки зрения входных данных для слоя Logistic, а не с точки зрения входных данных для более ранних слоев: мы эффективно ограничиваем область действия декоратора весами и смещениями, которые мы хотим изучить с помощью пользовательских градиентов, и оставляем остальные вычислений градиента в автодифф.

Пользовательский градиент определяется внутри logistic_loss в соответствии с требованиями декоратора
(подробности см. в «документации» TensorFlow).

> Внешняя функция, loss_fn, принимает в качестве входных данных необработанные функции и целевые метки из обучающих данных (или данных проверки/тестирования). Обратите внимание, что мы не обернули внешний loss_fn декоратором tf.custom_gradient. Это гарантирует, что автодифф используется для расчета градиентов потерь по отношению к остальным параметрам, которые не входят в область действия logistic_loss. Внешняя функция возвращает отрицательное логарифмическое правдоподобие, рассчитанное внутренней функцией logistic_loss.

Вы, возможно, заметили, что только входные данные для слоя Logistic необходимы для вычисления отрицательных логарифмических потерь правдоподобия входного пакета. Так зачем же вкладывать logistic_loss во внешний loss_fn? Если вы уберете внешнюю функцию и вместо этого используете logistic_loss в качестве общей функции потерь, TensorFlow предупредит вас, что градиенты потерь по отношению к параметрам плотного слоя больше не определены. Это потому, что мы не определили их в декораторе @tf.custom_gradient. Параметры логистического слоя будут обучены, но параметры плотного слоя не изменятся по сравнению с их начальными значениями.

Метод train_step

Этот метод используется методом Model.fit для обновления параметров модели и расчета показателей модели. Это компонент пользовательского объекта модели, который позволяет нам использовать высокоуровневый метод Model.fit с нашими пользовательскими потерями и градиентами. Диспетчер контекста GradientTape отслеживает все градиенты loss_fn, используя autodiff, где расчет пользовательского градиента не используется. Мы получаем доступ к градиентам, связанным с обучаемыми параметрами, с помощью tape.gradient(loss, self.trainable_weights) и сообщаем объекту оптимизатора использовать эти градиенты для оптимизации параметров в self.optimizer.apply_gradients(zip(grads, self.trainable_weights)). Наконец, шаг обучения обновляет loss_tracker средними потерями тренировочной партии (рассчитывается loss_fn).

Дуа Д., Графф С. Немецкие кредитные данные. 2017. Репозиторий машинного обучения UCI. Доступно по адресу: «http://archive.ics.uci.edu/ml».

Как использовать пользовательские потери с пользовательскими градиентами в TensorFlow с Keras