1. 从“ababa”到GPT-4:BPE为何成为大模型时代的基石

如果你最近关注过ChatGPT、Llama这些大语言模型,或者尝试过Hugging Face上的开源模型,那你一定对“Tokenizer”(分词器)这个词不陌生。在模型眼中,我们输入的“你好,世界!”并不是一句话,而是一串数字ID。将文本变成这串ID的过程,就是分词。而在当今几乎所有主流大模型(GPT系列、BERT、Llama等)的背后,都站着一个共同的、看似简单的算法——Byte Pair Encoding,简称BPE。

我第一次深入接触BPE,是在尝试微调一个开源模型时。当时模型总是把一些专业术语拆得支离破碎,导致生成效果怪异。排查了半天才发现,问题出在分词上:模型自带的BPE词表没能正确合并我的专业词汇。这让我意识到,不理解BPE,就没法真正“驾驭”大模型。它远不止是一个预处理步骤,而是决定了模型如何“理解”语言的基本单元。

BPE的核心思想极其巧妙:它从最基础的字符(或字节)开始,通过不断合并最高频的相邻“符号对”,像搭积木一样,从零开始构建出一个定大小的词表。这个“从字符到子词”的构建过程,完美平衡了字符级和单词级表示的优缺点。字符级表示词表小(比如英文就26个字母),但序列太长,模型学习效率低;单词级表示语义单元清晰,但词表会爆炸式增长(想想“running”、“runner”、“runs”都要单独收录),且无法处理未登录词(OOV)。BPE找到了一条中间道路,让“est”、“ing”、“ation”这些常见词缀能被单独学习,同时又能灵活组合出“tokenization”这样的长词。

接下来,我将彻底拆解BPE的原理,从算法步骤、数学直觉到代码实现,并分享在实际应用中那些官方文档里不会写的“坑”和技巧。无论你是想深入理解Transformer架构,还是准备在自己的NLP项目中应用或定制分词器,这篇文章都能给你一份清晰的路线图。

2. BPE算法原理:一场精心设计的“贪心”合并游戏

理解BPE,最好的方式就是亲手模拟一遍它的构建过程。我们暂时忘掉那些复杂的数学公式,先来看一个最简单的例子。

2.1 一个手工演算的完整例子

假设我们的训练语料只有一句话: "low lower lowest" 。在BPE处理中,我们首先会在每个单词末尾添加一个特殊的结束符号,比如 </w> ,用来标记单词边界,这样模型就能区分“low”和“lower”中的“low”了。所以初始输入变为: "l o w </w> l o w e r </w> l o w e s t </w>" 。此时,我们的词表就是所有单个字符: {l, o, w, e, r, s, t, </w>}

现在,BPE开始它的“合并游戏”了。游戏规则很简单:

  1. 统计频率 :在整个语料中,统计所有相邻符号对出现的频率。
  2. 选择冠军 :找到出现频率最高的那个符号对。
  3. 执行合并 :将这个符号对合并成一个新的符号,加入词表,并在语料中所有出现该对的地方用新符号替换。
  4. 重复循环 :回到步骤1,直到合并操作执行了预定的次数(比如10次),或者词表大小达到了我们设定的目标(比如50个)。

让我们手动玩一轮:

  • 初始状态 :语料频率统计。

    • "lo" 出现 2 次 (在 "low" "lower" 的开头)
    • "ow" 出现 3 次 (三个单词里都有 "low" )
    • "w</w>" 出现 1 次 (仅在第一个 "low" 末尾)
    • "l" (第二个单词的起始) 和 "o" 等... 我们关注相邻对。
    • 显然, "ow" 以3次夺冠。
  • 第一次合并 :合并 "o" "w" "ow"

    • 新词表: {l, o, w, e, r, s, t, </w>, ow}
    • 新语料: "l ow </w> l ow e r </w> l ow e s t </w>"
  • 第二次统计

    • "low" 现在变成了 "l" "ow" 这对。
    • "l" "ow" 这个对出现了3次!它成为了新的冠军。
  • 第二次合并 :合并 "l" "ow" "low"

    • 新词表: {l, o, w, e, r, s, t, </w>, ow, low}
    • 新语料: "low </w> low e r </w> low e s t </w>"
  • 第三次统计

    • 现在出现了 "low</w>" 对(1次), "lowe" 对(2次)。 "lowe" 胜出。
  • 第三次合并 :合并 "low" "e" "lowe"

    • 新词表加入 lowe
    • 新语料: "low </w> lowe r </w> lowe s t </w>"

