Общие API SavedModel для текстовых задач

На этой странице описывается, как TF2 SavedModels для задач, связанных с текстом, должен реализовывать Reusable SavedModel API . (Это заменяет и расширяет Common Signatures for Text для ныне устаревшего формата TF1 Hub .)

Обзор

Существует несколько API для вычисления встраивания текста (также известного как плотные представления текста или векторы текстовых признаков).

  • API для внедрения текста из текстовых входов реализуется с помощью SavedModel, который сопоставляет пакет строк с пакетом векторов внедрения. Это очень просто в использовании, и многие модели на TF Hub реализовали это. Однако это не позволяет провести тонкую настройку модели на ТПУ.

  • API для встраивания текста с предварительно обработанными входными данными решает ту же задачу, но реализуется двумя отдельными моделями SavedModels:

    • препроцессор , который может работать внутри входного конвейера tf.data и преобразовывать строки и другие данные переменной длины в числовые тензоры,
    • кодер , который принимает результаты препроцессора и выполняет обучаемую часть вычислений внедрения.

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

  • API для внедрения текста с помощью кодировщиков Transformer расширяет API для внедрения текста из предварительно обработанных входных данных на частный случай BERT и других кодировщиков Transformer.

    • Препроцессор расширен для создания входных данных кодировщика из более чем одного сегмента входного текста.
    • Кодер Transformer предоставляет контекстно-зависимые внедрения отдельных токенов.

В каждом случае текстовые входные данные представляют собой строки в кодировке UTF-8, обычно представляющие собой обычный текст, если в документации модели не указано иное.

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

Встраивание текста из текстовых вводов

SavedModel для встраивания текста из текстовых входов принимает пакет входных данных в строке Tensor формы [batch_size] и сопоставляет их с тензором float32 формы [batch_size, dim] с плотными представлениями (векторами признаков) входных данных.

Краткое описание использования

obj = hub.load("path/to/model")
text_input = ["A long sentence.",
              "single-word",
              "http://example.com"]
embeddings = obj(text_input)

Напомним, из API Reusable SavedModel , что для запуска модели в режиме обучения (например, для исключения) может потребоваться аргумент ключевого слова obj(..., training=True) и что obj предоставляет атрибуты .variables , .trainable_variables и .regularization_losses в зависимости от обстоятельств. .

В Керасе обо всем этом заботится

embeddings = hub.KerasLayer("path/to/model", trainable=...)(text_input)

Распределенное обучение

Если встраивание текста используется как часть модели, которая обучается с помощью стратегии распределения, hub.load("path/to/model") hub.KerasLayer("path/to/model", ...) , соответственно, должно произойти внутри области DistributionStrategy, чтобы создать переменные модели распределенным способом. Например

  with strategy.scope():
    ...
    model = hub.load("path/to/model")
    ...

Примеры

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

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

  • препроцессор , который сопоставляет строковый тензор формы [batch_size] с набором числовых тензоров,
  • кодер , который принимает набор тензоров, возвращенный препроцессором, выполняет обучаемую часть вычислений внедрения и возвращает набор выходных данных. Выходные данные под ключом "default" представляют собой тензор float32 формы [batch_size, dim] .

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

Это деталь реализации, какие тензоры содержатся в выходных данных препроцессора и какие (если таковые имеются) дополнительные тензоры, помимо "default" , содержатся в выходных данных кодировщика.

В документации кодировщика должно быть указано, какой препроцессор с ним использовать. Обычно существует ровно один правильный выбор.

Краткое описание использования

text_input = tf.constant(["A long sentence.",
                          "single-word",
                          "http://example.com"])
preprocessor = hub.load("path/to/preprocessor")  # Must match `encoder`.
encoder_inputs = preprocessor(text_input)

encoder = hub.load("path/to/encoder")
encoder_outputs = encoder(encoder_inputs)
embeddings = encoder_outputs["default"]

Вспомните из API Reusable SavedModel , что для запуска кодировщика в режиме обучения (например, для исключения) может потребоваться аргумент ключевого слова encoder(..., training=True) , и этот encoder предоставляет атрибуты .variables , .trainable_variables и .regularization_losses в зависимости от обстоятельств. .

Модель preprocessor может иметь .variables , но не предназначена для дальнейшего обучения. Предварительная обработка не зависит от режима: если preprocessor() вообще имеет аргумент training=... , он не имеет никакого эффекта.

В Керасе обо всем этом заботится

encoder_inputs = hub.KerasLayer("path/to/preprocessor")(text_input)
encoder_outputs = hub.KerasLayer("path/to/encoder", trainable=True)(encoder_inputs)
embeddings = encoder_outputs["default"]

Распределенное обучение

Если кодировщик используется как часть модели, которая обучается с помощью стратегии распределения, hub.load("path/to/encoder") hub.KerasLayer("path/to/encoder", ...) , соответственно, должно произойти внутри

  with strategy.scope():
    ...

чтобы воссоздать переменные кодировщика распределенным способом.

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

Примеры

Встраивание текста с помощью Transformer Encoders

Кодировщики-трансформеры для текста работают с пакетом входных последовательностей, каждая последовательность содержит n ≥ 1 сегментов токенизированного текста в пределах некоторой специфичной для модели привязки к n . Для BERT и многих его расширений эта граница равна 2, поэтому они принимают отдельные сегменты и пары сегментов.

API для внедрения текста с помощью кодировщиков Transformer расширяет API для внедрения текста с предварительно обработанными входными данными до этого параметра.

Препроцессор

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

