ScratchLLMStepByStep:训练自己的Tokenizer

news2025/1/19 7:08:36

1. 引言

分词器是每个大语言模型必不可少的组件,但每个大语言模型的分词器几乎都不相同。如果要训练自己的分词器,可以使用huggingface的tokenizers框架,tokenizers包含以下主要组件:

  1. Tokenizer: 分词器的核心组件,定义了分词的整个流程,包括标准化、预分词、模型分词、后处理等
  2. Normalizers:可选,负责将文本标准化,包括unicode归一化、大写转小写、去重音等操作
  3. Pre-tokenizers:负责将文本分割成更小的片段(如单词等),为模型分词做准备。常见的预分词器有按空格分词(Whitespace)、正则表达式分词(Regex)等
  4. Models:是实际的分词算法,负责将文本片段转换为子词,常见的有BPE、WordPiece、Unigram等。
  5. Post-Processors:负责对分词结果进行后处理,如添加特殊标记(CLS、SEP)。
  6. Decoders:负责将分词结果转换回原始文本,常见的解码器有 ByteLevel、WordPiece 等。
  7. Trainers:用于训练分词模型,不同的模型对应不同的训练器,如 BpeTrainer、WordPieceTrainer、UnigramTrainer 等。

在开始之前,先导入对应的包。

import json
import re
import os
from tokenizers import (
    decoders,
    models,
    normalizers,
    pre_tokenizers,
    processors,
    trainers,
    Tokenizer,
)

2. 加载语料库

我们准备好的语料库是一个jsonl文件,大概有736MB,每一行是一条json格式的文本数据,既有中文,也有英文。

!ls -n /data2/minigpt/dataset/tokenize/tokenizer_train.jsonl
-rw-rw-r-- 1  736729803 Nov  4 22:04 /data2/minigpt/dataset/tokenize/tokenizer_train.jsonl

在这里插入图片描述
在这里插入图片描述

定义一个函数用于从JSONL文件中读取文本数据,考虑到语料库会比较大,所以采用yield生成器来延迟到访问时再加载数据。

def read_texts_from_jsonl(file_path):
    with open(file_path, 'r', encoding='utf-8') as f:
        for i, line in enumerate(f):
            data = json.loads(line)
            yield data['text']
            
data_path = '/data2/minigpt/dataset/tokenize/tokenizer_train.jsonl'
texts = read_texts_from_jsonl(data_path)
type(texts)
generator

可以看到,函数read_texts_from_jsonl返回的并不是真正的数据,而是一个生成器generator。可以通过next函数像访问iterator一样访问迭代数据。

next(texts)
'好的。现在请你将这个文本中的所有的逗号都替换成空格。 好的,请稍等一下,现在我会将文本中的所有逗号替换为空格。处理后文本为:"这是一个句子 目的是看看是否可以正确地从这个句子中删除关键词。"。处理结果如何?'

3. 训练过程

3.1 模型选择

使用BPE模型来初始化Tokenizer实例。

tokenizer = Tokenizer(models.BPE())

BPE是一种基于子词的分词方法,例如:

  • cats -> cat + s
  • helpful -> help + ful
  • congratulation -> con + gr + at + ulation

这种基于子词的分词方法,相比基于完整单词和基于单个字符有以下好处:

  1. 子词相比于单词(可以认为多个子词的组合)数量要可控,这能避免词表过大,并且能避免生僻词带来的未知令牌问题。
  2. 子词相比于字符语义性更强,像单个字符f是没有语义的,但子词ful可以表达满的,比较像英语里的词根词缀。
3.2 预分词器选择

为tokenizer设置预分词器,预分词器有以下几类:

  • Whitespace:按空格分隔,粒度为单词,适用于空格分隔的语言,例如英语。
  • Regex:按自定义正则表达式分隔,适用于需要自定义复杂分词规则的场景。
  • ByteLevel:按字节分割,适用于特殊字符、非英语场景(例如中文)。

由于我们主要面向中文,所以这里采用ByteLevel的pre_tokenizer。

tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
tokenizer.pre_tokenizer
<tokenizers.pre_tokenizers.ByteLevel at 0x7f41641266f0>

预分词器的方法列表

dir(tokenizer.pre_tokenizer)
    [
    ……
     'add_prefix_space',
     'alphabet',
     'custom',
     'pre_tokenize',
     'pre_tokenize_str',
     'use_regex']

