Claude Media
Claude CodeでLoRAのrank・alpha・target_modulesを決め、過学習を確かめる

Claude CodeでLoRAのrank・alpha・target_modulesを決め、過学習を確かめる

PEFTのLoraConfigは、層名の確認、rankとalphaの対応、評価損失の比較の順に決めると根拠が残ります。Claude Codeに任せる作業の切り方と、過学習の見分け方をまとめます。

LoRAの設定で迷うのは r(rank)、lora_alpha、target_modules の3つです。値を勘で置くと、学習が走っても「なぜその値か」が後から説明できません。Claude Codeには、層名の洗い出し、rankごとの再学習、評価値の表づくりを任せられます。判断の材料が数字で残る形に作業を切るのが要点です。

この記事では、LoraConfigの各項目が何を決めるかを整理したうえで、層名の確認スクリプト、rankの比較、過学習の見分け方を順に示します。プラグインの導入と学習ジョブの投入は、huggingface-skillsプラグインの解説とSageMaker AIプラグインの解説が扱っています。

LoraConfigで決める3項目と既定値

PEFTのLoRAは、重み行列を更新する代わりに、2つの小さな低ランク行列を学習します。元の重みは固定されたままで、学習される量は主にrankと対象の行列の形で決まります。

項目役割既定値
r役割低ランク行列の次元。大きいほど学習できる容量が増える既定値8
lora_alpha役割スケーリング係数既定値8
target_modules役割LoRAを差し込むモジュール名既定値未指定(アーキテクチャから自動選択)
lora_dropout役割LoRA層のドロップアウト確率既定値0.0

target_modules を未指定にすると、モデルのアーキテクチャに応じてPEFTが対象を選びます。アーキテクチャが未知なら、エラーになり、手で指定する必要があります。文字列で渡すと正規表現として照合され、リストで渡すと完全一致、またはモジュール名がその文字列で終わるかどうかで照合されます。

PEFTの既定では、各アテンションブロックのクエリとバリューの層に学習可能な重みが加わります。QLoRAのように全ての線形層へ広げたいときは、target_modules="all-linear" を指定します。層名はアーキテクチャごとに違うため、個別に名前を並べるより楽な指定です。

PEFTの例では、r=16、lora_alpha=16、lora_dropout=0.1、対象は query と value でした。このときの学習可能パラメータは667,493個で、全体8,654万個の約0.77%です。rankを上げるとこの割合が増え、容量と過学習のしやすさが同時に動きます。

層名はモデルから読む

target_modules の名前は、モデルの実装によって q_proj、query、c_attn などに分かれます。記憶で書くと、層が見つからずエラーになるか、意図しない層に差し込むことになります。

Claude Codeには、重みを読み込まずに線形層の名前だけを数えさせます。次のスクリプトは例です。

# scripts/inspect_layers.py
import sys
import torch
from collections import Counter
from transformers import AutoConfig, AutoModelForCausalLM
 
name = sys.argv[1]
config = AutoConfig.from_pretrained(name)
with torch.device("meta"):
    model = AutoModelForCausalLM.from_config(config)
 
counter = Counter()
for module_name, module in model.named_modules():
    if isinstance(module, torch.nn.Linear):
        counter[module_name.split(".")[-1]] += 1
 
for suffix, count in counter.most_common():
    print(f"{suffix}\t{count}")

出力は、末尾の層名と出現数の一覧になります。たとえば q_proj が層の枚数と同じ数だけ並ぶなら、アテンションの射影が1層に1つあるとわかります。MLP側の名前も並ぶので、「アテンションだけに差すか、全ての線形層に広げるか」を数字を見て選べます。

meta デバイスで作るのは、重みを読み込まずに済ませるためです。ただし lm_head のような出力層も一覧に出ます。all-linear では、PEFTが対象から出力層を除外します(PreTrainedModel の場合)。手でリストを書くときは、出力層を含めるかどうかを自分で決めます。

層構造に偏りがありそうなら、PEFTの find_kappa_target_modules で候補を機械的に出す手もあります。top_p=0.2 のような引数で対象を絞り、返った target_modules を LoraConfig に渡します。

rankとalphaの対応を固定して比較する

lora_alpha はスケーリング係数です。PEFTの既定では、アダプターの出力が lora_alpha / r 倍されます。use_rslora=True を指定すると、この係数が lora_alpha / sqrt(r) に変わります。PEFTの説明では、こちらのほうが良い結果を示すと証明されたとされています。

