「ゼロから始めるLoRAの実装」
美とファッションの世界への魔法の入門 - ゼロから始めるLoRAの実装
ゼロからLoRAを実装する方法と実用的なヒント
このブログ投稿では、ゼロからLoRAを実装する方法を紹介します。
LoRAは、Low-Rank AdaptationまたはLow-Rank Adaptorsの略であり、既存の言語モデルの微調整に効率的で軽量なメソッドを提供します。これには、BERTやRoBERTaなどのマスクされた言語モデル、およびGPT、Llama、Mistralなどの因果(またはチャットボット)モデルが含まれます。
低ランクアダプタの主な利点の1つは、効率性です。パラメータを少なく利用することにより、LoRAは計算の複雑さとメモリ使用量を大幅に低下させます。これにより、消費者向けのGPUで大規模なモデルをトレーニングし、コンパクトな(メガバイト単位の)LoRAを他の人に簡単に配布できます。
さらに、LoRAは一般化性能を向上させることができます。モデルの複雑さを制約することで、特にトレーニングデータが限られたシナリオにおいて、過学習を防ぐのに役立ちます。これにより、新しい未知のデータで優れたモデルを得るか、少なくとも初期のトレーニングタスクからの知識を保持するような、より強力なモデルが得られます。
- 地球は平らではなく、あなたのボロノイ図もそうであるべきではありません
- ポイントクラウド用のセグメント化ガイド「Segment Anything 3D for Point Clouds Complete Guide (SAM 3D)」
- 2024年に探すべき6つのリモートAIジョブ
さらに、低ランクアダプタは既存のニューラルネットワークアーキテクチャにシームレスに統合することができます。この統合により、追加のトレーニングコストを最小限に抑えて、事前トレーニングされたモデルを微調整および適応できるため、転移学習アプリケーションに非常に適しています。
まず、LoRAの動作方法について詳しく説明し、次にRoBERTaモデルのLoRAをゼロから開発する方法をデモンストレーションし、それに続いてGLUEおよびSQuADベンチマークを使用した実装のベンチマークと一般的なヒントと改善に関するディスカッションを行います。
LoRAの動作原理
LoRAの基本的なアイデアは、事前トレーニングされた行列(つまり、元のモデルのパラメータ)を固定(不変の状態)に保ち、元の行列に少しのデルタを追加することです。このデルタ行列には、元の行列より少ないパラメータが含まれます。
たとえば、完全連結層のパラメータまたはトランスフォーマのセルフアテンションメカニズムの1つの行列である行列Wを考えてみましょう:
明らかに、もしW-origの次元がn×mで、同じ次元の新しいデルタ行列で微調整するだけであれば、何も得られません。それどころか、パラメータが倍増してしまいます。
そのトリックは、行列Bと行列Aを用いて、デルタ行列Wを元の行列よりも「次元的に」小さくすることです。
ここで、まず次元rを定義し、基本行列の次元よりもはるかに小さいr≪nおよびr≪mである行列Bはn×rであり、行列Aはr×mです。これらを乗算することにより、同じ次元の行列Wが得られますが、はるかに低いパラメータ数で構築されています。
明らかに、私達はデルタがトレーニングの最初にゼロであることを望みます。したがって、Bは通常ゼロで初期化され、Aはランダム(通常は正規分布)の値で初期化されます。
例えば、次のようになります:
基本次元が1024でLoRAランクrが4である状況を想像してみてください:
- Wは1024 * 1024 ≈ 100万のパラメータを持っています
- AとBはそれぞれr * 1024 = 4 * 1024 ≈ 4kのパラメータを持っており、合計で8kのパラメータになります
- したがって、LoRAを使用してマトリックスを更新するには、パラメータのわずか0.8%のトレーニングのみを行う必要があります。
ちなみに、LoRAの論文では、デルタマトリックスにアルファパラメータを掛けています:
最初のrにαをセットして学習率を微調整すると、後でrパラメータを変更しても学習率を再調整しなくても(少なくともおおよそ)大丈夫です。実装ではこの詳細を見逃しても問題ありませんが、Hugging FaceのPEFTなど、他の多くのLoRAライブラリに共通の機能です。
LoRAの実装
私たちの実装では、オリジナルのLoRA論文に近い形で進めたいと思います。彼らは、実際に置き換える必要のあるtransformerのマトリックスをテストしました。GPT-3の微調整タスクで異なる戦略を比較する際、自己注意機構のクエリとバリューベクトルのみを適応するだけで十分であることがわかりました。
現在では多くの人がこの評価を無視し、タスクやモデルに関係なく各マトリックスを微調整できるようにしています(QLoRA論文を参照)。
ここでは、PyTorchで実装しますが、他のフレームワークに簡単に適応できるはずです。
このブログ記事では、コードを少し簡略化しているので、読みやすくなっていますが、重要な要素はしっかりと示されています。完全なコードと一部のトレーニドLoRAの重みはこちらで見つけることができます: https://github.com/Montinger/Transformer-Workbench。
自己注意モデルの再実装
適応したいモデルは、HuggingfaceのRoBERTaモデルです。最も簡単な方法は、オリジナルの自己注意機構RobertaSelfAttention
を再ラップすることです。新しいクラスLoraRobertaSelfAttention
では、LoRAマトリックスを初期化します。すべてのBマトリックスはゼロで、すべてのAマトリックスは正規分布からのランダムな数値で初期化されます。
class LoraRobertaSelfAttention(RobertaSelfAttention): """ LoRA(Low-Rank Adaptation)マトリックスを追加したRobertaSelfAttentionを拡張します。 LoRAはクエリとバリューマトリックスのみを更新することで効率を向上させます。 このクラスはLoRAマトリックスを追加し、forwardメソッドでLoRAロジックを適用します。 Parameters: - r(int):LoRAマトリックスのランク。 - config:Robertaモデルの設定。 """ def __init__(self, r=8, *args, **kwargs): super().__init__(*args, **kwargs) d = self.all_head_size # クエリとバリューのためのLoRAマトリックスを初期化 self.lora_query_matrix_B = nn.Parameter(torch.zeros(d, r)) self.lora_query_matrix_A = nn.Parameter(torch.randn(r, d)) self.lora_value_matrix_B = nn.Parameter(torch.zeros(d, r)) self.lora_value_matrix_A = nn.Parameter(torch.randn(r, d))
これらのマトリックスがあるので、新しいクラスメソッドlora_query
とlora_value
を定義します。これらはΔWマトリックス、つまりBAを計算し、元のマトリックスに追加します。元のquery
とvalue
メソッドから呼び出されます。
class LoraRobertaSelfAttention(RobertaSelfAttention): # ... def lora_query(self, x): """ クエリコンポーネントにLoRAを適用します。通常のクエリ出力にLoRA適応を加えた修正されたクエリ出力を計算します。 トレーニング前に通常の線形層をフリーズする必要があります。 """ lora_query_weights = torch.matmul(self.lora_query_matrix_B, self.lora_query_matrix_A) return self.query(x) + F.linear(x, lora_query_weights) def lora_value(self, x): """ バリューコンポーネントにLoRAを適用します。通常のバリュー出力にLoRA適応を加えた修正されたバリュー出力を計算します。 トレーニング前に通常の線形層をフリーズする必要があります。 """ lora_value_weights = torch.matmul(self.lora_value_matrix_B, self.lora_value_matrix_A) return self.value(x) + F.linear(x, lora_value_weights)
今、醜い部分に入ります: 使用するためには、RobertaSelfAttention
の元のforward関数を上書きする必要があります。これは少しハードコーディングされていますが(後で改善に関するディスカッションを参照)、非常にシンプルです。まず、https://github.com/huggingface/transformers/blob/main/src/transformers/models/roberta/modeling_roberta.pyから元のforwardコードをコピーします。次に、query
の呼び出しをlora_query
に、value
の呼び出しをlora_value
に置き換えます。関数は次のようになります:
class LoraRobertaSelfAttention(RobertaSelfAttention): # ... def forward(self, hidden_states, *args, **kwargs): """Copied fromhttps://github.com/huggingface/transformers/blob/main/src/transformers/models/roberta/modeling_roberta.py but replaced the query and value calls with calls to the lora_query and lora_value functions. We will just sketch of how to adjust this here. Change every call to self.value and self.query in the actual version. """ # original code for query: ## mixed_query_layer = self.query(hidden_states) # updated query for LoRA: mixed_query_layer = self.lora_query(hidden_states) # The key has no LoRA, thus leave these calls unchanged key_layer = self.transpose_for_scores(self.key(hidden_states)) # original code for value: ## value_layer = self.transpose_for_scores(self.value(hidden_states)) # updated value for LoRA: value_layer = self.transpose_for_scores(self.lora_value(hidden_states)) # ... (rest of the forward code, unchanged)
タダ、そこに私たちのLoRA-self-attentionの実装があります。今残っている唯一のタスクは、元のRoBERTaモデルで注意モジュールを交換することです。
モジュールの置換
素晴らしい、自己注意を独自の実装に置き換えましたが、この新しいクラスを古いRoBERTaモデルにどのように組み込みますか?基本的には、RoBERTaモデルの各名前付きコンポーネントをループし、それがRobertaSelfAttention
クラスであるかどうかをチェックし、
そして、それがそうであれば、LoraRobertaSelfAttention
に置き換えます。ただし、元の重み行列が保持されるようにする必要があります。
これを実現するために、置換を行う新しいラッパー関数を作成します。さらに、実際のタスクでRoBERTaモデルを微調整する機能も追加したいと思います。
class LoraWrapperRoberta(nn.Module): def __init__(self, task_type, num_classes=None, dropout_rate=0.1, model_id="roberta-large", lora_rank=8, train_biases=True, train_embedding=False, train_layer_norms=True): """ さまざまなNLPタスクに対して、低ランク適応(LoRA)を使用したRoBERTaのラッパー。 - task_type:NLPタスクのタイプ(「glue」、「squad_v1」、「squad_v2」)。 - num_classes:分類のクラス数(タスクによって異なる)。 - dropout_rate:モデルのドロップアウト率。 - model_id:事前学習済みRoBERTaモデルのID。 - lora_rank:LoRA適応のランク。 - training_biases、training_embedding、training_layer_norms: パラメータを初期化した後もトレーニング可能な フラグ。 例: model = LoraWrapperRoberta(task_type='glue') """ super().__init__() # 1. パラメータを使用してベースモデルを初期化する self.model_id = model_id self.tokenizer = RobertaTokenizer.from_pretrained(model_id) self.model = RobertaModel.from_pretrained(model_id) self.model_config = self.model.config # 2. ベンチマークタスク用のレイヤーを追加する d_model = self.model_config.hidden_size self.finetune_head_norm = nn.LayerNorm(d_model) self.finetune_head_dropout = nn.Dropout(dropout_rate) self.finetune_head_classifier = nn.Linear(d_model, num_classes) # 3. トレーニング用のLoRAモデルを設定する self.replace_multihead_attention() self.freeze_parameters_except_lora_and_bias()
初期化で2つのヘルパーメソッドを呼び出すことが分かります:
self.replace_multihead_attention
:これは、すべてのニューラルネットワークの注意を以前に書かれたLoraRobertaSelfAttention
で置き換えます。self.freeze_parameters_except_lora_and_bias
:これにより、学習のためのメインパラメーターのすべてが凍結され、勾配とオプティマイザのステップがLoRAパラメーターと他のバイアスおよび層正規化パラメーターにのみ適用されるようになります。
class LoraWrapperRoberta(nn.Module): # ... def replace_multihead_attention_recursion(self, model): """ モデル内のRobertaSelfAttentionをLoraRobertaSelfAttentionで置換します。 このメソッドは再帰的にすべてのサブコンポーネントに置換を適用します。 パラメーター ---------- model : nn.Module 変更するPyTorchモジュールまたはモデル。 """ for name, module in model.named_children(): if isinstance(module, RobertaSelfAttention): # RobertaSelfAttentionをLoraRobertaSelfAttentionで置き換える new_layer = LoraRobertaSelfAttention(r=self.lora_rank, config=self.model_config) new_layer.load_state_dict(module.state_dict(), strict=False) setattr(model, name, new_layer) else: # 子モジュールのための再帰呼び出し self.replace_multihead_attention_recursion(module)
PyTorchでは、ネットワークの一部が(RoBERTaの場合)別々のPyTorchモジュールにパックされることがありますので、モデルのすべてのパーツを再帰的にループする必要があります。
次に、トレーニングしないパラメーターをすべて凍結する必要があります。
class LoraWrapperRoberta(nn.Module): # ... def freeze_parameters_except_lora_and_bias(self): """ 指定されたレイヤーやタイプの以外のすべてのモデルパラメーターを固定します。 LoRAのレイヤー、ファイントゥンヘッド、バイアスパラメーター、埋め込み、レイヤーノームのパラメーターは、クラスの設定に基づいて可訓練に設定できます。 """ for name, param in self.model.named_parameters(): is_trainable = ( "lora_" in name or "finetune_head_" in name or (self.train_biases and "bias" in name) or (self.train_embeddings and "embeddings" in name) or (self.train_layer_norms and "LayerNorm" in name) ) param.requires_grad = is_trainable
さらに、ファインチューニングするタスクに対応するためのforwardメソッドと、LoRAの重みを保存およびロードするための2つのメソッドを実装する必要があります。これにより、以前にトレーニングされたモデルのアダプターをロードすることができます。
クリフハンガー:コードをより優れたものにし、他のネットワークアーキテクチャにも簡単に汎化できる方法があります。 この方法について考えることができますか? 以下の「改善の可能性」セクションで議論するまで、この質問について考える時間があります。しかし、その前に:私たちの実装が実際に機能するかどうかをいくつかのベンチマークでテストしましょう。
GLUEとSQuADでの結果のベンチマーク
私たちの実装は、GLUE(General Language Understanding Evaluation)およびSQuAD(Stanford Question Answering Dataset)ベンチマークを使用して評価する準備が整いました。
GLUEベンチマークは、8つの異なるNLPタスクからなるスイートであり、言語モデルの包括的な理解能力を評価します。センチメント分析、テキストの論理関係、文の類似性といった課題が含まれており、モデルの言語適応性と達成度の力強い指標となっています。
一方、SQuADは、質問応答モデルの評価に重点を置いています。モデルは、Wikipediaの記事から回答を抽出し、関連するテキストスパンを特定します。より高度なバージョンであるSQuAD v2では、回答不可能な質問も導入され、テキストに回答がない場合を認識する能力が求められます。
次のベンチマークでは、ハイパーパラメーターのチューニング、複数回の実行(特に小規模なGLUEデータセットは、確率的ノイズに対して敏感です)、早期停止の実施、前のGLUEタスクでのファインチューニングからの開始(小規模なデータセットのノイズのばらつきを減らし、過学習を防ぐためにしばしば行われます)は行いませんでした。
すべての実行:
- RoBERTa-baseモデルにランク8のLoRAインジェクションを新たに初期化から開始
- 各タスクについてトレーニングは正確に6エポック行われ、早期停止は行いませんでした。
- 最初の2エポックでは学習率が最大値まで線形にスケーリングされ、残りの4エポックでゼロに線形に減衰しました。
- すべてのタスクの最大学習率は5e-4でした。
- すべてのタスクのバッチサイズは16でした。
RoBERTa-baseモデルは124.6百万のパラメータを持っています。LoRAパラメータ、バイアス、およびレイヤーノームによって、トレーニングするために使用可能なパラメータはわずか420千あります。つまり、元のパラメータのわずか0.34%でトレーニングを行います。
これらの特定のタスクによって導入されるパラメータの数は非常に少なく、実際のディスクサイズでわずか1.7MBです。トレーニング済みのLoRAは、GitリポジトリのOutputフォルダにあります。
トレーニング後、LoRAパラメータを再読み込みし、それを適用して各タスクの検証セットでパフォーマンスをテストしました。以下はその結果です:
これらの結果は、ハイパーパラメータの微調整により大幅に改善される可能性があります。それにもかかわらず、これは明確に私たちのLoRAの実装が機能しており、注入された低ランク行列が学習していることを証明しています。
可能な改善点
私たちの実装を反省すると、「セルフアテンションクラスを再コーディングし、複雑な置換処理を行うよりも、より効率的かつ汎用性のある(つまり他のネットワークアーキテクチャにも転用可能な)アプローチはなかったのか?」と疑問に思うかもしれません。
実際には、単純にpytorchのnn.Linear
関数の周りにラッパーを実装し、置換するレイヤーがどれなのかを指定できるようにすることもできました。同様に、ほとんどの基本的なpytorchレイヤーの周りにラッパーを作成し、新しいネットワークアーキテクチャにLoRAを迅速に適応させることができます。以下に、これがどのように行われるかの簡単なスケッチを示します:
class LoraLinear(nn.Linear): """ Low-Rank Adaptation(LoRA)をローカル線形層に拡張します。 LoRAは、効率的な大規模モデルのトレーニングを可能にするため、レイヤーに2つの行列を追加します。 """ def __init__(self, in_features, out_features, r=8, *args, **kwargs): super().__init__(in_features, out_features, *args, **kwargs) # LoRA行列を初期化します self.lora_matrix_B = nn.Parameter(torch.zeros(out_features, r)) self.lora_matrix_A = nn.Parameter(torch.randn(r, in_features)) # 元の重み行列をフリーズします self.weight.requires_grad = False def forward(self, x: Tensor) -> Tensor: # LoRAの重み調整を計算します lora_weights = torch.matmul(self.lora_matrix_B, self.lora_matrix_A) # 元の重みとLoRAによる線形変換を適用します return super().forward(x) + F.linear(x, lora_weights)
これは、ほとんどの場合、huggingfaceのPEFT(Parameter-Efficient Fine-Tuning)ライブラリがLoRAを実装する方法に非常に近いです。学習ではなく実際のアプリケーションでは、自分自身のコーディングではなく、このライブラリを使用することを強くお勧めします。
また、セルフアテンションのすべての線形層(つまり、自己注意と完全に接続されたフォワードネットワークの2つの線形層)にもLoRAを適用するのが一般的な手法になっています。それらは既に小さいので、低ランク注入は必要ありませんが、バイアスとレイヤーノームはトレーニング可能な状態に保つことが通常は良い考えです。
GPUのVRAMを節約するために、元の行列の重みを量子化することも推奨されます。これは、bits-and-bytesライブラリを使用することで効率的に行うことができます。このライブラリは、Hugging Faceと完全に統合されています(詳細は参考文献を参照)。
まとめると、真剣な状況でのローランク適用の5つのコマンドメントは次のとおりです:
読みづらいと感じた場合、ここに平文で再掲します:
低順位適応の5つの戒律
1. パラメータサイズを最小限に保つことに重点を置いて、効率的なモデル微調整にLoRAを利用します。2. 複雑なコーディングを必要としないように、LoRAの実装にPEFTライブラリを利用します。3. ローラの適応を全ての線形層に拡張し、全体のモデルの能力を向上させます。4. バイアスとレイヤーノームを訓練可能にしたままにします。これらはモデルの適応性に重要であり、低ランクの適応は必要ありません。5. Quantized-LoRA(QLoRA)を適用して、GPUのVRAMを保存し、より大きなモデルのトレーニングを可能にします。
QLoRAを使用してトレーニングする場合、各乗算時に行列を量子化解除するため、少し遅くなることに注意してください。例えば、Llama-7Bのような大規模なものを微調整する場合、QLoRAはVRAMを約75%減らすが、標準のLoRAと比べて約40%遅くなります。詳細については、引用文献にリンクされているブログ記事をご覧ください。
PEFTの実装に関するステップバイステップガイド
では、実際に戒律を順守し、PEFTを使用してより良いバージョンを実装してみましょう。
まず、モデルを量子化された方法でロードしましょう。BitsAndBytesのトランスフォーマーライブラリとの統合により(2023年5月に導入されました)、これは簡単に行えます。
構成ファイルを指定し、この量子化を使用してHuggingfaceから直接モデルをロードする必要があります。一般的には、transformersライブラリからAutoModelオブジェクトを使用することが最適です。量子化モデルを新しく定義された大規模なnn.module
オブジェクトのサブモジュールとしてロードするのは難しいです。通常はHuggingfaceの生のモデルで作業し、GLUEタスクの場合はAutoModelForSequenceClassification
、SQuADベンチマークの場合はAutoModelForQuestionAnswering
を直接インポートすることが最適です。構成では、量子化しないパラメータも指定できます。ここでは分類またはqa-outputヘッドを登録する必要があります。これらは微調整のために新たに初期化されたものであり、事前学習ベースモデルの一部ではありませんでした。
import bitsandbytes as bnbfrom transformers import AutoModel, AutoModelForSequenceClassification, BitsAndBytesConfig# 量子化モデルをロードするための構成bnb_config = BitsAndBytesConfig( load_in_4bit=True, # 4ビットロードを有効化 bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, llm_int8_skip_modules=['classifier', 'qa_outputs'] # これらを量子化しないようにスキップします) # Huggingfaceから量子化構成でモデルをロードmodel = AutoModelForSequenceClassification.from_pretrained('roberta-base', torch_dtype="auto", quantization_config=bnb_config)
モデルのモジュールとパラメータのデータ型を確認することで、4ビットのローディングを検証できます:
# 4ビットの要素(Linear4bit)がアテンション層にあるかを確認するprint("4ビットの要素(Linear4bit)がアテンション層にあります:")print(model.roberta.encoder.layer[4].attention)print("uint8のデータ型をチェックする:")print(model.roberta.encoder.layer[4].attention.self.query.weight.dtype)
さて、PEFTを使ってLoRAパラメータを注入します。PEFTライブラリは、モジュールの名前を使用して置換するモジュールに対処するため、モデルのmodel.named_parameters()
を確認する必要があります。ここでは、量子化されていないroberta-baseモデルの例をご紹介します。
Module Parameters---------------------------------------------------------- ------------roberta.embeddings.word_embeddings.weight 38_603_520roberta.embeddings.position_embeddings.weight 394_752roberta.embeddings.token_type_embeddings.weight 768roberta.embeddings.LayerNorm.weight 768roberta.embeddings.LayerNorm.bias 768roberta.encoder.layer.0.attention.self.query.weight 589_824roberta.encoder.layer.0.attention.self.query.bias 768roberta.encoder.layer.0.attention.self.key.weight 589_824roberta.encoder.layer.0.attention.self.key.bias 768roberta.encoder.layer.0.attention.self.value.weight 589_824roberta.encoder.layer.0.attention.self.value.bias 768roberta.encoder.layer.0.attention.output.dense.weight 589_824roberta.encoder.layer.0.attention.output.dense.bias 768roberta.encoder.layer.0.attention.output.LayerNorm.weight 768roberta.encoder.layer.0.attention.output.LayerNorm.bias 768roberta.encoder.layer.0.intermediate.dense.weight 2_359_296roberta.encoder.layer.0.intermediate.dense.bias 3_072roberta.encoder.layer.0.output.dense.weight 2_359_296roberta.encoder.layer.0.output.dense.bias 768roberta.encoder.layer.0.output.LayerNorm.weight 768roberta.encoder.layer.0.output.LayerNorm.bias 768roberta.encoder.layer.1.attention.self.query.weight 589_824...roberta.encoder.layer.11.output.LayerNorm.bias 768classifier.dense.weight 589_824classifier.dense.bias 768classifier.out_proj.weight 1_536classifier.out_proj.bias 2---------------------------------------------------------- ------------総合 124_647_170
それぞれの文字列を選択するために、LoRA targetsを指定することができます。フルネームに指定したサブストリングが含まれているかどうかをチェックします。したがって、query
とvalue
を書くことは、上記のスクラッチからの実装と同等です。密なレイヤーについては、分類器も密な出力を持っているため、より注意深くする必要があります。他の密なレイヤーを微調整する場合は、intermediate.dense
と output.dense
によって特定しなければなりません。
LoRAパラメータで注入されていないすべてのパラメータは自動的に凍結され、つまりどんな勾配の更新も受けません。元の形式でトレーニングしたいレイヤーがある場合は、Lora-Configの modules_to_save
パラメータにリストを渡すことでそれらを指定できます。この場合、LayerNorm
を追加し、GLUEとSQuADのファインチューンヘッドも追加したいとします。リストの各要素が何かに一致する必要はないことに注意してください。単に classifier
と qa_outputs
をこのリストに追加して、両方のタスクに正常に機能する単一の設定ファイルを持つことができます。
バイアスパラメータには便利な bias
の設定パラメータを使用できます。全てを選択すると、すべてのモジュールのすべてのバイアスを再トレーニングできます。lora_onlyを選択すると、注入されたバイアスのみをトレーニングできます。none を選択すると、トレーニング中にすべてのバイアスを一定に保ちます。
次の例は、ランク2のLoRAを注入しています。 上記で試したランクは、最初に試したランクで、スクラッチからの学習率を保持できるようにするために8で指定しています。
import peft# PEFTを使用してLoRAを注入するための設定peft_config = peft.LoraConfig( r=2, # 注入されるLoRA行列のランク次元 lora_alpha=8, # スケーリングのパラメータ、ここでは8を使用して、スクラッチからの実装と比較できるようにする target_modules=['query', 'key', 'value', 'intermediate.dense', 'output.dense'], # 分類器も含めて密を正確に指定 modules_to_save=["LayerNorm", "classifier", "qa_outputs"], # レイヤーノーマライゼーションの再トレーニング、ファインチューンヘッドのための分類器、SQuADのqa_outputs lora_dropout=0.1, # ドロップアウト確率 bias="all", # none, all, or lora_only)model = peft.get_peft_model(model, peft_config)
LoRA注入の対象モジュールを増やすと、VRAMの要件も増えます。VRAMの制限に達した場合は、対象モジュールの数やLoRAのランクを減らすことを検討してください。
特にQLoRAを使用する場合は、量子化行列と互換性のあるオプティマイザを選択してトレーニングしてください。通常のtorchオプティマイザの代わりに、bitsandbytesのバリアントを使用します:
import torchimport bitsandbytes as bnb#これをoptimizer = torch.optim.AdamW(args here)からoptimizer = bnb.optim.AdamW8bit(same args here)に変更
そして、前と同じようにこのモデルをトレーニングすることができますが、トレーニング中にQLoRAについて心配する必要はありません。
トレーニングが完了したら、モデルを保存して再ロードするプロセスは簡単です。 model.save_pretrained
を使用してモデルを保存し、必要なファイル名を指定します。PEFTライブラリは、モデルの重みと設定ファイルを保存するために、この位置にディレクトリを自動的に作成します。このファイルには、ベースモデルとLoRAの設定パラメータなどの重要な詳細が含まれています。
モデルを再ロードするには、peft.AutoPeftModel.from_pretrained
を使用し、ディレクトリパスを引数として渡します。覚えておかなければならない重要なポイントは、LoRAの設定に現時点では AutoModelForSequenceClassification
の初期化時に指定されたクラスの数を保持していないことです。 from_pretrained
を使用する場合は、このクラスの数を追加のパラメータとして手動で入力する必要があります。これを行わないとエラーが発生します。
再ロードされたモデルは、LoRAアダプタが適用された元のベースモデルから構成されます。LoRAアダプタをベースモデルの行列に恒久的に統合する場合は、 model.merge_and_unload()
を実行してください。
より実践的な理解と詳細な手順については、GitHubリポジトリを参照してください。そこには、Train-QLoRA-with-PEFT.ipynb と Load-LoRA-Weights-PEFT.ipynb という2つのノートブックがあり、PEFTを使用したモデルのトレーニングと読み込みの手順の具体的な例が提供されています。
結論
「私たちは探求をやめることはなく、私たちの探求の終わりは、最初のようにその場所を知り、初めて到着することです。」
— T.S.エリオットの「リトル・ギディング」から
この旅は、単純ながらもハードコードされたLoRAの実装から、低ランクアダプターのより深い理解、実際の実装、ベンチマークテストへと私たちを導きました。
より効率的な代替実装戦略を探求し、PEFTなどの既存ライブラリの優雅さに深く突き進みました。
私たちの冒険は、実世界のアプリケーションでこの技術を効率的かつ効果的に使用するための「五戒」という具体的なガイドラインと、それらを実践する方法についてのステップバイステップガイドで締めくくられます。
参考文献
すべての画像は、特記しない限り著者によるものです。
- 元のLoRA論文: https://arxiv.org/pdf/2106.09685.pdf
- QLoRA論文: https://arxiv.org/abs/2305.14314
- QLoRAファインチューニングのSentdexガイド: https://www.youtube.com/watch?v=J_3hDqSvpmg
- LlamaでのLoRAファインチューニングに関するブログ記事: https://www.anyscale.com/blog/fine-tuning-llms-lora-or-full-parameter-an-in-depth-analysis-with-llama-2
- bitsandbytes Hugging Faceの統合: https://huggingface.co/blog/4bit-transformers-bitsandbytes
- LoRAトレーニングの見解: https://lightning.ai/pages/community/lora-insights/
- QLoRAによるLlamaモデルのファインチューニング時の予想されるVRAMの節約: https://cloud.google.com/vertex-ai/docs/model-garden/lora-qlora
- 石板テキストに使用したフォント、独自のものを作成したい場合にご利用ください: https://www.fontspace.com/sharp-objects-nbp-font-f14469
We will continue to update VoAGI; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles