Docker ComposeでMLflow 3.15.1を作り直し、30問・150 Trace・5つの現行Review Queueを再現する
はじめに
LLMをFine-tuningすれば、本当に回答品質は向上するのでしょうか。
Fine-tuningの解説では、
- データセットを用意する
- モデルを学習する
- 学習後のモデルを評価する
という流れがよく紹介されます。
しかし、実際に試そうとすると、もっと手前に大きな問題があるのではと感じるようになりました。
実際lossが下がっても、全然イケてないことが多いです。
これは掲載中のなろう系小説の第一話を書かせるためにSFT+LoRAでチューニングしても全く精度が上がらなかったり、有名どころのマーケティングデータセットをSFT+LoRAでチューニングしてもやっぱり精度が上がらないという現象がありました。 結果最新モデルにすると精度が上がるというありきたりの結果になっています。
このことから
単にサンプルのデータセットを使ってファインチューニング出来たというやり方ではなく、生成出力の精度が期待通り向上したという結果を出すためにファインチューニングスキル以上に何が重要なのか検証するための実証実験を行っています。
今回はdataset_v1.0の30問と120候補を評価基盤へ登録し、データセットの品質をどう評価するのかという設計まで進めます。元の計画にあるbaseline_dataset_v0.5_candidateは90問×4候補、合計360候補まで作成済みで、元ファイルのSHA-256も固定しました。一方、完成版BaselineとしてのMLflow登録とFine-tuning前の性能測定はまだ実施していません。
前回までの振り返り
前回は評価項目の設計に基づき、有識者に評価依頼方法までを作成しました。
現時点で15名前後の方がご協力頂けそうなので、3−5名に絞った上で評価依頼を行う予定です。
ただ評価結果をもらうまでを考えると9月中旬ぐらいになるため、それまでの間に評価基盤の作成と作成した資料をMLflowに登録していくのが今回の記事の主旨です。
この記事で分かること
- なぜ「データを登録できた」と「実験の問いに答えられる」は別なのか
- 30問と120候補を、後から追跡しやすい形で分ける方法
- 元の実験計画に対して、現行設計に何が足りなかったのか
- Docker ComposeでMLflowを安全に作り直す手順
- 設定後の画面を撮影し、画像の中身まで確認する方法
- 実際に発生した失敗と修正方法
まず、今回の仕組みを身近な言葉で説明する
最初に学校のテストを想像してください。
- 問題帳: 30問の問題文をまとめたもの
- 解答用紙: 1問につき4人分、合計120件の回答
- 入口の自動検査: 人へ渡す前に、コードとLLMだけで条件違反を振り分ける
- 人の評価記録: 入口を通った案の分かりやすさや魅力を、コピー有識者が評価する
- 担当別の箱: コピー有識者用、匿名比較用、専門確認用、権利確認用
今回MLflowに作ったのは、これらを混ぜずに保管する仕組みです。特に、入口の自動検査であるHard Gateをつうかした問題集のみ、有識者がチェックします。有識者が見るのはキャッチコピーのコピー品質の項目だけです。
キャッチコピー群(A〜D)がどれも使えない場合に「それでも一番ましな案」を無理に選ぶと、悪い教師データが残ります。そこで、全案を却下できるだけでなく、有識者が「自分ならこう書く」という別案と理由を残す欄も用意しました。ただし、その場で正解扱いにはしません。別の人による確認を通過してから、次の問題帳へ入れる候補にします。
30問それぞれに4候補あるので、候補は 30 × 4 = 120件 です。さらに、1問ごとに4候補をまとめて比べる記録を30件作ったため、画面に表示される記録は 120 + 30 = 150件 になります。
正式な技術用語は記事の最後にまとめました。途中で言葉が分からなくなったら、まずは「問題帳・解答用紙・採点記録・担当別の箱」と読み替えてください。
Research Question
実験計画編では、単にFine-tuningを実行することではなく、次のような問いを掲げています。
- LLMだけで作った教師データでも品質は上がるのか
- 有識者レビューはどれだけ効くのか
- 評価者同士はどれだけ一致するのか
- 良い評価者を事前テストで選別できるのか
- 30件と100件で結果はどう変わるのか
- A/B PreferenceをFine-tuningに利用できるのか
- A〜D全却下時の有識者案はどの程度必要になるのか
- 教師データの品質と学習後の性能はどう関係するのか
- 完全に未知のデータでも改善するのか
- 100件の教師データを作る費用はいくらか
元の問いと中心仮説は置き換えません。今回は、元の問いへ進む前段階の補助Research Questionを追加しました。
現行のMLflow評価基盤は、実験計画編で掲げた問いへ答えるための設計として十分か。
仮説
30問、120候補、匿名比較、採点記録、担当別の作業箱を分離し、共通のIDで結べば、教師データ品質、人間評価、Fine-tuning、未知データ評価、費用の関係を後から検証できる。
仮説を棄却する条件
元の問いのうち1つでも、必要な情報を保存する場所やIDのつながりがなく、後から比較を再現できなければ棄却します。
ここが前稿からの重要な修正です。「30件を登録できた」「4つの箱を作れた」は作業の成功条件であって、研究上の仮説ではありません。登録件数は、設計監査の対象が壊れていないことを確かめる整合性チェックとして扱います。
先に結論
仮説は棄却しました。
現行設計で、30問、120候補、採点欄、担当別の作業箱を追跡できました。この部分は第一段階の評価基盤として有効です。
一方、元の実験計画全体へ答えるには、次のつながりが不足していました。
- 教師データの版と、その親子関係
- 匿名の評価者ID、事前テスト、評価基準の版
- どの教師データで、どのモデルを、どの設定で学習したか
- 評価に掛かった時間と費用
- 改善作業には使わない「完全に未知の評価データ」の固定情報
つまり、今回作ったものは「採点を始めるための整理棚」までは完成しています。しかし、「採点結果が学習後の品質にどう効いたか」まで証明する実験台帳には、まだ足りません。
仕組みとデータフロー
実線部分は今回構築・確認できた範囲です。Hard Gateは機械処理、有識者評価は人によるコピー品質評価です。専門家・権利担当は機械判定が保留または警告にした候補だけを確認し、H1〜H5を人が再採点する役割ではありません。点線部分は、箱はあっても回答が未入力、または学習結果までの関連付けが未設計です。
技術選定理由
解決した課題
JSONファイルだけで管理すると、評価が増えたときに「どの問題の、どの候補を、誰が、いつ、どの基準で評価したか」が追いにくくなります。そこで、問題、候補、評価を別々に保存し、共通IDでたどれるMLflowを使いました。
採用した構成
MLflow 3.15.1をDocker Composeで動かし、記録情報はSQLite、生成物は別の保存領域へ置きました。Python側は3.14.6とMLflow 3.15.1をuv.lockで固定しました。
この構成を選んだ理由
MLflowを起動するたびに長い設定を手入力すると、前回との違いが分かりません。Composeファイルにバージョン、ポート、保存先、起動方法をまとめると、同じ環境を作り直しやすくなります。
適用条件・制約
今回は1台のMacで設計を検証する段階です。複数人で同時利用する本番環境ではなく、構成を増やさず再現しやすいことを優先しました。高い同時書き込み負荷や本番可用性は検証していません。
検証環境
| 項目 | 実測値 |
|---|---|
| Hardware | Apple M5 Max / 18 cores / 128 GB |
| Architecture | arm64 |
| OS | macOS 26.5.1 (25F80) |
| Python | 3.14.6 |
| Package manager | uv 0.12.2 |
| Docker / Compose | 29.6.2 / v5.3.1 |
| MLflow | 3.15.1 |
| Container | linux/arm64 |
| 接続先 | http://127.0.0.1:5000 |
元々nvidia RTX PRO 6000を購入したかったのですがあまりにも高騰しすぎて諦めました。
Apple siliconはCUDAが使えない、nvidiaよりも遅いというデメリットはありますがUnified Memoryのため
nvidiaよりも安価に高性能なlocalLLMを試せることから採用しています。
ここまで高騰化するとM5 Maxを10台購入してslurmで並列実行した方が、H100/200と似た成果でるんじゃないかと勝手に思っています。
検証手順
1. 新しいMLflowをDocker Composeで起動する
使用したcompose.yamlです。
name: copy-right-mlflow-v1
services:
mlflow:
image: ghcr.io/mlflow/mlflow:v3.15.1
container_name: copy-right-mlflow-v1
restart: unless-stopped
ports:
- "127.0.0.1:5000:5000"
volumes:
- mlflow_db:/mlflow/db
- mlflow_artifacts:/mlflow/artifacts
command:
- mlflow
- server
- --host
- 0.0.0.0
- --port
- "5000"
- --allowed-hosts
- localhost:*,127.0.0.1:*,mlflow,mlflow:5000
- --backend-store-uri
- sqlite:////mlflow/db/mlflow.db
- --artifacts-destination
- /mlflow/artifacts
healthcheck:
test:
- CMD
- python
- -c
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:5000/health', timeout=3)"
interval: 5s
timeout: 5s
retries: 12
start_period: 10s
volumes:
mlflow_db:
mlflow_artifacts:
次の順で、設定の確認、起動、状態確認を行いました。
実行
docker compose config
docker compose up -d
docker compose ps
観測結果
Container: copy-right-mlflow-v1
Architecture: linux/arm64
Health: healthy
Bind: 127.0.0.1:5000:5000
2. Python環境を固定し、データを登録する
Python側はuv.lockでバージョンを固定しました。
[project]
requires-python = "==3.14.6"
dependencies = ["mlflow==3.15.1", "openpyxl==3.1.5"]
実行順です。
実行
UV_CACHE_DIR=/tmp/copy-right-mlflow-v1-uv-cache uv sync --frozen
uv run python scripts/build_expert_review_phase_manifest.py \
--workbook /path/to/expert-copy-review-request-v1.4.xlsx \
--candidate-mapping data/raw/expert_candidate_mapping_v1.0.json \
--output data/raw/expert_review_phase_manifest_v1.0.json
MLFLOW_TRACKING_URI=http://127.0.0.1:5000 \
UV_CACHE_DIR=/tmp/copy-right-mlflow-v1-uv-cache \
uv run python scripts/register_mlflow_assets.py \
--dataset data/raw/dataset_v1.0.json \
--candidate-mapping data/raw/expert_candidate_mapping_v1.0.json \
--gate-results data/raw/system_hard_gate_results_v1.1.json \
--review-phase-manifest data/raw/expert_review_phase_manifest_v1.0.json \
--output results/registration-summary-phase-separated-v1.1.json
# 同じ操作をしても対象が二重登録されないことを確認
MLFLOW_TRACKING_URI=http://127.0.0.1:5000 \
UV_CACHE_DIR=/tmp/copy-right-mlflow-v1-uv-cache \
uv run python scripts/register_mlflow_assets.py \
--dataset data/raw/dataset_v1.0.json \
--candidate-mapping data/raw/expert_candidate_mapping_v1.0.json \
--gate-results data/raw/system_hard_gate_results_v1.1.json \
--review-phase-manifest data/raw/expert_review_phase_manifest_v1.0.json \
--output results/registration-summary-phase-separated-v1.1.json
MLFLOW_TRACKING_URI=http://127.0.0.1:5000 \
UV_CACHE_DIR=/tmp/copy-right-mlflow-v1-uv-cache \
uv run python scripts/verify_mlflow_assets.py \
--dataset data/raw/dataset_v1.0.json \
--candidate-mapping data/raw/expert_candidate_mapping_v1.0.json \
--gate-results data/raw/system_hard_gate_results_v1.1.json \
--review-phase-manifest data/raw/expert_review_phase_manifest_v1.0.json \
--summary results/registration-summary-phase-separated-v1.1.json \
--output results/verification-phase-separated-v1.1.json
観測結果
Dataset records: 30
Candidate Trace: 120
Comparison Trace: 30
Hard Gate Assessment: 720
verification valid: true
2回目の登録後も、問題、候補、比較対象の件数は増えませんでした。
結果
整合性チェック
| 確認対象 | 期待値 | 実測値 | 判定 |
|---|---|---|---|
| 問題帳の問題数 | 30 | 30 | PASS |
| 候補ごとの解答用紙 | 120 | 120 | PASS |
| 匿名比較用の記録 | 30 | 30 | PASS |
| すべての記録ID | 150件が一意 | 150件が一意 | PASS |
| 機械的な事前確認 | 720 | 720 | PASS |
| 校正用の箱 | 13 | 13 | PASS |
| 本評価用の箱 | 77 | 77 | PASS |
| 匿名比較の箱 | 30 | 30 | PASS |
| 専門確認の箱 | 8 | 8 | PASS |
| 権利確認の箱 | 4 | 4 | PASS |
| 匿名比較へのモデル名漏れ | なし | なし | PASS |
720件になる理由は、120候補それぞれに6個の確認結果を持たせたためです。計算は 120 × 6 = 720 です。
設定後の画面
150件の記録が表示されました。
問題帳に30 recordsあることを確認しました。
追加修正前に使っていた4つの独自Queueは77、30、8、4件でした。UIには証跡として残した旧比較QueueとMLflow標準の空default Queueもあり、画面上は合計6件でした。この状態は変更前Evidenceとして残します。
有識者用の設定画面には、箱の名前、8 questions selected、質問のプレビューが表示されています。この8問は、分かりやすさ・適合・便益・独自性・完成度・採否・理由・修正版だけです。hard_gate.*とjudge.*は0問です。設定値を入れた後の画面です。
追加修正:Excelの「修正版」がMLflowへ届いていなかった
有識者への質問項目をいろいろイジっているうちに、当初計画にない質問事項を追加したのを忘れ、後から見直した時に追加質問事項を取り込む設定が抜けている事に気がつきました。
有識者に配布用のexpert-copy-review-request-v1.4.xlsxを入力側から見直しました。
追加修正前の状態
| 入力元 | 入力欄 | MLflow側 | 実際の接続 |
|---|---|---|---|
03_校正評価 |
S列「修正版(修正時のみ)」 | expert.rewrite |
未接続 |
04_本評価 |
S列「修正版(修正時のみ)」 | expert.rewrite |
未接続 |
記事では上の問を列挙しただけで、ExcelからMLflowへ値を取り込む方法を説明していませんでした。登録処理もDataset、候補対応表、Hard Gate結果しか受け取らず、Workbookを読み込んでいませんでした。
さらに、校正13候補は本評価77候補にも含まれます。同じexpert.rewriteへ保存すると、同一候補の「校正時の修正版」と「本評価時の修正版」を区別できません。これは記事説明だけでなく、保存設計そのものの不足です。
修正した保存設計
| Excel | 件数 | MLflowの保存名 | 目的 | Gold/学習利用 |
|---|---|---|---|---|
03_校正評価 |
13候補 | expert.calibration.* |
評価基準を合わせる | 禁止 |
04_本評価 |
77候補 | expert.formal.* |
基準固定後の正式評価 | 昇格条件通過後だけ |
現在のReview画面には、校正13件、本評価77件、比較30件、専門照会8件、権利照会4件の5つを現行Queueとして登録しています。旧個別Queueと旧比較Queue、MLflow標準defaultは変更前Evidenceとして残したため、画面上は合計8件です。
校正Queueは8 questions selectedで、expert.calibration.rewrite、理由、採否、5軸だけを持ちます。
本評価Queueも8問ですが、名前をexpert.formal.*へ分けました。これで同じ13候補を再評価しても校正記録を上書きしません。
3枚とも、Queue名、件数、質問名をAPIと画面で確認してから撮影し、保存PNGを開き直して期待値と一致することを確認しました。
Excelから取り込む処理
また何も有識者から回答をもらっていない段階で取り込みチェックをします。
匿名評価者ID、Rubric version、Workbook SHA-256、Sheet、行番号、親候補IDもAssessmentへ付けます。
# まず校正13件を取り込む。現状は未回答なので0件のまま停止する
uv run python scripts/import_expert_review_workbook.py \
--workbook /path/to/expert-copy-review-request-v1.4.xlsx \
--candidate-mapping data/raw/expert_candidate_mapping_v1.0.json \
--registration-summary results/registration-summary-phase-separated-v1.1.json \
--phase calibration \
--reviewer-id REV_001 \
--rubric-version 1.0 \
--output results/expert-review-calibration-import-v1.0.json
# 本評価は校正13件のimport lockがなければ拒否される
uv run python scripts/import_expert_review_workbook.py \
--workbook /path/to/expert-copy-review-request-v1.4.xlsx \
--candidate-mapping data/raw/expert_candidate_mapping_v1.0.json \
--registration-summary results/registration-summary-phase-separated-v1.1.json \
--phase formal \
--reviewer-id REV_001 \
--rubric-version 1.0 \
--calibration-lock results/expert-review-calibration-import-v1.0.json \
--output results/expert-review-formal-import-v1.0.json
入力規則は「修正を選んだときだけ修正版が必須」です。修正なのに空欄、採用/不採用なのに修正版あり、1〜5以外の点数、理由なし、実名やメールアドレスの評価者IDを拒否します。また、修正版は新しく作られた文章なので、元候補のHard Gate結果を引き継ぎません。
04_本評価で「修正」
→ HUMAN_EXPERT_REWRITEとして親候補ID付きで保存
→ コードとLLMだけでH1〜H5を再実行
→ UNCERTAIN / WARNINGだけ専門・権利照会
→ 提案者とは別のコピー有識者が確認
→ 全条件通過後だけdataset_v1.5候補
実Workbookの検査結果は校正0/13、本評価0/77です。そのため校正importはWAITING_FOR_EXPERT_ANSWERS、MLflow書き込み0件で停止しました。本評価は校正lockがないためBLOCKED_MISSING_CALIBRATION_LOCKで拒否されました。これは未実施を成功扱いしないための停止です。
実回答の代わりに、別のMLflow実験へNON_PRODUCTION_FORWARD_TEST_DO_NOT_PROMOTEを登録しました。校正8 Assessmentと本評価8 Assessmentを同じTraceへ保存し、名前衝突0、両方の修正版再読込、Human source、Hard Gate混入0、自動昇格禁止の7項目が7/7 PASSでした。これは保存経路の検査であり、有識者が評価を完了した証拠ではありません。
候補一覧では、事前確認のPASS / FAIL / UNCERTAINを確認できました。
初回検証の5枚すべてを保存後に開き直し、上記の数値と表示内容が読み取れることを確認しました。
元の実験計画に答えられるか
ここが今回の本題です。
| 元の問い | 現行設計 | 足りないもの |
|---|---|---|
| LLMだけで作った教師データで改善するか | 不足 | 学習記録・モデル・評価指標のつながり |
| 有識者レビューでどれだけ改善するか | 一部あり | レビュー前後の教師データと学習結果のつながり |
| 評価者同士はどれだけ一致するか | 一部あり | 匿名評価者ID、評価者の組、集計方法 |
| 事前テストで良い評価者を選べるか | 不足 | 事前テスト結果と採用グループ |
| 30件と100件でどう変わるか | 不足 | 教師データの版同士の親子関係と比較学習 |
| A/B比較を学習に使えるか | 一部あり | 学習に渡すペアデータの出力規則 |
| A〜D全却下時の有識者案を教師データへ使えるか | 一部あり | 必須入力と追跡は追加済み。二次確認と実測採用率は未実施 |
| 100件作る費用はいくらか | 不足 | 作業時間、単価、費用 |
| 教師データ品質と学習性能の関係は | 不足 | 品質評価と学習後指標のつながり |
| 未知データでも改善するか | 不足 | 固定した未知データのID、hash、固定日時 |
10問すべてに答えられる、という仮説は支持できませんでした。
キャッチコピー群(A〜D)をすべて却下した場合の設計
前の設計にも自由記述欄はありましたが、入力が空でも処理を進められ、提案をそのままGold Copyと呼んでいました。これでは「有識者の新案を必ず残す」「別の人が確認する」という条件を保証できません。
そこで、比較用の作業箱を copy-expert-comparison-v1.1 として新設し、次の3項目へ分けました。
| 項目 | 初学者向けの意味 | 入力規則 |
|---|---|---|
comparison.absolute_decision |
A〜Dを実務で使えるか決める | 全案不採用なら REJECT_ALL
|
comparison.expert_proposal |
有識者が自分で考えた別案 |
REJECT_ALLなら必須。A〜Dをそのまま写すのは禁止 |
comparison.proposal_rationale |
なぜA〜Dでは駄目で、新案なら何を解決できるか |
REJECT_ALLなら必須 |
設定後の画面では、v1.1 Queue、3 questions selected、3つの項目名を確認しました。
判定欄には、全案不採用時に REJECT_ALL を選び、有識者提案と理由を必須にする説明を入れています。
有識者提案欄には、A〜Dの転載を禁止し、この時点ではGold確定でないことを明記しました。
提案理由欄には、A〜Dの不足点と、新案が解決する狙いを書くよう設定しました。
MLflow 3.15.1の標準Label Schemaには「REJECT_ALLを選んだ場合だけ必須」という条件指定がありません。そのため、画面へ3項目を出すだけでは不十分です。保存後に検証処理を通し、提案または理由が空なら INCOMPLETE_REVIEW として次工程へ進めないようにしました。Label Schema APIでは入力形式、説明、コメント欄などを指定できますが、条件付き必須の引数はありません。
A〜Dを比較
→ 全案を却下する
→ 有識者案と理由を記入
→ 未入力・A〜Dの転載なら差し戻す
→ コードとLLMだけでHard Gateを実行する
→ FAILなら却下する
→ UNCERTAIN / WARNINGのときだけ専門家または権利担当へ照会する
→ Hard Gate通過後、別のコピー有識者が品質を確認する
→ 承認後だけdataset_v1.5の候補にする
重要なのは、有識者の提案をすぐ「正解」や「Gold Copy」と呼ばないことです。保存時点の状態は PROPOSED_PENDING_MACHINE_HARD_GATE とし、まずコードとLLMだけのHard Gateへ戻します。その後、必要な照会を解決し、別のコピー有識者が品質を確認します。誰が、どの評価基準で、どの比較記録から提案したかもIDで残します。これにより、有識者案の採用率や、採用後の学習効果を後から計算できます。
仮説と結果の対応
| 仮説 | 反証条件 | 実測 | 判定 |
|---|---|---|---|
| H1 | 1問でも必要な保存先やIDのつながりがない | 学習、評価者、費用、固定Holdoutの関連が不足 | REJECTED |
一方、30問、120候補、30比較と、校正・本評価・比較・専門照会・権利照会の5つの現行担当箱を欠損・重複なく作れたため、「人間評価を始める前の受け皿」としては利用できます。実回答はまだ0件です。
考察
何が正しかったのか
問題帳と候補ごとの解答用紙を分けたこと、匿名比較からモデル名を外したこと、専門確認と権利確認を別の担当箱にしたことは有効でした。元のJSONからMLflow上の記録まで、同じ候補をたどれます。
何が足りなかったのか
実験計画の中心は「採点できるか」ではなく、「良い教師データを作る工程が、学習後の品質へどう影響したか」です。
そのためには、少なくとも次の一本のつながりが必要です。
教師データの版
→ 誰がどの基準で評価したか
→ どの版を使って、どの設定で学習したか
→ どの学習済みモデルができたか
→ 固定した未知データで何点だったか
→ 作成と評価に何時間・いくら掛かったか
現行設計は、このうち最初の「教師データ」と「評価欄」までです。登録成功だけで全体設計が正しいと判断すると、あとから30件対100件や費用対品質を比較できなくなります。
一方、A〜Dを全却下した場合の有識者提案については、今回追加したv1.1設計で「提案を取りこぼさない」ところまで改善しました。ただし、実際の人間レビューと次のDatasetへの採用はまだ実施していません。
次に追加する設計
- 教師データの版、親の版、元ファイルのhashを保存する
- 未知データを早い段階で固定し、改善や学習へ使わないと明記する
- 評価者は氏名ではなく匿名IDで記録し、事前テスト群と評価基準の版を残す
- 評価開始・終了時刻、作業時間、単価、費用を残す
- 学習に使った教師データID、元モデル、学習方法、設定値、生成モデルIDを残す
- A/B比較を学習用ペアへ変換する規則と、有識者提案の採用率を集計する方法を決める
- 全却下時の有識者提案を機械Hard Gateへ戻し、通過後に別のコピー有識者が確認する。専門家・権利担当は機械判定が照会を要求した場合だけ確認し、レビュー済み
dataset_v1.5への昇格条件を実測する
追加検証1:なぜ行い、何を直したのか
追加検証の出発点は、直前の「次に追加する設計」です。この時点で、30問・120候補・評価欄・担当別Queueは登録できていました。しかし、元の実験計画に答えるための7項目は次の状態でした。
| 追加前に必要だった設計 | 追加前の実測 |
|---|---|
| Datasetの版と親子関係 | PASS |
| 未知データの固定 | FAIL |
| 匿名評価者ID・事前テスト・評価基準の版 | FAIL |
| 作業時間と費用 | FAIL |
| 学習Dataset・元モデル・設定・生成モデルのつながり | FAIL |
| A/Bペア出力規則と提案採用率 | FAIL |
| 全却下案の安全な昇格条件 | FAIL |
結果は 1/7 PASS です。これが「問題があったから追加検証した」理由です。追加前の状態を消すと、何を改善したのか比較できないため、Controlとしてそのまま保存しました。
最初の追加検証案の誤り
最初の案は、7番目の昇格条件を「別の有識者・専門家・権利担当が承認する」と一続きに書きました。これは誤りです。Hard Gateとコピー有識者評価を混同し、さらに専門家・権利担当の確認を全件必須のように見せていました。
役割は次の3本に分かれます。
| 役割 | 判定者 | 何を判断するか |
|---|---|---|
| Hard Gate | H1はコード、H2〜H5はLLM | 禁止条件、事実性、表現・安全上の入口条件 |
| コピー有識者評価 | コピー有識者 | 分かりやすさ、ターゲット適合、便益、独自性、完成度、実務採否 |
| 条件付き照会 | 専門家・権利担当 | Hard GateがUNCERTAINまたはWARNINGにした点の外部確認 |
コピー有識者はHard Gateを採点しません。専門家・権利担当も、H1〜H5を人手でやり直すのではなく、機械だけでは確定できなかった候補を必要時に確認します。
修正版で立てた問いと仮説
まず役割分離そのものを、次の問いで監査しました。
Hard Gateとコピー有識者評価は、実データとMLflow上で別の責務として分離されているか。
仮説は「H1はCODE、H2〜H5はLLM_JUDGEだけが判定し、コピー有識者QueueにはHard Gate質問が1件もない」です。これは登録可否ではなく、元の計画で求めた評価設計の境界が守られているかを確かめる仮説です。
役割分離の実測
120候補のMLflow Assessmentと、コピー有識者Queueの質問設定を照合しました。
| 確認内容 | 実測 | 判定 |
|---|---|---|
hard_gate.h1の判定元 |
CODE 120 / HUMAN 0 | PASS |
hard_gate.h2〜h5の判定元 |
LLM_JUDGE 各120 / HUMAN 0 | PASS |
コピー有識者Queue内のhard_gate.*
|
0問 | PASS |
コピー有識者Queue内のjudge.*
|
0問 | PASS |
| 専門家への照会 |
PENDING_SPECIALISTだけ8件 |
PASS |
| 権利担当への照会 | H5 WARNINGだけ4件 |
PASS |
4つの境界検査は 4/4 PASS でした。Queue名も、誤解を招くspecialist-hard-gate-v1.1からspecialist-referral-v1.2へ変更しました。専門家はHard Gateの判定者ではなく、機械判定後の照会先だからです。
設定後のQueue一覧では、specialist-referral-v1.2が8件、権利照会が4件、コピー有識者の個別評価が77件であることを確認しました。
コピー有識者Queueの設定画面を開き直すと、設定済みの8問はexpert.*だけでした。Hard Gate質問は表示されていません。
7項目のBefore / After検証
役割分離を直したうえで、追加前のControlと、追加契約を入れた非本番fixtureを同じ検証器へ通しました。
| 対象 | PASS数 | 意味 |
|---|---|---|
| 追加前のControl | 1/7 | 不足を再現した状態 |
| 修正版の非本番fixture | 7/7 | 7項目の系譜契約だけを満たす非本番例 |
| 意図的に壊した負例 | 6/6を拒否 | 欠損・混同を通さない |
MLflow Runの設定後画面でも、Control 1、修正版7、差分6、負例拒否6を確認しました。human_review.executed=falseとtraining.executed=falseも同じ画面に残しています。
負例には、未知データを学習へ混ぜる、実名メールを評価者IDに使う、費用計算をずらす、学習Dataset IDを欠落させる、提案者が自己承認する、そして有識者をHard Gate判定者へ混ぜる、の6件を入れました。
追加検証2:元の問いへ本当に答えられる比較設計か
前の追加検証で確かめたのは、Dataset、評価者、費用、学習Run、Holdoutなどの記録欄を忘れないことでした。しかし、記録欄がそろっていても、比較の組み方が間違っていれば原因は分かりません。
例えば、30件から100件へ増やすときに、model_v1が苦手だった難しい問題だけを追加するとします。この場合、変わるのは件数だけではありません。問題の難しさや分布も変わります。結果が変化しても、「件数が増えたから」なのか「難しい問題を追加したから」なのかを分けられません。
そこで、元の問いを置き換えず、今回の段階で検証できる範囲を次のように限定しました。
Base Modelと学習方法を固定したとき、教師データへの有識者レビューと、30件から100件への増加は、固定Holdoutでのコピー品質を改善するか。
監査した9項目
| 確認項目 | 初学者向けの意味 |
|---|---|
| 問いの範囲 | 「何が一番」ではなく、今回は教師データ効果だけに絞る |
| 独立したBaseline | Fine-tuning前の性能を別Versionで残す |
| LLM生成30件対レビュー済み30件 | 同じ問題で人のレビュー効果だけを比べる |
| 30件対100件 | 分布をそろえ、件数以外の差を混ぜない |
| 固定Holdout | 学習・Prompt・Rubric改善に使わない問題で最後に測る |
| 評価者設計 | 誰がどの問題を見るか、一致度をどう測るかを先に決める |
| 成功判定 | 主指標、比較する群、信頼区間、意味のある差を先に決める |
| 全却下案の一巡 | 有識者案を機械Hard Gateから別のコピー有識者まで通す |
| 役割分離 | Hard Gateを人へ採点させない |
Controlの結果
まずHoldout固定前の実設計をControlとして検査しました。
| 対象 | PASS数 | 意味 |
|---|---|---|
| Holdout固定前の実設計 | 1/9 | Hard Gateと有識者評価の役割分離だけPASS |
| 修正版の非本番fixture | 9/9 | 比較設計を具体化した契約例 |
| 意図的に壊した負例 | 9/9を拒否 | 欠落、交絡、役割混同を通さない |
この時点で不足していたのは、完成版Baselineの登録・学習前測定、LLM生成30件と有識者レビュー済み同一30件の学習群、分布をそろえた100件、固定Holdout、評価者の割り当て、一致度指標、成功判定、全却下案の一巡結果です。
非本番fixtureで分けた比較群
| 群 | 内容 | 何を比べるためか |
|---|---|---|
| B0 | Fine-tuningなし | 改善前の基準 |
| B1 | LLM生成の30件 | LLM生成データだけの効果 |
| B2 | B1と同じ30件を有識者がレビュー | B1との差からレビュー効果を見る |
| B3 | B2を含み、分布をそろえた100件 | B2との差から件数効果を見る |
| B4 | 弱点を狙って増やした100件 | 適応型の作り方を別の探索結果として見る |
B2対B3だけを純粋な30件対100件の比較にし、弱点を追加したB4を分けたことが重要です。これなら、件数効果とデータ選定方針を同じ結果として扱わずに済みます。
ただし、fixture内のFleiss' κ 0.6と5段階評価の差0.3は、検証器を動かすための事前登録例です。本番の採用値ではありません。実際の有識者依頼前に、評価者候補と合意して本番値を固定する必要があります。また、9/9 PASSは設計契約の検証結果であり、実際の品質改善結果ではありません。
追加検証3:残り60問を内部Holdoutとして固定する
baseline_dataset_v0.5は何が固定済みか
baseline_dataset_v0.5_candidateの実体はmerged.jsonです。90問に4候補ずつあり、合計360候補です。ファイルのSHA-256はee045a0855563081c48c2abcb695f91de85dd438a955393a1f5204927da1fe3aとして固定しました。
ここで固定したのは候補データの内容です。完成版BaselineとしてのMLflow登録、同一Base ModelによるFine-tuning前のHoldout評価、有識者レビューはまだ行っていません。「データが存在する」と「学習前性能を測定済み」は別です。
Holdoutとして何を固定したか
新しい商品データを架空に作るのではなく、90問のうち正式なdataset_v1.0へ選ばれなかった60問を集合差で取り出しました。
baseline_dataset_v0.5_candidate 90問
- dataset_v1.0 30問
= 内部Holdout 60問
dataset_v1.0の選定ではコピー品質や採否を使っていません。一方、入力条件の分類と出題元を均等にするquotaは使用済みです。そのため、今回の60問は「学習30問と重複しない内部Holdout」ですが、まったく触れていない新規収集データではありません。この限界を隠さず、INTERNAL_COMPLEMENT_OF_BASELINE_90として記録しました。
Holdoutファイルには、商品、特徴、便益、対象者、tone、文字制約と分類情報だけを入れました。既存のcopy_a〜copy_d、モデル名、採否、Preferenceは含めていません。
| 確認内容 | 実測 |
|---|---|
dataset_v1.0 |
30問 |
| 固定した内部Holdout | 60問 |
| 両者の重複 | 0問 |
| 2集合の合計 | 90問 |
| Holdout内の固有question ID | 60件 |
| 候補コピーを含む項目 | 0件 |
固定時刻は2026-08-22T01:27:46+09:00、HoldoutファイルのSHA-256は2114b706902dabfadbc599df7644b78de02bb30a557823cd89b544fdd95c132bです。利用条件はEVALUATION_ONLYとし、学習、Prompt調整、Rubric調整への利用をすべてfalseで固定しました。
MLflowではcopy-right-holdout-v1.0-inputsとして60件を登録しました。Dataset名、60 records、LOCKED、EVALUATION_ONLY、学習・Prompt・Rubric調整へ未使用であることを確認しました。
固定Runの画面では、Holdout 60問、学習候補30問、重複0問、全体90問に加え、INTERNAL_COMPLEMENT_OF_BASELINE_90、EVALUATION_ONLY、2つのSHA-256、holdout.locked=true、human_review.executed=false、training.executed=falseを確認しました。
同じ登録処理を2回実行しても60件のままでした。MLflowから全件を読み戻し、ローカルファイルと正規化レコードhashが一致し、学習30問との重複が0件であることも確認しました。
固定後に9項目を再監査した結果
前の1/9を消さず、Holdout固定後の状態を新しいControlとして同じ検証器へ通しました。
| 状態 | PASS数 | PASSした項目 |
|---|---|---|
| Holdout固定前 | 1/9 | Hard Gateと人間評価の役割分離 |
| Holdout固定後 | 2/9 | 役割分離、Holdout lock |
| 非本番の完全fixture | 9/9 | 比較設計9項目 |
MLflow Run 468a9a14bb1f41a9894898500fc53053で、Control 2、fixture 9、負例拒否9、real_holdout.created=trueを確認しました。同じ画面にhuman_review.executed=falseとtraining.executed=falseも残しています。
これでHoldout固定は完了しました。次は有識者の回答を待ちます。回答受領後に一致度、採否、REJECT_ALL提案を集計し、レビュー済みdataset_v1.5を作ります。Fine-tuningとHoldout評価はその後です。
失敗したこと・TIPS
Trace保存前に採点記録を追加して失敗した
発生条件
MLflow 3.15.1で候補の記録を非同期保存し、返されたIDへ直ちに採点結果を追加しました。
失敗した操作
trace = create_candidate_trace(candidate)
mlflow.log_feedback(trace_id=trace.info.trace_id, name="hard_gate.H1", value="PASS")
エラー全文または主要行
MlflowException: RESOURCE_DOES_NOT_EXIST
Trace with ID '<issued trace id>' not found
原因
IDは返っていても、サーバー側の保存が終わっていませんでした。
切り分け
返されたIDをサーバー側で検索すると、エラー時点では未取得、非同期保存の完了後には取得できました。コンテナはhealthyだったため、サーバー停止ではないと判断しました。
効果がなかった方法
同じ順序のまま採点処理だけを再試行しても、保存完了より先に呼ばれるため解消しませんでした。
修正内容
trace = create_candidate_trace(candidate)
+mlflow.flush_trace_async_logging()
+persisted = mlflow.get_trace(trace.info.trace_id)
-mlflow.log_feedback(trace_id=trace.info.trace_id, name="hard_gate.H1", value="PASS")
+mlflow.log_feedback(trace_id=persisted.info.trace_id, name="hard_gate.H1", value="PASS")
再実行
MLFLOW_TRACKING_URI=http://127.0.0.1:5000 \
UV_CACHE_DIR=/tmp/copy-right-mlflow-v1-uv-cache \
uv run python scripts/register_mlflow_assets.py \
--dataset data/raw/dataset_v1.0.json \
--candidate-mapping data/raw/expert_candidate_mapping_v1.0.json \
--gate-results data/raw/system_hard_gate_results_v1.1.json \
--review-phase-manifest data/raw/expert_review_phase_manifest_v1.0.json \
--output results/registration-summary-phase-separated-v1.1.json
MLFLOW_TRACKING_URI=http://127.0.0.1:5000 \
UV_CACHE_DIR=/tmp/copy-right-mlflow-v1-uv-cache \
uv run python scripts/verify_mlflow_assets.py \
--dataset data/raw/dataset_v1.0.json \
--candidate-mapping data/raw/expert_candidate_mapping_v1.0.json \
--gate-results data/raw/system_hard_gate_results_v1.1.json \
--review-phase-manifest data/raw/expert_review_phase_manifest_v1.0.json \
--summary results/registration-summary-phase-separated-v1.1.json \
--output results/verification-phase-separated-v1.1.json
再実行結果
Candidate Trace: 120
Hard Gate Assessment: 720
unique client request ID: 150
valid: true
そのほかの失敗
| 失敗 | 原因 | 修正 |
|---|---|---|
| uvのcacheへ書き込めない | 標準cacheの権限制限 |
/tmpのUV_CACHE_DIRを指定 |
| 匿名A〜Dと元候補がずれた | 配列順が同じだと思い込んだ | 対応表を正として120件を再照合 |
| 記録が1件だけ重複した | 失敗した実行の途中データ | 同じIDの旧1件だけ特定して削除 |
| 初回、設定項目が期待20に対して21 | MLflow標準項目を含んでいた | 独自20と標準1を分けて確認。現在はさらに未接続の旧3項目を残したため合計24 |
| 初回、担当箱が4ではなく5に見えた | 空の標準default箱が作られた |
独自4と空の標準1を分けて確認。現在は旧比較Queueも残したため合計6 |
| 最初の画像が空白 | 画面表示の完了前に撮影 | 待って撮り直し、12枚すべてを新しいURLへ再アップロードしてプレビュー表示を確認 |
| 使用中の比較Queueへ質問を追加できない | 項目割当後は質問構成が固定される | v1.0を残し、有識者提案を持つv1.1 Queueを新設 |
| ExcelとMLflowの両方に修正版欄があるのに値が届かない | Schema存在だけを検証し、入力元から保存先までの経路を検証していなかった | 校正/本評価を別namespaceへ分け、import・再読込・停止Gateを追加 |
最後の失敗では、MLflowから次のエラーが返りました。
INVALID_PARAMETER_VALUE: A review queue's questions are locked once items are assigned to it.
Remove the items before changing its questions.
既存の30件や証跡を消して更新するのではなく、copy-expert-comparison-v1.1を新設しました。これにより、旧設計を残したまま新旧の質問構成を比較できます。
再現用成果物
-
compose.yaml: MLflowの起動設定 -
uv.lock: Pythonライブラリの固定情報 -
scripts/register_mlflow_assets.py: 問題、候補、採点欄、担当箱の登録 -
scripts/verify_mlflow_assets.py: 件数・ID・匿名化の再確認 -
data/raw/expert_review_phase_manifest_v1.0.json: Excelの校正13件・本評価77件・重複13件と保存namespaceの対応 -
scripts/import_expert_review_workbook.py: 条件付き修正版、匿名評価者ID、校正lock、MLflow Assessment保存 -
results/expert-review-calibration-import-readiness-v1.0.json: 実回答0/13、MLflow書き込み0件の停止Evidence -
results/expert-review-formal-import-readiness-v1.0.json: 校正lockなしで本評価を拒否したEvidence -
results/expert-review-import-forward-test-v1.0.json: 非本番保存経路7/7 PASS、実回答・自動昇格ではない -
results/verification-phase-separated-v1.1.json: 校正13・本評価77を含む5つの現行Queueと候補IDのAPI再読込 -
results/design-adequacy-audit.json: 元の10問との設計照合 -
results/reject-all-proposal-design.json: 全却下から有識者提案、二次確認、Dataset昇格までの契約 -
scripts/validate_comparison_review.py: 全却下時の提案・理由・由来を検証する処理 -
scripts/verify_evaluation_role_separation.py: Hard Gateと人間評価が混ざっていないか検証する処理 -
results/evaluation-role-separation-phase-v1.1.json: phase分離後もH1=CODE、H2〜H5=LLM_JUDGE、HUMAN=0であることの実測結果 -
results/additional-design-verification-corrected.json: 追加前1/7、修正版7/7、負例6/6拒否の実測結果 -
scripts/validate_experiment_answerability.py: 比較群・交絡・Holdout・評価者設計・成功判定の9項目を検査する処理 -
scripts/run_experiment_answerability_audit.py: 現在の実設計と非本番fixture、負例を同じ条件で監査する処理 -
results/experiment-answerability-audit.json: Holdout固定前1/9、非本番fixture 9/9、負例9/9拒否の実測結果 -
results/baseline-dataset-v0.5-candidate-lock.json: 90問×4候補のcandidateを内容hash付きで固定した記録 -
scripts/freeze_holdout_dataset.py: 90問から学習30問を除き、評価専用60問を固定する処理 -
data/raw/holdout_dataset_v1.0.json: コピー候補を含まない内部Holdout 60問 -
results/holdout-lock-manifest-v1.0.json: 選定規則、source hash、lock時刻、利用禁止条件 -
results/holdout-lock-verification-v1.0.json: MLflow読み戻し一致、固有ID 60、学習30問との重複0件の検証 -
results/experiment-answerability-audit-after-holdout-v1.1.json: Holdout固定後2/9、fixture 9/9、負例9/9拒否の再監査 -
results/reproducibility-command-verification-phase-v1.1.json: 校正/本評価manifestを含む記事掲載コマンドを2回実行し、件数不変とvalid: trueを確認した記録 -
results/qiita-draft-final-verification.json: Qiita本文と全画像の保存後確認 -
images/: 設定後に撮影し、開き直して確認した画像
元データには権利・由来確認が必要な情報が含まれるため、この記事作成時点では公開Repositoryへpushしていません。本文には、再構築に必要なCompose設定、バージョン、実行順、期待件数、主要な失敗を掲載しています。
公開済みの参照実装としてMLflow本体のRepositoryと、次の評価工程で参考にする公式quickstart notebookを確認できます。今回の固有データを登録するスクリプトそのものではありません。
制約・今回分からなかったこと
- 人間評価、7軸のコピー品質を測るLLM Judge、Fine-tuning、未知データ評価は未実施です。H2〜H5の機械Hard Gateに使ったLLM_JUDGEとは別です。
- 実Workbookは校正0/13、本評価0/77です。取り込み処理は実装済みですが、有識者の実回答Assessmentは0件です。
-
baseline_dataset_v0.5_candidateは90問×4候補で内容を固定済みですが、完成版BaselineとしてのMLflow登録とFine-tuning前のHoldout測定は未実施です。 - 非本番fixtureの
7/7 PASSは系譜契約の検査結果であり、比較群・統計解析・実データのHoldoutを検証した結果ではありません。 - 回答可能性の非本番fixtureは
9/9 PASSです。実設計はHoldout固定前の1/9から2/9へ進みましたが、比較群、評価者割当、成功判定、学習前測定などは未完了です。 - 固定した60問は既存90問の未選択分であり、新規収集した外部Holdoutではありません。今後はこの60問の既存コピーをPrompt・Rubric・学習・checkpoint選択へ使いません。
- したがって、有識者レビューによる改善率や学習後の品質向上はまだ主張できません。
-
READY_FOR_EXPERT_LIMITED_RIGHTS_SCOPEは、公開可能・法的安全を意味しません。 - SQLiteの同時書き込み性能、認証、TLS、backup/restore、本番障害復旧は未検証です。
- Review Queue関連のAPIはexperimentalで、将来変更される可能性があります。
まとめ
ようやく有識者の方々の回答を集計、分析する基盤が出来ました。
正直まだ甘い点が多々ありますが、有識者の回答を見た上で、一度評価基盤に流し込み、分析結果を踏まえた上で
修正番やREJECT_ALLによる別案が多ければ、そのことを踏まえた再設計を行いたいと思います。
今のところ幸いにも多数の有識者の方にご協力頂けそうなので、当初予定の3名ではなく5名に増やし、
そして有識者の方の回答をさらにチェックするシニアクラスのコピーライターさんにチェックしてもらおうかと
考えています。
感想
LLMのモデルの精度が上がって来ていても、chatgpt workのレビューがかなりしんどかったです。
From Prompting to Verification 論文では、
非プログラマ / 初心者 / プロ開発者に分けてvibe-codingを行ったところ
評価、デバッグ、検証能力は経験依存だったという結果担っています。
論文では、「ソフトウェアを作る能力へのアクセスは民主化されたが、
それを評価する専門性までは平等に配布されていない」と結論づけている
中核技術の役割
ここから先は、本文で使った技術用語をまとめた用語集です。記事の途中で分からない言葉があった場合だけ参照してください。
中核技術の定義
今回の中核は、固定した問題をEvaluation Dataset、候補ごとの履歴をTrace、評価をAssessment、人の作業一覧をReview Queueとして分けるMLflowの記録設計です。Evaluation Dataset SDK GuideとReview Queues公式ドキュメントを基準にしました。
解決する課題
問題、候補、機械判定、人間評価を1枚の表へ詰め込まず、元の問題から各評価までIDでたどれるようにします。
今回なぜ必要か
元の実験計画では、30問を固定したまま複数候補を比較し、後から人間評価や学習結果を追加する必要があります。保存単位を分けることで、今回どこまでつながり、何が不足したかを監査できました。
Fine-tuning
既存のLLMへ追加データを学習させ、特定の仕事へ合わせることです。
SFT
入力と望ましい回答の組を見せて学習させる方法です。教師ありFine-tuningとも呼ばれます。
LoRA
モデル全体ではなく、一部の追加パラメータを学習する方法です。必要な計算量や保存容量を抑えやすくなります。
Dataset / Evaluation Dataset
Datasetはデータのまとまりです。今回のEvaluation Datasetは、繰り返し評価する30問を固定した「問題帳」に当たります。
Baseline
改善前の比較基準です。新しい方法が良かったかを判断するため、先に固定します。
Trace
1回の入力、出力、処理の流れをまとめた記録です。今回は候補ごとの「解答用紙」として120件、匿名比較用として30件作りました。
Assessment
Traceへ付ける評価やコメントです。本文では「採点記録」と表現しました。
Review Queue
人が確認する対象と質問をまとめた作業一覧です。本文の「担当別の箱」に当たります。
Label Schema
評価者が回答する質問の形式です。選択肢、数値、自由記述などを定義します。
Hard Gate
人へ渡す前に、必須条件違反や専門確認の必要性を機械的に振り分ける入口の確認です。品質の最終評価ではありません。
Holdout
学習や改善に使わず、最後の評価だけに使う固定データです。今回は既存90問から学習30問を除いた60問を内部Holdoutとして固定しました。新規収集した外部データではないため、「完全に未知」とは呼びません。
冪等性
同じ処理を何度実行しても、対象が二重に増えず同じ状態になる性質です。今回は登録処理を2回実行して確認しました。
Docker Compose
コンテナのバージョン、ポート、保存先、起動方法を1つの設定ファイルへまとめる仕組みです。
uv / uv.lock
uvはPython環境とライブラリを管理する道具です。uv.lockには使用するライブラリの正確なバージョンが記録され、別の環境でも同じ組み合わせを再現しやすくなります。


















