当前位置: 首页 > news >正文

一文搞懂SentencePiece的使用

目录

  • 1. 什么是 SentencePiece?
  • 2. SentencePiece 基础概念
    • 2.1 SentencePiece 的工作原理
    • 2.2 SentencePiece 的优点
  • 3. SentencePiece 的使用
    • 3.1 安装 SentencePiece
    • 3.2 训练模型与加载模型
    • 3.3 encode(高频)
    • 3.4 decode(高频)
    • 3.5 设置相关选项(不常用)
  • 4. Trainer的使用
  • 5. 大小写相关问题
  • 6. 一些误区
  • Ref

1. 什么是 SentencePiece?

在自然语言处理(NLP)任务中,文本的预处理环节至关重要。无论是机器翻译、语言模型,还是问答系统,如何将原始文本转化为模型能够理解的输入是其中一个关键步骤。词汇表的构建和分词方式的选择,往往会直接影响模型的性能。而 SentencePiece 是谷歌开发的一款用于构建词汇表和分词的工具,它特别适用于那些没有明确分词边界的语言,能够在子词级别上实现无监督的文本分割。

SentencePiece 是一种基于子词单元的分词器,广泛应用于机器翻译和文本生成任务中。与传统分词方法不同,SentencePiece 并不依赖于语言的词汇结构,能够直接处理不带空格的语言(例如中文、日文)。它基于两种主要的算法:Byte-Pair Encoding (BPE)Unigram Language Model,在生成子词单元的同时,提供了灵活的词汇表管理方式。

2. SentencePiece 基础概念

2.1 SentencePiece 的工作原理

SentencePiece 的核心思想是将文本分解为子词单元(subword units)。它不依赖预定义的词汇表,而是通过统计学习自动构建子词单元。无论是空格分隔的语言(如英语),还是无空格分隔的语言(如中文、日文),它都能够处理。这样做的好处是,它可以处理不在词汇表中的未知词,并且有效减少词汇表的大小,从而降低 OOV(Out of Vocabulary)问题。

SentencePiece 提供了两种主要的分词算法:

  • Byte-Pair Encoding (BPE):通过反复合并最频繁的子词对,逐渐构建子词词汇表。BPE 是一种贪心算法,它通过合并最频繁的字符或子词对来构建词汇表。
  • Unigram Language Model:这是一种概率模型,基于一个语言模型来选择最优的子词分割方式。它从一个完整的子词词典开始,逐步移除低概率的子词,最终保留高概率的子词作为最终的词典。

📝 SentencePiece底层使用C++实现,并通过SWIG暴露接口

2.2 SentencePiece 的优点

  • 语言无关性SentencePiece 不依赖于语言的词汇结构,可以直接用于无空格分隔的语言(如中文)。
  • 子词单元的灵活性:通过子词分割,模型可以处理未知词,避免 OOV 问题。
  • 词汇表大小可控:通过设定词汇表大小,可以精确控制子词的数量,平衡模型性能与存储资源。
  • 简化文本预处理:传统方法需要对文本进行分词和词汇表生成,而 SentencePiece 将这两个步骤合并,简化了工作流程。

3. SentencePiece 的使用

SentencePiece 提供了易用的 Python API,帮助开发者快速集成到项目中。在这一章节中,我们将详细介绍如何使用 SentencePiece 进行文本分割、词汇表训练、子词编码等操作。

3.1 安装 SentencePiece

在使用 SentencePiece 之前,需要先通过 pip 安装相关库:

pip install sentencepiece

安装完成后,我们就可以开始使用 SentencePiece 的 Python API 进行分词和词汇表构建。

3.2 训练模型与加载模型

SentencePiece 的第一个主要功能是训练分词模型。我们可以使用 SentencePiece 来学习文本中的子词分割模式,并生成一个可用于后续编码的词汇表。训练过程主要分为以下步骤:

  1. 准备好要训练的文本数据。
  2. 使用 SentencePieceTrainer 进行模型训练。
import sentencepiece as spm# 假设我们有一个文本文件 'data.txt',其中包含我们希望训练的文本数据
spm.SentencePieceTrainer.train(input='data.txt', model_prefix='mymodel', vocab_size=8000)

上述代码中,train 函数用于训练模型,input 参数指定输入文件,model_prefix 用于指定输出模型的前缀,vocab_size 则是我们希望的词汇表大小。在训练完成后,会生成两个文件:

  • mymodel.model:这是分词模型文件,包含了子词分割的规则。
  • mymodel.vocab:这是词汇表文件,列出了所有生成的子词。

训练完成后,我们可以加载生成的模型进行文本分割和编码操作。首先,我们需要使用 SentencePieceProcessor 来加载模型文件。

import sentencepiece as spm# 加载已训练好的模型
sp = spm.SentencePieceProcessor(model_file='mymodel.model')

加载完成后,sp 对象即是我们用于进行文本处理的分词器。

📝 .model 是二进制文件,.vocab 是文本文件。.model 文件已经包含了词汇表,所以加载模型的时候不需要传入 .vocab 文件。.vocab 文件仅仅是用来辅助开发者了解词汇表的。

接下来以LLaMA Tokenizer为例。

查看词表大小:

# 四种方法效果相同
print(sp.vocab_size())
print(sp.get_piece_size())
print(sp.piece_size())
print(len(sp))
# 均是32000

获取整个词表:

for i in range(len(sp)):print(sp.id_to_piece(i))

或者执行

spm_export_vocab --model=mymodel.model --output=mymodel.vocab

获取某一个token的id不仅可以用 piece_to_id 方法,还可以直接调用 __getitem__

print(sp['<unk>'])  # 0

查看特殊词元:

print(sp.unk_id())  # 0
print(sp.bos_id())  # 1
print(sp.eos_id())  # 2
print(sp.pad_id())  # -1,意味着没有设置pad token

3.3 encode(高频)

加载模型后,我们可以使用 SentencePieceProcessor 对文本进行分词和还原。

encodedecode 可以说是用的最多的两个方法了,前者用来分词,后者用来对分词后的结果进行还原。

encode 的函数签名如下:

def encode(self, input: str, out_type: Type[Union[int, str]] = int, add_bos: bool = False, add_eos: bool = False
) -> Union[List[int], List[str]]:

out_type 决定了分词结果是 List[int] 还是 List[str]

text = "This is a test."
print(sp.encode(text))
print(sp.encode(text, out_type=str))
print(sp.encode(text, add_bos=True, add_eos=True))
print(sp.encode(text, add_bos=True, out_type=str))

输出分别为:

[910, 338, 263, 1243, 29889]
['▁This', '▁is', '▁a', '▁test', '.']
[1, 910, 338, 263, 1243, 29889, 2]
['<s>', '▁This', '▁is', '▁a', '▁test', '.']

当然我们还可以批量进行分词:

text1 = "This is a test."
text2 = "Another test sentence."
text3 = "SentencePiece is a useful tool for tokenization."
text4 = "Batch encoding is efficient."texts = [text1, text2, text3, text4]print(sp.encode(texts))
print('-' * 15)
print(sp.encode(texts, out_type=str))

输出:

[[910, 338, 263, 1243, 29889], [7280, 1243, 10541, 29889], [28048, 663, 29925, 347, 346, 338, 263, 5407, 5780, 363, 5993, 2133, 29889], [350, 905, 8025, 338, 8543, 29889]]
---------------
[['▁This', '▁is', '▁a', '▁test', '.'], ['▁Another', '▁test', '▁sentence', '.'], ['▁Sent', 'ence', 'P', 'ie', 'ce', '▁is', '▁a', '▁useful', '▁tool', '▁for', '▁token', 'ization', '.'], ['▁B', 'atch', '▁encoding', '▁is', '▁efficient', '.']]

如果你觉得每次都要指定 out_type 略显麻烦,sp 还提供了 encode_as_idsencode_as_pieces 两种接口,它们相当于:

encode_as_ids = lambda input: encode(input, out_type=int)
encode_as_pieces = lambda input: encode(input, out_type=str)

3.4 decode(高频)

decode 就是将分词后的结果还原成字符串。其函数签名如下:

def decode(self, input: Union[List[int], List[str]], 
) -> str:

decode 会自动检测输入的类型,如下:

text = "This is a test."
list_int = sp.encode_as_ids(text)
list_str = sp.encode_as_pieces(text)print(sp.decode(list_int))
print(sp.decode(list_str))

均能正确还原原始的 text

sp 中还提供了 decode_idsdecode_pieces 两种方法,但实际上比较鸡肋,它们其实都指向 decode 方法,所以不如直接用 decode

