LLM大模型之基于SentencePiece扩充LLaMa中文词表实践

LLM大模型之基于SentencePiece扩充LLaMa中文词表实践

目前大模型的词表和分词器都是基于SentencePiece工具实现的,比如LLaMa,BLOOM,ChatGLM,Baichuan等,简单来说SentencePiece就是工程化的实现了之前写的各种的分词算法,而且实现的十分优雅,简单,快速,轻量。本文主要参考官方的Git[1],进行对这个工具的使用,以及自己在扩充LLaMa中文词表实验过程中的一些问题做记录。全文阅读和实现可能需要30分钟,建议收藏~如果觉得对你有帮助,那就点个赞吧 。

一些想法和总结写在前面,使用SentencePiece的除了从0开始训练大模型的土豪和大公司外,大部分应该都是使用其为当前开源的大模型扩充词表,比如为LLaMa扩充通用中文词表(垂直领域词表),为开源的中文大模型ChatGLM,Baichuan扩充垂直领域词表。那这部分工作有没有意义呢?或者说值不值得投入资源去做呢?先说自己的结论,有,以下两点的作用,第三点不确定:

1.提高模型的编解码的效率,在LLaMa原来的词表上,一个汉字平均1.45个token,扩充后的Chinese-LLaMa为0.65个token[2];那在垂直领域内呢?比如在LLaMa在继续扩充领域内词表,金融或者医疗等等,把“负债表”,“糖尿病”等领域词汇也加入词表里,那更加能提高其编解码的效率。

2.提高模型的上下文窗口长度,原LLaMa上下文长度是4096个token,不扩充词表前,按1.45来算就是最多只能输入2824个汉字,扩充后以0.65来算的话就是6301,垂直领域会更大。这点带来的好处是实打实的。

3.提高模型的效果?提高LLaMa在中文的表现?提高开源模型在垂直领域的表现?这一点上难以下结论,目前好像也没有确定的结论,自我感觉会有,但是不多,而且可能在垂直领域扩充词表后,垂直领域词太多过拟合影响通用领域效果,还有就是扩充完词表后还要经过一系列的后续处理和训练,可以控制变量的研究一下,但需要很多的资源哈哈。但是前两点的好处是实打实的,所以在有资源的情况下,扩充词表还是可以尝试的。

安装

SentencePiece的安装方式有两种,实现的效果是一样的。

1.源码安装

在linux Ubuntu 上,命令行执行:

sudo apt-get update
sudo apt-get install cmake build-essential pkg-config libgoogle-perftools-dev

然后下载源码进行安装:

git clone https://github.com/google/sentencepiece.git 
cd sentencepiece
mkdir build
cd build
cmake ..
make -j $(nproc)
sudo make install
# linux
sudo ldconfig -v
# OSX/macOS
sudo update_dyld_shared_cache

亲测安装没有问题,验证安装是否成功:

spm_train --help
2.python安装

通过pip安装,应该是最简单和快速的方式,实现的功能和上诉源码安装是一模一样的:

pip install sentencepiece

使用

安装完成后,我们开始使用,第一步是训练词表,用起来很简单,源码安装的方式直接命令行执行

spm_train --input=<input> --model_prefix=<model_name> --vocab_size=8000 --character_coverage=1.0 --model_type=<type>

python的方式则是:

import sentencepiece as spm
spm.SentencePieceTrainer.train(input=<input>, model_prefix=<model_name>, vocab_size=8000, character_coverage=1.0, model_type=<type>)

不过上述方式都建议写一个sh脚本,通过nohup或者screen放在后台执行,使用起来是很简单,但是这个训练的参数有接近40个,本着使用一个工具就尽量研究明白的态度,把主要的参数都进行了解释和一些不确定的参数用途的进行了实验。

训练参数详解

