PEFT documentation

Modèles personnalisés

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

and get access to the augmented documentation experience

to get started

Modèles personnalisés

Certaines techniques de fine-tuning, telles que le prompt tuning, sont spécifiques aux modèles de langage. Cela signifie que dans 🤗 PEFT, on suppose qu’un modèle 🤗 Transformers est utilisé. Cependant, d’autres techniques de fine-tuning, comme LoRA, ne sont pas limitées à des types de modèles spécifiques.

Dans ce guide, nous verrons comment appliquer LoRA à un perceptron multicouche, à un modèle de vision par ordinateur de la bibliothèque timm ou à une nouvelle architecture de 🤗 Transformers.

Perceptron multicouche

Supposons que nous voulons effectuer un fine-tuning d'un perceptron multicouche avec LoRA. Voici la définition :

from torch import nn


class MLP(nn.Module):
    def __init__(self, num_units_hidden=2000):
        super().__init__()
        self.seq = nn.Sequential(
            nn.Linear(20, num_units_hidden),
            nn.ReLU(),
            nn.Linear(num_units_hidden, num_units_hidden),
            nn.ReLU(),
            nn.Linear(num_units_hidden, 2),
            nn.LogSoftmax(dim=-1),
        )

    def forward(self, X):
        return self.seq(X)

Il s’agit d’un perceptron multicouche simple, composé d’une couche d’entrée, d’une couche cachée et d’une couche de sortie.

Pour cet exemple simplifié, nous choisissons un nombre de neurones cachés extrêmement élevé afin de mettre en évidence les gains d'efficacité apportés par PEFT, mais ces gains sont cohérents avec des exemples plus réalistes.

Ce modèle comporte plusieurs couches linéaires qui peuvent être ajustées avec LoRA. Lors de l'utilisation de modèles courants de 🤗 Transformers, PEFT sait quelles couches appliquer à LoRA, mais dans ce cas, c'est à nous, en tant qu'utilisateurs, de choisir les couches. Pour déterminer les noms des couches à ajuster :

print([(n, type(m)) for n, m in MLP().named_modules()])

Cela devrait afficher :

[('', __main__.MLP),
 ('seq', torch.nn.modules.container.Sequential),
 ('seq.0', torch.nn.modules.linear.Linear),
 ('seq.1', torch.nn.modules.activation.ReLU),
 ('seq.2', torch.nn.modules.linear.Linear),
 ('seq.3', torch.nn.modules.activation.ReLU),
 ('seq.4', torch.nn.modules.linear.Linear),
 ('seq.5', torch.nn.modules.activation.LogSoftmax)]

Supposons que nous voulions appliquer LoRA à la couche d’entrée et à la couche cachée, qui sont 'seq.0' et 'seq.2'. De plus, supposons que nous voulions mettre à jour la couche de sortie sans LoRA, ce qui correspondrait à 'seq.4'. La configuration correspondante serait :

from peft import LoraConfig

config = LoraConfig(
    target_modules=["seq.0", "seq.2"],
    modules_to_save=["seq.4"],
)

Avec cela, nous pouvons créer notre modèle PEFT et vérifier la fraction de paramètres entraînés :

from peft import get_peft_model

model = MLP()
peft_model = get_peft_model(model, config)
peft_model.print_healthcheck()

Enfin, nous pouvons utiliser n'importe quel framework d'entraînement de notre choix, ou écrire notre propre boucle d'entraînement, pour entraîner le peft_model.

Pour un exemple complet, consultez ce notebook.

modèles timm

La bibliothèque timm contient un grand nombre de modèles de vision par ordinateur pré-entraînés. Ces modèles peuvent également être affinés avec PEFT. Voyons comment cela fonctionne en pratique.

Pour commencer, assurez-vous que timm est installé dans l'environnement Python :

python -m pip install -U timm

Nous chargeons ensuite un modèle timm pour une tâche de classification d'images :

import timm

num_classes = ...
model_id = "timm/poolformer_m36.sail_in1k"
model = timm.create_model(model_id, pretrained=True, num_classes=num_classes)

