当前位置: 首页 > news >正文

【15】Transformers快速入门:添加自定义 Token


课程模块:扩展你的词汇表——添加自定义 Token

目标: 掌握如何向预训练模型的分词器和词表中添加新的 Token(包括普通 Token 和特殊 Token),理解其对模型的影响,并学会正确调整模型权重。

核心概念: 自定义 Token (Custom Tokens), 特殊 Token (Special Tokens), 词表 (Vocabulary), Token Embedding, add_tokens(), add_special_tokens(), resize_token_embeddings()


1. 为什么需要添加自定义 Token?—— 当模型“不认识”你的标记时

想象一下,你给模型一个任务:“找出文本里的汽车和隧道实体”。你想用特殊的标记把它们框起来,比如:
"Two [ENT_START] cars [ENT_END] collided in a [ENT_START] tunnel [ENT_END] this morning."

你期望模型看到 [ENT_START][ENT_END] 就知道:“哦!这里标记了一个实体!”

但现实很骨感:

from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
sentence = 'Two [ENT_START] cars [ENT_END] collided in a [ENT_START] tunnel [ENT_END] this morning.'
print(tokenizer.tokenize(sentence))
# 输出:['two', '[', 'en', '##t', '_', 'start', ']', 'cars', '[', 'en', '##t', '_', 'end', ']', ...]

发生了什么?

  • 分词器把 [ENT_START] 无情地切成了 '[', 'en', '##t', '_', 'start', ']' 6个碎片!
  • 模型看到的是一堆毫无意义的字符片段,根本理解不了你想标记实体的意图。
  • 模型词表里压根没有 [ENT_START][ENT_END] 这两个 Token!

这就是问题所在: 预训练模型的词表是固定的。它不认识你自定义的特殊标记(如实体标记、领域术语、新表情符号等)。如果强行使用,这些标记会被错误地切分,导致模型无法理解你的意图。

添加自定义 Token 的必要性:

  1. 引入特殊功能标记:[ENT_START], [ENT_END], [DOMAIN_SPECIFIC]
  2. 添加领域专业词汇: 如医学名词 "Deoxyribonucleic acid" (DNA) 可能被切分,不如直接添加 "DNA" 作为一个 Token(如果它足够高频且重要)。
  3. 处理新出现的词汇: 如网络新词、品牌名、产品名。

核心目标: 让分词器和模型“认识”并“理解”你的新 Token!


2. 如何添加自定义 Token?—— 两种武器:add_tokensadd_special_tokens

Transformers 库提供了两种主要方法:

  • 武器一:tokenizer.add_tokens(new_tokens_list)

    • 作用: 向词表添加普通的新 Token
    • 参数: 一个包含新 Token 字符串的列表 ["token1", "token2", ...]
    • 特点:
      • 新 Token 会被添加到词表末尾
      • 新 Token 会参与分词器的标准化(Normalization)流程(如小写化、Unicode 规范化等)。
      • 适用于添加领域词汇、新名词等。
    checkpoint = "bert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)new_words = ["cybersecurity", "NFT", "metaverse"]  # 假设这些不在原词表中
    num_added = tokenizer.add_tokens(new_words)
    print(f"Added {num_added} new tokens!")  # 输出:Added 3 new tokens!# 检查词表大小是否增加
    print(f"Original vocab size: 30522, New vocab size: {len(tokenizer)}")  # 输出:New vocab size: 30525
    
  • 武器二:tokenizer.add_special_tokens(special_tokens_dict)

    • 作用: 向词表添加特殊的新 Token
    • 参数: 一个字典,键必须是预定义的特殊 Token 类型,值是对应的 Token 字符串。例如:
      special_tokens_dict = {"cls_token": "[MY_CLS]",     # 替换或添加 [CLS] Token"sep_token": "[MY_SEP]",     # 替换或添加 [SEP] Token"pad_token": "[PAD]",        # 通常已有,但可以确保或修改"unk_token": "[UNKNOWN]",    # 通常已有,但可以确保或修改"mask_token": "[MASK]",      # 通常已有,但可以确保或修改"additional_special_tokens": ["[ENT_START]", "[ENT_END]", "[DOMAIN]"]  # 添加全新的特殊Token
      }
      
    • 特点:
      • 新 Token 会被添加到词表末尾
      • 新 Token 不会较少参与分词器的标准化流程(例如,通常不会被小写化)。
      • 添加后,可以通过属性直接访问这些 Token(如 tokenizer.cls_token, tokenizer.additional_special_tokens)。
      • 适用于添加具有特殊功能的标记(如实体标记、任务控制标记)或替换/确保核心特殊 Token
    checkpoint = "bert-base-uncased"
    tokenizer = AutoTokenizer.from_pretrained(checkpoint)# 添加全新的实体标记作为 "additional_special_tokens"
    special_tokens_dict = {"additional_special_tokens": ["[ENT_START]", "[ENT_END]"]}
    num_added = tokenizer.add_special_tokens(special_tokens_dict)
    print(f"Added {num_added} special tokens!")  # 输出:Added 2 special tokens!# 现在分词器认识它们了!
    sentence = 'Two [ENT_START] cars [ENT_END] collided in a [ENT_START] tunnel [ENT_END] this morning.'
    tokens = tokenizer.tokenize(sentence)
    print(tokens)
    # 输出:['two', '[ENT_START]', 'cars', '[ENT_END]', 'collided', 'in', 'a', '[ENT_START]', 'tunnel', '[ENT_END]', 'this', 'morning', '.']
    

