子词分词器

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 下载笔记本

本教程演示了如何从数据集生成子词词汇表,并使用它从词汇表中构建一个 text.BertTokenizer

子词分词器的主要优点是它在基于词的分词和基于字符的分词之间进行插值。常用词在词汇表中占有一席之地,但分词器可以回退到词片和单个字符来处理未知词。

概述

tensorflow_text 包含许多常见分词器的 TensorFlow 实现。这包括三种子词风格的分词器

  • text.BertTokenizer - 该 BertTokenizer 类是更高级别的接口。它包括 BERT 的标记拆分算法和一个 WordPieceTokenizer。它以句子作为输入,并返回标记 ID
  • text.WordpieceTokenizer - 该 WordPieceTokenizer 类是更低级别的接口。它只实现了 WordPiece 算法。您必须在调用它之前将文本标准化并拆分为单词。它以单词作为输入,并返回标记 ID。
  • text.SentencepieceTokenizer - 该 SentencepieceTokenizer 需要更复杂的设置。它的初始化程序需要一个预训练的 Sentencepiece 模型。有关如何构建这些模型之一的说明,请参阅 google/sentencepiece 存储库。它在分词时可以接受句子作为输入。

本教程以自上而下的方式构建词片词汇表,从现有单词开始。此过程不适用于日语、中文或韩语,因为这些语言没有明确的多字符单位。要对这些语言进行分词,请考虑使用 text.SentencepieceTokenizertext.UnicodeCharTokenizer这种方法

设置

pip install -q -U "tensorflow-text==2.11.*"
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
2024-06-25 11:38:46.052238: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory
2024-06-25 11:38:46.780906: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory
2024-06-25 11:38:46.781005: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory
2024-06-25 11:38:46.781016: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.
tf.get_logger().setLevel('ERROR')
pwd = pathlib.Path.cwd()

下载数据集

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']
2024-06-25 11:38:48.987468: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory
2024-06-25 11:38:48.987595: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcublas.so.11'; dlerror: libcublas.so.11: cannot open shared object file: No such file or directory
2024-06-25 11:38:48.987663: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcublasLt.so.11'; dlerror: libcublasLt.so.11: cannot open shared object file: No such file or directory
2024-06-25 11:38:48.987726: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcufft.so.10'; dlerror: libcufft.so.10: cannot open shared object file: No such file or directory
2024-06-25 11:38:49.042669: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcusparse.so.11'; dlerror: libcusparse.so.11: cannot open shared object file: No such file or directory
2024-06-25 11:38:49.042862: W tensorflow/core/common_runtime/gpu/gpu_device.cc:1934] Cannot dlopen some GPU libraries. Please make sure the missing libraries mentioned above are installed properly if you would like to use GPU. Follow the guide at https://tensorflowcn.cn/install/gpu for how to download and setup the required libraries for your platform.
Skipping registering GPU devices...

此数据集生成葡萄牙语/英语句子对

for pt, en in train_examples.take(1):
  print("Portuguese: ", pt.numpy().decode('utf-8'))
  print("English:   ", en.numpy().decode('utf-8'))
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 .

请注意上面示例句子的一些特点

  • 它们是小写。
  • 标点符号周围有空格。
  • 不清楚是否使用或使用什么 Unicode 规范化。
train_en = train_examples.map(lambda pt, en: en)
train_pt = train_examples.map(lambda pt, en: pt)

生成词汇表

本节从数据集生成词片词汇表。如果您已经拥有词汇表文件,并且只想了解如何构建一个 text.BertTokenizertext.WordpieceTokenizer 分词器,那么您可以跳过本节,直接进入 构建分词器 部分。

词汇表生成代码包含在 tensorflow_text pip 包中。它不是默认导入的,您需要手动导入它

from tensorflow_text.tools.wordpiece_vocab import bert_vocab_from_dataset as bert_vocab

bert_vocab.bert_vocab_from_dataset 函数将生成词汇表。

您可以设置许多参数来调整其行为。在本教程中,您将主要使用默认值。如果您想了解有关这些选项的更多信息,请先阅读有关 算法 的内容,然后查看 代码

这大约需要 2 分钟。

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 23s, sys: 2.53 s, total: 1min 26s
Wall time: 1min 19s

以下是生成的词汇表的一些片段。

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']
['##–', '##—', '##‘', '##’', '##“', '##”', '##⁄', '##€', '##♪', '##♫']

编写词汇表文件

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)

使用该函数从英语数据生成词汇表

%%time
en_vocab = bert_vocab.bert_vocab_from_dataset(
    train_en.batch(1000).prefetch(2),
    **bert_vocab_args
)
CPU times: user 59.2 s, sys: 2.02 s, total: 1min 1s
Wall time: 54.8 s
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']
['##_', '##`', '##ย', '##ร', '##อ', '##–', '##—', '##’', '##♪', '##♫']

