- LoRA(Low-Rank Adaptation) は、LLM全体を再学習せずに小さな低ランク行列だけを更新してファインチューニングコストを下げる手法であり、このStudioではLoRAレイヤーを直接実装して動作を確認する
- 核心は、通常のファインチューニングにおける重み変化 ΔW を、2つの小さな行列
A、B の積で近似することにあり、ランク r が小さいほど学習パラメータと表現容量の両方が減る
- 5,000×10,000 の重み行列は 5,000万パラメータを持つが、
r=8 のLoRAでは B 5,000×8 と A 8×10,000 を追加するだけで、12万パラメータとなり 400 倍小さい
- DistilBERTベースのIMDb感情分類では、基本LoRAは Test acc 89.44% を記録し、最後の2レイヤーだけを学習した場合の 86.22% より高く、全体ファインチューニングの 92.31% よりは低かった
- ハイパーパラメータ探索後のLoRAは、約 50万学習パラメータで Val acc 92.96%、Test acc 92.39% を記録し、6,695万5,010個のパラメータを学習した全体ファインチューニングよりわずかに高い精度を示した
LoRAが削減するファインチューニングコスト
- LoRA は Low-Rank Adaptation の略で、LLMをより効率的にファインチューニングするための手法である
- 通常のファインチューニングではディープラーニングモデルのすべてのパラメータを調整するが、LoRAでは小さな 低ランク行列 の集合だけを更新する
- 事前学習済みLLMは複数のタスクに利用できるが、特定のデータセットやタスクに合わせるにはファインチューニングが有用である
- モデルが大きくなるほど、すべてのレイヤーを更新する方式は計算コストの負担が大きくなる
ΔWを小さな行列積で近似
- 通常のファインチューニングでは、重み行列
W の更新を ΔW として計算する
- LoRAでは
ΔW を 2 つの小さな行列 A と B の積で近似する
- PCAやSVDに慣れていれば、
ΔW を A と B に分解する方法に近いと考えられる
- ランク
r はLoRAの ハイパーパラメータ である
r が小さいほど学習パラメータ数が減り、学習が高速化し、必要な計算量も抑えられる
- 同時に、タスク固有の情報を捉える低ランク行列の容量も小さくなる
- 5,000×10,000 の重み行列の例:
- 通常の更新
ΔW: 合計 5,000万パラメータ
r=8 のLoRA: B 5,000×8、A 8×10,000
- 追加パラメータ: 80,000 + 40,000 = 120,000個
- 通常のファインチューニング比で 400 倍小さい
- 実運用では、性能とコストのバランスを取るために複数の
r の値を試す必要がある
PyTorchでLoRAレイヤーを実装
- 基本の
LoRALayer は入力次元、出力次元、ランク、スケーリング係数 alpha を受け取る
class LoRALayer(torch.nn.Module):
def __init__(self, in_dim, out_dim, rank, alpha):
super().__init__()
std_dev = 1 / torch.sqrt(torch.tensor(rank).float())
self.A = torch.nn.Parameter(torch.randn(in_dim, rank) * std_dev)
self.B = torch.nn.Parameter(torch.zeros(rank, out_dim))
self.alpha = alpha
def forward(self, x):
x = self.alpha * (x @ self.A @ self.B)
return x
in_dim はLoRAを適用するレイヤーの入力次元で、out_dim は出力次元である
rank は A、B 行列の複雑さと、LoRAが追加するパラメータ数を制御する
alpha は既存モデルの重みに対してLoRAが与える変化の 大きさ を決める
alpha が高いほどモデルの動作を大きく調整する
alpha が低いほど変化はより微細になる
A は小さな乱数で初期化し、標準偏差はランクの平方根で決める
- 初期の
A の値が大きくなりすぎないようにするための選択である
B は 0 で初期化する
- 学習開始前は
B=0 のため AB=0
- 逆伝播によって
A と B が更新される前は、LoRALayer は元の重みに影響を与えない
LinearレイヤーをLinearWithLoRAに置き換える
- LoRAは通常、ニューラルネットワークの Linear/フィードフォワードレイヤー に適用される
- 既存の forward が 2 つの Linear レイヤーを順番に呼び出す場合、LoRA適用後は各 Linear の出力に LoRA の出力を加える
def forward(self, x):
x = self.linear_1(x) + self.lora_1(x)
x = F.relu(x)
x = self.linear_2(x) + self.lora_2(x)
return logits
- 既存のPyTorchモデルを修正する際は、各
Linear レイヤーを LinearWithLoRA に置き換える方法がシンプルである
class LinearWithLoRA(torch.nn.Module):
def __init__(self, linear, rank, alpha):
super().__init__()
self.linear = linear
self.lora = LoRALayer(
linear.in_features, linear.out_features, rank, alpha
)
def forward(self, x):
return self.linear(x) + self.lora(x)
LinearWithLoRA は元の Linear レイヤーと新しい LoRALayer をあわせて保持する
- 事前学習モデルの
Linear レイヤーを LinearWithLoRA に置き換えれば、LoRAを組み込んだうえでファインチューニングできる
DistilBERTでIMDb分類を実験
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=2)
- 新しいLoRA重みだけを学習するため、すべてのモデルパラメータの
requires_grad を False に設定する
for param in model.parameters():
param.requires_grad = False
- DistilBERTには 6 個のTransformerレイヤーがあり、各レイヤー内に Linear レイヤーがある
- attention には
q_lin、k_lin、v_lin、out_lin がある
- FFN には
lin1、lin2 がある
- 出力側には
pre_classifier、classifier の 2 つの Linear レイヤーがある
LoRAを選択的に適用する設定
- 基本のLoRA設定では、attention の query と value の重み行列にだけ LoRA を適用する
lora_r = 8
lora_alpha = 16
lora_dropout = 0.05
lora_query = True
lora_key = False
lora_value = True
lora_projection = False
lora_mlp = False
lora_head = False
- ループを回して DistilBERT の各 Transformer レイヤーで選択した Linear レイヤーを
LinearWithLoRA に置き換える
lora_query=True なら q_lin を置き換える
lora_key=True なら k_lin を置き換える
lora_value=True なら v_lin を置き換える
lora_projection=True なら out_lin を置き換える
lora_mlp=True なら ffn.lin1、ffn.lin2 を置き換える
lora_head=True なら pre_classifier、classifier を置き換える
- 置き換え後は、モデル出力で
q_lin や v_lin などが LinearWithLoRA に変わっていることを確認できる
基本LoRAと通常ファインチューニングの比較
- 基本のLoRA設定で IMDb Movie Reviews 分類を学習した結果:
- Train acc: 92.15%
- Val acc: 89.98%
- Test acc: 89.44%
- 最後の 2 つの出力レイヤーだけをファインチューニングした結果:
- Train acc: 86.68%
- Val acc: 87.26%
- Test acc: 86.22%
- 学習パラメータ数: 592,130個
- 基本LoRAは、最後の 2 レイヤーだけを学習する方式より Test acc が高く、学習パラメータ数も 147,456個 と少なかった
- 全レイヤーを従来方式でファインチューニングした結果:
- Train acc: 96.41%
- Val acc: 92.80%
- Test acc: 92.31%
- 学習パラメータ数: 66,955,010個
- 全体ファインチューニングは基本LoRAより Test acc が約 2% 高いが、LoRA設定より約 450倍多いパラメータ を更新する
LoRAハイパーパラメータ探索
- LoRAの性能は
lora_r、lora_alpha、適用対象レイヤーの設定によって変わりうる
03_finetune-lora.py はハイパーパラメータをコマンドライン引数で受け取る
python 03_finetune-lora.py --lora_alpha 32 --lora_r 16
python 03_finetune-lora.py \
--lora_alpha 32 \
--lora_r 16 \
--lora_query True \
--lora_key True \
--lora_value True \
--lora_projection True \
--lora_mlp True \
--lora_head True
03_gridsearch.py は利用可能なすべてのGPUで次のグリッドを実行する
alpha_values = [1, 4, 8, 16, 32, 64]
rank_values = [1, 2, 4, 8, 16, 32]
lora_query = ["True"]
lora_key = ["False", "True"]
lora_value = ["True"]
lora_projection = ["False", "True"]
lora_mlp = ["False", "True"]
lora_head = ["False", "True"]
- スクリプトは Visual Studio Code、コマンドラインターミナル、Job で実行でき、Job は完了後に自動終了する
- 結果は
results.txt に保存される
グリッド探索で得られた最高設定
results.txt 基準で最良のハイパーパラメータ構成は次のとおり
lora_r: 8
lora_alpha: 1
lora_query: True
lora_key: False
lora_value: True
lora_projection: False
lora_mlp: True
lora_head: False
- この設定の結果:
- Val acc: 92.96%
- Test acc: 92.39%
- このLoRA設定の学習パラメータは約 500k で、全体ファインチューニングの 66M パラメータよりはるかに少ない
- 精度は全体ファインチューニングの Val acc 92.80%、Test acc 92.31% をわずかに上回る
実行環境と追加資料
- Studio 上部の Run をクリックすると、コードを含む環境を複製できる
- Studio を複製した後は、追加のインストール、ダウンロード、設定手順なしでコードファイルを実行できる
- 関連ノートブックとスクリプト:
00_lora-layer.ipynb: LoRAレイヤーの実装
01_finetune-last-layers.ipynb: 最後のレイヤーのファインチューニング
02_finetune-with-lora.ipynb: LoRAファインチューニング
03_finetune-lora.py: LoRAハイパーパラメータ引数実行
03_gridsearch.py: LoRAハイパーパラメータのグリッド探索
04_finetune-all-layers.ipynb: 全レイヤーのファインチューニング
- 追加資料:
1件のコメント
Hacker Newsの意見
手法の流れは Maxime Labonne の LLMs 101 に沿っている: https://github.com/mlabonne/llm-course#4-supervised-fine-tun...
LoRA != LoRa なので、いつも混乱する。既存の略語を再利用したのが気に入らない
HN のトップページのように両方の意味が自然に成り立つ場所では特にそうだ
コンピューターサイエンスの分野で、「この数字、つまり ハイパーパラメータ が結果に正確にどう影響するのかわからないから、いろいろな値を入れてみて一番うまくいくものを使おう」みたいな話をするのは、いまだに妙な感じがする
ときどき最適解や正解ではなく局所最大値にはまることもあるが、それでも機能する。閉形式の公式で解けないので、何十億回かランダムサンプリングして欲しい値を見つけるわけで、LLM も同じだと言いたいわけではないが、こういうアプローチ自体はかなりよく使われる
これまで業界の大半は 工学的に設計 されたものだったが、LLM は発見されたものに近い
理想を言えば理論的基盤が必要だが、その理論を作ったり検証したりできるだけのデータを引き出すには、時にランダム探索が必要になる
私たちは本来いるべき位置より数年遅れている。1990年代にゲーム業界で働いていた頃は、ニューラルネットワークは良くて袋小路、悪ければ詐欺だというのが「常識」だった。少数の権威者が皆を止めたせいでこれほど多くの時間を失ったのは本当に残念で、今回はそういうことが繰り返されないようにしなければならない
いつ ファインチューニング をして、いつ RAG を使うべきなのか、まだはっきりしない
以前はファインチューニングは主にモデルの挙動を変えるためのものだと思っていたが、最近は一部の企業が知識の追加にもファインチューニングを使っているように見える。ファインチューニングの主な用途が気になる
主な用途は依然として 挙動の変更 だと思う。指示ファインチューニングや分類用ファインチューニングなどがそうだ
重みに知識を追加するなら、事前学習で行うのが最善だ。あるいは生成中に問い合わせる外部データベースや文書があるなら、質問のように RAG を使えばよい。参考までに、NeurIPS 2023 LLM Efficiency Challenge で 24 時間以内に GPU 1 台で「最高」の LLM をファインチューニングしたすべての優勝者は、LoRA または QLoRA(量子化 LoRA)を使っていた
追加データが簡潔でなかったり文脈を必要としたりするなら、ファインチューニング は RAG より良い
文脈が多すぎたり焦点がぼやけたりすると、プロンプト追従性が薄まることがあり、RAG ではモデルがより高次元のトークン関連性を学ぶことはできない。だから必要な内容を補強資料から運よく引っ張ってこなければならず、そうなると大して高度な検索エンジン以上のものではなくなる。政府や大企業の社内文書のように、公開データセットにはあまり現れない独自の細かな方言を持つ専門コーパスを扱うときに、特に問題になる
私の理解では、ファインチューニングは 異常なほど効果的 だ [0]。インコンテキスト学習は、ベースモデルがどれだけ強力か、そして RAG をどう構成するか、つまりクエリ処理・埋め込み検索・結果のランキングなどに大きく依存するからだ [1]
読んだ論文によれば、ファインチューニングは新しいドメイン知識を追加したり特定の知識を強化したりできる一方で、RAG は強化にしか使えない。ただし、異なるトレードオフを持つ2つの手法が同程度の能力を示すこともある [2]
[0] Fast.ai: Can Models learn from one sample, https://www.fast.ai/posts/2023-09-04-learning-jumps/ / https://archive.is/eJMPR
[1] LlamaIndex: Advanced RAG, https://blog.llamaindex.ai/a-cheat-sheet-and-some-recipes-fo... / https://archive.is/qtBXX
[2] Microsoft: RAG vs Fine-tuning: Pipelines, Tradeoffs, and a Case Study, https://arxiv.org/html/2401.08406v2#S6 / https://archive.is/UQ8Sa#S6
これらは 自己回帰モデル だ。前の部分から後に来る要素を予測できる新しいタイプのシーケンスがあり、そのやり方がモデルが以前に見たものと異なるなら、ファインチューニングは妥当に思える
特定のデータ状況で何をするかを決める基準としてはかなり曖昧だが、大まかなヒューリスティックとしては十分かもしれない。知識の追加がこれに含まれるかどうかは、実験してみないと好みの問題かもしれない
良い記事。自分はこの分野の人間ではないが、原論文を読んだときは LoRAが最後の全結合層 にだけ適用され、すべての層に独立して適用されるわけではないと理解していた。自分の読み違いかもしれない。
リンク先の実装がなぜこうなっているのか少し掘ってみると、QLoRAでこの方式が使われていて、興味深い効果があるようだった。QLoRAの判断についてのメモを追加するとよさそう。ただ、なぜ動くのかはよく分からず、初心者の視点では最後の層にLoRAを適用するのは理にかなっているとしても、各線形層に繰り返し適用する根拠はいまひとつ直感的に分からない。直感的な説明はできるだろうか?
LoRA層に置き換える層が多いほど、最適化の自由度は大きくなる。一部のファインチューニング手法は最後の層だけをファインチューニングすることを勧めているが、それは入力の「最も高次の」表現を含んでいると推定されるためだ。別の手法ではすべての層をファインチューニングする。たいていはデータと課題次第であり、LoRAはこの慣行をそのまま反映している
Axolotlの「最初から」ではなく、設定から始めるアプローチ を好む。AxolotlはMistral、Llama 2のファインチューニングをサポートし、サンプルパッキング、FlashAttention、xFormersのような最新手法も多くサポートしている。
自分はLoRAを最初から学ぶより、ファインチューニング用データを集めてキュレーションすることに集中して、データ中心 のファインチューニングをしている
https://github.com/Lightning-AI/lit-gpt
命名は難しい。最初はこれが「long range」の LoRa や、IoTセンサー通信のLoRaWANの話だと思っていた
ファインチューニングで最もよく使われるライブラリは何だろう? 「最初から」ではない方式を前提に
うわ、自分も最初は当然 LoRa の話だと思っていた
LoRAの性能コスト はどのくらいか?
推論中は2通りある。順伝播中にLoRAの値を動的に足し込むなら、理論上は少し遅くなる可能性があるが、顧客ごとに小さな重みセットを別々に保持したい場合には利点にもなる。大きなベースモデルを1つだけ動かし、顧客ごとのLoRA重みをその場で適用できるからだ。LoRA重みをベースモデルに再度マージすれば、ベースモデルとまったく同じ性能を出せる。