Tokenizer(切词器)的不同实现算法

 博主会经常分享自己在人工智能阶段的学习笔记,欢迎大家访问我滴个人博客!(都不白来!)

小牛壮士 - 个人博客https://kukudelin.top/

前言

Tokenizer(分词器) 的作用是将一段文本分割成一个个有意义的单元,每个单元在词向量表中对应一个的索引,根据这个索引我们能得到词向量,例如jieba就是非常流行的中文分词器Transformers的AutoTokenizer可以用于英文的分词,各个模型使用的分词逻辑也有不同,对于输入的一串文本,Tokenizer会如何处理,下面介绍了一系列算法

一、Tokenizer的三种颗粒度实现

  • Word-based 分词:直接从词汇表中查找文本中的单词进行分词。

    • 示例 1:词汇表 ["hello", "how", "are", "you"],句子 "how are you" → 分词结果:["how", "are", "you"]

    • 示例 2:句子 "how are you today" → 分词结果:["how", "are", "you", "<unk>"](“today”为未知单词)。

  • Character-based 分词:将文本分割成单个字符,不依赖词汇表。

    • 示例:句子 "how are you ?" → 分词结果:["h", "o", "w", " ", "a", "r", "e", " ", "y", "o", "u", "?"]

  • Subword-based 分词:将单词分解为子词单元

    • 示例:句子 "playing games is fun" → 分词结果:["play", "##ing", "games", "is", "fun"](“playing”被分解为“play”和“##ing”)。##表示一个词的结尾

上述三种方式分出的字词进入到字典{Word:index}中被转换为索引,最终在词向量表中映射成为词向量

二、Subword的不同算法

下面我们讨论如何将一个句子分成一串字词

2.1 BPE(Byte-Pair Encoding,字节对编码)

直接用一个句子来演示BPE是怎么进行分词的:"the cat sat on the mat"

①:初始化词汇表:['t', 'h', 'e', ' ', 'c', 'a', 't', 's', 'o', 'n', 'm']包括空格

②:统计相邻字符对频率:'th': 2, 'he': 2, 'e ': 2, ' c': 1, 'ca': 1, 'at': 3, ' s': 1, 'sa': 1, ' o': 1, 'on': 1, 'n ': 1, ' m': 1, 'ma': 1

③:词汇表中合并频率最高的字符对:这里最高的是3次的"at",['t', 'h', 'e', ' ', 'c', 'at' , 's', 'o', 'n', 'm']

④:更新相邻字符对频率'th': 2, 'he': 2, 'e ': 2, ' c': 1, 'cat': 1, ' s': 1, 'sat': 1, ' o': 1, 'on': 1, 'n ': 1, ' m': 1, 'mat': 1

⑤:选中频率最高的“th”,重复③

当所有字符对的频率都为 1 时,停止合并操作,最终分词结果"the" "cat" "sat" "on" "the" "mat"

缺陷:这一过程中,词汇表需要涵盖句子中的所有字符,可能会相当大

2.2 BBPE(Byte-Level Byte-Pair Encoding,BPE的字节级扩展版本)

原理和BPE一致,只是使用字节(byte)作为初始token,适用于任何文本。

还是以"the cat sat on the mat"为例,将其中的每个字母和符号转换为字节

[116, 104, 101, 32, 99, 97, 116, 32, 115, 97, 116, 32, 111, 110, 32, 116, 104, 101, 32, 109, 97, 116]

(这里,每个数字代表一个字节,例如 116t 的 ASCII 码),当第③步进行合并后,将合并成的字符串赋予新的ASCII 码,例如"at"——>97_116——>256,

然后按照和BPE一样的处理逻辑进行分词,不会出现OOV问题

2.3 WordPiece (★)

WordPiece 是一种基于统计的子词分词算法,它通过训练数据动态学习词汇表,将罕见词分解为更小的已知子词单元,同时保留常见词作为完整单元

在经过和BPE中②一样的步骤得到相邻字符对频率后'th': 2, 'he': 2, 'e ': 2, ' c': 1, 'ca': 1, 'at': 2, ' s': 1, 'sa': 1, 'at': 2, ' o': 1, 'on': 1, 'n ': 1, ' m': 1, 'ma': 1

根据WordPiece 得分公式来计算pair得分

例如出现3次的"at"的token1为"a",出现了3次,token2为"t",出现了5次,最终pair得分为0.2

最终计算出全部pair得分,合并对应字符

三、token过程——贪婪最长匹配优先算法

上述已经讲解了几种分词的流程,那么对于一个复杂的单词running,是如何对他进行拆解的呢

首先给定词汇表如下

["un", "##able", "##ing", "##e", "##d", "re", "##run", "##runing", "run"]

分词 unrunning 的过程:

  • 匹配最长前缀 un,剩余 running 。

  • 匹配最长前缀 run,剩余 ning。

  • 无法匹配 ning,回退到字符级:

    • 匹配 n + ing

最终分词结果:

["un", "run", "##n", "##ing"]
### Tokenizer算法原理 Tokenizer的主要功能在于将输入文本转换成一系列的标记(tokens),这些标记可以是单、字符或是子单位。此过程对于自然语言处理至关重要,因为机学习模型通常无法直接理解原始文本数据。 汇表作为Tokenizer的核心组件,负责将所有可能出现的标记映射至独一无二的整数ID[^1]。这种映射不仅涵盖了基础标记和特殊标记,还包含了由特定子分割算法产生的子单元。这样的设计使得即使遇到未曾见过的新项,也能有效地将其分解并编码为已知的部分组合形式。 ### 子分割技术简介 常见的子字分策略有Byte Pair Encoding (BPE),WordPiece 和 SentencePiece等几种方式: - **Byte Pair Encoding (BPE)** 是一种基于频率统计的方法,在训练过程中不断寻找最常出现的一对标记对,并创建一个新的复合标记来表示这对标记。 - **WordPiece** 类似于BPE,但在选择新的标记时会考虑最大化整体语料库的概率分布,从而更倾向于保留高频短语而不拆解它们。 - **SentencePiece** 则提供了一种无监督的学习框架,可以直接作用于未标注的数据集上构建高效的前缀树结构用于快速查找匹配路径。 ### 实现示例:简易版 BPE Tokenizer 下面给出一段Python代码片段展示如何简单实现一个基于BPE算法的Tokenzier: ```python from collections import defaultdict, Counter import re class SimpleBPETokenizer: def __init__(self): self.vocab = {} # Vocabulary mapping from token to id self.id_to_token = [] # Reverse lookup list for ids def train(self, corpus, vocab_size=30000): """Train the tokenizer on a given text corpus.""" initial_vocab = set(' '.join(corpus).split()) self._build_initial_vocab(initial_vocab) pairs_freq = self._get_pair_frequencies(corpus) while len(self.vocab) < vocab_size and pairs_freq: best_pair = max(pairs_freq, key=pairs_freq.get) new_token = ''.join(best_pair) self.add_token(new_token) # Update pair frequencies after adding new token pairs_freq = self._update_pairs_frequency(best_pair, pairs_freq) def add_token(self, token): idx = len(self.vocab) self.vocab[token] = idx self.id_to_token.append(token) def _build_initial_vocab(self, tokens): for tok in sorted(tokens): self.add_token(tok) @staticmethod def _get_pair_frequencies(texts): counts = Counter() pattern = re.compile(r'(?u)([a-zA-Z]+)') all_tokens = ' '.join(pattern.findall(' '.join(texts))) bigrams = zip(*[all_tokens[i:] for i in range(2)]) counts.update(bigrams) return dict(counts) @staticmethod def _update_pairs_frequency(pair, freq_dict): updated = {} for k, v in freq_dict.items(): if pair not in k: updated[k] = v return updated def encode(self, sentence): encoded_sentence = [] words = sentence.split() for word in words: subwords = [] current_subword = '' for char in word + '</w>': current_subword += char if current_subword in self.vocab or '</w>' in current_subword: subwords.append(current_subword.replace('</w>', '')) current_subword = '' encoded_ids = [self.vocab[subword] for subword in subwords] encoded_sentence.extend(encoded_ids) return encoded_sentence # Example usage of the simple BPE tokenizer corpus = ["hello world", "hello there"] tokenizer = SimpleBPETokenizer() tokenizer.train(corpus, vocab_size=50) print(f"Vocabulary: {tokenizer.vocab}") encoded_text = tokenizer.encode("hello") print(f"Encoded Text IDs: {encoded_text}") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值