こしあん
2026-02-07

ModernBERTでGitHubのライセンスの自動判定をやりたかったが難しかった話


2{icon} {views}


ModernBERTはOpenAIのファインチューニングに勝てるのか、GitHubライセンスの自動判定タスクで検証しました。 結果はOpenAIがF1値0.816で圧勝し、複雑な意味理解や推論を要するタスクではEncoderモデルの限界があることが明らかになりました。

はじめに: 何を作りたかったか

GitHub リポジトリの README と LICENSE ファイルを読み込んで、ライセンスに関する 13 の属性を自動判定するシステムを作りたいと考えました。

例えば、以下のような質問に答えるシステムです。

  • そのリポジトリにライセンスファイルはあるか?
  • コードの商用利用は許可されているか?
  • モデルの重みに別途ライセンスがあるか?
  • データセット論文が存在するか?

これは『AI論文年鑑2025』の論文選別時に使用していたもので、当時はOpenAIのファインチューニングモデルを使用していました。これのコストが一定あったので、もっと手軽なモデルがないか選んで、ModernBERTを試してみたというものです。

これを実現するために、手動で 200 件のリポジトリをアノテーションし、そのうち 140 件を学習データ、60 件をテストデータとして用意しました。このデータで PoC(概念実証)を行います。これは前回のOpenAIのファインチューニング時で使用したものです。

問い: ローカルで動く encoder モデル (ModernBERT) は、OpenAI の fine-tuning に勝てるか?

ModernBERT を選んだ理由

2024 年末に公開された BERT 系の encoder モデルである ModernBERT を採用しました。これにはいくつか魅力的な特徴があります。

特徴 詳細
長文対応 最大 8,192 トークンまで対応しており、GitHub の README + LICENSE をほぼ切らずに入力可能。
効率的な attention SDPA (Scaled Dot-Product Attention) を標準搭載。
軽量 パラメータ数は 149M。T4 (16GB) 1 枚でファインチューニングが可能。

ONNX 変換の検証

将来的な CPU 推論を見据えて、ONNX 変換についても検証を行いました。

項目 結果
FP32 ONNX export 成功。精度も PyTorch と一致 (max_diff < 1e-4)。
CPU 推論速度 EC2 (m8i.large) で 1.82倍の高速化を確認。
INT8 量子化 失敗。dynamo export のグラフ構造と量子化ツールが非互換。

ONNX FP32 であれば、PyTorch が不要な軽量コンテナで推論できます。ただし、INT8 量子化については現時点のツールチェーンでは対応できませんでした。

Gradient Checkpointing の重要性

重要な発見もありました。「gradient checkpointing を併用しない LoRA は、VRAM 削減効果がほぼゼロである」 という点です。

Transformer の学習では、パラメータ数よりも中間層の活性化(activation)メモリが支配的になりがちです。checkpointing を使わずに LoRA を適用しても、活性化メモリが数十 GB 規模で残ってしまい、LoRA によるパラメータ削減(オプティマイザ状態の削減)の恩恵が完全に埋もれてしまいます。checkpointing で活性化メモリを圧縮して初めて、LoRA の効果が VRAM 使用量に現れてきます。

また、ModernBERT で checkpointing を有効化するには、以下の 3 つの設定が全て必要です。1 つでも欠けると正しく機能しません。

base.train()                          # training mode にする
base.config.use_cache = False         # KV cache を無効化
base.gradient_checkpointing_enable()  # checkpointing を有効化

ファインチューニング戦略: 安いほうから試す

VRAM ベンチマーク(後述の付録 B)の結果を踏まえて、段階的にアプローチする方針を立てました。

head-only で精度が出る?
  → Yes: 終了(最安)
  → No ↓
LoRA + checkpointing で精度が出る?
  → Yes: 終了(コスパ最良)
  → No ↓
Full finetune + checkpointing

head-only → LoRA → full の順で VRAM 消費量は増加します。

ベンチマーク上では T4 (16GB) に収まる計算でしたが、実際の学習ではデータローダのオーバーヘッドやメモリ断片化の影響もあり、LoRA や Full finetune ではピーク時に 20GB を超える場面も観測されました。家庭用 GPU (16GB〜24GB) で試す場合は、batch size を下げるなどの工夫が必要かもしれません。

実験の記録: 4 つの ModernBERT 実験

タスクの設定

入力は GitHub リポジトリから取得した README と LICENSE ファイルを統合したマークダウンテキストです。出力は、これに対する 13 の属性(ラベル)それぞれの True/False 判定です。

損失関数には BCEWithLogitsLoss を使用し、クラス不均衡への対策として pos_weight を加えました。また、is_dataset_license_commercially_permissive というラベルについては、is_dataset_paper=True のときのみ損失を計算する条件付きマスクを実装しています。

