Byte Pair Encoding(BPE)算法及代码笔记

Byte Pair Encoding(BPE)算法

BPE算法是Transformer中构建词表的方法,大致分为如下几个步骤:

  1. 将语料中的文本切分为字符
  2. 统计高频共现二元组
  3. 将共现频率最高的二元组合并加入词表
  4. 重复上述第二和第三直到词表规模达到预先设置的数量,或没有可以合并的二元组为止

以GPT-2中BPE相关的代码为例对代码进行整理

完整代码如下所示

"""
BPE算法:字节对编码算法,将任意UTF-8字符串转换为整数索引序列,方便后续的神经网络运算。

bpe is short for Byte Pair Encoder. It translates arbitrary utf-8 strings into
sequences of integers, where each integer represents small chunks of commonly
occuring characters. This implementation is based on openai's gpt2 encoder.py:
https://github.com/openai/gpt-2/blob/master/src/encoder.py
but was mildly modified because the original implementation is a bit confusing.
I also tried to add as many comments as possible, my own understanding of what's
going on.
"""

import os
import json
import regex as re
import requests

import torch

# -----------------------------------------------------------------------------

def bytes_to_unicode():
    """
    将字节(8bit->2**8->256个)转换为unicode表示的字符。
    有些字节表示的字符太"丑"了,比如chr(0)为'\x00',OpenAI选择进行额外的转换。
    
    Every possible byte (really an integer 0..255) gets mapped by OpenAI to a unicode
    character that represents it visually. Some bytes have their appearance preserved
    because they don't cause any trouble. These are defined in list bs. For example:
    chr(33) returns "!", so in the returned dictionary we simply have d[33] -> "!".
    However, chr(0), for example, is '\x00', which looks ugly. So OpenAI maps these
    bytes, into new characters in a range where chr() returns a single nice character.
    So in the final dictionary we have d[0] -> 'Ā' instead, which is just chr(0 + 2**8).
    In particular, the space character is 32, which we can see by ord(' '). Instead,
    this function will shift space (32) by 256 to 288, so d[32] -> 'Ġ'.
    So this is just a simple one-to-one mapping of bytes 0..255 into unicode characters
    that "look nice", either in their original form, or a funny shifted character
    like 'Ā', or 'Ġ', etc.
    """
    # the 188 integers that render fine in their original form and need no shifting
    bs = list(range(ord("!"), ord("~")+1))+list(range(ord("¡"), ord("¬")+1))+list(range(ord("®"), ord("ÿ")+1))
    cs = bs[:] # all integers b in bs will simply map to chr(b) in the output dict
    # now get the representations of the other 68 integers that do need shifting
    # each will get mapped chr(256 + n), where n will grow from 0...67 in the loop
    n = 0
    for b in range(2**8):
        if b not in bs:
            # if this byte is "ugly" then map it to the next available "nice" character
            bs.append(b)
            cs.append(2**8+n)
            n += 1
    cs = [chr(n) for n in cs]
    d = dict(zip(bs, cs))
    return d

def get_pairs(word):
    """
    获取一个单词中所有可能的字符二元组
    
    Return all bigrams as a set of tuples, of consecutive elements in the iterable word.
    """
    pairs = set()
    prev_char = word[0]
    for char in word[1:]:
        pairs.add((prev_char, char))
        prev_char = char
    return pairs

class Encoder:

    def __init__(self, encoder, bpe_merges):
        # byte encoder/decoder
        self.byte_encoder = bytes_to_unicode()
        self.byte_decoder = {v:k for k, v in self.byte_encoder.items()}
        # bpe token encoder/decoder
        self.encoder = encoder  # 将字符串转换为整数索引
        self.decoder = {v:k for k,v in self.encoder.items()}  # 将整数索引转换为字符串
        # bpe merge list that defines the bpe "tree", of tuples (a,b) that are to merge to token ab
        self.bpe_ranks = dict(zip(bpe_merges, range(len(bpe_merges))))
        # the splitting pattern used for pre-tokenization
        # Should haved added re.IGNORECASE so BPE merges can happen for capitalized versions of contr