那pre_tokenizer具体对文本作了什么处理呢?可以通过下面两个例子来观察下。

  1. 测试英文文本处理。
tokenizer.pre_tokenizer.pre_tokenize_str("Pre-tokenize a :class:`~tokenizers.PyPreTokenizedString` in-place")
    [('Pre', (0, 3)),
     ('-', (3, 4)),
     ('tokenize', (4, 12)),
     ('Ġa', (12, 14)),
     ('Ġ:', (14, 16)),
     ('class', (16, 21)),
     (':`~', (21, 24)),
     ('tokenizers', (24, 34)),
     ('.', (34, 35)),
     ('PyPreTokenizedString', (35, 55)),
     ('`', (55, 56)),
     ('Ġin', (56, 59)),
     ('-', (59, 60)),
     ('place', (60, 65))]

可以看到,pre_tokenizer将文本按照空格和特殊字符作了初步分词,空格处理成了特殊字符Ġ,并记录了每个词的起始和结束位置。

  1. 测试中文文本处理。
zh_sentence = "在查处虚开增值税专用发票案件中,常常涉及进项留抵税额和税款损失的认定和处理。"
tokenizer.pre_tokenizer.pre_tokenize_str(zh_sentence)
    [('åľ¨æŁ¥å¤ĦèĻļå¼Ģå¢ŀåĢ¼ç¨İä¸ĵçĶ¨åıij票æ¡Ī件ä¸Ń', (0, 15)),
     ('ï¼Į', (15, 16)),
     ('常常æ¶īåıĬè¿Ľé¡¹çķĻæĬµç¨İé¢ĿåĴĮç¨İ款æįŁå¤±çļĦ认å®ļåĴĮå¤ĦçIJĨ', (16, 37)),
     ('ãĢĤ', (37, 38))]

中文基本也是按照特殊符号进行了分词,但分词的结果是一堆不认识的字符,这些字符是如何产生的呢?

3.3 预分词原理探究

预分词常常使用类似下面一样的正则表达式先对文本进行分隔。

import regex as re

PRETOKENIZE_REGEX = r"""(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}\p{P}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+"""
pat = re.compile(PRETOKENIZE_REGEX)
tokens = re.findall(pat, zh_sentence)
tokens
    ['在查处虚开增值税专用发票案件中', ',', '常常涉及进项留抵税额和税款损失的认定和处理', '。']

