Wstępne przetwarzanie BERT z tekstem TF

Zobacz na TensorFlow.org Uruchom w Google Colab Zobacz na GitHub Pobierz notatnik

Przegląd

Wstępne przetwarzanie tekstu to kompleksowe przekształcenie nieprzetworzonego tekstu na dane wejściowe modelu w postaci liczb całkowitych. Modelom NLP często towarzyszy kilkaset (jeśli nie tysiące) wierszy kodu Pythona do wstępnego przetwarzania tekstu. Wstępne przetwarzanie tekstu jest często wyzwaniem dla modeli, ponieważ:

  • Pochylenie obsługi treningowej. Coraz trudniej jest zapewnić, że logika przetwarzania wstępnego danych wejściowych modelu jest spójna na wszystkich etapach tworzenia modelu (np. uczenie wstępne, dostrajanie, ocena, wnioskowanie). Korzystanie z różnych hiperparametrów, tokenizacja, algorytmy wstępnego przetwarzania ciągów lub po prostu niespójne pakowanie danych wejściowych modelu na różnych etapach może spowodować trudne do debugowania i katastrofalne skutki dla modelu.

  • Wydajność i elastyczność. Chociaż wstępne przetwarzanie można wykonać w trybie offline (np. poprzez zapisanie przetworzonych danych wyjściowych do plików na dysku, a następnie ponowne wykorzystanie tych wstępnie przetworzonych danych w potoku wejściowym), ta metoda wiąże się z dodatkowym kosztem odczytu i zapisu plików. Wstępne przetwarzanie offline jest również niewygodne, jeśli istnieją decyzje dotyczące wstępnego przetwarzania, które muszą być realizowane dynamicznie. Eksperymentowanie z inną opcją wymagałoby ponownego wygenerowania zestawu danych.

  • Złożony interfejs modelu. Modele tekstowe są znacznie bardziej zrozumiałe, gdy ich dane wejściowe to czysty tekst. Trudno jest zrozumieć model, którego dane wejściowe wymagają dodatkowego, pośredniego kroku kodowania. Zmniejszenie złożoności przetwarzania wstępnego jest szczególnie doceniane w przypadku debugowania, udostępniania i oceny modeli.

Ponadto prostsze interfejsy modelu ułatwiają również wypróbowanie modelu (np. wnioskowanie lub uczenie) na różnych, niezbadanych zestawach danych.

Wstępne przetwarzanie tekstu za pomocą TF.Text

Korzystając z interfejsów API przetwarzania wstępnego tekstu TF.Text, możemy skonstruować funkcję przetwarzania wstępnego, która może przekształcić zbiór danych tekstowych użytkownika w dane wejściowe w postaci liczb całkowitych. Użytkownicy mogą pakować przetwarzanie wstępne bezpośrednio jako część swojego modelu, aby złagodzić wyżej wymienione problemy.

Ten poradnik pokaże jak korzystać TF.Text przerób ops przekształcić dane tekstowe do wejścia do modelu BERT i wejść do języka maskujących pretraining zadanie opisane w „Zamaskowany LM i maskowanie postępowania” z BERT: pre-szkolenia głębokiego dwukierunkowych Transformers dla języka zrozumienie . Proces obejmuje tokenizację tekstu na jednostki podsłów, łączenie zdań, przycinanie treści do ustalonego rozmiaru i wyodrębnianie etykiet dla zadania modelowania języka maskowanego.

Ustawiać

Najpierw zaimportujmy pakiety i biblioteki, których potrzebujemy.

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

Nasze dane zawiera dwie funkcje tekstowe i możemy stworzyć przykład tf.data.Dataset . Naszym celem jest stworzenie funkcji, które możemy dostarczyć Dataset.map() z być stosowane w treningu.

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

Tokenizacja

Naszym pierwszym krokiem jest uruchomienie przetwarzania wstępnego dowolnego ciągu i tokenizacja naszego zestawu danych. Można to zrobić za pomocą text.BertTokenizer , który jest text.Splitter że może tokenize zdań na język subwords lub wordpieces dla modelu BERT danego słownictwo generowane przez algorytm Wordpiece . Możesz dowiedzieć się więcej o innych tokenizers podsłowo dostępnych w TF.Text z tutaj .

Słownictwo może pochodzić z wcześniej wygenerowanego punktu kontrolnego BERT lub możesz wygenerować je samodzielnie na podstawie własnych danych. Na potrzeby tego przykładu utwórzmy słownictwo zabawek:

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

Konstrukt Let to text.BertTokenizer stosując powyższą słownictwo i tokenize wejść tekst do 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']]]>

Wyjście tekst z text.BertTokenizer pozwala nam zobaczyć, jak tekst jest tokenized, ale model wymaga Integer identyfikatory. Można ustawić token_out_type param do tf.int64 uzyskać całkowitą identyfikatorów (które są wskaźniki do słownika).

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 zwraca RaggedTensor w kształcie [batch, num_tokens, num_wordpieces] . Ponieważ nie potrzebujemy dodatkowych num_tokens wymiary dla naszego obecnego przypadku użycia, możemy połączyć dwa ostatnie wymiary, aby uzyskać RaggedTensor z kształtu [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]]>

Przycinanie treści

Głównym wejściem do BERT jest konkatenacja dwóch zdań. Jednak BERT wymaga, aby dane wejściowe miały stałą wielkość i kształt, a my możemy mieć treści przekraczające nasz budżet.