以下是两个词汇表文件

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

构建分词器

text.BertTokenizer 可以通过将词汇表文件的路径作为第一个参数传递来初始化(有关其他选项,请参阅有关 tf.lookup 的部分)

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

现在,您可以使用它来编码一些文本。从英语数据中获取一个包含 3 个示例的批次

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 ."

将文本通过 BertTokenizer.tokenize 方法进行处理。最初,这将返回一个带有轴 (batch, word, word-piece)tf.RaggedTensor

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

如果用文本表示替换标记 ID(使用 tf.gather),您会发现,在第一个示例中,单词 "searchability""serendipity" 已分解为 "search ##ability""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)>

要从提取的标记重新组装单词,请使用 BertTokenizer.detokenize 方法。

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

自定义和导出

本教程构建了 Transformer 教程中使用的文本标记器和去标记器。本节添加了方法和处理步骤以简化该教程,并使用 tf.saved_model 导出标记器,以便其他教程可以导入它们。

自定义标记化

下游教程都期望标记化的文本包含 [START][END] 标记。

reserved_tokens 在词汇表的开头保留空间,因此 [START][END] 在两种语言中具有相同的索引。

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

自定义去标记化

在导出标记器之前,您可以为下游教程清理一些内容。

  1. 他们希望生成干净的文本输出,因此请删除 [START][END][PAD] 等保留标记。
  2. 他们对完整的字符串感兴趣,因此请在结果的 words 轴上应用字符串连接。
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)

导出

以下代码块构建了一个 CustomTokenizer 类,以包含 text.BertTokenizer 实例、自定义逻辑以及导出所需的 @tf.function 包装器。

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)

为每种语言构建一个 CustomTokenizer

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

将标记器导出为 saved_model

model_name = 'ted_hrlr_translate_pt_en_converter'
tf.saved_model.save(tokenizers, model_name)

重新加载 saved_model 并测试方法。

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 !

将其归档到 翻译教程 中。

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

可选:算法

值得注意的是,WordPiece 算法有两个版本:自下而上和自上而下。在这两种情况下,目标都是相同的:“给定一个训练语料库和所需标记数量 D,优化问题是选择 D 个词片,以便在根据所选词片模型进行分段时,得到的语料库在词片数量上最小。”

最初的 自下而上 WordPiece 算法 基于 字节对编码。与 BPE 一样,它从字母表开始,并迭代地组合常见的双字母组以形成词片和单词。

TensorFlow Text 的词汇表生成器遵循 BERT 中的自上而下实现。从单词开始,将其分解为更小的组件,直到它们达到频率阈值,或者无法进一步分解。下一节将详细描述这一点。对于日语、中文和韩语,这种自上而下的方法不起作用,因为没有明确的单词单位可以作为起点。对于这些语言,您需要 不同的方法

选择词汇表

自上而下的 WordPiece 生成算法接受一组 (word, count) 对和一个阈值 T,并返回一个词汇表 V

该算法是迭代的。它运行 k 次迭代,其中通常 k = 4,但只有前两次真正重要。第三次和第四次(以及以后)与第二次完全相同。请注意,二分搜索的每一步都从头开始运行算法 k 次迭代。

下面描述的迭代