実験 1: head-only Linear — ベースライン

最もシンプルな構成です。ModernBERT の base モデルを凍結(frozen)し、[CLS] トークンの 768 次元ベクトルに Linear 層 (768→13) を載せて学習しました。

設定: L=4096, LR=1e-3, 学習パラメータ ~10K
結果: test macro F1 = 0.665 (best val epoch 29/30)

30 epoch 回しても loss は緩やかに減少し続けており、過学習の兆候はありませんでした。ただし、この実験では MAX_LEN=4096 としたため、約 30% のサンプルで入力が切り詰められています。

実験 2: head-only MLP — head の表現力向上

head の表現力を上げるため、Linear 層を MLP (768→256→ReLU→Dropout(0.3)→13) に変更しました。同時に、MAX_LEN を 8192 に拡張しました。

設定: L=8192, MLP head ~200K params, LR=1e-3
結果: test macro F1 = 0.675 (best val epoch 27/30)

結果として +0.010 の改善が見られました。入力の切り詰めも 30% から 5% に削減されています。dropout のおかげで過学習も起きていません。これが今回の ModernBERT の実験における最良の結果となりました。

実験 3: LoRA — base を部分的に解凍

head-only の限界を超えるため、LoRA (r=8) を attention projection に適用し、base モデルの表現自体を学習させることを試みました。

設定: L=8192, LoRA r=8 (Wqkv, Wo), LR=2e-4 (LoRA) / 1e-3 (head)
学習パラメータ: ~1.15M (LoRA) + ~10K (head)
結果: test macro F1 = 0.663 (best val epoch 8/30)

残念ながら改善は見られず、それどころか train loss が epoch 30 で 0.005 まで落ちるという激しい過学習が発生しました。

Epoch  1: loss=0.807
Epoch  8: loss=0.457  ← best val (ここで打ち止め)
Epoch 15: loss=0.098
Epoch 30: loss=0.005  ← 完全に暗記

原因は、学習データ量に対して学習させるパラメータ数が多すぎたことだと考えられます。112 サンプルに対して 1.15M パラメータは過剰です。今回の設定では dropout=0.1、weight decay=0.01 という標準的な正則化を行っていましたが、それでも過学習を抑えることはできませんでした。r をさらに下げる(1〜2)、適用層を絞る、より強い正則化を追加するといった対策は未検証ですが、この規模のデータセットにおいては LoRA は過学習のリスクが高いと言えます。

実験 4: 全 140 件学習 — OpenAI と条件を揃える

比較対象である OpenAI の fine-tuning は val split なしで全 140 件を学習に使っています。条件を揃えるため、最も良かった MLP head の構成で全件学習を試しました。

設定: 実験 2 と同じ MLP head, Train=140 (val なし)
結果: test macro F1 = 0.647 (last epoch 30)
参考: テストを見て最良 epoch を選ぶと 0.675 (epoch 19) だが、これは評価リークなので主張には使わない

データを 25% 増やしても、macro F1 は改善しませんでした。test F1 は epoch ごとに 0.57〜0.68 と大きく変動しており、val セットがない状態では最適な epoch を選ぶ根拠がありませんでした。

4 つの実験のまとめ: 何を変えても 0.67 に収束する

変えたもの 結果 改善幅
Head を MLP 化 0.665→0.675 +0.010
MAX_LEN 4096→8192 truncation 30%→5% (上記に含む)
LoRA で base 解凍 0.665→0.663 -0.002 (過学習)
Train 112→140 件 0.675→0.647 (last) 改善なし

head の表現力、入力長、base の解凍、データ量。これらを変更してもスコアは 0.67 付近に収束しました。これは、frozen ModernBERT-base の [CLS] 表現から引き出せる情報の上限に達していることを示唆しています。なお、mean pooling などの代替プーリング手法で改善する可能性はまだ残されていますが、今回の PoC では [CLS] + MLP が限界であると判断しました。

OpenAI との対決: 0.675 vs 0.816

OpenAI の fine-tuned gpt-4o-mini (全 140 件で学習) をテストセットで評価したところ、macro F1 = 0.816 という結果が出ました。ModernBERT の最良値 (0.675) に対して、+0.141 という大きな差がつきました。

ラベル別 F1 の比較

ラベル OpenAI ModernBERT (best)
has_license_file 1.000 0.972 +0.028
has_license_in_readme 0.962 0.778 +0.184
is_license_consistent 0.894 0.723 +0.171
is_model_public 0.903 0.712 +0.191
has_separate_model_license 0.667 0.381 +0.286
has_commercial_usage_warning 0.750 0.387 +0.363
is_code_commercial_use_allowed 0.932 0.880 +0.052
is_code_license_commercially_permissive 0.932 0.868 +0.064
requires_author_contact_for_commercial_use 0.000 0.800 -0.800
is_model_commercial_use_allowed 0.933 0.595 +0.338
is_model_license_commercially_permissive 0.933 0.647 +0.286
is_dataset_paper 0.889 0.359 +0.530
is_dataset_license_commercially_permissive (条件付き) 0.750 0.600 +0.150

