前言
在自然语言处理领域,训练分词器和训练大模型是两个基本而关键的过程。它们相互依赖,共同影响着语言模型的性能和应用效果。
训练分词器是将连续文本分割成有意义单元的过程,它是数据预处理的一个重要步骤。一个精确的分词器不仅能够提高数据的质量,还能帮助模型更准确地理解句子结构和语义。这对于后续的大模型训练至关重要,因为即使是小的分词错误也可能在大规模模型中被放大,影响最终的应用效果。训练大模型需要大量的数据和计算资源,它从分词器得到的数据中学习语言的深层次结构和模式。大模型的训练过程可以揭示分词器的重要性:一个优秀的分词器能够显著提升模型的性能,尤其是在处理复杂文本或新领域数据时。
一、从零训练Tokenizer
想要自己训练一个自己专属的Tokenizer有多种方法,使用sentencepiece工具是一种非常便捷、高效的方法。
import sentencepiece as spm
from sentencepiece import sentencepiece_model_pb2 as sp_model_pb2
import json
model_name = "starry_tokenizer"
model_type = "bpe"
vocab_size = 20000
# 从语料中训练自己的tokenizer
# byte_fallback=True,这样在解码时如果遇到了词表以外的词,会用UTF-8 编码将这些字符转换成字节表示,可以避免 OOV(out of vocabulary)问题
# character_coverage 模型覆盖的字符数量,对于字符集丰富的语言(如日语或中文)推荐默认值为 0.9995,对于其他字符集较小的语言推荐默认值为 1.0。
# input 训练语料;model_prefix 模型输出路径;vocab_size训练后词表的大小;character_coverage 模型中覆盖的字符数;
# model_type 模型类型:unigram,bpe,char,word;byte_fallback在解码时如果遇到了词表以外的词,会用UTF-8 编码将这些字符转换成字节表示
spm.SentencePieceTrainer.Train(input=path, vocab_size=vocab_size, model_type=model_type, model_prefix=model_name,
byte_fallback=True, character_coverage=0.9995)
二、现有Tokenizer的基础上加上自己的词表
如果我们觉得重新训练一个自己的Tokenizer很麻烦,没有必要性, 但是又想加入一些特殊词汇在词表中。这时候我们可以使用sentencepiece进行词表扩充。
# 使用spm.SentencePieceProcessor()来读取tokenizer模型
sp = spm.SentencePieceProcessor(model_file='starry_tokenizer.model')
# 输出去Tokenizer模型的词表大小
print(sp.vocab_size())
old_model = sp_model_pb2.ModelProto()
old_model.PaeseFromString(sp.serialized_model_proto())
existed_pieces = set(p.piece for p in old_model.pieces)
for piece in pieces:
if piece not in existed_pieces:
existed_pieces.add(piece)
new_piece = sp_model_pb2.ModelProto().SentencePiece()
new_piece.piece = piece
new_piece.score = 0
old_model.pieces.append(new_piece)
# 保存Tokenizer模型
with open('new-starry-tokenizer.model', 'wb') as f:
f.write(old_model.SerializeToString())
print(len(old_model.pieces))
三、合并两个分词器
对于现有的两个分词器,如果我们合并起来取并集的词表,那么我们可以用下面的代码:
# 合并两个分词器
def expand_vocab():
model_file = "wiki_model.model"
jp_model_file = "jp_koa_model.model"
# 加载两个预训练的SentencePiece模型文件:wiki_model.model和jp_koa_model.model
# 使用spm.SentencePieceProcessor类分别创建两个分词器对象:chinese_sp和jp_sp。
chinese_sp = spm.SentencePieceProcessor(model_file=model_file)
jp_sp = spm.SentencePieceProcessor(model_file=jp_model_file)
print(chinese_sp.vocab_size())
# 将chinese_sp的序列化模型协议(serialized model protocol)解析为sp_model_pb2.ModelProto对象:chinese_spm
chinese_spm = sp_model_pb2.ModelProto()
chinese_spm.ParseFromString(chinese_sp.serialized_model_proto())
# 将jp_sp的序列化模型协议解析为sp_model_pb2.ModelProto对象:jp_spm。
jp_spm = sp_model_pb2.ModelProto()
jp_spm.ParseFromString(jp_sp.serialized_model_proto())
# 创建一个集合chinese_sp_set,包含chinese_spm中的所有词汇片段(piece)。
chinese_sp_set = set(p.piece for p in chinese_spm.pieces)
# 遍历jp_spm中的每个词汇片段,如果该片段不在chinese_sp_set中,则将其添加到chinese_sp_set和chinese_spm.pieces中,并设置其得分为0。
for p in jp_spm.pieces:
piece = p.piece
if piece not in chinese_sp_set:
chinese_sp_set.add(piece)
new_piece = sp_model_pb2.ModelProto().SentencePiece()
new_piece.piece = piece
new_piece.score = 0
chinese_spm.pieces.append(new_piece)
# 打印扩展后的词汇表大小。
print(f"after expand vocab={len(chinese_spm.pieces)}") # 20253
# save model
with open(model_file, 'wb') as file:
file.write(chinese_spm.SerializeToString())
注意:无论用什么方法扩充词表后,需要同时对语言模型的 token embedding 进行 resize,否则会出现维度不一致错误。
更具体的,如果使用 transformers model,可以使用 resize_token_embeddings 方法进行 resize,详情见resize_token_embeddings官方文档
以下是一个简单用Transformers加载bert模型后进行resize的示例代码。
model = BertForMaskedLM.from_pretrained(model_dir)
model.resize_token_embeddings(len(tokenizer))