关键区别:标准化 (Normalization) 行为!

  • add_tokens (普通 Token): 会经过分词器的标准化流程(如小写化、清理空白、Unicode 处理等)。
  • add_special_tokens (特殊 Token): 通常会被视为“字面字符串”,标准化处理较少或没有。例如,在 bert-base-uncased(不区分大小写)分词器中:
    # 使用 add_tokens 添加
    tokenizer.add_tokens(["[NEW_tok1]"])
    print(tokenizer.tokenize('[NEW_tok1]'))  # 输出:['[new_tok1]']  (被小写了!)# 使用 add_special_tokens 添加 (作为 additional_special_tokens)
    tokenizer.add_special_tokens({"additional_special_tokens": ["[NEW_tok2]"]})
    print(tokenizer.tokenize('[NEW_tok2]'))  # 输出:['[NEW_tok2]']  (保持大写!)
    
    结论: 对于你希望保持原样(尤其是大小写)的功能性标记(如 [ENT_START]),务必使用 add_special_tokens 并放入 additional_special_tokens 中!

避免重复添加:

new_tokens = ["[ENT_START]", "[ENT_END]"]
# 方法1:检查是否已在词表中
new_tokens = [tok for tok in new_tokens if tok not in tokenizer.get_vocab()]
# 方法2:使用 set 操作 (更简洁)
new_tokens = list(set(new_tokens) - set(tokenizer.vocab.keys()))if new_tokens:tokenizer.add_special_tokens({"additional_special_tokens": new_tokens})

3. 让模型“认识”新 Token —— 调整 Embedding 矩阵

重要警告:仅仅修改分词器是不够的!

  • 分词器负责:文本 -> Token ID
  • 模型负责:Token ID -> Embedding Vector (词向量) -> 后续计算

当你通过 add_tokensadd_special_tokens 增加了词表大小(比如从 30522 增加到 30524),模型的 Embedding 矩阵(model.embeddings.word_embeddings还是原来的大小(30522 x 768)。新 Token 的 ID(30523, 30524)在模型的 Embedding 矩阵里没有对应的位置

解决方案:model.resize_token_embeddings(len(tokenizer))

from transformers import AutoTokenizer, AutoModelForSequenceClassificationcheckpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)# 1. 添加新 Token (这里是特殊Token)
new_special_tokens = ["[ENT_START]", "[ENT_END]"]
tokenizer.add_special_tokens({"additional_special_tokens": new_special_tokens})# 2. 关键一步:调整模型 Embedding 层大小!
old_embedding_size = model.get_input_embeddings().weight.size(0)
model.resize_token_embeddings(len(tokenizer))  # 传入新的词表大小
new_embedding_size = model.get_input_embeddings().weight.size(0)print(f"Embedding size changed from {old_embedding_size} to {new_embedding_size}")
# 输出:Embedding size changed from 30522 to 30524

