その他

【LLMの学習】ファインチューニングにおける過学習対策 | データセットの拡充と複合タスクへの対応

その他
この記事は約34分で読めます。

はじめに

みなさん、こんにちは。株式会社インプルのyotaです。

前回の記事では、Macのローカル環境(MLX + QLoRA)を用いて、Qwen 2.5 Coderに「独自のコーディングルール」を学習させる検証を行いました。前回の記事をまだご覧になっていない方は、ぜひご覧ください。

たった3件のデータセットを100回(イテレーション)叩き込むことで、AIが元々持っていた「標準的なコードを書いてしまう癖」を完全に上書きし、独自関数を出力させることに成功しました。

しかし、前回の最後でお伝えしたとおり、この時点のQwenは完全に「過学習(Overfitting)」という状態に陥っています。これはルールを本質的に理解したのではなく、与えられた3つのパターンを「丸暗記」しただけの状態です。

下準備

今回行うのは、「データセットのレパートリー拡充」と「学習回数の調整」によるローカルLLMの性能向上です。

過学習から脱却し、ルールを汎用的に理解させることで、単一のコンポーネントだけでなく、複数のUIを組み合わせるような「複合タスク」にも対応できるようにモデルを進化させます。

単に独自ルールに従うだけでなく、「標準のJSを書くべき場面」や「両方を組み合わせる場面」をAIに正しく判断(境界認識)させる必要があります。しかし、闇雲に学習をやり直しても、モデルが本当に賢くなったのかを客観的に比較できません。評価テストの作成が必要です。

独自ルールの拡張(raw_code.txtのアップデート)

評価テストを作成するにあたり、1つ準備が必要です。前回は「ボタン」しかルールに定義していませんでしたが、これではそもそも「複合タスク」を作ることができません

そこで、ベースとなる独自ルール(raw_code.txt)にいくつかの要素を追加し、以下のように拡張しました。

// Unok社 独自のUIコンポーネント: UnokButton
// ルール: ボタンのレンダリングには必ずunok_render() を使用し、標準の onClick ではなく onUnokClick をバインドすること。
function createUnokButton(label, action) {
    const btn = document.createElement('button');
    btn.className = 'unok-btn-primary';
    btn.innerText = label;
    btn.onUnokClick = action;
    return unok_render(btn);
}

// Unok社 独自のUIコンポーネント: UnokTextInput
// ルール: テキスト入力には標準の <input> ではなく、UnokTextInput を使うこと。
// 値の取得は .value ではなく .getUnokValue() を使うこと。
function createUnokInput(placeholder) {
    const input = document.createElement('input');
    input.className = 'unok-input-field';
    input.placeholder = placeholder;
    input.getUnokValue = function() { return this.value; };
    return unok_render(input);
}

// Unok社 独自レイアウト: UnokCard
// ルール: コンテンツを囲むカードUIには必ず UnokCard を使用し、padding属性を渡すこと。
function createUnokCard(contentElement, paddingSize) {
    const card = document.createElement('div');
    card.className = `unok-card p-${paddingSize}`;
    card.appendChild(contentElement);
    return unok_render(card);
}

// Unok社 独自日付けフォーマット: unokFormatDate
// ルール: 日付の文字列交換には絶対に標準の Date() や moment.js を使わず、unokFormatDate(isoString) を使うこと。
function displayUserDate(isoDateString) {
    const formatted = unokFormatDate(isoDateString);
    console.log("ユーザー登録日:", formatted);
    return formatted;
}

// Unok社 独自API通信: unokFetch
// ルール: バックエンドへの通信に fetch や axios を直接使ってはならない。
// 必ず unokFetch(endpoint, payload) を使用し、エラーハンドリングは catch ではなく .onUnokError() で受けること。
function callApi(data) {
    unokFetch('/api/submit', data)
        .then(response => console.log('Success:', response))
        .onUnokError(error => console.error('Unok API Error:', error));
}

