Tokenizadores de subpalavra

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

Este tutorial demonstra como gerar um vocabulário subword a partir de um conjunto de dados, e usá-lo para construir uma text.BertTokenizer do vocabulário.

A principal vantagem de um tokenizer de subpalavra é que ele interpola entre a tokenização baseada em palavras e baseada em caracteres. Palavras comuns ocupam um espaço no vocabulário, mas o tokenizer pode recorrer a pedaços de palavras e caracteres individuais para palavras desconhecidas.

Visão geral

O tensorflow_text pacote inclui implementações TensorFlow de muitos tokenizers comuns. Isso inclui três tokenizadores de estilo de subpalavra:

  • text.BertTokenizer - O BertTokenizer classe é uma interface de nível superior. Ele inclui algoritmo de divisão simbólica do BERT e uma WordPieceTokenizer . Leva frases como entrada e retorna token-IDs.
  • text.WordpeiceTokenizer - O WordPieceTokenizer classe é uma interface de nível inferior. Ele só implementa o algoritmo WordPiece . Você deve padronizar e dividir o texto em palavras antes de chamá-lo. É preciso palavras como entrada e retorna token-IDs.
  • text.SentencepieceTokenizer - O SentencepieceTokenizer requer uma configuração mais complexa. Seu inicializador requer um modelo de frase pré-treinado. Veja o repositório google / sentencepiece para obter instruções sobre como construir um destes modelos. Ele pode aceitar frases como entrada quando tokenizing.

Este tutorial constrói um vocabulário Wordpiece de cima para baixo, começando com palavras existentes. Esse processo não funciona para japonês, chinês ou coreano, pois esses idiomas não têm unidades claras de vários caracteres. Para tokenizar línguas conside usando text.SentencepieceTokenizer , text.UnicodeCharTokenizer ou esta abordagem .

Configurar

pip install -q -U tensorflow-text
pip install -q tensorflow_datasets
import collections
import os
import pathlib
import re
import string
import sys
import tempfile
import time

import numpy as np
import matplotlib.pyplot as plt

import tensorflow_datasets as tfds
import tensorflow_text as text
import tensorflow as tf
2021-08-11 18:46:18.414452: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcudart.so.11.0
tf.get_logger().setLevel('ERROR')
pwd = pathlib.Path.cwd()

Baixe o conjunto de dados

Buscar o Português / Inglês dataset tradução do TFDS :

examples, metadata = tfds.load('ted_hrlr_translate/pt_to_en', with_info=True,
                               as_supervised=True)
train_examples, val_examples = examples['train'], examples['validation']
2021-08-11 18:46:23.603290: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcuda.so.1
2021-08-11 18:46:24.267575: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-11 18:46:24.268580: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1733] Found device 0 with properties: 
pciBusID: 0000:00:05.0 name: Tesla V100-SXM2-16GB computeCapability: 7.0
coreClock: 1.53GHz coreCount: 80 deviceMemorySize: 15.78GiB deviceMemoryBandwidth: 836.37GiB/s
2021-08-11 18:46:24.268617: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcudart.so.11.0
2021-08-11 18:46:24.271953: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcublas.so.11
2021-08-11 18:46:24.272053: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcublasLt.so.11
2021-08-11 18:46:24.273192: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcufft.so.10
2021-08-11 18:46:24.273527: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcurand.so.10
2021-08-11 18:46:24.274580: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcusolver.so.11
2021-08-11 18:46:24.275559: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcusparse.so.11
2021-08-11 18:46:24.275779: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcudnn.so.8
2021-08-11 18:46:24.275884: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-11 18:46:24.276973: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-11 18:46:24.277878: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1871] Adding visible gpu devices: 0
2021-08-11 18:46:24.278525: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 AVX512F FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2021-08-11 18:46:24.279136: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-11 18:46:24.280033: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1733] Found device 0 with properties: 
pciBusID: 0000:00:05.0 name: Tesla V100-SXM2-16GB computeCapability: 7.0
coreClock: 1.53GHz coreCount: 80 deviceMemorySize: 15.78GiB deviceMemoryBandwidth: 836.37GiB/s
2021-08-11 18:46:24.280116: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-11 18:46:24.281009: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-11 18:46:24.281856: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1871] Adding visible gpu devices: 0
2021-08-11 18:46:24.281894: I tensorflow/stream_executor/platform/default/dso_loader.cc:53] Successfully opened dynamic library libcudart.so.11.0
2021-08-11 18:46:24.912068: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1258] Device interconnect StreamExecutor with strength 1 edge matrix:
2021-08-11 18:46:24.912106: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1264]      0 
2021-08-11 18:46:24.912115: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1277] 0:   N 
2021-08-11 18:46:24.912363: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-11 18:46:24.913392: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-11 18:46:24.914368: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-08-11 18:46:24.915336: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1418] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 14646 MB memory) -> physical GPU (device: 0, name: Tesla V100-SXM2-16GB, pci bus id: 0000:00:05.0, compute capability: 7.0)