13 ラベル中 12 ラベルで OpenAI が上回っています(唯一 ModernBERT が勝ったのは requires_author_contact_for_commercial_use です)。特に、「意味理解」が必要なラベルでその差が顕著です。

唯一の例外: requires_author_contact_for_commercial_use

このラベルは 60 件中 25 件が正例という重要なラベルですが、OpenAI は F1 = 0.000 でした。全てを False と予測しており、完全に崩壊しています。一方、ModernBERT は 0.800 と正常に機能しています。

これは、学習データのラベル定義をモデルが誤解した可能性が高いです。fine-tuning データの修正で対処可能と思われますが、「LLM であっても学習データの品質次第で完全に機能しなくなることがある」という良い教訓にはなりました。

なぜ encoder は負けるのか

ModernBERT と GPT-4o-mini の性能差は偶然ではなく、アーキテクチャの構造的な違いに起因していると考えられます。

差の原因 1: frozen encoder のタスク不一致

ModernBERT (encoder) は、文書全体を [CLS] トークンの 768 次元ベクトル 1 本に集約(プーリング)してから分類します。

[8192 tokens] → frozen encoder → [CLS] 768dim → head → 13 ラベル

768 次元というサイズ自体がボトルネックなのではなく、frozen のまま使っているため、「このタスクに必要な情報を [CLS] に集約するように表現を再配置する」学習ができていないことが最大の要因です。

一方、GPT-4o-mini (decoder) は、回答を生成する各ステップで入力中の任意の箇所を”見直しながら”判断を組み立てることができます。

[instruction + 全文] → decoder → 各ステップでコンテキストを参照しつつ逐次生成 → JSON 出力

decoder は instruction tuning を経ているため、「このラベルに答えるにはどこを見るべきか」を生成ステップごとに柔軟に判断できます。frozen encoder + head-only の構成では、この柔軟性が欠けています。

差の原因 2: パターンマッチ vs 推論

ModernBERT の事前学習は MLM (Masked Language Modeling) — つまり穴埋め問題です。

"This software is released under the [MASK] License" → "MIT"

これに対して GPT-4o-mini は instruction tuning と RLHF を経ており、「ライセンス名 → 法的な意味 → 判定」といった推論チェーンを実行できると考えられます。

入力: "CC-BY-NC-4.0"
内部推論: NC = Non-Commercial → 商用利用不可
出力: is_model_commercial_use_allowed = False

この差は、ラベルごとの結果にはっきりと表れています。

ラベルの性質 OpenAI ModernBERT
パターンマッチで解ける has_license_file 1.000 0.972 小さい
推論が必要 is_model_commercial_use_allowed 0.933 0.595 大きい
高度な意味理解が必要 is_dataset_paper 0.889 0.359 極大

「LICENSE ファイルが存在するか」のようなパターンマッチ系のラベルでは差は小さいですが、「README の内容からデータセット論文の有無を判断する」ような推論が必要なラベルでは、その差は圧倒的です。

差の原因 3: モデル規模と知識量

ModernBERT-base GPT-4o-mini
パラメータ数 149M 非公開(ModernBERT-base より大幅に大きいと推測される)
事前学習データ 2T tokens 非公開 (遥かに大量)
ライセンス知識 事前学習で偶然含まれた分 意図的に学習 + instruction tuning

GPT-4o-mini は、ModernBERT-base (149M) とは比べ物にならないほど大規模なモデルであり、学習データ量も豊富であると推測されます。「MIT は permissive」「GPL は copyleft」といった世界知識が最初から備わっているため、140 件という少数のデータでも高い精度が出せるのです。

結局、このタスクは encoder に不利

仮に ModernBERT を全パラメータ fine-tune したとしても、149M というパラメータ数に、わずか 140 件のデータで「ライセンスの意味理解」を教え込むのは困難です。

このタスクは本質的に「文書を読んで意味を理解し、法的な含意を推論する」能力が求められます。これは LLM の instruction-following 能力が圧倒的に有利な領域です。encoder モデルが勝つには、数千件以上の学習データを用意してパターンを網羅的に学習させるか、タスク自体をパターンマッチ系のラベルに限定する必要があります。

結論: どちらを選ぶべきか

判定結果

コストが許容できるなら OpenAI fine-tuning 一択です。 0.14〜0.17 という精度の差を ModernBERT 側で埋めようとする努力は、費用対効果が見合わないでしょう。