resize_token_embeddings 做了什么?

  1. 扩容: 创建一个新的、更大的 Embedding 矩阵(例如 30524 x 768)。
  2. 复制: 将原来 30522 个 Token 的 Embedding 向量原封不动地复制到新矩阵的前 30522 行。
  3. 初始化: 为新添加的 Token(第 30523 和 30524 行)随机初始化 Embedding 向量(通常是从某个分布中采样,比如正态分布)。
  4. 替换: 将模型的旧 Embedding 层替换为这个新的、扩容后的 Embedding 层。

查看新 Token 的 Embedding:

# 获取新 Token 的 ID (通常在词表末尾)
ent_start_id = tokenizer.convert_tokens_to_ids("[ENT_START]")
ent_end_id = tokenizer.convert_tokens_to_ids("[ENT_END]")# 获取 Embedding 层权重
embeddings = model.get_input_embeddings().weight# 查看新 Token 的 Embedding (最后两行)
print("Embedding for [ENT_START]:", embeddings[ent_start_id, :5])  # 看前5维
print("Embedding for [ENT_END]:", embeddings[ent_end_id, :5])
# 输出示例 (每次运行值不同,因为是随机初始化的):
# Embedding for [ENT_START]: tensor([-0.0123,  0.0456, -0.0789,  0.1234,  0.0001], grad_fn=<SliceBackward>)
# Embedding for [ENT_END]: tensor([ 0.0987, -0.0321,  0.0567, -0.0456,  0.0123], grad_fn=<SliceBackward>)

重要提示:随机初始化意味着什么?

  • 模型在初始时对这些新 Token 没有任何语义理解。它们的 Embedding 是随机的乱码。
  • 要让模型理解这些新 Token 的含义,必须进行后续训练(微调)!
  • 在微调过程中,模型会根据你标注的任务数据(例如,实体识别任务中标记了 [ENT_START] cars [ENT_END] 是实体),调整这些新 Token 的 Embedding,同时也调整模型的其他参数,最终让模型学会这些新 Token 的语义和功能。

4. 完整流程与最佳实践

添加自定义 Token 并调整模型的正确步骤:

  1. 加载分词器和模型:
    tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
    model = AutoModelForSequenceClassification.from_pretrained("bert-base-uncased")  # 或其他任务模型
    
  2. 定义要添加的新 Token:
    • 普通词汇:new_regular_tokens = ["cyberattack", "phishing"]
    • 特殊标记:new_special_tokens = ["[ENT_START]", "[ENT_END]", "[REVIEW]"]
  3. 添加 Token 到分词器:
    # 添加普通 Token
    tokenizer.add_tokens(new_regular_tokens)
    # 添加特殊 Token (强烈推荐这种方式添加功能标记)
    tokenizer.add_special_tokens({"additional_special_tokens": new_special_tokens})
    
  4. (可选但推荐) 调整模型 Embedding 层大小:
    model.resize_token_embeddings(len(tokenizer))
    
    • 注意: 对于 AutoModelFor... 类模型(如 AutoModelForSequenceClassification),直接调用 resize_token_embeddings 是安全的,它会自动找到并调整输入 Embedding 层。对于自定义模型结构,需要确保调整了正确的 Embedding 层。
  5. (必须) 保存修改后的分词器和模型 (如果需要后续使用):
    tokenizer.save_pretrained("./my_custom_tokenizer/")
    model.save_pretrained("./my_custom_model/")
    
  6. (必须) 进行模型微调 (Fine-tuning):
    • 使用包含新 Token 的训练数据对模型进行训练。
    • 模型会在训练过程中学习新 Token 的 Embedding 表示及其在任务中的作用。

