模型嵌入层和权重矩阵关系的理解

模型的词嵌入层与权重矩阵

在基于 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
2
3
4
5
6
weight = [
[0.1, 0.2, 0.3], # token_id = 0 的嵌入向量
[0.4, 0.5, 0.6], # token_id = 1 的嵌入向量
[0.7, 0.8, 0.9], # token_id = 2 的嵌入向量
[1.0, 1.1, 1.2] # token_id = 3 的嵌入向量
]

如果 '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
2
3
4
5
6
7
8
9
10
11
import torch
import AutoModel, AutoTokenizer

token_id = tokenizer.convert_tokens_to_ids('entity')
token_embedding = model.embeddings.word_embeddings.weight[token_id]
print(token_id)

with torch.no_grad():
for i in range(1, num_added_toks+1):
model.embeddings.word_embeddings.weight[-i:, :] = token_embedding.clone().detach().requires_grad_(True)
print(model.embeddings.word_embeddings.weight[-2:, :])

对于token_embedding的赋值我们省略,这里就是把矩阵里面向量坐标给赋值给token_embedding
对于with torch.no_grad()后面的代码:

初始化新添加 token 的嵌入向量

1
2
3
with torch.no_grad():
for i in range(1, num_added_toks+1):
model.embeddings.word_embeddings.weight[-i:, :] = token_embedding.clone().detach().requires_grad_(True)
  • 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 的文本。