以下是官方给出的训练参数解释,后面是笔者通过实验的一些理解。

   --input (comma separated list of input sentences)  type: std::string default: ""
   --input_format (Input format. Supported format is `text` or `tsv`.)  type: std::string default: ""
   --model_prefix (output model prefix)  type: std::string default: ""
   --model_type (model algorithm: unigram, bpe, word or char)  type: std::string default: "unigram"
   --vocab_size (vocabulary size)  type: int32 default: 8000
   --accept_language (comma-separated list of languages this model can accept)  type: std::string default: ""
   --self_test_sample_size (the size of self test samples)  type: int32 default: 0
   --character_coverage (character coverage to determine the minimum symbols)  type: double default: 0.9995
   --input_sentence_size (maximum size of sentences the trainer loads)  type: std::uint64_t default: 0
   --shuffle_input_sentence (Randomly sample input sentences in advance. Valid when --input_sentence_size > 0)  type: bool default: true
   --seed_sentencepiece_size (the size of seed sentencepieces)  type: int32 default: 1000000
   --shrinking_factor (Keeps top shrinking_factor pieces with respect to the loss)  type: double default: 0.75
   --num_threads (number of threads for training)  type: int32 default: 16
   --num_sub_iterations (number of EM sub-iterations)  type: int32 default: 2
   --max_sentencepiece_length (maximum length of sentence piece)  type: int32 default: 16
   --max_sentence_length (maximum length of sentence in byte)  type: int32 default: 4192
   --split_by_unicode_script (use Unicode script to split sentence pieces)  type: bool default: true
   --split_by_number (split tokens by numbers (0-9))  type: bool default: true
   --split_by_whitespace (use a white space to split sentence pieces)  type: bool default: true
   --split_digits (split all digits (0-9) into separate pieces)  type: bool default: false
   --treat_whitespace_as_suffix (treat whitespace marker as suffix instead of prefix.)  type: bool default: false
   --allow_whitespace_only_pieces (allow pieces that only contain (consecutive) whitespace tokens)  type: bool default: false
   --control_symbols (comma separated list of control symbols)  type: std::string default: ""
   --control_symbols_file (load control_symbols from file.)  type: std::string default: ""
   --user_defined_symbols (comma separated list of user defined symbols)  type: std::string default: ""
   --user_defined_symbols_file (load user_defined_symbols from file.)  type: std::string default: ""
   --required_chars (UTF8 characters in this flag are always used in the character set regardless of --character_coverage)  type: std::string default: ""
   --required_chars_file (load required_chars from file.)  type: std::string default: ""
   --byte_fallback (decompose unknown pieces into UTF-8 byte pieces)  type: bool default: false
   --vocabulary_output_piece_score (Define score in vocab file)  type: bool default: true
   --normalization_rule_name (Normalization rule name. Choose from nfkc or identity)  type: std::string default: "nmt_nfkc"
   --normalization_rule_tsv (Normalization rule TSV file. )  type: std::string default: ""
   --denormalization_rule_tsv (Denormalization rule TSV file.)  type: std::string default: ""
   --add_dummy_prefix (Add dummy whitespace at the beginning of text)  type: bool default: true
   --remove_extra_whitespaces (Removes leading, trailing, and duplicate internal whitespace)  type: bool default: true
   --hard_vocab_limit (If set to false, --vocab_size is considered as a soft limit.)  type: bool default: true
   --use_all_vocab (If set to true, use all tokens as vocab. Valid for word/char models.)  type: bool default: false
   --unk_id (Override UNK (<unk>) id.)  type: int32 default: 0
   --bos_id (Override BOS (<s>) id. Set -1 to disable BOS.)  type: int32 default: 1
   --eos_id (Override EOS (</s>) id. Set -1 to disable EOS.)  type: int32 default: 2
   --pad_id (Override PAD (<pad>) id. Set -1 to disable PAD.)  type: int32 default: -1
   --unk_piece (Override UNK (<unk>) piece.)  type: std::string default: ""
   --bos_piece (Override BOS (<s>) piece.)  type: std::string default: ""
   --eos_piece (Override EOS (</s>) piece.)  type: std::string default: ""
   --pad_piece (Override PAD (<pad>) piece.)  type: std::string default: ""
   --unk_surface (Dummy surface string for <unk>. In decoding <unk> is decoded to `unk_surface`.)  type: std::string default: " ⁇ "
   --train_extremely_large_corpus (Increase bit depth for unigram tokenization.)  type: bool default: false
   --random_seed (Seed value for random generator.)  type: uint32 default: 4294967295
   --enable_differential_privacy (Whether to add DP while training. Currently supported only by UNIGRAM model.)  type: bool default: false
   --differential_privacy_noise_level (Amount of noise to add for DP)  type: float default: 0
   --differential_privacy_clipping_threshold (Threshold for clipping the counts for DP)  type: std::uint64_t default: 0
   --help (show help)  type: bool default: false
   --version (show version)  type: bool default: false
   --minloglevel (Messages logged at a lower level than this don't actually get logged anywhere)  type: int default: 0

1.input

指定训练语料文件,支持两种格式.txt和.tsv(以制表符(Tab)作为分隔符的文件,类似于.csv文件),也可以传递以逗号分隔的文件列表。.txt文件内格式为每一行作为一个句子(sentences)。默认为""

--input "/path/botchan.txt"
--input ["/path/botchan1.txt", "path/botchan2.txt"]

一般大规模训练时,我们会有几十个文件,在一个文件夹下,这时候我们可以通过sh脚本:

files="/path/train_vocab/*" # 你的训练文件夹地址
file_list=$(echo $files | tr ' ' ',')
nohup spm_train --input $file_list
#...其他参数

2.input_format

指定输入文件的格式,支持的格式有两种:text对应.txt;tsv对应.tsv。默认为""

3.model_prefix

指定模型的输出前缀名,模型训练完成后,将使用这个前缀名来保存模型和词表文件。默认为""

4.model_type

指定模型的分词算法,支持的选项有 unigram、bpe、word和char。之前的文章已经介绍过这些分词算法,强烈建议看一下!默认为"unigram"

5.vocab_size

指定词表大小,默认为8000

6.accept_language

指定模型所支持的语言列表,多个语言可以用逗号分隔,语言代码是 ISO 639 标准定义的缩写,这个参数就是帮助模型识别语言,不设置也是可以的,默认为""

--accept_language "en,zh"

7.character_coverage

指定模型的字符覆盖率,较高的覆盖率可以使模型包含更多字符。对于字符集丰富的语言(如日语或中文)推荐的默认值为 0.9995,对于其他字符集较小的语言推荐默认值为 1.0。默认值为0.9995,如果词表比较大,或者说扩充的词表比较大,可以适当调大该参数。

8.input_sentence_size

指定训练过程中加载的训练句子的最大数量。如果设置为非0值,模型将只加载小于设定的训练句子数量,默认为0,不设置数量。

9.shuffle_input_sentence

当 --input_sentence_size 设置大于 0 时,此参数控制是否在加载输入句子之前对其进行随机采样,因为一般设置input_sentence_size时,是因为输入的句子太多了,比如我们输入的文件有1000个训练句子,但是我们只想要100个句子参与训练,这个时候设置这个参数就会随机采样100句。默认为True(但是input_sentence_size 设置大于 0 时才生效)。

10.seed_sentencepiece_size

指定用于种子子词单元的最大数量,默认为1000000

11.num_threads

指定在训练过程中使用的线程数,默认为16。这个要在解释一下,这个线程只有在 EM-step阶段使用即这个参数num_sub_iterations,其他阶段都是单线程。原作者的回复:“Muti-thread computation is used only in the EM-step, after the seed vocab generation phase with suffix array.”所以大部分时间你只能看到只有一个CPU达到了100%,其他CPU都没有利用,作者说会在将来实现。。

12.max_sentencepiece_length

指定子词单元的最大长度,默认为16。

13.max_sentence_length

指定输入句子的最大长度,是以字节为单位的,默认为4192,UTF-8中一个汉字3个字节,大概就是.txt一行最多1397个汉字。

14.split_by_unicode_script

指定是否用unicode脚本信息来划分子词单元,默认为True,解释一下Unicode 脚本,是 Unicode 标准中定义的一组字符集合,每个字符都被分配到一个或多个脚本(例如拉丁字母、希腊字母、汉字等)。当此参数启用时,模型在分割句子片段时会考虑每个字符所属的 Unicode 脚本,以便更好地处理不同脚本之间的边界,在多语言环境中非常有用。

15.split_by_number

指定是否用数字来划分子词单元,默认为False,就是划分子词的时候要不要用数字来划分。

16.split_by_whitespace

指定是否用空格来划分子词单元,默认为True

17.split_digits

指定是否将所有数字字符拆分为单独的单元,就是将”2023“拆成”2“,”0“,”2“,”3“这种独立的子词单元,好处是减少词表的数字量,所有数字都能表示,坏处是token数量会变多,一个”2023“就是4个token,默认是False,LLaMa是True

18.treat_whitespace_as_suffix

指定是否将空格字符作为子词的后缀而不是前缀,这里需要说明一下,空格也是基本字符,SentencePiece 使用元符号 “▁” (U+2581) 转义空格,这里的意思是空格放在前缀还是后缀,默认False

"say hi"
前缀:["say","_hi"]
后缀:["say_","hi"]

19.allow_whitespace_only_pieces

指定是否允许空格作为子词单元,就是单独的一个空格,默认为False。

20.control_symbols

指定一组控制符号,这些符号用于划分子词,方便词汇表的构建,默认为""。

21.control_symbols_file

指定包含一组控制符号的文件,这些符号用于划分子词,方便词汇表的构建,默认为""。

22.user_defined_symbols

用户可以定义一组符号,这些符号可能不在训练文本中出现,但需要用于划分子词,默认为""。

23.required_chars

指定一组 UTF-8 字符,它们将始终包含在生成的词汇表中,无论 --character_coverage参数的设置是多少,因为默认0.9995,并不会覆盖完全。

24.byte_fallback

这个参数是比较重要的,用于指定在遇到未知或很少的字符时将其分解为 UTF-8 字节来表示,这个参数打开了BPE实现的效果就和BBPE是一样的了(还是建议看一下上一篇文章),比如”魑魅魍魉“,假如在我们训练语料中出现的次数太少,我们最后的词表里没有这个词,如果不开启这个参数就会OOV,如果开启了,这个词就会被用UTF-8的编码来分词即:”0xE9 0xAD 0x91 0xE9 0xAD 0x85 0xE9 0xAD 0x8D 0xE9 0xAD 0x89“,就可以被分词,分成12个token。默认为False。

25.vocabulary_output_piece_score

是否将词汇表中的每个子词给出一个分数,默认为True。

26.normalization_rule_name

指定文本规范化的规则,可以选择 “nfkc” 或 “identity”。解释一下nfkc:是一种常见的文本规范化规则,它使用了 Unicode 规范化形式 NFKC (Normalization Form KC)。NFKC 规范化通过将字符进行规范化,去除字符的多种表示形式,来确保文本在比较和处理时保持一致。它会将一些特定的字符组合转换为等效的单一字符,例如将带有重音符号的字符转换为没有重音符号的字符,以便更容易进行搜索、排序和匹配。identity:不对文本进行任何规范化处理。如果选择这个规则,文本将按照原始输入的方式进行处理,不进行任何字符合并、替换或重排。默认为nfkc。

27.normalization_rule_tsv

允许从文件中加载自定义的文本规范化规则,默认为""

28.add_dummy_prefix

是否在文本的开头添加一个虚拟的空格标记,以帮助处理文本的开头,默认为True。

29.remove_extra_whitespaces

是否删除文本中的多余空格,包括开头、结尾和连续的内部空格,默认为True。

30.hard_vocab_limit

如果启用,–vocab_size 参数将被视为硬限制,词汇表的大小不会超过该值。如果禁用,词汇表大小可能会略微超过指定的值,默认为True。

31.use_all_vocab

如果启用,将使用子词作为词汇表,而不考虑 --vocab_size 参数的设置,默认为False。

32.接下来是一系列特殊id的解释就不做赘述了,unk_id,bos_id,eos_id,pad_id,unk_piece等等,在训练词表时,这些最好和原要扩充的词表相对应。

33.train_extremely_large_corpus

如果启用,将增加 unigram 分词算法中的比特深度,用于处理极大的训练语料库,只有在选用unigram 才生效,默认为False。

34.random_seed

随机种子,如果随机种子是一样的,训练结果是可以重复的

35.enable_differential_privacy

控制是否在训练过程中添加差分隐私设置,差分隐私:差分隐私用于防止模型过度依赖特定训练样本的信息,从而减少了对个体数据的敏感性。它通过在训练数据中引入噪音来实现这一点,使得模型不太可能准确地学习任何个别样本的细节信息。这有助于保护数据隐私,尤其是对于包含敏感信息的数据。仅在unigram 分词算法可设置。

扩充LLaMa 中文词表简单实践

训练过程

上述就是一系列参数的解释,理解了这些参数后,再训练词表应该就会更游刃有余,我们以扩充LLaMa 中文词表为例开始进行一下简单实践:

准备一份中文训练语料保存为按照每一行保存为.txt文件,以BLOOM开源的中文维基百科数据前10000行为例:https://huggingface.co/datasets/bigscience-data/roots_zh-cn_wikipedia,下载后数据处理代码:

import pandas as pd
# 读取.parquet文件
parquet_file = '/path/file_name.parquet'
df = pd.read_parquet(parquet_file)

# 获取text列的前1万条数据,只用10000条来做测试
text_col = df['text'][:10000]

# 指定要写入的txt文件
txt_file = '/path/file_name.txt'

# 将数据追加写入txt文件
with open(txt_file, 'a') as file:
    content_col.to_csv(file, sep='\t', index=False, header=False)
print(f'前1万条content数据已写入到 {txt_file}')

开始训练,这里面有几个参数要注意一下,第一个是model_type分词算法选择bpe,split_digits为True,byte_fallback为True,和LLaMa 保持一致,max_sentence_length设置的大一点:

nohup spm_train --input '/path/file_name.txt' \
--input_format text \
--model_prefix bpe_test \
--model_type bpe \
--vocab_size 10000 \
--character_coverage 0.9995 \
--num_threads 32 \
--split_digits True \
--byte_fallback True \
--max_sentence_length 24000 > bpe_test.log &

执行上述训练过程,大概需要90S左右,会在当前目录下生成三个文件,bpe_test.model,bpe_test.vocab,bpe_test.log。看一下模型的分词效果:

import sentencepiece as spm
sp_bpe = spm.SentencePieceProcessor() 
sp_bpe.load('bpe_test.model')
print('*** BPE ***')
print(sp_bpe.encode_as_pieces('The excellence of a translation can only be judged by noting'))
print(len(sp_bpe.encode_as_pieces('The excellence of a translation can only be judged by noting')))
print(sp_bpe.encode_as_pieces('麒麟,是中国古代神话中的一种瑞兽'))
print(len(sp_bpe.encode_as_pieces('麒麟,是中国古代神话中的一种瑞兽')))

# 结果
*** BPE ***
['▁The', '▁', 'ex', 'c', 'ell', 'en', 'ce', '▁of', '▁a', '▁t', 'ran', 's', 'l', 'ation', '▁c', 'an', '▁', 'on', 'ly', '▁b', 'e', '▁', 'j', 'ud', 'g', 'ed', '▁b', 'y', '▁n', 'ot', 'ing']
31
['▁', '麒', '麟', ',', '是中国', '古代', '神', '话', '中', '的一种', '瑞', '兽']

可以看到,因为训练语料几乎都是中文的,对中文的分词效果是好于英文的,中文常见的一些词都变成了一个token,而英文被分的很碎。接下里把这个词表和原生LLaMa的词表进行合并。

合并LLaMa词表

直接看代码,参考代码见[3][4]:

import os
os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python"
from transformers import LlamaTokenizer
from sentencepiece import sentencepiece_model_pb2 as sp_pb2_model
import sentencepiece as spm

# 位置
llama_tokenizer_dir = "/path/llama-2-7b-hf" # 换成你自己模型的位置
chinese_sp_model_file ="/path/bpe_test.model" # 刚才训练的模型

# 加载
llama_tokenizer = LlamaTokenizer.from_pretrained(llama_tokenizer_dir)
chinese_sp_model = spm.SentencePieceProcessor()
chinese_sp_model.Load(chinese_sp_model_file)
llama_spm = sp_pb2_model.ModelProto()
llama_spm.ParseFromString(llama_tokenizer.sp_model.serialized_model_proto())
chinese_spm = sp_pb2_model.ModelProto()
chinese_spm.ParseFromString(chinese_sp_model.serialized_model_proto())


# 打印两个词表的大小和原llama的特殊token
print(len(llama_tokenizer),len(chinese_sp_model))
print(llama_tokenizer.all_special_tokens)
print(llama_tokenizer.all_special_ids)
print(llama_tokenizer.special_tokens_map)

# 结果
32000 10000
['', '', '']
[1, 2, 0]
{'bos_token': '', 'eos_token': '', 'unk_token': ''}

# 开始往llama词表里添加
llama_spm_tokens_set=set(p.piece for p in llama_spm.pieces)
print(len(llama_spm_tokens_set))
print(f"Before:{len(llama_spm_tokens_set)}")
for p in chinese_spm.pieces:
    piece = p.piece
    if piece not in llama_spm_tokens_set:
        new_p = sp_pb2_model.ModelProto().SentencePiece()
        new_p.piece = piece
        new_p.score = 0
        llama_spm.pieces.append(new_p)
print(f"New model pieces: {len(llama_spm.pieces)}")

# 结果
32000
Before:32000
New model pieces: 40114
# 我们中文词表原来有1万,去重添加后,添加了8114个词。

# 保存合并后的模型
output_sp_dir = 'merged_tokenizer_sp_test'
output_hf_dir = 'merged_tokenizer_hf_test'
os.makedirs(output_sp_dir,exist_ok=True)
with open(output_sp_dir+'/chinese_llama.model', 'wb') as f:
    f.write(llama_spm.SerializeToString())
tokenizer = LlamaTokenizer(vocab_file=output_sp_dir+'/chinese_llama.model')

tokenizer.save_pretrained(output_hf_dir)
print(f"Chinese-LLaMA tokenizer has been saved to {output_hf_dir}")

# 看一下效果
llama_tokenizer = LlamaTokenizer.from_pretrained(llama_tokenizer_dir)
chinese_llama_tokenizer = LlamaTokenizer.from_pretrained(output_hf_dir)


text = "The excellence of a translation can only be judged by noting"
print("Test text:\n",text)
print(f"Tokenized by LLaMA tokenizer:{llama_tokenizer.tokenize(text)}")
print(f"Tokenized length by LLaMA tokenizer:{len(llama_tokenizer.tokenize(text))}")
print(f"Tokenized by chinese_llama tokenizer:{chinese_llama_tokenizer.tokenize(text)}")
print(f"Tokenized length by LLaMA-extent-1 tokenizer:{len(chinese_llama_tokenizer.tokenize(text))}")

#结果,可以看到在英文上是没有变化的
Test text:
 The excellence of a translation can only be judged by noting
Tokenized by LLaMA tokenizer:['▁The', '▁excell', 'ence', '▁of', '▁a', '▁translation', '▁can', '▁only', '▁be', '▁jud', 'ged', '▁by', '▁not', 'ing']
Tokenized length by LLaMA tokenizer:14
Tokenized by chinese_llama tokenizer:['▁The', '▁excell', 'ence', '▁of', '▁a', '▁translation', '▁can', '▁only', '▁be', '▁jud', 'ged', '▁by', '▁not', 'ing']
Tokenized length by chinese_llama tokenizer:14


text = "麒麟,是中国古代神话中的一种瑞兽"
print("Test text:\n",text)
print(f"Tokenized by LLaMA tokenizer:{llama_tokenizer.tokenize(text)}")
print(f"Tokenized length by LLaMA tokenizer:{len(llama_tokenizer.tokenize(text))}")
print(f"Tokenized by chinese_llama tokenizer:{chinese_llama_tokenizer.tokenize(text)}")
print(f"Tokenized length by chinese_llama tokenizer:{len(chinese_llama_tokenizer.tokenize(text))}")

# 结果
Test text:
 麒麟,是中国古代神话中的一种瑞兽
Tokenized by LLaMA tokenizer:['▁', '<0xE9>', '<0xBA>', '<0x92>', '<0xE9>', '<0xBA>', '<0x9F>', ',', '是', '中', '国', '古', '代', '神', '话', '中', '的', '一', '种', '<0xE7>', '<0x91>', '<0x9E>', '<0xE5>', '<0x85>', '<0xBD>']
Tokenized length by LLaMA tokenizer:25
Tokenized by chinese_llama tokenizer:['▁', '麒', '麟', ',', '是中国', '古代', '神', '话', '中的', '一种', '瑞', '兽']
Tokenized length by chinese_llama tokenizer:12

至此,我们完成了LLaMa中文词表的扩充,扩充垂直领域词表也是如此,要准备垂直领域的训练语料,最好和通用领域的训练语料混合一下。但是这些新增的词在模型的 embedding 如何初始化,以及后续的如何训练是更为重要的,下篇文章将介绍这部分的内容,欢迎关注~

参考

ab[1] https://github.com/google/sentencepiece
[2] https://zhuanlan.zhihu.com/p/636491955
[3] https://github.com/ymcui/Chinese-LLaMA-Alpaca/blob/main/scripts/merge_tokenizer/merge_tokenizers.py
[4]https://github.com/shibing624/MedicalGPT/blob/main/merge_tokenizers.py

你可能感兴趣的:(LLM大模型,llama,chatgpt,语言模型,nlp)