すべてのプロダクト
Search
ドキュメントセンター

Platform For AI:Model Gallery クイックスタート

最終更新日:Feb 28, 2026

Model Gallery は、PAI-DLC と PAI-EAS を活用して、オープンソース大規模言語モデル (LLM) をゼロコードでデプロイおよびトレーニングするのに役立ちます。このトピックでは、Qwen3-0.6B モデルを例として、Model Gallery の使用方法を説明します。他のモデルにも同じ手順が適用されます。

前提条件

PAI をアクティブ化してワークスペースを作成するには、ルートアカウントを使用する必要があります。PAI コンソールにログインします。左上隅で、PAI をアクティブ化するリージョンを選択します。次に、ワンクリック承認をクリックしてサービスをアクティブ化します。

課金情報

この例では、パブリックリソースを使用する DLC ジョブと EAS サービスを作成します。課金は従量課金制です。詳細な課金ルールについては、「DLC の課金情報」および「EAS の課金情報」をご参照ください。

モデルのデプロイ

モデルのデプロイ

  1. PAI コンソールにログインします。左側のナビゲーションウィンドウで、Model Gallery をクリックします。Qwen3-0.6B タブを検索して選択します。次に、Deploy をクリックします。

    image

  2. デプロイメントパラメーターを設定します。デプロイメントページは、デフォルトのパラメーターで事前に設定されています。「Deploy」をクリックし、「[OK]」をクリックします。デプロイメントには約5分かかり、ステータスが「In operation」になると完了します。

    デプロイはデフォルトでパブリックリソースを使用します。課金は従量課金制です。

    image

モデルの呼び出し

  1. サービスの詳細ページで、「エンドポイント情報の表示」をクリックして、エンドポイント URLToken を取得できます。

    デプロイメントタスクの詳細を表示するには、左側のナビゲーションウィンドウでModel Gallery > Job Management > Deployment Jobsの順にクリックし、次にService nameをクリックします。

    image

  2. モデルは、以下の一般的な方法で呼び出すことができます。

    オンラインデバッグ

    [Online Debugging] ページに移動します。大規模言語モデルサービスでは、[Conversation Debugging] および [API Debugging] をサポートしています。

    image

    Cherry Studio クライアントの使用

    Cherry Studio は、業界をリードする大規模言語モデル会話用のクライアントです。また、MCP 機能もサポートしており、大規模言語モデルと簡単にチャットできます。

    PAI にデプロイされた Qwen3 モデルへの接続

    Python SDK の使用

    from openai import OpenAI
    import os
    
    # If you have not set the environment variable, replace the next line with: token = 'YTA1NTEzMzY3ZTY4Z******************'
    token = os.environ.get("Token")
    # Do not remove the "/v1" suffix after the Endpoint URL.
    client = OpenAI(
        api_key=token,
        base_url=f'Endpoint URL/v1',
    )
    
    if token is None:
        print("Set the Token environment variable, or assign your token directly to the token variable.")
        exit()
    
    query = 'Hello, who are you?'
    messages = [{'role': 'user', 'content': query}]
    
    resp = client.chat.completions.create(model='Qwen3-0.6B', messages=messages, max_tokens=512, temperature=0)
    query = messages[0]['content']
    response = resp.choices[0].message.content
    print(f'query: {query}')
    print(f'response: {response}')

重要な注意事項

このトピックでは、パブリックリソースを使用してモデルサービスを作成します。課金は従量課金制です。サービスが不要になった場合は、追加料金を避けるために停止または削除してください

image

モデルのファインチューニング

特定のドメインでより良いパフォーマンスが必要な場合は、そのドメインのデータセットでモデルをファインチューニングします。このトピックでは、以下のシナリオを例として、モデルのファインチューニングがどのように機能するかを説明します。

シナリオ例

物流では、自然言語から構造化情報 (受信者、住所、電話番号など) を抽出する必要があることがよくあります。Qwen3-235B-A22B のような大規模パラメーターモデルはうまく機能しますが、コストが高く、応答が遅くなります。パフォーマンスとコストのバランスを取るために、まず大規模パラメーターモデルを使用してデータにラベルを付けます。次に、そのデータを使用して Qwen3-0.6B のような小規模パラメーターモデルをファインチューニングし、同じタスクで同様のパフォーマンスを発揮するようにします。このプロセスはモデル蒸留と呼ばれます。

同じ構造化情報抽出タスクにおいて、元の Qwen3-0.6B モデルの精度は、50% です。ファインチューニング後、精度は 90% を超えます。

受信先アドレスの例

構造化された情報の例

アミナ・パテル - 電話番号 (474) 598-1543 - 米国ペンシルベニア州アレンタウン市 South 5th Street 1425 番地 Apt 3B、郵便番号 18104

{
    "state": "Pennsylvania",
    "city": "Allentown",
    "zip_code": "18104",
    "street_address": "1425 S 5th St, Apt 3B",
    "name": "Amina Patel",
    "phone": "(474) 598-1543"
}

データ準備

このタスクにおいて、教師モデル(Qwen3-235B-A22B)から Qwen3-0.6B へ知識を蒸留するには、まず教師モデルの API を使用して受信元アドレス情報を構造化された JSON データに抽出します。この JSON データの生成には時間がかかる場合があります。そのため、本トピックでは、学習用および検証用のサンプルデータセットを提供します train.json および eval.json。これらのファイルは、そのままダウンロードして使用できます。

モデル蒸留では、大規模パラメーターモデルは教師モデルとも呼ばれます。ここで使用されるすべてのデータは大規模モデルによってシミュレートされており、ユーザーの機密情報は含まれていません。

本番環境への適用

このソリューションをビジネスに適用するには、以下の方法でデータを準備することをお勧めします。

実際のビジネスシナリオ (推奨)

実際のビジネスデータはビジネスシナリオをよりよく反映し、ファインチューニングされたモデルはビジネスにより適応できます。ビジネスデータを取得したら、プログラムで以下の形式の JSON ファイルに変換する必要があります。

[
    {
        "instruction": "米国向けの配送情報から構造化された JSON を抽出するエキスパートアシスタントです。JSON キーは、name、street_address、city、state、zip_code、phone です。  Name: Isabella Rivera Cruz | 182 Calle Luis Lloréns Torres, Apt 3B, Mayagüez, Puerto Rico 00680 | MOBILE: (640) 486-5927",
        "output": "{\"name\": \"Isabella Rivera Cruz\", \"street_address\": \"182 Calle Luis Lloréns Torres, Apt 3B\", \"city\": \"Mayagüez\", \"state\": \"Puerto Rico\", \"zip_code\": \"00680\", \"phone\": \"(640) 486-5927\"}"
    },
    {
        "instruction": "米国向けの配送情報から構造化された JSON を抽出するエキスパートアシスタントです。JSON キーは、name、street_address、city、state、zip_code、phone です。  1245 Broadwater Avenue, Apt 3B, Bozeman, Montana 59715Receiver: Aisha PatelP: (429) 763-9742",
        "output": "{\"name\": \"Aisha Patel\", \"street_address\": \"1245 Broadwater Avenue, Apt 3B\", \"city\": \"Bozeman\", \"state\": \"Montana\", \"zip_code\": \"59715\", \"phone\": \"(429) 763-9742\"}"
    }
]

JSON ファイルには複数のトレーニングサンプルが含まれています。各サンプルには、instruction と output の2つのフィールドが含まれています。

  • instruction: 大規模モデルの動作をガイドするプロンプトと入力データが含まれています。

  • output: 期待される標準回答で、通常は人間の専門家または qwen3-235b-a22b などの大規模モデルによって生成されます。

モデル生成

ビジネスデータが不十分な場合は、データ拡張のためにモデルを使用することを検討してください。これにより、データの多様性とカバー率を向上させることができます。ユーザープライバシーの漏洩を避けるため、このソリューションではモデルを使用して仮想アドレスデータのバッチを生成します。以下の生成コードはご参照ください。

ビジネスデータ生成をシミュレートするためのコード

以下のコードを実行するには、Alibaba Cloud Model Studio API キーを作成する必要があります。このコードは、qwen-plus-latest を使用してビジネスデータを生成し、qwen3-235b-a22b を使用してラベル付けを行います。

# -*- coding: utf-8 -*-
import os
import asyncio
import random
import json
import sys
from typing import List
import platform
from openai import AsyncOpenAI

# 非同期クライアントインスタンスを作成します。
# 注: このスクリプトは DashScope 互換の API エンドポイントを使用します。
# 別の OpenAI 互換サービスを使用している場合は、base_url を変更してください。
client = AsyncOpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)

# 米国の州と準州のリスト。
us_states = [
    "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware",
    "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky",
    "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi",
    "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico",
    "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania",
    "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont",
    "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming", "District of Columbia",
    "Puerto Rico", "Guam", "American Samoa", "U.S. Virgin Islands", "Northern Mariana Islands"
]