这个过程可以继续下去,合并 "lowe" "r" 得到 "lower" ,合并 "lowe" "s" 得到 "lowes" ,最后合并 "lowes" "t" 得到 "lowest"

通过这个例子,你可以直观地看到BPE如何从字符开始,逐步“发现”了单词 "low" 、词根 "lowe" 以及完整的单词 "lower" "lowest" 。它根据数据中的统计规律,自适应地构建出具有语言学意义的子词单元。

2.2 算法形式化描述与关键设计

上面是感性的认识,现在我们形式化地定义BPE的训练(词表构建)和应用(编码新文本)过程。

BPE训练算法(构建词表)

  1. 初始化 :将训练语料中所有单词拆分为字符序列,并在末尾添加单词结束符(如 </w> )。初始词表即为字符集。
  2. 循环合并 ,直到达到预设的合并次数 num_merges 或目标词表大小 vocab_size : a. 统计语料中所有相邻符号对的频率。 b. 找到频率最高的符号对 (A, B) 。 c. 将词表中所有的 A B 序列替换为新的合并符号 AB 。 d. 将 AB 加入词表。
  3. 输出 :最终的词表(包含所有字符和合并出的子词)以及合并规则记录(一个记录了所有 (A, B) -> AB 操作的列表)。

这里有几个至关重要的设计点,直接影响了BPE的最终效果:

  • 贪心与全局 :BPE是“贪心”的,它每一步只合并当前频率最高的对。这意味着它无法保证最终得到的是全局最优的词表,但计算效率极高。这种局部最优在实践中被证明非常有效。
  • 单词边界的重要性 :结束符 </w> 是关键。没有它,“cat”和“concatenate”中的“cat”会被同样对待。有了 </w> "cat</w>" "c a t" (作为子词)就是不同的符号,模型能学会区分单词结尾。
  • 词表大小与合并次数 :这是最重要的超参数。词表大小通常在1万到10万之间。更大的词表能捕获更精细的语义单元,但会导致词向量矩阵更大,增加模型参数和过拟合风险;更小的词表则更紧凑,但可能迫使模型用更长的序列来表达相同意思。 num_merges = vocab_size - len(initial_vocab)

BPE编码算法(应用词表) : 训练好后,我们需要用学到的词表来编码新的、未见过的文本。

  1. 分词 :将新文本按空格等分隔符分成单词,每个单词后加 </w> ,并拆分为字符序列。
  2. 迭代合并 :遍历训练时保存的合并规则列表( 按照它们被学习到的顺序 ),对于每条规则 (A, B) -> AB ,尝试在当前单词的符号序列中,找到所有 A 后面紧跟着 B 的位置,并将其替换为 AB
  3. 输出 :当所有合并规则都应用完毕后,得到的符号序列就是该单词的BPE分词结果。

编码过程的核心是 “最长匹配”或“贪心匹配” 。因为合并规则是按学习顺序应用的(先学的规则对应更频繁、更基础的合并),所以这个过程等价于:对于当前字符序列,总是优先尝试合并词表中存在的最长的可能子词。这确保了分词的稳定性和一致性。

注意 :这里有一个常见的误解区。编码时并不是用最终的词表去做字符串完全匹配,而是 重放合并过程 。这是因为词表中的子词(如“ing”)可能本身也是中间合并的产物,直接匹配会导致无法正确拆分出更基础的单元。必须遵循合并的历史路径。

3. 从原理到实践:Python代码实现与逐行解析

理解了算法,我们亲手实现一个简化但功能完整的BPE分词器。我们将分两步走:先实现词表训练,再实现编码解码。

3.1 BPE训练器(Vocabulary Builder)实现

import re
from collections import defaultdict, Counter

