В этом посте мы рассмотрим кастомные Estimators. В частности, продемонстрируем как создавать кастомный tf.estimator.Estimator, который имитирует поведение предсозданного Estimator tf.estimator.DNNClassifier в решении классической задачи классификации ирисов. Детали использования предсозданных Estimators для Iris задачи в этом посте.
Для загрузки и доступа к коду примеров выполните следующие команды:
git clone https://github.com/tensorflow/models/
cd models/samples/core/get_started
В этом посте мы рассмотрим custom_estimator.py. Запустить его можно простой командой:
python custom_estimator.py
При желании можно легко сранивать результаты custom_estimator.py и premade_estimator.py (который находится в этой же директории).
Предсозданные vs. кастомные Estimators
Как показано на следующей иллюстрации предсозданные Estimators - это подклассы tf.estimator.Estimator базового класса, в то время как кастомные Estimators - это инстансы tf.estimator.Estimator:
Предсозданные и кастомные Estimators - это все Estimators.
Предсозданные Estimators полностью готовы для использованиня. Иногда, однако, необходим больший контроль над поведением Estimator. Вот где приходят на помощь кастомные Estimators. Вы можете создать кастомный Estimator, выполняющий все что угодно. Если вы хотите скрытые слои, связанные в необычной манере, напишите кастомный Estimator. Если вы хотите вычислять уникальную метрику для модели, напишите кастомный Estimator. В общем, если вы хотите Estimator, оптимизированный для вашей специфичной проблемы, напишите кастомный Estimator.
Функция модели (model function) (или model_fn) реализует алгоритм машинного обучения. Единственное различие между работой предсозданных Estimators и кастомных Estimators это:
- В предсозданных Estimators кто-то уже написал функцию модели для вас.
- В кастомных Estimators необходимо написать функцию модели самому.
Ваша функция модели может реализовывать широкий спектр алгоритмов, определяя все виды скрытых слоев и метрик. Как и функции ввода, все функции модели должны принимать стандартную группу вводных параметров и возвращать стандартную группу значений вывода. Также как функции ввода используют механизмы Dataset API, так и функции модели используют Layers API и Metrics API.
Рассмотрим как разрешить Iris задачу с кастомным Estimator. В качестве напоминания - вот организация Iris модели, которую мы пробуем имитировать:
Наша реализация Iris содержит четыре свойства, два скрытых слоя и слой вывода.
Напишем функцию ввода
Наша кастомная реализация Estimator использует ту же функцию ввода, что и реализация предсозданного Estimator, из iris_data.py. А именно:
def train_input_fn(features, labels, batch_size):
"""Функция ввода для тренировки"""
# Преобразуем ввод в Dataset.
dataset = tf.data.Dataset.from_tensor_slices(
(dict(features), labels))
# Перемешиваем, повторяем, и пакетизируем примеры.
dataset = dataset.shuffle(1000).repeat().batch(batch_size)
# Возвращаем конец пайплайна для чтения.
return dataset.make_one_shot_iterator().get_next()
Эта функция ввода строит пайплайн ввода, который выводит пакеты (features, labels) пар, где features - это словарные свойства.
Создаем колонки свойств
Как уже рассказывалось в предыдущих постах о Estimators, необходимо определить колонки свойств вашей модели для того, чтобы задать как модели следует использовать каждое свойство. Работаем ли мы с предсозданными или кастомными Estimators - задание колонок свойств происходит одинаковым образом.
Следующий код создает простую numeric_column для каждого вводного свойства, обозначая, что значение вводного свойства следует использовать напрямую как ввод для модели:
# Колонки свойств описывают как использовать ввод.
my_feature_columns = []
for key in train_x.keys():
my_feature_columns.append(tf.feature_column.numeric_column(key=key))
Пишем функцию модели
Функция модели, которую мы будем использовать, имеет следующую сигнатуру вызова:
def my_model_fn(
features, # Это batch_features из input_fn
labels, # Это batch_labels из input_fn
mode, # Инстанс tf.estimator.ModeKeys
params): # Дополнительная конфигурация
Первые два аргумента - это пакеты свойств и меток, возвращенные функцией ввода, то есть свойства и метки - это ручки для данных, которые будет использовать модель. mode аргумент обозначает какая операция вызывается: тренировка, обучение или прогнозирование.
Вызывающий может передать параметры к конструктору Estimstor'а. Любые параметры, переданные конструктору, в свою очередь, передаются в model_fn. В custom_estimator.py следующие строки создают estimator и устанавливают параметры для конфигурации модели. Этот шаг конфигурации схож с тем как конфигурировался tf.estimator.DNNClassifier в предсозданных Estimators.
classifier = tf.estimator.Estimator(
model_fn=my_model_fn,
params={
'feature_columns': my_feature_columns,
# Два скрытых слоя по 10 узлов каждый.
'hidden_units': [10, 10],
# Модель должны сделать выбор между 3 классами.
'n_classes': 3,
})
Для того чтобы реализовать типичную функцию модели необходимо выполнить следующее:
- Определить модель.
- Определить дополнительные вычисления для каждого из трех различных режимов:
- Прогнозирование
- Оценка
- Тренировка
Определяем модель
Базовая модель глубокой нейронной сети должна содержать следующие три компонента:
- Слой ввода
- Один или больше скрытых слоев
- Слой вывода
Определяем слой ввода
Первая строка model_fn вызывает tf.feature_column.input_layer, чтобы преобразовать словарь свойств и feature_columns во ввод для вашей модели:
# Используем input_layer для применения колонок свойств.
net = tf.feature_column.input_layer(features, params['feature_columns'])
Предыдущий код применяет преобразования, определенные колонками свойств, при создании слоя ввода модели.
Скрытые слои
Если вы создаете глубокую нейронную сеть, вы должны определить один или больше скрытых слоев. Layers API предоставляет богатый набор функций для определения всех типов скрытых слоев, включая сверточные, пуловые и дропаут слои. Для Iris мы просто собираемся вызвать tf.layers.dense, чтобы создать скрытые слои, с измерениями, определенными параметром params['hidden_layers']. В тесносвязанном (dense) слое каждый узел связан с каждым узлом в предыдущем слое.
# Строим скрытые слои, размером согласно 'hidden_units' параметру.
for units in params['hidden_units']:
net = tf.layers.dense(net, units=units, activation=tf.nn.relu)
- units параметр определяет количество нейронов вывода в данном слое.
- activation параметр определяет функцию активации - Relu в данном случае.
Переменная net означает здесь текущий верхний слой сети. В ходе первой итерации net обозначает слой ввода. На каждой итерации цикла tf.layers.dense создает новый слой, который принимает вывод предыдущего слоя как свой ввод.
После создания двух скрытых слоев, сеть выглядит следующим образом. Для простоты, на изображении не показаны все элементы в каждом слое.
Следует отметить что tf.layers.dense предоставляет много дополнительных возможностей, включая возможность устанавливать множество параметров регуляризации. Для простоты, однако, мы примем дефолтные значения для параметров.
Слой вывода
Мы определим слой вывода, вызвав tf.layers.dense еще раз, на этот раз без функции активации:
logits = tf.layers.dense(net, params['n_classes'], activation=None)
Здесь net обозначает финальный слой вывода. Более того, полный набор слоев сейчас связан следующим образом:
Финальный скрытый слой передает результат в слой вывода.
При определении слоя вывода units параметр задает количество выводов. Таким образом, с установкой units равным params['n_classes'], модель производит одно значение вывода на класс. Каждый элемент вектора вывода будет содержать счет, или "logit", рассчитанный для соответствующего класса ириса: Setosa, Versicolor, или Virginica, соответственно.
Позже эти logits будут преобразованы в вероятности tf.nn.softmax функцией.
Реализуем тренировку, оценку, и прогнозирование.
Финальный шаг в создании функции модели - это написание ветвящегося кода, который реализует прогнозирование, оценку, и тренировку.
Функция модели вызывается когда кто-либо вызывает методы train, evaluate, или predict Estimator'а. Напомним, что сигнатура для функции модели выглядит следующим образом:
def my_model_fn(
features, # Это batch_features из input_fn
labels, # Это batch_labels из input_fn
mode, # Инстанс tf.estimator.ModeKeys
params): # Дополнительная конфигурация
Сфокусируемся на третьем аргументе, mode. Как показывает следующая таблица, когда кто-то вызывает train, evaluate, или predict, Estimator вызывает функцию модели с mode параметром, установленным следующим образом:
Например, предположим вы создали кастомный Estimator, чтобы сгенерировать объект, названный classifier.
classifier = tf.estimator.Estimator(...)
classifier.train(input_fn=lambda: my_input_fn(FILE_TRAIN, True, 500))
Estimator затем вызывает функцию модели с аргументом mode установленный в ModeKeys.TRAIN.
Ваша функция модели должна предоставить код, чтобы обрабатывать все три mode значения. Для каждого mode значения, код должен возвращать инстанс tf.estimator.EstimatorSpec, который содержит информацию, необходимую вызывающему. Рассмотрим каждый mode.
Predict mode (Прогнозирование)
Когда вызывается predict метод Estimator'а, model_fn получает mode = ModeKeys.PREDICT. В этом случае, функция модели должна вернуть tf.estimator.EstimatorSpec, содержащий прогноз.
Модель должна быть натренирована перед проведением прогнозирования. Тренированная модель сохраняется на диск в model_dir директории, установленной при создании инстанса Estimator.
Код для генерации прогноза для этой модели выглядит следующим образом:
# Вычисляем прогнозы.
predicted_classes = tf.argmax(logits, 1)
if mode == tf.estimator.ModeKeys.PREDICT:
predictions = {
'class_ids': predicted_classes[:, tf.newaxis],
'probabilities': tf.nn.softmax(logits),
'logits': logits,
}
return tf.estimator.EstimatorSpec(mode, predictions=predictions)
Словарь predictions содержит все, что модель возвращает, когды выполняется в predict mode.
predictions содержит следующие три ключ/значение пары:
- class_ids содержит идентификаторы классов (0, 1, или 2), представляющие прогноз модели наиболее вероятного вида ирисов для этого примера.
- probabilities содержит три вероятности (в этом примере, 0.02, 0.95, и 0.03)
- logit содержит сырые logit значения (в этом примере, -1.3, 2.6, и -0.9)
Мы возвращаем этот словарь вызывающему через predictions параметр tf.estimator.EstimatorSpec. Метод tf.estimator.Estimator.predict будет выводить этот словарь.
Рассчет потери
Для обоих случаев - тренировки и оценки - нам необходимо рассчитать потерю модели. Это целевое значение, которое мы будем оптимизировать в ходе тренировки и оценки.
Мы можем рассчитать потерю, вызвав tf.losses.sparse_softmax_cross_entropy. Значение, возвращенное этой функцией будет приближаться к нулю, когда вероятность правильного класса (в индекс метке) будет равна почти 1.0. Возвращаемое значение потери будет прогрессивно расти с уменьшением вероятности правильного класса.
Эта функция возвращает среднее значение для целого пакета.
# Вычисляем потерю.
loss = tf.losses.sparse_softmax_cross_entropy(labels=labels,
logits=logits)
Evaluate mode (Оценка)
Когда вызывается evaluate метод Estimator'а, model_fn получает mode = ModeKeys.EVAL. В этом случае, функция модели должна вернуть tf.estimator.EstimatorSpec, содержащий потерю модели и опционально одну или несколько метрик.
Хотя возвращение метрик опционально, большинство кастомных Estimators возвращают по крайней мере одну метрику. TensorFlow предоставляет Metrics модуль tf.metrics для вычисления общих метрик. В целях краткости, мы будем возвращать только аккуратность. tf.metrics.accuracy функция сравнивает наши прогнозы с настоящими значениями, то есть, с метками, предоставленными вводной функцией. tf.metrics.accuracy функция требует, чтобы метки и прогнозы имели одинаковую форму. Вот вызов tf.metrics.accuracy:
# Вычисляем оценку метрик.
accuracy = tf.metrics.accuracy(labels=labels,
predictions=predicted_classes,
name='acc_op')
tf.estimator.EstimatorSpec возвращенный для оценки обычно содержит следующую информацию:
- loss - потеря модели
- eval_metric_ops - опциональный словарь метрик
Таким образом, мы будем создавать словарь, содержащий единственную метрику. Если бы мы рассчитали другие метрики, мы бы добавили их как дополнительные ключ/значение пары к этому же словарю. Затем, мы передадим этот словарь в eval_metric_ops аргумент tf.estimator.EstimatorSpec.
metrics = {'accuracy': accuracy}
tf.summary.scalar('accuracy', accuracy[1])
if mode == tf.estimator.ModeKeys.EVAL:
return tf.estimator.EstimatorSpec(
mode, loss=loss, eval_metric_ops=metrics)
tf.summary.scalar сделает аккуратность доступной для TensorBoard в обоих TRAIN и EVAL режимах(modes).
Train mode (Тренировка)
Когда вызывается train метод Estimator'а, model_fn получает mode = ModeKeys.TRAIN. В этом случае, функция модели должна вернуть tf.estimator.EstimatorSpec, содержащий потерю и тренировочную операцию.
Построение тренировочной операции потребует оптимизатор. Мы будем использовать tf.train.AdagradOptimizer, потому что мы имитируем DNNClassifier, который также использует Adagrad по умолчанию. tf.train пакет также предоставляет много других оптимизаторов.
Вот код, который создает оптимизатор:
optimizer = tf.train.AdagradOptimizer(learning_rate=0.1)
Далее мы построим тренировочную операцию, используя метод tf.train.Optimizer.minimize оптимизатора на потере, которую мы рассчитали ранее.
minimize метод принимает global_step параметр. TensorFlow использует этот параметр для счета количества произведенных тренировочных шагов (для того чтобы знать когда тренировка должна остановиться). Более того global_step необходим для корректной работы TensorBoard графов. Просто вызываем tf.train.get_global_step и передаем результат как global_step аргумент minimize метода.
Вот код тренировки модели:
train_op = optimizer.minimize(loss,
global_step=tf.train.get_global_step())
tf.estimator.EstimatorSpec возвращаемый для тренировки должен иметь следующий набор полей:
- loss, содержащее значение потери модели.
- train_op, которое исполняет тренировочный шаг.
Вот код для вызова EstimatorSpec:
return tf.estimator.EstimatorSpec(mode, loss=loss, train_op=train_op)
Кастомный Estimator
Создадим инстанс кастомного Estimator'а через базовый Estimator класс:
# Строим 2 скрытых слоя с 10 узлами в каждом.
classifier = tf.estimator.Estimator(
model_fn=my_model_fn,
params={
'feature_columns': my_feature_columns,
# 2 скрытых слоя с 10 узлами в каждом
'hidden_units': [10, 10],
# Модель должна выбирать между 3 классами.
'n_classes': 3,
})
Здесь params словарь служит той же цели, что и key-word аргументы DNNClassifier; то есть, params словарь позволяет конфигурировать Estimator без изменения кода в model_fn.
Остальной код для тренировки, оценки и выполнения прогнозов такой же как и в предсозданных Estimators. Например, следующий код будет тренировать модель:
# Тренируем модель
classifier.train(
input_fn=lambda:iris_data.train_input_fn(train_x,
train_y,
args.batch_size),
steps=args.train_steps)
TensorBoard
Вы можете просматривать результаты тренировки для кастомного Estimator'а в TensorBoard. Для того чтобы увидеть результаты, запустите TensorBoard из командной строки следующим образом:
# Замените PATH настоящим путем, переданным как model_dir
tensorboard --logdir=PATH
Затем откройте TensorBoard в браузере по адресу http://localhost:6006
Все предсозданные Estimators автоматически логируют множество информации в TensorBoard. С кастомными Estimators, однако, TensorBoard предоставляет только один лог по умолчанию (граф потери) плюс информацию, которую вы явно передадите на логирование в TensorBoard. Для кастомного Estimator, который мы только что создали, TensorBoard генерирует следующее:
TensorBoard показывает три графа.
Вкратце, вот что эти три графа говорят вам:
- global_step/sec: Индикатор производительности, показывающий как много пакетов (градиентных апдейтов) мы обработали за секунду в ходе тренировки модели.
- loss: отчет о потере.
- accuracy: Аккуратность записанная следующими двумя строками:
- eval_metric_ops={'my_accuracy': accuracy}, в ходе оценки.
- tf.summary.scalar('accuracy', accuracy[1]), в ходе тренировки.
Эти TensorBoard графы - одна из главных причин, по которой важно передавать аргумент global_step в метод minimize оптимизатора. Модель не может записывать x-координаты для этих графов без этого аргумента.
Отметим следующее в my_accuracy и loss графах:
- Оранжевая линия представляет тренировку.
- Синяя точка представляет оценку.
В ходе тренировки заключения (оранжевая линия) записаны периодично с ходом обработки пакетов, которые поэтому становятся графом растянутым по x-оси.
В противоположность этому, оценка производит только единственную точку на графе для каждого вызова оценки. Эта точка содержит среднее значение за весь вызов оценки. Эта точка не имеет ширины на графе, потому что оценка происходит целиком по состоянию модели в отдельный шаг тренировки (из единственной точки).
Как изображено на следующей иллюстрации вы можете отдельно включать/выключать отчет, используя управление с левой стороны TensorBoard.
Резюме
Хотя предсозданные Estimators могут быть эффективным путем для быстрого создания новых моделей, часто требуется дополнительная гибкость, которую предоставляют кастомные Estimators. К счастью, предсозданные и кастомные Estimators следуют одной и той же программной модели. Единственное практическое различие в том, что вы должны написать функцию модели для кастомных Estimators; остальное все тоже самое.