分词和词嵌入

1. 分词和词嵌入

分词是将原始文本切分成一系列基本单位(tokens)的过程,将连续文本转换为离散的标记序列,便于模型处理。常见算法包括基于空格/规则的分词,BPE(Byte-Pair Encoding)和WordPiece等。

词嵌入是将token(词或子词)映射到低维连续向量空间,将离散符号转换为捕捉语义关系的连续向量表示。常见的词嵌入模型包括Word2Vec, GloVe, FastText, B预训练模型的嵌入层等。

在LLM的处理流程中,一般

  1. 先通过分词将文本切分称token序列。
  2. 通过嵌入层将token ID转换为词嵌入向量。
  3. 将词嵌入向量输入模型进行后续处理。

1. 分词

根据分词粒度的不同,可以分为整词切分,字词切分。

整词切分是指以整个词或词组为最小单位对文本序列进行拆分。对于英文这类符号语言可以直接以空格为分隔符进行切分。对于中文这类无空格的语言,需要通过词典或规则进行切分。中文分词存在切分歧义和未登录词识别两大问题。切分歧义是指一个词可以有多种切分方式,如“中国人民银行”可以切分为“中国 人民银行”或“中国人民 银行”。未登录词识别是指识别出词典中未出现的新词。

子词是介于整词和单个符号之间的一种粒度,子词切分算法将整词按照一定规则继续切分成单个字母或连续字母片段,对于未登录词,通过词汇表中所记录的子词进行合并,来对未登录词进行重建。

常见的分词算法包括:BPE, WordPiece, SentencePiece等。

常见分词库包括:

  • Tokenizers:Hugging Face的高性能分词库
  • SentencePiece:Google开发的通用分词工具
  • NLTK/SpaCy:传统NLP库中的分词组件
  • tiktoken:OpenAI为GPT模型开发的分词库

使用tokenizers库进行分词:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from tokenizers import ByteLevelBPETokenizer

# 初始化分词器
tokenizer = ByteLevelBPETokenizer()

# 训练分词器
tokenizer.train(
files=["corpus.txt"], # 语料文件列表
vocab_size=32000, # 词表大小
min_frequency=2, # 最小出现频率
special_tokens=["[PAD]", "[UNK]", "[CLS]", "[SEP]", "[MASK]"] # 特殊符号
)

# 保存模型
tokenizer.save_model("my_tokenizer")

# 加载并使用
from tokenizers import Tokenizer
tokenizer = Tokenizer.from_file("my_tokenizer/vocab.json")
tokens = tokenizer.encode("这是一个测试句子")
print(tokens.tokens)

my_tokenizer目录下将出现三个文件:

  • vocab.json:词表文件,{"token": id,...}的映射
  • merges.txt:BPE合并文件,记录BPE合并规则,如"a b": "ab"
  • tokenizer.json: 完整的分词器配置文件,包括词表、合并规则等,预处理规则(如标准化,小写化等)

2. 词嵌入

词嵌入是将token映射到低维连续向量空间的过程,将离散符号转换为捕捉语义关系的连续向量表示。一般可分为静态词嵌入和动态词嵌入。

静态词嵌入一般采用基于矩阵分解,上下文窗口或者神经翻译模型。每个词都有一个固定的向量表示,不考虑上下文的变化。

动态词嵌入基于预训练语言模型实现,词的表示随上下文变化而变化,通过自注意力机制捕捉上下文信息,同一个词在不同语境中会有不同的向量表示,目前的大语言模型如llama3等都采用动态词嵌入。

llama3的嵌入层代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from fairscale.nn.model_parallel.layers import (
ColumnParallelLinear,
RowParallelLinear,
VocabParallelEmbedding,
)

# ...

class Transformer(nn.Module):
def __init__(self, params: ModelArgs):
super().__init__()
self.params = params
self.vocab_size = params.vocab_size
self.n_layers = params.n_layers

self.tok_embeddings = VocabParallelEmbedding(
params.vocab_size, params.dim, init_method=lambda x: x
)

# ...

# ...

其中VocabParallelEmbedding是一个分布式的嵌入层,将token ID映射为词嵌入向量。简化起见可以看作nn.Embedding的分布式版本,其本质是一个查找表结构。

维护一个vocab_size x dim的矩阵([vocab_size, embedding_dim]),通过token ID索引到对应的词嵌入向量。每一行代表词表中一个token的向量表示. 伪代码表示为

1
2
3
4
5
6
7
8
def forward(token_ids):
# token_ids形状: [batch_size, seq_len]
result = torch.zeros(batch_size, seq_len, embedding_dim)
for b in range(batch_size):
for s in range(seq_len):
idx = token_ids[b, s]
result[b, s] = weight_matrix[idx]
return result

2.1 利用预训练模型的词嵌入

预训练语言模型如llama3等一般为Decoder-Only架构,使用了因果注意力机制,因此在训练过程中只能看到当前位置之前的token,无法直接利用上下文信息。这种结构一般不适合做词嵌入。

一般embedding模型采用双向Transformer结构,可以看到当前位置之前和之后的token,可以更好的捕捉上下文信息。如BERT模型。

下文提出了一种将LLM转换为Embedding模型的方法

2.2 如何训练一个Embedding模型

常见的Embedding模型如bge系列,jina,text2vec等


分词和词嵌入
https://wenzhaoabc.github.io/llm/tokenization/
作者
wenzhaoabc
发布于
2025年2月26日
许可协议