class BPEtrainer:
    def __init__(self, vocab_size=10000):
        self.vocab_size = vocab_size
        self.vocab = {}  # 最终词表:token -> id
        self.merges = {}  # 记录合并规则:(token1, token2) -> merged_token
        self.pattern = re.compile(r"(\w+|[^\w\s]+|\s+)")  # 简单的分词模式,区分单词、标点、空格

    def _get_stats(self, word_freq):
        """统计当前语料中所有相邻符号对的频率"""
        pairs = defaultdict(int)
        for word, freq in word_freq.items():
            symbols = word.split()  # 单词已被表示为用空格隔开的符号序列
            for i in range(len(symbols) - 1):
                pair = (symbols[i], symbols[i + 1])
                pairs[pair] += freq
        return pairs

    def _merge_vocab(self, pair, word_freq):
        """在所有单词中合并指定的符号对"""
        bigram = ' '.join(pair)
        replacement = ''.join(pair)
        new_word_freq = {}
        for word, freq in word_freq.items():
            # 替换所有出现的 bigram
            new_word = word.replace(bigram, replacement)
            new_word_freq[new_word] = freq
        return new_word_freq

    def train(self, corpus):
        """训练BPE词表"""
        # 1. 预处理:分词、加结束符、字符化
        word_freq = Counter()
        for text in corpus:
            # 使用简单正则进行初步分词,更复杂的可以基于空格
            tokens = self.pattern.findall(text)
            for token in tokens:
                if token.isspace():
                    continue  # 忽略纯空格
                # 在单词末尾添加结束符 `</w>`,并拆分成字符(用空格隔开)
                word = ' '.join(list(token)) + ' </w>'
                word_freq[word] += 1

        # 初始词表是所有字符
        vocab = set()
        for word in word_freq.keys():
            vocab.update(word.split())
        self.vocab = {token: idx for idx, token in enumerate(sorted(vocab))}

        # 2. 迭代合并
        num_merges = self.vocab_size - len(self.vocab)
        for i in range(num_merges):
            pairs = self._get_stats(word_freq)
            if not pairs:
                break  # 没有可合并的对
            # 找到频率最高的对
            best_pair = max(pairs, key=pairs.get)
            # 执行合并
            word_freq = self._merge_vocab(best_pair, word_freq)
            # 记录合并规则
            merged_token = ''.join(best_pair)
            self.merges[best_pair] = merged_token
            # 更新词表
            self.vocab[merged_token] = len(self.vocab)

        print(f"训练完成。词表大小:{len(self.vocab)}, 合并次数:{len(self.merges)}")
        return self.vocab, self.merges

关键代码解析与避坑点

  1. 预处理中的结束符 word = ' '.join(list(token)) + ' </w>' 这行代码做了两件事: list(token) 将单词变成字符列表, ' '.join() 用空格连接它们使其成为初始符号序列,然后加上 </w> 务必确保 </w> 是一个独立的符号 ,前面有空格。
  2. _get_stats 的统计逻辑 :这里统计的是“符号对”的频率,符号可能已经是合并过的子词(如 "lo" )。 word_freq 中的 word 此时是一个字符串,如 "l o w </w>" ,我们用 split() 将其还原成符号列表再统计相邻对。
  3. _merge_vocab 的合并操作 bigram = ' '.join(pair) 是关键。因为我们的符号序列是用空格连接的字符串,所以合并对象是中间带空格的 "o w" ,要将其替换为不带空格的 "ow" 。这里使用字符串的 replace 方法,它是一个全局替换,简单但需注意:如果 bigram 恰好是另一个更长token的子串(概率极低),可能会错误替换。工业级实现会遍历符号列表进行精确替换。
  4. 合并顺序的保存 self.merges[best_pair] = merged_token 按顺序记录了每一轮合并。 编码时必须严格按照这个顺序进行 ,才能复现训练时的合并路径。

3.2 BPE编码器(Tokenizer)实现

有了训练好的词表和合并规则,我们就可以编码新文本了。

