InstructLabでポケモンタイプ相性の知識を学習させる

## はじめに
大規模言語モデル (LLM) を自分好みに調整し、新しい知識を付与したり活用できる仕組みがどんどん整備されてきています。

[前回の記事](https://qiita.com/sdt-bizdev/items/5ccc2691a662b7d906fb)で紹介した **InstructLab** は、合成データ生成やファインチューニング機能が用意されており、比較的少ないリソースで独自のLLMを構築・拡張できます。

この記事では、InstructLabを用いて学習させる手順を簡単にまとめます。実際のユースケースの一例として、**ポケモンタイプ相性の知識**を学習させたモデルを使って、ポケモン対戦における相性を予測するアプリケーションを作成してみます。


## InstructLabの特徴

### 1. モデル調整に必要なデータを生成
InstructLabには、学習に必要な合成データを簡単に生成する仕組みが整っています。あらかじめ追加したい知識やスキルをYAMLファイルなどで定義し、合成データ生成コマンドを実行するだけで、かなりのボリュームの学習データを自動生成できます。

### 2. 少ないリソースでLLMファインチューニング可能
多くのリソースを必要とするファインチューニングですが、InstructLabは効率的な学習アルゴリズムを備えており、コマンド一発でローカルマシンでも学習を進められます。

### 3. 知識をコミュニティに還元し、基盤モデルの拡張に貢献
InstructLabでは、学習させたい新知識をGitHubリポジトリにアップロード・プルリクエストすることで、コミュニティに知識を還元できます。モデル調整で得られた知見や合成データの品質が高い場合、将来的には基盤モデルに反映されるチャンスもあるとのことです。


## 今回の検証内容
今回は、**ポケモンタイプ相性**という新しい知識をLLMに学習させるケースを検証します。具体的には、以下の要素を追加することで合成データを生成し、モデル調整を行います。


## データ生成
### 知識ファイルの用意
1. **taxonomyを記述したYAMLファイル**:  
    - **knowledge**: 学習させたい新しい事実・知識
        - タイプに関する知識コンテキストとQAペアを定義  （例：「みずタイプはほのおタイプに対してこういう効果がある」など）
    - **skills**: 「韻を踏む」「語尾を変更」「表の読み取り方」といったルール・規範
        - タイプ相性表の読み取り方を定義  （例：「相性表の行が攻撃側のタイプ、列が防御側のタイプを示している」など）

    これらの追加知識をもとに、合成データ生成・モデル調整・学習済みモデルの活用を行います。
    
    合成データ生成のために、あらかじめ用意した種データ数については下記の通りです。
    |           | context | QAペア数 |
    |-----------|-----|-----|
    | knowledge | 32  | 96  |
    | skills    | 5   | 5   |

    以下はknowledgeファイルの例です。
    ```yaml: qna.yaml (knowledge)
    seed_examples:
      - context: |
          Pokémon type matchups determine the effectiveness of moves in battle. Types like **Fire**, **Water**, and **Grass** have advantages and disadvantages against one another. Fire moves are super effective against Grass but are not very effective against Water. Water moves, in turn, are strong against Fire but weak against Grass. Grass moves are powerful against Water but weak against Fire. These interactions create a strategic element in battles.
        questions_and_answers:
          - question: |
              Which type is weak to fire moves but strong against water moves?
            answer: |
              Grass-type.
          - question: |
              What type of move is super effective against fire-type Pokémon?
            answer: |
              Water-type.
          - question: |
              How effective are fire moves against water-type Pokémon?
            answer: |
              not very effective.
    ```

    以下はskillsファイルの例です。
    ```yaml: qna.yaml (skills)
    seed_examples:
      - context: |
          | **Attack/Defense** | **Fire** | **Water** | **Grass** |
          |--------------------|----------|-----------|-----------|
          | **Fire**           | 0.5x     | 0.5x      | 2x        |
          | **Water**          | 2x       | 0.5x      | 0.5x      |
          | **Grass**          | 0.5x     | 2x        | 0.5x      |
        question: |
          Which type is weak against fire but strong against water?
        answer: |
          Grass-type.
      - context: |
          | **Attack/Defense** | **Fire** | **Water** | **Grass** |
          |--------------------|----------|-----------|-----------|
          | **Fire**           | 0.5x     | 0.5x      | 2x        |
          | **Water**          | 2x       | 0.5x      | 0.5x      |
          | **Grass**          | 0.5x     | 2x        | 0.5x      |
        question: |
          If a fire-type Pokémon attacks a water-type Pokémon, what is the damage multiplier?
        answer: |
          0.5x.
    ```
    <br>

2. **学習したい知識を記述したマークダウンファイル:**
InstructLabで学習させた知識を管理するために、GitHubリポジトリを用意しましょう。
実際に学習させたい知識をテキストにまとめたマークダウン形式のファイルを用意し、自身が管理するGitHubリポジトリにアップロードしておきます。例えば「pokemon_type_pros_cons.md」のような名前で、各タイプ相性の具体的な説明を記載しておきます。
![スクリーンショット 2025-02-06 9.25.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3958389/258656fa-ba8e-c29a-9fae-ad2f2c481394.png)
知識ファイルを格納するGitHubリポジトリとマークダウンファイルは[こちら](https://github.com/juliadenham/Summit_knowledge)を参考に作成してください。

以上のファイルがそろったら、InstructLabコマンドを使って**合成データ生成**を実行することで、学習に最適化されたデータが自動的に作られます。

```bash
$ ilab data generate --pipeline full --gpus 4
```
データ数が多かったためか、自分の環境（M3 Mac Pro メモリ36GB）では処理に3時間くらいかかりました。。

## （任意） 新しい知識をコミュニティに還元する
一連の知識ファイル（`qna.yaml`）は内容を精査したうえで**知識の信頼性**が確保されれば、最終的に基盤モデルにも反映される可能性があります。InsructLabコミュニティに貢献したいという方は積極的に本家[taxonomyリポジトリ](https://github.com/instructlab/taxonomy)にプルリクエストをして知識を還元しましょう。

## モデル学習
生成した合成データを使って、InstructLabでLLMのファインチューニングを行います。
学習のモードで`--pipeline full`は64GB以上のRAMが必要そうなので、スペックのあるマシンをお持ちの方は試してみてください。
今回はイテレーション数500回で検証しましたが、本来であれば検証データセットへの損失を見て最適なイテレーション数を探ってください。
```bash
$ ilab model train --pipeline simple --device mps --iters 500
```

自分の環境では1時間弱で学習が終了しました。


## モデルアップロード
`instructlab==0.23.0`から学習モデルのアップロードが可能になりました。
満足のいく学習済みモデルが完成したら、以下コマンドでモデルをHugging Faceにアップロードしておきましょう。
```bash
$ ilab model upload --dest-type hf --model granite-7b-lab --destination instructlab/granite-7b-lab --release main
```


## アプリ作成
学習済みのポケモンタイプ相性モデルを使って、Streamlitで簡単なデモアプリを作成してみます。
アプリは、防御側のタイプをユーザーが入力すると、学習済みモデルから相性を予測して相性の良いタイプ表示してくれるものです。

以下のようにUIを作成し、モデル調整有無での比較をしてみました
![スクリーンショット 2025-02-06 8.37.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3958389/42bfd002-196b-37ba-b66a-67de1900b9a7.png)

まだまだ学習の余地があるかと思いますが、調整後の方が端的に求めている回答をしてくれています。

アプリのソースコードは以下の通りです。画像では日本語翻訳をしています。
```python: app.py
import streamlit as st
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage

# Streamlit アプリのタイトル
st.title("ポケモンタイプ相性チェッカー（モデル比較）")

# ユーザー入力
defense_type = st.selectbox("防御側のタイプを選んでください", ["ほのお", "みず", "くさ", "でんき"])
mapping_type = {
    "ほのお": "fire-type",
    "みず"　: "water-type",
    "くさ"　: "grass-type",
    "でんき": "electirc-type",
}

# 質問を作成
question = f"Which type of moves are effective against {mapping_type[defense_type]} Pokemon? 1 sentence answer please."

# モデル1の設定（オリジナル）
model_path_1 = "~/.cache/instructlab/models/granite-7b-lab-Q4_K_M.gguf"
model_1 = ChatOpenAI(
    model_name = model_path_1,
    base_url   = "http://127.0.0.1:8000/v1/",
    api_key    = "myapi",
)

# モデル2の設定（ファインチューニング版）
model_path_2 = "~/ilab/instructlab-granite-7b-lab-trained/instructlab-granite-7b-lab-Q4_K_M.gguf"
model_2 = ChatOpenAI(
    model_name = model_path_2,
    base_url   = "http://127.0.0.1:8001/v1/",
    api_key    = "myapi",
)

def generate(model, text: str):
    messages = [
        SystemMessage(content="You are a helpful assistant."),
        HumanMessage(content=text)
    ]

    response = ""
    for chunk in model.stream(messages):
        response += chunk.content
        print(chunk.content, end="", flush=True)

    return response

# モデルからの応答を取得
if st.button("相性をチェック"):
    with st.spinner("AIが回答中です..."):
        # モデル1からの応答
        response_1 = generate(model_1, question)

        # モデル2からの応答
        response_2 = generate(model_2, question)

        # 結果を表示
        st.subheader("モデル調整前の結果")
        st.write(f"相性結果：{response_1}")

        st.subheader("モデル調整後の結果")
        st.write(f"相性結果：{response_2}")

```


続いて表の読み取りも検証してみました。
LLMの入力プロンプト部分に、表を文字列として直接結合しています。
```python
header = """
      | **Attack/Defense** | **Fire** | **Water** | **Grass** |
      |--------------------|----------|-----------|-----------|
      | **Fire**           | 0.5x     | 0.5x      | 2x        |
      | **Water**          | 2x       | 0.5x      | 0.5x      |
      | **Grass**          | 0.5x     | 2x        | 0.5x      |
"""
question = f"Which type deals 2x damage to {mapping_type[defense_type]} Pokémon?"
response = generate(header + question)
```

結果は以下の通りで、水タイプの弱点は草タイプであるとうまく推論することができました。
![スクリーンショット 2025-02-06 9.08.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3958389/be3a38e2-dc4f-0ab6-0cd9-4f909234c713.png)
表の読み取りに向けて用意したskillsの初期データ数は5件のみでしたが、これは強力です。


## まとめ
- InstructLabを使うことで、合成データ生成からファインチューニング、モデル公開までの流れがシンプルになります。
- ポケモンタイプ相性のように新しい知識をYAMLやMarkdownで定義し、それをコミュニティに還元する仕組みが整っているので、LLMの拡張が容易です。
- ファインチューニング自体も少ないリソースで実行可能で、ハードルが下がっている点は夢のある話だと思います。

短い検証でしたが、思った以上に手軽にLLMを拡張できる可能性を感じました。今回はポケモンを題材にしましたが、法律や医学などの専門領域でも活用が進んでいくことが期待されます。

以上、InstructLabでポケモンタイプ相性の知識を学習させる手順でした。ぜひ興味のある方は試してみてください！