【16】Transformers快速入门:Token Embedding
课程模块:新 Token Embedding 的智慧初始化
目标: 理解为何需要精心初始化新 Token 的 Embedding,掌握多种初始化策略(包括零初始化、复制已有 Embedding、语义组合初始化),并了解其适用场景。
核心概念: Embedding 初始化,小样本学习 (Few-shot Learning),微调 (Fine-tuning),torch.no_grad()
,requires_grad
,语义描述平均
1. 为什么不能总是随机初始化?—— 小样本学习的困境
当我们通过 model.resize_token_embeddings(len(tokenizer))
添加新 Token 时,它们的 Embedding 会被随机初始化。这在有充足训练数据进行充分微调的场景下通常没问题——模型有足够的机会学习这些新 Token 的正确表示。
但是!在以下场景,随机初始化会带来问题:
- 小样本学习 (Few-shot Learning): 只有非常少的标注数据(例如几十条甚至几条)可用于微调。
- 资源受限的微调: 训练轮次 (epochs) 很少,或者学习率设置得很小。
- 新 Token 出现频率低: 即使数据总量不少,但新 Token 本身在训练数据中出现次数极少。
问题本质:
- 随机初始化的 Embedding 起点是“乱码”。
- 训练数据少或训练不充分意味着模型调整 Embedding 的幅度非常有限。
- 研究表明,在这些情况下,新 Token 的 Embedding 最终值会非常接近其随机初始值,几乎没有学到有意义的语义信息。
- 模型对这些新 Token 的处理效果会很差,因为它们本质上还是“随机的噪音”。
结论: 在小样本或训练不充分的场景下,为关键的新 Token(尤其是功能性的特殊 Token)提供合理的初始 Embedding 至关重要! 这相当于给模型一个“好起点”,让它更容易在有限的训练中学会使用这些 Token。
2. 初始化策略:从简单到智能
我们有多种策略来初始化新 Token 的 Embedding:
-
策略一:零初始化 (
Zero Initialization
)- 做法: 将新 Token 的 Embedding 向量所有维度设置为
0
。
import torch from transformers import AutoModelmodel = AutoModel.from_pretrained("bert-base-uncased") # ... (假设已添加新 Token 并 resize_token_embeddings)with torch.no_grad(): # 重要!初始化操作不参与梯度计算# 假设新添加的两个 Token 在 Embedding 矩阵的最后两行model.embeddings.word_embeddings.weight[-2:, :] = torch.zeros(2, model.config.hidden_size, requires_grad=True)
- 优点: 极其简单。
- 缺点:
0
向量在向量空间中通常没有特殊意义。- 模型需要从头开始学习其表示,起点较差。
- 适用场景: 当新 Token 纯粹作为占位符,或者你确实没有更好的先验知识时。不推荐作为首选。
- 做法: 将新 Token 的 Embedding 向量所有维度设置为
-
策略二:复制已有 Token (
Copy from Existing Token
)- 做法: 选择一个与新 Token 语义或功能相关的已有 Token,将其 Embedding 复制给新 Token。
import torch from transformers import AutoTokenizer, AutoModeltokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") model = AutoModel.from_pretrained("bert-base-uncased") # ... (假设已添加新 Token `[ENT_START]`, `[ENT_END]` 并 resize_token_embeddings)# 选择一个语义相关的词,比如 "entity" source_token = "entity" source_token_id = tokenizer.convert_tokens_to_ids(source_token) source_embedding = model.embeddings.word_embeddings.weight[source_token_id].clone().detach()with torch.no_grad():# 将 [ENT_START] 和 [ENT_END] 都初始化为 "entity" 的 Embeddingmodel.embeddings.word_embeddings.weight[-2:, :] = source_embedding.unsqueeze(0).repeat(2, 1)# 或者分别初始化(如果需要不同)# model.embeddings.word_embeddings.weight[-2, :] = source_embedding # [ENT_START]# model.embeddings.word_embeddings.weight[-1, :] = source_embedding # [ENT_END]
- 优点:
- 简单有效,为新 Token 提供了一个有语义基础的起点。
- 模型更容易理解新 Token 的大致角色(例如,
[ENT_START]
初始化为"entity"
,暗示它与实体概念相关)。
- 缺点:
- 可能过于简化,忽略了新 Token 的特殊性(如
[ENT_START]
是一个标记符,而"entity"
是一个名词)。
- 可能过于简化,忽略了新 Token 的特殊性(如
- 适用场景: 当新 Token 与词表中某个已有 Token 语义高度相关时。这是最常见且推荐的简单策略。
-
策略三:语义描述平均 (
Semantic Description Averaging
)- 做法: 为新 Token 定义一个描述其含义或功能的短语。计算这个短语中所有单词 Embedding 的平均值,作为新 Token 的初始 Embedding。
import torch from transformers import AutoTokenizer, AutoModeltokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") model = AutoModel.from_pretrained("bert-base-uncased") # ... (假设已添加新 Token `[ENT_START]`, `[ENT_END]` 并 resize_token_embeddings)# 为每个新 Token 定义描述短语 descriptions = {"[ENT_START]": "start of entity", # 描述 [ENT_START] 的含义"[ENT_END]": "end of entity" # 描述 [ENT_END] 的含义 }with torch.no_grad():# 倒序处理新 Token(假设它们添加在词表末尾)for i, (token, desc) in enumerate(reversed(descriptions.items()), start=1):# 分词描述短语desc_tokens = tokenizer.tokenize(desc)desc_ids = tokenizer.convert_tokens_to_ids(desc_tokens)# 获取描述短语中所有 Token 的 Embeddingdesc_embeddings = model.embeddings.word_embeddings.weight[desc_ids]# 计算平均 Embeddingnew_embedding = desc_embeddings.mean(dim=0)# 赋值给新 Token (注意索引:-i 表示倒数第 i 个 Token)model.embeddings.word_embeddings.weight[-i, :] = new_embedding.clone().detach()
- 优点:
- 提供更丰富、更精确的语义起点。
- 能够捕捉新 Token 的复合含义(例如
[ENT_START]
结合了"start"
和"entity"
的概念)。 - 特别适合功能标记或复杂概念的初始化。
- 缺点: 实现稍复杂,需要为每个新 Token 精心设计描述短语。
- 适用场景: 当新 Token 的含义无法用单个已有词完美表达,或者需要更精细控制其初始语义时。这是最智能、效果通常最好的策略。
3. 技术细节与最佳实践
-
torch.no_grad()
是必须的:with torch.no_grad():# 在这里进行 Embedding 赋值操作
- 为什么? Embedding 初始化是一个设置参数初始值的操作,不是模型学习过程的一部分。我们不希望这个操作产生梯度(
grad
)或者影响反向传播。torch.no_grad()
上下文管理器确保赋值操作在无梯度计算的环境中进行,避免不必要的计算开销和潜在错误。
- 为什么? Embedding 初始化是一个设置参数初始值的操作,不是模型学习过程的一部分。我们不希望这个操作产生梯度(
-
requires_grad=True
是必要的:new_embedding = ... # 从其他地方获取的 Embedding 向量 model.embeddings.word_embeddings.weight[-i, :] = new_embedding.clone().detach().requires_grad_(True)
- 为什么?
.clone().detach()
:确保我们赋值的是一个新的、独立的 Tensor,并且断开了它可能来自原始 Embedding 的计算图(避免意外影响)。.requires_grad_(True)
:明确设置这个新 Embedding 需要计算梯度。这是关键!它告诉 PyTorch 在后续的微调训练中,这个新 Token 的 Embedding 参数是需要被优化(更新)的。如果不设置requires_grad=True
,模型在训练时就不会更新这个 Embedding,它就永远停留在初始值了。
- 为什么?
-
获取 Embedding 层:
- 对于
transformers
库提供的模型(如BertModel
,AutoModel
),Embedding 层通常位于model.embeddings.word_embeddings
。 - 对于自定义模型或某些特定架构,可能需要通过
model.get_input_embeddings()
来获取 Embedding 层。
embedding_layer = model.get_input_embeddings() with torch.no_grad():embedding_layer.weight[-1, :] = ... # 修改最后一个 Token 的 Embedding
- 对于
-
新 Token 的位置:
- 新 Token 通过
add_tokens
或add_special_tokens
添加后,通常位于词表的末尾。 - 因此,在
model.resize_token_embeddings(len(tokenizer))
后,新 Token 的 Embedding 对应着 Embedding 矩阵的最后几行(weight[-num_new_tokens:, :]
)。 - 可以通过
tokenizer.convert_tokens_to_ids("[NEW_TOKEN]")
获取新 Token 的确切 ID 来验证其位置。
- 新 Token 通过
-
何时初始化?
- 初始化操作必须在调用
model.resize_token_embeddings(len(tokenizer))
之后进行。 - 推荐在添加 Token、调整 Embedding 大小后立即进行初始化,然后再开始模型的微调训练。
- 初始化操作必须在调用
-
微调依然必要:
- 切记! 无论采用哪种初始化策略,都不能替代后续的微调训练!
- 初始化只是提供了一个更好的起点(Prior)。
- 模型仍然需要根据你的具体任务数据来调整(Adapt) 这些新 Token 的 Embedding,使其更好地服务于任务目标(例如,让
[ENT_START]
更精确地标记实体开始的位置)。
4. 策略选择指南与总结
初始化策略 | 优点 | 缺点 | 推荐场景 |
---|---|---|---|
零初始化 | 极其简单 | 起点差,模型学习负担重 | 无更好选择时的备用方案;纯粹占位符 |
复制已有 Token | 简单有效,提供语义基础起点 | 可能过于简化,忽略特殊性 | 新 Token 与某个已有词语义高度相似时(最常见) |
语义描述平均 | 提供更丰富、精确的语义起点 | 需设计描述短语,实现稍复杂 | 新 Token 含义复杂或需精细控制初始语义时 |
核心原则:
- 避免纯随机初始化: 尤其是在小样本或训练不充分的场景下。
- 选择有意义的起点: 利用模型预训练阶段学到的丰富语义知识(通过复制或平均已有 Embedding),为新 Token 提供一个合理的初始表示。
- 精心设计描述短语: 对于语义描述平均法,描述短语的质量直接影响初始化效果。短语应准确、简洁地反映新 Token 的核心含义或功能。
- 技术细节要严谨: 使用
torch.no_grad()
和正确设置requires_grad=True
。 - 微调不可或缺: 初始化只是开始,必须通过任务数据的微调让模型学会如何有效地使用新 Token。
提示:
- 实验对比: 对一个分类任务(如情感分析),添加一个特殊 Token
[REVIEW]
放在每个评论开头。分别尝试:- 随机初始化(默认)
- 初始化为
"review"
词的 Embedding - 初始化为
"text"
和"opinion"
的平均 Embedding - 初始化为
"this is a review text"
的平均 Embedding
在极少量训练数据(如 20 条)下微调模型,比较验证集准确率。观察哪种初始化策略在小样本下表现最好?
- 理解梯度: 在初始化新 Token Embedding 时,故意省略
.requires_grad_(True)
。训练模型几个 epoch 后,打印出新 Token 的 Embedding。观察它是否被更新了?为什么? - 设计描述: 假设你要添加一个 Token
[MEDICAL_TERM]
来标记医学专业术语。设计 2-3 个不同的描述短语(如"medical term"
,"specialized medical vocabulary"
,"clinical terminology"
)。用代码实现语义描述平均初始化,并打印出不同描述得到的初始 Embedding(取前 5 维)。它们相似吗?为什么? - 思考: 如果添加的新 Token 是一个表情符号,比如
"😊"
,哪种初始化策略最合适?为什么?(提示:考虑表情符号的语义通常与“开心”、“微笑”相关)