Este conjunto de dados produz pares de frases em português / inglês:

for pt, en in train_examples.take(1):
  print("Portuguese: ", pt.numpy().decode('utf-8'))
  print("English:   ", en.numpy().decode('utf-8'))
2021-08-11 18:46:25.028577: I tensorflow/compiler/mlir/mlir_graph_optimization_pass.cc:176] None of the MLIR Optimization Passes are enabled (registered 2)
2021-08-11 18:46:25.029197: I tensorflow/core/platform/profile_utils/cpu_utils.cc:114] CPU Frequency: 2000165000 Hz
Portuguese:  e quando melhoramos a procura , tiramos a única vantagem da impressão , que é a serendipidade .
English:    and when you improve searchability , you actually take away the one advantage of print , which is serendipity .

Observe algumas coisas sobre as frases de exemplo acima:

  • Eles são minúsculos.
  • Existem espaços ao redor da pontuação.
  • Não está claro se ou qual normalização Unicode está sendo usada.
train_en = train_examples.map(lambda pt, en: en)
train_pt = train_examples.map(lambda pt, en: pt)

Gere o vocabulário

Esta seção gera um vocabulário de palavras a partir de um conjunto de dados. Se você já tem um arquivo de vocabulário e só quero ver como construir um text.BertTokenizer ou text.Wordpiece tokenizer com ele, então você pode pular para a Criar o tokenizer seção.

O código de geração de vocabulário está incluído no tensorflow_text pacote pip. Não é importado por padrão, você precisa importá-lo manualmente:

from tensorflow_text.tools.wordpiece_vocab import bert_vocab_from_dataset as bert_vocab

O bert_vocab.bert_vocab_from_dataset função irá gerar o vocabulário.

Existem muitos argumentos que você pode definir para ajustar seu comportamento. Para este tutorial, você usará principalmente os padrões. Se você quer aprender mais sobre as opções, primeiro ler sobre o algoritmo , em seguida, ter um olhar para o código .

Isso leva cerca de 2 minutos.

bert_tokenizer_params=dict(lower_case=True)
reserved_tokens=["[PAD]", "[UNK]", "[START]", "[END]"]

bert_vocab_args = dict(
    # The target vocabulary size
    vocab_size = 8000,
    # Reserved tokens that must be included in the vocabulary
    reserved_tokens=reserved_tokens,
    # Arguments for `text.BertTokenizer`
    bert_tokenizer_params=bert_tokenizer_params,
    # Arguments for `wordpiece_vocab.wordpiece_tokenizer_learner_lib.learn`
    learn_params={},
)
%%time
pt_vocab = bert_vocab.bert_vocab_from_dataset(
    train_pt.batch(1000).prefetch(2),
    **bert_vocab_args
)
CPU times: user 1min 36s, sys: 2.48 s, total: 1min 39s
Wall time: 1min 33s

Aqui estão algumas fatias do vocabulário resultante.

print(pt_vocab[:10])
print(pt_vocab[100:110])
print(pt_vocab[1000:1010])
print(pt_vocab[-10:])
['[PAD]', '[UNK]', '[START]', '[END]', '!', '#', '$', '%', '&', "'"]
['no', 'por', 'mais', 'na', 'eu', 'esta', 'muito', 'isso', 'isto', 'sao']
['90', 'desse', 'efeito', 'malaria', 'normalmente', 'palestra', 'recentemente', '##nca', 'bons', 'chave']
['##–', '##—', '##‘', '##’', '##“', '##”', '##⁄', '##€', '##♪', '##♫']

Escreva um arquivo de vocabulário:

def write_vocab_file(filepath, vocab):
  with open(filepath, 'w') as f:
    for token in vocab:
      print(token, file=f)
write_vocab_file('pt_vocab.txt', pt_vocab)