class BPEtokenizer:
    def __init__(self, vocab, merges):
        self.vocab = vocab  # token -> id 映射
        self.id2token = {id: token for token, id in vocab.items()}  # id -> token 反向映射
        self.merges = merges  # (t1, t2) -> merged 的合并规则记录
        # 将合并规则按照学习顺序存储在一个列表中,用于编码
        self.merge_rules = list(merges.items())  # [((t1, t2), merged), ...]

    def encode(self, text):
        """将文本编码为token id列表"""
        # 1. 预处理:简单分词,字符化,加结束符
        tokens = self.pattern.findall(text)
        word_tokens = []
        for token in tokens:
            if token.isspace():
                # 对于空格,可以赋予一个特殊的token,如`<space>`,这里简单跳过或保留
                continue
            # 字符化并加结束符
            symbols = list(token) + ['</w>']
            word_tokens.append(symbols)

        # 2. 对每个单词应用合并规则
        encoded_ids = []
        for symbols in word_tokens:
            # 遍历所有合并规则(按学习顺序)
            for pair, merged in self.merge_rules:
                i = 0
                while i < len(symbols) - 1:
                    if symbols[i] == pair[0] and symbols[i + 1] == pair[1]:
                        # 合并
                        symbols[i] = merged
                        del symbols[i + 1]
                        # 合并后继续检查当前位置,因为新的符号可能与后面的再次合并
                    else:
                        i += 1
            # 将合并后的符号转换为id
            for token in symbols:
                if token in self.vocab:
                    encoded_ids.append(self.vocab[token])
                else:
                    # 处理未知字符(应很少出现,因为词表基于字符初始化)
                    encoded_ids.append(self.vocab.get('<unk>', 0))  # 假设有<unk> token
        return encoded_ids

    def decode(self, token_ids):
        """将token id列表解码回文本"""
        tokens = [self.id2token.get(id, '<unk>') for id in token_ids]
        # 拼接tokens,并处理结束符</w>
        text = ''
        for token in tokens:
            if token.endswith('</w>'):
                text += token[:-4] + ' '  # 去掉</w>并加空格
            else:
                text += token
        return text.strip()

编码过程的核心细节与避坑点

  1. 按顺序应用合并规则 for pair, merged in self.merge_rules: 这行代码确保了先应用最早学习到(最基础、最频繁)的合并规则。这是实现“最长匹配”的关键。
  2. 合并时的指针操作 while i < len(symbols) - 1: 循环需要小心处理。当在位置 i 执行合并后, symbols[i] 变成了新token merged 。此时 不应立即 i += 1 ,因为 merged 可能与 symbols[i+1] (原 symbols[i+2] )构成新的可合并对。所以只有不合并时,指针才前进。
  3. 解码的歧义性 :解码看似简单,直接拼接。但这里存在一个理论上的歧义:如果词表中同时存在 "low" "lower" ,那么编码 "lower" 时可能直接输出 "lower" 的id,也可能输出 "low" "er" 的id。只要编码过程是确定的,解码就能唯一还原。但我们的解码器 decode 函数假设每个id对应一个完整的子词,直接拼接。 在实际的BPE实现(如Hugging Face的tokenizers库)中,解码时会特别注意处理空格和特殊符号 ,通常会在词汇表中为空格保留一个特殊token(如 Ġ <space> ),或者在编码时保留单词间的空格信息。
  4. 未知词处理 :我们的简单实现假设所有字符都在初始词表中。但现实文本中可能出现罕见字符(如特殊符号)。一个健壮的实现需要包含 <unk> (未知)token,并在初始化词表时加入。当遇到不在 vocab 中的字符时,就映射到 <unk>

3.3 运行一个端到端的示例

让我们用上面的类来训练并测试一下。

# 训练
corpus = [
    "The cat sat on the mat.",
    "The dog chased the cat.",
    "Cats and dogs are pets."
]
trainer = BPEtrainer(vocab_size=50)
vocab, merges = trainer.train(corpus)

# 初始化分词器
tokenizer = BPEtokenizer(vocab, merges)

# 编码新句子
text = "The cat sat on a new mat."
encoded_ids = tokenizer.encode(text)
print(f"编码结果(ID): {encoded_ids}")
print(f"编码结果(Token): {[tokenizer.id2token[id] for id in encoded_ids]}")

# 解码
decoded_text = tokenizer.decode(encoded_ids)
print(f"解码文本: {decoded_text}")

这个简单的例子会让你看到BPE如何从“T h e ”开始,逐步合并出“The”、“cat”、“s at”(可能)等子词。通过调整 vocab_size ,你可以观察到词表大小如何影响分词粒度。

4. 工业级实现考量与Hugging Face Tokenizers库探秘

我们自己实现的BPE是教学性质的,理解了核心逻辑。但在生产环境中,我们几乎不会从头写BPE,而是使用高度优化的库,如 Hugging Face tokenizers 。了解这些工业级实现的细节,能帮助我们更好地使用和调试它。

