标签TokenClassification
spaCy的NER标签转换成RoBERTa能用的格式
最近在做命名实体识别的项目,遇到了一个挺头疼的问题:spaCy标注出来的数据格式和RoBERTa需要的格式不一样。折腾了好久才搞明白,这里记录一下转换的过程。
先看看我们的数据长什么样:
text = "女 大专 xfwang@dayee.com 身份证 杨洋 无经验 学士"
entities = [
(27, 29, 'NAME'),
(1, 2, 'GENDER'),
(6, 22, 'EMAIL_ADDRESS'),
(30, 33, 'YEARS_OF_WORK_EXPERIENCE'),
(34, 36, 'DEGREE')
]
这种格式是字符级别的起止位置 + 标签(spaCy的doc.ents就是这样的)。但是RoBERTa/Longformer做NER的时候,需要的是token级别的BIO/BILOU标签。所以我们得想办法把它们对应起来。
第一步:用RoBERTa的tokenizer分词
这一步很关键,因为RoBERTa用的是BPE分词,跟spaCy的分词方式完全不一样。字符位置和token位置肯定对不上,所以必须重新分词。
from transformers import RobertaTokenizerFast
tokenizer = RobertaTokenizerFast.from_pretrained("roberta-base")
tokens = tokenizer.tokenize(text)
print(tokens)
输出大概是这样的:
['女', 'Ġ大', '专', 'Ġxf', 'wang', '@', 'day', 'ee', '.', 'com', 'Ġ身份证', 'Ġ杨', '洋', 'Ġ无', '经验', 'Ġ学士']
这里的
Ġ表示空格,BPE会把空格当成特殊的前缀处理。
第二步:建立字符和token的对应关系
这一步用tokenizer的char_to_token方法,把字符位置映射到token位置。
encoding = tokenizer(text, return_offsets_mapping=True)
offsets = encoding["offset_mapping"]
print(offsets)
输出:
[(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对应原文的哪个字符区间了。
第三步:初始化BIO标签
先给所有token都打上"O"标签(Outside,表示不属于任何实体)。
labels = ["O"] * len(offsets)
第四步:把实体的字符位置转换成token位置
对每个(start_char, end_char, label),找到对应的token下标。
for start_char, end_char, ent_label in entities:
token_start = None
token_end = None
for i, (start, end) in enumerate(offsets):
if start_char >= start and start_char < end:
token_start = i
if end_char > start and end_char <= end:
token_end = i
# BIO标注
if token_start is not None and token_end is not None:
labels[token_start] = "B-" + ent_label
for j in range(token_start + 1, token_end + 1):
labels[j] = "I-" + ent_label
第五步:看看最终结果
for tok, lab in zip(tokens, labels):
print(f"{tok:15} {lab}")
输出大概是这样:
女 B-GENDER
Ġ大 B-DEGREE
专 I-DEGREE
Ġxf B-EMAIL_ADDRESS
wang I-EMAIL_ADDRESS
@ I-EMAIL_ADDRESS
day I-EMAIL_ADDRESS
ee I-EMAIL_ADDRESS
. I-EMAIL_ADDRESS
com I-EMAIL_ADDRESS
Ġ身份证 O
Ġ杨 B-NAME
洋 I-NAME
Ġ无 B-YEARS_OF_WORK_EXPERIENCE
经验 I-YEARS_OF_WORK_EXPERIENCE
Ġ学士 B-DEGREE
总结一下整个流程
经过这么一番折腾,我们成功把:
- spaCy的字符级span标签
- 转换成了RoBERTa需要的token级BIO标签
关键就是用offset_mapping来对齐字符位置和token位置。这样就能把spaCy的(start, end, label)格式转成RoBERTa能直接用的**input_ids + labels**对了。
最终的效果大概是这样:
原文: 女 大专 xfwang@dayee.com 身份证 杨洋 无经验 学士
tokens: ['女', '大', '专', 'x', '##fw', '##ang', '@', 'day', '##ee', '.', 'com', '身', '份', '证', '杨', '洋', '无', '经', '验', '学', '士']
labels: ['B-GENDER', 'O', 'O', 'B-EMAIL_ADDRESS', 'I-EMAIL_ADDRESS', 'I-EMAIL_ADDRESS', 'I-EMAIL_ADDRESS', 'I-EMAIL_ADDRESS', 'I-EMAIL_ADDRESS', 'I-EMAIL_ADDRESS', 'I-EMAIL_ADDRESS', 'O', 'O', 'O', 'B-NAME', 'I-NAME', 'B-YEARS_OF_WORK_EXPERIENCE', 'I-YEARS_OF_WORK_EXPERIENCE', 'I-YEARS_OF_WORK_EXPERIENCE', 'B-DEGREE', 'I-DEGREE']
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 中加入组件时,需要在这里添加对应的块,例如:
[components.tagger]
factory = "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(最安全的改动)
from transformers import BertTokenizerFast, BertModel
tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")
model = BertModel.from_pretrained("bert-base-uncased")
# 假设我们要加入三个行业专有词
new_tokens = ["[MED]", "[FIN]", "[GEO]"] # 这里用方括号只是举例
num_added = tokenizer.add_tokens(new_tokens) # <-- 自动在词表末尾追加
print("新增 token 数量:", num_added) # 3
print("新词表大小:", len(tokenizer)) # 30523 (原来 30522)
# 对模型的 embedding 进行对应扩展
model.resize_token_embeddings(len(tokenizer))
# 现在可以直接 fine‑tune,新增 token 的 embedding 会是随机初始化
- 影响:
[MED]、[FIN]、[GEO]的 ID 会是原词表最后的三个(如 30522、30523、30524)。 - 好处:原有 token‑ID 保持不变,所有预训练权重仍然对应原来的向量,只有 新增行 需要在微调阶段学习。
3.2 删除 现有 token(不推荐)
# 假设你想把词表里 "the" 这个 token 删除
old_id = tokenizer.convert_tokens_to_ids("the")
# 直接删除会导致 ID 错位,模型会把原来 "the" 的 embedding 用在别的 token 上
- 后果:所有 ID > old_id 的 token 会向前平移 1 位,embedding 行全部错位,模型在每一步都会使用错误的向量,导致 几乎所有下游任务的性能崩溃。
- 唯一可行的做法:重新训练一个完整的 tokenizer(重新生成
vocab.txt),从头训练或 从头微调(使用随机初始化的 embedding),这等价于 换模型。
3.3 更改 特殊 token 的 ID(极度危险)
# 把 <pad> 的 ID 从 0 改成 5
tokenizer.pad_token = "[PAD]" # 确保 token 本身仍在词表中
tokenizer.pad_token_id = 5
model.config.pad_token_id = 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):
from transformers import AutoConfig, AutoModel, AutoModelForTokenClassification
model_name = "bert-base-uncased" # 只含 BERT
cfg = AutoConfig.from_pretrained(model_name)
print("architectures:", cfg.architectures) # ['BertModel']
print("num_labels:", getattr(cfg, "num_labels", None)) # None → 没有任务头
# 如果是已经微调好的 NER checkpoint
ner_name = "dslim/bert-base-NER"
cfg2 = AutoConfig.from_pretrained(ner_name)
print(cfg2.architectures) # ['BertForTokenClassification']
print("num_labels =", cfg2.num_labels) # e.g. 9 (B‑ORG, I‑ORG, …)
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 |
import torch, os
state = torch.load("pytorch_model.bin", map_location="cpu")
print([k for k in state.keys() "classifier" in k]) # → ['classifier.weight', 'classifier.bias']
如果 只看到 embeddings.*, encoder.*,没有 classifier、qa_outputs、lm_head,说明 没有任务头。
2.3 看 config.json 中的 special fields
{
"architectures": ["BertForTokenClassification"],
"id2label": {"0":"O","1":"B-PER","2":"I-PER", ...},
"label2id": {"O":0,"B-PER":1, ...},
"num_labels": 9
}
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_gradfor name, param in model.named_parameters(): if param.requires_grad: print(name) # 只会列出被训练的层- 若只看到
classifier.*(或qa_outputs.*)被requires_grad=True,说明 BERT 本体被冻结,只有头在训练。 - 若所有
encoder.layer.*也在列表中,说明 全模型微调。
- 若只看到
输出张量的形状
outputs = 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.configprint(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):
[components.transformer]
factory = "transformer"
model.name = "bert-base-uncased" # 只加载 BERT 本体
[components.ner]
factory = "ner"
model.hidden_width = 512 # MLP 宽度(不影响 BERT 输出维度)
extra_state_tokens = false
nO = null # 自动取 transformer.hidden_size (768)
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:
from transformers import AutoModelForTokenClassification, AutoTokenizer
model = AutoModelForTokenClassification.from_pretrained(
"bert-base-uncased",
num_labels=9, # 你的实体类别数
id2label={0:"O",1:"B-PER",2:"I-PER",...},
label2id={v:k for k,v in id2label.items()}
)
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
- 这里
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 头(推荐)
- 加载模型
from 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)from 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(去掉语言模型头)
[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)python -m spacy init config ernie_ner.cfg \ --lang zh \ --pipeline transformer,ner \ --optimize efficiency - 微调 NER(和前面示例的 Longformer、BigBird 等一样)
python -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)
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
import torch
model_name = "your_org/ernie4_5_causal" # 如 "bert-base-zh" 等
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16, # 取决于硬件,CPU 可用 float32
device_map="auto"
)
5.2 添加 Token‑Classification 头(用于 NER)
num_labels = 5 # 示例:O, B-PER, I-PER, B-LOC, I-LOC
config = AutoConfig.from_pretrained(
model_name,
num_labels=num_labels,
is_encoder_decoder=False,
# 以下参数保持原模型结构
hidden_size=1024,
num_hidden_layers=18,
num_attention_heads=16,
# ...
)
# 用同一权重初始化 token‑classification 模型
from transformers import AutoModelForTokenClassification
ner_model = AutoModelForTokenClassification.from_pretrained(
model_name,
config=config,
)
5.3 使用 Trainer 进行微调(示例)
from datasets import load_dataset
from transformers import DataCollatorForTokenClassification, Trainer, TrainingArguments
# 假设已有 CoNLL‑style 数据集
dataset = load_dataset("conll2003", split="train")
def tokenize_and_align_labels(example):
tokenized = tokenizer(example["tokens"], is_split_into_words=True, truncation=True, max_length=4096)
# 这里需要把 word‑level 标签映射到 sub‑word
# ...
return tokenized
train_dataset = dataset.map(tokenize_and_align_examples, batched=True)
training_args = TrainingArguments(
output_dir="./ner_ernie",
per_device_train_batch_size=8,
gradient_accumulation_steps=2,
learning_rate=5e-5,
num_train_epochs=3,
fp16=True, # 若支持 bfloat16,可改为 bf16
eval_strategy="steps",
eval_steps=500,
logging_steps=100,
)
trainer = Trainer(
model=ner_model,
args=training_args,
train_dataset=train_dataset,
data_collator=DataCollatorForTokenClassification(tokenizer),
)
trainer.train()
5.4 在 spaCy 中使用(仅 Encoder)
# big_ernie.cfg
[components.transformer]
factory = "transformer"
max_batch_items = 8192
[components.transformer.model]
@architectures = "spacy-transformers.TransformerModel.v3"
name = "your_org/ernie4_5_causal"
tokenizer_config = {"use_fast": true}
extra_layers = [] # 只保留 encoder
[components.ner]
factory = "ner"
然后:
python -m spacy train big_ernie.cfg \
--paths.train ./train.spacy \
--paths.dev ./dev.spacy \
--output ./output_ernie
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,尤其适合非常长的文本处理任务。