ゼロから学ぶLoRA — 細い行列2枚だけを学習して巨大モデルを微調整する
7Bモデルの全パラメータ微調整は、実質的な中身が小さい仕事のために100 GB超のオプティマイザ状態を要求する。LoRAの主張は、モデルに必要な「変化」は低ランクだから、細い行列2枚の積 ΔW = BA として保持できる、というもの。この記事はコストの問題から出発してそれを組み立て、式を1行ずつ分解し、なぜ B が0から始まるのか、α/r は何のためなのかを説明し、実モデルでパラメータ数を数え、最後に本当に重要な3つの判断——ランク、対象レイヤ、推論時のマージ——で締める。
要点
- 全パラメータ微調整は1パラメータあたり約16バイトを要し、67.4億パラメータで約108 GBになる。
- LoRAは を凍結し、更新を ()という細い行列2枚の積として学習する。
- を0、 をランダムに初期化するので、ステップ0では となり学習済みモデルと同じ出力から始まる。
- 更新の強さは の比で決まり、 を据え置いたまま を上げると更新が弱まる。
- 7Bモデルのqとvに を当てると学習パラメータは約420万(0.062 %)で、保存物は約16 MBになる。
LoRA: Low-Rank Adaptation of Large Language Models
一次資料 — この記事の根拠
論文の発表 2021-06-17→この解説の公開 2026-09-235年3か月後
LoRA: Low-Rank Adaptation of Large Language ModelsEdward J. Hu, Yelong Shen, Phillip Wallis ほか · 2021-06-17 · v2arXiv:2106.09685論文ページ·PDFIntrinsic Dimensionality Explains the Effectiveness of Language Model Fine-TuningarXiv:2012.13255論文ページ·PDF
原文の要旨(Abstract)を読む
An important paradigm of natural language processing consists of large-scale pre-training on general domain data and adaptation to particular tasks or domains. As we pre-train larger models, full fine-tuning, which retrains all model parameters, becomes less feasible. Using GPT-3 175B as an example -- deploying independent instances of fine-tuned models, each with 175B parameters, is prohibitively expensive. We propose Low-Rank Adaptation, or LoRA, which freezes the pre-trained model weights and injects trainable rank decomposition matrices into each layer of the Transformer architecture, greatly reducing the number of trainable parameters for downstream tasks. Compared to GPT-3 175B fine-tuned with Adam, LoRA can reduce the number of trainable parameters by 10,000 times and the GPU memory requirement by 3 times. LoRA performs on-par or better than fine-tuning in model quality on RoBERTa, DeBERTa, GPT-2, and GPT-3, despite having fewer trainable parameters, a higher training throughput, and, unlike adapters, no additional inference latency. We also provide an empirical investigation into rank-deficiency in language model adaptation, which sheds light on the efficacy of LoRA. We release a package that facilitates the integration of LoRA with PyTorch models and provide our implementations and model checkpoints for RoBERTa, DeBERTa, and GPT-2 at https://github.com/microsoft/LoRA.
LoRAが存在する理由となったコストの問題
70億パラメータの言語モデルと、書かせたい文体の例が数千件あります。仕事は小さい。請求書は小さくありません。
全パラメータ微調整(フル微調整)とは、その70億個すべてを更新することで、そのひとつひとつにオプティマイザが付き添わなければなりません。混合精度のAdamでは1パラメータあたりの標準的な内訳は、マスター重み4バイト、勾配4バイト、Adamの2つのモーメント推定に8バイト——1パラメータあたり約16バイトで、67.4億パラメータならおよそ108 GB。活性化を1つも保存していない時点でこれです。80 GBのアクセラレータ1枚には載りません。しかもそれを払った末に手に入るのはモデルの完全なコピー——タスクごと、顧客ごと、実験ごとに、ディスク上でさらに14 GB。
明らかに釣り合っていません。文体、フォーマット、ドメイン語彙をモデルに教えることは、モデルがすでに持っている情報量とは比べるべくもないのに、フル微調整はそれと同額を請求してきます。LoRA(Low-Rank Adaptation、低ランク適応)——Huらが2021年に提案——は、モデルに比例してではなく変化に比例して払えるという着眼であり、それを実現する具体的なパラメータ化です。
この記事は土台から組み立てます。「低ランク」とは何で、なぜ更新がそうだと信じられたのか。行列2枚への置き換えを1行ずつ分解。なぜ一方の行列がちょうど0から始まるのか。謎の lora_alpha は何を割っているのか。実モデルでのパラメータ数。そして実際にあなたが回す3つのつまみ。
前提: ランクと、低ランク行列に足りないもの
以下すべてがここに乗っているので、手早く定義します。行列のランク(階数)とは、その中にある線形独立な方向の数——ほかの行の組み合わせにすぎない行を捨てたあとに残る、本当に別個の行(列でも同じ)の数です。 の行列はランク4096までありえます。それよりはるかに小さいランクなら低ランクです。
有用な帰結が因数分解です。ランク の任意の行列 は、縦長で細い行列と、横長で薄い行列の積として書けます。
要するに、ランク の行列は、細い行列2枚の掛け算として丸ごと書き直せる、ということです。
これでどれだけ節約できるか数えましょう。 を直接持つと 個の数。ペアで持つと 個。、 なら 16,777,216 個が 65,536 個に——256分の1です。そして算数は逆向きにも働きます。 と が何を含んでいようと、その積 のランクは を超えられません。情報は途中で 次元のボトルネックを通らねばならないからです。これがLoRAが課す制約であり、問題はその制約がタスクにとって許容できるかどうか、ただそれだけです。
直感: モデルは低ランクでないが、更新は低ランク
ここが飛躍です。学習済みの重み行列 が低ランクだ、と主張する人はいません。 は密で、容量を使い切っています。LoRAの主張は、微調整後の重みと学習済みの重みの差についてのものです。
要するに、 は「微調整で重みがどれだけ動いたか」という差分そのものです。
賭けているのは、これが低ランクだ、あるいは十分に近い、ということ。なぜそうなるのか? 事前学習がすでに汎用の機構——文法、世界知識、パターンに従う能力——を据え付けているからです。下流タスクへの適応はその機構を作り直すのではなく、重みづけを変えて舵を切る。舵を切ることは、作ることよりはるかに低次元の行為です。
この直感には実験的な先例があります。Aghajanyanら(arXiv:2012.13255)は微調整の内在次元(intrinsic dimensionality)を測りました。更新をランダムに選んだ低次元部分空間に制限したところ、驚くほど少ない次元でフル微調整の性能の大半に到達でき、しかも事前学習モデルが大きいほど内在次元は低かった。タスクの更新が本当に小さな部分空間に住んでいるなら、 を低ランクに強制することは必要なものを捨てているのではなく、タスクが使わない容量の代金を払うのを断っているだけです。
そこから設計が従います。 を学習しない。低ランクな を学習して足す。
手法を1行ずつ
1行目: 更新は上書きではなく加算
要するに、微調整後の重みは「元の重み+補正」であり、元の重みには手を付けない、ということです。
は学習済みの重み行列、形は 。 は微調整後のモデルが使う重み。 が補正です。まだ何も得ていません——差に名前を付けただけ——が、これが鍵となる一手を用意します。 は決して変更されない。 事前学習が残したそのままの姿でとどまり、学習はすべて の内側で起こります。
2行目: 補正を低ランクに強制する
要するに、補正は必ず幅 の細い通り道を通る形でしか作れない、ということです。
これが1行に収まった考えの全部です。データが流れる向き、つまり右から左へ読むと、 が 次元の入力を受け取って 次元へ圧縮し、 がその 個の数を受け取って 次元へ展開する。 がランク——ボトルネックの幅——で、LoRAの容量を定義するただ1つのハイパーパラメータです。すべてが 次元を通らねばならないので、 が罰則項ではなく構成によって保証されます。
3行目: 順伝播で実際に走るもの
を行列として作ることはありません。入力 に対し、積を展開します。
要するに、元のレイヤの出力に、小さな横道の出力を足しているだけです。
2つの経路の和です。 は元のレイヤで、手つかず、以前とまったく同じ計算。 がLoRA枝で、 として評価します——まず 個の数への安い圧縮、次に展開。枝のコストは 回の積和で、元のレイヤの に対して、・ なら追加の計算量は1 %を大きく下回ります。
4行目: ベースを凍結し、ペアだけを学習する
要するに、損失を下げるために動かしてよいのは と だけで、 は定数として扱う、ということです。
この記事のすべての節約を生むのがこの行です。 は勾配もオプティマイザ状態も受け取りません。定数です。勾配は と にだけ流れるので、請求書を支配していた1パラメータ8バイトのAdamモーメントは、レイヤあたり 個ではなく 個のぶんだけ確保されます。
安くならないものに注意してください。順伝播は依然としてモデル全体を通り、逆伝播のために活性化は依然として保存されます。 LoRAが削るのはオプティマイザと勾配のメモリで、ベースモデルの重みのメモリ(bf16の14 GBは相変わらずロードします)でも活性化のメモリでもありません。これがLoRAの利得についての最もよくある誤読です。LoRA論文はGPT-3 175Bについて、学習パラメータ数が10,000分の1、GPU メモリが約3分の1と報告しています。この2つの数が3桁以上も開いているのは、まさにこの区別のせいです。
5行目: 初期化 — なぜ は0から始まるのか
はランダムに初期化され(ガウス、または参照実装ではKaiming一様)、 はすべて0に初期化されます。したがってステップ0では、
要するに、学習開始の瞬間は、LoRAを付けていないモデルとまったく同じ出力を返す、ということです。
微調整モデルは学習済みモデルの完全なコピーとして始まります。丹念に学習されたネットワークにランダムなノイズが注入されないので、初期の損失スパイクもなく、ベースモデルの品質まで戻るために温め直す必要もありません。学習は使える最良の点から始まり、そこから登っていきます。
なぜ両方でなく片方だけを0にするのか? と が両方0なら、損失のそれぞれに関する勾配も0になり(各々の勾配はもう一方に比例するため)、ペアは永遠に0で立ち往生します——死んだ対称性です。ちょうど片方を0にするとそれが破れます。 が出力を0にする一方、 が の勾配を生かしておくので、最初のステップが を0から動かし、枝が命を得ます。
6行目: スケーリング係数
実際には枝はスケーリングされます。
要するに、補正の強さは で決め、ランクを変えても強さがぶれないように で割っている、ということです。
(どのライブラリでも lora_alpha)は自分で設定する定数で、これをランクで割ります。理由は、 の典型的な大きさが とともに増える——ボトルネックが広いほど足し合わされる項が多い——ため、この割り算がないとランクを2倍にすると更新の実効的な大きさもおよそ2倍になり、丹念に調整した学習率が通用しなくなるからです。 で割ることで学習率がランク間でおおむね転用可能になり、それが を「容量と歩幅の両方のつまみ」ではなく「容量のつまみ」にしています。
実務上の帰結は、更新の強さに効くのは比 だけだということ。r=8, alpha=16 と r=16, alpha=32 はスケールが同じ2で、容量が違います。ありがちな事故: アダプタに余裕を持たせようと を8から64に上げ、 を16のままにし、実効的な更新を黙って8分の1に縮め、そして「ランクを上げても何も起きなかった」と結論する。
実モデルで数え上げる
7B級のLlama型モデルを取ります。隠れ次元 、Transformerレイヤ32層。論文が推奨するペアである query と value の射影に、 でLoRAを当てます。どちらも です。
適応する行列1つあたり、
要するに、4096×4096の行列1枚につき、学習するのは約6.5万個だけ、ということです。
レイヤあたり2行列、32層なので、
要するに、モデル全体に当てても、学習するのは約420万個にとどまる、ということです。
合計67.4億に対して0.062 %——おおよそ1,600個に1個です。そして帰結が積み上がります。
| フル微調整 | LoRA(、q と v) | |
|---|---|---|
| 学習パラメータ | 67.4 億 | 420 万 |
| オプティマイザ+勾配の状態 | 約 108 GB | 約 50 MB |
| タスクごとの保存物 | 約 14 GB | 約 16 MB |
| メモリ上のベース重み | 約 14 GB | 約 14 GB(凍結) |
最後の行が誠実な部分です。LoRAはモデルを小さくしません。消し去るのはオプティマイザ状態で、運用を変えるのは最後から2番目の行——14 GBのチェックポイントではなく16 MBのアダプタ。だから1つのベースモデルを配信したまま、何百ものタスク別アダプタをリクエストごとに差し替えて載せられる。これがLoRAが本当に解き放ったデプロイ形態です。
コード上の置き換えは平凡で、それも魅力の一部です。
import torch
import torch.nn as nn
class LoRALinear(nn.Module):
def __init__(self, base: nn.Linear, r=8, alpha=16):
super().__init__()
self.base = base
for p in self.base.parameters(): # 学習済み重みを凍結
p.requires_grad = False
self.A = nn.Parameter(torch.randn(r, base.in_features) * 0.01)
self.B = nn.Parameter(torch.zeros(base.out_features, r)) # 0 なので ΔW = 0
self.scale = alpha / r
def forward(self, x):
return self.base(x) + self.scale * (x @ self.A.T) @ self.B.T
手法の全部を担っているのは3つの細部です。ベースへの requires_grad = False、B の zeros、そしてスケールとしての alpha / r。
3つの罠は、同時に唯一の3つの判断でもある
ランクを選ぶ
は容量の上限で、どちらの方向にも失敗モードがあります。小さすぎれば、更新がタスクに必要なものを物理的に表現できません——どれだけ長く学習しても未学習(underfit)のままです。良い答えを含まない部分空間に解を制限してしまったからです。大きすぎれば、LoRAの利点を失いつつ(パラメータが増え、アダプタが大きくなり、小さなデータセットに過適合する余地が広がる)、必ずしも品質は上がりません。
人を驚かせる実験的な発見は、「十分」がどれほど小さいかです。LoRA論文は、彼らのタスクでは ですでに競争力があり、 と で学習したアダプタの上位特異方向が大きく重なっていたと報告しています——余分なランクの多くは、寄与の小さい方向に費やされていました。そこから従う目安: 文体・フォーマット・トーンなら か から始め、本当に新しいドメイン知識を教えるときに 〜 を検討する。 そして を上げずに を上げると更新が縮むので、両方を一緒に変えるか、比を意図して固定してください。
低ランクで未学習に見えたとき、最初に上げるべきは ではなく学習率の確認です。LoRAはフル微調整より1桁高い学習率を好むのが典型です( ではなく 程度)。学習されるパラメータが少なく、初期化されたばかりで、 で縮められているからです。「LoRAではこれが学習できない」という報告の相当な割合は、設定ファイルに残されたフル微調整用の学習率です。
どのレイヤを適応するか選ぶ
LoRAは行列ごとに適用するので、どれに当てるかを自分で決めます。元論文は固定のパラメータ予算のもとで、少ない予算を と に分散させるほうが、1つの行列に高ランクで集中させるより良いことを見出しました——深さより広がりが効いたのです。現在の実務はさらにその方向へ進んでおり、予算が制約でないなら、アテンション()とMLPの射影を含むすべての線形層を対象にするほうが概して良く、ツールが target_modules="all-linear" と呼ぶのがこれです。
罠は「静かな当て漏れ」です。target_modules が自分のアーキテクチャの何にもマッチしないパターンを指していると(射影の名前はモデル系統ごとに違うので簡単に起こります)、学習は走り、たまたまマッチしたわずかな行列のおかげで損失も下がり、しかし意図よりはるかに少ししか学びません。毎回の実行前に学習パラメータ数を印字してください(PEFTなら print_trainable_parameters())。0.5 %を期待していたのに0.01 %と出たら、パターンが外れています。
もう1つの罠は、適応した行列のどれにも手が届かないものをタスクが必要とする場合。トークナイザに新しいトークンを追加したなら、そのトークンに対応する埋め込みと出力ヘッドの行は凍結されたランダム値のままで、アテンション層をどれだけ適応させても直りません。それらのモジュールは学習する(またはトークン追加を避ける)必要があります。
推論時のマージ
枝は線形で加算なので、学習が終わればベース重みに畳み込めます。
要するに、学習が終われば補正を元の重みに足し込み、ふつうの行列1枚に戻せる、ということです。
そうするとLoRA行列は消えます。配信されるモデルは元のアーキテクチャ、元のパラメータ数を持ち、そして——これが、順伝播に追加モジュールを挿し込むボトルネック型アダプタに対するLoRAの看板の利点ですが——追加の推論レイテンシがちょうど0になります。
実務でここで壊れるものが3つあります。
量子化されたベースへのマージは精度を失う。 QLoRAではベース重みが4ビットで保持され、 と はより高い精度で学習されます。16ビットの更新を4ビットのテンソルに足すということは再量子化を意味し、小さな更新は一部が丸めで消えます。これは評価(量子化を解いたベースにアダプタを付けた状態)と本番(マージ済み)の間の品質低下として現れます。アダプタをマージしないままにするか、量子化コピーではなく元の高精度重みにマージしてください。
マージはLoRAを使いたかった理由そのものを手放す。 マージしていないアダプタはリクエストごとに差し替えられ、ロード済みのベースモデル1つで多数のタスクを配信できます。マージしたものは丸ごと別のモデルです。1タスクを高トラフィックで配信するならマージし、多数を配信するならアダプタを分けたままに。
アダプタ2つは単純には足せない。 は算数としては定義できますが、2つの振る舞いの信頼できる合成ではありません。各更新は他方が無い前提で学習されており、方向が衝突しうるからです。ランク8のアダプタ2つを合わせたものはランク8ではなく最大16にもなります。複数アダプタのマージは、構成上うまくいく操作ではなく、評価すべき実験として扱ってください。
現場ではこう使う
LoRAを日常的に触るのは、公開済みの基盤モデルを自社の文体・出力フォーマット・ドメイン用語に合わせたい機械学習エンジニアです。典型的な場面は「プロンプトの工夫だけでは出力形式が安定しない」「タスクごとにモデルを丸ごと複製して保存・配信する余裕がない」とき。画像生成モデルで画風やキャラクターを覚えさせる用途でも、同じ仕組みが広く使われています。
実際に触る設定。 Hugging Face の PEFT なら LoraConfig の数個がすべてです。
rとlora_alpha— 容量と強さ。比 を意識して一緒に動かす(上の節のとおり)target_modules— どの線形層に当てるか。モデル系統ごとに名前が違うので、model.named_modules()で実名を確認してから書くlora_dropout— LoRA枝の入力に掛けるドロップアウト。データが少ないときの過学習対策modules_to_save— LoRAではなく丸ごと学習・保存したいモジュール(新トークンを足したときの埋め込みや出力ヘッドなど)print_trainable_parameters()で割合を確認し、保存はsave_pretrained()(アダプタの重みとadapter_config.jsonだけが出る)、マージはmerge_and_unload()
知らないと事故になる落とし穴。
- アダプタは学習したベースモデルにしか合わない。 アダプタが持っているのは差分 だけなので、別のベース(同名の別リビジョン、別の量子化版を含む)に載せると、形が合えばエラーも出ずに読み込まれ、出力だけがおかしくなります。
adapter_config.jsonのbase_model_name_or_pathとリビジョンを配信側と揃えてください。 - 学習時と推論時でプロンプトの形式を変えない。 アダプタは学習データの書式(チャットテンプレート、区切り記号)ごと覚えます。推論側で書式が違うと、アダプタが効いていないように見えます。
- 評価は出荷する成果物そのもので行う。 マージ前後、量子化の有無で別物になりうるのは上で見たとおりです。
面接・設計レビューで問われる問い。
- 「LoRAでメモリが減るのはどこで、減らないのはどこか」→ 減るのは勾配とオプティマイザ状態。ベース重みと活性化は減らない
- 「なぜ を0で初期化するのか。両方0ではだめな理由は」→ 開始時に元モデルと一致させるため。両方0だと勾配が互いに0で動かない
- 「
rを上げたのに効かなかった。何を疑うか」→ を据え置いて実効スケールが縮んでいないか、学習率がフル微調整用のままでないか、target_modulesが実際にマッチしているか - 「マージすべきか」→ 1タスクを大量に配信するならマージしてレイテンシ0、多数のタスクを配信するならアダプタを分けたまま差し替える
まとめ
- フル微調整は1パラメータあたり約16バイトのオプティマイザ状態を請求する——7Bモデルで100 GB超——それも、モデルよりはるかに小さい変化のために
- LoRAは を凍結し、更新を細い行列2枚の積として学ぶ: 、 が構成によって更新を低ランクに強制する
- は0から始まるので学習は学習済みモデルの完全なコピーから始まる。 はランダムなので対称性が破れ、勾配が流れる
- のスケーリングが学習率をランク間でおおむね転用可能にする。更新の強さを決めるのは比だけで、だから だけを上げると更新が弱まることがある
- q と v に なら、7Bモデルの学習パラメータは420万(0.062 %)で、14 GBのチェックポイントではなく約16 MBのアダプタを出荷できる——ただしベース重みと活性化は相変わらずメモリを丸ごと占める
- 本当の判断は3つ: ランク、どの行列を対象にするか(数を確かめる。パターンを信じない)、そしてマージするか——マージはレイテンシ0を買うが、リクエストごとの差し替えを手放し、量子化ベースでは品質が落ちる
実務でのフル微調整との比較はLoRA対フル微調整へ。原論文自身の測定値の精読はLoRA論文精読へ。ランクと因数分解の言葉があやしいと感じたら、まずAIのための線形代数から。
確認問題
- あるレイヤが (LlamaのMLP射影)です。 のLoRAアダプタは学習パラメータを何個追加し、それは元の行列の何 %にあたりますか。
- あなたは
r=8, lora_alpha=16で学習し、良い結果を得ました。同僚があなたの設定をコピーし、「容量を増やす」ためにr=64にして他は何も変えず、モデルがベースからほとんど動かないと報告しました。何が起きたのか、また元の更新の強さを取り戻すにはlora_alphaをいくらにすべきですか。 - と を両方0に初期化すると学習が完全に止まるのに、 だけを0にすると止まらないのはなぜか説明してください。
- 4ビット量子化されたベースにアダプタを付けた状態で評価すると好成績でした。本番用にアダプタをマージするとスコアが落ちました。考えられる原因と、それを避ける方法を2つ挙げてください。
解答
- は 、 は なので、 パラメータ。元の行列は なので、アダプタはその約 0.54 % です。
- 更新のスケールを決めるのは比 だけです。元のスケールは で、変更後は ——8分の1に弱い更新なので、モデルはベースの近くに留まります。 でスケール2を取り戻すには
lora_alpha = 128。(ただしこれで戻るのは強さだけで、元の振る舞いではありません。アダプタの容量は8倍になっており、学習率は再調整が必要かもしれません。) - LoRA枝の出力は です。 に関する勾配は に比例し、 に関する勾配は に比例します。両方が0なら両方の勾配も0で、どちらも永遠に動きません——ペアは原点に固定されたままです。 だけにすると枝の出力が0になる(だからモデルは学習済みモデルの完全なコピーから始まる)一方、 が の勾配を0でないまま保つので、最初の最適化ステップが を0から持ち上げ、以後は両方の行列が学習されます。
- マージは高精度の更新を4ビットの重みに足すので、和は再量子化されねばならず、小さな更新の一部が丸めで消えます——マージ後のモデルは、あなたが評価したモデルではありません。避け方は、配信時もアダプタをマージしないままにする(評価と配信で同じ構成にする)か、量子化コピーではなく元の高精度重みへマージする。いずれの場合も、出荷するその成果物そのもので評価を再実行してください。
コメント
コメントにはログインが必要です