# 受信者テンプレート。
recipient_templates = [
    "To: {name}", "Recipient: {name}", "Deliver to {name}", "For: {name}",
    "ATTN: {name}", "{name}", "Name: {name}", "Contact: {name}", "Receiver: {name}"
]

# 電話番号テンプレート。
phone_templates = [
    "Tel: {phone}", "Tel. {phone}", "Mobile: {phone}", "Phone: {phone}",
    "Contact number: {phone}", "Phone number {phone}", "TEL: {phone}", "MOBILE: {phone}",
    "Contact: {phone}", "P: {phone}", "{phone}", "Call: {phone}",
]


# もっともらしい米国スタイルの電話番号を生成します。
def generate_us_phone():
    """(XXX) XXX-XXXX 形式でランダムな 10 桁の米国電話番号を生成します。"""
    area_code = random.randint(201, 999)  # 0xx、1xx で始まる市外局番を避けます。
    exchange = random.randint(200, 999)
    line = random.randint(1000, 9999)
    return f"({area_code}) {exchange}-{line}"


# LLM を使用して受信者と住所の情報を生成します。
async def generate_recipient_and_address_by_llm(state: str):
    """指定された州の受信者の名前と住所の詳細を生成するために LLM を使用します。"""
    prompt = f"""米国 {state} の場所に関する受信者情報を生成してください。以下を含みます。
1. 現実的な英語のフルネーム。多様性を目指してください。
2. その州内の実在する市区町村名。
3. 番地、通り名、アパート番号など、特定の住所。現実的である必要があります。
4. その市区町村またはエリアに対応する 5 桁の郵便番号。

以下の形式で JSON オブジェクトのみを返してください。
{{"name": "受信者名", "city": "市区町村名", "street_address": "特定の住所", "zip_code": "郵便番号"}}

他のテキストは含めず、JSON のみを含めてください。名前が John Doe だけでなく、多様であることを確認してください。
"""

    try:
        response = await client.chat.completions.create(
            messages=[{"role": "user", "content": prompt}],
            model="qwen-plus-latest",
            temperature=1.5,  # temperature を高くして、より多様な名前と住所を生成します。
        )

        result = response.choices[0].message.content.strip()
        # Markdown コードブロックマーカーの可能性をクリーンアップします。
        if result.startswith('```'):
            result = result.split('\n', 1)[1]
        if result.endswith('```'):
            result = result.rsplit('\n', 1)[0]

        # JSON の解析を試みます。
        info = json.loads(result)
        print(info)
        return info
    except Exception as e:
        print(f"受信者と住所の生成に失敗しました: {e}、フォールバックを使用します。")
        # フォールバックメカニズム。
        backup_names = ["Michael Johnson", "Emily Williams", "David Brown", "Jessica Jones", "Christopher Davis",
                        "Sarah Miller"]
        return {
            "name": random.choice(backup_names),
            "city": "Anytown",
            "street_address": f"{random.randint(100, 9999)} Main St",
            "zip_code": f"{random.randint(10000, 99999)}"
        }


# 単一の生データレコードを生成します。
async def generate_record():
    """米国の住所情報を含む、乱雑に結合された 1 つの文字列を生成します。"""
    # 州をランダムに選択します。
    state = random.choice(us_states)

    # LLM を使用して受信者と住所の情報を生成します。
    info = await generate_recipient_and_address_by_llm(state)

    # 受信者名をフォーマットします。
    recipient = random.choice(recipient_templates).format(name=info['name'])

    # 電話番号を生成します。
    phone = generate_us_phone()
    phone_info = random.choice(phone_templates).format(phone=phone)

    # 完全な住所の行を組み立てます。
    full_address = f"{info['street_address']}, {info['city']}, {state} {info['zip_code']}"

    # すべてのコンポーネントを結合します。
    components = [recipient, phone_info, full_address]

    # コンポーネントの順序をランダム化します。
    random.shuffle(components)

    # ランダムな区切り文字を選択します。
    separators = [' ', ', ', '; ', ' | ', '\t', ' - ', ' // ', '', '  ']
    separator = random.choice(separators)

    # コンポーネントを結合します。
    combined_data = separator.join(components)
    return combined_data.strip()


