Чтобы прояснить некоторые из ваших вопросов, позвольте мне начать с базовой анатомии запроса на прогноз:
{"instances": [<instance>, <instance>, ...]}
Где instance
- это объект JSON (dict / map, в дальнейшем я буду использовать термин Python «dict»), а атрибуты / ключи - это имена входов со значениями, содержащими данные для этого входа.
Что делает облачная служба (и gcloud ml-engine local predict
использует те же базовые библиотеки, что и служба), так это берет список словарных статей (которые можно рассматривать как строки данных), а затем преобразует их в словарь списков (которые можно представить как столбчатые данные, содержащие пакеты экземпляров) с теми же ключами, что и в исходных данных. Например,
{"instances": [{"x": 1, "y": "a"}, {"x": 3, "y": "b"}, {"x": 5, "y": "c"}]}
становится (внутренне)
{"x": [1, 3, 5], "y": ["a", "b", "c"]}
Ключи в этом dict (и, следовательно, в экземпляре в исходном запросе) должны соответствовать ключам dict, переданного ServingInputFnReceiver
. Из этого примера должно быть очевидно, что служба «пакетирует» все данные, то есть все экземпляры вводятся в граф как единый пакет. Вот почему внешний размер формы входов должен быть None
- это размер пакета, и он не известен до того, как будет сделан запрос (поскольку каждый запрос может иметь разное количество экземпляров). При экспорте графика для приема вышеуказанных запросов вы можете определить такую функцию:
def serving_input_fn():
inputs = {'x': tf.placeholder(dtype=tf.int32, shape=[None]),
'y': tf.placeholder(dtype=tf.string, shape=[None]}
return tf.estimator.export.ServingInputReceiver(inputs, inputs)
Поскольку JSON не поддерживает (напрямую) двоичные данные и поскольку TensorFlow не имеет возможности отличить «строки» от «байтов», нам нужно обрабатывать двоичные данные особым образом. Прежде всего, нам нужно, чтобы имена указанных входов заканчивались на «_bytes», чтобы помочь отличить текстовую строку от байтовой. Используя приведенный выше пример, предположим, что y
содержит двоичные данные вместо текста. Мы заявляем следующее:
def serving_input_fn():
inputs = {'x': tf.placeholder(dtype=tf.int32, shape=[None]),
'y_bytes': tf.placeholder(dtype=tf.string, shape=[None]}
return tf.estimator.export.ServingInputReceiver(inputs, inputs)
Обратите внимание, что единственное, что изменилось, - это использование y_bytes
вместо y
в качестве имени входа.
Затем нам нужно на самом деле кодировать данные base64; везде, где допустима строка, вместо этого мы можем использовать такой объект: {"b64": ""}. Адаптируя текущий пример, запрос может выглядеть так:
{
"instances": [
{"x": 1, "y_bytes": {"b64": "YQ=="}},
{"x": 3, "y_bytes": {"b64": "Yg=="}},
{"x": 5, "y_bytes": {"b64": "Yw=="}}
]
}
В этом случае служба делает то же самое, что и раньше, но добавляет один шаг: она автоматически base64 декодирует строку (и «заменяет» объект {"b64": ...} байтами) перед отправкой в TensorFlow. Таким образом, TensorFlow на самом деле заканчивает тем же, что и раньше:
{"x": [1, 3, 5], "y_bytes": ["a", "b", "c"]}
(Обратите внимание, что имя входа не изменилось.)
Конечно, текстовые данные в формате base64 бессмысленны; вы обычно делаете это, например, для данных изображения, которые нельзя отправить другим способом через JSON, но я надеюсь, что приведенного выше примера в любом случае достаточно, чтобы проиллюстрировать эту мысль.
Следует отметить еще один важный момент: сервис поддерживает своего рода сокращение. Когда в вашей модели TensorFlow есть ровно один вход, нет необходимости постоянно повторять имя этого входа в каждом отдельном объекте в вашем списке экземпляров. Для иллюстрации представьте, что экспортируете модель только с x
:
def serving_input_fn():
inputs = {'x': tf.placeholder(dtype=tf.int32, shape=[None])}
return tf.estimator.export.ServingInputReceiver(inputs, inputs)
Запрос в "длинной форме" будет выглядеть так:
{"instances": [{"x": 1}, {"x": 3}, {"x": 5}]}
Вместо этого вы можете отправить запрос в сокращенном виде, например:
{"instances": [1, 3, 5]}
Обратите внимание, что это применимо даже к данным в кодировке base64. Так, например, если бы вместо экспорта только x
мы экспортировали только y_bytes
, мы могли бы упростить запросы из:
{
"instances": [
{"y_bytes": {"b64": "YQ=="}},
{"y_bytes": {"b64": "Yg=="}},
{"y_bytes": {"b64": "Yw=="}}
]
}
To:
{
"instances": [
{"b64": "YQ=="},
{"b64": "Yg=="},
{"b64": "Yw=="}
]
}
Во многих случаях это лишь небольшая победа, но она определенно способствует удобочитаемости, например, когда входные данные содержат данные CSV.
Итак, если все вместе адаптировать к вашему конкретному сценарию, вот как должна выглядеть ваша функция обслуживания:
def serving_input_fn():
feature_placeholders = {
'image_bytes': tf.placeholder(dtype=tf.string, shape=[None], name='source')}
single_image = tf.decode_raw(feature_placeholders['image_bytes'], tf.float32)
return tf.estimator.export.ServingInputReceiver(feature_placeholders, feature_placeholders)
Заметные отличия от вашего текущего кода:
- Имя ввода - не
b64
, а image_bytes
(может быть что угодно, заканчивающееся на _bytes
)
feature_placeholders
используется как оба аргумента для ServingInputReceiver
Пример запроса может выглядеть так:
{
"instances": [
{"image_bytes": {"b64": "YQ=="}},
{"image_bytes": {"b64": "Yg=="}},
{"image_bytes": {"b64": "Yw=="}}
]
}
Или, по желанию, сокращенно:
{
"instances": [
{"b64": "YQ=="},
{"b64": "Yg=="},
{"b64": "Yw=="}
]
}
Последнее последнее замечание. gcloud ml-engine local predict
и gcloud ml-engine predict
создают запрос на основе содержимого переданного файла. Очень важно отметить, что содержимое файла в настоящее время является не полным действительным запросом, а скорее каждой строкой --json-instances
становится одной записью в списке экземпляров. В частности, в вашем случае файл будет выглядеть так (новые строки здесь имеют значение):
{"image_bytes": {"b64": "YQ=="}}
{"image_bytes": {"b64": "Yg=="}}
{"image_bytes": {"b64": "Yw=="}}
или эквивалентное сокращение. gcloud
возьмет каждую строку и построит фактический запрос, показанный выше.
person
rhaertel80
schedule
08.03.2018