Transformers documentation

自定义模型组件

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

自定义模型组件

自定义模型的另一种方式是修改其组件,而不是完全编写一个新模型,从而将模型调整到适合您的特定用例。例如,您可以添加新的层,或优化架构中的注意力机制。这些自定义直接应用于 Transformers 模型,因此您可以继续使用诸如 Trainer、PreTrainedModel 以及 PEFT 库等功能。

本指南将展示如何自定义模型的注意力机制,以便对其应用 低秩适应(LoRA)。

clear_import_cache 工具在迭代修改和开发模型代码时非常有用。它会清除所有已缓存的 Transformers 模块,使 Python 能够重新加载修改后的代码,而无需不断重启环境。

from transformers import AutoModel
from transformers.utils.import_utils import clear_import_cache

model = AutoModel.from_pretrained("bert-base-uncased")
# modifications to model code
# clear cache to reload modified code
clear_import_cache()
# re-import to use updated code
model = AutoModel.from_pretrained("bert-base-uncased")

Attention 类

Segment Anything 是一个图像分割模型,其注意力机制中使用了查询-键-值(qkv)投影。为了减少可训练参数数量和计算开销,您可以对 qkv 投影应用 LoRA。这需要拆分 qkv 投影,以便分别针对 q 和 v 应用 LoRA。

  1. 通过继承原始的 SamVisionAttention 类,创建一个自定义的注意力类 SamVisionAttentionSplit。在 __init__ 中,删除合并的 qkv,并为 q、k 和 v 分别创建独立的线性层。
import torch
import torch.nn as nn
from transformers.models.sam.modeling_sam import SamVisionAttention

class SamVisionAttentionSplit(SamVisionAttention, nn.Module):
    def __init__(self, config, window_size):
        super().__init__(config, window_size)
        # remove combined qkv
        del self.qkv
        # separate q, k, v projections
        self.q = nn.Linear(config.hidden_size, config.hidden_size, bias=config.qkv_bias)
        self.k = nn.Linear(config.hidden_size, config.hidden_size, bias=config.qkv_bias)
        self.v = nn.Linear(config.hidden_size, config.hidden_size, bias=config.qkv_bias)
        self._register_load_state_dict_pre_hook(self.split_q_k_v_load_hook)
  1. split_q_k_v_load_hook 函数在加载模型时,将预训练的 qkv 权重拆分为独立的 q、k 和 v 权重,以确保与任何预训练模型兼容。
    def split_q_k_v_load_hook(self, state_dict, prefix, *args):
        keys_to_delete = []
        for key in list(state_dict.keys()):
            if "qkv." in key:
                # split q, k, v from the combined projection
                q, k, v = state_dict[key].chunk(3, dim=0)
                # replace with individual q, k, v projections
                state_dict[key.replace("qkv.", "q.")] = q
                state_dict[key.replace("qkv.", "k.")] = k
                state_dict[key.replace("qkv.", "v.")] = v
                # mark the old qkv key for deletion
                keys_to_delete.append(key)
        
        # remove old qkv keys
        for key in keys_to_delete:
            del state_dict[key]
  1. 在 forward 前向传播中,q、k 和 v 是分别计算的,而注意力机制的其余部分保持不变。
    def forward(self, hidden_states: torch.Tensor, output_attentions=False) -> torch.Tensor:
        batch_size, height, width, _ = hidden_states.shape
        qkv_shapes = (batch_size *  self.num_attention_heads,  height * width, -1)
        query = self.q(hidden_states).reshape((batch_size,  height * width,self.num_attention_heads, -1)).permute(0,2,1,3).reshape(qkv_shapes)
        key = self.k(hidden_states).reshape((batch_size,  height * width,self.num_attention_heads, -1)).permute(0,2,1,3).reshape(qkv_shapes)
        value = self.v(hidden_states).reshape((batch_size,  height * width,self.num_attention_heads, -1)).permute(0,2,1,3).reshape(qkv_shapes)

        attn_weights = (query * self.scale) @ key.transpose(-2, -1)

        attn_weights = torch.nn.functional.softmax(attn_weights, dtype=torch.float32, dim=-1).to(query.dtype)
        attn_probs = nn.functional.dropout(attn_weights, p=self.dropout, training=self.training)
        attn_output = (attn_probs @ value).reshape(batch_size, self.num_attention_heads, height, width, -1)
        attn_output = attn_output.permute(0, 2, 3, 1, 4).reshape(batch_size, height, width, -1)
        attn_output = self.proj(attn_output)

        if output_attentions:
            outputs = (attn_output, attn_weights)
        else:
            outputs = (attn_output, None)
        return outputs

将自定义的 SamVisionAttentionSplit 类分配给原始模型的 SamVisionAttention 模块以将其替换。模型中所有 SamVisionAttention 的实例都会被替换为拆分注意力版本。

在调用 from_pretrained() 之前替换该类。加载钩子仅在检查点加载期间运行,因此在已加载的模型上替换模块会导致新的 q、k 和 v 层保持随机初始化状态。

from transformers import SamModel
from transformers.models.sam import modeling_sam

# replace the attention class the vision layers instantiate
modeling_sam.SAM_VISION_ATTENTION_CLASSES["eager"] = SamVisionAttentionSplit

# load the pretrained SAM model, the hook splits the qkv weights during loading
model = SamModel.from_pretrained("facebook/sam-vit-base", attn_implementation="eager")

LoRA

当 q、k 和 v 投影相互独立时,对 q 和 v 应用 LoRA。

创建一个 LoraConfig,并指定秩 r、lora_alpha、lora_dropout、task_type,以及最重要的,要针对的模块。

from peft import LoraConfig, get_peft_model

config = LoraConfig(
    r=16,
    lora_alpha=32,
    # apply LoRA to q and v
    target_modules=["q", "v"],
    lora_dropout=0.1,
    task_type="FEATURE_EXTRACTION"
)

将模型和 LoraConfig 传递给 get_peft_model,以将 LoRA 应用于该模型。

model = get_peft_model(model, config)

调用 print_trainable_parameters 以查看训练后的参数数量与总参数数量的对比。

model.print_trainable_parameters()
"trainable params: 589,824 || all params: 94,274,096 || trainable%: 0.6256"
Update on GitHub