Możemy zająć się tym za pomocą text.Trimmer przyciąć nasze treści w dół do określonej wielkości (raz łączone wzdłuż ostatniej osi). Istnieją różne text.Trimmer typy, które Wybierz zawartość do zachowania przy użyciu różnych algorytmów. text.RoundRobinTrimmer np przeznaczy kwotę równo dla każdego segmentu, ale może przyciąć końce zdań. text.WaterfallTrimmer będzie przyciąć począwszy od końca ostatniego zdania.

W naszym przykładzie użyjemy RoundRobinTrimmer elementów, które wybiera z każdego segmentu w lewy-prawy sposób.

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 już zawiera segmenty, w których liczba elementów w partii wynosi 8 elementy (gdy łączone wzdłuż osi = 1).

Łączenie segmentów

Teraz, gdy mamy segmenty przycięte, możemy połączyć je ze sobą, aby uzyskać pojedynczą RaggedTensor . BERT wykorzystuje specjalne znaki do wskazania początku ( [CLS] ) i koniec segmentu ( [SEP] ). Potrzebujemy też RaggedTensor wskazujące, które elementy w połączeniu Tensor należeć do którego segmentu. Możemy użyć text.combine_segments() , aby uzyskać zarówno tych Tensor ze specjalnymi tokenami wstawionych.

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

Zadanie modelu języka maskowanego

Teraz, gdy mamy nasze podstawowe wejść, możemy zacząć, aby wyodrębnić nakładów potrzebnych do „zamaskowane LM i maskowanie postępowania” zadanie opisane w BERT: Wstępne szkolenie Deep dwukierunkowych Transformers dla rozumienia języka

Zadanie zamaskowanego modelu języka ma dwa podproblemy, nad którymi powinniśmy się zastanowić: (1) jakie elementy wybrać do zamaskowania i (2) jakie wartości są im przypisane?

Wybór pozycji

Ponieważ będziemy wybierać, aby wybrać elementy losowo do maskowania, użyjemy text.RandomItemSelector . RandomItemSelector losowo wybiera przedmioty u osobnika wsadowym do ograniczeń podanych ( max_selections_per_batch , selection_rate i unselectable_ids ) i zwraca wartość logiczną maski wskazujące, które zostały wybrane pozycje.

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

Wybór wartości maskowanej

Metodologia opisana w oryginalnym dokumencie BERT dotyczącym wyboru wartości maskowania jest następująca:

Dla mask_token_rate czasu, wymienić element z [MASK] tokena:

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

Dla random_token_rate czasu, wymienić element z losowym wyrazem:

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

Dla 1 - mask_token_rate - random_token_rate czasu, utrzymać pozycję na niezmienionym poziomie:

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

text.MaskedValuesChooser obudowuje tę logikę i mogą być wykorzystane do naszej funkcji przetwarzania wstępnego. Oto przykład tego, co MaskValuesChooser powraca dali mask_token_rate 80% i domyślna 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]]>

W przypadku zasilania RaggedTensor wejściu, text.MaskValuesChooser zwraca RaggedTensor tego samego kształtu albo z _MASK_VALUE (0), a losowy numer identyfikacyjny, lub tej samej niezmienionej id.

Generowanie danych wejściowych dla zadania modelu języka maskowanego

Teraz, gdy mamy RandomItemSelector pomóc nam wybrać elementy do maskowania i text.MaskValuesChooser przypisać wartości, możemy użyć text.mask_language_model() , aby zebrać wszystkie wejścia tego zadania dla naszego modelu 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.

Chodźmy nurkować głębiej i zbadanie wyjścia mask_language_model() . Wyjście masked_token_ids jest:

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

Pamiętaj, że nasze wejście jest kodowane przy użyciu słownictwa. Gdybyśmy dekodować masked_token_ids pomocą naszego słownictwa, otrzymujemy:

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

Należy zauważyć, że pewne znaczniki wordpiece zastąpiono albo [MASK] , [RANDOM] lub różne wartości identyfikatora. masked_pos wyjście daje nam indeksy (w odpowiedniej partii) z tokenów, które zostały zastąpione.

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

masked_lm_ids daje nam pierwotną wartość tokenu.

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

Możemy ponownie zdekodować identyfikatory tutaj, aby uzyskać wartości czytelne dla człowieka.

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

Wejścia modelu dopełniania

Teraz, gdy mamy wszystkie wejścia do naszego modelu, ostatni krok w naszej wyprzedzającym jest pakowanie ich w stałej 2-wymiarowej Tensor s z wyściółką, a także generowania maski Tensor wskazujący wartości, które są wartościami kłódek. Możemy użyć text.pad_model_inputs() , aby pomóc nam w realizacji tego zadania.

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

Recenzja

Przyjrzyjmy się, co mamy do tej pory i zmontujmy naszą funkcję przetwarzania wstępnego. Oto, co mamy:

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

Wcześniej skonstruował tf.data.Dataset i możemy teraz skorzystać z naszej zmontowany przebiegu wyprzedzającego funkcji bert_pretrain_preprocess() w Dataset.map() . Dzięki temu możemy utworzyć potok wejściowy do przekształcania naszych nieprzetworzonych danych łańcuchowych na dane wejściowe liczb całkowitych i przesyłać je bezpośrednio do naszego modelu.

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]])>}
  • Tekst klasyfikować z Bertem - tutorial jak korzystać z pretrained modelu BERT tekstu sklasyfikować. Jest to miłe uzupełnienie teraz, gdy znasz już sposób wstępnego przetwarzania danych wejściowych używanych przez model BERT.

  • Tokenizing z TF Tekst - Tutorial szczegółowo różne rodzaje tokenizers które istnieją w TF.Text.

  • Obchodzenie Tekst z RaggedTensor - szczegółowy przewodnik na temat tworzenia, wykorzystania i manipulowania RaggedTensor s.