bert源码学习(一)——tokenization.py&&WordPiece
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基本流程如下:
- 定义tokenization中定义的FullTokenizer类的一个对象,即tokenizer = tokenization.FullTokenizer
- 将原始文本变成一个个tokens,即tokens = tokenizer.tokenize(raw_text)
- 将生成的每一组tokens都裁剪到max_seq_length长度
- 在正确的地方加上[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对输入的处理,主要包含三个步骤:
-
Text normalization(文本归一化):将所有的空白字符转换为空格,并且小写。E.g.,
John Johanson's, → john johanson's
-
Punctuation splitting(标点分割):将所有的符号的两边分开。E.g.,
john johanson's, → john johanson ' s ,
-
WordPiece tokenization:分别对每一个token进行WordPiece tokenization。E.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.”:
- 除去非法字符,并将空格字符转化为空格
- 将句子头尾的空字符(空格和换行符‘\n’)除去,将句子按照空格进行分片,返回单词列表,即[“I’m”, “a”, “good”, “girl.”]
- 对于单词列表中的每个单词,
- 如果do_lower_case为True,则将所有字符小写,即[“i’m”, “a”, “good”, “girl.”]
- 除去单词中的重音
- 将单词中的标点(如果有)单独分离出来,即[“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
算法流程如下:
- 将输入的token转化为字符列表,如 “unaffable” -> [“u”, “n”, “a”, “f”, “f”, “a”, “b”, “l”, “e” ]
- 如果字符列表的长度大于200,如token字符串的长度大于200,则直接输出[UNK]标识符
- 对于小于200长度的字符列表
- 从start=0, end=len(chars)开始,在词汇表中查找“”.join(chars[start: end])得到的字词是否在词汇表中,
- 如果不在,则令end=end-1,重复以上步骤;
- 如果在,则记下该字词,然后令start=end,end=len(chars),即在原单词中将该字词溢出,对剩下的部分继续查找,直到start=len(chars),即从头到尾遍历结束,
- 此时,如果依旧没有找到对应于词汇表中的任一字词,则对于该token,输出[UNK]标识符;如果有找到,则输出这些找到的字词
禁止转载
有问题欢迎大家指出~
上一篇: JAVA SE (14)
下一篇: sql 简略便捷的分页技术(原创)
推荐阅读