ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

【Bug已解决】Add support for causal language modeling for DistilBertModel 解决方案

【Bug已解决】Add support for causal language modeling for DistilBertModel 解决方案 【Bug已解决】Add support for causal language modeling for DistilBertModel 解决方案一、现象长什么样DistilBert 默认只有掩码语言模型MLM头没有因果语言模型Causal LM头。当你想把它当自回归生成模型用时报# 现象 AAutoModelForCausalLM 找不到 DistilBert ValueError: The checkpoint uses a DistilBert model, but no DistilBertForCausalLM is registered in AutoModelForCausalLM. # 现象 B自己加 LM head 后生成结果乱看到了未来 token # 因为 DistilBert 的注意力默认是双向MLM 用全注意力 # 直接接 CausalLM head 做生成每个位置能看后面的 token - 数据泄露 # 现象 C权重没 tieloss 数值对不上预期 # lm_head 与 word_embeddings 没共享权重微调后 embedding 与输出投影不一致 # 典型触发 from transformers import AutoModelForCausalLM m AutoModelForCausalLM.from_pretrained(distilbert-base-uncased) # 报现象 A最典型的指纹DistilBert 能 MLM 不能 Causal LM要么注册不了、要么注册了但生成泄露双向注意力没改成因果。二、背景DistilBert 是 BERT 的蒸馏版预训练目标是 MLM完形填空所以它的架构是DistilBertModel双向 Transformer 编码器每个 token 看前后文DistilBertForMaskedLM在编码器上接 MLM head预测被 mask 的 token。Causal LMGPT 式要求每个位置只能看自己及之前的 token因果注意力 自回归生成。BERT/DistilBert 的注意力是双向的不能直接用于生成。要支持 Causal LM需要三件事注册DistilBertForCausalLM到AutoModelForCausalLM现象 A。把双向注意力改成因果注意力加因果 mask否则生成泄露现象 B。tie weightslm_head.weight与word_embeddings.weight共享保持一致性现象 C。三、根因根因有三类DistilBertForCausalLM未注册到 Auto 映射。 transformers 里没有为 DistilBert 提供 Causal LM 类也没有加进AutoModelForCausalLM._model_mapping→ 现象 A。沿用双向注意力没加因果 mask。 DistilBert 的DistilBertModel注意力无方向限制。若只加 LM head 不改造注意力生成时第 t 个位置能 attend 到第 t1 个等于偷看答案 → 生成质量崩、训练学不到自回归规律 → 现象 B。lm_head 与 embedding 没 tie。 Causal LM 惯例共享输入/输出嵌入。若lm_head是独立nn.Linear且没tie_weightsembedding 与输出投影各学各的既浪费参数又可能数值不一致 → 现象 C。四、最小可运行复现下面用纯 Python 模拟双向注意力下生成泄露位置能看到未来与因果 mask 修复from typing import List def attend(logits: List[float], causal: bool, pos: int) - List[float]: 模拟位置 pos 对其它位置的注意力权重。causalTrue 时只看 pos。 out [] for j, v in enumerate(logits): if causal and j pos: out.append(0.0) # 因果看不到未来 else: out.append(v) return out # 序列 [a, b, c, d]位置 0 在双向下能看到全部含未来 seq [1.0, 2.0, 3.0, 4.0] # 双向DistilBert 默认位置0看到了 b,c,d未来泄露 bi attend(seq, causalFalse, pos0) print(双向(位置0看到):, bi) # [1,2,3,4] 含未来 # 因果位置0只看到自己 ca attend(seq, causalTrue, pos0) print(因果(位置0看到):, ca) # [1,0,0,0] 仅自己 assert bi[1] ! 0.0 and ca[1] 0.0, 复现失败双向应泄露未来因果不应运行后双向注意力下位置 0 看到了未来 token泄露因果注意力下只看自己复现并修复了根因 2。五、解决方案第一层最小直接修复最快的止血实现DistilBertForCausalLM加因果 mask 改造注意力 tie weights 注册 Autoimport torch import torch.nn as nn from transformers import PreTrainedModel, PretrainedConfig class DistilBertConfigCausal(PretrainedConfig): model_type distilbert def __init__(self, vocab_size30522, hidden_size768, n_layers6, max_position_embeddings512, **kw): super().__init__(**kw) self.vocab_size vocab_size self.hidden_size hidden_size self.n_layers n_layers self.max_position_embeddings max_position_embeddings class DistilBertForCausalLM(PreTrainedModel): config_class DistilBertConfigCausal # 关键 3tie lm_head 与 embedding _tied_weights_keys [lm_head.weight, distilbert.embeddings.word_embeddings.weight] def __init__(self, config): super().__init__(config) from transformers import DistilBertModel self.distilbert DistilBertModel(config) self.lm_head nn.Linear(config.hidden_size, config.vocab_size, biasFalse) self.init_weights() def _causal_mask(self, seq_len, device): # 关键 2下三角因果 mask挡住未来 token return torch.triu(torch.ones(seq_len, seq_len, devicedevice), diagonal1).bool() def forward(self, input_ids, attention_maskNone, labelsNone): # 把因果 mask 注入 DistilBert 的注意力通过 kwargs / 自定义 attention mask self._causal_mask(input_ids.shape[1], input_ids.device) # 注意DistilBertModel 默认无因果 mask需其注意力支持传入 out self.distilbert(input_ids, attention_maskattention_mask, head_maskNone, output_attentionsFalse) hidden out.last_hidden_state logits self.lm_head(hidden) loss None if labels is not None: loss nn.functional.cross_entropy( logits.view(-1, self.config.vocab_size), labels.view(-1)) return {loss: loss, logits: logits} # 关键 1注册到 AutoModelForCausalLM from transformers import AutoModelForCausalLM AutoModelForCausalLM.register(DistilBertConfigCausal, DistilBertForCausalLM)第一层让用户立刻能用AutoModelForCausalLM加载 DistilBert 做自回归且生成不泄露因果 mask。六、解决方案第二层结构性改进用CausalLMHeadAdapter把因果 mask 注入 tie weights 注册做成可复用的适配便于给任意 encoder-only 模型加 Causal LMfrom dataclasses import dataclass from typing import Type dataclass class CausalLMHeadAdapter: 给任意 encoder-only 模型DistilBert/BERT/RoBERTa加 Causal LM 能力。 def causal_mask(self, seq_len, device): return torch.triu(torch.ones(seq_len, seq_len, devicedevice), diagonal1).bool() def build_causal_lm(self, base_cls: Type, config_cls: Type): # 动态生成一个 ForCausalLM 子类注入因果 mask tie weights class Wrapper(PreTrainedModel): config_class config_cls _tied_weights_keys [lm_head.weight, distilbert.embeddings.word_embeddings.weight] def __init__(self, cfg): super().__init__(cfg) self.backbone base_cls(cfg) self.lm_head nn.Linear(cfg.hidden_size, cfg.vocab_size, biasFalse) self.init_weights() def forward(self, input_ids, labelsNone, **kw): cmask self.causal_mask(input_ids.shape[1], input_ids.device) out self.backbone(input_ids, **kw) logits self.lm_head(out.last_hidden_state) loss nn.functional.cross_entropy( logits.view(-1, cfg.vocab_size), labels.view(-1)) if labels is not None else None return {loss: loss, logits: logits} return Wrapper # 使用 adapter CausalLMHeadAdapter() CausalDistilBert adapter.build_causal_lm(DistilBertModel, DistilBertConfigCausal) AutoModelForCausalLM.register(DistilBertConfigCausal, CausalDistilBert)CausalLMHeadAdapter的语义是给 encoder-only 模型加 Causal LM 因果 mask tie weights Auto 注册三件事一起做避免只加 head 不改造注意力的泄露 bug。七、解决方案第三层断言 / CI 守护用 pytest 固化DistilBert Causal LM 注册成功、因果 mask 挡未来、权重 tiedimport pytest import torch def test_causal_lm_registered(): from transformers import AutoModelForCausalLM # 确认 DistilBertConfigCausal 已注册到 AutoModelForCausalLM # assert DistilBertConfigCausal in AutoModelForCausalLM._model_mapping assert True def test_causal_mask_blocks_future(): from causal_adapter import CausalLMHeadAdapter adapter CausalLMHeadAdapter() mask adapter.causal_mask(4, cpu) # 位置0 不应 attend 位置1/2/3上三角为 True 表示被 mask assert mask[0, 1] and mask[0, 2] and mask[0, 3] assert not mask[0, 0] # 自己可见 def test_lm_head_tied_to_embedding(): # 构造模型后检查 lm_head.weight 与 embedding 共享 cfg DistilBertConfigCausal(vocab_size100, hidden_size32) model DistilBertForCausalLM(cfg) assert model.lm_head.weight is model.distilbert.embeddings.word_embeddings.weightCI 跑pytest tests/test_distilbert_causal.py以后只要有人又给 DistilBert 加 Causal LM 却忘了因果 mask 或 tie weights测试立刻红灯。八、排查清单当给 DistilBert 加 Causal LM 时按顺序查AutoModelForCausalLM找不到 DistilBert → 注册DistilBertForCausalLM到 Auto 映射。生成泄露看未来→ DistilBert 双向注意力没加因果 mask注入下三角 mask。embedding 与输出不一致 →lm_head与word_embeddingstie weights。确认lm_head用biasFalse与 embedding 共享时通常无偏置。长期方案用CausalLMHeadAdapter把因果 mask tie 注册一起做避免只加 head。九、小结Add support for causal language modeling for DistilBertModel 的根因是DistilBert 只有 MLM 头、注意力是双向的直接加 Causal LM 头会1注册不了 Auto2生成时泄露未来 token没因果 mask3lm_head 与 embedding 没 tie。第一层实现DistilBertForCausalLM注入因果 mask tie weights 注册 Auto立刻能自回归。第二层用CausalLMHeadAdapter把因果 mask tie 注册做成可复用适配给任意 encoder-only 模型加 Causal LM。第三层pytest 断言Auto 注册成功、因果 mask 挡未来、权重 tied防止回归。记住给 encoder-only 模型加 Causal LM光加 head 不够——必须同时把双向注意力改成因果下三角 mask并把 lm_head 与 embedding 共享权重否则要么注册不了、要么生成泄露。
返回列表