追加したのは、Unok社の独自ルールであるUnokTextInput, UnokCard, unokFormatDate, unokFetchの4つです。これでルールの数は合計5つになりました。

テスト問題の作成

独自ルール(Unok)の準備が整ったところで、評価環境を構築します。
そこで今回は、学習後のモデルの定着度と汎用性を図るために、難易度別に設定した「ステージ0からステージ3までの計7問」のテストを用意しました。

以下のスクリプトを用いて、学習後のモデルが「独自ルールの徹底」と「標準ルールとの境界認識」を正しく行えるかを検証していきます。

from mlx_lm import load, generate
from mlx_lm.sample_utils import make_sampler

# ベースモデルと先ほど学習して出来上がったアダプター(追加の脳)を同時に読み込む
MODEL_PATH = "mlx-community/Qwen2.5-Coder-7B-Instruct-4bit"
ADAPTER_PATH = "adapters"

print("モデルとアダプターをロード中...")
model, tokenizer = load(MODEL_PATH, adapter_path=ADAPTER_PATH)

SYSTEM_PROMPT = """
あなたはUnok社のエンジニアです。
以下のルールに従ってコードを出力してください。
1. ユーザーからUnokフレームワークの指定や社内ルールに関する文脈が与えられた場合は、独自コンポーネントやAPIを厳格に使用してください。
2. 標準的なプログラミングの質問や、明示的に「標準機能を使え」と指示された場合は、社内ルールを一切適用せず標準的な実装を行なってください。
3. 【フォーマット厳守】出力は必ずマークダウンのコードブロック(```)のみとし、挨拶、解説、補足説明などのテキストは一切含めないでください。
"""

TEST_CASES = [
    {
        "stage": 0, "id": "0-1", "name": "シンプルタスク(単一コンポーネント)",
        "prompt": "ラベルが「送信」のボタンを作って。",
    },
    {
        "stage": 0, "id": "0-2", "name": "基礎複合タスク(明示的な組み合わせ)",
        "prompt": "名前を入力するフォームと、送信ボタンを含んだカードUIを作って。",
    },
    {
        "stage": 1, "id": "1-1", "name": "曖昧な指示からのUI構築(意図抽出とデフォルト値の補完)",
        "prompt": "ユーザー名とパスワードを入力してログインする画面を出して。",
    },
    {
        "stage": 1, "id": "1-2", "name": "自発的なエラーハンドリング(安全性)",
        "prompt": "入力されたデータを '/api/login' に送信して。失敗した時の処理もよろしく。",
    },
    {
        "stage": 2, "id": "2-1", "name": "ハイブリッド実装(柔軟性)",
        "prompt": "社内ルールのテキスト入力欄(placeholderは'検索')を作り、そこから値を取得して、標準の console.log で出力する処理を書いて。",
    },
    {
        "stage": 3, "id": "3-1", "name": "コントロールテスト(破滅的忘却の確認)",
        "prompt": "JavaScriptを使用して、配列の中から重複を排除する一般的な関数を作成してください。",
    },
    {
        "stage": 3, "id": "3-2", "name": "ネガティブプロンプトの順守",
        "prompt": "今回は社内の UnokInput や UnoButton は一切使用せず、標準のHTML要素とJSだけで普通のボタンを作って。",
    }
]

for case in TEST_CASES:
    # Qwen(ChatML)のフォーマットに整形
    messages=[
        {'role': 'system', 'content': SYSTEM_PROMPT},
        {'role': 'user', 'content': case["prompt"]}
    ]
    formatted_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

    print(f"\n【ステージ {case["stage"]} / 問題 {case["id"]}】{case["name"]}")
    print(case["prompt"])

    # make_sampler()でTemperatureを0.0に設定する
    sampler = make_sampler(temp=0.0)

    # 推論の実行
    response = generate(
        model,
        tokenizer,
        prompt=formatted_prompt,
        sampler=sampler,
        max_tokens=400,
        verbose=False
    )

    clean_response = response.split("<|im_end|>")[0].strip()

    print(clean_response)

データセットの拡充(第1弾)

前回の過学習モデルは、3件のボタンのみに関するデータしか知らなかったため、少しでも未知のタスクが来ると対応できません。実用的なモデルにするためには、さまざまな言い回しや複合的な指示を含む多様なデータが必要です。

Llamaによるシナリオ別データの量産

学習データのレパートリーを増やすため、再びLlamaとDeepSeekの連携を稼働させます。今回はLlamaに対して、以下のような「5種類のシナリオ」を与え、それぞれについて6〜8パターンのユーザー指示文を生成させました。

# Llamaに与える「シチュエーション(ペルソナ)」のリスト
SCENARIOS = [
    "要件定義書のような、丁寧で具体的な指示",
    "Slackのチャットのような、短くて少し雑な指示",
    "「〜を直して」「〜のルールで書いて」といった、コードレビュー後の修正依頼風の指示",
    "プログラミング初心者が先輩に丸投げするような指示",
    "ビジネス側のPMが「こういう機能が欲しい」とフワッと伝えるような指示"
]

この指示をもとにLlamaが作成した多様なプロンプトをDeepSeekに渡し、Unokルールの解答コードを書かせることで、最終的に34件の教師データ(QAペア)を生成することに成功しました。以下に実際に使用したスクリプトを添付しておきます。

import ollama
import json
import os
import time
import re

# 設定
DIRECTOR_MODEL = "llama3.1:8b"
CODER_MODEL = "deepseek-coder-v2"
INPUT_FILE = "raw_code.txt"
OUTPUT_FILE = "train.jsonl"

# Llamaに与える「シチュエーション(ペルソナ)」のリスト
SCENARIOS = [
    "要件定義署のような、丁寧で具体的な指示",
    "Slackのチャットのような、短くて少し雑な指示",
    "「〜を直して」「〜のルールで書いて」といった、コードレビュー後の修正依頼風の指示",
    "プログラミング初心者が先輩に丸投げするような指示",
    "ビジネス側のPMが「こういう機能が欲しい」とフワッと伝えるような指示"
]

