分析 tokenizer_config.json 文件的设计,以及如何添加特殊token。结合代码来详细讲解 tokenizer_config.json 的设计原理以及如何添加特殊token。
tokenizer_config.json 是 HuggingFace Transformers 库中用于配置分词器的核心文件。从你的 Qwen2-VL 模型的配置文件中,我可以看到它包含了以下关键部分:
json展开代码{
  "added_tokens_decoder": {
    "151643": {
      "content": "<|endoftext|>",
      "lstrip": false,
      "normalized": false, 
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    // ... 更多token
  },
  "additional_special_tokens": ["<|im_start|>", "<|im_end|>", ...],
  // ... 其他配置
}
added_tokens_decoder: 定义了每个特殊token的详细属性
content: token的实际内容lstrip/rstrip: 是否自动移除左/右空格normalized: 是否进行标准化处理single_word: 是否作为单个词处理special: 标记为特殊tokenadditional_special_tokens: 额外的特殊token列表
chat_template: 定义对话格式的Jinja2模板
tokenizer_class: 指定使用的分词器类
model_max_length: 模型的最大序列长度
根据我查看的源码,transformers 库的加载过程如下:
python展开代码# 在 tokenization_utils_base.py 中
TOKENIZER_CONFIG_FILE = "tokenizer_config.json"
# from_pretrained 方法会首先尝试加载配置文件
tokenizer_config_file = resolved_vocab_files.pop("tokenizer_config_file", None)
if tokenizer_config_file is not None:
    with open(tokenizer_config_file, encoding="utf-8") as tokenizer_config_handle:
        init_kwargs = json.load(tokenizer_config_handle)
从源码中可以看到,在 _from_pretrained 方法中,有专门的逻辑处理 additional_special_tokens:
python展开代码# 第2203-2211行的关键代码
elif key == "additional_special_tokens" and isinstance(value, list):
    additional_special_tokens = init_kwargs.pop("additional_special_tokens", []) or []
    for token in value:
        if isinstance(token, dict):
            token["special"] = True
            token = AddedToken(**token)
        if token not in additional_special_tokens:
            additional_special_tokens.append(token)
    value = additional_special_tokens
在 LLaMA-Factory 中,qwen2vl 模板使用了这些特殊token:
python展开代码# 来自 template.py 第1705行
register_template(
    name="qwen2_vl",
    format_user=StringFormatter(slots=["<|im_start|>user\n{{content}}<|im_end|>\n<|im_start|>assistant\n"]),
    format_assistant=StringFormatter(slots=["{{content}}<|im_end|>\n"]),
    stop_words=["<|im_end|>"],
    mm_plugin=get_mm_plugin(name="qwen2_vl", image_token="<|image_pad|>", video_token="<|video_pad|>"),
)
现在让我演示如何添加 <toolcalls> 作为新的特殊token:
python展开代码#!/usr/bin/env python3
"""
演示如何向 tokenizer 添加特殊token的示例脚本
"""
from transformers import AutoTokenizer
import json
def add_special_tokens_to_tokenizer(model_path, new_tokens=["<toolcalls>"]):
    """
    向现有的 tokenizer 添加新的特殊token
    
    Args:
        model_path: 模型路径
        new_tokens: 要添加的新token列表
    """
    print(f"正在从 {model_path} 加载 tokenizer...")
    
    # 加载现有的 tokenizer
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    
    print(f"原始词汇表大小: {len(tokenizer)}")
    print(f"现有的特殊token: {tokenizer.special_tokens_map}")
    
    # 确保 new_tokens 是列表
    if isinstance(new_tokens, str):
        new_tokens = [new_tokens]
    
    # 方法1:使用 add_special_tokens 方法
    special_tokens_dict = {
        "additional_special_tokens": new_tokens
    }
    
    # 添加特殊token
    num_added_tokens = tokenizer.add_special_tokens(special_tokens_dict)
    
    print(f"成功添加 {num_added_tokens} 个新token: {new_tokens}")
    print(f"新的词汇表大小: {len(tokenizer)}")
    
    # 显示每个新token的ID
    for token in new_tokens:
        token_id = tokenizer.convert_tokens_to_ids(token)
        print(f"'{token}' 的 ID: {token_id}")
    
    # 验证token是否正确添加
    test_text = f"这是一个测试 {' '.join(new_tokens)} 文本"
    tokens = tokenizer.tokenize(test_text)
    print(f"测试文本分词结果: {tokens}")
    
    # 保存更新后的tokenizer
    output_path = "./updated_tokenizer"
    tokenizer.save_pretrained(output_path)
    print(f"更新后的tokenizer已保存到: {output_path}")
    
    return tokenizer
def modify_tokenizer_config_directly(config_path, new_tokens=["<toolcalls>"]):
    """
    直接修改 tokenizer_config.json 文件
    
    Args:
        config_path: tokenizer_config.json 文件路径
        new_tokens: 要添加的新token列表
    """
    print(f"正在修改 {config_path}...")
    
    # 确保 new_tokens 是列表
    if isinstance(new_tokens, str):
        new_tokens = [new_tokens]
    
    # 读取现有配置
    with open(config_path, 'r', encoding='utf-8') as f:
        config = json.load(f)
    
    # 找到下一个可用的token ID
    existing_ids = [int(id) for id in config.get("added_tokens_decoder", {}).keys()]
    next_id = max(existing_ids) + 1 if existing_ids else 151657
    
    # 添加到 added_tokens_decoder
    if "added_tokens_decoder" not in config:
        config["added_tokens_decoder"] = {}
    
    added_token_ids = []
    for i, token in enumerate(new_tokens):
        token_id = next_id + i
        config["added_tokens_decoder"][str(token_id)] = {
            "content": token,
            "lstrip": False,
            "normalized": False,
            "rstrip": False,
            "single_word": False,
            "special": True
        }
        added_token_ids.append(token_id)
    
    # 添加到 additional_special_tokens
    if "additional_special_tokens" not in config:
        config["additional_special_tokens"] = []
    
    for token in new_tokens:
        if token not in config["additional_special_tokens"]:
            config["additional_special_tokens"].append(token)
    
    # 保存修改后的配置
    backup_path = config_path + ".backup"
    with open(backup_path, 'w', encoding='utf-8') as f:
        json.dump(config, f, indent=2, ensure_ascii=False)
    
    print(f"配置已备份到: {backup_path}")
    print(f"新token已添加:")
    for token, token_id in zip(new_tokens, added_token_ids):
        print(f"  '{token}' -> ID: {token_id}")
    
    return config
def demonstrate_usage():
    """演示如何使用新添加的特殊token"""
    
    # 定义要添加的多个特殊token
    new_tokens = [
        "<|toolcall_start|>",
        "<|toolcall_end|>",
        "<|call_start|>",
        "<|call_end|>"
    ]
    
    # 示例1:在代码中直接添加
    # print("=" * 50)
    # print("方法1: 使用 Python 代码添加多个特殊token")
    # print("=" * 50)
    
    # # 注意:这里需要你提供实际的模型路径
    # model_path = "/mnt/jfs/model/Qwen/Qwen2-VL-2B-Instruct"
    # try:
    #     tokenizer = add_special_tokens_to_tokenizer(model_path, new_tokens)
    # except Exception as e:
    #     print(f"加载模型失败: {e}")
    #     print("请确保模型路径正确")
    
    # 示例2:直接修改配置文件
    print("\n" + "=" * 50)
    print("方法2: 直接修改 tokenizer_config.json")
    print("=" * 50)
    
    config_path = "./tokenizer_config.json"
    try:
        config = modify_tokenizer_config_directly(config_path, new_tokens)
        print("配置修改成功!")
    except Exception as e:
        print(f"修改配置失败: {e}")
if __name__ == "__main__":
    demonstrate_usage()
当你添加新的特殊token时,模型的词汇表会扩展,因此需要:
python展开代码# 在训练前调整模型的embedding层
model = AutoModelForCausalLM.from_pretrained(model_path)
model.resize_token_embeddings(len(tokenizer))
python展开代码#!/usr/bin/env python3
"""
测试 Qwen2-VL-2B-Instruct tokenizer 对特殊token的分词效果
"""
from transformers import AutoTokenizer
def test_tokenizer_special_tokens():
    """测试tokenizer对特殊token的处理"""
    
    model_path = "/mnt/jfs/model/Qwen/Qwen2-VL-2B-Instruct"
    
    print("=" * 60)
    print("测试 Qwen2-VL-2B-Instruct Tokenizer 特殊Token分词效果")
    print("=" * 60)
    
    # 加载原始tokenizer
    print(f"正在加载 tokenizer: {model_path}")
    try:
        tokenizer = AutoTokenizer.from_pretrained(model_path)
        print(f"✓ Tokenizer 加载成功,词汇表大小: {len(tokenizer)}")
    except Exception as e:
        print(f"✗ Tokenizer 加载失败: {e}")
        return
    
    # 测试的四个特殊token
    test_tokens = [
        "<|toolcall_start|>",
        "<|toolcall_end|>", 
        "<|call_start|>",
        "<|call_end|>"
    ]
    
    print(f"\n测试的特殊token: {test_tokens}")
    
    # 构建测试文本
    test_text = f"这是一个测试 {' '.join(test_tokens)} 文本"
    print(f"\n测试文本: {test_text}")
    
    # 测试分词
    tokens = tokenizer.tokenize(test_text)
    print(f"\n分词结果: {tokens}")
    print(f"总token数: {len(tokens)}")
    
    # 检查特殊token是否被正确识别
    print(f"\n特殊token识别情况:")
    for test_token in test_tokens:
        if test_token in tokens:
            print(f"  ✓ '{test_token}' 被识别为单个token")
        else:
            print(f"  ✗ '{test_token}' 被分解为多个token")
    
    # 编码测试
    token_ids = tokenizer.encode(test_text, add_special_tokens=False)
    print(f"\nToken IDs: {token_ids}")
    
    # 解码测试
    decoded = tokenizer.decode(token_ids)
    print(f"\n解码结果: {decoded}")
    
    # 检查解码是否准确
    if decoded == test_text:
        print("✓ 编码解码一致")
    else:
        print("✗ 编码解码不一致")
def main():
    test_tokenizer_special_tokens()
if __name__ == "__main__":
    main() 


本文作者:Dong
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!