Предварительная обработка BERT с текстом TF

Посмотреть на TensorFlow.org Запускаем в Google Colab Посмотреть на GitHub Скачать блокнот

Обзор

Предварительная обработка текста - это сквозное преобразование необработанного текста в целочисленные входные данные модели. Модели НЛП часто сопровождаются несколькими сотнями (если не тысячами) строк кода Python для предварительной обработки текста. Предварительная обработка текста часто является проблемой для моделей, потому что:

  • Тренировочно-обслуживающий перекос. Становится все труднее обеспечить согласованность логики предварительной обработки входных данных модели на всех этапах разработки модели (например, предварительное обучение, точная настройка, оценка, вывод). Использование различных гиперпараметров, токенизации, алгоритмов предварительной обработки строк или просто непоследовательная упаковка входных данных модели на разных этапах может привести к трудным для отладки и катастрофическим последствиям для модели.

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

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

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

Предварительная обработка текста с помощью TF.Text

Используя API-интерфейсы предварительной обработки текста TF.Text, мы можем создать функцию предварительной обработки, которая может преобразовывать набор текстовых данных пользователя в целочисленные входные данные модели. Пользователи могут упаковать предварительную обработку напрямую как часть своей модели, чтобы облегчить вышеупомянутые проблемы.

В этом руководстве будет показано , как использовать TF.Text Preprocessing оп для преобразования текстовых данных в входы для модели Берта и входов для языка маскирующих pretraining задачи , описанной в «Маске LM и маскирование процедуры» от BERT: Pre-обучения Deep двунаправленных Трансформаторов языка Понимание . Процесс включает в себя разметку текста на подсловные блоки, объединение предложений, обрезку содержимого до фиксированного размера и извлечение меток для задачи моделирования языка с масками.

Настраивать

Давайте сначала импортируем нужные нам пакеты и библиотеки.

pip install -q -U tensorflow-text
import tensorflow as tf
import tensorflow_text as text
import functools

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

examples = {
    "text_a": [
      b"Sponge bob Squarepants is an Avenger",
      b"Marvel Avengers"
    ],
    "text_b": [
     b"Barack Obama is the President.",
     b"President is the highest office"
  ],
}

dataset = tf.data.Dataset.from_tensor_slices(examples)
next(iter(dataset))
{'text_a': <tf.Tensor: shape=(), dtype=string, numpy=b'Sponge bob Squarepants is an Avenger'>,
 'text_b': <tf.Tensor: shape=(), dtype=string, numpy=b'Barack Obama is the President.'>}

Токенизация

Наш первый шаг - запустить любую предварительную обработку строки и токенизировать наш набор данных. Это может быть сделано с помощью text.BertTokenizer , который является text.Splitter , которые могут токенизировать предложения на подслова или wordpieces для модели BERT даной словарного запаса генерируется из алгоритма Wordpiece . Вы можете узнать больше о других tokenizers подсловной доступной в TF.Text из здесь .

Словарь может быть из ранее сгенерированной контрольной точки BERT, или вы можете создать его самостоятельно на основе собственных данных. Для целей этого примера давайте создадим игрушечный словарь:

_VOCAB = [
    # Special tokens
    b"[UNK]", b"[MASK]", b"[RANDOM]", b"[CLS]", b"[SEP]",
    # Suffixes
    b"##ack", b"##ama", b"##ger", b"##gers", b"##onge", b"##pants",  b"##uare",
    b"##vel", b"##ven", b"an", b"A", b"Bar", b"Hates", b"Mar", b"Ob",
    b"Patrick", b"President", b"Sp", b"Sq", b"bob", b"box", b"has", b"highest",
    b"is", b"office", b"the",
]

_START_TOKEN = _VOCAB.index(b"[CLS]")
_END_TOKEN = _VOCAB.index(b"[SEP]")
_MASK_TOKEN = _VOCAB.index(b"[MASK]")
_RANDOM_TOKEN = _VOCAB.index(b"[RANDOM]")
_UNK_TOKEN = _VOCAB.index(b"[UNK]")
_MAX_SEQ_LEN = 8
_MAX_PREDICTIONS_PER_BATCH = 5

_VOCAB_SIZE = len(_VOCAB)