# データのバッチを生成します。
async def generate_batch_data(count: int) -> List[str]:
    """指定された数のデータレコードを生成します。"""
    print(f"{count} 件のレコードの生成を開始しています...")

    # セマフォを使用して同時実行性を制御します (例: 最大 20 の同時リクエスト)。
    semaphore = asyncio.Semaphore(20)

    async def generate_single_record(index):
        async with semaphore:
            try:
                record = await generate_record()
                print(f"レコード #{index + 1} を生成しました: {record}")
                return record
            except Exception as e:
                print(f"レコード #{index + 1} の生成に失敗しました: {e}")
                return None

    # データを同時に生成します。
    tasks = [generate_single_record(i) for i in range(count)]

    data = await asyncio.gather(*tasks)

    successful_data = [record for record in data if record is not None]

    return successful_data


# データをファイルに保存します。
def save_data(data: List[str], filename: str = "us_recipient_data.json"):
    """生成されたデータを JSON ファイルに保存します。"""
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    print(f"データが {filename} に保存されました")


# フェーズ 1: データ生成。
async def produce_data_phase():
    """生の受信者データの生成を処理します。"""
    print("=== フェーズ 1: 生の受信者データ生成の開始 ===")

    # 2,000 件のレコードを生成します。
    batch_size = 2000
    data = await generate_batch_data(batch_size)

    # データを保存します。
    save_data(data, "us_recipient_data.json")

    print(f"\n生成されたレコードの総数: {len(data)}")
    print("\nサンプルデータ:")
    for i, record in enumerate(data[:3]):  # 最初の 3 件を例として表示します。
        print(f"{i + 1}. 生データ: {record}\n")

    print("=== フェーズ 1 完了 ===\n")
    return True


# 抽出モデルのシステムプロンプトを定義します。
def get_system_prompt_for_extraction():
    """情報抽出タスクのシステムプロンプトを返します。"""
    return """あなたは、非構造化テキストから米国の配送先住所を解析することを専門とする、プロの情報抽出アシスタントです。

## タスクの説明
指定された入力テキストに基づいて、以下の 6 つのフィールドを含む JSON オブジェクトを正確に抽出し、生成してください。
- name: 受信者のフルネーム。
- street_address: 番地、通り名、アパートやスイート番号を含む完全な住所。
- city: 市区町村名。
- state: 州の正式名称 (例: "CA" ではなく "California")。
- zip_code: 5 桁または 9 桁の郵便番号。
- phone: 完全な連絡先電話番号。

## 抽出ルール
1.  **住所の処理**:
    -   住所、市区町村、州、郵便番号の各コンポーネントを正確に特定します。
    -   `state` フィールドは、州の正式名称でなければなりません (例: "NY" ではなく "New York")。
    -   `street_address` には、市区町村名の前にあるすべての詳細 (例: "123 Apple Lane, Apt 4B") を含める必要があります。
2.  **名前の特定**:
    -   受信者のフルネームを抽出します。
3.  **電話番号の処理**:
    -   元のフォーマットを維持したまま、完全な電話番号を抽出します。
4.  **郵便番号**:
    -   5 桁または 9 桁 (ZIP+4) の郵便番号を抽出します。

## 出力フォーマット
以下の JSON フォーマットに厳密に従ってください。説明文や Markdown を追加しないでください。
{
  "name": "受信者のフルネーム",
  "street_address": "完全な住所",
  "city": "市区町村名",
  "state": "州の正式名称",
  "zip_code": "郵便番号",
  "phone": "連絡先電話番号"
}
"""


# LLM を使用して生テキストから構造化データを予測します。
async def predict_structured_data(raw_data: str):
    """LLM を使用して生の文字列から構造化データを予測します。"""
    system_prompt = get_system_prompt_for_extraction()

    try:
        response = await client.chat.completions.create(
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": raw_data}
            ],
            model="qwen3-235b-a22b",  # このタスクには強力なモデルが推奨されます。
            temperature=0.0,  # 抽出の精度を高めるために temperature を低くします。
            response_format={"type": "json_object"},
            extra_body={"enable_thinking": False}
        )

        result = response.choices[0].message.content.strip()

        # Markdown コードブロックマーカーの可能性をクリーンアップします。
        if result.startswith('```'):
            lines = result.split('\n')
            for i, line in enumerate(lines):
                if line.strip().startswith('{'):
                    result = '\n'.join(lines[i:])
                    break
        if result.endswith('```'):
            result = result.rsplit('\n```', 1)[0]

        structured_data = json.loads(result)
        return structured_data

    except Exception as e:
        print(f"構造化データの予測に失敗しました: {e}、生データ: {raw_data}")
        # 失敗した場合は空の構造を返します。
        return {
            "name": "",
            "street_address": "",
            "city": "",
            "state": "",
            "zip_code": "",
            "phone": ""
        }