其中,各部分正则表达式的作用如下:

  • (?i:'s|'t|'re|'ve|'m|'ll|'d): 匹配常见的英文缩略形式,例如:'s(is 或 has),'t(not),'re(are),'ve(have),'m(am),'ll(will),'d(would 或 had)。
  • [^\r\n\p{L}\p{N}\p{P}]?\p{L}+:匹配一个或多个 Unicode 字母\p{L},这里的unicode字母包括英文、中文、拉丁等所有语言中的字母,允许前面有一个非换行符\r\n、非字母\p{L}、非数字\p{N}和非标点\p{P}的字符,相当于是匹配空格、制表符等空白字符。
  • \p{N}:匹配任何 Unicode 数字字符。
  • ?[^\s\p{L}\p{N}]+[\r\n]*:匹配非空白、非字母、非数字的字符,允许前面有一个空格,后面跟随换行符,相当于是匹配标点符号后面跟换行符。
  • \p{L}:匹配任何 Unicode 字母字符,包括拉丁字母、希腊字母、汉字等所有语言中的字母。
  • \p{N}:匹配任何 Unicode 数字字符,涵盖阿拉伯数字、罗马数字等所有形式的数字字符。
  • \p{P}:匹配任何 Unicode 标点字符,涵盖句号、逗号、引号、括号等所有形式、所有语言中的标点符号。

对于英文以外的其它语言(例如中文),需要进行utf-8编码,将字符编码为字节。

utf-8编码的目的是解决英文、中文、日文、俄文等多语言的问题,因为世界上所有语言的字符都可以用一个或多个utf-8字节的组合来表示。

tokens_utf8 = [token.encode("utf-8") for token in tokens]
tokens_utf8
[b'\xe5\x9c\xa8\xe6\x9f\xa5\xe5\xa4\x84\xe8\x99\x9a\xe5\xbc\x80\xe5\xa2\x9e\xe5\x80\xbc\xe7\xa8\x8e\xe4\xb8\x93\xe7\x94\xa8\xe5\x8f\x91\xe7\xa5\xa8\xe6\xa1\x88\xe4\xbb\xb6\xe4\xb8\xad',
 b'\xef\xbc\x8c',
 b'\xe5\xb8\xb8\xe5\xb8\xb8\xe6\xb6\x89\xe5\x8f\x8a\xe8\xbf\x9b\xe9\xa1\xb9\xe7\x95\x99\xe6\x8a\xb5\xe7\xa8\x8e\xe9\xa2\x9d\xe5\x92\x8c\xe7\xa8\x8e\xe6\xac\xbe\xe6\x8d\x9f\xe5\xa4\xb1\xe7\x9a\x84\xe8\xae\xa4\xe5\xae\x9a\xe5\x92\x8c\xe5\xa4\x84\xe7\x90\x86',
 b'\xe3\x80\x82']

但有个问题是:Ascii码中是会包含回车、制表、换行等控制字符的,同样utf-8编码中也会有。而我们最终构造的词表必须是可显示的文本,所以还要做一个工作是把控制字符都转换为可显示字符,为此需要制作一个unicode字节编码表,用于将单字节(256以内)都编码为可显示字符。

0-255范围内的可显示字符分为三段:

  • 从 !(ASCII 33)到 ~(ASCII 126)。
  • 从 ¡(Unicode 161)到 ¬(Unicode 172)。
  • 从 ®(Unicode 174)到 ÿ(Unicode 255)。

这三段以外的ASCII码均无法正常显示,需要用可显示字符来填充替代。

def bytes_to_unicode():
    # 收集0-255范围内的可显示字符对应的数字值,ord函数用于将字符编码为数字
    bs = (
        list(range(ord("!"), ord("~") + 1)) + 
        list(range(ord("¡"), ord("¬") + 1)) + 
        list(range(ord("®"), ord("ÿ") + 1))
    )
    cs = bs[:]
    n = 0
    # 补充0-255范围内不可显示字符对应的数字,并转换为256以上可显示字符对应的数字值
    for b in range(2**8):
        if b not in bs:
            bs.append(b)
            cs.append(2**8 + n)
            n += 1
    # chr函数用于将数字转换回unicode字符,并创建一个字节值到字符值的映射表。
    cs = [chr(n) for n in cs]
    return dict(zip(bs, cs))

byte_encoder = bytes_to_unicode()
json.dumps(byte_encoder, ensure_ascii=False)
    '{"33": "!", "34": "\\"", "35": "#", "36": "$", "37": "%", "38": "&", "39": "\'", "40": "(", "41": ")", "42": "*", "43": "+", "44": ",", "45": "-", "46": ".", "47": "/", "48": "0", "49": "1", "50": "2", "51": "3", "52": "4", "53": "5", "54": "6", "55": "7", "56": "8", "57": "9", "58": ":", "59": ";", "60": "<", "61": "=", "62": ">", "63": "?", "64": "@", "65": "A", "66": "B", "67": "C", "68": "D", "69": "E", "70": "F", "71": "G", "72": "H", "73": "I", "74": "J", "75": "K", "76": "L", "77": "M", "78": "N", "79": "O", "80": "P", "81": "Q", "82": "R", "83": "S", "84": "T", "85": "U", "86": "V", "87": "W", "88": "X", "89": "Y", "90": "Z", "91": "[", "92": "\\\\", "93": "]", "94": "^", "95": "_", "96": "`", "97": "a", "98": "b", "99": "c", "100": "d", "101": "e", "102": "f", "103": "g", "104": "h", "105": "i", "106": "j", "107": "k", "108": "l", "109": "m", "110": "n", "111": "o", "112": "p", "113": "q", "114": "r", "115": "s", "116": "t", "117": "u", "118": "v", "119": "w", "120": "x", "121": "y", "122": "z", "123": "{", "124": "|", "125": "}", "126": "~", "161": "¡", "162": "¢", "163": "£", "164": "¤", "165": "¥", "166": "¦", "167": "§", "168": "¨", "169": "©", "170": "ª", "171": "«", "172": "¬", "174": "®", "175": "¯", "176": "°", "177": "±", "178": "²", "179": "³", "180": "´", "181": "µ", "182": "¶", "183": "·", "184": "¸", "185": "¹", "186": "º", "187": "»", "188": "¼", "189": "½", "190": "¾", "191": "¿", "192": "À", "193": "Á", "194": "Â", "195": "Ã", "196": "Ä", "197": "Å", "198": "Æ", "199": "Ç", "200": "È", "201": "É", "202": "Ê", "203": "Ë", "204": "Ì", "205": "Í", "206": "Î", "207": "Ï", "208": "Ð", "209": "Ñ", "210": "Ò", "211": "Ó", "212": "Ô", "213": "Õ", "214": "Ö", "215": "×", "216": "Ø", "217": "Ù", "218": "Ú", "219": "Û", "220": "Ü", "221": "Ý", "222": "Þ", "223": "ß", "224": "à", "225": "á", "226": "â", "227": "ã", "228": "ä", "229": "å", "230": "æ", "231": "ç", "232": "è", "233": "é", "234": "ê", "235": "ë", "236": "ì", "237": "í", "238": "î", "239": "ï", "240": "ð", "241": "ñ", "242": "ò", "243": "ó", "244": "ô", "245": "õ", "246": "ö", "247": "÷", "248": "ø", "249": "ù", "250": "ú", "251": "û", "252": "ü", "253": "ý", "254": "þ", "255": "ÿ", "0": "Ā", "1": "ā", "2": "Ă", "3": "ă", "4": "Ą", "5": "ą", "6": "Ć", "7": "ć", "8": "Ĉ", "9": "ĉ", "10": "Ċ", "11": "ċ", "12": "Č", "13": "č", "14": "Ď", "15": "ď", "16": "Đ", "17": "đ", "18": "Ē", "19": "ē", "20": "Ĕ", "21": "ĕ", "22": "Ė", "23": "ė", "24": "Ę", "25": "ę", "26": "Ě", "27": "ě", "28": "Ĝ", "29": "ĝ", "30": "Ğ", "31": "ğ", "32": "Ġ", "127": "ġ", "128": "Ģ", "129": "ģ", "130": "Ĥ", "131": "ĥ", "132": "Ħ", "133": "ħ", "134": "Ĩ", "135": "ĩ", "136": "Ī", "137": "ī", "138": "Ĭ", "139": "ĭ", "140": "Į", "141": "į", "142": "İ", "143": "ı", "144": "IJ", "145": "ij", "146": "Ĵ", "147": "ĵ", "148": "Ķ", "149": "ķ", "150": "ĸ", "151": "Ĺ", "152": "ĺ", "153": "Ļ", "154": "ļ", "155": "Ľ", "156": "ľ", "157": "Ŀ", "158": "ŀ", "159": "Ł", "160": "ł", "173": "Ń"}'

这样,每个字节值都从 Unicode 表的开头获得一个分配给它的可见字符。这一点非常重要,因为每个utf-8字符都是由一到多个字节组成的,将这个长度为256的编码表中的字节进行组合,理论上就能对世界上所有语言中的字符进行编码,并且还不会出现未知标记。

使用这个unicode字节编码表将前面utf-8编码后的文本序列进行ByteLevel级的编码。

tokens_unicode = ["".join(byte_encoder[b] for b in token) for token in tokens_utf8]
tokens_unicode
['åľ¨æŁ¥å¤ĦèĻļå¼Ģå¢ŀåĢ¼ç¨İä¸ĵçĶ¨åıij票æ¡Ī件ä¸Ń',
 'ï¼Į',
 '常常æ¶īåıĬè¿Ľé¡¹çķĻæĬµç¨İé¢ĿåĴĮç¨İ款æįŁå¤±çļĦ认å®ļåĴĮå¤ĦçIJĨ',
 'ãĢĤ']

可以看到,结果与使用pre_tokenizer预分词的结果完全相同。

3.4 构建训练器

BPE训练器中需要指定几个参数:

  • vocab_size:训练后词表中的词条数量,BPE是一个从短词到长词的组合过程,达到词表大小后就会停止训练。
  • special_tokens:特殊token,和语言模型的特殊token相同,例如开始、结束、填充标记。
  • initial_alphabet:初始字符表,使用上面长度为256的unicode字节编码表作为初始字符表。

通过pre_tokenizers.ByteLevel.alphabet()可以获得初始字符编码表。

json.dumps(pre_tokenizers.ByteLevel.alphabet(), ensure_ascii=False)
'["ø", "\\\\", "ľ", "v", "ć", "¬", "ł", "°", "ġ", "ķ", "ĕ", "»", "]", "Q", "ģ", "G", "ñ", "¶", "é", "H", "9", ")", "×", "Í", "Ó", "º", "£", "~", "Ā", "s", "Ô", "2", "Ý", "í", "â", "·", "Ą", "ý", "ĭ", "²", "4", "Ù", "ĺ", "ă", "ĸ", "Ī", "z", "K", "IJ", "N", "Ø", "1", "n", "b", "ó", "¼", "õ", "V", "Ö", "6", "©", "ė", "O", "đ", "j", "h", "İ", "į", "¨", "¯", "Ğ", "I", "0", "Ă", "=", "ß", "Û", "Ć", "Å", "ĩ", "Ê", "B", "ª", "W", "_", "S", "Ĥ", "Ł", "q", "ë", "Ķ", "Ò", "Ĉ", "Ċ", "L", "«", "U", "#", "ļ", "Ė", "å", "´", "Î", "M", "&", "D", "¤", "ô", "ç", "Y", "R", "ð", "Ĺ", "ij", ">", "Ŀ", "ę", "d", "É", "à", "ŀ", "<", "Ĩ", "¹", "Ã", "/", "¸", "Ģ", "X", "ê", "u", "ğ", "m", "w", "Ì", "¢", "Æ", "C", "t", "Ļ", "ì", "ě", "Ď", "l", "ē", "Ħ", "Ñ", "3", "÷", "{", "$", "y", "Ç", "¡", "Ë", "ĉ", "ĵ", "Ĵ", "Č", "a", "T", ";", "Ń", "Ú", "f", "§", "Z", "+", "\'", "Ä", "A", "ÿ", "Ę", "Õ", "Ġ", "c", "%", "Ē", "ï", "ī", "Ĕ", "?", "(", "Đ", "Ï", "ö", "^", "P", "±", "x", ",", "i", "æ", "®", "-", "³", "î", "*", "û", "ú", "¾", "ä", "Į", "5", "ò", "Ĭ", "Â", "þ", "J", "ċ", ":", "ü", "¥", "`", "è", "È", "ĝ", "ą", "}", "!", "r", "Þ", "g", "\\"", "[", "À", "ā", "ď", "7", "¿", "ħ", "F", "ı", "Á", "á", "Ð", ".", "@", "E", "č", "½", "e", "ã", "ù", "Ľ", "Ĝ", "ĥ", "¦", "Ě", "p", "Ü", "8", "|", "k", "o", "µ"]'

定义特殊token,分别为填充、开始、结束标记。

special_tokens = ["<|endoftext|>", "<|im_start|>", "<|im_end|>"]

构建训练器,词条数量设置为32000。

trainer = trainers.BpeTrainer(
    vocab_size=32000,
    special_tokens=special_tokens,  # 确保这三个token被包含
    show_progress=True,
    initial_alphabet=pre_tokenizers.ByteLevel.alphabet()
)

使用上面的texts生成器作为语料库,使用trainer开始训练分词器。

tokenizer.train_from_iterator(texts, trainer=trainer)

注:这个训练过程的用时长短与文本数据大小有关,我们前面加载的文本数据有700多MB, 大概需要十几分钟。

3.5 保存训练结果

在保存结果之前,需要先设置相匹配的解码器,否则ASCII以外的字符可能无法正常解码。

上面编码阶段使用了ByteLevel的预分词器,相对应的解码阶段也需要使用ByteLevel,表示将token id转换为token后,还需要进行一次unicode字节级别的解码,才能正常显示中文等多语言字符。

tokenizer.decoder = decoders.ByteLevel()

将训练的分词器保存到指定目录。

tokenizer_dir = "/data2/minigpt/models/tokenizer_v3"
os.makedirs(tokenizer_dir, exist_ok=True)
tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
tokenizer.model.save(tokenizer_dir)
['/data2/minigpt/models/tokenizer_v3/vocab.json',
 '/data2/minigpt/models/tokenizer_v3/merges.txt']

还需要一个分词器配置文件,包括模型类型、是否使用小写字母等。

config = {
    "add_bos_token": False,
    "add_eos_token": False,
    "add_prefix_space": True,
    "added_tokens_decoder": {
        "0": {
            "content": "<|endoftext|>",
            "lstrip": False,
            "normalized": False,
            "rstrip": False,
            "single_word": False,
            "special": True
        },
        "1": {
            "content": "<|im_start|>",
            "lstrip": False,
            "normalized": False,
            "rstrip": False,
            "single_word": False,
            "special": True
        },
        "2": {
            "content": "<|im_end|>",
            "lstrip": False,
            "normalized": False,
            "rstrip": False,
            "single_word": False,
            "special": True
        }
    },
    "additional_special_tokens": [],
    "bos_token": "<|im_start|>",
    "clean_up_tokenization_spaces": False,
    "eos_token": "<|im_end|>",
    "legacy": True,
    "model_max_length": 1000000000000000019884624838656,
    "pad_token": None,
    "sp_model_kwargs": {},
    "spaces_between_special_tokens": False,
    "tokenizer_class": "PreTrainedTokenizerFast",
    "unk_token": "<|endoftext|>",
    "use_default_system_prompt": False,
    "chat_template": "{% if messages[0]['role'] == 'system' %}{% set system_message = messages[0]['content'] %}{% endif %}{% if system_message is defined %}{{ system_message }}{% endif %}{% for message in messages %}{% set content = message['content'] %}{% if message['role'] == 'user' %}{{ '<|im_start|>user\\n' + content + '<|im_end|>\\n<|im_start|>assistant\\n' }}{% elif message['role'] == 'assistant' %}{{ content + '<|im_end|>' + '\\n' }}{% endif %}{% endfor %}"
}

保存分词器配置

with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w", encoding="utf-8") as config_file:
    json.dump(config, config_file, ensure_ascii=False, indent=4)

print("Tokenizer training completed and saved.")
Tokenizer training completed and saved.

查看磁盘上的词表文件。

!ls -l /data2/minigpt/models/tokenizer_v3
total 2548
-rw-rw-r-- 1 golfxiao golfxiao  407951 Oct 10 21:45 merges.txt
-rw-rw-r-- 1 golfxiao golfxiao    1686 Oct 10 21:45 tokenizer_config.json
-rw-rw-r-- 1 golfxiao golfxiao 1572840 Oct 10 21:45 tokenizer.json
-rw-rw-r-- 1 golfxiao golfxiao  621912 Oct 10 21:45 vocab.json
  • vocab.json:词汇表文件,包含词条和对应的索引。
  • merges.txt: 合并表文件,定义了子词的合并规则。
  • tokenizer.json: 完整的分词器文件,它包含了分词器的所有信息,包括词汇表、合并规则、特殊标记等。
  • tokenizer_config.json: 分词器配置文件,包括了起始token、结束token的定义,以及提示词模板。

4. 测试分词器

from transformers import AutoTokenizer

# 加载刚训练的tokenizer
tokenizer_dir = "/data2/minigpt/models/tokenizer_v3"
tokenizer_trained = AutoTokenizer.from_pretrained(tokenizer_dir)
4.1 英文文本测试

先测试英文文本的分词。

text_en = "Pre-tokenize a :class:`~tokenizers.PyPreTokenizedString` in-place"
tokenized = tokenizer_trained.tokenize(text_en)
tokenized
    ['Pre',
     '-',
     'token',
     'ize',
     'Ġa',
     'Ġ:',
     'class',
     ':',
     '`',
     '~',
     'token',
     'izers',
     '.',
     'Py',
     'Pre',
     'T',
     'oken',
     'ized',
     'String',
     '`',
     'Ġin',
     '-',
     'place']

tokenize方法只负责分词,就是将一串文本切分为token列表。我们在给模型输入时一般需要的是token_id,这时就需要使用encode方法,同时完成token切分和文本到数字的序列化。

token_ids_en = tokenizer_trained.encode(text_en)
token_ids_en
    [19714,
     15,
     24535,
     1038,
     260,
     6938,
     9939,
     28,
     66,
     96,
     24535,
     11344,
     16,
     22966,
     19714,
     54,
     9071,
     1228,
     13863,
     66,
     295,
     15,
     2383]

对上面的token_id进行反序列化,以测试解码功能。

tokenizer_trained.decode(token_ids_en)
'Pre-tokenize a :class:`~tokenizers.PyPreTokenizedString` in-place'

可以看到,解码的结果与原始英文串完全相同。

4.2 中文文本测试

下面测试下中文文本的序列化和反序列化。

text_zh = "在查处虚开增值税专用发票案件中,常常涉及进项留抵税额和税款损失的认定和处理。"
token_ids_zh = tokenizer_trained.encode(text_zh)
token_ids_zh
    [368,
     1698,
     1319,
     4304,
     953,
     30571,
     2147,
     411,
     646,
     3917,
     6723,
     413,
     270,
     6679,
     4743,
     631,
     1467,
     3692,
     9083,
     3534,
     2676,
     315,
     3534,
     1805,
     8576,
     269,
     1374,
     627,
     12769,
     286]
tokenizer_trained.decode(token_ids_zh)
    '在查处虚开增值税专用发票案件中,常常涉及进项留抵税额和税款损失的认定和处理。'

我们刚训练的分词器在中文和英文上都能正常进行的文本的序列化和反序列化操作。

小结:本文借助huggingface提供的tokenizers框架,以一个真实的语料库为案例,演示了分词器训练的过程,并最终得到了一个切实可用的分词器。但tokenizers框架封装的比较多,所以在训练过程中对多语言的编码和解码部分作了内部实现的剖析和讲解,如果你还对其它部分(如BPE算法)感兴趣,下面的参考内容或许能为你提供进一步的帮助。

参考阅读

  • 带你从零认识语言模型
  • 手搓BPE算法
  • 什么是tokenizer?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2278823.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习项目--基于LSTM的火灾预测研究(pytorch实现)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 LSTM模型一直是一个很经典的模型&#xff0c;这个模型当然也很复杂&#xff0c;一般需要先学习RNN、GRU模型之后再学&#xff0c;GRU、LSTM的模型讲解将…

社区版Dify实现文生视频 LLM+ComfyUI+混元视频

社区版Dify实现文生视频 LLMComfyUI混元视频 一、 社区版Dify实现私有化混元视频效果二、为什么社区版Dify可以在对话框实现文生视频&#xff1f;LLMComfyUI混元视频 实现流程图&#xff08;重点&#xff09;1. 文生视频模型支持ComfyUI2. ComfyUI可以轻松导出API实现封装3. Di…

SpringBoot的Bean-中级-作用域

5个作用域&#xff1a; 初级演示的是第一种默认的singleton&#xff1a;SpringBoot的Bean-初级获取bean对象-CSDN博客 中级-1&#xff1a;Lazy注解使其在使用的时候再实例化 中级-2&#xff1a;Scope("prototype")使其每次需要注入的时候都实例化新的对象 测试程序&…

放大芯片参数阅读

一、芯片的增益能力 1. GBW&#xff08;增益带宽积&#xff09; 例如&#xff0c;GBW (typ) 1 MHz。 增益带宽积&#xff08;Gain Bandwidth Product&#xff09;是一个关键参数&#xff0c;用于计算在特定频率下的最大增益。 定义公式为&#xff1a; 增益带宽G…

蓝桥杯算法日常|枚举[*找到最多的数]

**找到最多的数** 重点疑问总结&#xff1a; 1、数组输入输出c一般会采用那种方便的方式&#xff1f;&#xff1f; 用的就是我想的那种&#xff0c;就是用的最大范围定义的。 2、怎样方便给数组中每个数出现的次数计数&#xff1f;&#xff1f; 刚开始想的是&#xff1a;每个数…

Docker安装PostGreSQL docker安装PostGreSQL 完整详细教程

Docker安装PostGreSQL docker安装PostGreSQL 完整详细教程 Docker常用命令大全Docker 运行命令生成Docker 上安装 PostGreSQL 14.15 的步骤&#xff1a;1、拉取 PostGreSQL 14.15 镜像2、创建并运行容器3、测试连接4、设置所有IP都可以运行连接进入容器内 修改配置文件关闭容器…

基于机器学习随机森林算法的个人职业预测研究

1.背景调研 随着信息技术的飞速发展&#xff0c;特别是大数据和云计算技术的广泛应用&#xff0c;各行各业都积累了大量的数据。这些数据中蕴含着丰富的信息和模式&#xff0c;为利用机器学习进行职业预测提供了可能。机器学习算法的不断进步&#xff0c;如深度学习、强化学习等…

Go 语言 select 的实现原理

介绍 select是Go在语言层面提供的I/O多路复用的机制&#xff0c;其专门用来让Goroutine同时等待多个channel是否准备完毕:可读或可写。在Channel状态改变之前&#xff0c;select会一直阻塞当前线程或者goroutine。 特性&#xff1a; case 必须是一个通信操作&#xff0c;主要是…

Java 视频处理:基于 MD5 校验秒传及 ffmpeg 切片合并的实现

本文介绍两种网络技术实现方法。一是 MD5 校验秒传&#xff0c;服务器端用数据库记上传文件 MD5 值及存储路径&#xff0c;Java 代码接收客户端 MD5 值并查询校验&#xff0c;返回状态码。二是用 ffmpeg 切片视频成 m3u8 上传&#xff0c;异步合并文件实现视频按需加载。 1. …

一文读懂iOS中的Crash捕获、分析以及防治

Crash系统性总结 Crash捕获与分析Crash收集符号化分析 Crash类别以及解法分析子线程访问UI而导致的崩溃unrecognized selector send to instance xxxKVO crashKVC造成的crashNSTimer导致的Crash野指针Watch Dog超时造成的crash其他crash待补充 参考文章&#xff1a; 对于iOS端开…

RK3576 Android14 状态栏和导航栏增加显示控制功能

问题背景&#xff1a; 因为RK3576 Android14用户需要手动控制状态栏和导航栏显示隐藏控制&#xff0c;包括对锁屏后下拉状态栏的屏蔽&#xff0c;在设置功能里增加此功能的控制&#xff0c;故参考一些博客完成此功能&#xff0c;以下是具体代码路径的修改内容。 解决方案&…

【Rust自学】13.5. 迭代器 Pt.1:迭代器的定义、iterator trait和next方法

13.5.0. 写在正文之前 Rust语言在设计过程中收到了很多语言的启发&#xff0c;而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。 在本章中&#xff0c;我们会讨论 Rust 的一…

LabVIEW 蔬菜精密播种监测系统

在当前蔬菜播种工作中&#xff0c;存在着诸多问题。一方面&#xff0c;播种精度难以达到现代农业的高标准要求&#xff0c;导致种子分布不均&#xff0c;影响作物的生长发育和最终产量&#xff1b;另一方面&#xff0c;对于小粒径种子&#xff0c;传统的监测手段难以实现有效监…

2024年年终总结——坎坷与坚持,焦虑与收获

不知不觉间&#xff0c;2024年已经悄然过去&#xff0c;回望这一年的时间&#xff0c;一时间竟感觉混混沌沌无法形容&#xff0c;选择一些时间坐下来让自己简单回忆一下自己的2024。 先简单回望一下24年一整年的工作情况&#xff1a; 24年一开始&#xff0c;工作最期待的的节点…

无人机技术架构剖析!

一、飞机平台系统 飞机平台系统是无人机飞行的主体平台&#xff0c;主要提供飞行能力和装载功能。它由机体结构、动力装置、电气设备等组成。 机体结构&#xff1a;无人机的机身是其核心结构&#xff0c;承载着其他各个组件并提供稳定性。常见的机身材料包括碳纤维、铝合金、…

springboot基于微信小程序的传统美食文化宣传平台小程序

Spring Boot 基于微信小程序的传统美食文化宣传平台 一、平台概述 Spring Boot 基于微信小程序的传统美食文化宣传平台是一个集传统美食展示、文化传承、美食制作教程分享、用户互动交流以及美食相关活动推广为一体的综合性线上平台。它借助 Spring Boot 强大的后端开发框架构…

Android系统开发(八):从麦克风到扬声器,音频HAL框架的奇妙之旅

引言&#xff1a;音浪太强&#xff0c;我稳如老 HAL&#xff01; 如果有一天你的耳机里传来的不是《咱们屯里人》&#xff0c;而是金属碰撞般的杂音&#xff0c;那你可能已经感受到了 Android 音频硬件抽象层 (HAL) 出问题的后果&#xff01;在 Android 音频架构中&#xff0c…

51.WPF应用加图标指南 C#例子 WPF例子

完整步骤&#xff1a; 先使用文心一言生成一个图标如左边使用Windows图片编辑器编辑&#xff0c;去除背景使用正方形&#xff0c;放大图片使图标铺满图片使用格式工程转换为ico格式&#xff0c;分辨率为最大 在资源管理器中右键项目添加ico类型图片到项目里图片属性设置为始终…

运行fastGPT 第四步 配置ONE API 添加模型

上次已经装好了所有的依赖和程序。 下面在网页中配置One API &#xff0c;这个是大模型的接口。配置好了之后&#xff0c;就可以配置fastGPT了。 打开 OneAPI 页面 添加模型 这里要添加具体的付费模型的API接口填进来。 可以通过ip:3001访问OneAPI后台&#xff0c;**默认账号…

道旅科技借助云消息队列 Kafka 版加速旅游大数据创新发展

作者&#xff1a;寒空、横槊、娜米、公仪 道旅科技&#xff1a;科技驱动&#xff0c;引领全球旅游分销服务 道旅科技 &#xff08;https://www.didatravel.com/home&#xff09; 成立于 2012 年&#xff0c;总部位于中国深圳&#xff0c;是一家以科技驱动的全球酒店资源批发商…