lookup_table = tf.lookup.StaticVocabularyTable(
    tf.lookup.KeyValueTensorInitializer(
      keys=_VOCAB,
      key_dtype=tf.string,
      values=tf.range(
          tf.size(_VOCAB, out_type=tf.int64), dtype=tf.int64),
      value_dtype=tf.int64),
      num_oov_buckets=1
)

Давайте Построим text.BertTokenizer с использованием вышеуказанного словаря и разметить текстовые входы в RaggedTensor .`.

bert_tokenizer = text.BertTokenizer(lookup_table, token_out_type=tf.string)
bert_tokenizer.tokenize(examples["text_a"])
<tf.RaggedTensor [[[b'Sp', b'##onge'], [b'bob'], [b'Sq', b'##uare', b'##pants'], [b'is'], [b'an'], [b'A', b'##ven', b'##ger']], [[b'Mar', b'##vel'], [b'A', b'##ven', b'##gers']]]>
bert_tokenizer.tokenize(examples["text_b"])
<tf.RaggedTensor [[[b'Bar', b'##ack'], [b'Ob', b'##ama'], [b'is'], [b'the'], [b'President'], [b'[UNK]']], [[b'President'], [b'is'], [b'the'], [b'highest'], [b'office']]]>

Вывод текста из text.BertTokenizer позволяет нам увидеть , как текст быть лексемы, но модель требует целочисленных идентификаторов. Мы можем установить token_out_type параметры для tf.int64 , чтобы получить целое число идентификаторов (которые являются индексами в словарь).

bert_tokenizer = text.BertTokenizer(lookup_table, token_out_type=tf.int64)
segment_a = bert_tokenizer.tokenize(examples["text_a"])
segment_a
<tf.RaggedTensor [[[22, 9], [24], [23, 11, 10], [28], [14], [15, 13, 7]], [[18, 12], [15, 13, 8]]]>
segment_b = bert_tokenizer.tokenize(examples["text_b"])
segment_b
<tf.RaggedTensor [[[16, 5], [19, 6], [28], [30], [21], [0]], [[21], [28], [30], [27], [29]]]>

text.BertTokenizer возвращает RaggedTensor с формой [batch, num_tokens, num_wordpieces] . Потому что нам не нужны дополнительные num_tokens размеров для нашего текущего случая использования, мы можем объединить последние два измерения , чтобы получить RaggedTensor с формой [batch, num_wordpieces] :

segment_a = segment_a.merge_dims(-2, -1)
segment_a
<tf.RaggedTensor [[22, 9, 24, 23, 11, 10, 28, 14, 15, 13, 7], [18, 12, 15, 13, 8]]>
segment_b = segment_b.merge_dims(-2, -1)
segment_b
<tf.RaggedTensor [[16, 5, 19, 6, 28, 30, 21, 0], [21, 28, 30, 27, 29]]>

Обрезка контента

Основной вход в BERT - это объединение двух предложений. Однако BERT требует, чтобы входные данные были фиксированного размера и формы, и у нас может быть контент, превышающий наш бюджет.

Мы можем решить это с помощью text.Trimmer урезать наш контент вниз до заданного размера (один раз сцепляются вдоль последней оси). Существуют различные text.Trimmer типов выбирающих содержимого , чтобы сохранить с использованием различных алгоритмов. text.RoundRobinTrimmer , например , будет выделять квоты в равной степени для каждого сегмента , но может обрезать концы фраз. text.WaterfallTrimmer будет обрезать , начиная с конца последнего предложения.

Для нашего примера мы будем использовать RoundRobinTrimmer , который выбирает элементы из каждого сегмента в левой направо порядке.

trimmer = text.RoundRobinTrimmer(max_seq_length=[_MAX_SEQ_LEN])
trimmed = trimmer.trim([segment_a, segment_b])
trimmed
[<tf.RaggedTensor [[22, 9, 24, 23], [18, 12, 15, 13]]>,
 <tf.RaggedTensor [[16, 5, 19, 6], [21, 28, 30, 27]]>]

trimmed теперь содержит сегменты , где число элементов всей партии составляет 8 элементов (при конкатенации вдоль оси = -1).

Объединение сегментов

Теперь, когда мы сегменты обрезаны, мы можем объединить их вместе , чтобы получить один RaggedTensor . БЕРТ использует специальные маркеры для обозначения начала ( [CLS] ) и конец сегмента ( [SEP] ). Мы также нужен RaggedTensor , указывающий , какие элементы в сочетании Tensor принадлежит к какому сегменту. Мы можем использовать text.combine_segments() , чтобы получить оба этих Tensor со специальными маркерами вставляемых.

segments_combined, segments_ids = text.combine_segments(
  [segment_a, segment_b],
  start_of_sequence_id=_START_TOKEN, end_of_segment_id=_END_TOKEN)
segments_combined, segments_ids
(<tf.RaggedTensor [[3, 22, 9, 24, 23, 11, 10, 28, 14, 15, 13, 7, 4, 16, 5, 19, 6, 28, 30, 21, 0, 4], [3, 18, 12, 15, 13, 8, 4, 21, 28, 30, 27, 29, 4]]>,
 <tf.RaggedTensor [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]]>)

Задача маскированной языковой модели

Теперь, когда у нас есть наши основные входы, мы можем начать извлекать ресурсы , необходимые для «Маске LM и маскирования процедуры» Задача , описанная в BERT: Предварительное обучение глубоких двунаправленных Трансформаторы для понимания языка

Задача модели маскированного языка имеет две подзадачи, над которыми мы должны подумать: (1) какие элементы выбрать для маскирования и (2) какие значения им присваиваются?

Выбор предмета

Потому что мы выберем для выбора элементов в случайном порядке для маскировки, мы будем использовать text.RandomItemSelector . RandomItemSelector случайным образом выбирает элементы в пакетном с учетом ограничений , заданных ( max_selections_per_batch , selection_rate и unselectable_ids ) , и возвращает логическое значение , указывающее маски , которые были выбраны пункты.

random_selector = text.RandomItemSelector(
    max_selections_per_batch=_MAX_PREDICTIONS_PER_BATCH,
    selection_rate=0.2,
    unselectable_ids=[_START_TOKEN, _END_TOKEN, _UNK_TOKEN]
)
selected = random_selector.get_selection_mask(
    segments_combined, axis=1)
selected
<tf.RaggedTensor [[False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, True, True, True, False, False], [False, False, False, False, False, True, False, False, False, False, False, True, False]]>

Выбор маскированного значения

Методология, описанная в оригинальном документе BERT для выбора значения маскирования, выглядит следующим образом:

Для mask_token_rate времени, замените элемент с [MASK] лексем:

"my dog is hairy" -> "my dog is [MASK]"

Для random_token_rate времени, замените элемент со случайным словом:

"my dog is hairy" -> "my dog is apple"

Для 1 - mask_token_rate - random_token_rate времени, сохранить этот пункт без изменений:

"my dog is hairy" -> "my dog is hairy."

text.MaskedValuesChooser инкапсулирует эту логику и может быть использован для нашей функции предварительной обработки. Вот пример того , что MaskValuesChooser возвращает дано mask_token_rate 80% и по умолчанию random_token_rate :

input_ids = tf.ragged.constant([[19, 7, 21, 20, 9, 8], [13, 4, 16, 5], [15, 10, 12, 11, 6]])
mask_values_chooser = text.MaskValuesChooser(_VOCAB_SIZE, _MASK_TOKEN, 0.8)
mask_values_chooser.get_mask_values(input_ids)
<tf.RaggedTensor [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1], [1, 10, 1, 1, 6]]>

При поставке с RaggedTensor входом, text.MaskValuesChooser возвращает RaggedTensor той же формы либо с _MASK_VALUE (0), случайный ID, или же неизменным идентификатором.

Генерация входных данных для задачи маскированной языковой модели

Теперь, когда мы имеем RandomItemSelector , чтобы помочь нам выбрать элементы для маскировки и text.MaskValuesChooser присвоить значения, мы можем использовать text.mask_language_model() , чтобы собрать все входы этой задачи для нашей модели BERT.

masked_token_ids, masked_pos, masked_lm_ids = text.mask_language_model(
  segments_combined,
  item_selector=random_selector, mask_values_chooser=mask_values_chooser)
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.7/site-packages/tensorflow/python/util/dispatch.py:206: batch_gather (from tensorflow.python.ops.array_ops) is deprecated and will be removed after 2017-10-25.
Instructions for updating:
`tf.batch_gather` is deprecated, please use `tf.gather` with `batch_dims=-1` instead.

Давайте погрузимся глубже и исследовать выходы mask_language_model() . Выход masked_token_ids является:

masked_token_ids
<tf.RaggedTensor [[3, 22, 1, 24, 23, 1, 10, 28, 1, 15, 1, 7, 4, 16, 5, 19, 6, 28, 30, 21, 0, 4], [3, 18, 12, 15, 13, 1, 4, 21, 28, 30, 27, 1, 4]]>

Помните, что наш ввод кодируется с использованием словаря. Если расшифровать masked_token_ids , используя наш словарный запас, мы получим:

tf.gather(_VOCAB, masked_token_ids)
<tf.RaggedTensor [[b'[CLS]', b'Sp', b'[MASK]', b'bob', b'Sq', b'[MASK]', b'##pants', b'is', b'[MASK]', b'A', b'[MASK]', b'##ger', b'[SEP]', b'Bar', b'##ack', b'Ob', b'##ama', b'is', b'the', b'President', b'[UNK]', b'[SEP]'], [b'[CLS]', b'Mar', b'##vel', b'A', b'##ven', b'[MASK]', b'[SEP]', b'President', b'is', b'the', b'highest', b'[MASK]', b'[SEP]']]>

Обратите внимание , что некоторые wordpiece маркеры были заменены либо [MASK] , [RANDOM] или другое значение ID. masked_pos выход дает нам индексы (в соответствующей партии) лексем , которые были заменены.

masked_pos
<tf.RaggedTensor [[2, 5, 8, 10], [5, 11]]>

masked_lm_ids дает первоначальное значение маркеров.

masked_lm_ids
<tf.RaggedTensor [[9, 11, 14, 13], [8, 29]]>

Здесь мы снова можем декодировать идентификаторы, чтобы получить удобочитаемые значения.

tf.gather(_VOCAB, masked_lm_ids)
<tf.RaggedTensor [[b'##onge', b'##uare', b'an', b'##ven'], [b'##gers', b'office']]>

Padding Model Inputs

Теперь, когда у нас есть все материалы для нашей модели, последний шаг в нашей предварительной обработке, чтобы упаковать их в фиксированную 2-мерной Tensor s с прокладкой , а также генерировать маску Tensor с указанием значения , которые являются значениями пэдов. Мы можем использовать text.pad_model_inputs() , чтобы помочь нам с этой задачей.

# Prepare and pad combined segment inputs
input_word_ids, input_mask = text.pad_model_inputs(
  masked_token_ids, max_seq_length=_MAX_SEQ_LEN)
input_type_ids, _ = text.pad_model_inputs(
  masked_token_ids, max_seq_length=_MAX_SEQ_LEN)

# Prepare and pad masking task inputs
masked_lm_positions, masked_lm_weights = text.pad_model_inputs(
  masked_token_ids, max_seq_length=_MAX_PREDICTIONS_PER_BATCH)
masked_lm_ids, _ = text.pad_model_inputs(
  masked_lm_ids, max_seq_length=_MAX_PREDICTIONS_PER_BATCH)

model_inputs = {
    "input_word_ids": input_word_ids,
    "input_mask": input_mask,
    "input_type_ids": input_type_ids,
    "masked_lm_ids": masked_lm_ids,
    "masked_lm_positions": masked_lm_positions,
    "masked_lm_weights": masked_lm_weights,
}
model_inputs
{'input_word_ids': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[ 3, 22,  1, 24, 23,  1, 10, 28],
        [ 3, 18, 12, 15, 13,  1,  4, 21]])>,
 'input_mask': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1]])>,
 'input_type_ids': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[ 3, 22,  1, 24, 23,  1, 10, 28],
        [ 3, 18, 12, 15, 13,  1,  4, 21]])>,
 'masked_lm_ids': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[ 9, 11, 14, 13,  0],
        [ 8, 29,  0,  0,  0]])>,
 'masked_lm_positions': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[ 3, 22,  1, 24, 23],
        [ 3, 18, 12, 15, 13]])>,
 'masked_lm_weights': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1]])>}

Обзор

Давайте рассмотрим, что у нас есть на данный момент, и соберем нашу функцию предварительной обработки. Вот что у нас есть:

def bert_pretrain_preprocess(vocab_table, features):
  # Input is a string Tensor of documents, shape [batch, 1].
  text_a = features["text_a"]
  text_b = features["text_b"]

  # Tokenize segments to shape [num_sentences, (num_words)] each.
  tokenizer = text.BertTokenizer(
      vocab_table,
      token_out_type=tf.int64)
  segments = [tokenizer.tokenize(text).merge_dims(
      1, -1) for text in (text_a, text_b)]

  # Truncate inputs to a maximum length.
  trimmer = text.RoundRobinTrimmer(max_seq_length=6)
  trimmed_segments = trimmer.trim(segments)

  # Combine segments, get segment ids and add special tokens.
  segments_combined, segment_ids = text.combine_segments(
      trimmed_segments,
      start_of_sequence_id=_START_TOKEN,
      end_of_segment_id=_END_TOKEN)

  # Apply dynamic masking task.
  masked_input_ids, masked_lm_positions, masked_lm_ids = (
      text.mask_language_model(
        segments_combined,
        random_selector,
        mask_values_chooser,
      )
  )

  # Prepare and pad combined segment inputs
  input_word_ids, input_mask = text.pad_model_inputs(
    masked_input_ids, max_seq_length=_MAX_SEQ_LEN)
  input_type_ids, _ = text.pad_model_inputs(
    masked_input_ids, max_seq_length=_MAX_SEQ_LEN)

  # Prepare and pad masking task inputs
  masked_lm_positions, masked_lm_weights = text.pad_model_inputs(
    masked_input_ids, max_seq_length=_MAX_PREDICTIONS_PER_BATCH)
  masked_lm_ids, _ = text.pad_model_inputs(
    masked_lm_ids, max_seq_length=_MAX_PREDICTIONS_PER_BATCH)

  model_inputs = {
      "input_word_ids": input_word_ids,
      "input_mask": input_mask,
      "input_type_ids": input_type_ids,
      "masked_lm_ids": masked_lm_ids,
      "masked_lm_positions": masked_lm_positions,
      "masked_lm_weights": masked_lm_weights,
  }
  return model_inputs

Ранее мы построили tf.data.Dataset и теперь мы можем использовать собранные предобработки функции bert_pretrain_preprocess() в Dataset.map() . Это позволяет нам создать входной конвейер для преобразования наших необработанных строковых данных в целочисленные входные данные и напрямую передавать их в нашу модель.

dataset = tf.data.Dataset.from_tensors(examples)
dataset = dataset.map(functools.partial(
    bert_pretrain_preprocess, lookup_table))

next(iter(dataset))
{'input_word_ids': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[ 3, 22,  9,  1,  4, 16,  5, 19],
        [ 3, 18,  1, 15,  4,  1, 28, 30]])>,
 'input_mask': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1]])>,
 'input_type_ids': <tf.Tensor: shape=(2, 8), dtype=int64, numpy=
 array([[ 3, 22,  9,  1,  4, 16,  5, 19],
        [ 3, 18,  1, 15,  4,  1, 28, 30]])>,
 'masked_lm_ids': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[24, 19,  0,  0,  0],
        [12, 21,  0,  0,  0]])>,
 'masked_lm_positions': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[ 3, 22,  9,  1,  4],
        [ 3, 18,  1, 15,  4]])>,
 'masked_lm_weights': <tf.Tensor: shape=(2, 5), dtype=int64, numpy=
 array([[1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1]])>}
  • Классифицировать текст с Бертом - Учебник о том , как использовать pretrained модель BERT для классификации текста. Это хорошее продолжение, когда вы знакомы с тем, как предварительно обрабатывать входные данные, используемые моделью BERT.

  • Tokenizing с TF Текст - Учебник подробно различные типы tokenizers , которые существуют в TF.Text.

  • Обработка текста с RaggedTensor - Подробное руководство о том , как создавать, использовать и управлять RaggedTensor s.