tensor2tensor 1.10--SubwordTextEncoder

如何构建一个字典是基本所有自然语言任务都需要考虑的,当然也是根据具体任务因地制宜的。

  • 基于单个字符的字典应用于一些中文任务中,如NER,BERT,但是在英文任务中可能使学习任务难度剧增。
  • 基于单词的字典可能会使字典维度过大,单个单词的出现次数过少,得不到充分的训练。

Neural Machine Translation of Rare Words with Subword Units中提出了Subword的概念,并提供了一个从训练数据中构建字典的算法(BPE)。当然这里不会炒冷饭,接下来介绍一种tensor2tensor框架中构建Subword字典的方法。大致的项目法

构建

tensor2tensor中text_encoder.SubwordTextEncoder类提供了构建字典和根据已知字典编码、解码功能,其中构建字典的方法使这里重点关注的。类方法build_from_generator从一个文本生成器构建指定字典大小的subword字典,大致流程如下:

  1. 对所有的token计数,token可以是英文单词或者中文分词。
  2. build_to_target_size方法利用二分查找法在初设的min_valmax_val中找到合适的min_token_count使得构建的字典维度近似目标target_size,其中build_from_token方法负责核心的构建字典操作。

build_from_token方法的主要想法是从所有character ngram寻找字符长度长、出现频率高的subtoken集合。

  1. 使用全部单个字符和预留token初始化字典,其中预留token一般包括填充符<pad>、句子结束符<EOS>和_ESCAPE_CHARS=set(u"\_u;0123456789")

    alphabet_tokens = chain(six.iterkeys(token_counts),
                            [native_to_unicode(t) for t in reserved_tokens])
    
    self._init_alphabet_from_tokens(alphabet_tokens)
    
    # Bootstrap the initial list of subtokens with the characters from the
    # alphabet plus the escaping characters.
    self._init_subtokens_from_list(list(self._alphabet),
                                   reserved_tokens=reserved_tokens)
    
  2. For each iteration:

    a. 首先利用现有的subtoken字典对所有原始token编码,并计数。其中_escape_token在token末尾添加标志符号'_',同时使用Unicode值替换字符表alphabet不存在的字符(OOV)。

    subtoken_counts = collections.defaultdict(int)
    for token, count in six.iteritems(token_counts):
        iter_start_time = time.time()
        escaped_token = _escape_token(token, self._alphabet)
        subtokens = self._escaped_token_to_subtoken_strings(escaped_token)
        start = 0
        for subtoken in subtokens:
            last_position = len(escaped_token) + 1
            if max_subtoken_length is not None:
                last_position = min(last_position, start + max_subtoken_length)
    
            for end in range(start + 1, last_position):
                new_subtoken = escaped_token[start:end]
                subtoken_counts[new_subtoken] += count
            start += len(subtoken)
    

    其中针对subtokens的for循环可能比较晦涩,举个例子

    # Example 1
    subtokens=['l', 'o', 'w', 'e', 'r', '_']
    new_subtokens=['l', 'lo', 'low', 'lowe', 'lower', 'lower_', 'o', 'ow', 'owe', 'ower', 'ower_', 'w', 'we', 'wer', 'wer_', 'e', 'er', 'er_', 'r', 'r_', '_']
      
    # Example 2
    subtokens=['low', 'er_']
    new_subtokens=['l', 'lo', 'low', 'e', 'er', 'er_']
    

    b. 将subtoken_counts按照subtoken的长度聚类

    len_to_subtoken_strings = []
    for subtoken_string, count in six.iteritems(subtoken_counts):
        lsub = len(subtoken_string)
        if count >= min_count:
            while len(len_to_subtoken_strings) <= lsub:
                len_to_subtoken_strings.append(set())
            len_to_subtoken_strings[lsub].add(subtoken_string)
    

    c. 按照subtoken长度由大到小遍历所有subtokens,保留频数大于等于预设min_count的subtoken,同时减去当前subtoken子串的相应频数。

    new_subtoken_strings = []
    for lsub in range(len(len_to_subtoken_strings) - 1, 0, -1):
        subtoken_strings = len_to_subtoken_strings[lsub]
        for subtoken_string in subtoken_strings:
            count = subtoken_counts[subtoken_string]
            if count >= min_count:
                # Exclude alphabet tokens here, as they must be included later,
                # explicitly, regardless of count.
                if subtoken_string not in self._alphabet:
                    new_subtoken_strings.append((count, subtoken_string))
                for l in range(1, lsub):
                    subtoken_counts[subtoken_string[:l]] -= count
    

    d. 利用new_subtoken_strings和全部单字符(alphabet)的并集更新字典

编码

给定一个SubwordTextEncoder和一个token字符串,首先通过_escape_token规范原始token,再利用_escaped_token_to_subtoken_strings把token分解为subtoken_strings,最后根据字典编码映射strings到ids。其中_escaped_token_to_subtoken_strings是利用前向最大匹配算法,由于subtoken字典中包含了所有单个字符,所以必然存在一个分解。

def _escaped_token_to_subtoken_strings(self, escaped_token):
    """Converts an escaped token string to a list of subtoken strings.

    Args:
      escaped_token: An escaped token as a unicode string.
    Returns:
      A list of subtokens as unicode strings.
    """
    # NOTE: This algorithm is greedy; it won't necessarily produce the "best"
    # list of subtokens.
    ret = []
    start = 0
    token_len = len(escaped_token)
    while start < token_len:
        for end in range(
                min(token_len, start + self._max_subtoken_len), start, -1):
            subtoken = escaped_token[start:end]
            if subtoken in self._subtoken_string_to_id:
                ret.append(subtoken)
                start = end
                break

        else:  # Did not break
            # If there is no possible encoding of the escaped token then one of the
            # characters in the token is not in the alphabet. This should be
            # impossible and would be indicative of a bug.
            assert False, "Token substring not found in subtoken vocabulary."

    return ret

解码

相对于编码,解码是比较简单的。由于每个token的结尾都会被标记'',解码过程就是拼接所有输出subtokens,再按照''分割,就可以得到tokens。在编码过程中,_escape_token针对OOV做的特殊处理需要通过_unescape_token逆向转化回来。

def _subtoken_ids_to_tokens(self, subtokens):
    """Converts a list of subtoken ids to a list of tokens.

    Args:
      subtokens: a list of integers in the range [0, vocab_size)
    Returns:
      a list of strings.
    """
    concatenated = "".join(
        [self._subtoken_id_to_subtoken_string(s) for s in subtokens])
    split = concatenated.split("_")
    ret = []
    for t in split:
        if t:
            unescaped = _unescape_token(t + "_")
            if unescaped:
                ret.append(unescaped)
    return ret
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容