Use essa função para gerar um vocabulário a partir dos dados em inglês:

%%time
en_vocab = bert_vocab.bert_vocab_from_dataset(
    train_en.batch(1000).prefetch(2),
    **bert_vocab_args
)
CPU times: user 1min 8s, sys: 1.98 s, total: 1min 10s
Wall time: 1min 5s
print(en_vocab[:10])
print(en_vocab[100:110])
print(en_vocab[1000:1010])
print(en_vocab[-10:])
['[PAD]', '[UNK]', '[START]', '[END]', '!', '#', '$', '%', '&', "'"]
['as', 'all', 'at', 'one', 'people', 're', 'like', 'if', 'our', 'from']
['choose', 'consider', 'extraordinary', 'focus', 'generation', 'killed', 'patterns', 'putting', 'scientific', 'wait']
['##_', '##`', '##ย', '##ร', '##อ', '##–', '##—', '##’', '##♪', '##♫']

Aqui estão os dois arquivos de vocabulário:

write_vocab_file('en_vocab.txt', en_vocab)
ls *.txt
en_vocab.txt  pt_vocab.txt

Construir o tokenizer

O text.BertTokenizer podem ser inicializados passando o caminho do arquivo de vocabulário como o primeiro argumento (veja a seção sobre tf.lookup para outras opções):

pt_tokenizer = text.BertTokenizer('pt_vocab.txt', **bert_tokenizer_params)
en_tokenizer = text.BertTokenizer('en_vocab.txt', **bert_tokenizer_params)

Agora você pode usá-lo para codificar algum texto. Pegue um lote de 3 exemplos dos dados em inglês:

for pt_examples, en_examples in train_examples.batch(3).take(1):
  for ex in en_examples:
    print(ex.numpy())
b'and when you improve searchability , you actually take away the one advantage of print , which is serendipity .'
b'but what if it were active ?'
b"but they did n't test for curiosity ."

Executá-lo através do BertTokenizer.tokenize método. Inicialmente, isso retorna um tf.RaggedTensor com machados (batch, word, word-piece) :

# Tokenize the examples -> (batch, word, word-piece)
token_batch = en_tokenizer.tokenize(en_examples)
# Merge the word and word-piece axes -> (batch, tokens)
token_batch = token_batch.merge_dims(-2,-1)

for ex in token_batch.to_list():
  print(ex)
[72, 117, 79, 1259, 1491, 2362, 13, 79, 150, 184, 311, 71, 103, 2308, 74, 2679, 13, 148, 80, 55, 4840, 1434, 2423, 540, 15]
[87, 90, 107, 76, 129, 1852, 30]
[87, 83, 149, 50, 9, 56, 664, 85, 2512, 15]

Se você substituir os IDs token com suas representações de texto (usando tf.gather ) você pode ver que no primeiro exemplo as palavras "searchability" e "serendipity" foram decompostos em "search ##ability" e "s ##ere ##nd ##ip ##ity" :

# Lookup each token id in the vocabulary.
txt_tokens = tf.gather(en_vocab, token_batch)
# Join with spaces.
tf.strings.reduce_join(txt_tokens, separator=' ', axis=-1)
<tf.Tensor: shape=(3,), dtype=string, numpy=
array([b'and when you improve search ##ability , you actually take away the one advantage of print , which is s ##ere ##nd ##ip ##ity .',
       b'but what if it were active ?',
       b"but they did n ' t test for curiosity ."], dtype=object)>

Para re-montar palavras a partir das fichas extraídos, utilizar o BertTokenizer.detokenize método:

words = en_tokenizer.detokenize(token_batch)
tf.strings.reduce_join(words, separator=' ', axis=-1)
<tf.Tensor: shape=(3,), dtype=string, numpy=
array([b'and when you improve searchability , you actually take away the one advantage of print , which is serendipity .',
       b'but what if it were active ?',
       b"but they did n ' t test for curiosity ."], dtype=object)>

Personalização e exportação

Este tutorial constrói o tokenizer texto e detokenizer usado pelo Transformer tutorial. Esta seção acrescenta métodos e etapas de processamento para simplificar esse tutorial, e exporta o tokenizers usando tf.saved_model para que possam ser importados por outros tutoriais.

Tokenização personalizada

Os tutoriais jusante ambos espera que o texto tokenized para incluir [START] e [END] fichas.

O reserved_tokens espaço reserva no início do vocabulário, então [START] e [END] têm os mesmos índices para ambas as línguas:

START = tf.argmax(tf.constant(reserved_tokens) == "[START]")
END = tf.argmax(tf.constant(reserved_tokens) == "[END]")

def add_start_end(ragged):
  count = ragged.bounding_shape()[0]
  starts = tf.fill([count,1], START)
  ends = tf.fill([count,1], END)
  return tf.concat([starts, ragged, ends], axis=1)
words = en_tokenizer.detokenize(add_start_end(token_batch))
tf.strings.reduce_join(words, separator=' ', axis=-1)
<tf.Tensor: shape=(3,), dtype=string, numpy=
array([b'[START] and when you improve searchability , you actually take away the one advantage of print , which is serendipity . [END]',
       b'[START] but what if it were active ? [END]',
       b"[START] but they did n ' t test for curiosity . [END]"],
      dtype=object)>

Detocagem personalizada

Antes de exportar os tokenizadores, há algumas coisas que você pode limpar para os tutoriais downstream:

  1. Eles querem gerar saída de texto limpo, por isso cair fichas reservados como [START] , [END] e [PAD] .
  2. Eles estão interessados em cordas completas, por isso, aplicar uma seqüência de juntar-se ao longo das words eixo do resultado.
def cleanup_text(reserved_tokens, token_txt):
  # Drop the reserved tokens, except for "[UNK]".
  bad_tokens = [re.escape(tok) for tok in reserved_tokens if tok != "[UNK]"]
  bad_token_re = "|".join(bad_tokens)

  bad_cells = tf.strings.regex_full_match(token_txt, bad_token_re)
  result = tf.ragged.boolean_mask(token_txt, ~bad_cells)

  # Join them into strings.
  result = tf.strings.reduce_join(result, separator=' ', axis=-1)

  return result
en_examples.numpy()
array([b'and when you improve searchability , you actually take away the one advantage of print , which is serendipity .',
       b'but what if it were active ?',
       b"but they did n't test for curiosity ."], dtype=object)
token_batch = en_tokenizer.tokenize(en_examples).merge_dims(-2,-1)
words = en_tokenizer.detokenize(token_batch)
words
<tf.RaggedTensor [[b'and', b'when', b'you', b'improve', b'searchability', b',', b'you', b'actually', b'take', b'away', b'the', b'one', b'advantage', b'of', b'print', b',', b'which', b'is', b'serendipity', b'.'], [b'but', b'what', b'if', b'it', b'were', b'active', b'?'], [b'but', b'they', b'did', b'n', b"'", b't', b'test', b'for', b'curiosity', b'.']]>
cleanup_text(reserved_tokens, words).numpy()
array([b'and when you improve searchability , you actually take away the one advantage of print , which is serendipity .',
       b'but what if it were active ?',
       b"but they did n ' t test for curiosity ."], dtype=object)

Exportar

O bloco de código a seguir constrói um CustomTokenizer classe para conter os text.BertTokenizer casos, a lógica personalizada, e os @tf.function wrappers necessários para exportação.

class CustomTokenizer(tf.Module):
  def __init__(self, reserved_tokens, vocab_path):
    self.tokenizer = text.BertTokenizer(vocab_path, lower_case=True)
    self._reserved_tokens = reserved_tokens
    self._vocab_path = tf.saved_model.Asset(vocab_path)

    vocab = pathlib.Path(vocab_path).read_text().splitlines()
    self.vocab = tf.Variable(vocab)

    ## Create the signatures for export:   

    # Include a tokenize signature for a batch of strings. 
    self.tokenize.get_concrete_function(
        tf.TensorSpec(shape=[None], dtype=tf.string))

    # Include `detokenize` and `lookup` signatures for:
    #   * `Tensors` with shapes [tokens] and [batch, tokens]
    #   * `RaggedTensors` with shape [batch, tokens]
    self.detokenize.get_concrete_function(
        tf.TensorSpec(shape=[None, None], dtype=tf.int64))
    self.detokenize.get_concrete_function(
          tf.RaggedTensorSpec(shape=[None, None], dtype=tf.int64))

    self.lookup.get_concrete_function(
        tf.TensorSpec(shape=[None, None], dtype=tf.int64))
    self.lookup.get_concrete_function(
          tf.RaggedTensorSpec(shape=[None, None], dtype=tf.int64))

    # These `get_*` methods take no arguments
    self.get_vocab_size.get_concrete_function()
    self.get_vocab_path.get_concrete_function()
    self.get_reserved_tokens.get_concrete_function()

  @tf.function
  def tokenize(self, strings):
    enc = self.tokenizer.tokenize(strings)
    # Merge the `word` and `word-piece` axes.
    enc = enc.merge_dims(-2,-1)
    enc = add_start_end(enc)
    return enc

  @tf.function
  def detokenize(self, tokenized):
    words = self.tokenizer.detokenize(tokenized)
    return cleanup_text(self._reserved_tokens, words)

  @tf.function
  def lookup(self, token_ids):
    return tf.gather(self.vocab, token_ids)

  @tf.function
  def get_vocab_size(self):
    return tf.shape(self.vocab)[0]

  @tf.function
  def get_vocab_path(self):
    return self._vocab_path

  @tf.function
  def get_reserved_tokens(self):
    return tf.constant(self._reserved_tokens)