Encore une fois, nous devons décider sur quelles couches appliquer LoRA. Étant donné que LoRA prend en charge les couches de convolution 2D et que celles-ci constituent un élément fondamental de ce modèle, nous devrions appliquer LoRA aux couches de convolution 2D. Pour identifier les noms de ces couches, examinons tous les noms de couches :

print([(n, type(m)) for n, m in model.named_modules()])

Cela affichera une liste très longue, nous n’en montrerons que les premières entrées :

[('', timm.models.metaformer.MetaFormer),
 ('stem', timm.models.metaformer.Stem),
 ('stem.conv', torch.nn.modules.conv.Conv2d),
 ('stem.norm', torch.nn.modules.linear.Identity),
 ('stages', torch.nn.modules.container.Sequential),
 ('stages.0', timm.models.metaformer.MetaFormerStage),
 ('stages.0.downsample', torch.nn.modules.linear.Identity),
 ('stages.0.blocks', torch.nn.modules.container.Sequential),
 ('stages.0.blocks.0', timm.models.metaformer.MetaFormerBlock),
 ('stages.0.blocks.0.norm1', timm.layers.norm.GroupNorm1),
 ('stages.0.blocks.0.token_mixer', timm.models.metaformer.Pooling),
 ('stages.0.blocks.0.token_mixer.pool', torch.nn.modules.pooling.AvgPool2d),
 ('stages.0.blocks.0.drop_path1', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.0.layer_scale1', timm.models.metaformer.Scale),
 ('stages.0.blocks.0.res_scale1', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.0.norm2', timm.layers.norm.GroupNorm1),
 ('stages.0.blocks.0.mlp', timm.layers.mlp.Mlp),
 ('stages.0.blocks.0.mlp.fc1', torch.nn.modules.conv.Conv2d),
 ('stages.0.blocks.0.mlp.act', torch.nn.modules.activation.GELU),
 ('stages.0.blocks.0.mlp.drop1', torch.nn.modules.dropout.Dropout),
 ('stages.0.blocks.0.mlp.norm', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.0.mlp.fc2', torch.nn.modules.conv.Conv2d),
 ('stages.0.blocks.0.mlp.drop2', torch.nn.modules.dropout.Dropout),
 ('stages.0.blocks.0.drop_path2', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.0.layer_scale2', timm.models.metaformer.Scale),
 ('stages.0.blocks.0.res_scale2', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.1', timm.models.metaformer.MetaFormerBlock),
 ('stages.0.blocks.1.norm1', timm.layers.norm.GroupNorm1),
 ('stages.0.blocks.1.token_mixer', timm.models.metaformer.Pooling),
 ('stages.0.blocks.1.token_mixer.pool', torch.nn.modules.pooling.AvgPool2d),
 ...
 ('head.global_pool.flatten', torch.nn.modules.linear.Identity),
 ('head.norm', timm.layers.norm.LayerNorm2d),
 ('head.flatten', torch.nn.modules.flatten.Flatten),
 ('head.drop', torch.nn.modules.linear.Identity),
 ('head.fc', torch.nn.modules.linear.Linear)]
 ]

En examinant de plus près, nous constatons que les couches de convolution 2D portent des noms tels que "stages.0.blocks.0.mlp.fc1" et "stages.0.blocks.0.mlp.fc2". Comment correspondre spécifiquement à ces noms de couches ? Vous pouvez écrire une expression régulière pour correspondre aux noms des couches. Dans notre cas, l'expression régulière r".*\.mlp\.fc\d" devrait faire l'affaire.

De plus, comme dans le premier exemple, nous devons nous assurer que la couche de sortie, en l’occurrence la tête de classification, est également mise à jour. En examinant la fin de la liste affichée ci-dessus, nous pouvons voir qu’elle est nommée 'head.fc'. À cet égard, voici notre configuration LoRA :

config = LoraConfig(target_modules=r".*\.mlp\.fc\d", modules_to_save=["head.fc"])

Il ne nous reste alors qu'à créer le modèle PEFT en passant notre modèle de base et la configuration à get_peft_model :

peft_model = get_peft_model(model, config)
peft_model.print_healthcheck()

Cela nous montre que nous n'avons besoin d'entraîner que moins de 2 % de tous les paramètres, ce qui représente un gain d'efficacité considérable.

Pour un exemple complet, consultez ce notebook.

Nouvelles architectures transformers

Lors de la sortie de nouvelles architectures transformers populaires, nous faisons de notre mieux pour les ajouter rapidement à PEFT. Si vous rencontrez un modèle transformers qui n’est pas pris en charge nativement, ne vous inquiétez pas, il fonctionnera très probablement si la configuration est correctement définie. Plus précisément, vous devez identifier les couches qui doivent être adaptées et les définir correctement lors de l’initialisation de la classe de configuration correspondante, par exemple LoraConfig. Voici quelques conseils pour vous y aider.

En première étape, il est judicieux de consulter les modèles existants pour s’inspirer. Vous pouvez les trouver dans constants.py du dépôt PEFT. Souvent, vous y trouverez une architecture similaire qui utilise les mêmes noms. Par exemple, si l’architecture du nouveau modèle est une variante du modèle « mistral » et que vous souhaitez appliquer LoRA, vous pouvez constater que l’entrée « mistral » dans TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING contient ["q_proj", "v_proj"]. Cela vous indique que, pour les modèles « mistral », les target_modules pour LoRA doivent être ["q_proj", "v_proj"] :

from peft import LoraConfig, get_peft_model

my_mistral_model = ...
config = LoraConfig(
    target_modules=["q_proj", "v_proj"],
    ...,  # other LoRA arguments
)
peft_model = get_peft_model(my_mistral_model, config)

Si cela ne suffit pas, vérifiez les modules existants de l’architecture de votre modèle à l’aide de la méthode named_modules et essayez d’identifier les couches d’attention, en particulier les couches clé, requête et valeur. Celles-ci portent souvent des noms tels que c_attn, query, q_proj, etc. La couche clé n’est pas toujours adaptée, et il est idéal de vérifier si son inclusion entraîne de meilleures performances.

De plus, les couches linéaires sont des cibles courantes pour l’adaptation (par exemple, dans l’article QLoRA, les auteurs suggèrent de les adapter également). Leurs noms contiennent souvent les chaînes fc ou dense.

Si vous souhaitez ajouter un nouveau modèle à PEFT, veuillez créer une entrée dans constants.py et ouvrir une pull request sur le dépôt. N’oubliez pas de mettre à jour le README également.

Vérifier les paramètres et les couches

Vous pouvez vérifier que vous avez correctement appliqué une méthode PEFT à votre modèle de plusieurs façons.

  • Exécutez print_healthcheck() pour inspecter la fraction des paramètres entraînables et l'état de l'adaptateur. Si la fraction de paramètres est inférieure ou supérieure à la valeur attendue, vérifiez la repr du modèle en l'imprimant. Cela affiche les noms de tous les types de couches du modèle. Assurez-vous que seules les couches cibles prévues sont remplacées par les couches d'adaptateur. Par exemple, si LoRA est appliqué aux couches nn.Linear, vous ne devriez voir que des couches lora.Linear utilisées.
peft_model.print_healthcheck()
  • Une autre manière de visualiser les couches adaptées consiste à utiliser l'attribut targeted_module_names pour lister le nom de chaque module qui a été adapté.
print(peft_model.targeted_module_names)

Types de modules non pris en charge

Des méthodes comme LoRA ne fonctionnent que si les modules cibles sont pris en charge par PEFT. Par exemple, il est possible d’appliquer LoRA aux couches nn.Linear et nn.Conv2d, mais pas, par exemple, à nn.LSTM. Si vous constatez qu’une classe de couche à laquelle vous souhaitez appliquer PEFT n’est pas prise en charge, vous pouvez :

  • définir une correspondance personnalisée pour dispatcher dynamiquement des modules personnalisés dans LoRA
  • ouvrir un issue et demander la fonctionnalité, les mainteneurs l’implémenteront ou vous guideront pour l’implémenter vous-même si la demande pour ce type de module est suffisamment élevée

Prise en charge expérimentale de la distribution dynamique de modules personnalisés dans LoRA

Cette fonctionnalité est expérimentale et susceptible d'évoluer, en fonction de son accueil par la communauté. Nous introduirons une API publique et stable si la demande est significative.

PEFT prend en charge une API expérimentale pour les types de modules personnalisés pour LoRA. Supposons que vous disposiez d’une implémentation de LoRA pour les LSTM. Normalement, vous ne pourriez pas indiquer à PEFT de l’utiliser, même si elle fonctionnerait théoriquement avec PEFT. Cependant, cela est possible grâce à la distribution dynamique des couches personnalisées.

L'API expérimentale ressemble actuellement à ceci :

class MyLoraLSTMLayer:
    ...

base_model = ...  # load the base model that uses LSTMs

# add the LSTM layer names to target_modules
config = LoraConfig(..., target_modules=["lstm"])
# define a mapping from base layer type to LoRA layer type
custom_module_mapping = {nn.LSTM: MyLoraLSTMLayer}
# register the new mapping
config._register_custom_module(custom_module_mapping)
# after registration, create the PEFT model
peft_model = get_peft_model(base_model, config)
# do training

Lorsque vous appelez get_peft_model(), un avertissement s’affiche car PEFT ne reconnaît pas le type de module ciblé. Dans ce cas, vous pouvez ignorer cet avertissement.

En fournissant une correspondance personnalisée, PEFT vérifie d’abord les couches du modèle de base par rapport à cette correspondance personnalisée et affecte le type de couche LoRA personnalisé s’il y a une correspondance. En l’absence de correspondance, PEFT vérifie les types de couches LoRA intégrés.

Par conséquent, cette fonctionnalité peut également être utilisée pour remplacer la logique de dispatch existante, par exemple si vous souhaitez utiliser votre propre couche LoRA pour nn.Linear au lieu de celle fournie par PEFT.

Lors de la création de votre module LoRA personnalisé, veuillez respecter les mêmes règles que les modules LoRA existants. Voici quelques contraintes importantes à prendre en compte :

  • Le module personnalisé doit hériter de nn.Module et de peft.tuners.lora.layer.LoraLayer.
  • La méthode __init__ du module personnalisé doit avoir les arguments positionnels base_layer et adapter_name. Ensuite, il y a des **kwargs supplémentaires que vous êtes libre d'utiliser ou d'ignorer.
  • Les paramètres apprenables doivent être stockés dans un nn.ModuleDict ou un nn.ParameterDict, où la clé correspond au nom de l'adaptateur spécifique (n'oubliez pas qu'un modèle peut avoir plus d'un adaptateur à la fois).
  • Le nom de ces attributs de paramètres apprenables doit commencer par "lora_", par exemple self.lora_new_param = ....
  • Certaines méthodes sont facultatives, par exemple vous n'avez besoin d'implémenter merge et unmerge que si vous souhaitez prendre en charge la fusion de poids.

Actuellement, les informations relatives au module personnalisé ne sont pas conservées lors de l'enregistrement du modèle. Lors du chargement du modèle, vous devez enregistrer à nouveau les modules personnalisés.

# saving works as always and includes the parameters of the custom modules
peft_model.save_pretrained(<model-path>)

# loading the model later:
base_model = ...
# load the LoRA config that you saved earlier
config = LoraConfig.from_pretrained(<model-path>)
# register the custom module again, the same way as the first time
custom_module_mapping = {nn.LSTM: MyLoraLSTMLayer}
config._register_custom_module(custom_module_mapping)
# pass the config instance to from_pretrained:
peft_model = PeftModel.from_pretrained(model, tmp_path / "lora-custom-module", config=config)

Si vous utilisez cette fonctionnalité et la trouvez utile, ou si vous rencontrez des problèmes, faites-le-nous savoir en créant un issue ou une discussion sur GitHub. Cela nous permet d'estimer la demande pour cette fonctionnalité et d'ajouter une API publique si celle-ci est suffisamment élevée.

Update on GitHub