NLP -- tokenization 详解

845 阅读4分钟

简介

BERT 表示 Bidirectional Encoder Representations from Transformers,是 Google 于 2018 年发布的一种语言表示模型。

BERT 源码tokenization.py 就是预处理进行分词的程序,主要有两个分词器:BasicTokenizerWordpieceTokenizer,另外一个 FullTokenizer 是这两个的结合:先进行 BasicTokenizer 得到一个分得比较粗的 token 列表,然后再对每个 token 进行一次 WordpieceTokenizer,得到最终的分词结果。

BasicTokenizer

BasicTokenizer(以下简称 BT)是一个初步的分词器。对于一个待分词字符串,流程大致就是转成 unicode -> 去除各种奇怪字符 -> 处理中文 -> 空格分词 -> 去除多余字符和标点分词 -> 再次空格分词,结束。

WordpieceTokenizer

WordpieceTokenizer(以下简称 WPT)是在 BT 结果的基础上进行再一次切分,得到子词(subword,以 ## 开头),词汇表就是在此时引入的。该类只有两个方法:一个初始化方法 __init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=200),一个分词方法 tokenize(self, text)

tokenize(self, text):该方法就是主要的分词方法了,大致分词思路是 按照从左到右的顺序,将一个词拆分成多个子词,每个子词尽可能长。 按照源码中的说法,该方法称之为 greedy longest-match-first algorithm,贪婪最长优先匹配算法。

longest-match-first


  • convert_to_unicode(text)

    • 转成 unicode
      • 如果是str 类型,直接返回
      • 如果是bytes 类型, 用 uft-8 解码,若报错,采取 ignore 方案
  • printable_text(text)

    • 返回文本解码,用于打印和日志输出
      • 如果是str 类型,直接返回
      • 如果是bytes 类型, 用 uft-8 解码,若报错,采取 ignore 方案
  • load_vocab(vocab_file)

    • 导入 词表
      • 创建一个有序字典
      • 打开文件,按行读取
        • 如果读到空则跳出循环
        • 去除空格,并将 数据 保存到有序字典。格式:vocab[token] = index,下表是词,值是排序号
  • convert_by_vocab(vocab, items)

    • items 转成 词表
      • 定义output数组
      • 遍历items -> item,output.append(vocab[item])
      • return output
  • convert_tokens_to_ids(vocab, tokens)

    • tokens 转成 ids
    • return convert_by_vocab(vocab, tokens)
  • convert_ids_to_tokens(inv_vocab, ids)

    • ids 转成 tokens
    • return convert_by_vocab(inv_vocab, ids)
  • whitespace_tokenize(text)

    • 返回去除前后空格的文本
      • 如果text去除空格后为空,返回空数组
      • 否则返回 tokens = text.split()

class FullTokenizer(object)

返回 端到端 的tokeniziation(标识化)

  • init(self, vocab_file, do_lower_case=True)

    • vocab_file:词表文件

    • do_lower_case:输入是否小写

      • 词表 self.vocab
      • 源词表 self.inv_vocab = {v: k for k, v in self.vocab.items()}
      • 基础分词器 self.basic_tokenizer
      • 词块分词器 self.wordpiece_tokenizer
  • tokenize(self, text):

    • 一层循环 : for token in self.basic_tokenizer.tokenize(text):
    • 二层循环: for sub_token in self.wordpiece_tokenizer.tokenize(token):
  • convert_tokens_to_ids(self, tokens): return convert_by_vocab(self.vocab, tokens)

  • convert_ids_to_tokens(self, ids): return convert_by_vocab(self.inv_vocab, ids)


BasicTokenizer

按空格切分,返回单个词的 tokenization

  • init(self, do_lower_case=True)

    • self.do_lower_case = do_lower_case
  • tokenize(self, text)

    • 标记一段文本
      • text 转成 unicode
      • text 调用 self._clean_text() 函数,清理文本中无效字符空格
      • text 调用 self._tokenize_chinese_chars(),在中文字符前后添加空格
      • 将text去除前后空格 后的文本 赋值给变量 orig_tokens
      • 定义 split_tokens 数组
      • 遍历 orig_tokens
        • 如果初始化时,设置了小写,则将文本转成小写
        • 调用 self._run_strip_accents() 函数,去除 音标,如:ê-》转换为e和组合字符^
        • split_tokens列表尾端追加 self._run_split_on_punc(token)
  • _run_strip_accents(self, text):

    • 格式化文本,去除 音标,如:ê-》转换为e和组合字符^
      • 调用 unicodedata.normalize() 为整个字符串实现 Unicode 规范化算法。D 是一个规范化: D 将字符分解为组件,因此ê转换为e和组合字符^。

      • 遍历格式化后文本

        • 使用unicodedata.category(char),如果为 "Mn",continue

        [Mn] Mark, Nonspacing

  • _run_split_on_punc(self, text)

    • 根据标点符号切分文本
      • 将文本转为 List:chars = list(text)
      • start_new_word = True
      • 遍历 List(text)
        • 判断是否为标点字符
        if _is_punctuation(char):
          output.append([char])
          start_new_word = True
        else:
          if start_new_word:
            output.append([])
          start_new_word = False
          output[-1].append(char)
        
  • _clean_text(self, text):

    • 清理文本中无效字符和空格
      • 创建 output 数组: output.append(" ")
      • 遍历text 的字符
        • 通过函数 ord()-》返回表示指定字符的 unicode 代码的数字
        • 如果 unicode 为 0 OR 0xfffd(空字符串) OR _is_control(char),continue
        • 如果 _is_whitespace(char) , output数组添加空字符串:output.append(" ");否则output添加该字符:output.append(char)
      • 将output数组转成字符串 后返回:return "".join(output)

_tokenize_chinese_chars(self, text):

  • 在中文字符 前后添加 空格
  output = []
  for char in text:
    cp = ord(char)
    if self._is_chinese_char(cp):
      output.append(" ")
      output.append(char)
      output.append(" ")
    else:
      output.append(char)
  return "".join(output)

_is_chinese_char(self, cp):

  • 通过 ord 函数的unicode 找出中文
    if ((cp >= 0x4E00 and cp <= 0x9FFF) or  #
        (cp >= 0x3400 and cp <= 0x4DBF) or  #
        (cp >= 0x20000 and cp <= 0x2A6DF) or  #
        (cp >= 0x2A700 and cp <= 0x2B73F) or  #
        (cp >= 0x2B740 and cp <= 0x2B81F) or  #
        (cp >= 0x2B820 and cp <= 0x2CEAF) or
        (cp >= 0xF900 and cp <= 0xFAFF) or  #
        (cp >= 0x2F800 and cp <= 0x2FA1F)):  #
      return True

    return False

  • _is_control(char)

    • 检查' chars '是否为控制字符
      • unicodedata.category():把一个字符返回它在UNICODE里分类的类型。
        • Cc Other, Control
        • Cf Other, Format
      if char == "\t" or char == "\n" or char == "\r":
        return False
      cat = unicodedata.category(char)
      if cat in ("Cc", "Cf"):
        return True
      return False
    
  • _is_whitespace(char)

    • 检查' chars '是否为空格符
      • unicodedata.category():把一个字符返回它在UNICODE里分类的类型。
        • Zs Separator, Space
    if char == " " or char == "\t" or char == "\n" or char == "\r":
       return True
     cat = unicodedata.category(char)
     if cat == "Zs":
       return True
     return False
    
  • _is_punctuation(char)

    • 检查 char 是否为标点字符
    if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or
        (cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)):
      return True
    cat = unicodedata.category(char)
    if cat.startswith("P"):
      return True
    return False