Construir uma CustomTokenizer para cada idioma:

tokenizers = tf.Module()
tokenizers.pt = CustomTokenizer(reserved_tokens, 'pt_vocab.txt')
tokenizers.en = CustomTokenizer(reserved_tokens, 'en_vocab.txt')

Exportar os tokenizers como saved_model :

model_name = 'ted_hrlr_translate_pt_en_converter'
tf.saved_model.save(tokenizers, model_name)
2021-08-11 18:49:08.917974: W tensorflow/python/util/util.cc:348] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.

Recarregar o saved_model e testar os métodos:

reloaded_tokenizers = tf.saved_model.load(model_name)
reloaded_tokenizers.en.get_vocab_size().numpy()
7010
tokens = reloaded_tokenizers.en.tokenize(['Hello TensorFlow!'])
tokens.numpy()
array([[   2, 4006, 2358,  687, 1192, 2365,    4,    3]])
text_tokens = reloaded_tokenizers.en.lookup(tokens)
text_tokens
<tf.RaggedTensor [[b'[START]', b'hello', b'tens', b'##or', b'##f', b'##low', b'!', b'[END]']]>
round_trip = reloaded_tokenizers.en.detokenize(tokens)

print(round_trip.numpy()[0].decode('utf-8'))
hello tensorflow !

Arquivá-lo para os tutoriais de tradução :

zip -r {model_name}.zip {model_name}
adding: ted_hrlr_translate_pt_en_converter/ (stored 0%)
  adding: ted_hrlr_translate_pt_en_converter/variables/ (stored 0%)
  adding: ted_hrlr_translate_pt_en_converter/variables/variables.data-00000-of-00001 (deflated 51%)
  adding: ted_hrlr_translate_pt_en_converter/variables/variables.index (deflated 33%)
  adding: ted_hrlr_translate_pt_en_converter/assets/ (stored 0%)
  adding: ted_hrlr_translate_pt_en_converter/assets/pt_vocab.txt (deflated 57%)
  adding: ted_hrlr_translate_pt_en_converter/assets/en_vocab.txt (deflated 54%)
  adding: ted_hrlr_translate_pt_en_converter/saved_model.pb (deflated 91%)
du -h *.zip
184K    ted_hrlr_translate_pt_en_converter.zip

Opcional: o algoritmo

É importante notar aqui que existem duas versões do algoritmo WordPiece: de baixo para cima e de cima para baixo. Em ambos os casos, o objetivo é o mesmo: "Dado um corpus de treinamento e um número de tokens D desejados, o problema de otimização é selecionar peças de texto D de modo que o corpus resultante seja mínimo no número de peças de palavra quando segmentado de acordo com o modelo de peça de palavra escolhido. "

O original algoritmo WordPiece de baixo para cima , é baseado em codificação byte de par . Como o BPE, começa com o alfabeto e combina iterativamente bigramas comuns para formar pedaços de palavras e palavras.

Gerador de vocabulário de TensorFlow texto segue a implementação de cima para baixo a partir BERT . Começando com palavras e dividindo-as em componentes menores até que atinjam o limite de frequência ou não possam ser mais quebradas. A próxima seção descreve isso em detalhes. Para japonês, chinês e coreano, essa abordagem de cima para baixo não funciona, pois não há unidades de palavras explícitas para começar. Para aqueles que você precisa de uma abordagem diferente .

Escolhendo o vocabulário

O algoritmo de geração de WordPiece de cima para baixo leva em um conjunto de (palavra, contagem) pares e um limiar T , e retorna um vocabulário V .

