模块化变压器
transformers
是一个有主见的框架;我们的哲学在以下 概念指南 中定义。
该哲学的核心体现在库的单一模型,单一文件方面。这个组件的缺点是它限制了组件从文件到工具包中其他组件的继承和可导入性。
因此,模型组件往往在许多文件中重复出现。在transformers
中定义的注意力层数量与模型数量一样多,其中相当一部分是彼此相同的。不幸的后果是,随着修复和更改应用于代码的特定部分,独立的实现往往会分叉。
为了解决这个问题,我们在整个库中引入了“副本”的概念。通过添加注释表明代码是另一个代码的副本,我们可以通过CI和本地命令确保副本不会出现差异。然而,尽管复杂性较低,但这样做通常相当繁琐。
最后,这增加了贡献模型的显著开销,我们希望消除这种开销。 这种方法通常要求模型贡献者添加建模代码(约1千行)、处理器(约500行)、测试、文档等。 模型贡献的PR很少添加少于3-5千行代码,其中大部分代码是样板代码。
这提高了贡献的门槛,而通过模块化变压器,我们的目标是将门槛降低到一个更可接受的水平。
这是什么?
模块化Transformers引入了“模块化”文件的概念到模型文件夹中。这个模块化文件接受通常在建模/处理文件中不被接受的代码,因为它允许从邻近的模型导入以及类之间的继承。
这个模块化文件定义了模型、处理器和配置类,这些内容原本会在各自的模块中定义。
最后,此功能引入了一个新的linter
,它将“解构”模块化文件为“单一模型,单一文件”的目录结构。这些文件将在每次运行脚本时自动生成;减少对模块化文件的必要贡献,因此仅涉及贡献模型与其他模型之间的更改。
模型用户最终将导入并使用单文件接口,因此这里预计不会有变化。通过这样做,我们希望结合两者的优点:在坚持我们理念的同时,实现简单的贡献。
因此,这是对# Copied from
标记的替代,预计之前贡献的模型将在未来几个月内迁移到新的模块化Transformers格式。
详情
“linter”会解析继承关系并从模块化文件中创建所有单文件,同时尽量对Python用户透明地扁平化继承关系。目前,linter会扁平化单层继承。
例如:
- 如果一个配置类继承自另一个类并添加/删除一个参数,生成的文件将直接引用它(在添加的情况下)或完全删除它(在删除的情况下)。
- 如果一个类继承自另一个类,例如:class GemmaModel(LlamaModel):,依赖关系会自动推断。所有子模块将从超类中自动推断。
- 如果你在
modular
中定义了新函数并在类中使用它们,linter 将自动推断
你应该能够在这个modular
文件中编写所有内容(分词器、图像处理器、模型、配置),并且相应的文件将会为你创建。
执行
[待办] 我们正在引入一个新的测试,以确保生成的内容与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
)
# The RoBERTa config is identical to BERT's config
class RobertaConfig(BertConfig):
model_type = 'roberta'
# We redefine the embeddings here to highlight the padding ID difference, and we redefine the position embeddings
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
)
# The RoBERTa model is identical to the BERT model, except for the embedding layer.
# We redefine the embeddings above, so here there is no need to do additional work
class RobertaModel(BertModel):
def __init__(self, config):
super().__init__(config)
self.embeddings = RobertaEmbeddings(config)
# The heads now only need to redefine the model inside to the correct `RobertaModel`
class RobertaForMaskedLM(BertForMaskedLM):
def __init__(self, config):
super().__init__(config)
self.model = RobertaModel(config)
请注意,如果您不使用您定义的依赖项,将会出现以下错误:
ValueError: You defined `RobertaEmbeddings` in the modular_roberta.py, it should be used
when you define `BertModel`, as it is one of it's direct dependencies. Make sure
you use it in the `__init__` function.
此外,您可以在这里找到示例列表:
它不是什么
它并不是建模代码的替代品(目前还不是?),如果你的模型不是基于任何已经存在的东西,那么你可以像往常一样添加一个modeling
文件。
高级用法
移除属性和函数
要移除在模块化模型中未使用的属性,并且您不希望在这些展开的建模中看到的属性:
class GemmaModel(LlamaModel): | class GemmaModel(PreTrainedModel):
def __init__(self, config): | def __init__(self, config):
super().__init__(self, eos_token) | super().__init__(config)
del self.embed_tokens | 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
|
| # Initialize weights and apply final processing
| self.post_init()
如果你检查原始的 LlamaModel
,它有一个 embed_tokens
,在这里被移除了(正如你所期望的!)
删除一个函数非常相似,你只需要用raise ValueError("")
来编写它,以模拟你在Python中删除父函数时实际想要的行为。
class GemmaTokenizer(LlamaTokenizer):
...
def get_spm_processor(self):
raise AttributeError("Not needed for Gemma")
def unk_token_length(self):
raise AttributeError("Not needed for Gemma")
定义新函数
如果你在modular
文件中定义了一个新函数,以便在类内部使用,比如说
def my_new_function(*args, **kwargs):
# Do something here
pass
class GemmaModel(LlamaModel):
def forward(*args, **kwargs):
# Call the function
example = my_new_function(*args, **kwargs)
# continue here
the my_new_function
函数(以及递归地,在其主体中调用的任何其他新函数)将自动复制粘贴到使用它的文件中。
调用 super()
我们最近推出了一些功能,允许您从以下方面进行改进:
class GemmaTokenizer(LlamaTokenizer, PretrainedTokenizerFast): | class GemmaModel(nn.Module):
def __init__(self, eos_token="</s>"): | def __init__(self):
eos_token = AddedToken(eos_token) | eos_token = AddedToken(eos_token)
PretrainedTokenizerFast.__init__(self, eos_token) | super().__init__(eos_token)
这在您不想解开对super()
的调用时非常有用,并且您希望区分您正在进行的哪个超级初始化调用!
特殊命名
我们现在也支持特殊情况,例如
class GemmaVisionModel(CLIPModel):
pass
其中你的类名 GemmaVision
与模块 Gemma
不同。这对于复合模型非常有用。