Pré-processamento de BERT com texto TF

Ver no TensorFlow.org Executar no Google Colab Ver no GitHub Baixar caderno

Visão geral

O pré-processamento de texto é a transformação de ponta a ponta do texto bruto em entradas inteiras de um modelo. Os modelos de PNL geralmente são acompanhados por várias centenas (senão milhares) de linhas de código Python para texto de pré-processamento. O pré-processamento de texto costuma ser um desafio para os modelos porque:

  • Desvio de treinamento para servir. Torna-se cada vez mais difícil garantir que a lógica de pré-processamento das entradas do modelo seja consistente em todos os estágios de desenvolvimento do modelo (por exemplo, pré-treinamento, ajuste fino, avaliação, inferência). Usar diferentes hiperparâmetros, tokenização, algoritmos de pré-processamento de string ou simplesmente empacotar entradas de modelo de maneira inconsistente em diferentes estágios pode gerar efeitos desastrosos e difíceis de depurar para o modelo.

  • Eficiência e flexibilidade. Embora o pré-processamento possa ser feito offline (por exemplo, gravando as saídas processadas em arquivos no disco e, em seguida, reconsumindo os dados pré-processados ​​no pipeline de entrada), este método incorre em um custo adicional de leitura e gravação de arquivo. O pré-processamento offline também é inconveniente se houver decisões de pré-processamento que precisem acontecer dinamicamente. Experimentar uma opção diferente exigiria regenerar o conjunto de dados novamente.

  • Interface de modelo complexo. Os modelos de texto são muito mais compreensíveis quando suas entradas são texto puro. É difícil entender um modelo quando suas entradas exigem uma etapa extra de codificação indireta. A redução da complexidade do pré-processamento é especialmente apreciada para depuração, veiculação e avaliação de modelo.

Além disso, interfaces de modelo mais simples também tornam mais conveniente tentar o modelo (por exemplo, inferência ou treinamento) em conjuntos de dados diferentes e inexplorados.

Pré-processamento de texto com TF.Text

Usando as APIs de pré-processamento de texto do TF.Text, podemos construir uma função de pré-processamento que pode transformar o conjunto de dados de texto de um usuário em entradas inteiras do modelo. Os usuários podem empacotar o pré-processamento diretamente como parte de seu modelo para aliviar os problemas mencionados acima.

Este tutorial irá mostrar como usar TF.Text ops de pré-processamento para transformar dados de texto em entradas para o modelo de BERT e insumos para a linguagem de máscara pré-treinamento tarefa descrita em "Masked LM e Masking Procedure" de BERT: Pré-formação do Deep bidirecionais Transformadores para Idioma entendimento . O processo envolve tokenizar o texto em unidades de subpalavra, combinar frases, cortar o conteúdo para um tamanho fixo e extrair rótulos para a tarefa de modelagem de linguagem mascarada.

Configurar

Vamos importar os pacotes e bibliotecas de que precisamos primeiro.

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

Nossos dados contém dois recursos de texto e podemos criar um exemplo tf.data.Dataset . Nosso objetivo é criar uma função que nós podemos fornecer Dataset.map() com para ser usado em treinamento.

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.'>}

Tokenização

Nossa primeira etapa é executar qualquer pré-processamento de string e tokenizar nosso conjunto de dados. Isso pode ser feito usando o text.BertTokenizer , que é um text.Splitter que podem tokenizar sentenças em subpalavras ou wordpieces para o modelo BERT dado um vocabulário gerado a partir do algoritmo Wordpiece . Você pode aprender mais sobre outras tokenizers subword disponíveis no TF.Text de aqui .

O vocabulário pode ser de um ponto de verificação de BERT gerado anteriormente ou você mesmo pode gerar um com seus próprios dados. Para os fins deste exemplo, vamos criar um vocabulário de brinquedos:

_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
)

Vamos construir um text.BertTokenizer usando o vocabulário acima e tokenizar as entradas de texto em um 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']]]>

Saída de texto a partir text.BertTokenizer permite-nos ver como o texto está sendo indexado, mas o modelo exige inteiros IDs. Podemos definir a token_out_type param a tf.int64 obter identificações inteiro (que são os índices na vocabulário).

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 retorna um RaggedTensor com forma [batch, num_tokens, num_wordpieces] . Porque nós não precisamos dos extras num_tokens dimensões para o nosso caso de uso atual, podemos mesclar as duas últimas dimensões para obter um RaggedTensor com forma [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]]>

Corte de conteúdo

A principal entrada do BERT é uma concatenação de duas sentenças. No entanto, o BERT exige que os insumos tenham tamanho e formato fixos e podemos ter conteúdo que exceda o nosso orçamento.