# フェーズ 2: データ変換。
async def convert_data_phase():
    """生データを読み取り、構造化フォーマットを予測し、SFT データとして保存します。"""
    print("=== フェーズ 2: SFT フォーマットへのデータ変換の開始 ===")

    try:
        print("us_recipient_data.json ファイルを読み込んでいます...")
        with open('us_recipient_data.json', 'r', encoding='utf-8') as f:
            raw_data_list = json.load(f)

        print(f"{len(raw_data_list)} 件のレコードの読み取りに成功しました。")
        print("抽出モデルを使用して構造化データの予測を開始しています...")

        # シンプルで明確なシステムメッセージは、トレーニングと推論の速度を向上させることができます。
        system_prompt = "あなたは、米国の配送先情報から構造化 JSON を抽出する専門のアシスタントです。JSON キーは name、street_address、city、state、zip_code、phone です。"
        output_file = 'us_recipient_sft_data.json'

        # セマフォを使用して同時実行性を制御します。
        semaphore = asyncio.Semaphore(10)

        async def process_single_item(index, raw_data):
            async with (semaphore):
                structured_data = await predict_structured_data(raw_data)
                print(f"レコード #{index + 1} を処理しています: {raw_data}")

                conversation = {
                        "instruction": system_prompt + '  ' + raw_data,
                        "output": json.dumps(structured_data, ensure_ascii=False)
                }

                return conversation

        print(f"{output_file} への変換を開始しています...")

        tasks = [process_single_item(i, raw_data) for i, raw_data in enumerate(raw_data_list)]
        conversations = await asyncio.gather(*tasks)

        with open(output_file, 'w', encoding='utf-8') as outfile:
            json.dump(conversations, outfile, ensure_ascii=False, indent=4)

        print(f"変換完了!{len(raw_data_list)} 件のレコードを処理しました。")
        print(f"出力ファイル: {output_file}")
        print("=== フェーズ 2 完了 ===")

    except FileNotFoundError:
        print("エラー: us_recipient_data.json が見つかりません。")
        sys.exit(1)
    except json.JSONDecodeError as e:
        print(f"JSON デコードエラー: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"変換中にエラーが発生しました: {e}")
        sys.exit(1)


# メイン関数。
async def main():
    print("データ処理パイプラインを開始しています...")
    print("このプログラムは 2 つのフェーズを順次実行します:")
    print("1. 生の米国受信者データを生成します。")
    print("2. 構造化データを予測し、SFT フォーマットに変換します。")
    print("-" * 50)

    # フェーズ 1: データを生成します。
    success = await produce_data_phase()

    if success:
        # フェーズ 2: データを変換します。
        await convert_data_phase()

        print("\n" + "=" * 50)
        print("すべてのプロセスが正常に完了しました。")
        print("生成されたファイル:")
        print("- us_recipient_data.json: 生の非構造化データリスト。")
        print("- us_recipient_sft_data.json: SFT フォーマットのトレーニングデータ。")
        print("=" * 50)
    else:
        print("データ生成フェーズが失敗しました。終了します。")


if __name__ == '__main__':
    # 必要に応じて Windows のイベントループポリシーを設定します。
    if platform.system() == 'Windows':
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

    # メインのコルーチンを実行します。
    asyncio.run(main(), debug=False)

モデルのファインチューニング

  1. 左側のナビゲーションウィンドウで、[モデルギャラリー] をクリックします。 「Qwen3-0.6B」タブを検索して選択します。次に、Fine-tune をクリックします。

    image

  2. これらの主要なパラメーターのみを構成する必要があります。その他のパラメーターはデフォルト値のままにしてください。

    • Training Mode: 教師ありファインチューニング (SFT) および LoRA がデフォルトで選択されています。

      LoRA は、モデルのパラメーターの一部のみを変更してトレーニングリソースを節約する効率的なファインチューニング手法です。
    • Training dataset: まず、サンプルトレーニングデータセット train.json をダウンロードします。 次に、設定ページで OSS file or directory を選択します。 image アイコンをクリックしてバケットを選択します。 [ファイルのアップロード] をクリックして、ダウンロードしたトレーニングデータセットを OSS にアップロードします。 その後、そのファイルを選択します。

      image

    • Validate dataset: 最初に、検証用データセット eval.json をダウンロードします。次に、Add validation dataset をクリックします。学習用データセットの場合と同じ手順に従って、ファイルをアップロードおよび選択できます。

      検証データセットは、トレーニング中のモデルのパフォーマンスを評価し、モデルが未知のデータにどの程度汎化するかを測定するのに役立ちます。
    • Model output path: ファインチューニング済みモデルは、デフォルトで OSS に保存されます。OSS フォルダが空の場合、[新しいフォルダを作成] して指定できます。

    • Resource Group Type: Public Resource Group を選択します。このファインチューニングには、約 5 GB の GPU メモリが必要です。コンソールでは、すでに適切なインスタンスタイプがフィルター処理されています。たとえば、ecs.gn7i-c16g1.4xlarge を選択してください。

    • Hyperparameters:

      • learning_rate: 0.0005 に設定

      • num_train_epochs: 4 に設定

      • per_device_train_batch_size: 8 に設定

      • seq_length: 512 に設定

      次に、Train[OK] をクリックします。トレーニングジョブのステータスが Creating に変わります。ステータスが In operation になると、ファインチューニングが開始されます。

  3. トレーニングジョブを表示し、完了するまで待つことができます。ファインチューニングには約10分かかります。ファインチューニング中、ジョブ詳細ページにはログとメトリック曲線が表示されます。トレーニングが完了すると、ファインチューニングされたモデルは指定した OSS ディレクトリに保存されます。

    後でトレーニングジョブの詳細を表示するには、左側のナビゲーションウィンドウで、Model GalleryJob ManagementTraining Jobs をクリックします。次に、ジョブ名をクリックします。

    image

    (オプション) 損失曲線に基づいてハイパーパラメーターを調整し、モデルのパフォーマンスを向上させる

    ジョブ詳細ページで、train_loss 曲線 (トレーニングセット損失) と eval_loss 曲線 (検証セット損失) を確認できます。

    imageimage

    損失値の傾向を使用して、現在のトレーニングパフォーマンスを評価できます。

    • トレーニング終了前に train_loss と eval_loss の両方が減少し続ける場合 (過小適合)

      num_train_epochs (トレーニングエポック、トレーニング深度と正の相関関係がある) を増やすか、lora_rank (低ランク行列のランク。ランクが高いほど複雑なタスクが可能になりますが、過学習のリスクがあります) を増やすことで、トレーニングデータへのモデルの適合度を向上させることができます。

    • トレーニング終了前に train_loss が減少し続け、eval_loss が増加し始める場合 (過学習)

      過学習を防ぐために、num_train_epochs を減らすか、lora_rank を減らすことができます。

    • トレーニング終了前に train_loss と eval_loss の両方が安定する場合 (良好な適合)

      次に、次のステップに進むことができます。

ファインチューニング済みモデルのデプロイ

学習タスクの詳細ページで、Deploy ボタンをクリックしてデプロイ構成ページを開きます。「Resource Type」を「Public Resources」に設定します。0.6B モデルをデプロイするには、約 5 GB の VARM が必要です。「[リソース仕様]」セクションでは、適切なオプションがあらかじめフィルター処理されています。たとえば、「ecs.gn7i-c8g1.2xlarge」などのインスタンスを選択します。その他のすべてのパラメーターはデフォルト値のままにして、Deploy > OK をクリックします。

デプロイメントには約 5 分かかります。ステータスが Running に変更されると、デプロイメントが完了します。

後でトレーニングジョブの詳細を表示するには、左側のナビゲーションウィンドウで、Model Gallery > Job Management > Training Jobs をクリックします。次に、ジョブの名前をクリックします。

image

トレーニングジョブが成功と表示された後も [デプロイ] ボタンが無効になっている場合、モデルはまだ登録中です。約1分間お待ちください。

image

次に、「モデルの呼び出し」で説明されているのと同じ手順に従ってモデルを呼び出します。

ファインチューニング済みモデルの評価

ファインチューニングされたモデルを本番環境にデプロイする前に、そのパフォーマンスを体系的に評価します。これにより、良好な安定性と精度が確保され、起動後の予期せぬ問題を防ぐことができます。

テストデータの準備

トレーニングデータと重複しないテストデータを準備する必要があります。このトピックではテストデータセットを提供します。精度テストコードはテストデータを自動的にダウンロードします。

重複しないテストデータは、新しいデータに対するモデルの汎化能力をより正確に測定し、「既知の」サンプルからの誇張されたスコアを回避します。

評価メトリックの定義

評価メトリックは実際のビジネス目標と一致する必要があります。このトピックでは、生成された JSON 文字列が有効であるかを確認するだけでなく、キーと値が正しいことも検証します。

コードで評価メトリックを定義する必要があります。たとえば、以下の精度テストコードの compare_address_info メソッドをご参照ください。

ファインチューニング済みモデルの評価

以下のテストコードを実行して、テストデータセットに対するモデルの精度を取得します。

モデル精度をテストするためのサンプルコード

注:Token およびエンドポイント URL を、事前に取得した実際の値に置き換えてください。

# pip3 install openai
from openai import AsyncOpenAI
import requests
import json
import asyncio
import os

# 'Token' 環境変数が設定されていない場合、次の行を EAS サービスから取得したトークンに置き換えてください: token = 'YTA1NTEzMzY3ZTY4Z******************'
token = os.environ.get("Token")

# サービス URL の後の "/v1" サフィックスは削除しないでください。
client = OpenAI(
    api_key=token,
    base_url=f'<Your_Service_URL>/v1',
)

if token is None:
    print("Please set the 'Token' environment variable, or assign your token directly to the 'token' variable.")
    exit()

system_prompt = """You are a professional information extraction assistant specializing in parsing US shipping addresses from unstructured text.

## Task Description
Based on the given input text, accurately extract and generate a JSON object containing the following six fields:
- name: The full name of the recipient.
- street_address: The complete street address, including number, street name, and any apartment or suite number.
- city: The city name.
- state: The full state name (e.g., "California", not "CA").
- zip_code: The 5 or 9-digit ZIP code.
- phone: The complete contact phone number.

## Extraction Rules
1.  **Address Handling**:
    -   Accurately identify the components: street, city, state, and ZIP code.
    -   The `state` field must be the full official name (e.g., "New York", not "NY").
    -   The `street_address` should contain all details before the city, such as "123 Apple Lane, Apt 4B".
2.  **Name Identification**:
    -   Extract the full recipient name.
3.  **Phone Number Handling**:
    -   Extract the complete phone number, preserving its original format.
4.  **ZIP Code**:
    -   Extract the 5-digit or 9-digit (ZIP+4) code.

## Output Format
Strictly adhere to the following JSON format. Do not add any explanatory text or markdown.
{
  "name": "Recipient's Full Name",
  "street_address": "Complete Street Address",
  "city": "City Name",
  "state": "Full State Name",
  "zip_code": "ZIP Code",
  "phone": "Contact Phone Number"
}
"""


def compare_address_info(actual_address_str, predicted_address_str):
    """Compares two JSON strings representing address information to see if they are identical."""
    try:
        # Parse the actual address information
        if actual_address_str:
            actual_address_json = json.loads(actual_address_str)
        else:
            actual_address_json = {}

        # Parse the predicted address information
        if predicted_address_str:
            predicted_address_json = json.loads(predicted_address_str)
        else:
            predicted_address_json = {}

        # Directly compare if the two JSON objects are identical
        is_same = actual_address_json == predicted_address_json

        return {
            "is_same": is_same,
            "actual_address_parsed": actual_address_json,
            "predicted_address_parsed": predicted_address_json,
            "comparison_error": None
        }

    except json.JSONDecodeError as e:
        return {
            "is_same": False,
            "actual_address_parsed": None,
            "predicted_address_parsed": None,
            "comparison_error": f"JSON parsing error: {str(e)}"
        }
    except Exception as e:
        return {
            "is_same": False,
            "actual_address_parsed": None,
            "predicted_address_parsed": None,
            "comparison_error": f"Comparison error: {str(e)}"
        }


async def predict_single_conversation(conversation_data):
    """Predicts the label for a single conversation."""
    try:
        # Extract user content (excluding assistant message)
        messages = conversation_data.get("messages", [])
        user_content = None

        for message in messages:
            if message.get("role") == "user":
                user_content = message.get("content", "")
                break

        if not user_content:
            return {"error": "User message not found"}

        response = client.chat.completions.create(
            model="Qwen3-0.6B",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_content}
            ],
            response_format={"type": "json_object"},
            extra_body={
                "enable_thinking": False
            }
        )

        predicted_labels = response.choices[0].message.content.strip()
        return {"prediction": predicted_labels}

    except Exception as e:
        return {"error": f"Prediction failed: {str(e)}"}


async def process_batch(batch_data, batch_id):
    """Processes a batch of data."""
    print(f"Processing batch {batch_id}, containing {len(batch_data)} items...")

    tasks = []
    for i, conversation in enumerate(batch_data):
        task = predict_single_conversation(conversation)
        tasks.append(task)

    results = await asyncio.gather(*tasks, return_exceptions=True)

    batch_results = []
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            batch_results.append({"error": f"Exception: {str(result)}"})
        else:
            batch_results.append(result)

    return batch_results


async def main():
    output_file = "predicted_labels.jsonl"
    batch_size = 20  # Number of items to process per batch

    # Read test data
    url = 'https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20251015/yghxco/test.jsonl'
    conversations = []

    try:
        response = requests.get(url)
        response.raise_for_status()  # Check if the request was successful
        for line_num, line in enumerate(response.text.splitlines(), 1):
            try:
                data = json.loads(line.strip())
                conversations.append(data)
            except json.JSONDecodeError as e:
                print(f"JSON parsing error on line {line_num}: {e}")
                continue
    except requests.exceptions.RequestException as e:
        print(f"Request error: {e}")
        return

    print(f"Successfully read {len(conversations)} conversation data items")

    # Process in batches
    all_results = []
    total_batches = (len(conversations) + batch_size - 1) // batch_size

    for batch_id in range(total_batches):
        start_idx = batch_id * batch_size
        end_idx = min((batch_id + 1) * batch_size, len(conversations))
        batch_data = conversations[start_idx:end_idx]

        batch_results = await process_batch(batch_data, batch_id + 1)
        all_results.extend(batch_results)

        print(f"Batch {batch_id + 1}/{total_batches} completed")

        # Add a small delay to avoid making requests too quickly
        if batch_id < total_batches - 1:
            await asyncio.sleep(1)

    # Save results
    same_count = 0
    different_count = 0
    error_count = 0

    with open(output_file, 'w', encoding='utf-8') as f:
        for i, (original_data, prediction_result) in enumerate(zip(conversations, all_results)):
            result_entry = {
                "index": i,
                "original_user_content": None,
                "actual_address": None,
                "predicted_address": None,
                "prediction_error": None,
                "address_comparison": None
            }

            # Extract original user content
            messages = original_data.get("messages", [])
            for message in messages:
                if message.get("role") == "user":
                    result_entry["original_user_content"] = message.get("content", "")
                    break

            # Extract actual address information (if assistant message exists)
            for message in messages:
                if message.get("role") == "assistant":
                    result_entry["actual_address"] = message.get("content", "")
                    break

            # Save prediction result
            if "error" in prediction_result:
                result_entry["prediction_error"] = prediction_result["error"]
                error_count += 1
            else:
                result_entry["predicted_address"] = prediction_result.get("prediction", "")

                # Compare address information
                comparison_result = compare_address_info(
                    result_entry["actual_address"],
                    result_entry["predicted_address"]
                )
                result_entry["address_comparison"] = comparison_result

                # Tally comparison results
                if comparison_result["comparison_error"]:
                    error_count += 1
                elif comparison_result["is_same"]:
                    same_count += 1
                else:
                    different_count += 1

            f.write(json.dumps(result_entry, ensure_ascii=False) + '\n')

    print(f"All predictions complete! Results have been saved to {output_file}")

    # Statistics
    success_count = sum(1 for result in all_results if "error" not in result)
    prediction_error_count = len(all_results) - success_count
    print(f"Number of samples: {success_count}")
    print(f"Correct responses: {same_count}")
    print(f"Incorrect responses: {different_count}")
    print(f"Accuracy: {same_count * 100 / success_count} %")


