新加入的token的初始化相关说明
模型嵌入层和权重矩阵关系的理解
模型的词嵌入层与权重矩阵
在基于 Transformer 架构的预训练模型(如 BERT、GPT 等)中,词嵌入层(Embedding Layer
)是模型处理输入文本的重要组件。它的主要作用是将离散的 token(例如单词、子词等)转换为连续的向量表示,以便模型能够对其进行数值计算。
词嵌入层有一个权重矩阵(weight
),这个矩阵的形状通常是 (V, D)
,其中 V
是词汇表的大小,即分词器所能识别的所有 token 的数量;D
是嵌入向量的维度,也就是每个 token 对应的向量的长度。这个权重矩阵的每一行都对应着词汇表中一个特定 token 的嵌入向量。
通俗理解:
每个词嵌入,即word_embedding,都是一个向量,从表现形式来看本质就是向量的坐标表示:
如:[0.1, 0.3, 0.96],这些数字储存了每个token的相关语义和性质
(V,D)
其中的V就是有几行这样的向量数据,即分词器能识别的所有token数量,学名叫做:词汇表
其中的D就是每个向量的维度大小,学名就是:嵌入向量维度,比如这上面的这个向量嵌入向量维度就是3
**注:**嵌入向量维度就是嵌入词
token_id
的含义
token_id
是通过 tokenizer.convert_tokens_to_ids('entity')
得到的。分词器(tokenizer
)会为词汇表中的每个 token 分配一个唯一的整数 ID,这个 ID 就相当于该 token 在词汇表中的索引。所以,token_id
实际上就是 'entity'
这个 token 在词汇表中的索引值。
赋值操作的意义
model.embeddings.word_embeddings.weight[token_id]
这一操作就是从词嵌入层的权重矩阵中,根据 token_id
这个索引取出对应的行向量。
**这里相当简朴:**这里的id就是一个纯粹的索引值:
比如:fruits=['apple', 'banana', 'orange']
print(fruits[0])
就会输出apple
。
由于这一行向量就是 'entity'
这个 token 对应的嵌入向量,所以最终 token_embedding
被赋予的值就是 'entity'
的嵌入向量。这个向量包含了模型在预训练过程中学习到的关于 'entity'
这个 token 的语义和语法信息。
举个简单的例子,假设词嵌入层的权重矩阵如下(这里为了简化,假设词汇表大小为 4,嵌入向量维度为 3):
1 | weight = [ |
如果 'entity'
对应的 token_id
是 2,那么 token_embedding
就会被赋值为 [0.7, 0.8, 0.9]
。
综上所述,token_embedding
被赋予的是预训练模型中 'entity'
这个 token 对应的嵌入向量,该向量存储了模型学习到的关于这个 token 的语义信息。
使用原有token embedding初始化新token的过程详解
首先,为什么要用原有token embedding来初始化新token呢,因为让一个语义相近的已经预训练过的token embedding去给new token的embedding赋值的话,可以使得这个new token的embedding初始有比较完善的数据,在后续的数值更新的时候,可以更好的更新
看源码:(手动初始化)
1 | import torch |
对于token_embedding的赋值我们省略,这里就是把矩阵里面向量坐标给赋值给token_embedding
对于with torch.no_grad()
后面的代码:
初始化新添加 token 的嵌入向量
1 | with torch.no_grad(): |
with torch.no_grad():
:这是一个上下文管理器,在其作用域内,PyTorch 不会计算梯度。因为这里只是对词嵌入矩阵进行赋值操作,不需要计算梯度,使用torch.no_grad()
可以提高计算效率,并且避免不必要的梯度记录。for i in range(1, num_added_toks+1):
:假设num_added_toks
是之前向分词器中添加新 token 的数量(比如通过tokenizer.add_tokens(...)
方法添加新 token 后返回的添加数量)。这个循环从 1 到num_added_toks
,用于遍历新添加的 token。model.embeddings.word_embeddings.weight[-i:, :]
:-i
是负索引,用于从词嵌入权重矩阵的末尾开始选取行。[-i:, :]
表示从倒数第i
行开始,选取所有列,也就是选取新添加的 token 对应的嵌入向量行(因为新添加的 token 的嵌入向量行通常会被添加到权重矩阵的末尾)。token_embedding.clone().detach().requires_grad_(True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- `clone()`:创建`token_embedding`的一个副本,避免直接修改原始的嵌入向量。
- `detach()`:将副本从计算图中分离出来,使其不参与梯度计算(因为是从已有的嵌入向量复制)。
- `requires_grad_(True)`:将分离后的副本设置为需要计算梯度,这样在后续模型训练过程中,新添加 token 的嵌入向量可以随着训练进行更新。
- 整行赋值语句的作用是将`token_embedding`的副本赋值给新添加 token 对应的嵌入向量行,实现用`'entity'`的嵌入向量来初始化新添加 token 的嵌入向量。
- 关于这里的遍历:
最后一行的嵌入向量不会被添加两次,原因如下:
在代码 `for i in range(1, num_added_toks + 1):` 的循环中,虽然每次循环都会对 `model.embeddings.word_embeddings.weight[-i:, :]` 进行赋值操作,但本质上是一种覆盖式的赋值。
当 `i = 1` 时,`model.embeddings.word_embeddings.weight[-1:, :]` 只选取了权重矩阵的最后一行(即最后一个新添加 token 对应的嵌入向量行),然后将 `token_embedding` 赋值给这一行,此时最后一行的嵌入向量被初始化为 `token_embedding`。
当 `i = 2` 时,`model.embeddings.word_embeddings.weight[-2:, :]` 选取了权重矩阵的最后两行,然后再次将 `token_embedding` 赋值给这两行。对于最后一行来说,这并不是又添加了一次嵌入向量,而是对其进行了再次赋值,**用相同的 `token_embedding` 覆盖了之前的值(其实值本身没有变化,因为都是 `token_embedding` )。**
当 `i` 继续增大,直到 `i = num_added_toks` 时,都是类似的覆盖式赋值操作。每一次循环的赋值操作,都是对之前已赋值的行再次用相同的 `token_embedding` 进行覆盖,而不是额外添加新的嵌入向量,所以不会出现最后一行嵌入向量被添加两次的情况。
### 打印新添加 token 的嵌入向量
```python
print(model.embeddings.word_embeddings.weight[-2:, :])
这行代码打印词嵌入权重矩阵中最后两行的嵌入向量,也就是新添加的两个 token 的嵌入向量,用于查看初始化后的结果是否符合预期。
总体而言,这段代码的核心目的是使用已有 token('entity'
)的嵌入向量来初始化新添加 token 的嵌入向量,以便模型在后续训练中能够更好地处理包含这些新 token 的文本。