3.5 设置相关选项(不常用)

SentencePiece 提供了许多选项,例如:

# 分词的时候默认加上eos
sp.set_encode_extra_options("eos")
text = "This is a test."
print(sp.encode(text))  # [910, 338, 263, 1243, 29889, 2]

此时即使设置 add_bos=False 也无济于事,所以 set_encode_extra_options优先级是最高的,要谨慎设置。

常见的选项有:

  • eos:默认在分词结果里加上 eos
  • bos:默认在分词结果里加上 bos
  • reverse:默认颠倒分词结果。
  • unk:在分词结果里对未出现的词元设置为 unk

还可以通过 : 将多个选项组合在一起使用:

sp.set_encode_extra_options("bos:eos:reverse")
text = "This is a test."
print(sp.encode(text, out_type=str))
# ['</s>', '.', '▁test', '▁a', '▁is', '▁This', '<s>']

清空已设置的选项只需执行

sp.set_encode_extra_options('')

此外,set_vocabulary 限制分词器使用指定的词汇,reset_vocabulary 恢复完整词汇表,load_vocabulary 根据频率阈值动态加载词汇表,但这三个方法都不会改变原始模型的词汇表大小。

4. Trainer的使用

以上我们仅讨论了如何使用 Processor,但还没有讨论如何使用 Trainer

spm.SentencePieceTrainer.train 中的参数可通过执行如下命令

spm_train --help

来查看。函数签名如下(仅列举了常用的参数):

def train(input: Optional[Union[str, List[str]]] = None,model_prefix: Optional[str] = None,vocab_size: int = 8000,model_type: str = "unigram",character_coverage: float = 0.9995,max_sentence_length: int = 4192,user_defined_symbols: Optional[Union[str, List[str]]] = None,unk_id: int = 0,bos_id: int = 1,eos_id: int = 2,pad_id: int = -1,unk_piece: str = '<unk>',bos_piece: str = '<s>',eos_piece: str = '</s>',pad_piece: str = '<pad>',
):
  • input:输入可以是一个文件也可以是多个文件,多个文件就是 List[str]。文件中的每一行都应当是一个未经处理的原始句子
  • model_prefix:训练结束后,生成的两个文件分别为 <model_prefix>.model<model_prefix>.vocab
  • vocab_size:词汇表大小,默认为8000。注意训练完后不一定会严格等于这个大小,可开启 hard_vocab_limit 来强行限制(事实上这个选项默认就是 True)。
  • model_type:选择哪一个模型。有 unigram(默认)、bpecharword 四种可选。如果选择 word,那么输入文件中的每一个句子必须是已经预分词的形态。
  • character_coverage:控制模型覆盖多少比例的字符。对于日文,中文基本字符比较多的语言,0.9995是一个不错的选择。对于英文这种基本字符比较少的语言,设置为1就行。默认为0.9995。
  • max_sentence_length:训练时每个句子在字节意义下的长度超过这个值就会被阶段。默认是4192。
  • user_defined_symbols:可以传入用户自定义的token。注意,这里不能包含 unk_piece,否则会报错。但可以包含 bos_piece 等特殊词元,只不过会合二为一。
  • 特殊词元:可通过设置相应的id为 − 1 -1 1 来关闭这个词元。注意 <unk> 必须存在。默认没有 <pad> 词元。

如果将 bos_id 设为 − 1 -1 1,那么 user_defined_symbols[0] 就会被放置在词汇表中索引为 1 1 1 的地方(因为 bos_id 原先是1),以此类推,从前往后填满所有的空缺位置。如果 bos_id 没有设为 − 1 -1 1,但是 user_defined_symbols 中又含有 bos_piece,那么 user_defined_symbols 中的 bos_piece 就会失效。

由此可知,.vocab 文件的前半部分由Special Tokens和User Defined Symbols组成,而后半部分就是模型训练过程中所产生的Subwords了。

我们可以观察词表中的前半部分是怎么排列的:

spm.SentencePieceTrainer.train(input='train.txt',model_prefix='m',vocab_size=16,user_defined_symbols=["<cls>", "<sep>", "<s>", "</s>", "<mask>"],unk_id=1,bos_id=3,eos_id=5,
)

词表:

<cls>	0
<unk>	0
<sep>	0
<s>	0
<mask>	0
</s>	0
▁	-1.4849
i	-2.31823
s	-2.31823
t	-2.31823
.	-3.31823
a	-3.31823
e	-3.31823
h	-3.31823
x	-3.31823
T	-3.31823

5. 大小写相关问题

细心的读者可能已经发现了,明明 __init__.py 文件中定义的方法是按照驼峰命名法来命名的,为什么实际使用的时候可以采用蛇形命名法的方法呢?

如下展示了部分源码:

def EncodeAsPieces(self, input, **kwargs):return self.Encode(input=input, out_type=str, **kwargs)def EncodeAsIds(self, input, **kwargs):return self.Encode(input=input, out_type=int, **kwargs)

这是因为在 __init__.py 文件中的第 1020 行,SentencePiece 通过 _add_snake_case 函数在原有的基础上注入了蛇形命名法的方法:

def _add_snake_case(classname):# 定义一个名为 _add_snake_case 的函数,它接受一个类名 classname 作为参数。# 该函数的作用是将类中的驼峰命名法方法转化为蛇形命名法方法。snake_map = {}# 初始化一个空字典 snake_map,用来存储从驼峰命名法转换为蛇形命名法的键值对。for k, v in classname.__dict__.items():# 遍历类 classname 的 __dict__ 属性,该属性是类的字典,包含类中的所有属性(包括方法)。if re.match(r'^[A-Z]+', k):# 检查属性名 k 是否以大写字母开头,这是驼峰命名法方法的特征。snake = re.sub(r'(?<!^)(?=[A-Z])', '_', k).lower().replace('n_best', 'nbest')# 使用正则表达式将属性名中的大写字母前添加下划线(忽略第一个字母)。# 然后将结果转换为小写,形成蛇形命名法。# replace('n_best', 'nbest') 是特殊处理 n_best 这种命名情况,将其替换为 nbest。snake_map[snake] = v# 将转换后的蛇形命名方法名与对应的原始方法 v 存入 snake_map 字典。for k, v in snake_map.items():# 遍历 snake_map 字典的键值对。setattr(classname, k, v)# 使用 setattr 函数,将新的蛇形命名方法 k 赋值给类 classname,使其指向原始方法 v。_add_snake_case(SentencePieceProcessor)
_add_snake_case(SentencePieceTrainer)

6. 一些误区

SentencePiece并不是一种分词算法,而是一些分词算法的implementation,并在其基础上做了一些优化。还有一些其他的implementations,例如fastBPE,BlingFire等。

from tokenizers import SentencePieceBPETokenizertokenizer = SentencePieceBPETokenizer()
print(tokenizer.pre_tokenizer.pre_tokenize_str("こんにちは世界"))
print(tokenizer.pre_tokenizer.pre_tokenize_str("Hello   world."))

输出:

[('▁こんにちは世界', (0, 7))]
[('▁Hello', (0, 5)), ('▁', (5, 6)), ('▁', (6, 7)), ('▁world.', (7, 14))]

对于不含空格的语种,例如日语,SentencePiece不会进行pre tokenize,而是将其视为1个token。

💻 pre_tokenize_str 的实现在 https://github.com/huggingface/tokenizers/blob/main/bindings/python/src/pre_tokenizers.rs#L173

在HuggingFace源码中,SentencePiece的pre_tokenizer是MetaSpace,它的核心逻辑如下:

def pre_tokenize(text: str) -> list:if not text:return []text = text.replace(' ', '▁')if not text.startswith('▁'):text = '▁' + texttokens = re.findall(r'▁[^▁]*|▁', text)return tokens
  • ▁[^▁]*:表示匹配以 开头,后面跟随零个或多个非 字符的子串,也就是单词前有 标记。
  • |▁:表示单独匹配一个 ,即如果有连续的 ,也会被单独作为一个 token 返回。

我们可以通过构造各种极端测试用例来验证它的正确性:

import re
from tokenizers.pre_tokenizers import Metaspacedef pre_tokenize_1(text: str) -> list:if not text:return []text = text.replace(' ', '▁')if not text.startswith('▁'):text = '▁' + texttokens = re.findall(r'▁[^▁]*|▁', text)return tokensdef pre_tokenize_2(text: str) -> list:global modelres = model.pre_tokenize_str(text)return [token[0] for token in res]model = Metaspace()test_cases = ["hello world","hello     world","   leading spaces","trailing spaces   ","multiple   spaces in   between","▁▁hello▁world","▁hello     world▁▁","▁hello▁world▁",""," ","▁","▁ "," ▁","    ","▁▁▁▁","hello▁","▁▁hello▁▁world▁▁","a","▁▁","hello, world!","▁▁@hello▁#world$","▁▁(hello)▁[world]","1 2 3▁▁4 5","▁▁h3llo▁w0rld123▁","你好 世界","▁▁こんにちは▁世界","Привет▁мир","안녕하세요▁세계","👋🏽▁🌍","▁▁👋🏽▁🌍","Hello▁123▁!▁你好▁世界","▁▁H3llo▁▁123▁▁你好▁世界▁▁","▁▁▁▁▁▁", "     ▁▁    ▁▁", "▁a▁b▁c▁d▁e▁f▁g▁", "▁▁▁▁▁abc", "▁▁▁▁▁▁▁", "你好世界▁▁▁▁▁", "▁▁▁123▁456▁789▁", "This▁▁is▁▁▁▁an▁▁▁▁▁▁▁example", "▁▁▁▁▁▁🌟🌟🌟▁🌟▁", 
]def compare_and_validate_tokenizers(test_cases, verbose=False):for i, text in enumerate(test_cases):tokens_1 = pre_tokenize_1(text)tokens_2 = pre_tokenize_2(text)if verbose:print(f"Test case {i+1}: '{text}'")print(f"pre_tokenize_1: {tokens_1}")print(f"pre_tokenize_2: {tokens_2}")if tokens_1 != tokens_2:if verbose:print("❌ Results differ!\n")print(f"Difference: \n pre_tokenize_1: {tokens_1}\n pre_tokenize_2: {tokens_2}\n")raise AssertionError(f"Test case {i+1} failed: '{text}'\n pre_tokenize_1: {tokens_1}\n pre_tokenize_2: {tokens_2}")elif verbose:print("✅ Both methods produce the same result.\n")print("All test cases passed!")compare_and_validate_tokenizers(test_cases, verbose=True)

Ref

[1] https://www.reddit.com/r/MachineLearning/comments/rprmq3/d_sentencepiece_wordpiece_bpe_which_tokenizer_is/
[2] https://arxiv.org/pdf/1808.06226

http://www.lryc.cn/news/452371.html

相关文章:

  • 一个简单的摄像头应用程序1
  • 通过PHP获取商品详情
  • 【Android】获取备案所需的公钥以及签名MD5值
  • 看480p、720p、1080p、2k、4k、视频一般需要多大带宽呢?
  • 解决IDEA中@Autowired红色报错的实用指南:原因与解决方案
  • 408知识点自检(一)
  • 负载均衡--相关面试题(六)
  • 【Unity踩坑】Unity更新Google Play结算库
  • Redis:hash类型
  • 力扣9.30
  • kafka下载配置
  • nlp任务之预测中间词-huggingface
  • 《程序猿之Redis缓存实战 · Redis 与数据库一致性》
  • 【无标题】observer: error while loading shared libraries: libmariadb.so.3处理办法
  • 极客兔兔Gee-Cache Day1
  • [MAUI]数据绑定和MVVM:MVVM的属性验证
  • 2024年水利水电安全员考试题库及答案
  • 【快速删除 node_modules 】rimraf
  • 毕业设计选题:基于ssm+vue+uniapp的教学辅助小程序
  • 13-指针和动态内存-内存泄漏
  • 基于深度学习的视频摘要生成
  • 适合初学者的[JAVA]: 基础面试题
  • internal.KaptWithoutKotlincTask$KaptExecutionWorkAction 问题 ---Room数据库
  • Frequency-aware Feature Fusion for Dense Image Prediction 论文阅读
  • Springboot + netty + rabbitmq + myBatis
  • 电磁兼容(EMC):整改案例(四)人体对EFT测试影响有多大?
  • 数据可视化基础:让数据说话
  • 有哪些优化数据库性能的方法?如何定位慢查询?数据库性能优化全攻略:从慢查询定位到高效提升
  • C语言 | Leetcode C语言题解之第450题删除二叉搜索树中的节点
  • 智慧防灾,科技先行:EasyCVR平台助力地质灾害视频监测系统建设