标签TokenClassification
标签TokenClassification
spaCy的NER标签 → RoBERTa可用的标签
数据结构:
1 | text = "女 大专 xfwang@dayee.com 身份证 杨洋 无经验 学士" |
这个格式是 字符级别的起止位置 + 标签(spaCy的doc.ents
也是这种)。 而 RoBERTa/Longformer NER 要吃的是 token级别的BIO/BILOU标签。 所以转换流程就是:
步骤 1:用RoBERTa的tokenizer分词
RoBERTa的分词器是BPE,不是spaCy的词切分,所以字符位置和token位置不会一一对应,必须重新对齐。
1 | from transformers import RobertaTokenizerFast |
输出示例:
1 | ['女', 'Ġ大', '专', 'Ġxf', 'wang', '@', 'day', 'ee', '.', 'com', 'Ġ身份证', 'Ġ杨', '洋', 'Ġ无', '经验', 'Ġ学士'] |
这里的
Ġ
表示空格,BPE会把空格当成特殊的前缀。
步骤 2:字符级span → token级span
用tokenizer的 char_to_token
方法,把字符起止位置映射到token位置。
1 | encoding = tokenizer(text, return_offsets_mapping=True) |
输出:
1 | [(0,1), (1,3), (3,4), (4,6), (6,10), (10,11), (11,14), (14,16), (16,17), (17,20), (20,24), (24,26), (26,27), (27,29), (29,31), (31,33)] |
每个元组是
(start_char, end_char)
。这样就可以判断哪个token对应哪个字符区间。
步骤 3:BIO标签初始化
先给所有token都标记为 "O"
(Outside)。
1 | labels = ["O"] * len(offsets) |
步骤 4:实体字符位置 → 对应token位置
对每个 (start_char, end_char, label)
找到对应的token下标。
1 | for start_char, end_char, ent_label in entities: |
步骤 5:查看结果
1 | for tok, lab in zip(tokens, labels): |
输出可能是:
1 | 女 B-GENDER |
最终效果
- spaCy标签是 字符级span
- RoBERTa需要 token级BIO标签
- 关键就是
offset_mapping
对齐字符→token位置 - 这样就能把 spaCy 的
(start, end, label)
转成 RoBERTa 能直接喂的input_ids
+labels
对
1 | 原文: 女 大专 xfwang@dayee.com 身份证 杨洋 无经验 学士 |
spacy config.cfg
配置文件分成 六大块:
- 路径 & 系统全局
- nlp 基础设置(语言、pipeline、tokenizer、vectors)
- 语料库 (corpora)
- 训练通用超参数(seed、dropout、early‑stop、epoch/step 等)
- 训练子模块(logger、batcher、optimizer)
- 初始化 (initialize) 阶段的资源
阅读提示
null
在配置里表示 “不指定 / 使用默认值”。${...}
是 变量引用,把前面定义的值插入当前位置。@xxx = "module.v1"
表示 使用 spaCy 的注册工厂(factory)来实例化对应的对象。
下面逐节、逐项解释每个变量的 含义、默认行为、以及在训练/推理时的作用。
1️⃣ [paths]
– 文件路径占位符
变量 | 含义 | 典型取值 | 说明 |
---|---|---|---|
train |
训练语料库的 磁盘路径(可以是 .spacy、.jsonl、.txt 等) | "./train.spacy" |
null 表示 未指定,需要在运行时通过 -c paths.train=/path/to/train.spacy 覆盖。 |
dev |
验证(dev)语料库的路径 | "./dev.spacy" |
同上,若没有 dev 集可以保持 null (但 eval_frequency 仍会尝试读取,会报错)。 |
vectors |
预训练词向量文件的路径(.vec 、.bin 、.spacy ) |
"./en_vectors_web_lg.vectors" |
若为 null ,nlp.vectors 会使用 空向量表(即没有外部向量)。 |
init_tok2vec |
用于 transfer‑learning 的 tok2vec 权重文件路径 |
"./tok2vec_init.bin" |
当你想在已有 tok2vec 基础上继续训练时使用。若 null ,则不加载。 |
为什么要把路径放在单独的
[paths]
节?
这样在 命令行(spacy train config.cfg -c paths.train=./mytrain.spacy
)或 Python 脚本 中可以统一覆盖,避免在多个位置硬编码。
2️⃣ [system]
– 系统层面的全局设置
变量 | 含义 | 说明 |
---|---|---|
seed |
随机数种子(Python、NumPy、PyTorch、cupy 等) | 0 表示 固定种子,保证每次运行得到相同的结果(只要其它因素也固定)。若设为 null 则使用 随机种子。 |
gpu_allocator |
GPU 内存分配器名称 | null 使用 spaCy 默认的 pytorch‑cuda 分配器。可设为 "pinned" 、"cuda_async" 等,以优化大模型的显存利用。 |
3️⃣ [nlp]
– 核心 Language
对象的配置
变量 | 含义 | 说明 |
---|---|---|
lang |
语言代码(en 、zh 、de …) |
null 让 spaCy 自动推断(如果在 initialize 中提供 vocab、vectors 等),否则必须显式指定。 |
pipeline |
流水线组件名称列表(执行顺序) | 例如 ["tok2vec","tagger","parser","ner"] 。这里是空列表 [] ,意味着 不加载任何组件(常用于只想跑 nlp.initialize() 或自己手动添加组件)。 |
disabled |
启动时 禁用 的组件 | 例如 ["parser"] 会在 nlp.from_disk 时加载 parser,但在训练前把它标记为 disabled 。默认 [] (全部启用)。 |
before_creation / after_creation / after_pipeline_creation |
可选回调(Python 表达式或函数路径) | 这些钩子在 Language 对象 创建前/后、以及 pipeline 完成构建后 被调用,可用于自定义修改 nlp (例如注入自定义属性、注册新组件等)。null 表示不使用。 |
batch_size |
nlp.pipe 与 nlp.evaluate 的 默认批大小 |
这里是 1000 (按 文档数),如果你的模型对显存要求高,可调小。此值仅在 未显式传入 batch size 时使用。 |
3.1 [nlp.tokenizer]
– 分词器工厂
变量 | 含义 | 说明 |
---|---|---|
@tokenizers = "spacy.Tokenizer.v1" |
通过 spaCy 注册系统 创建 Tokenizer 的工厂 | spacy.Tokenizer.v1 是默认实现,除非你想使用自定义 Tokenizer(如 my_pkg.MyTokenizer.v1 )才需要改动。 |
3.2 [nlp.vectors]
– 词向量表工厂
变量 | 含义 | 说明 |
---|---|---|
@vectors = "spacy.Vectors.v1" |
使用 spaCy 提供的 向量容器 | 当 paths.vectors 被指定时,这个工厂会读取向量文件并填充 nlp.vocab 。如果路径为空,则创建 空向量表(所有向量默认为 0)。 |
4️⃣ [components]
– 流水线组件的具体配置
当前文件里 没有任何子节(如
[components.tagger]
),因为pipeline = []
。
当你向 pipeline 中加入组件时,需要在这里添加对应的块,例如:
1 | [components.tagger] |
5️⃣ [corpora]
– 语料库读取器的配置
5.1 [corpora.train]
– 训练语料
变量 | 含义 | 说明 |
---|---|---|
@readers = "spacy.Corpus.v1" |
使用 spaCy 的 Corpus 读取器 | 负责把磁盘文件转化为 Example 对象(包含 gold 标注)。 |
path = ${paths.train} |
语料所在路径的 引用 | 若 paths.train 为 null ,则此读取器会报错,必须在运行时提供。 |
gold_preproc = false |
是否 使用 gold‑标准的句子边界和 token 化 | true 时会直接把文件里已经分好句、分好词的标注当作“真值”,不再让 spaCy 的 tokenizer 重新切分。 |
max_length = 0 |
文档长度上限(字符数) | 0 表示 不限制。若设置成 1000 ,超过长度的文档会被 截断或丢弃(取决于 reader 实现)。 |
limit = 0 |
最大读取的样本数 | 0 表示 读取全部。常用于快速调试,只读取前 N 条。 |
augmenter = null |
数据增强的工厂或回调 | 若提供(如 "spacy.Augmenter.v1" ),在读取每个 Example 时会随机进行 大小写、标点、同义词 替换等。 |
augmenter 之外的其它字段(如 gold_preproc )同理。 |
5.2 [corpora.dev]
– 验证语料
同训练块,只是 path
指向 paths.dev
,其它字段意义完全相同。
为什么两块几乎相同?
因为 训练 与 评估 常常使用相同的读取逻辑,只是数据来源不同。把它们分开可以让你在training.dev_corpus
中指向corpora.dev
,而不必在每个块里重复写dev_corpus = "corpora.dev"
。
6️⃣ [training]
– 训练过程的全局超参数
变量 | 含义 | 说明 |
---|---|---|
seed = ${system.seed} |
随机种子(从 [system] 继承) |
保证 训练、批次划分、模型初始化 的可重复性。 |
gpu_allocator = ${system.gpu_allocator} |
GPU 内存分配器(从 [system] 继承) |
影响显存分配策略,尤其在多卡或大模型时重要。 |
dropout = 0.1 |
Dropout 率(对所有支持 dropout 的层生效) | 训练时随机丢弃 10% 的神经元,防止过拟合。 |
accumulate_gradient = 1 |
梯度累积步数 | 若设为 >1 ,会在更新前累计多次 mini‑batch 的梯度,等效于增大 batch size 而不占用更多显存。 |
patience = 1600 |
Early‑stopping 的容忍步数 | 若在 1600 步(eval_frequency 计数)内验证指标没有提升,则提前结束训练。0 表示关闭 early‑stop。 |
max_epochs = 0 |
最大 epoch 数(遍历完整训练集的次数) | 0 → 无限 epoch(只受 max_steps 限制)。-1 表示 流式读取(不在内存中一次性加载全部数据),适用于极大语料。 |
max_steps = 20000 |
训练上限步数(更新次数) | 0 表示不限制,只受 max_epochs 控制。这里设为 20000 ,训练将在 20k 步后自动停止(即使 epoch 仍未结束)。 |
eval_frequency = 200 |
每隔多少步 进行一次 dev 评估 | 评估时会打印 dev 分数、保存 checkpoint(取决于 training.logger )。 |
score_weights = {} |
自定义评分权重(用于 nlp.evaluate ) |
空字典意味着使用 默认权重("ents_f" 、"tags_acc" 等)。如果想只关注 NER,可写 {"ents_f":1.0} 。 |
frozen_components = [] |
冻结的组件列表(不更新权重) | 例如 ["tok2vec"] 会在训练时保持不变,常用于 微调(只调 classifier)。 |
annotating_components = [] |
负责产生 gold 标注的组件 | 某些组件(如 entity_ruler ) 在训练时会 自动生成 额外标注;把它们列在这里可让 nlp.update 自动把这些标注加入 Example 。 |
dev_corpus = "corpora.dev" |
验证语料的配置块路径 | 训练循环会通过 registry.get("corpora.dev") 读取该语料。 |
train_corpus = "corpora.train" |
训练语料的配置块路径 | 同上,用于实际模型更新。 |
before_to_disk = null |
保存模型前的回调 | 可用于压缩、添加自定义文件、或移除临时属性。 |
before_update = null |
每一步更新前的回调 | 常用于 自定义学习率调度、梯度修改、日志记录等。 |
7️⃣ [training.logger]
– 训练日志记录器
变量 | 含义 | 说明 |
---|---|---|
@loggers = "spacy.ConsoleLogger.v1" |
使用 控制台日志(打印到 stdout) | 还有 spacy.FileLogger.v1 (写文件)或自定义 logger。它负责把 训练进度、损失、评估分数 输出到终端或文件。 |
8️⃣ [training.batcher]
– 批次划分器(batch generator)
变量 | 含义 | 说明 |
---|---|---|
@batchers = "spacy.batch_by_words.v1" |
按 词数 动态划分批次的工厂 | 其它可选:batch_by_sentences.v1 、batch_by_documents.v1 。 |
discard_oversize = false |
是否丢弃超过当前 batch size 上限的样本 | true 会把超长样本直接抛掉,false 会把它们放入 单独的 batch(可能导致极大 batch)。 |
tolerance = 0.2 |
容差:允许 batch 的实际大小在目标大小 ± tolerance * target 之间 |
例如 target=1000,tolerance=0.2 → batch 大小会在 800~1200 之间波动,以更好地填满 GPU。 |
8.1 [training.batcher.size]
– batch size 的调度(schedule)
变量 | 含义 | 说明 |
---|---|---|
@schedules = "compounding.v1" |
复合(compounding) 调度器:从 start 逐步 指数增长至 stop ,每次乘 compound |
常用于 从小 batch 开始(帮助模型快速收敛)逐步放大,以提升吞吐。 |
start = 100 |
初始 目标 batch size(词数) | 第一次迭代大约 100 个词。 |
stop = 1000 |
最大 目标 batch size | 随着训练进行,batch size 会慢慢增长到约 1000 词。 |
compound = 1.001 |
增长因子(每次 batch size ≈ 前一次 × 1.001) | 该值越大增长越快,若设为 1.0 则保持不变。 |
实际 batch 大小 =
schedule(step)
→ 受tolerance
、discard_oversize
进一步调节。
9️⃣ [training.optimizer]
– 优化器(Adam)及其超参数
变量 | 含义 | 说明 |
---|---|---|
@optimizers = "Adam.v1" |
使用 Adam 优化器(spaCy 包装版) | 其它可选:AdamW.v1 、SGD.v1 、RAdam.v1 等。 |
beta1 = 0.9 |
Adam 一阶矩(动量)衰减系数 | 与原始 Adam 相同,控制 梯度的指数移动平均。 |
beta2 = 0.999 |
Adam 二阶矩(方差)衰减系数 | 控制 梯度平方的指数移动平均。 |
L2_is_weight_decay = true |
将 L2 正则 解释为 权重衰减(而非普通的 L2 损失) | 当 true 时,更新公式会变成 θ ← θ - lr * (grad + λ * θ) ,更符合 AdamW 的实现。 |
L2 = 0.01 |
权重衰减系数 λ | 对所有 可学习参数(除 bias)施加衰减。值越大正则越强,防止过拟合。 |
grad_clip = 1.0 |
梯度裁剪阈值(全局 L2 范数) | 若梯度范数 > 1.0,则会 按比例缩放,防止梯度爆炸。 |
use_averages = false |
是否使用 参数滑动平均(EMA) | 若 true ,会在训练结束后把模型参数替换为 历史平均值(有助于提升泛化),但会占用额外显存。 |
eps = 1e-8 |
Adam 的 数值稳定项 | 防止除零错误。 |
learn_rate = 0.001 |
基础学习率(会被学习率调度器覆盖) | 在每一步,实际使用的学习率 = schedule(step) * learn_rate (如果 schedule 已经把 initial_rate 包含进来,则这里的值通常保持和 initial_rate 相同)。 |
关联解释:
learn_rate
与[training.optimizer.learn_rate]
(如果你另建该块)配合使用,schedule 会在每 step 调整learn_rate
的实际数值。L2_is_weight_decay
与L2
共同决定 权重衰减,而 权重衰减 会 乘以当前学习率,所以它们间接受schedule
的影响。
🔟 [initialize]
– nlp.initialize()
时的资源加载
变量 | 含义 | 说明 |
---|---|---|
vectors = ${paths.vectors} |
初始化时加载 外部向量(与 [nlp.vectors] 同步) |
若为 null ,则 nlp.vocab 中的向量表保持空。 |
init_tok2vec = ${paths.init_tok2vec} |
预训练的 tok2vec 权重路径(用于 transfer‑learning) |
常见于 微调 已有模型的 tok2vec 部分。 |
vocab_data = null |
额外的 词表数据(如 vocab.json ) |
若提供,会在 nlp.vocab.from_disk 时读取。 |
lookups = null |
lookup tables(比如 lemma_lookup , morph_lookup ) |
适用于自定义语言的 词形还原、形态学等。 |
tokenizer = {} |
传递给 Tokenizer 初始化的 关键字参数 | 如 {"use_legacy": false} 。 |
components = {} |
为每个 pipeline 组件 传递的 初始化参数(键是组件名) | 例如 components.tagger = {"learn_from": "gold_tag"} 。 |
before_init = null / after_init = null |
回调:在 nlp.initialize() 前后执行 |
可以用于 检查资源、动态生成标签集合、写日志等。 |
何时会调用
initialize
?
- 训练前(
spacy train
自动调用)- 预训练模型(
spacy pretrain
)- 手动脚本中使用
nlp.initialize()
来 预加载词表、标签、向量,确保后续nlp.update()
不会因缺失信息报错。
📌 小结(关键关联)
区块 | 关键变量 | 与其它区块的关联 |
---|---|---|
paths | train/dev/vectors/init_tok2vec |
被 corpora、initialize、nlp.vectors 引用 |
system | seed/gpu_allocator |
被 training、nlp 继承 |
nlp | pipeline、tokenizer、vectors |
pipeline 指向 components,vectors 与 initialize.vectors 同步 |
corpora | path、augmenter、gold_preproc |
path → ${paths.xxx} ;augmenter 可在 training 的 batcher 中使用 |
training | max_steps、eval_frequency、optimizer、batcher |
optimizer.learn_rate 与 learning‑rate schedule(若另建)配合;batcher.size 决定每 step 的 batch 大小,进而影响 梯度累积、显存 |
initialize | vectors、init_tok2vec |
为 nlp 对象提供 预训练资源,在 training 开始前完成 |
调参技巧(快速参考)
- 学习率:先把
initial_rate
(在 schedule)和optimizer.learn_rate
设为相同值(如 1e‑3),再观察 loss 曲线;若收敛慢可把initial_rate
降到 5e‑4。- 批次大小:
compounding
的start
与stop
影响显存占用;对显存紧张的 GPU 把stop
降到 500,或把tolerance
调小。- 正则:
L2 = 0.01
对大模型通常足够;如果出现 过拟合(dev 分数下降),可以把L2
加大到0.02
或0.05
。- 梯度裁剪:对 序列标注、NER 这类梯度可能爆炸的任务,保持
grad_clip = 1.0
;若出现 nan,尝试调小到0.5
。- 早停:
patience
与eval_frequency
配合使用;如果每 200 步评估一次,patience=1600
相当于 最多 8 次(1600/200)没有提升才停止。
token词表固定吗
1️⃣ 词表(Vocabulary)到底是「固定」还是「可改」?
视角 | 说明 |
---|---|
预训练阶段 | 词表 在模型训练之前 就已经确定。所有的 权重矩阵(尤其是 token‑embedding)的行数等于词表大小 vocab_size ,因此在正式 预训练 完成后,词表实际上是 固定的。 |
下游微调阶段 | 你可以 在已有词表的基础上 做两件事: 1️⃣ 保持原词表不变(最常见的做法),直接使用预训练权重; 2️⃣ 扩展或替换词表(例如加入行业专有词、字符集、emoji 等),这会导致 embedding 层的维度变化,需要特殊处理。 |
总结 | - 原始预训练模型的词表是固定的,因为它和模型参数是耦合的。 - 在下游任务里,你可以 增删 token,只要遵循下面的「映射影响」规则。 |
2️⃣ 词表改变会产生哪些直接影响?
影响维度 | 具体表现 | 需要的额外操作 |
---|---|---|
① token‑embedding 矩阵 | Embedding 大小 = vocab_size × hidden_dim 。如果词表大小改变,embedding 的 行数 必须相应增/删。 |
- 增词 → 用 随机初始化(或使用已有子词的均值)来补齐新增行。 - 删词 → 直接删除对应行,且对应的 预训练权重 会被丢弃。 |
② 预训练权重的对应关系 | 每个 ID 对应 embedding 矩阵的 第 ID 行。如果你把某个 token 的 ID 改动(比如把 [CLS] 从 101 改成 1),原来的 embedding 行会被错误地“搬走”,导致模型在该位置上学到 错误的向量。 |
- 不建议随意改动已有 token 的 ID。若必须改动(比如想把 <pad> 放在词表最后),必须 重新加载/重新训练 整个模型的 embedding(甚至全部参数)。 |
③ 特殊 token 的功能 | [PAD] 、[CLS] 、[SEP] 、[MASK] 、[UNK] 在 模型代码 中有硬编码的 默认 ID(如 pad_token_id=0 、unk_token_id=100 等),很多函数(attention_mask 、position_ids 、loss )都会依据这些 ID 做特殊处理。 |
- 改动 特殊 token 的 ID 时,需要在 Tokenizer 配置、模型配置 (config.json )、以及 代码里显式传入(如 model.config.pad_token_id = new_id )全部保持一致。 |
④ Attention mask / token_type_ids | attention_mask 依据 非‑padding 的位置(mask=1 )生成。如果 pad_token_id 改了,tokenizer 仍会把原来的 0 当作 padding,导致 mask 错误。 |
- 确保 tokenizer.pad_token_id 与 model.config.pad_token_id 同步。 |
⑤ 位置编码(position_ids) | 与 token ID 本身无关,但 序列长度 必须 ≤ max_position_embeddings 。若扩充词表导致 序列更长(例如加入很多子词),需要 增大 max_position_embeddings (或使用相对位置编码)。 |
- 重新训练/微调时可通过 model.resize_position_embeddings(new_max_len) 调整。 |
⑥ 迁移学习/跨模型兼容 | 两个模型如果 词表不一致,它们的 embedding 行 不对应,直接把一个模型的权重加载到另一个模型会产生 错位,导致性能大幅下降。 | - 使用 model.resize_token_embeddings(new_vocab_size) 让模型自动 扩展(新增行随机初始化)或 裁剪(删除多余行),再 继续微调。 |
3️⃣ 具体案例:改动词表会怎样「看得见」?
3.1 只 增加 新 token(最安全的改动)
1 | from transformers import BertTokenizerFast, BertModel |
- 影响:
[MED]
、[FIN]
、[GEO]
的 ID 会是原词表最后的三个(如 30522、30523、30524)。 - 好处:原有 token‑ID 保持不变,所有预训练权重仍然对应原来的向量,只有 新增行 需要在微调阶段学习。
3.2 删除 现有 token(不推荐)
1 | # 假设你想把词表里 "the" 这个 token 删除 |
- 后果:所有 ID > old_id 的 token 会向前平移 1 位,embedding 行全部错位,模型在每一步都会使用错误的向量,导致 几乎所有下游任务的性能崩溃。
- 唯一可行的做法:重新训练一个完整的 tokenizer(重新生成
vocab.txt
),从头训练或 从头微调(使用随机初始化的 embedding),这等价于 换模型。
3.3 更改 特殊 token 的 ID(极度危险)
1 | # 把 <pad> 的 ID 从 0 改成 5 |
- 如果不同步:
attention_mask
仍会把原来的 0 当作 padding,导致模型把实际 token 当作 padding,注意力会 忽略 这些位置,输出全是 0。 - 如果同步:模型仍然可以工作,但 所有已有的 checkpoint(在
pad_token_id=0
时训练得到的)在 梯度计算 时会把错误的 mask 传进来,微调效果会变差。 - 推荐:保持默认 ID(0、101、102、103、100)不动,若必须改动请 重新训练 或 在微调前彻底重新生成
attention_mask
。
4️⃣ 为什么词表的 ID 映射 会影响模型表现?
机制 | 解释 |
---|---|
Embedding 行对应唯一语义 | 每个 ID 在 embedding 矩阵中对应一个 固定向量。如果 ID 与语义不匹配(比如把 “北京” 的向量当成 “Apple”),模型的 上下文表示 会被错误地引导。 |
Transformer 的自注意力 | 注意力公式 softmax(QKᵀ / √d) 中的 Q/K 来自 token embedding。错误的向量会导致 错误的注意力分布,从而影响所有后续层的计算。 |
任务头的标签映射 | 对于 Token Classification(NER),标签是 逐 token 的。如果 token ID 错位,标签会被投射到错误的向量上,导致 损失函数 计算错误,模型学习不到正确的实体边界。 |
损失函数的 Mask | ignore_index=-100 (用于 MLM)是基于 特定 ID(pad_token_id )来过滤 padding。ID 改动后,ignore_index 失效,会把 padding 位置计入 loss,降低训练质量。 |
跨模型迁移 | 把 BERT‑base 的权重直接加载到 词表不同 的 BERT‑large(或自定义 tokenizer)时,权重会 错位,导致 几乎所有层的输出都不对。 |
5️⃣ 如何安全地 扩展 或 微调 词表?
步骤 | 代码示例(HuggingFace) | 关键点 |
---|---|---|
① 加载原始 tokenizer & model | python\nfrom transformers import AutoTokenizer, AutoModel\ntokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")\nmodel = AutoModel.from_pretrained("bert-base-uncased")\n |
保证两者版本匹配。 |
② 添加新 token | python\new_tokens = ["[MED]", "[FIN]", "[GEO]"]\nadded = tokenizer.add_tokens(new_tokens) # 只在词表末尾追加\n |
add_tokens 会返回新增数量。 |
③ 调整模型的 embedding 大小 | python\nmodel.resize_token_embeddings(len(tokenizer)) # 自动在 embedding 末尾补零/随机\n |
只会 新增行,原有行保持不变。 |
④ (可选)初始化新 token 的向量 | python\nimport torch\nwith torch.no_grad():\n # 这里把新 token 的向量初始化为已有 token 均值\n old_emb = model.get_input_embeddings().weight[:-added]\n mean_vec = old_emb.mean(dim=0)\n model.get_input_embeddings().weight[-added:] = mean_vec\n |
随机初始化也可以,微调时会自行学习。 |
⑤ 重新保存 tokenizer & model | python\ntokenizer.save_pretrained("./my_bert_extended")\nmodel.save_pretrained("./my_bert_extended")\n |
以后直接 from_pretrained("./my_bert_extended") 使用。 |
⑥ 开始下游任务微调 | 按常规 Trainer 、pipeline 等方式进行。 |
注意 batch size、学习率,因为新增向量初始较弱,可能需要稍大一点的学习率。 |
常见的「扩展」技巧
场景 | 建议 |
---|---|
行业专有词(医学、金融) | 把专有词 完整放进词表(不拆成子词),这样实体在 NER 中会得到 完整的向量,提升识别准确率。 |
emoji / 表情符号 | 这些字符往往不在原始英文 BERT 词表中,直接 add_tokens 并 在微调数据 中出现即可。 |
多语言混合 | 对于中英混合的任务,可使用 SentencePiece Unigram 生成统一词表,或在已有英文词表上 追加中文子词(注意 tokenizer.do_lower_case=False )。 |
把 [UNK] 替换为更细粒度的子词 |
重新训练 SentencePiece(或 WordPiece)时,将 unk_id 设为 词表末尾,这样在微调时仍然可以使用 ignore_index=-100 来过滤。 |
6️⃣ 何时必须 重新训练 整个模型?
条件 | 必要重新训练的原因 |
---|---|
删除或重新排列已有 token 的 ID | 这会导致 所有 embedding 行错位,必须从头训练或使用 全新 tokenizer 重新训练。 |
更改特殊 token的默认 ID 并希望保持原有权重 | 只能在 重新训练 时让模型学习新 ID 对应的向量,否则旧权重会被错误映射。 |
大幅度改变 max_position_embeddings (超过原来的 512/1024)且不使用 model.resize_position_embeddings |
位置编码矩阵也需要重新学习,否则超过范围的位置信息会被截断或错误映射。 |
从一个语言(英文)直接迁移到另一语言(中文) | 词表差异太大,必须 从头预训练 或 使用跨语言模型(如 XLM‑R、mBERT)并重新微调。 |
7️⃣ 小结(要点回顾)
- 词表在预训练时是固定的,因为它与模型的 embedding 行一一对应。
- 下游任务可以安全地
- 增加 新 token(
add_tokens
→resize_token_embeddings
), - 保持 所有已有 token 的 ID 不变。
- 增加 新 token(
- 删除或改动已有 token 的 ID 会导致 embedding 行错位,从而 破坏模型的所有层——只能重新训练。
- 特殊 token(
[PAD]
、[CLS]
、[SEP]
、[MASK]
、[UNK]
)的 ID 在 模型代码、Tokenizer、config 中都有硬编码,改动时必须 同步更新,否则会出现 mask 错误、attention 错位等问题。 - 词表扩展的影响:新增 token 的向量是随机(或均值)初始化,需要 微调 让它们学到有意义的表示;但原有权重保持不变,模型的整体能力基本不受影响。
- 实战建议:
- 先 检查
len(tokenizer)
与model.config.vocab_size
是否一致。 - 用
model.resize_token_embeddings(new_vocab_size)
自动扩展/裁剪。 - 对 特殊 token 使用
tokenizer.pad_token_id
,tokenizer.cls_token_id
等属性,而不是硬写数字。 - 若需要 大量新词(>5% 词表),考虑 重新训练 tokenizer(SentencePiece/WordPiece)并 从头或继续预训练(
model.train()
)以获得更好的子词分布。
- 先 检查
一句话概括:
词表的 ID ↔ embedding 行 是模型的核心映射,只要保持这一映射不变(即不改动已有 token 的 ID),你就可以安全地 增添新 token,并通过resize_token_embeddings
让模型接受这些新词;若破坏了映射,就必须 重新训练(或重新生成 tokenizer),否则模型的所有表示都会错位,性能会瞬间崩溃。
bert和任务头
1️⃣ 「BERT 本身」和「任务头」的边界
组件 | 作用 | 典型文件 / 参数 | 是否在 预训练 时出现 |
---|---|---|---|
BERT 基础模型 (BertModel , BertForMaskedLM …) |
只负责 上下文化的词向量(最后几层的 hidden states) | config.hidden_size , config.num_hidden_layers |
✅ 只在 ML NSP) 上训练,不含任何下游任务的线性层 |
任务头 (task‑specific head) | 把 BERT 最后一层的 hidden state 映射到 具体任务的标签空间(分类、序列标注、问答、序列生成 …) | classifier.weight , classifier.bias , qa_outputs , lm_head , crf … |
❌ 只有在 微调(fine‑tuning或 Adapter、Prompt 等技术里才会出现 |
Adapter / Prompt / LoRA | 轻量化的 可插拔 层,通常只修改 BERT 的内部几层或添加外部投影 | adapter_layers ,lora_A, lora_B` |
也是 微调阶段 的产物,预训练模型里没有 |
结论:
- 原始的
bert-base-uncased
、bert-large‑cased
等 checkpoint 只提供 通用隐藏向量,没有任何 NER、分类或其他任务的专用头。- 任何 “跑分” (GLUE、CoNLL‑2003、SQuAD 等)都是 在这些通用向量之上 再 训练(或 微调)一个 任务头(有时连同全部参数一起微调,有时只微调头部)。
2️⃣ 怎样判断一个 checkpoint 已经 包含了任务头?
2.1 看模型类(AutoModel…
)
类名 | 说明 | 典型 config 字段 |
---|---|---|
AutoModel / BertModel |
只有 基础 BERT,输出 (last_hidden_state, pooler_output, hidden_states…) |
config.is_decoder=False , 没有 num_labels 、classifier |
AutoModelForSequenceClassification / BertForSequenceClassification |
带 分类头 (classifier Linear) |
config.num_labels (>2) |
AutoModelForTokenClassification / BertForTokenClassification |
带 NER/POS 头 (classifier Linear) |
config.num_labels (token‑level) |
AutoModelForQuestionAnswering / BertForQuestionAnswering |
带 QA 头 (qa_outputs Linear) |
config.start_n_top , config.end_n_top |
AutoModelForMaskedLM / BertForMaskedLM |
带 语言模型头 (lm_head ) |
config.vocab_size |
AutoModelForCausalLM / BertLMHeadModel |
带 生成头 (lm_head ) |
同上 |
快速检查Python):
1 | from transformers import AutoConfig, AutoModel, AutoModelForTokenClassification |
2.2 看模型的 权重文件(pytorch_model.bin
)
权重名称 | 说明 | 是否出现说明了什么 |
---|---|---|
cls.predictions.bias 、cls.predictions.transform |
MLM 头 | 只在 BertForMaskedLM |
classifier.weight 、classifier.bias |
通用分类/序列标注头 | 只在 *ForSequenceClassification 、*ForTokenClassification |
qa_outputs.weight 、qa_outputs.bias |
QA 头 | 只在 *ForQuestionAnswering |
lm_head.weight 、lm_head.bias |
Causal/Masked LM 生成头 | 只在 *LMHeadModel |
adapter.* 、lora_* |
Adapter / LoRA 参数 | 只在使用这些轻量化微调技术的 checkpoint |
1 | import torch, os |
如果 只看到 embeddings.*
, encoder.*
,没有 classifier
、qa_outputs
、lm_head
,说明 没有任务头。
2.3 看 config.json
中的 special fields
1 | { |
architectures
指明模型类。id2label/label2id
/num_labels
只在 下游微调 时出现。- 如果这些字段缺失,则模型是 纯 BERT。
3️⃣ “跑分” 是怎么来的?
场景 | 训练方式 | 评分对象 | 常见误解 |
---|---|---|---|
GLUE / SuperGLUE(文本分类/相似度) | 微调 整个 BERT(或只微调头) | 在验证集/测试集上 计算 accuracy / F1 / MCC 等指标 | 只看 预训练模型(如 bert-base-uncased )是没有这些分数的;分数来自 微调后的 checkpoint |
CoNLL‑2003 NER | 在标注数据上 微调 BertForTokenClassification (加入 CRF) |
实体级 F1 | 同上,只有 加入 NER 头 并在 NER 数据上训练后才会有分数 |
SQuAD | 微调 BertForQuestionAnswering (输出 start/end logits) |
Exact Match / F1 | 需要 问答头,原始 BERT 只能输出隐藏向量,无法直接算分 |
仅用预训练 BERT 做特征提取 | 冻结全部参数,只取 last_hidden_state 作为特征 |
下游任务 需要自己训练 classifier | 这种情况下 模型本身不产生分数,分数来自你自己训练的 classifier |
所以:如果你在某篇论文或模型库里看到 “BERT‑base achieves 92.3 % on SST‑2”,那 一定 是在 SST‑2 上微调(或在 冻结 BERT + 训练一个小 linear classifier)后得到的 微调 checkpoint,不是原始
bert-base-uncased
。
4️⃣ 如何确认 最后几层 是否已经被 任务头 “吃掉” 了?
检查
requires_grad
1
2
3for name, param in model.named_parameters():
if param.requires_grad:
print(name) # 只会列出被训练的层- 若只看到
classifier.*
(或qa_outputs.*
)被requires_grad=True
,说明 BERT 本体被冻结,只有头在训练。 - 若所有
encoder.layer.*
也在列表中,说明 全模型微调。
- 若只看到
输出张量的形状
1
2
3
4outputs = model(**batch) # batch 为 tokenized inputs
if hasattr(outputs, "logits"):
print("logits shape:", outputs.logits.shape) # (batch, seq_len, num_labels) for token classification
print("hidden shape:", outputs.hidden_states[-1].shape) # (batch, seq_len, hidden_size)logits
的最后一维是 任务标签数,不等于hidden_size
→ 说明有 线性映射层。hidden_states[-1]
仍然保留 原始 BERT 隐藏向量,未被 “吞掉”。
查看
model.config
1
2print(model.config.hidden_size) # e.g. 768
print(getattr(model.config, "num_labels", None)) # e.g. 9 (NER) → 任务头存在
5️⃣ 如何在 spaCy 里把 BERT 接入 NER(如果你只看到 BERT 没有任务头)
spaCy 的 spacy‑transformers
会 自动在 BERT 之上加一个 NER 头(Transition‑Based Parser):
1 | [components.transformer] |
transformer
只提供 hidden vectors(shape = (batch, subword_len, 768))。ner
会在内部Linear(hidden, num_labels)
上做 token‑classification(如果你在config
中写nO = null
,spaCy 会把hidden_size
自动写进去)。- 训练时,spaCy 会 微调整个 BERT(默认)或者只微调 NER 头(通过
trainer.optimizer
的freeze
参数控制)。
如果你想自己手动加 NER 头(不走 spaCy),可以直接使用 HuggingFace:
1 | from transformers import AutoModelForTokenClassification, AutoTokenizer |
- 这里
AutoModelForTokenClassification
会 在ERT 最后一层 上自动添加nn.Linear(hidden_size, num_labels)
(以及可选的 CRF、dropout)。 - 只要把
num_labels
、id2label
、label2id
填好,模型在forward
时返回logits
(shape =(batch, seq_len, num_labels)
)。
6️⃣ 小结:一步步判断与使用
步骤 | 目的 | 关键代码/文件 |
---|---|---|
① 看文件 | 判断 checkpoint 是否已经包含任务头 | .json architectures , num_labels ;pytorch_model.bin → 是否有 classifier.* |
② 用 AutoModel 系列加载 |
自动匹配对应的类(只有 BERT → AutoModel ; 有头 →AutoModelFor…`) |
from transformers import AutoModel, AutoModelForTokenClassification |
③ 检查 requires_grad |
确认是 全模型微调 还是 只训练头 | for n in model.named_parameters(): print(n, p.requires_grad) |
④ 看输出 shape | 确认 logits 的最后一维是 标签数,而 hidden_states[-1] 仍是 hidden_size |
outputs.logits.shape , outputs.hidden_states[-1].shape |
⑤ 评估分数来源 | 分数是 微调后 的 checkpoint 还是 仅预训练 模型 | 阅读模型文档/论文 → “trained on X dataset”, “fine‑tuned on Y” |
⑥ 在 spaCy / HuggingFace 中接入 NER | 如果只有 BERT 本体,手动或自动添加 NER 头 | spaCy config(extra_state_tokens = false , nO = null )或 AutoModelForTokenClassification |
核心概念:
- 原始 BERT 只提供 通用的上下文向量,没有任何任务的输出层。
- 所有跑分(GLUE、CoNLL‑2003、SQuAD 等)都是 在这些向量之上 训练(或微调)任务头(线性层、CRF、QA 输出层等)得到的。
- 只要检查
config.json
、权重名称、requires_grad
,就能快速判断 是否已经做了任务适配,以及 是否只训练了头部。
Ernie‑4.5‑Causal‑LM 是什么模型?
项目 | 说明 |
---|---|
模型名称 | Ernie4_5ForCausalLM (Ernie‑4.5 系列的 Causal LM) |
模型类型 | 自回归(Causal)语言模型(decoder‑only) |
主要用途 | 语言生成、续写、对话、摘要、代码生成等 生成式任务 |
预训练目标 | Causal Language Modeling(预测下一个 token) |
是否包含 MLM / NSP | 没有。Causal LM 只用 `loss = -log p(x_t |
是否可直接用于 NER | 不直接;如果要做 NER,需要在此模型之上 添加一个 token‑classification 头(例如 ErnieForTokenClassification ),或把它当作 Encoder(只保留 transformer 部分)再接上 spaCy、HuggingFace‑Trainer 的 NER 头。 |
2️⃣ 配置文件里各字段的含义(简要)
字段 | 含义 |
---|---|
model_type: "ernie4_5" |
该模型属于 Ernie‑4.5 系列(百度推出的 “Enhanced Representation through Knowledge Integration”),是基于 GPT‑style 的架构。 |
hidden_size: 1024 、num_hidden_layers: 18 、num_attention_heads: 16 |
典型的 base‑size Transformer,参数约 120‑130 M(不算词表)。 |
max_position_embeddings: 131072 |
支持 最长 131 072(≈ 128 k)位置(使用 Rope 旋转位置编码)。 |
head_dim: 128 、num_key_value_heads: 2 |
多查询(MHA)配置:每个注意力 head 维度 128,KV 头数 2(即 多查询注意力,有助于推理时的 KV 缓存) |
rope_theta: 500000.0 、rope_scaling: null |
RoPE(旋转位置编码)参数,θ 越大对长序列越友好。 |
torch_dtype: "bfloat16" |
推荐使用 bfloat16(在支持的硬件上可提升算力/显存效率)。 |
use_cache: true 、num_key_value_heads: 2 |
表示模型已经实现 KV‑Cache,适合 高效推理(一次生成可复用缓存)。 |
vocab_size: 103424 |
词表大小约 103k,采用 BPE/WordPiece(与 AutoTokenizer 配合)。 |
hidden_act: "silu" 、use_bias: false 、rms_norm_eps: 1e-05 |
激活函数 SiLU (Swish)、无 bias、RMSNorm(比 LayerNorm 更轻量)。 |
bos_token_id: 1 、eos_token_id: 2 、pad_token_id: 0 |
开始、结束、填充 token 的 ID。 |
3️⃣ 与 “预训练” / “NSP” 的关系
任务 | 是否在此模型里出现 |
---|---|
Causal LM (自回归) | ✅(模型的唯一目标) |
Masked LM (MLM) | ❌(没有 lm_head 用于掩码) |
Next‑Sentence Prediction (NSP) | ❌(没有 next_sentence_head ) |
序列标注(NER) | ❌(需要额外的 token‑classification 头) |
因此,它是一个“预训练的自回归语言模型”,而不是针对 MLM 或 NSP 的预训练模型。如果你看到 Ernie4_5ForCausalLM
,就可以把它当作 GPT‑style 的 生成模型 来使用。
4️⃣ 如何把它用于 中文 NER(两种常用思路)
方案 A:直接在上层加 NER 头(推荐)
- 加载模型
1
2
3from transformers import AutoModelForCausalLM, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("your/ernie4_5_causal", use_fast=True)
model = AutoModelForCausalLM.from_pretrained("your/ernie4_5_causal") - **在其上添加 TokenClassificationHead(官方提供
ErnieForTokenClassification
)1
2
3
4
5
6from transformers import AutoModelForTokenClassification, AutoConfig
cfg = AutoConfig.from_pretrained("your/ernie4_5_causal", num_labels=NUM_LABELS)
model = AutoModelForTokenClassification.from_pretrained(
"your/ernie4_5_causal",
config=cfg
)- 这里
NUM_LABELS
对应你的实体类别数(如 5:["O","B-PER","I-PER","B-LOC","I-LOC"]
等)。
- 这里
- 使用
Trainer
或accelerate
进行 token‑classification 微调(输入是input_ids
,标签是 token‑level 标签)。
优点:直接利用已有的 Causal LM 权重,迁移学习效果好。
缺点:模型本身是 decoder‑only,在训练时会把每个 token 的 全部隐藏层 作为特征输出,计算量略大于 encoder‑only(如 BERT、RoBERTa)但仍可接受(显存≈ 8‑10 GB)。
方案 B:只保留 Encoder,配合 spaCy‑transformers(推荐用于 spaCy)
- 在 spaCy‑transformers 中只加载 Encoder(去掉语言模型头)
1
2
3
4
5
6[components.transformer.model]
@architectures = "spacy-transformers.TransformerModel.v3"
name = "your/ernie4_5_causal"
tokenizer_config = {"use_fast": true}
# 只保留 Encoder,省显存
extra_layers = [] # <-- 关键 - 在 spaCy pipeline 中加入
ner
组件(如ner
、entity_ruler
)1
2
3
4python -m spacy init config ernie_ner.cfg \
--lang zh \
--pipeline transformer,ner \
--optimize efficiency - 微调 NER(和前面示例的 Longformer、BigBird 等一样)
1
2
3
4
5python -m spacy train ernie_ner.cfg \
--paths.train train.spacy \
--paths.dev dev.spacy \
--output ./output_ner \
--gpu-id 0好处:只保留 Encoder,显存占用约 30‑40% 更低;适合 spaCy 的 token‑level 任务(NER、POS、依存)且不需要额外的 MLM 头。
如何选择?
场景 | 推荐方案 |
---|---|
仅做 NER、对显存有要求 | 方案 B(只保留 Encoder) |
想在同一个模型上先做生成(对话/摘要)再做 NER | 先方案 A(带 token‑classification head)并在需要时切换为 AutoModelForCausalLM 进行生成,切换为 AutoModelForTokenClassification 进行 NER(同一 checkpoint) |
希望在业务领域(医学、金融等)继续 MLM 预训练 | 先使用 Ernie4_5ForCausalLM 继续 Causal LM 预训练,再在 B 步骤中接入 NER(只保留 Encoder) |
需要超长序列(> 32 k) | 该模型的 max_position_embeddings 已达 131 072,在 bfloat16 下可直接处理超长文本(需显存 ≥ 12 GB),但要在 spaCy 中设置 max_batch_items 为对应的 token 数目。 |
5️⃣ 关键代码示例(完整流程)
5.1 下载并加载模型(PyTorch)
1 | from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig |
5.2 添加 Token‑Classification 头(用于 NER)
1 | num_labels = 5 # 示例:O, B-PER, I-PER, B-LOC, I-LOC |
5.3 使用 Trainer
进行微调(示例)
1 | from datasets import load_dataset |
5.4 在 spaCy 中使用(仅 Encoder)
1 | # big_ernie.cfg |
然后:
1 | python -m spacy train big_ernie.cfg \ |
6️⃣ 小结
关键点 | 说明 |
---|---|
模型类型 | Causal LM(自回归),Ernie4_5ForCausalLM |
是否是 MLM / NSP | 不是,只进行 下一词预测(Causal LM) |
是否已预训练 | 已预训练(在大规模中文语料上做了 Causal LM 预训练) |
如何用它做 NER | ① 在模型上 添加 token‑classification 头(AutoModelForTokenClassification )进行微调;② 或 只保留 encoder,在 spaCy‑transformers 中接入 ner 组件进行微调。 |
适合的下游任务 | 文本生成、对话、摘要、代码生成;经微调后 可用于实体抽取、序列标注(NER)等。 |
注意事项 | - 需要 bfloat16 或 fp16 才能在显存有限的 GPU 上高效运行。 - 若要在 spaCy 中使用,务必在 cfg 中 extra_layers = [] ,否则会加载 MLM 头导致显存浪费。- max_position_embeddings=131072 可直接处理 超长文本(> 8 k),只要显存足够即可。 |
BigBird和Longformer推理速度
参数量:
Longformer基于BERT架构,参数量与BERT-base相当,约1.1亿参数,具体取决于变体大小。
BigBird参数量稍大,类似于BERT-large级别,约3亿多参数,因其引入稀疏注意力机制稍有增加。
DeBERTa-v3-Long版本参数量取决于具体的模型大小,比如base型约8600万主干参数,另有较大词表嵌入参数,总体略大于BERT-base。
RoBERTa-Long则是RoBERTa模型的长文本版本,参数量接近RoBERTa-base或large,分别约1.25亿或3.55亿参数。
推理速度:
Longformer采用局部+全局稀疏注意力,推理速度相对提升,适合长文本但仍高于普通BERT。
BigBird使用块稀疏注意力结合随机和全局注意力,推理复杂度近似线性,推理速度优于Longformer,特别适合超长序列。
DeBERTa-v3-Long采用增强解码器结构,聚焦于改进表示能力,推理速度相较于普通BERT略慢,具体取决于实现。
RoBERTa-Long基于RoBERTa进化,实现在Longformer、BigBird等框架下,速度表现介于Longformer和BigBird之间,根据具体优化不同有所差异。
最大上下文长度:
Longformer支持最大输入长度可达4096 tokens及以上。
BigBird支持最长输入长度在4096到8192 tokens,具体实现可调。
DeBERTa-v3-Long支持较长上下文,通常为2048到4096 tokens。
RoBERTa-Long支持最多4096 tokens左右。
- Longformer使用了局部窗口注意力结合少量全局注意力,降低了Transformer原有的O(n²)复杂度,推理速度显著比标准Transformer快,但其复杂度仍大约是O(n√n)(局部+全局注意力混合),推理速度随序列长度增长有一定上升。
- BigBird采用块稀疏注意力机制,包括全局token、局部滑动窗口token和随机token三部分。该机制使模型的复杂度接近线性O(n),理论上在长序列推理时比Longformer更快,特别是超长序列(如4000+ tokens)情况下。
- 论文和实测结果中,BigBird在长文本任务的效果和推理速度整体优于Longformer,尤其在处理非常长的序列时,BigBird能更高效完成计算,同时保持较好性能。Longformer推理速度虽快于标准BERT,但实际速度比BigBird稍慢。
- 具体论文中提到,BigBird的推理速度几乎能保持与普通Transformer相当的性能优势(线性复杂度),而Longformer则在部分场景速度稍逊,但资源使用较少,适合中等长度长文本。
总结:
模型 | 推理复杂度 | 推理速度表现 | 适用场景 |
---|---|---|---|
Longformer | O(n√n) | 快于标准Transformer,但通常慢于BigBird | 中等长度长文本(数千tokens) |
BigBird | 近O(n) | 推理速度更快,特别是超长文本 | 极长文本(4k+ tokens及以上) |
因此,BigBird在理论复杂度和实测推理速度上均优于Longformer,尤其适合非常长的文本处理任务。