返回介绍

数学基础

统计学习

深度学习

工具

Scala

二、算法原理

发布于 2023-07-17 23:38:23 字数 40857 浏览 0 评论 0 收藏 0

  1. 有三种常见的 subword tokenization 算法:Byte Pair Encoding: BPEWordPieceUnigram

1.1 BPE

  1. Byte Pair Encoding: BPE 来自于论文 《Neural Machine Translation of Rare Words with Subword Units》(2015)

  2. BPE 是一种简单的数据压缩技术,它迭代式地替换序列中最频繁的字节对。我们不是合并频繁的字节对,而是合并频繁的字符或字符序列。

    • 首先,我们用 character vocabulary 初始化 symbol vocabulary ,将每个单词表示为一个字符序列,加上一个特殊的单词结束符 </w>,这允许我们在 tokenization 后恢复原始的 tokenization

    • 然后,我们迭代地计算所有 symbol pair ,并用新的 symbol 'AB' 替换最频繁的 symbol pair ('A','B') 。每个merge 操作产生一个新的 symbol ,它代表一个 character n-gram

      同时,每个 merge 代表一个规则。

    最终的 symbol vocabulary 大小等于 initial vocabulary 的大小,加上 merge 操作的次数(这是算法唯一的超参数)。

  3. 下面的显示了一个最小化的 Python 实现。在实践中,我们通过索引所有 pair 并增量更新数据结构来提高效率:

    ​x
    import re, collections
    
    
    def get_stats(vocab): # vocab : 存储 word -> freq 的 dict
        ''' 计算词表中,字符的 2-gram 及其出现频次
        '''
        pairs = collections.defaultdict(int)
        for word, freq in vocab.items():
            symbols = word.split() # 拆分为字符序列
            for i in range(len(symbols)-1):
                pairs[symbols[i],symbols[i+1]] += freq # 计算字符的 2-gram 及其出现频次
        return pairs
    
    
    def merge_vocab(pair, v_in): # pair 为最高频的 2-gram,v_in 为已有的 vocab
        ''' 利用最高频的 2-gram 来更新已有的词表
        '''
        v_out = {}
        bigram = re.escape(' '.join(pair)) # 对字符串中可能被解释为正则运算符的字符进行转义
        p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)') # 编译一个正则模式
        # \S 匹配任意非空字符
        # (?<! \S) 前向否定界定符。当 bigram 之前不是任意非空字符之时,匹配成功
        # (?! \S) 后向否定界定符。当 bigram 之后不是任意非空字符之时,匹配成功
        for word in v_in:
          w_out = p.sub(''.join(pair), word) # 将word中已有的pair替换为紧凑版本(移除中间的空格)
          # 注意这里有两个 join(pair), 一个是 ' '.join() 带空格, 另一个是 ''.join() 不带空格
          v_out[w_out] = v_in[word]
        return v_out

    示例:

    
    
    xxxxxxxxxx
    vocab = {'l o w </w>' : 5, 'l o w e r </w>' : 2, # initial vocabulary 'n e w e s t </w>':6, 'w i d e s t </w>':3} num_merges = 10 for i in range(num_merges): pairs = get_stats(vocab) best = max(pairs, key=pairs.get) vocab = merge_vocab(best, vocab) print(best) # 最终 vocab: {'low</w>': 5, 'low e r </w>': 2, 'newest</w>': 6, 'wi d est</w>': 3}

    注意,初始的 vocab 已经将单词拆分为字符序列,并用 ' ' 分隔。这个步骤被称作 pre-tokenization

  4. 在机器翻译任务上,有两种应用 BPE 的方法:

    • 学习两个独立的编码,一个用于 source vocabulary 、另一个用于 target vocabulary

      这种方法的优点是:在文本和词表规模方面更紧凑,并且更能保证在相应语言的训练文本中看到每个 subword 单元。

    • 学习两个 vocabulary 的并集上的编码,称之为 joint BPE

      这种方法的优点是:提高了 source tokenizationtarget tokenization 之间的一致性。如果我们独立地应用 BPE ,相同的 name 在两种语言中可能被不同地 tokenization ,这使得神经模型更难学习 subword 单元之间的映射。

  5. Byte-level BPE:包含所有基础字符 base characterbase vocabulary 可能非常大,例如,将所有 unicode 字符(一共 65536 个,即2 个字节的表示范围)作为基础字符。

    为了获得更小的 base vocabularyGPT-2 使用 byte 作为 base vocabulary 。这是一个聪明的技巧,它强制 base vocabulary 的大小为 256 (一个字节的表示范围),同时确保每个基本字符都包含在 vocabulary 中。GPT-2 具有 50257 的词表大小,其对应于 256byte-base token 、一个特殊的文本结束 token 、以及通过 50000merge 所学到的 symbol

    相比之下,使用传统 BPEGPT 的词表规模为 40478 ,其中包含 478 个基本字符,并在40000merge 后停止训练。

  6. 来自 Hugging Face 上的例子:

    • 假设在 pre-tokenization 之后,我们得到了如下的单词及其频次的集合:

      
      
      xxxxxxxxxx
      ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

      将所有单词拆分到字符,则我们得到:

      
      
      xxxxxxxxxx
      ("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)

      此时 base vocabulary 为:

      
      
      xxxxxxxxxx
      ["b", "g", "h", "n", "p", "s", "u"]
    • 然后,BPE 计算每个可能的 symbol pair ,然后挑选出现频次最高的 symbol pair

      此时,频次最高的 symbol pair 是:将 "u" 后面跟着 "g"symbol pair 合并为 "ug"

      此时单词及其频次的集合为:

      
      
      xxxxxxxxxx
      ("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)

      此时 base vocabulary 为:

      
      
      xxxxxxxxxx
      ["b", "g", "h", "n", "p", "s", "u", "ug"]
    • BPE 然后确定下一个最常见的 symbol pair,即 "u" 后面跟着 "n" 。因此,BPE"u", "n" 合并为 "un"

      下一个最常见的 symbol pair,即 "h" 后面跟着 "ug" 。因此,BPE"h", "ug" 合并为 "hug"

      此时单词及其频次的集合为:

      
      
      xxxxxxxxxx
      ("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)

      此时 base vocabulary 为:

      
      
      xxxxxxxxxx
      ["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]

    假设 BPE 的训练在这个时刻结束,那么所学习的所有 merge rule 将被应用于新的单词。例如:

    • 单词 "bug"tokenized["b", "ug"]
    • 单词 "mug"tokenized["<unk>", "ug"],因为 symbol "m" 不在 base vocabulary 中。

1.2 WordPiece

  1. BPE 一样,WordPiece《Japanese and korean voice search》(2012))从一个小的词汇表开始,并学习 merge 规则。二者之间的区别在于 merge 的方式不同:WordPiece 不是选择最高频的 pair ,而是通过如下公式计算每个 pair 的得分:

    (1)score(t1,t2)=freq(t1,2)freq(t1)×freq(t2)

    其中:

    • t1$ t_1 $ 和t2$ t_2 $ 为两个 tokent1,2$ t_{1,2} $ 为它们 merge 之后得到的新的 token
    • freq(t)$ \text{freq}(t) $ 为 tokent$ t $ 在语料库中出现的频次。

    选择 score 最高的一对 token 等价于:

    (2)maxt1,t2score(t1,t2)=maxt1,t2freq(t1,2)/Nfreq(t1)/N×freq(t2)/N=maxt1,t2logp(t1,2)[logp(t1)+logp(t2)]

    其中:N$ N $ 为语料库中的 token 总数。

    因此 WordPiece 的物理意义为:通过将t1,t2$ t_1,t_2 $ 合并为t1,2$ t_{1,2} $ 之后,语料库的对数似然的增量最大化。

  2. 来自 Hugging Face 上的例子:

    • 假设在 pre-tokenization 之后,我们得到了如下的单词及其频次的集合:

      
      
      xxxxxxxxxx
      ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

      将所有单词拆分到字符,则我们得到:

      
      
      xxxxxxxxxx
      ("h" "##u" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("h" "##u" "##g" "##s", 5)

      注意:WordPiece 通过添加前缀(在 BERT 中是 ##)来识别子词,这可以识别一个子词是否是单词的开始。这里通过将前缀添加到单词内的每个字符来拆分的,单词的首字符不添加前缀。

      此时的 base vocabulary 为:

      
      
      xxxxxxxxxx
      ["b", "h", "p", "##g", "##n", "##s", "##u"]
    • 然后,WordPiece 计算每个可能的 symbol pair ,然后挑选 score 最高的 symbol pair

      学到的第一个 merge("##g", "##s") -> ("##gs")。注意,当我们合并时,我们删除了两个 token 之间的 ##,所以我们添加 "##gs" 到词表中。

      此时单词及其频次的集合为:

      
      
      xxxxxxxxxx
      ("h" "##u" "##g", 10), ("p" "##u" "##g", 5), ("p" "##u" "##n", 12), ("b" "##u" "##n", 4), ("h" "##u" "##gs", 5)

      此时 base vocabulary 为:

      
      
      xxxxxxxxxx
      ["b", "h", "p", "##g", "##n", "##s", "##u", "##gs"]
    • 我们继续这样处理,直到达到我们所需的词汇量。

  3. tokenization 算法:WordPieceBPE 中的 tokenization 的不同在于:WordPiece 仅保存最终词表,而不保存学到的 merge rule

    在应用时,从待 tokenized 的单词开始,WordPiece 找到词表中能够匹配到的最长的子词,然后对单词进行拆分。例如,如果我们使用上面例子中学到的词表来 tokenize 单词 "hugs"

    • 首先,单词从头开始能匹配到的词表中的最长子词是 "hug",所以我们在那里拆分并得到 ["hug", "##s"]
    • 然后,我们继续匹配剩下的 "##s"。刚好能够匹配到词表中的子词 "##s"

    最终, "hugs"tokenization["hug", "##s"]

    如果使用 BPE , 我们将按顺序应用学习到的merge rule,并将其 tokenize["hu", "##gs"],所以编码不同。

  4. tokenization 无法在词表中找到子词时,整个单词被 tokenizeunknown 。 例如 "bum",由于"##m" 不在词表中,由此产生的tokenization 将只是 ["[UNK]"], 不是 ["b", "##u", "[UNK]"]

    这是与 BPE 的另一个区别:BPE 只会将不在词汇表中的单个字符 tokenizeunknown 。 例如 "bum",由于"##m" 不在词表中,由此产生的tokenization["b", "##u", "[UNK]"]

1.3 SentencePiece

  1. SentencePiece《Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates》(2018))中经常使用 Unigram 算法。

  2. Unigram 算法假设每个子词都是独立出现的,因此一个子词序列x=(x1,,xM)$ \mathbf x = (x_1,\cdots,x_M) $ 出现的概率是每个子词出现概率的乘积,即:

    (3)P(x)=i=1Mp(xi)ixiV,xVp(x)=1.0

    其中:x$ x $ 为子词,p(x)$ p(x) $ 为子词出现的概率,V$ \mathcal V $ 为词表。

    对于给定的句子X$ X $ ,其最佳 tokenization 为:

    (4)x=argmaxxS(X)P(x)

    其中:S(X)$ S(X) $ 为句子X$ X $ 的所有候选 tokenization

    如果我们已知词表V$ \mathcal V $ 以及每个子词出现的概率p(x)$ p(x) $ ,则x$ \mathbf x^* $ 可以通过维特比算法求解得到。

  3. 现在的问题是,给定一个训练语料库D$ D $ ,如何获得词表V$ \mathcal V $ 以及每个子词出现的概率p(x)$ p(x) $ 。Unigram 利用 EM 算法来求解如下的边际似然 marginal likelihoodL$ \mathcal L $ :

    (5)L=s=1|D|logP(X(s))=s=1|D|log(xS(X(s))P(x))

    其中:X(s)$ X^{(s)} $ 表示语料库D$ D $ 中的第s$ s $ 个句子。

    这里 Unigramp(x)$ p(x) $ 视作隐变量。

  4. 为求解L$ \mathcal L $ ,Unigram 采用了迭代式算法:

    • 首先,启发式地从训练语料库中获取一个足够大的 seed vocabulary

      一种选择方法是:使用所有字符、以及语料库中最高频的 substring

    • 重复以下步骤,直到词表规模|V|$ |\mathcal V| $ 达到预期的值:

      • 固定词表,通过 EM 算法优化p(x)$ p(x) $ 。
      • 对每个子词xi$ x_i $ 计算lossi$ \text{loss}_i $ ,其中lossi$ \text{loss}_i $ 表示当xi$ x_i $ 从当前词表移除时,似然L$ \mathcal L $ 降低的数值。
      • 根据lossi$ \text{loss}_i $ 进行降序排列,保留 topη%$ \eta\% $ 的子词(例如,80% )。

      注意,我们总是在词表中保留单个 character 从而防止 out-of-vocabulary

    最终的词表V$ \mathcal V $ 包含了语料库中的所有单个字符、也包括了一些 character-based tokenization 结果、甚至包括一些 word-based tokenization 结果。因此 Unigram 算法是这三者的混合体。

  5. 来自 Hugging Face 上的例子:

    • 假设在 pre-tokenization 之后,我们得到了如下的单词及其频次的集合:

      
      
      xxxxxxxxxx
      ("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)

      seed vocabulary 采用初始词表的所有严格子字符串(即,不包含它自身):

      
      
      xxxxxxxxxx
      ["h", "u", "g", "hu", "ug", "p", "pu", "n", "un", "b", "bu", "s", "hug", "gs", "ugs"]
    • 对于每个单词,考虑 tokenization 概率最高的。例如,对于 "pug"

      • tokenization["p", "u", "g"] 的概率为:

        (6)P(["p","u","g"])=P("p")×P("u")×P("g")=5210×36210×20210=0.000389

        这里 210 为词表中所有 token 的频次之和。

      • tokenization["pu", "g"] 的概率为:

        (7)P(["pu","g"])=P("pu")×P("g")=5210×20210=0.0022676

      Unigram 选择对单词进行 tokenization 最高的那个:

      
      
      xxxxxxxxxx
      ["p", "u", "g"] : 0.000389 ["p", "ug"] : 0.0022676 ["pu", "g"] : 0.0022676

      所以, "pug" 将被标记为 ["p", "ug"] 或者 ["pu", "g"],取决于首先遇到这些 中的哪一个。注意,在更大的语料库中这样的相等的情况很少见。

      通常在语料库中找到所有可能的 tokenization 并计算它们的概率,一般来说会有点困难。因此需要利用维特比算法。

      这里我们得到每个单词的最佳 tokenization

      
      
      xxxxxxxxxx
      "hug": ["hug"] (score 0.071428) "pug": ["pu", "g"] (score 0.007710) "pun": ["pu", "n"] (score 0.006168) "bun": ["bu", "n"] (score 0.001451) "hugs": ["hug", "s"] (score 0.001701)
    • 现在我们需要计算从词表中删除每个 token 如何影响损失。然后我们根据这个损失对 token 进行排序,选择 topη%$ \eta\% $ 的 token

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文