Podemos resolver isso usando um text.Trimmer para aparar o nosso baixo conteúdo para um tamanho pré-determinado (uma vez concatenadas ao longo do último eixo). Existem diferentes text.Trimmer tipos que selecionam o conteúdo para preservar o uso de diferentes algoritmos. text.RoundRobinTrimmer por exemplo, irá atribuir, igualmente, para cada segmento, mas pode aparar as extremidades de frases. text.WaterfallTrimmer vai cortar a partir do final da última frase.

Para o nosso exemplo, vamos usar RoundRobinTrimmer que seleciona os itens de cada segmento de forma da esquerda para a direita.

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 agora contém os segmentos em que o número de elementos através de um lote é de 8 elementos (quando concatenada ao longo do eixo = -1).

Combinando segmentos

Agora que temos segmentos aparados, podemos combiná-los para obter um único RaggedTensor . BERT utiliza fichas especiais para indicar o início ( [CLS] ) e a extremidade de um segmento ( [SEP] ). Precisamos também de um RaggedTensor indicando quais itens na combinadas Tensor pertencem a qual segmento. Podemos usar text.combine_segments() para obter esses dois Tensor com fichas especiais inseridos.

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]]>)

Tarefa de modelo de linguagem mascarada

Agora que temos nossos insumos básicos, podemos começar a extrair os insumos necessários para a "Masked LM e Masking Procedure" funções descritas no BERT: Pré-formação do Deep bidirecionais Transformadores para compreensão da linguagem

A tarefa do modelo de linguagem mascarada tem dois subproblemas para pensarmos sobre: ​​(1) quais itens selecionar para mascaramento e (2) quais valores são atribuídos a eles?

Seleção de Item

Porque vamos escolher para selecionar itens aleatoriamente para mascarar, usaremos um text.RandomItemSelector . RandomItemSelector selecciona aleatoriamente itens em um lote sujeito a restrições dadas ( max_selections_per_batch , selection_rate e unselectable_ids ) e retorna uma máscara booleano indicando que os itens foram selecionados.

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]]>

Escolha do valor mascarado

A metodologia descrita no artigo original de BERT para a escolha do valor de mascaramento é a seguinte:

Para mask_token_rate das vezes, substituir o item com o [MASK] token:

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

Para random_token_rate das vezes, substituir o item com uma palavra aleatória:

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

Para 1 - mask_token_rate - random_token_rate do tempo, manter o item inalterada:

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

text.MaskedValuesChooser encapsula esta lógica e pode ser usado para nossa função de pré-processamento. Aqui está um exemplo do que MaskValuesChooser retornos dado um mask_token_rate de 80% e padrão 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]]>

Quando fornecido com um RaggedTensor entrada, text.MaskValuesChooser retorna um RaggedTensor com a mesma forma com qualquer _MASK_VALUE (0), um código aleatório, ou o mesmo ID de inalterado.

Gerando Entradas para Tarefa de Modelo de Linguagem Mascarada

Agora que temos um RandomItemSelector para nos ajudar a selecionar os itens para mascarar e text.MaskValuesChooser para atribuir os valores, podemos usar text.mask_language_model() para montar todas as entradas desta tarefa para o nosso modelo 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.

Vamos mergulhar mais fundo e examinar as saídas de mask_language_model() . A saída do 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]]>

Lembre-se de que nossa entrada é codificada usando um vocabulário. Se decodificar masked_token_ids usando nosso vocabulário, temos:

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]']]>

Note-se que algumas fichas wordpiece foram substituídos com um ou outro [MASK] , [RANDOM] ou um valor de ID diferente. masked_pos saída nos dá os índices (no respectivo lote) das fichas que foram substituídos.

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

masked_lm_ids nos dá o valor original do token.

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

Podemos novamente decodificar os IDs aqui para obter valores legíveis por humanos.

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

Entradas de modelo de preenchimento

Agora que temos todas as entradas para o nosso modelo, o último passo em nosso pré-processamento é empacotá-los em 2-dimensional fixo Tensor s com estofamento e também gerar uma máscara Tensor indicando os valores que são valores almofada. Podemos usar text.pad_model_inputs() para nos ajudar com esta tarefa.

# 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]])>}

Análise

Vamos revisar o que temos até agora e montar nossa função de pré-processamento. Aqui está o que temos:

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

Nós já construiu um tf.data.Dataset e agora podemos usar nossa função de pré-processamento montado bert_pretrain_preprocess() em Dataset.map() . Isso nos permite criar um pipeline de entrada para transformar nossos dados de string brutos em entradas inteiras e alimentar diretamente em nosso modelo.

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]])>}
  • Texto classificar com BERT - Um tutorial sobre como usar um modelo BERT pré-treinado para texto classificar. Este é um bom acompanhamento, agora que você está familiarizado com como pré-processar as entradas usadas pelo modelo BERT.

  • Tokenizing com TF Texto - Tutorial detalhando os diferentes tipos de tokenizers que existem em TF.Text.

  • Manipulação de Texto RaggedTensor - guia detalhado sobre como criar, usar e manipular RaggedTensor s.