PEFT documentation
カスタムモデル
カスタムモデル
プロンプトチューニングなどの一部のファインチューニング手法は、言語モデルに固有です。つまり、🤗 PEFTでは、🤗 Transformersのモデルが使用されていることが前提とされています。ただし、LoRAなどの他のファインチューニング手法は、特定のモデルタイプに限定されません。
このガイドでは、timmライブラリのコンピュータビジョンモデルや、新しい🤗 TransformersアーキテクチャにLoRAを適用する方法について説明します。
多層パーセプトロン
LoRAで多層パーセプトロンをファインチューニングしたいと仮定します。定義は以下の通りです。
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)これは、入力層、隠れ層、出力層を持つ、シンプルな多層パーセプトロンです。
このトイ例では、PEFTによる効率化の利点を強調するために、隠れユニットの数を極端に大きくしていますが、これらの利点はより現実的な例でも同様に得られます。
このモデルには、LoRAでチューニングできる線形層がいくつかあります。一般的な🤗 Transformersモデルを扱う場合、PEFTはLoRAを適用する層を自動的に判別しますが、この場合はユーザーである私たちが層を選択する必要があります。 チューニング対象の層の名前を特定するには:
print([(n, type(m)) for n, m in MLP().named_modules()])これにより、以下が出力されるはずです:
[('', __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)]LoRAを入力層と隠れ層に適用したいとします。これらは'seq.0'と'seq.2'です。さらに、LoRAなしで出力層を更新したいと仮定すると、それは'seq.4'になります。対応する設定は以下の通りです。
from peft import LoraConfig
config = LoraConfig(
target_modules=["seq.0", "seq.2"],
modules_to_save=["seq.4"],
)これにより、PEFTモデルを作成し、学習されるパラメータの割合を確認できます。
from peft import get_peft_model
model = MLP()
peft_model = get_peft_model(model, config)
peft_model.print_healthcheck()最後に、peft_modelを学習するために、お好みの学習フレームワークを使用したり、独自のfitループを作成したりできます。
完全な例については、このノートブックをご覧ください。
timmモデル
timm ライブラリには、多数の事前学習済みコンピュータビジョンモデルが含まれています。 これらは PEFT を使用してファインチューニングすることもできます。実際の動作を確認してみましょう。
まず、Python環境にtimmがインストールされていることを確認してください。
python -m pip install -U timm
次に、画像分類タスク用のtimmモデルを読み込みます:
import timm
num_classes = ...
model_id = "timm/poolformer_m36.sail_in1k"
model = timm.create_model(model_id, pretrained=True, num_classes=num_classes)繰り返しになりますが、LoRAを適用するレイヤーについて決定する必要があります。LoRAは2D畳み込みレイヤーをサポートしており、それらはこのモデルの主要な構成要素であるため、2D畳み込みレイヤーにLoRAを適用すべきです。それらのレイヤーの名前を特定するために、すべてのレイヤー名を確認しましょう:
print([(n, type(m)) for n, m in model.named_modules()])これは非常に長いリストを出力しますが、ここでは最初の数行のみを示します。
[('', 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)]
]よく見ると、2D 畳み込み層の名前は"stages.0.blocks.0.mlp.fc1"や"stages.0.blocks.0.mlp.fc2"のようなものになっています。これらの層名を具体的に一致させるにはどうすればよいでしょうか。正規表現を使って層名を一致させることができます。今回の場合、r".*\.mlp\.fc\d"という正規表現で対応できます。
さらに、最初の例と同様に、出力層、この場合は分類ヘッドも更新されることを確認する必要があります。上記で出力されたリストの末尾を見ると、'head.fc'という名前であることがわかります。これを踏まえて、LoRAの設定は以下のようになります。
config = LoraConfig(target_modules=r".*\.mlp\.fc\d", modules_to_save=["head.fc"])次に、ベースモデルと設定をget_peft_modelに渡すだけで、PEFTモデルを作成できます:
peft_model = get_peft_model(model, config) peft_model.print_healthcheck()
これにより、すべてのパラメータのうち2%未満しか学習する必要がないことが示されており、これは大きな効率の向上です。
完全な例については、このノートブックをご覧ください。
新しいTransformersアーキテクチャ
新しい人気のあるtransformersアーキテクチャがリリースされると、PEFTに迅速に追加できるよう最善を尽くしています。サポートされていないtransformersモデルに遭遇しても心配しないでください。設定が正しく行われていれば、おそらくそのまま動作します。具体的には、適応させるべきレイヤーを特定し、対応する設定クラス(例: LoraConfig)を初期化する際に正しく設定する必要があります。以下に、この作業を助けるヒントをいくつか示します。
最初のステップとして、既存のモデルを確認して参考にするのがおすすめです。PEFTリポジトリ内のconstants.pyにそれらが含まれています。多くの場合、同じ名前を使用している類似のアーキテクチャが見つかります。例えば、新しいモデルのアーキテクチャが「mistral」モデルのバリエーションであり、LoRAを適用したい場合、TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING内の「mistral」のエントリが["q_proj", "v_proj"]を含むことを確認できます。これは、「mistral」モデルの場合、LoRAのtarget_modulesは["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)それでも解決しない場合は、named_modules メソッドを使用してモデルアーキテクチャ内の既存モジュールを確認し、特にキー、クエリ、バリューのレイヤーを含むアテンションレイヤーを特定してください。これらのレイヤーには、c_attn、query、q_proj などの名前が付けられていることが多いです。キーレイヤーは常に適応されるわけではなく、理想的には、これを含めることでパフォーマンスが向上するかどうかを確認する必要があります。
さらに、線形層は適応の対象として一般的です(QLoRA論文では、著者らがこれらも適応することを提案しています)。その名前には、fcまたはdenseという文字列が含まれることがよくあります。
PEFT に新しいモデルを追加したい場合は、constants.py にエントリを作成し、リポジトリ でプルリクエストを開いてください。README も更新することを忘れないでください。
パラメータとレイヤーを検証する
PEFTメソッドがモデルに正しく適用されているかどうかは、いくつかの方法で確認できます。
- print_healthcheck()を実行して、学習可能なパラメータの割合とアダプタの状態を確認します。パラメータの割合が想定より低い、または高い場合は、モデルを出力して
reprを確認してください。これにより、モデル内のすべてのレイヤータイプの名前が表示されます。意図したターゲットレイヤーのみがアダプタレイヤーに置き換えられていることを確認してください。例えば、LoRAをnn.Linearレイヤーに適用している場合、使用されているのはlora.Linearレイヤーのみであるはずです。
peft_model.print_healthcheck()
- アダプテーションされたレイヤーを確認する別の方法として、
targeted_module_names属性を使用して、アダプテーションされた各モジュールの名前をリスト表示できます。
print(peft_model.targeted_module_names)サポートされていないモジュールタイプ
LoRAなどの手法は、対象モジュールがPEFTによってサポートされている場合にのみ機能します。例えば、nn.Linearやnn.Conv2dレイヤーにはLoRAを適用できますが、nn.LSTMには適用できません。PEFTを適用したいレイヤーのクラスがサポートされていない場合は、次のことができます。
- LoRAでカスタムモジュールを動的にディスパッチするためのカスタムマッピングを定義します
- issue を作成し、機能の追加を依頼してください。このモジュールタイプへの需要が十分に高い場合、メンテナーが実装するか、ご自身で実装する方法について案内します。
LoRAにおけるカスタムモジュールの動的ディスパッチの実験的サポート
この機能は実験的なものであり、コミュニティからの反応に応じて変更される可能性があります。需要が大きい場合、公開かつ安定したAPIを導入します。
PEFTは、LoRAのカスタムモジュールタイプ用の実験的なAPIをサポートしています。LSTM用のLoRA実装があると仮定します。通常、理論上PEFTと互換性がある場合でも、PEFTにその実装を使用するように指示することはできません。しかし、カスタムレイヤーの動的ディスパッチにより、これが可能になります。
実験的なAPIは現在、以下のようになっています:
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 trainingget_peft_model()を呼び出すと、PEFTが対象のモジュールタイプを認識できないため、警告が表示されます。この場合、この警告は無視して問題ありません。
カスタムマッピングを指定すると、PEFT はまずベースモデルのレイヤーをカスタムマッピングと照合し、一致がある場合はカスタムの LoRA レイヤー型にディスパッチします。一致しない場合、PEFT は組み込みの LoRA レイヤー型を照合します。
したがって、この機能は既存のディスパッチロジックの上書きにも使用できます。例えば、PEFTが提供するものではなく、独自のLoRAレイヤーをnn.Linearに使用したい場合などに利用できます。
カスタムLoRAモジュールを作成する際は、既存のLoRAモジュールと同じルールに従ってください。考慮すべき重要な制約事項は以下の通りです:
- カスタムモジュールは
nn.Moduleとpeft.tuners.lora.layer.LoraLayerを継承する必要があります。 - カスタムモジュールの
__init__メソッドには、位置引数としてbase_layerとadapter_nameが必要です。その後、自由に使用または無視できる追加の**kwargsが続きます。 - 学習可能なパラメータは、
nn.ModuleDictまたはnn.ParameterDictに保存されるべきです。ここで、キーは特定のアダプタの名前に対応します(モデルには同時に複数のアダプタが存在し得ることを忘れないでください)。 - これらの学習可能なパラメータ属性の名前は
"lora_"で始まる必要があります。例えば、self.lora_new_param = ...のようにします。 - 一部のメソッドは任意です。例えば、重みのマージをサポートしたい場合は、
mergeとunmergeを実装するだけで構いません。
現在、モデルを保存した際にカスタムモジュールに関する情報が保持されません。モデルを読み込む際は、カスタムモジュールを再度登録する必要があります。
# 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)この機能を使用し、有用だと感じられた場合、または問題が発生した場合、GitHubでissueまたはdiscussionを作成してご連絡ください。これにより、この機能への需要を推定し、需要が十分に高い場合はパブリックAPIを追加することができます。
Update on GitHub