4.1 我们的实现与工业实现的差距

  1. 效率 :我们的 _merge_vocab 使用字符串替换,并且每次合并都全量更新 word_freq 字典,复杂度很高。工业实现(如 tokenizers )使用更高效的数据结构,例如优先队列(堆)来维护符号对频率,并在合并时只更新受影响的部分,将训练复杂度从 O(V * N) 优化到接近 O(N log V),其中V是词表大小,N是语料总符号数。
  2. 预处理与规范化(Normalization) :我们用了简单的正则分词。实际上,在BPE之前,文本需要经过一系列规范化处理:
    • Unicode规范化 :将“café”和“cafe\u0301”统一。
    • 大小写处理 :是否全部转为小写(会丢失信息)。
    • 标点符号分离 :将“don't”处理成“do n't”还是“don t”。
    • 特定语言规则 :中文、日文等不需要空格分词的语言,需要先用专门的分词器(如Jieba)切分,或者直接采用字符/子词作为起点。 tokenizers 库提供了丰富的 Normalizer PreTokenizer 组件。
  3. 字节级BPE(Byte-Level BPE) :这是GPT-2/3/4等模型采用的技术。它解决了一个根本问题:用UTF-8字符作为初始词表,对于多语言语料,字符集可能非常大(如中文有数万个字符)。字节级BPE的初始词表是 256个字节 (0-255)。任何文本都先编码为UTF-8字节序列,然后在这个字节序列上运行BPE。这样做的好处是:
    • 词表极小且固定 :永远只有256个基础单元。
    • 永不出现未知token :任何文本都能被表示为字节序列。
    • 多语言无缝支持 :所有语言都被降解到相同的字节空间。 缺点是序列长度会变长(一个非ASCII字符可能由多个字节表示),但模型能力足以应对。
  4. 词表文件与合并规则文件 :工业实现通常将训练结果保存为两个文件:
    • vocab.json : 一个从token到id的映射字典。
    • merges.txt : 一个文本文件,每行记录一次合并,例如 l o (表示先合并l和o)。 这个文件的顺序就是合并规则的应用顺序

4.2 使用Hugging Face Tokenizers库实战

让我们看看如何用 tokenizers 库快速实现一个BPE分词器,并理解其配置。

from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace

# 1. 初始化一个BPE模型
tokenizer = Tokenizer(BPE(unk_token="[UNK]")) # 使用BPE模型,指定未知token

# 2. 设置预分词器:这里用简单的空格分词。对于英文,这通常够了。
tokenizer.pre_tokenizer = Whitespace()

# 3. 配置训练器
trainer = BpeTrainer(
    vocab_size=30000,        # 目标词表大小
    special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"], # 特殊token
    min_frequency=2,         # 忽略出现次数低于此值的token
    show_progress=True       # 显示进度条
)

# 4. 准备训练文件列表(假设我们有很多文本文件)
files = ["path/to/corpus1.txt", "path/to/corpus2.txt"]

# 5. 开始训练
tokenizer.train(files, trainer)

# 6. 保存与加载
tokenizer.save("my_bpe_tokenizer.json")
loaded_tokenizer = Tokenizer.from_file("my_bpe_tokenizer.json")

# 7. 使用
encoded = loaded_tokenizer.encode("Hello, world! This is a test.")
print(encoded.tokens)  # 输出:['Hello', ',', 'world', '!', 'This', 'is', 'a', 'test', '.']
print(encoded.ids)     # 输出对应的id列表

# 查看词表
vocab = loaded_tokenizer.get_vocab()
print(f"词表大小: {len(vocab)}")

关键参数解析与经验

  • vocab_size :这是最重要的参数。对于通用英文模型,3万是一个常用起点。对于多语言或代码模型,可能需要5万到10万。 建议从小数据集开始实验 ,观察分词结果是否合理。
  • special_tokens :必须包含 [UNK] [CLS] , [SEP] , [PAD] , [MASK] 是BERT类模型需要的。GPT类模型通常需要 <|endoftext|> 等。这些特殊token会被强制加入词表,不参与BPE合并过程。
  • min_frequency :这是一个重要的过滤阈值。出现次数太少的单词或子词,可能只是噪声,将其排除在合并候选之外可以提升词表质量。一般设置为2或3。
  • 预分词器(PreTokenizer)的选择 Whitespace 只是按空格分。对于英文,更常用的是 ByteLevel (配合BPE模型实现字节级BPE)或者 Metaspace (将空格替换为特定符号如 ,便于还原)。对于中文,你需要先使用 BertPreTokenizer (基于空格和标点)或传入预切分好的词序列。

4.3 调试与可视化:你的分词器在“看”什么?