Кроме того, препроцессор SavedModel предоставляет вызываемые подобъекты tokenize для токенизации (отдельно для каждого сегмента) и bert_pack_inputs для упаковки n токенизированных сегментов в одну входную последовательность для кодировщика. Каждый подобъект соответствует API Reusable SavedModel .

Краткое описание использования

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

preprocessor = hub.load("path/to/preprocessor")

# Tokenize batches of both text inputs.
text_premises = tf.constant(["The quick brown fox jumped over the lazy dog.",
                             "Good day."])
tokenized_premises = preprocessor.tokenize(text_premises)
text_hypotheses = tf.constant(["The dog was lazy.",  # Implied.
                               "Axe handle!"])       # Not implied.
tokenized_hypotheses = preprocessor.tokenize(text_hypotheses)

# Pack input sequences for the Transformer encoder.
seq_length = 128
encoder_inputs = preprocessor.bert_pack_inputs(
    [tokenized_premises, tokenized_hypotheses],
    seq_length=seq_length)  # Optional argument.

В Keras это вычисление можно выразить как

tokenize = hub.KerasLayer(preprocessor.tokenize)
tokenized_hypotheses = tokenize(text_hypotheses)
tokenized_premises = tokenize(text_premises)

bert_pack_inputs = hub.KerasLayer(
    preprocessor.bert_pack_inputs,
    arguments=dict(seq_length=seq_length))  # Optional argument.
encoder_inputs = bert_pack_inputs([tokenized_premises, tokenized_hypotheses])

Подробности tokenize

Вызов preprocessor.tokenize() принимает строковый Tensor формы [batch_size] и возвращает RaggedTensor формы [batch_size, ...] , значениями которого являются идентификаторы токенов int32, представляющие входные строки. После batch_size может быть r ≥ 1 неровных размеров, но не может быть другого однородного измерения.

  • Если r =1, форма имеет [batch_size, (tokens)] и каждый ввод просто преобразуется в плоскую последовательность токенов.
  • Если r >1, имеется r -1 дополнительных уровней группировки. Например, tensorflow_text.BertTokenizer использует r =2 для группировки токенов по словам и возвращает форму [batch_size, (words), (tokens_per_word)] . От рассматриваемой модели зависит, сколько из этих дополнительных уровней существует, если таковые имеются, и какие группы они представляют.

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

С точки зрения Reusable SavedModel API , объект preprocessor.tokenize может иметь .variables , но не предназначен для дальнейшего обучения. Токенизация не зависит от режима: если preprocessor.tokenize() вообще имеет аргумент training=... , он не имеет никакого эффекта.

Подробности о bert_pack_inputs

Вызов preprocessor.bert_pack_inputs() принимает список токенизированных входных данных Python (отдельно для каждого входного сегмента) и возвращает набор тензоров, представляющий пакет входных последовательностей фиксированной длины для модели кодера Transformer.

Каждый токенизированный ввод представляет собой int32 RaggedTensor формы [batch_size, ...] , где число r неровных измерений после пакетного размера либо равно 1, либо такое же, как в выходных данных preprocessor.tokenize(). (Последнее предназначено только для удобства; дополнительные размеры перед упаковкой распрямляются.)

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

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

Упаковка не зависит от режима: если preprocessor.bert_pack_inputs() вообще имеет аргумент training=... , он не имеет никакого эффекта. Кроме того, не ожидается, что preprocessor.bert_pack_inputs будет иметь переменные или поддерживать тонкую настройку.

Кодер

Кодировщик вызывается для dict encoder_inputs так же, как и в API для встраивания текста с предварительно обработанными входными данными (см. выше), включая положения из Reusable SavedModel API .

Краткое описание использования

encoder = hub.load("path/to/encoder")
encoder_outputs = encoder(encoder_inputs)

или эквивалентно в Керасе:

encoder = hub.KerasLayer("path/to/encoder", trainable=True)
encoder_outputs = encoder(encoder_inputs)

Подробности

encoder_outputs — это набор тензоров со следующими ключами.

  • "sequence_output" : тензор float32 формы [batch_size, seq_length, dim] с контекстно-зависимым внедрением каждого токена каждой упакованной входной последовательности.
  • "pooled_output" : тензор float32 формы [batch_size, dim] с встраиванием каждой входной последовательности в целом, полученной изsequence_output некоторым обучаемым способом.
  • "default" , как того требует API для встраивания текста с предварительно обработанными входными данными: тензор float32 формы [batch_size, dim] с встраиванием каждой входной последовательности. (Возможно, это просто псевдонимpooled_output.)

Содержимое encoder_inputs не является строго обязательным для этого определения API. Однако для кодеров, использующих входные данные в стиле BERT, рекомендуется использовать следующие имена (из набора инструментов NLP Modeling Toolkit TensorFlow Model Garden ), чтобы минимизировать трения при смене кодеров и повторном использовании моделей препроцессора:

  • "input_word_ids" : тензор int32 формы [batch_size, seq_length] с идентификаторами токенов упакованной входной последовательности (то есть включая токен начала последовательности, токены конца сегмента и дополнение).
  • "input_mask" : тензор int32 формы [batch_size, seq_length] со значением 1 в позиции всех входных токенов, присутствующих перед заполнением, и значением 0 для токенов заполнения.
  • "input_type_ids" : тензор int32 формы [batch_size, seq_length] с индексом входного сегмента, который привел к появлению входного токена в соответствующей позиции. Первый входной сегмент (индекс 0) включает в себя токен начала последовательности и токен конца сегмента. Второй и последующие сегменты (если они присутствуют) включают соответствующий токен конца сегмента. Токены заполнения снова получают индекс 0.

Распределенное обучение

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

Примеры