最佳实践总结:

  • 优先使用 add_special_tokens 添加功能标记: 确保它们保持原样(如大小写),并通过 additional_special_tokens 添加。
  • 添加后务必 resize_token_embeddings 让模型为新 Token 腾出位置。
  • 理解新 Token Embedding 是随机初始化的: 模型一开始不认识它们,必须通过后续训练(微调) 来赋予它们意义。
  • 检查词表: 使用 tokenizer.vocabtokenizer.get_vocab() 查看 Token 是否成功添加及其 ID。
  • 利用分词器属性: 对于通过 add_special_tokens 添加的核心特殊 Token(如自定义的 cls_token),使用 tokenizer.cls_token 访问更安全。对于 additional_special_tokens,使用 tokenizer.additional_special_tokens
  • 考虑 Token 的重要性: 不要随意添加大量低频词。优先添加高频、关键的功能标记或核心领域术语。

课程总结:

  • 问题: 预训练模型的词表固定,不认识自定义 Token(特殊标记、新词、术语)。
  • 解决方案:
    1. 扩展分词器词表:
      • tokenizer.add_tokens(list):添加普通 Token(会标准化)。
      • tokenizer.add_special_tokens(dict):添加特殊 Token(推荐用于功能标记,较少标准化)。
    2. 扩展模型 Embedding 层: model.resize_token_embeddings(new_vocab_size)new_vocab_size = len(tokenizer))。
  • 关键后果:
    • 新 Token 的 Embedding 是随机初始化的
    • 必须通过后续微调 (Fine-tuning) 让模型学习新 Token 的含义和功能。
  • 核心原则: 分词器和模型的词表大小必须同步更新!只改分词器不改模型(或反之)会导致错误。

提示:

  1. 实验对比: 加载一个 BERT 分词器。分别用 add_tokensadd_special_tokens (放入 additional_special_tokens) 添加同一个 Token (如 "[TEST]")。调用 tokenizer.tokenize('[TEST]') 观察输出有何不同?为什么?
  2. 观察 Embedding: 完成添加 Token 和 resize_token_embeddings 后,多次运行代码,打印出新 Token 的 Embedding 向量(如取前5个值)。观察它们是否变化?这说明了什么?
  3. 验证流程: 尝试只添加 Token 到分词器 (add_special_tokens),但不调用 model.resize_token_embeddings。然后用这个分词器编码一个包含新 Token 的句子,并尝试将 input_ids 送入模型。会发生什么错误?(提示:IndexError)
  4. 思考: 假设你要做一个医学文本分类任务,需要添加 100 个高频医学术语作为普通 Token。添加后直接使用模型进行预测(不微调),效果会好吗?为什么?如何改进?
http://www.dtcms.com/a/330062.html

相关文章:

  • 服务器安全防护
  • ARM芯片架构之CoreSight Channel Interface 介绍
  • 基于边缘深度学习的棒球击球训练评估研究
  • 模型训练不再“卡脖子”:国产AI训练平台对比与落地实践指南
  • 马力是多少W,常见车辆的马力范围
  • RK3568项目(十四)--linux驱动开发之常用外设
  • 中科米堆CASAIM蓝光三维扫描仪用于焊接件3D尺寸检测
  • 2025 开源语音合成模型全景解析:从工业级性能到创新架构的技术图谱
  • Python实现点云概率ICP(GICP)配准——精配准
  • static 和 extern 关键字
  • 公用表表达式和表变量的用法区别?
  • 【SpringBoot】12 核心功能-配置文件详解:Properties与YAML配置文件
  • WinForm中C#扫描枪功能实现(含USB串口)
  • 终端安全检测与防御
  • 20250813比赛总结
  • C++ list模拟实现
  • 未来AI:微算法科技(NASDAQ:MLGO)开发基于忆阻器网络储层计算MemristorPattern虚拟平台
  • 精准阻断内网渗透:联软科技终端接入方案如何“锁死”横向移动?
  • 科技赋能虚拟形象:3D人脸扫描设备的应用与未来
  • 钻井泥浆搅拌机的设计cad1张三维图+设计说明书
  • ULN2003与ULN2803的区别
  • MySQL优化常用的几个方法
  • 0813 网络编程基础
  • docker 容器内编译onnxruntime
  • cisco无线WLC flexconnect配置
  • 【Virtual Globe 渲染技术笔记】4 椭球面上的曲线
  • 大数据可视化设计 | 智能家居 UI 设计:从落地方法到案例拆解
  • 室外 3DVG 基准
  • mysql - 查询重复数据,不区分大小重复问题解决
  • Redis的基础命令