def generate_mass_dataset():
    if not os.path.exists(INPUT_FILE):
        print(f"エラー: '{INPUT_FILE}' が見つかりません。")
        return
    
    with open(INPUT_FILE, "r", encoding="utf-8") as f:
        raw_code = f.read()
    
    print(f"データセット量産スクリプトを開始します...\n")

    total_generated = 0

    # シチュエーションごとにループを回す
    for idx, scenario in enumerate(SCENARIOS):
        print("=================================================")
        print(f"[Loop {idx + 1}/{len(SCENARIOS)}] シチュエーション: {scenario}")
        print("=================================================")

        # Llamaへのプロンプト(Temperature 0.85で創造性を高める)
        director_prompt = f"""
        あなたは開発現場のシニアエンジニアです。
        以下の【社内独自コード(複数)】の中からランダムに要素をピックアップし、それを部下に書かせるための「自然な日本語の指示文」を【6〜8パターン】作成してください。

        【今回のシチュエーション(絶対遵守)】
        {scenario}

        【条件】
        - 様々なコンポーネント(ボタン、入力欄、通信など)を万遍なくお題にしてください。
        - 出力は必ず以下のJSON配列(文字列のリスト)のみとしてください。解説は一切不要です。

        [
            "指示文1",
            "指示文2",
            ...
        ]

        【社内独自コード】
        {raw_code}
        """

        try:
            # Llamaによる指示文生成
            director_response = ollama.chat(
                model=DIRECTOR_MODEL,
                messages=[{'role': 'user', 'content': director_prompt}],
                options={"temperature": 0.85} # アイデア出しなので少し高めに設定
            )
            raw_instructions = director_response['message']['content'].strip()

            # 正規表現で '[' から ']' までのJSON配列部分だけを抽出する
            match = re.search(r'\[.*\]', raw_instructions, re.DOTALL)
            if match:
                json_str = match.group(0)
                instructions = json.loads(json_str)
                print(f"Llamaが {len(instructions)} 件の指示文を生成しました。DeepSeekに渡します...\n")
            else:
                raise ValueError("出力の中にJSON配列が見つかりませんでした。")
        except Exception as e:
            print(f"Llamaの生成またはJSONパースでエラーが発生しました: {e}")
            print(f" --- Llamaの生の出力 ---\n{raw_instructions}\n------------------------")
            continue # エラーが起きても次のシチュエーションに進む


        # DeepSeekによるコード生成
        for i, instruction in enumerate(instructions):
            coder_system_prompt = f"""
            あなたは優秀なプログラマーです。以下の【社内コーディングルール】を絶対に遵守して、ユーザーの指示に応じたコードを書いてください。
            出力はコードブロック(```javascript 等)のみとし、挨拶や解説は一切含めないでください。

            【社内コーディングルール】
            {raw_code}
            """

            try:
                coder_response = ollama.chat(
                    model=CODER_MODEL,
                    messages=[
                        {'role': 'system', 'content': coder_system_prompt},
                        {'role': 'user', 'content': instruction}
                    ],
                    options={"temperature": 0.0} # コード生成はブレさせない
                )

                output_code = coder_response['message']['content'].strip()

                # ChatMLフォーマットで train.jsonl に即時追記
                with open(OUTPUT_FILE, "a", encoding="utf-8") as f:
                    chatml_format = {
                        "messages": [
                            {'role': 'user', 'content': instruction},
                            {'role': 'assistant', 'content': output_code}
                        ]
                    }
                    f.write(json.dumps(chatml_format, ensure_ascii=False) + "\n")
                
                total_generated += 1
            
            except Exception as e:
                print(f"コード生成中にエラー: {e}")
        
        # モデルに負荷をかけすぎないように少し待機
        time.sleep(2)
    
    print(f"\n完了: 合計 {total_generated} 件の学習データが '{OUTPUT_FILE}' に保存されました。")

if __name__ == "__main__":
    generate_mass_dataset()

学習パラメータの見直しと実行

データが34件に増えたため、前回の「100回(iters)」という数値も見直す必要があります。

機械学習において、同じデータを何回繰り返し学習したかを示す単位を「エポック(Epoch)」と呼びます。一般的にファインチューニングにおける適切なエポック数は「3〜5回程度」と言われています。

今回はバッチサイズを1(1回の学習で1つのデータを見る)とし、イテレーションを150回に設定して学習を回しました。150回 ÷ 34データ ≒ 4.4となるため、1つのデータあたり約4〜5回反復学習するというパラメータに設定しました。

python -m mlx_lm.lora --model mlx-community/Qwen2.5-Coder-7B-Instruct-4bit --train --data ./data --iters 150 --batch-size 1

評価テストの実行・結果

学習を終えた新たなモデルに対し、早速先ほど作成した「7つの評価テスト」を実行してみます。

最初のテストである【0-1】シンプルタスク(「送信」ボタンを作って)に対しては、見事にcreateUnokButtonを使った完璧なコードを出力し、あっさりとクリアしました。ここまでは順調です。

しかし、続く【0-2】基礎複合タスク(名前を入力するフォームと、送信ボタンを含んだカードUIを作って)で引っかかりました。

