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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352