Что делает облачный ML-движок Google, когда запрос Json содержит _bytes или b64?

В документации по облаку Google говорится: :

Закодированная строка должна быть отформатирована как объект JSON с одним ключом с именем b64. В следующем примере Python кодируется буфер необработанных данных JPEG с использованием библиотеки base64 для создания экземпляра:

{"image_bytes":{"b64": base64.b64encode(jpeg_data)}}

В коде модели TensorFlow вы должны назвать псевдонимы для ваших входных и выходных тензоров так, чтобы они оканчивались на _bytes.

Я хотел бы больше узнать о том, как этот процесс работает на стороне облака Google.

  • ML-движок автоматически декодирует любой контент после строки «b64» в байтовые данные?

  • Когда запрос имеет эту вложенную структуру, проходит ли он только в разделе «b64» обслуживающей функции ввода и удаляет ли ключ «image_bytes»?

  • Каждый запрос передается индивидуально в обслуживающую функцию ввода или они группируются?

  • Определяем ли мы псевдонимы ввода-вывода в ServingInputReceiver, возвращаемом обслуживающей функцией ввода?

Я не нашел способа создать обслуживающую функцию ввода, которая использует эту вложенную структуру для определения заполнителей функций. В моем я использую только "b64", и я не уверен, что делает gcloud ml-engine при получении запросов.

Кроме того, при локальном прогнозировании с использованием gcloud ml-engine local predict отправка запроса с вложенной структурой завершается ошибкой (неожиданный ключ image_bytes, поскольку он не определен в обслуживающей функции ввода). Но при прогнозировании с использованием gcloud ml-engine predict отправка запросов с вложенной структурой работает, даже если обслуживающая функция ввода не содержит ссылки на «image_bytes». Предсказание gcloud также работает, если исключить «image_bytes» и передать только «b64».

Пример обслуживающей функции ввода

def serving_input_fn():
    feature_placeholders = {'b64': tf.placeholder(dtype=tf.string,
                                                  shape=[None],
                                                  name='source')}
    single_image = tf.decode_raw(feature_placeholders['b64'], tf.float32)
    inputs = {'image': single_image}
    return tf.estimator.export.ServingInputReceiver(inputs, feature_placeholders)

Я привел пример с использованием изображений, но предполагаю, что то же самое должно применяться ко всем типам данных, отправляемых в байтах и ​​в кодировке base64.

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

Вопросы по этой теме


person NickDGreg    schedule 08.03.2018    source источник


Ответы (1)


Чтобы прояснить некоторые из ваших вопросов, позвольте мне начать с базовой анатомии запроса на прогноз:

{"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
comment
Потрясающий ответ! У меня все еще есть два момента, которые нужно прояснить (1) (как я понимаю из вашего JSON не (напрямую) ... предложение). Сообщив тензорный поток типа с помощью _bytes, это значение, которое он затем считывает как тип байта? А без _bytes tensorflow читает его как строку? (2) feature_placeholder является входом для обоих аргументов обслуживающей входной функции, как тогда модель может получить доступ к декодированному одиночному изображению? Конечно, это нужно будет предоставить модели в качестве выходных данных от обслуживающей входной функции? В противном случае модель может получить доступ только к незакодированным байтам - person NickDGreg; 09.03.2018
comment
(1) TF имеет только один тип - байты. В основном, если вы используете имя _bytes, ожидается, что данные будут {b64: ...}, но внутренний тип TF всегда байтовый, даже для строк. (2) Возможно, в этом вы правы. Если вы попробовали приведенные выше инструкции, но они не работают, дайте мне знать, и я могу обновить сообщение. - person rhaertel80; 14.03.2018