第一次迭代

  1. 迭代输入中的每个单词和计数对,表示为 (w, c)
  2. 对于每个单词 w,生成每个子字符串,表示为 s。例如,对于单词 human,我们生成 {h, hu, hum, huma, human, ##u, ##um, ##uma, ##uman, ##m, ##ma, ##man, #a, ##an, ##n}
  3. 维护一个子字符串到计数的哈希映射,并将每个 s 的计数增加 c。例如,如果我们的输入中有 (human, 113)(humas, 3),则 s = huma 的计数将为 113+3=116
  4. 收集完每个子字符串的计数后,迭代 (s, c) 对,从最长的 s 开始
  5. 保留任何 s,其 c > T。例如,如果 T = 100,并且我们有 (pers, 231); (dogs, 259); (##rint; 76),那么我们将保留 persdogs
  6. 保留 s 时,从其所有前缀中减去其计数。这就是在步骤 4 中按长度对所有 s 进行排序的原因。这是算法的关键部分,因为否则单词会被重复计算。例如,假设我们保留了 human,并且我们得到了 (huma, 116)。我们知道,这 116 个计数中有 113 个来自 human,而 3 个来自 humas。但是,现在 human 已经在我们的词汇表中,我们知道我们永远不会将 human 分割成 huma ##n。因此,一旦保留了 human,那么 huma 只有 3有效计数。

该算法将生成一组词片 s(其中许多将是完整的单词 w),我们可以将其用作我们的 WordPiece 词汇表。

但是,存在一个问题:该算法将严重过度生成词片。原因是我们只减去前缀标记的计数。因此,如果我们保留单词 human,我们将减去 h, hu, hu, huma 的计数,但不会减去 ##u, ##um, ##uma, ##uman 等等的计数。因此,我们可能会生成 human##uman 作为词片,即使 ##uman 永远不会被应用。

那么为什么不从每个子字符串中减去计数,而不仅仅是从每个前缀中减去计数呢?因为这样我们可能会多次减去计数。假设我们正在处理长度为 5 的 s,并且我们保留了 (##denia, 129)(##eniab, 137),其中这些计数中有 65 个来自单词 undeniable。如果我们从每个子字符串中减去,我们将从子字符串 ##enia 中减去 65 两次,即使我们应该只减去一次。但是,如果我们只从前缀中减去,它将正确地只减去一次。

第二次(以及第三次……)迭代

为了解决上面提到的过度生成问题,我们执行算法的多次迭代。

后续迭代与第一次相同,但有一个重要的区别:在步骤 2 中,我们不是考虑每个子字符串,而是使用前一次迭代的词汇表应用 WordPiece 标记化算法,并且只考虑分割点开始的子字符串。

例如,假设我们正在执行算法的步骤 2,并且遇到了单词 undeniable。在第一次迭代中,我们将考虑每个子字符串,例如,{u, un, und, ..., undeniable, ##n, ##nd, ..., ##ndeniable, ...}

现在,对于第二次迭代,我们将只考虑其中的一部分。假设第一次迭代后,相关的词片是

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

WordPiece 算法将将其分割成 un ##deni ##able(有关更多信息,请参阅 应用 WordPiece 部分)。在这种情况下,我们将只考虑分割点开始的子字符串。我们将仍然考虑每个可能的结束位置。因此,在第二次迭代中,undeniables 集是

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

该算法在其他方面是相同的。在这个例子中,在第一次迭代中,该算法产生了虚假的标记 ##ndeni##iable。现在,这些标记永远不会被考虑,因此它们不会被第二次迭代生成。我们执行多次迭代只是为了确保结果收敛(尽管没有文字收敛保证)。

应用 WordPiece

一旦生成了 WordPiece 词汇表,我们就需要能够将其应用于新数据。该算法是一个简单的贪婪最长匹配优先应用。

例如,考虑将单词 undeniable 分割。

我们首先在我们的 WordPiece 字典中查找 undeniable,如果它存在,我们就完成了。如果没有,我们将结束点减去一个字符,然后重复,例如,undeniabl

最终,我们将找到词汇表中的一个子标记,或者降到单个字符子标记。(通常,我们假设每个字符都在我们的词汇表中,尽管对于罕见的 Unicode 字符可能并非如此。如果我们遇到词汇表中不存在的罕见的 Unicode 字符,我们只需将整个单词映射到 <unk>)。

在这种情况下,我们在词汇表中找到了 un。所以这是我们的第一个词片。然后我们跳到 un 的末尾,并重复处理,例如,尝试找到 ##deniable,然后是 ##deniabl 等等。重复此操作,直到我们分割了整个单词。

直觉

直观地说,WordPiece 分词试图满足两个不同的目标

  1. 将数据分词成尽可能少的片段。重要的是要记住,WordPiece 算法并不“希望”拆分单词。否则,它只会将每个单词拆分成它的字符,例如,human -> {h, ##u, ##m, ##a, #n}。这是 WordPiece 与形态分析器不同的一个关键点,形态分析器会将语言形态词素拆分,即使是常见的单词(例如,unwanted -> {un, want, ed})。

  2. 当一个单词确实需要被拆分成片段时,将其拆分成在训练数据中出现次数最多的片段。例如,单词 undeniable 被拆分成 {un, ##deni, ##able} 而不是像 {unde, ##niab, ##le} 这样的替代方案的原因是,un##able 的出现次数特别高,因为它们是常见的词缀。即使 ##le 的出现次数必须高于 ##able,但 unde##niab 的低出现次数将使这种分词对算法来说不那么“理想”。

可选:tf.lookup

如果您需要访问词汇表或对其进行更多控制,值得注意的是您可以自己构建查找表并将其传递给 BertTokenizer

当您传递一个字符串时,BertTokenizer 会执行以下操作

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)

现在您可以直接访问分词器中使用的查找表。

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

您不需要使用词汇表文件,tf.lookup 有其他初始化选项。如果您在内存中拥有词汇表,您可以使用 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)