一,分词器(Tokenizer)
分词器的作用是切分用户输入。
比如用户输入 "今天天气怎么样",分词器可能会将其分成数组:["今", "天", "天", "气", "怎", "么", "样"]。根据词汇表不同,不同的模型可能有不同的切法。
注意,分词器切分存在子词的概念,比如:"playing"切成["play","ing"]
二,词汇表
词汇表是分词器通过扫描海量文本(语料库)训练出来的一个映射表,里面记录了从Token到id的映射,每个token都能在这里找到唯一的id。词汇表一般是固定的,即使模型经过训练,词汇表也不会改动。
不同的模型使用的词汇表不同,格式也不同:
1,BERT(基于WordPiece):简单的文本文件,每个Token占一行。
2,GPT系列(基于BPE):JSON格式的文件,例如 {"the": 55, "cat": 656, ...}
3,SentencePiece:通常输出两个文件,一个 .model(二进制模型文件)和一个 .vocab(可读的词汇表文件)
三,Token和TokenId
分词器切分用户输入后的每个词都是一个Token,每个Token都能在词汇表里找到一个唯一的TokenId。
大语言模型输出的回复文案,本质上也是一个一个Token组合起来的。很多大语言模型会按输入+输出的Token数量来计费。
TokenId是从0开始的整数,最大值是词汇表中单词数量-1。
四,嵌入向量和嵌入矩阵
嵌入向量是一个浮点数数组(比如 [0.12, -0.56, ..., 0.78]),它是Token语义的数值化表示。数组的每个元素往往没有具体含义,它们是组合在一起表达语义的。
嵌入向量在不同的模型中的长度(即数组中元素个数,d_model)不同,比如BERT-base和GPT-2 small中都是768,BERT-large和GPT-2 medium都是1024。
嵌入向量长度越长,代表携带的信息越丰富,但计算时耗时越长,每个模型的嵌入向量长度都是设计者平衡利弊之后的选择。
每个TokenId对应的嵌入向量,组合形成嵌入矩阵,矩阵行号就是TokenId,用TokenId作为行号可以快速找到Token对应的嵌入向量。
embedding_vector = embedding_matrix[token_id] # embedding_matrix是嵌入矩阵,embedding_vector是得到的嵌入向量。
所以,矩阵行数就是词汇表大小(vocab_size),列数就是嵌入向量的长度(d_model)。嵌入矩阵在训练结束后就不变了,每次训练后其中的值会发生改变。
另外,两个TokenId数字接近,不代表两个Token语义接近,Token语义是否接近由嵌入向量决定。
五,键矩阵、查询矩阵、值矩阵
直接使用嵌入矩阵里的嵌入向量去和其他词交互不够灵活,所以嵌入向量进行线性变换后得到得到了键向量、查询向量、值向量。
就像嵌入向量组合成嵌入矩阵一样,键向量、查询向量、值向量分别组合后形成了键矩阵、查询矩阵、值矩阵。
所谓KQV自注意力机制,就是由键矩阵K、查询矩阵Q、值矩阵V为基础进行计算的一种机制。
也有文章中的KQV分别代表键向量、查询向量、值向量,本质上是一回事。
1,键矩阵K,与Q计算注意力权重,决定被关注的程度。
2,查询矩阵Q,与K计算注意力权重,决定关注哪里。
3,值矩阵V,提供最终要被加权求和的内容。
六,嵌入矩阵的线性变换
定义三个线性变换矩阵,分别给嵌入矩阵做乘法,得到键矩阵、查询矩阵、值矩阵。这个过程就是矩阵的线性变换。
线性变换矩阵的形状:(d_model, d_k),即行数=嵌入向量长度,宽度d_k可以自己定义为正整数,Transformer的论文中设定:d_k=嵌入向量长度/多头并行数,Transformer这么设计是为了在多头并行时,所有头的维度和等于d_model,方便后面做残差连接。
小结,值矩阵生成过程
用户输入文字后,生成键矩阵、查询矩阵、值矩阵的具体步骤是:
步骤1:用户输入原始字符串
python
user_input = "今天天气怎么样"
步骤2:分词(tokenization)
python
# 切分用户输入得到Token列表,可能是这样的:["今", "天", "天", "气", "怎", "么", "样"]
tokens = tokenizer.tokenize(user_input)
# 每个Token查询TokenId,TokenId列表可能是这样的:[101, 234, 234, 345, ...]
input_ids = tokenizer.convert_tokens_to_ids(tokens)
步骤3:生成嵌入矩阵
python
# 读取总嵌入矩阵,词汇表包含5000个Token,所以总嵌入矩阵有5000行,自定义嵌入向量宽度=768
embedding_layer = nn.Embedding(vocab_size=5000, embedding_dim=768)
# 从总嵌入矩阵中查询本次7个Token的嵌入向量,组成新的嵌入矩阵X,形状是(7, 768),即7行768列
X = embedding_layer(torch.tensor(input_ids))
步骤4:线性变换生成值矩阵
下面的代码生成了值矩阵,另外键矩阵、查询矩阵生成方式相同。
python
# 初始化可训练的权重矩阵,d_k=64
W_V = nn.Linear(in_features=768, out_features=64, bias=False)
# 线性变换得到值矩阵V,形状是(7, 64),即7行64列。
V = W_V(X)