当你发现模型生成奇怪的结果时,第一个怀疑对象往往是分词器。如何调试?

  1. 直接编码查看 tokenizer.encode("你的句子").tokens 。这是最直接的方法,看一个句子被切成了什么样。
  2. 检查未知词 :统计一批数据中 [UNK] 的比例。如果比例过高,说明词表太小或训练语料与当前数据领域不匹配。
  3. 可视化工具 tokenizers 库提供了 enable_padding() enable_truncation() ,但更直观的是用第三方库如 tiktoken (OpenAI的)或写个小函数统计子词长度分布。
  4. 一个实用的调试技巧 :当你有一个领域特定的术语(如“Transformer”),但模型总是将其拆开导致效果不好时,你可以:
    • 在训练语料中增加该术语的重复次数 ,提高其频率,使其更可能被合并成一个token。
    • 使用 tokenizer.add_tokens() 方法 ,在训练后手动将该术语作为新token加入词表(需要重新调整模型嵌入层的大小)。这是解决特定领域术语问题的有效后门。

5. BPE的局限、变体与未来

尽管BPE是当前的事实标准,但它并非完美。了解其局限和演进方向,能帮助我们在合适的场景选择或改进它。

5.1 BPE的已知局限

  1. 贪心算法的局部最优 :BPE每一步的合并只考虑当前最优对,无法回退。这可能导致次优的词表。例如,在“abcd”语料中,如果“ab”和“bc”频率相同,先合并哪个会导致不同的最终词表。
  2. 对形态丰富的语言不友好 :对于土耳其语、芬兰语等具有大量复杂词形变化的语言,BPE可能会产生大量非常长且稀疏的子词,因为词缀组合太多。
  3. 编码和解码的不对称性 :编码时采用贪心最长匹配,但解码只是简单拼接。对于某些边缘情况,可能存在多个分词序列对应同一文本,但BPE只输出一种。
  4. 数字处理问题 :BPE通常会把数字(如“123”)拆分成单个数字字符(“1”,“2”,“3”),这不利于模型学习数值关系和数学推理。

5.2 主流变体:WordPiece与SentencePiece

  • WordPiece :谷歌BERT模型采用。它与BPE流程几乎一样,唯一区别在于选择合并哪个“符号对”的标准。BPE看 频率最高 的,而WordPiece看 能最大程度提升语言模型概率 的。具体来说,它计算合并一对符号后,训练数据似然值的增加量,选择提升最大的对。WordPiece倾向于合并能形成更常见“词”的单元。在实践中,两者效果非常接近。
  • SentencePiece :谷歌推出的一个开源工具, 将分词器本身视为一个独立的语言模型 。它的关键创新是:
    • 无需预分词 :直接将原始文本(包括空格)当作一个字符序列输入。空格被当作普通字符(如 _ )处理,这样分词器可以自己决定是否将空格作为token的一部分,从而支持像中文、日文这样没有空格分隔的语言。
    • 支持多种训练算法 :它实现了BPE和Unigram两种算法。Unigram算法从一个大词表开始,逐步丢弃概率低的token,是一种自上而下的方法,理论上能获得全局更优的词表。
    • 标准化与去标准化 :内置了丰富的文本规范化选项。

5.3 面向未来的思考:BPE还是终点吗?

随着模型规模扩大和能力增强,分词本身的一些根本问题被重新审视:

  • 是否需要分词? 一些研究(如CANINE、ByT5)直接使用字符或字节序列作为输入,完全抛弃了分词。这简化了流程,避免了分词错误,但序列长度极大增加,对计算带来挑战。然而,更强大的模型(如Mamba、Transformer with Long Context)正在缓解这一压力。
  • 数据驱动的局限性 :BPE的词表完全由训练数据分布决定。如果数据有偏(比如代码训练数据少),那么对代码的分词就会很差。未来可能会有更多 任务自适应 动态分词 的技术。
  • 与模型架构的协同 :分词器和模型是分开训练的。是否存在一种端到端的方式,让模型在训练过程中同时学习最优的“分词”方式?这可能是下一个研究方向。

对于我们应用者来说,现阶段掌握BPE及其变体,理解其原理和调参方法,足以应对绝大多数NLP任务。当你的模型在特定任务上表现不佳时,不妨看一眼它的分词结果,也许问题的钥匙就藏在那些被切分开的子词里。

更多推荐