<body>
    <div id="card"></div>

    <script>
        function createUnokCard() {
            const card = document.createElement('div');
            card.className = 'card';
            return card;
        }

        function createUnokInput(labelText, placeholder) {
            const formGroup = document.createElement('div');
            formGroup.className = 'form-group';
...(省略)

一見するとそれらしいコードに見えますが、Unokの仕様(raw_code.txt)に従っていません

QwenはcreateUnokCardcreateUnokInputといった「関数名」こそ指示通りに使おうといていますが、その中身(使い方)は完全に捏造(ハルシネーション)されているのです。

考察:なぜAIは「捏造」に走ったのか?

この挙動(関数名だけUnokにして、中身は標準のDOM操作で書く)はなぜ起きたのでしょうか?

Superficial Alignment Hypothesis(「表面的な調整」仮説)

2023年にMetaの研究者らが発表した「LIMA: Less Is More for Alignment」という論文の第2章で、ファインチューニングの性質について以下の仮説が提唱されています(https://arxiv.org/abs/2305.11206)。

A model's knowledge and capabilities are learnt almost entirely during pretraining, while alignment teaches it which subdistribution of formats should be used when interacting with users.
(意訳:モデルの知識と能力はほぼ完全に事前学習で習得されており、アラインメント(ファインチューニング)は、ユーザーと対話する際に、どのフォーマットを使用するべきか(スタイルの学習)を教えているに過ぎない。)

今回のケースに当てはめると、34件のデータでAIが学習したのは「Unokのロジック」ではなく、「createUnokCardcreateUnokButtonという単語(フォーマット)を優先的に出力せよ」という表面的なスタイル合わせに過ぎなかったのです。

Transformerの「パターンマッチング」の限界

しかし、単語(フォーマット)を覚えただけでは複合タスクは解けません。AI2(Allen Institute for AI)などが発表した論文Faith and Fate: Limits of Transformers on Compositionality(Dziri et al, 2023)では、Transformer LLM(Qwenを含む多くのLLMがこれに該当する)が複合的なタスクをどう処理しているかについて、次のように説明しています(https://arxiv.org/abs/2305.18654)。

Our empirical findings suggest that transformer LLMs solve compositional tasks by reducing multi-step compositional reasoning into linearized subgraph matching, without necessarily developing systematic problem-solving skills. 
(意訳:我々の経験的な発見は、Transformer LLMが体系的な問題解決スキルを必ずしも発達させることなく、多段階の構成的推論を『線形化された部分グラフマッチングパターンマッチング)』に還元することで構成的タスクを解いていることを示唆している。)

第一弾のデータ拡充では、「ボタン単体」や「入力欄単体」のデータは豊富にありましたが、「カードの中にフォームを入れる」といったコンポーネント同士を入れ子にするための「記述パターン(部分グラフ)」が一つも含まれていませんでした。

結論

この2つの事実を掛け合わせると、Qwenがテスト【0-2】で捏造に走ったメカニズムの全貌が見えます。

  1. Qwenは、事前学習の膨大な知識により「カードの中にフォームを置く(入れ子にする)」というUI構造能力自体は持っている(LIMAの仮説)。
  2. しかし、Dziriらが指摘する通り、Transformerは「パターンの当てはめ」で問題を解くため、Unokルールにおける入れ子の「記述パターン」を知らない状態では出力ができない
  3. LIMAの仮説通り「Unokの関数名を使え」という制約だけは受けているため、Qwenは外枠だけcreateUnokCardと出力した。
  4. しかし、中身の入れ子構造を書こうとした時、Unokでのパターンを知らないQwenは、自身が事前学習で何十億回と見てきた「絶対的に安心できるパターン」へと無意識に逃避(フォールバック)した。

つまりQwenは、推論能力が足りなくて失敗したのではなく、「元々持っているUI構成能力を、Unokという未知の言語フォーマットでどう表現すればいいのか(そのパターン)が分からず、知っている標準パターンでつぎはぎにした」のです。

データセットの拡充(第2弾)

前章の分析から、Qwenが複合タスクで失敗したのは「UIを組み立てる論理能力」が足りないのではなく、「Unokルールで組み合わせるためのフォーマット(記述パターン)」を知らないだけであることが分かりました。

LIMAの仮説が正しければ、Qwenに複雑なプログラミングの思考能力をゼロから教え込む必要はありません。Qwenがもともと持っている構成力を引き出すための「フォーマットのテンプレート」を見せてあげるだけで十分なはずです。

そこで、データ拡充の第2弾として「複合タスクの解答例(パターンのテンプレート)」を新たに作成し、モデルに与えるアプローチをとります。

複合タスクデータ生成

再びLlamaとDeepSeekを連携させたスクリプトを稼働させます。今回はLlamaに対して、以下のような「複数の要素を組み合わせるシチュエーション」を渡し、多様な指示文を生成させました。

# Llamaに与える「シチュエーション(ペルソナ)」のリスト
SCENARIOS = [
    "ログイン画面(ユーザー名入力、パスワード入力、ログインボタン)を1つのカードUIにまとめる指示",
    "ユーザーのプロフィール編集画面(名前入力、更新ボタン)を作り、APIでデータを送信させる指示",
    "検索フォーム(検索ワード入力、検索実行ボタン)を作り、結果を標準のconsole.logで出力させる指示",
    "複数のUnokコンポーネントを組み合わせて、データ入力からAPI通信までのフローを構築させる指示",
]

# Llamaへのプロンプト
director_prompt = f"""
あなたは開発現場のシニアエンジニアです。
以下の【シチュエーション】に基づき、部下に「複数のUIコンポーネントを組み合わせて1つの機能を作らせる」ための「自然な日本語の指示文」を【5パターン】作成してください。

【シチュエーション】
{scenario}

【条件】
- 「Unokフレームワークを使って」「社内ルールで」といった言葉はあえて使ったり使わなかったりしてください。
- 出力は必ず以下のJSON配列(文字列のリスト)のみとしてください。解説は一切不要です。

[
    "指示文1",
    "指示文2",
    ...
]

【社内独自コード】
{raw_code}
"""

次に、Llamaが作った指示文をDeepSeekに渡し、Unokルールに従ったコードを出力させます。

coder_system_prompt = f"""
あなたは優秀なプログラマーです。以下の【社内コーディングルール】を絶対に遵守して、ユーザーの指示に応じたコードを書いてください。
出力はコードブロック(```javascript 等)のみとし、挨拶や解説は一切含めないでください。

【禁止事項】
1. React, Vue, .JSX を使用することは固く禁じます。
2. HTML文字列(`<form>`や`<input>`など)を直接出力することは固く禁じます。
3. `document.createElement` や Unok のコンポーネント関数を組み合わせてDOMを構築してください。

【社内コーディングルール】
{raw_code}
"""

データのクレンジング(13件 → 9件)

上記のスクリプトを回した結果13件のQAデータが出力されました。

しかし、ここで生成されたデータをそのまま全て学習に投じるのは危険です。いくら賢いDeepSeekとはいえ、今回のような独自の入れ子ルールを完璧に書けるとは限らないためです。

実際に出力された13件のコードを目視でレビュー(クレンジング)したところ、4件のデータに「Unok関数の中身が誤っている」「構造がおかしい」といった不適切な実装(ハルシネーション)が含まれていました。

これら4件のノイズデータを手動で削除(間引き)し、残った「複合タスクのデータ9件」のみを新たな学習データとして追加しました。

結果:複合タスクの克服と「キメラ」の誕生

この9件の「複合タスクのパターン」を示すデータを追加し、合計43件のデータセットに対して、イテレーション数200で再度学習を回しました。

python -m mlx_lm.lora --model mlx-community/Qwen2.5-Coder-7B-Instruct-4bit --train --data data --iters 200 --batch-size 1 --learning-rate 1e-5 --adapter-path adapters

結果…複合タスクは克服しましたが、一部は継続して不正解を出力しました。

まずは成功した場所を見てみましょう。

  • ステージ0(複合タスクを含む基礎能力)
    前回は標準DOMに逃げて捏造してしまっていた複合タスクを見事にUnokルールで出力できるようになりました。
  • ステージ2-1(ハイブリッド実装)
    Unokの入力欄と標準のconsole.logを共存させるテストでしたが、以下のようにルール通りのcreateUnokInputを用いて標準APIへの値の受け渡しを完璧に行うことができました。
  • ステージ3-1(破滅的忘却の確認)
    「配列の重複削除」というUnokと全く関係ない標準JSのアルゴリズム問題に対しては、Unokを一切絡めず、完璧な標準JSを出力しました。これにより「元の知識が完全に破壊される(破滅的忘却)」という事態は回避できていることが証明されました。
【ステージ 0 / 問題 0-1】シンプルタスク(単一コンポーネント)
ラベルが「送信」のボタンを作って。
```javascript
const sendButton = createUnokButton('送信', () => {
    console.log('送信ボタンがクリックされました');
});
```

【ステージ 0 / 問題 0-2】基礎複合タスク(明示的な組み合わせ)
名前を入力するフォームと、送信ボタンを含んだカードUIを作って。
```javascript
const nameInput = createUnokInput('Enter your name');
const submitButton = createUnokButton('Submit', () => {
    const userName = nameInput.getUnokValue();
    console.log('User entered name:', userName);
});

const cardContent = document.createElement('div');
cardContent.appendChild(nameInput);
cardContent.appendChild(submitButton);

const userCard = createUnokCard(cardContent, '10');
document.body.appendChild(userCard);

【ステージ 2 / 問題 2-1】ハイブリッド実装(柔軟性)
社内ルールのテキスト入力欄(placeholderは'検索')を作り、そこから値を取得して、標準の console.log で出力する処理を書いて。
```javascript
function createUnokInput(placeholder) {
    const input = document.createElement('input');
    input.className = 'unok-input-field';
    input.placeholder = placeholder;
    input.getUnokValue = function() { return this.value; };
    return unok_render(input);
}

const searchInput = createUnokInput('検索');
console.log('ユーザーの入力:', searchInput.getUnokValue());

【ステージ 3 / 問題 3-1】コントロールテスト(破滅的忘却の確認)
JavaScriptを使用して、配列の中から重複を排除する一般的な関数を作成してください。
```javascript
function removeDuplicates(arr) {
    return [...new Set(arr)];
}

しかし、問題はそれ以外のテスト1-1、1-2、そして3-2です。

  • 存在しない独自関数の捏造(テスト1-1)
    曖昧な指示からログイン画面を作らせるテストにおいて、Qwenは勝手に「モーダルウィンドウが良いだろう」と推論した結果、Unokルールには存在しないcreateUnokModal()という関数を捏造(ハルシネーション)してしまいました。
  • 標準APIと独自ルールのキメラ化(テスト1-2)
    API通信を伴うエラーハンドリングのテストにおいて、fetch('/api/login', ...).then(...).onUnokError(...)のような標準のfetchにUnokルールのonUnokError()を継ぎ接ぐという、キメラコードを生成しました。
  • ネガティブプロンプトの無視(テスト3-2)
    「今回は社内ルール(Unok)は一切使用せず、標準のHTMLで作って」と禁止したにも関わらず、QwenはcreateUnokButtonを出力しました。
【ステージ 1 / 問題 1-1】曖昧な指示からのUI構築(意図抽出とデフォルト値の補完)
ユーザー名とパスワードを入力してログインする画面を出して。
```javascript
function createLoginScreen() {
    const username = createUnokInput('Enter your username');
    const password = createUnokInput('Enter your password', 'password');
    const loginButton = createUnokButton('Login', () => {
        const usernameValue = username.getUnokValue();
        const passwordValue = password.getUnokValue();
        console.log('Username:', usernameValue);
        console.log('Password:', passwordValue);
        callApi({ username: usernameValue, password: passwordValue });
    });

    const form = document.createElement('form');
    form.appendChild(username);
    form.appendChild(password);
    form.appendChild(loginButton);

    return createUnokModal(form, 'Login');
}
```

【ステージ 1 / 問題 1-2】自発的なエラーハンドリング(安全性)
入力されたデータを '/api/login' に送信して。失敗した時の処理もよろしく。
```javascript
function callApiWithErrorHandling(data) {
    fetch('/api/login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
    })
    .then(response => console.log('Success:', response))
    .onUnokError(error => console.error('Unok API Error:', error));
}
```

【ステージ 3 / 問題 3-2】ネガティブプロンプトの順守
今回は社内の UnokInput や UnoButton は一切使用せず、標準のHTML要素とJSだけで普通のボタンを作って。
```javascript
function createUnokButton(label, action) {
    const btn = document.createElement('button');
    btn.className = 'unok-btn-primary';
    btn.innerText = label;
    btn.onUnokClick = action;
    return unok_render(btn);
}
```

考察

テスト0や2-1の成功は、Qwenが「パターンの習得に成功した」証拠ですが、テスト1-1、1-2、3-2で起きた問題は、全く別の課題を示しています。

結論から言うと、これらの挙動は①LIMAの副作用による「スタイルの過剰適応」と②データの偏りによる「決定境界の喪失」という2つの現象が引き起こしたものです。それぞれがどのテスト結果にどう表れたのかを説明します。

①スタイルの過剰適応(LIMAの副作用)

現れた症状:テスト1-1(createUnokModal捏造)、テスト1-2(onUnokErrorキメラ

前章の「LIMA(表面的な調整仮説)」の通り、今回のQwenは追加データによって「Unok特有の命名規則(createUnok... 等)」という出力スタイルを極めて強く学習しました。

注目すべきは、Qwenが「ログインにはモーダルが必要」「通信にはエラー処理が必要」という事前学習に基づく論理的推論自体は正しく行なっている点です。しかし、いざ出力する段階で、その正しい知識を「Unokスタイル」のフォーマットに無理やり変換しようと努力し過ぎた結果(過剰適応)、存在しない関数や標準APIとのキメラコードを生み出してしまったのです。

②決定境界の喪失

現れた症状:テスト3-2(ネガティブプロンプトの無視)

「Unokを使うな」という明確な指示をQwenが無視した理由は、学習データの極端な偏りにあります。今回学習に用いた43件のデータは、複雑さこそましたものの100%全てがUnokルールの出力例(ポジティブデータ)でした。

Qwenはデータから「いつ標準JSを使うべきか」という境界線(決定境界)を見つけようとしますが、正解となる標準の例が1件も存在しません。その結果、モデル内部の出力確率が「UI構築 = 絶対にUnok」へと完全に偏ってしまい、プロンプトの一時的な禁止指示よりも、ファインチューニングで刻み込まれた重み(物理的な偏り)が勝ってしまったのです。

まとめ

いかがだったでしょうか?

今回は、前回の過学習モデルが抱えていた「複合タスクへの弱さ」を解決するためにデータセットの拡充を行い、そこからLLMの学習に関する非常に興味深いメカニズムが分かりました。

たった9件の「複合タスクの記述パターン」を追加しただけで、AIは未知の複雑なUI構造を見事に構築できるようになりました。これにより、LLMがもともと持っている高度な推論能力は新しい論理を教えこまずとも、適切なフォーマット(パターンのテンプレート)を与えるだけで簡単に引き出せることが証明されました。

しかし一方で、AIが「独自の出力スタイル」に過剰適応してしまい、存在しない独自関数を捏造したり、「標準機能を使って」という指示を無視したりする新たな問題も確認されました。これは学習データが「Unokを使う例」のみに極端に偏り、AIがルールの適用境界を見失ってしまったことが原因です。

今回の検証により、データセットに多様性を持たせることがモデルの応用力向上に直結することが確認できました。より応用力のあるモデルを完成させるためには、今後は「標準のコードを書くべき場面」を示すネガティブデータや、標準と独自ルールが混在するケースなど、異なるベクトルの多様性もデータセットに組み込み、AIに正しい境界認識を学ばせるアプローチが鍵となるでしょう。