if __name__ == "__main__":
    asyncio.run(main())

出力:

All predictions complete! Results have been saved to predicted_labels.jsonl
Number of samples: 400
Correct responses: 382
Incorrect responses: 18
Accuracy: 95.5 %
ファインチューニング時のランダム性(例:乱数シード)や大規模言語モデルの出力のばらつきにより、実際の精度はこのトピックの結果と異なる場合があります。これは正常な現象です。

精度は 95.5 % であり、元の Qwen3-0.6B モデルの 50 % よりも大幅に向上しています。これにより、ファインチューニングされたモデルが物流フォーム入力における構造化情報抽出を大幅に改善することが示されています。

このトピックでは、学習時間を短縮するために、トレーニングエポック数を 4 回に限定しています。わずか 4 エポックで精度は 95.5 % に達しています。さらに精度を高めるには、エポック数を増やすことができます。

重要な注意事項

このトピックでは、パブリックリソースを使用してモデルサービスを作成します。課金は従量課金制です。サービスが不要になった場合は、追加料金を避けるために停止または削除してください

image

参考

  • 評価や圧縮などの Model Gallery の詳細については、「Model Gallery」をご参照ください。

  • Auto Scaling、ストレステスト、監視アラートなどの EAS の詳細については、「EAS の概要」をご参照ください。