rankだけを動かすと、スケーリングも同時に変わってしまいます。比較実験では、どちらを動かしているかを分けて扱うと読み解きやすくなります。

  • rankの影響だけを見たいなら、lora_alpha / r が一定になるように lora_alpha をrankに比例させる(例: r=8 でalpha 16、r=16 でalpha 32)
  • alphaの影響を見たいなら、rankを固定してalphaだけを動かす

層ごとに値を変えたいときは、rank_pattern と alpha_pattern を LoraConfig に渡します。キーに層名(正規表現も可)、値にrankやalphaを書き、書いていない層は既定の r と lora_alpha になります。PEFT側は、迷ったら正規表現を凝らず、^ 付きでフルネームを書くよう勧めています。

書き方の例を示します。層名は inspect_layers.py が出した名前に合わせて置き換えます。

config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    rank_pattern={"^model.layers.0.self_attn.q_proj": 16},
    alpha_pattern={"^model.layers.0.self_attn.q_proj": 32},
    task_type="CAUSAL_LM",
)

^ を付けない "q_proj" のようなキーは、名前に q_proj を含む全ての層に一致します。先頭の1層だけを変えたいのに全層のrankが変わった、という取り違えは、キーの先頭に ^ を置くかどうかで起きます。print_trainable_parameters() の数がrankの変更に合わせて動いたかを見ると、パターンが狙いどおりに当たったかを確かめられます。

対象を広げるか、rankを上げるかは、容量の足し方が違います。target_modules を all-linear に広げると、差し込む場所が増えます。rankを上げると、1か所あたりの容量が増えます。評価損失が足りないとき、どちらを先に動かしたかを lora_sweep.json に残しておくと、効いた要因を後から切り分けられます。

rankを振る比較スクリプトの骨格

次の骨格は、rankを複数試し、学習損失と評価損失を1行ずつJSONに残します。train_and_eval の中身は、使っている学習ループに合わせて書きます。

# scripts/sweep_lora.py
import json
import sys
from peft import LoraConfig, get_peft_model
from my_training import load_base, train_and_eval  # 自分の学習コード
 
TARGETS = sys.argv[1].split(",")  # 例: q_proj,v_proj
results = []
 
for r in [4, 8, 16, 32]:
    base = load_base()  # rankごとにベースモデルを読み直す
    config = LoraConfig(
        r=r,
        lora_alpha=2 * r,
        target_modules=TARGETS,
        lora_dropout=0.05,
        task_type="CAUSAL_LM",
    )
    model = get_peft_model(base, config)
    model.print_trainable_parameters()
    train_loss, eval_loss = train_and_eval(model)
    results.append({"r": r, "train_loss": train_loss, "eval_loss": eval_loss})
 
with open("lora_sweep.json", "w") as f:
    json.dump(results, f, indent=2)

ベースモデルをrankごとに読み直しているのは、前の実験のアダプターが残った状態で次の実験を始めないためです。alpha = 2r は比を2に固定する一例で、この値が最適という意味ではありません。

学習損失は下がるのに評価が落ちるとき

過学習の見分けは、学習データとは別の評価データで同じ指標を測るところから始まります。評価データは、学習データを分割した時点で切り離し、チューニングの途中で中身を学習に混ぜません。評価データを使って設定を探索する流れは、/claude-api hillclimbの解説にある、trainとtestを分ける考え方と同じです。

lora_sweep.json を表にすると、次のような形になります(数値は読み方を示す仮の例です)。

r学習損失評価損失読み方
4学習損失1.42評価損失1.51読み方差が小さい。容量が足りない可能性
8学習損失1.18評価損失1.39読み方評価も下がっている
16学習損失0.92評価損失1.37読み方評価の改善が頭打ち
32学習損失0.61評価損失1.46読み方学習は下がるのに評価が悪化

見るのは、学習損失と評価損失の差の広がり方です。この例では、rank 16から32で学習損失が大きく下がる一方、評価損失は上がっています。容量が増えた分を、学習データの癖の暗記に使っている状態です。

判断の手順は次のとおりです。

  1. 評価損失が最小になるrankを探す(例では16)
  2. 評価損失が横ばいで、パラメータ数が少ない側を採る(rank 8と16が近いなら、小さい方)
  3. 最小点より大きいrankで学習損失だけが下がり続けていたら、過学習の疑いとして記録する