<think>嗯,用户想了解BPE算法的工作原理和实现。首先,我需要回忆一下BPE的基本概念。BPE是字节对编码,主要用于自然语言处理中的分词,尤其是在像GPT这样的模型里。用户提到了参考内容里的BPE分词器用于训练GPT-2、GPT-3等模型,所以可能需要详细说明BPE在分词中的应用步骤。 首先,我应该解释BPE的核心思想:通过合并频率最高的字符对来逐步构建词汇表。比如,像“low”出现多次,可能会合并成“lo”和“w”,然后再继续合并。这个过程需要迭代进行,直到达到预设的词汇表大小。可能需要举一个具体的例子,比如用户提到的“low”五次出现的例子,这样更直观。 然后,需要分步骤说明算法的工作流程。比如初始化词汇表为字符,统计所有相邻字符对的频率,合并最高频的对,更新词汇表,重复直到达到所需词表大小。这部分要详细但清晰,确保用户能一步步理解。 接下来是实现部分。用户可能需要代码示例,比如用Python实现BPE的基本框架。这里要注意代码的正确性和可读性,比如如何统计频率,如何合并字符对。可能需要用字典来统计频率,用优先队列或者排序来找到最高频的对。然后展示训练和编码的过程,比如将单词拆分成子词。 最后,用户可能想知道BPE的优势和应用场景,比如处理未登录词,平衡词表大小和分词粒度。同时,生成相关问题,比如与WordPiece的区别,处理OOV词的方法,参数调整等。这些相关问题需要紧扣主题,帮助用户深入理解。 需要确保回答结构清晰,用中文,正确使用LaTeX格式,比如变量用$符号包裹。同时引用给定的参考内容,比如在提到BPE的应用时引用[^1]。还要检查是否有遗漏的步骤,比如预处理步骤是否需要提到,比如小写转换或处理特殊符号。另外,要注意代码示例的注释和可运行性,确保用户能理解每个部分的作用。 可能用户对BPE的具体合并过程不太清楚,比如如何选择合并对,如何处理多字节的情况。所以在例子中需要详细说明每一步的合并结果,比如初始分割、第一次合并后的结果,直到最终词汇表。同时,要指出BPE如何有效减少词表大小,同时保持分词的有效性。 在总结部分,需要强调BPE的优势,比如高效处理OOV,灵活适应不同语言,这对训练大模型很重要。同时,指出参数调整的重要性,如词表大小和合并次数的影响,帮助用户在实际应用中选择合适的参数。 最后,生成的相关问题要覆盖算法对比、应用中的问题和优化方法,这样用户可以进一步探索。比如与WordPiece的区别,处理多语言的能力,参数调整策略等。</think>### Byte Pair Encoding (BPE) 算法解析 #### 1. BPE 核心原理 BPE 是一种基于**频率统计**的分词算法,通过逐步合并高频字符对(byte pair)构建子词词汇表。其核心思想是:将常见字符组合编码为单一符号,从而平衡词汇表大小与分词粒度。 例如,假设语料中单词 $"low"$ 出现5次,$"lower"$ 出现2次,初始词汇表为字符集合 $\{l, o, w, e, r\}$。BPE 会优先合并最高频的字符对 $"lo"$,生成新符号 $"lo"$,将其加入词汇表。 #### 2. 算法实现步骤 1. **初始化**:将文本拆分为字符级单位 ```python # 输入文本预处理为字符序列 corpus = {"low":5, "lower":2} vocab = {'l', 'o', 'w', 'e', 'r'} ``` 2. **频率统计**:计算所有相邻字符对频率 ```python # 统计字符对频率 pairs = defaultdict(int) for word, freq in corpus.items(): symbols = list(word) for i in range(len(symbols)-1): pairs[(symbols[i], symbols[i+1])] += freq ``` 3. **合并操作**:选择最高频字符对合并 ```python # 找到最高频字符对 best_pair = max(pairs, key=pairs.get) # 示例中为 ('l','o') # 更新词汇表和语料 new_symbol = ''.join(best_pair) vocab.add(new_symbol) ``` 4. **迭代更新**:重复步骤2-3直到达到预设词汇表大小或合并次数 #### 3. 分词过程示例 初始词汇表:$\{l, o, w, e, r\}$ 第一次合并 $"lo"$ → 新词汇表:$\{lo, w, e, r\}$ 第二次合并 $"low"$ → 最终词汇表:$\{low, e, r\}$ 此时分词结果: - $"low"$ → `["low"]` - $"lower"$ → `["low", "er"]` #### 4. BPE 的优势(引用自参考内容) > "BPE分词器用于训练GPT-2、GPT-3等大型语言模型",其优势体现在: > - 有效处理未登录词(OOV) > 通过子词组合生成新词,如 $"ChatGPT"$ → `["Chat", "G", "PT"]` > - 平衡词汇表规模与分词粒度 > - 支持多语言混合处理 #### 5. 完整实现框架 ```python class BPE: def __init__(self, vocab_size): self.vocab_size = vocab_size def train(self, corpus): # 初始化词汇表为字符集合 vocab = set("".join(corpus.keys())) while len(vocab) < self.vocab_size: # 统计字符对频率 pairs = get_pairs(corpus) if not pairs: break # 合并最高频对 best_pair = max(pairs, key=pairs.get) corpus = merge_vocab(corpus, best_pair) vocab.add("".join(best_pair)) return vocab def encode(self, text): # 将文本拆分为子词 tokens = [] for word in text.split(): while word not in self.vocab: # 寻找可合并的子词 pair = find_mergeable_pair(word) word = word.replace("".join(pair), "_".join(pair)) tokens.extend(word.split("_")) return tokens ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值