観点 OpenAI ft:gpt-4o-mini ModernBERT head-only MLP
Test macro F1 0.816 0.675
推論速度 1-2 秒/件 (実測) ~0.01 秒/件
推論コスト (1万件/月) ~$5 (※下記試算) ~$0 (GPU 電気代のみ)
データ外部送信 あり (API) なし (ローカル)
致命的な弱点 requires_author_contact_for_commercial_use = 0.000 意味理解系ラベル全般

コスト試算の前提: 入力 ~2,000 tokens/件、出力 ~200 tokens/件と仮定。ft:gpt-4o-mini の価格(入力 $0.30/1M tokens, 出力 $1.20/1M tokens)で計算すると、1万件/月でおよそ $8.4 程度です(実際のトークン数により $5〜$10 の幅があります)。

OpenAI を選ぶ場合の注意点

requires_author_contact_for_commercial_use が F1 = 0.000 となっている点については修正が必須です。学習データのラベル定義を明確にするか、正例パターンを追加することで対処できる見込みです。これが修正されれば、macro F1 は 0.87 前後まで向上すると予想されます。

ModernBERT が再び選択肢に入る条件

以下のような条件下では、ModernBERT を検討する価値があります。

条件 理由
処理量が数万件/月以上 API コストがローカル GPU 運用のコストを上回る場合
リアルタイム推論が必要 OpenAI の 1-2 秒/件というレイテンシが許容できない場合
データを外部に送信できない セキュリティ要件でデータを社外に出せない場合
学習データが 1,000 件以上 LoRA が有効に機能し始める規模の場合

PoC で得られた知見

今回の実験を通じて、encoder モデルの fine-tuning について以下の実践的な知見が得られました。

  1. frozen encoder + head-only は最初に試すべき最安の選択肢。わずか 112 サンプルでも macro F1 = 0.675 を達成。
  2. 小規模データ (100 件台) + 高 rank の LoRA は過学習のリスクが高い。r=8 ではパラメータ過多。r の削減やより強力な正則化が必要。
  3. gradient checkpointing は T4 (16GB) では必須。L=8192 の場合、これがないと OOM になるが、有効にすれば 12.27 GB に収まる。
  4. LoRA は必ず checkpointing と併用すべき。ckpt なしの LoRA は VRAM 削減効果なし。
  5. encoder vs LLM の使い分けはタスクの性質で決まる。パターンマッチ系なら encoder で十分だが、推論や意味理解が必要なら LLM が圧倒的に有利。

付録

A. 13 ラベルの一覧

# ラベル 正例率 (train) 性質
1 has_license_file 91% パターンマッチ
2 has_license_in_readme 59% 構造理解
3 is_license_consistent 62% 構造理解
4 is_model_public 57% 意味理解
5 has_separate_model_license 17% 意味理解
6 has_commercial_usage_warning 30% 意味理解
7 is_code_commercial_use_allowed 61% パターンマッチ
8 is_code_license_commercially_permissive 58% パターンマッチ
9 requires_author_contact_for_commercial_use 44% 構造理解
10 is_model_commercial_use_allowed 29% 意味理解
11 is_model_license_commercially_permissive 27% 意味理解
12 is_dataset_paper 21% 意味理解
13 is_dataset_license_commercially_permissive 4% 条件付き評価

B. VRAM ベンチマーク (L=8192, checkpointing ON)

※以下はダミーデータを用いた単体ベンチマークの理論値です。実際の学習スクリプトを回した際は、データローダやシステムのオーバーヘッドにより、LoRA や Full finetune で 20GB 以上 を消費するケースがありました。

方式 B=1 B=4 B=8
head-only 1.40 GB 3.84 GB 7.09 GB
LoRA r=8 1.41 GB 3.85 GB 7.10 GB
Full finetune 3.38 GB 6.43 GB 12.27 GB

C. 実験設定の詳細

設定 head-only Linear head-only MLP MLP 全件 LoRA
Train 件数 112 112 140 112
MAX_LEN 4096 8192 8192 8192
Head 構造 Linear (768→13) MLP (768→256→13) MLP (768→256→13) Linear (768→13)
学習パラメータ ~10K ~200K ~200K ~1.16M
LR 1e-3 1e-3 1e-3 2e-4 (LoRA) / 1e-3 (head)
Test macro F1 0.665 0.675 0.647 (last) 0.663

D. pos_weight (クラス不均衡の補正)

ラベル pos_weight 意味
has_license_file 0.10 正例 90% → 負例を重視
is_dataset_license_commercially_permissive 17.67 正例 4% → 正例を大幅に重視
has_separate_model_license 4.89 正例 17%
is_dataset_paper 3.87 正例 21%


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です