学習を始める前の評価損失も、1行目として表に入れておきます。PEFTの既定の初期化では、AをKaiming-uniform、Bをゼロで始めるため、学習前のアダプターは恒等変換になり、出力はベースモデルと同じです。この行が、どのrankでも「何も学習していない状態」の基準値になります。学習後の評価損失がこの値より悪ければ、設定の優劣以前に、学習が評価データの形式とずれている疑いがあります。

1回の学習内でも同じ見方ができます。エポックを進めて、評価損失が底を打ってから上がり始めた地点が、学習を止める候補です。過学習が見えたときは、rankを下げる、lora_dropout を上げる(既定は0.0)、対象の層を絞る、の順に手を打つ余地があります。

評価損失が下がっても、出力の質が良くなったとは限りません。損失は学習データと同じ形式のテキストに対する予測の良さを測っており、実際のタスクの合否とは別物です。タスクに合った採点(正解率など)を評価データに加えると、損失の改善が実利に結びついているかを見られます。

Claude Codeに任せる範囲と権限

作業を3つに分け、Claude Codeには「実行して数字を持ち帰る」ところまでを渡します。

  • 層名の一覧を出す(inspect_layers.py)
  • rankごとの再学習と評価(sweep_lora.py)
  • lora_sweep.json を表にして、最小点と過学習の疑いを報告する

CLAUDE.mdには、判断の基準を先に書いておきます。次は書き方の一例です。

## LoRA設定の決め方
- target_modules は inspect_layers.py の出力にある名前だけを使う
- rank を比較するときは lora_alpha / r を固定する
- 評価データ(data/eval)は学習に使わない。中身も編集しない
- 結果は lora_sweep.json に残し、評価損失が最小の r と、学習損失だけが下がっている r を分けて報告する

権限は .claude/settings.json で絞ります。スクリプトの実行だけを許可し、生データの読み取りは拒否します。

{
  "permissions": {
    "allow": [
      "Bash(python scripts/inspect_layers.py *)",
      "Bash(python scripts/sweep_lora.py *)"
    ],
    "deny": [
      "Read(./data/raw/**)"
    ]
  }
}

Read の拒否ルールは、Claudeのファイルツールからの読み取りを止めるものです。パスを指定したdenyルールの書き方は、権限の公式ドキュメントに Read(./.env) や Read(./secrets/**) の例があります。

非対話で回すなら、-p を使います。

claude -p "scripts/inspect_layers.py を実行して、線形層の名前を報告して" \
  --allowedTools "Read,Bash(python scripts/inspect_layers.py *)" \
  --output-format json

--output-format json を付けると、応答に total_cost_usd などのメタ情報が入り、実行ごとの費用を追えます。-p で起動した実行は、--bare を付けない限り、プロジェクトの .claude/settings.json のフックや .mcp.json のサーバー、CLAUDE.mdを読み込みます。上の設定とCLAUDE.mdを効かせたいなら --bare は付けません。CIのように環境差を消したいときは --bare を付け、必要な設定は引数で渡します。

学習データやアダプターの版管理を任せるときは、DVCの権限設計のように、push だけを確認に残す運用と組み合わせられます。

つまずきやすい点

落とし穴

評価データでrankを選ぶと、評価データも使い込まれる

比較を何度も繰り返してrankやdropoutを選ぶと、評価データへの当てはめが起きます。選定用とは別に、最後の1回だけ使う評価データを取っておくと、選んだ設定の最終的な成績を偏りなく測れます。

次の点も実験前に確認しておくと、手戻りが減ります。

  • target_modules が1つも一致しないと、LoRAが差し込まれません。print_trainable_parameters() の出力が想定より極端に小さくないかを見ます
  • GPT-2のように Conv1D を使うモデルでは、fan_in_fan_out=True が必要です
  • bias を all や lora_only にすると、アダプターを無効にしてもベースモデルと同じ出力に戻りません
  • 入力側の埋め込みや出力層を学習対象に加えるときは、重みの共有(tying)の扱いが ensure_weight_tying で変わります

まとめ

LoRAの設定は、層名を数えてから対象を選び、alphaとrankの比を固定して評価損失で比べる順に決めると、後から理由を説明できます。過学習は、学習損失だけが下がり続けて評価損失が上がり始めた地点で見分けられます。

Claude Codeには、スクリプトの実行と結果の表づくりまでを任せ、最小点の選択と最終評価データの扱いは人が決める分担が噛み合います。損失が良くなっても、タスクの採点で確かめるまでは設定の優劣を断定しない運用が安全です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. huggingface.co/docs/peft/package_reference/lora
  2. code.claude.com/docs/en/headless
  3. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn