欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

bert源码学习(一)——tokenization.py&&WordPiece

程序员文章站 2022-05-14 17:16:14
...

bert谷歌官方源码地址:https://github.com/google-research/bert

这篇博客主要讲关于tokenization.py文件的源码阅读

 

1. bert—tokenization.py官方文档

首先来看一下bert上tokenization.py的官方文档。

 

对于句子级(或句子对)任务,tokenization.py的使用非常简单,run_classifier.py和extract_features.py中有使用它的例子。句子级任务的tokenization基本流程如下:

  1. 定义tokenization中定义的FullTokenizer类的一个对象,即tokenizer = tokenization.FullTokenizer
  2. 将原始文本变成一个个tokens,即tokens = tokenizer.tokenize(raw_text)
  3. 将生成的每一组tokens都裁剪到max_seq_length长度
  4. 在正确的地方加上[cls]和[sep]标识符

 

词级任务会复杂一些,因为我们需要保持输入文本和输出文本的对应,只有这样才能为输出文本添加正确的labels(since you need to maintain alignment between your input text and output text so that you can project your training labels

在开始详细地描述如何处理词级任务之前,可以先看一下tokenizer对输入的处理,主要包含三个步骤:

  1. Text normalization文本归一化):将所有的空白字符转换为空格,并且小写。E.g., John Johanson's, → john johanson's
  2. Punctuation splitting(标点分割):将所有的符号的两边分开。E.g., john johanson's, → john johanson ' s ,
  3. WordPiece tokenization:分别对每一个token进行WordPiece tokenizationE.g., john johanson ' s , → john johan ##son ' s ,

 

接下来给出词级的tokenization的使用,关键在于对齐输入输出,并标记输出labels。假设输入为预token过(即上述1.2步骤)的单词序列,那么只需要对每个单词单独进行WordPiece Tokenization,然后对齐原输入和WordPiece Tokenization的输出。官方给出的词级tokenization代码如下

### Input
orig_tokens = ["John", "Johanson", "'s",  "house"]
labels      = ["NNP",  "NNP",      "POS", "NN"]

### Output
bert_tokens = []

# Token map will be an int -> int mapping between the `orig_tokens` index and
# the `bert_tokens` index.
orig_to_tok_map = []

tokenizer = tokenization.FullTokenizer(
    vocab_file=vocab_file, do_lower_case=True)

bert_tokens.append("[CLS]")
for orig_token in orig_tokens:
  orig_to_tok_map.append(len(bert_tokens))
  bert_tokens.extend(tokenizer.tokenize(orig_token))
bert_tokens.append("[SEP]")

# bert_tokens == ["[CLS]", "john", "johan", "##son", "'", "s", "house", "[SEP]"]
# orig_to_tok_map == [1, 2, 4, 6]

这样,就可以通过orig_to_tok_map对bert_tokens进行标记labels。

 

2.bert—tokenization源码

来看一下tokenization.py的源码。

 

tokenization.py文件中总共包含3个类:FullTokenizer, BasicTokenizer, WordpieceTokenizer。这三个类的关系为FullTokenizer中的tokenize函数依次调用了BasicTokenizer中的tokenize函数和WordpieceTokenizer中的tokenize函数,核心代码在170-176

def tokenize(self, text):
  split_tokens = []
  for token in self.basic_tokenizer.tokenize(text):
    for sub_token in self.wordpiece_tokenizer.tokenize(token):
      split_tokens.append(sub_token)
  return split_tokens

 

因此,转入BasicTokenizer中的tokenize函数和WordpieceTokenizer中的tokenize函数。

BasicTokenizer中的tokenize函数在第196-218行。

def tokenize(self, text):
  """Tokenizes a piece of text."""
  text = convert_to_unicode(text)
  text = self._clean_text(text)

  # This was added on November 1st, 2018 for the multilingual and Chinese
  # models. This is also applied to the English models now, but it doesn't
  # matter since the English models were not trained on any Chinese data
  # and generally don't have any Chinese data in them (there are Chinese
  # characters in the vocabulary because Wikipedia does have some Chinese
  # words in the English Wikipedia.).
  text = self._tokenize_chinese_chars(text)

  orig_tokens = whitespace_tokenize(text)
  split_tokens = []
  for token in orig_tokens:
    if self.do_lower_case:
      token = token.lower()
      token = self._run_strip_accents(token)
    split_tokens.extend(self._run_split_on_punc(token))

  output_tokens = whitespace_tokenize(" ".join(split_tokens))
  return output_tokens

其中,

self._clean_text(): 除去非法字符,并将空格字符转化为空格(“whitespace cleanup on text”可能是在计算机中的表示不一样?这里不太懂)

whitespace_tokenize(): 将句子头尾的空字符(空格和换行符‘\n’)除去,将句子按照空格进行分片,返回单词列表。

self._run_strip_accent(): 除去文本中的重音(这里,关于‘Mn’也不是很懂)

self._run_split_on_punc(): 将文本中的标点分开。e.g., I'm -> I ' m

那么,BasicTokenizer中的tokenize函数对text的操作可以统一为如下,假设输入为句子(str)“I’m a good girl.”:

  1. 除去非法字符,并将空格字符转化为空格
  2. 将句子头尾的空字符(空格和换行符‘\n’)除去,将句子按照空格进行分片,返回单词列表,即[“I’m”, “a”, “good”, “girl.”]
  3. 对于单词列表中的每个单词,
    1. 如果do_lower_case为True,则将所有字符小写,即[“i’m”, “a”, “good”, “girl.”]
    2. 除去单词中的重音
    3. 将单词中的标点(如果有)单独分离出来,即[“i”, “’”, “m”, “a”, “good”, “girl”, “.”]

 

WordpieceTokenizer中的tokenize函数将BasicTokenizer的tokenize返回的单词序列中的每个单词单独处理,它将每个单词单独进行wordpiece。

WordPiece,词如其名,就是将单词分成一片一片。WordPiece生成的是比单词更小的单位,因此可以称为子词;而比单词更小的单位是什么呢?是字(或者说字符),因此也可以称为字词,那么WordPiece就是一个将字转化为字词(子词)的过程。

源码中给出例子,输入为“unaffable”,输出为[“un”, “##aff”, “##able”]。实现方法为根据给定的词汇表,使用贪心最长优先匹配策略。具体实现方法来看源码。

WordpieceTokenizer中的tokenize函数在第308-359行。

def tokenize(self, text):
  
  text = convert_to_unicode(text)

  output_tokens = []
  for token in whitespace_tokenize(text):
    chars = list(token)
    if len(chars) > self.max_input_chars_per_word:
      output_tokens.append(self.unk_token)
      continue

    is_bad = False
    start = 0
    sub_tokens = []
    while start < len(chars):
      end = len(chars)
      cur_substr = None
      while start < end:
        substr = "".join(chars[start:end])
        if start > 0:
          substr = "##" + substr
        if substr in self.vocab:
          cur_substr = substr
          break
        end -= 1
      if cur_substr is None:
        is_bad = True
        break
      sub_tokens.append(cur_substr)
      start = end

    if is_bad:
      output_tokens.append(self.unk_token)
    else:
      output_tokens.extend(sub_tokens)
  return output_tokens

算法流程如下:

  1. 将输入的token转化为字符列表,如 “unaffable” -> [“u”, “n”, “a”, “f”, “f”, “a”, “b”, “l”, “e” ]
  2. 如果字符列表的长度大于200,如token字符串的长度大于200,则直接输出[UNK]标识符
  3. 对于小于200长度的字符列表
    1. 从start=0, end=len(chars)开始,在词汇表中查找“”.join(chars[start: end])得到的字词是否在词汇表中,
    2. 如果不在,则令end=end-1,重复以上步骤;
    3. 如果在,则记下该字词,然后令start=end,end=len(chars),即在原单词中将该字词溢出,对剩下的部分继续查找,直到start=len(chars),即从头到尾遍历结束,
    4. 此时,如果依旧没有找到对应于词汇表中的任一字词,则对于该token,输出[UNK]标识符;如果有找到,则输出这些找到的字词

 

禁止转载

有问题欢迎大家指出~