O algoritmo é iterativo. É gerido por k iterações, onde normalmente k = 4 , mas apenas os dois primeiros são realmente importantes. O terceiro e o quarto (e além) são idênticos ao segundo. Note-se que cada etapa da busca binária é executado o algoritmo a partir do zero para k iterações.

As iterações descritas abaixo:

Primeira iteração

  1. Itera sobre cada palavra e par contagem na entrada, indicado como (w, c) .
  2. Para cada palavra w , gerar cada substring, denotado como s . Por exemplo, para a palavra human , geramos {h, hu, hum, huma, human, ##u, ##um, ##uma, ##uman, ##m, ##ma, ##man, #a, ##an, ##n} .
  3. Manter um mapa de hash subsequência-a-count, e incrementa a contagem de cada um s por c . Por exemplo, se tivermos (human, 113) e (humas, 3) na nossa entrada, a contagem de s = huma será 113+3=116 .
  4. Assim que tiver recolhido as contagens de cada substring, iterar sobre os (s, c) pares começando com a mais longa s primeiro.
  5. Mantenha todas as s que tem um c > T . Por exemplo, se T = 100 e temos (pers, 231); (dogs, 259); (##rint; 76) , então poderíamos manter pers e dogs .
  6. Quando um s é mantido, subtrair fora de sua contagem de todos os seus prefixos. Esta é a razão para a triagem de todos os s por tamanho no passo 4. Esta é uma parte crítica do algoritmo, porque de outro modo seria palavras dupla contados. Por exemplo, digamos que mantivemos human e nós conseguimos (huma, 116) . Sabemos que 113 dos 116 veio do human , e 3 veio de humas . No entanto, agora que human está em nosso vocabulário, sabemos que nunca será segmento human em huma ##n . Assim, uma vez human tem sido mantido, em seguida, huma só tem uma contagem efetiva de 3 .

Este algoritmo irá gerar um conjunto de peças palavra s (muitos dos quais será palavras inteiras w ), que poderíamos usar como nosso vocabulário WordPiece.

No entanto, há um problema: esse algoritmo irá gerar excessivamente trechos de palavras. O motivo é que apenas subtraímos as contagens de tokens de prefixo. Portanto, se mantivermos a palavra human , vamos subtrair off a contagem para h, hu, hu, huma , mas não para ##u, ##um, ##uma, ##uman e assim por diante. Assim, poderíamos gerar tanto human e ##uman como peças palavra, embora ##uman nunca vai ser aplicado.

Então por que não subtrair off a contagem para cada substring, não apenas cada prefixo? Porque então poderíamos acabar subtraindo a contagem várias vezes. Digamos que estamos processando s de comprimento 5 e nós manter tanto (##denia, 129) e (##eniab, 137) , em que 65 dessas contagens veio da palavra undeniable . Se subtrairmos fora de todos os substring, nós subtrair 65 do substring ##enia duas vezes, embora nós só deve subtrair uma vez. No entanto, se subtrairmos apenas dos prefixos, ele será corretamente subtraído apenas uma vez.

Segunda (e terceira ...) iteração

Para resolver o problema de geração excessiva mencionado acima, realizamos várias iterações do algoritmo.

Iterações subseqüentes são idênticos ao primeiro, com uma diferença importante: Na etapa 2, em vez de considerar cada substring, aplicamos o algoritmo WordPiece tokenization usando o vocabulário da iteração anterior, e considerar apenas substrings que começam em um ponto de divisão.

Por exemplo, digamos que estamos realizando passo 2 do algoritmo e encontrar a palavra undeniable . Na primeira iteração, poderíamos considerar cada substring, por exemplo, {u, un, und, ..., undeniable, ##n, ##nd, ..., ##ndeniable, ...} .

Agora, para a segunda iteração, consideraremos apenas um subconjunto deles. Digamos que, após a primeira iteração, as palavras relevantes sejam:

un, ##deni, ##able, ##ndeni, ##iable

O segmento de vontade algoritmo WordPiece isso em un ##deni ##able (consulte a seção WordPiece Aplicando para mais informações). Neste caso, só vamos considerar substrings que começam em um ponto de segmentação. Nós ainda vamos considerar cada posição final possível. Assim, durante a segunda iteração, o conjunto de s para undeniable é:

{u, un, und, unden, undeni, undenia, undeniab, undeniabl, undeniable, ##d, ##de, ##den, ##deni, ##denia, ##deniab, ##deniabl , ##deniable, ##a, ##ab, ##abl, ##able}

O algoritmo é idêntico de outra forma. Neste exemplo, na primeira iteração, o algoritmo produz o suprious fichas ##ndeni e ##iable . Agora, esses tokens nunca são considerados, portanto, não serão gerados pela segunda iteração. Realizamos várias iterações apenas para ter certeza de que os resultados convergem (embora não haja garantia de convergência literal).

Aplicando WordPiece

Uma vez que um vocabulário WordPiece tenha sido gerado, precisamos ser capazes de aplicá-lo a novos dados. O algoritmo é um aplicativo ganancioso de correspondência mais longa primeiro.

Por exemplo, considere segmentar a palavra undeniable .

Nós primeira pesquisa undeniable em nosso dicionário WordPiece, e se ele está presente, estamos a fazer. Se não, vamos diminuir o ponto final de um caractere, e repita, por exemplo, undeniabl .

Eventualmente, encontraremos um subtoken em nosso vocabulário ou chegaremos a um subtoken de um único caractere. (Em geral, assumimos que cada personagem está em nosso vocabulário, embora isto possa não ser o caso de caracteres Unicode raras. Se encontrarmos um caractere Unicode raro que não está no vocabulário nós simplesmente mapear toda a palavra para <unk> ).

Neste caso, encontramos un em nosso vocabulário. Então essa é a nossa primeira palavra. Em seguida, saltar para o fim de un e repita o processamento, por exemplo, tentar encontrar ##deniable , em seguida, ##deniabl , etc. Isto é repetido até que tenhamos segmentado a palavra inteira.

Intuição

Intuitivamente, a tokenização do WordPiece tenta satisfazer dois objetivos diferentes:

  1. Tokenizar os dados para o menor número de peças possível. É importante ter em mente que o algoritmo WordPiece não "quer" dividir palavras. Caso contrário, seria apenas dividir cada palavra em seus personagens, por exemplo, human -> {h, ##u, ##m, ##a, #n} . Isso é uma coisa crítica que faz WordPiece diferente de divisores morfológicas, que irá dividir morfemas linguísticas mesmo para palavras comuns (por exemplo, unwanted -> {un, want, ed} ).

  2. Quando uma palavra precisa ser dividida em partes, divida-a em partes que tenham contagens máximas nos dados de treinamento. Por exemplo, a razão pela qual a palavra undeniable seria dividido em {un, ##deni, ##able} ao invés de alternativas como {unde, ##niab, ##le} é que as contagens para un e ##able de particular será muito alto, uma vez que esses são prefixos e sufixos comuns. Mesmo que a contagem para ##le deve ser superior a ##able , as baixas contagens de unde e ##niab irá tornar este um tokenization menos "desejável" para o algoritmo.

Opcional: tf.lookup

Se você precisa de acesso ou mais controle sobre o vocabulário é importante notar que você pode construir a tabela de referência a si mesmo e passar isso para BertTokenizer .

Quando você passar uma string, BertTokenizer faz o seguinte:

pt_lookup = tf.lookup.StaticVocabularyTable(
    num_oov_buckets=1,
    initializer=tf.lookup.TextFileInitializer(
        filename='pt_vocab.txt',
        key_dtype=tf.string,
        key_index = tf.lookup.TextFileIndex.WHOLE_LINE,
        value_dtype = tf.int64,
        value_index=tf.lookup.TextFileIndex.LINE_NUMBER)) 
pt_tokenizer = text.BertTokenizer(pt_lookup)

Agora você tem acesso direto à tabela de pesquisa usada no tokenizer.

pt_lookup.lookup(tf.constant(['é', 'um', 'uma', 'para', 'não']))
<tf.Tensor: shape=(5,), dtype=int64, numpy=array([7765,   85,   86,   87, 7765])>

Você não precisa usar um arquivo de vocabulário, tf.lookup tem outras opções inicializador. Se você tem o vocabulário de memória que pode utilizar lookup.KeyValueTensorInitializer :

pt_lookup = tf.lookup.StaticVocabularyTable(
    num_oov_buckets=1,
    initializer=tf.lookup.KeyValueTensorInitializer(
        keys=pt_vocab,
        values=tf.range(len(pt_vocab), dtype=tf.int64))) 
pt_tokenizer = text.BertTokenizer(pt_lookup)