模块化 Transformers¶
transformers 是一个有明确主张的框架;我们的理念在以下 概念指南 中定义。
这一理念的核心体现在库的“单模型、单文件”方面。这个组件的缺点是限制了从文件到工具包中其他部分的继承和导入。
因此,模型组件在许多文件中重复出现。transformers 中定义了与模型数量相同的关注层(attention layers),其中许多是相同的。不幸的后果是,当对特定部分的代码进行修复和更改时,独立实现往往会偏离。
为了平衡这个问题,我们在库中引入了“复制”的概念。通过添加一个注释表明代码是从另一个地方复制的,我们可以通过持续集成(CI)和本地命令来确保复制的代码不会偏离。虽然复杂度较低,但这通常是相当繁琐的。
最后,这增加了贡献模型的显著开销,这是我们希望消除的。这种方法通常需要模型贡献者添加建模代码(约1000行)、处理器(约500行)、测试、文档等。模型贡献的拉取请求(PR)很少少于3-5000行代码,其中大部分是样板代码。
这提高了贡献的门槛,而通过模块化 Transformers(Modular Transformers),我们旨在降低这个门槛到一个更可接受的点。
什么是模块化 Transformers?¶
模块化Transformers在模型文件夹中引入了“模块化”文件的概念。这个模块化文件接受通常不在建模/处理文件中接受的代码,因为它允许从相邻模型导入以及类之间的继承。
这个模块化文件定义了原本在各自模块中定义的模型、处理器和配置类。
最后,这个功能引入了一个新的 linter,它会将模块化文件“展开”为“单模型、单文件”目录结构。每次运行脚本时,这些文件都会自动生成;减少了对模块化文件的贡献,因此只需要更改贡献模型与其他模型之间的差异。
模型用户最终会导入并使用单文件接口,因此这里没有变化。这样做,我们希望结合两种方法的优点:简化贡献同时坚持我们的理念。
因此,这是对 # Copied from 标记的替代,并且预计以前贡献的模型将在未来的几个月内迁移到新的模块化格式。
细节¶
“linter”会展开继承关系,并从模块化文件创建所有单文件。在这个过程中,linter 会尽量对 Python 用户透明地展平继承关系。目前,linter 只展平 单层 继承。
例如:
- 如果一个配置类继承了另一个类并添加或删除了一个参数,生成的文件将直接引用它(在添加的情况下)或完全删除它(在删除的情况下)。
- 如果一个类继承了另一个类,例如
class GemmaModel(LlamaModel):,依赖关系将自动推断。所有子模块将从父类中自动推断。 - 如果你在
modular文件中定义了新函数并在类中使用它们,linter 将自动推断并复制这些函数。
你应该能够在 modular 文件中编写一切(分词器、图像处理器、模型、配置),相应的文件将为你自动生成。
强制执行¶
[TODO] 我们引入了一项新的测试,确保生成的内容与 modular_xxxx.py 中的内容匹配。
示例¶
这里是一个快速示例,展示 BERT 和 RoBERTa。这两个模型密切相关:它们的建模实现仅在嵌入层有所不同。
与其重新定义整个模型,以下是 modular_roberta.py 文件中建模和配置类的样子(为了示例,暂时忽略了分词器,因为它们非常不同)。
from torch import nn
from ..bert.configuration_bert import BertConfig
from ..bert.modeling_bert import (
BertModel,
BertEmbeddings,
BertForMaskedLM
)
# RoBERTa 配置与 BERT 的配置相同
class RobertaConfig(BertConfig):
model_type = 'roberta'
# 重新定义嵌入层以突出填充 ID 的差异,并重新定义位置嵌入
class RobertaEmbeddings(BertEmbeddings):
def __init__(self, config):
super().__init__(config)
self.padding_idx = config.pad_token_id
self.position_embeddings = nn.Embedding(
config.max_position_embeddings, config.hidden_size, padding_idx=self.padding_idx
)
# RoBERTa 模型与 BERT 模型相同,除了嵌入层。
# 我们在上面重新定义了嵌入层,因此这里不需要额外的工作
class RobertaModel(BertModel):
def __init__(self, config):
super().__init__(config)
self.embeddings = RobertaEmbeddings(config)
# 头部现在只需要将模型内部重新定义为正确的 `RobertaModel`
class RobertaForMaskedLM(BertForMaskedLM):
def __init__(self, config):
super().__init__(config)
self.model = RobertaModel(config)
如果你没有使用你定义的依赖关系,将会出现以下错误:
class GemmaModel(LlamaModel):
def __init__(self, config):
super().__init__(config)
del self.embed_tokens
Example 2: GemmaModel 类定义 (PreTrainedModel)¶
class GemmaModel(PreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.padding_idx = config.pad_token_id
self.vocab_size = config.vocab_size
self.layers = nn.ModuleList(
[LlamaDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
)
self.norm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.rotary_emb = LlamaRotaryEmbedding(config=config)
self.gradient_checkpointing = False
# 初始化权重并应用最终处理
self.post_init()
如果你检查原始的 LlamaModel,它有一个 embed_tokens 属性,这里将会被删除。
删除函数也很类似,只需将其写成包含 raise ValueError("") 来模拟在 Python 中删除父类函数的行为。
class GemmaTokenizer(LlamaTokenizer):
# ...
def get_spm_processor(self):
raise AttributeError("Gemma 不需要此功能")
def unk_token_length(self):
raise AttributeError("Gemma 不需要此功能")
定义新函数¶
如果你在 modular 文件中定义了一个新函数并在类中使用它,例如:
def my_new_function(*args, **kwargs):
# 在这里做某事
pass
class GemmaModel(LlamaModel):
def forward(*args, **kwargs):
# 调用函数
example = my_new_function(*args, **kwargs)
# 继续操作
class GemmaTokenizer(LlamaTokenizer, PretrainedTokenizerFast):
def __init__(self, eos_token="</s>"):
eos_token = AddedToken(eos_token)
PretrainedTokenizerFast.__init__(self, eos_token)
Part 2: GemmaModel 类定义¶
class GemmaModel(nn.Module):
def __init__(self):
eos_token = AddedToken(eos_token)
super().__init__(eos_token)
class GemmaVisionModel(CLIPModel):
pass
这里你的类名 GemmaVision 与模块化的 Gemma 不同。这对于复合模型非常有用。