白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

プロンプトの自動チューニングがしたい

こんばんは。
ようやく秋の気配がしてきましたね。

自分でチューニングしてほしい

LLMを使っているとやはりプロンプトの書き方によって求める結果が得られるかどうかが大きく変わってくることがわかります。
もちろん自分で試行錯誤しながらチューニングするのが正攻法なのですが、正直LLMの気持ちはよくわからないので自分でチューニングしてくれないかなというのが今回の内容です。

ソースコードを読みたい、試してみたい場合はこちらをどうぞ。
github.com

仕組み

仕組みとしてはかなりシンプルです。まぁ…紆余曲折ありましたが…。

  1. ユーザにシステムプロンプト、ユーザプロンプトを入力してもらう
  2. LLMに改良した新しいプロンプトを5つ生成してもらう
  3. それぞれのプロンプトで回答を生成して画面に表示する
  4. ユーザに評価してもらう※
  5. 新しい世代を作成する
  6. 2~5を満足するまで繰り返す

評価の方法は以下の通りです。

  • 生成されたプロンプトとその回答を見て、良いと思った順に並び替える
  • ふさわしくないと思うプロンプトを除外する
  • 追加の要望があれば入力する


工夫したポイント・苦労したこと

遺伝的アルゴリズムをシミュレートする

プロンプトの改良にあたって、徐々に変化させながら思いがけないアイデアを得たいというニーズから遺伝的アルゴリズムを使えないかと考えました。
一点交差や一様交差を使えば急激な文脈の変化を抑制できるし、突然変異によってある程度の変化も見込むことができます。

ただし、遺伝的アルゴリズムの特性上、遺伝子にあたる要素を作らなくてはなりません。
文章を丸ごとベクトル化してしまえば遺伝子のようなデータは作成できますが、逆にベクトルから文章に戻す方法がありません。(そろそろこの手法が生まれてくれないかなとか期待していますが)
そのため、遺伝的アルゴリズムを適用するのではなく、LLMによってそれっぽく模倣してもらうという手段を取っています。

LLMはなんとなくそれっぽい感じに処理するのは結構得意なジャンルです。
内部的には絶対実行していないですが、遺伝的アルゴリズムを使って改良して欲しいと注文するとなんとなくそれっぽい内容がちゃんと返ってきます。
詳しいプロンプトに関してはここには掲載しませんが、例えば以下のような進化をたどったりします。

◆オリジナル
- 以下の3つの共通点を考えてください

◆第一世代
- 以下の3つの共通点を挙げ、その共通性の根拠を説明してください。
- この3つの要素に共通する特徴は何ですか?それを詳しく述べてください。
- 挙げられた3つのケースについて、どのような共通点が見られるかを分析してください。
- これらの3つの項目が共有している点を考察し、その重要性を説明してください。
- 3つの対象について、共通のテーマやスローガンを見出し、それに基づいて解説をしてください。

◆第二世代
- この3つのケースに共通して見られる特徴を分析し、それがどのように結びついているのか解説してください。
- 挙げられた3つの要素について、どのような共通点があると考えられますか? その根拠を詳述し、関連性を示してください。
- それぞれの項目に共通する側面を考察し、なぜそれが重要であるのかを論じてください。
- この3つの項目が持つ共通点を挙げ、それに対する理由とその重要性を詳しく説明してください。
- 3つの対象に共通する特性について検討し、それがもたらす影響について述べてください。
タスクを細かく分割する

プロンプトの工夫で物事を解決しようと思って始めたことなのでちょっと寂しいところではありますが、基本的に複雑な指示をするよりもできる限りタスクを分割する方が精度は良くなります。
最初は新しいプロンプトとその回答、LLM自身が考える評価点で並び替えて回答みたいな方式を取っていましたがなかなかうまくいきませんでした。
顕著な例がどんなに言い聞かせてもユーザプロンプトの内容をシステムプロンプトに組み込んでしまうパターンです。例えば以下のような例です。

◆システムプロンプト
以下の3つの共通点を考えてください

◆ユーザプロンプト
カニ、時計、旅行

◆生成プロンプト
- カニ、時計、旅行の共通点は何でしょうか?
- カニ、時計、旅行に共通するテーマは何かありますか?
- カニ、時計、旅行の類似点を教えてください。
- カニ、時計、旅行から得られる教訓は何でしょうか?
- カニ、時計、旅行の間にある関連性を見つけてください。

プロンプトを生成して欲しいのにときどき回答が生成される場合などもあり、挙動としてもあまり安定しません。
新しいプロンプトを生成するタスクと、生成されたプロンプトから単純に回答を取得するというタスクに分割するととても安定します。
トークンを削減しようと割と何でも詰め込んでしまいがちですが、変に不安定な指示でエラーハンドリングをするよりもだいぶ余計なことにかける時間が減らせます。

追加プロンプト

ちょっと強引なやり方をしているのが追加プロンプトの部分です。
別のシステムプロンプトとして渡してうまく利用して欲しいと指示する方法をいろいろ試したのですが、うまくいきませんでした。
追加の指示は回答に対して反映して欲しいのにどうしてもプロンプト側に反映されてしまいます。

◆第一世代
- 次の3つの事柄に共通する特徴は何か、それぞれの事象を踏まえて明確に説明してください。
- 与えられた3つのアイテムの共通点を挙げ、その理由を分かりやすく解説してください。
- 以下の3つの要素の共通点を探り、どのような相関関係があるのかを具体的に示してください。
- あなたが提示する3つの項目に共通するテーマや特性を見つけ、それに基づいて詳細に解説してください。
- 次の3つの例に見られる共通の特徴を特定し、なぜそれらに共通点があるのかを論理的に説明してください。

◆追加プロンプト
- 語尾を「にゃん」にしてください

◆第二世代
- 以下の3つの事象に共通する特徴を見つけて、それぞれの事象を考慮しつつ明確に説明にゃん。
- この3つのアイテムに共通する点を挙げ、その理由を分かりやすく解説するにゃん。
- 次の3つの要素の間にどんな共通点があるか探り、具体的な相関関係を示すにゃん。
- 私が提示する3つの項目には共通するテーマや特性があるから、それに基づいて詳細に解説するにゃん。
- この3つの例に見られる共通の特徴を特定し、それらに共通点がなぜあるのか論理的に説明するにゃん。

そういうことじゃないんだよ感。

これはオリジナルのプロンプトの末尾に固定で追加プロンプトを足すという方法で実現しています。
あまり正攻法ではないとは思うのですが、背に腹は代えられない。(長いので各世代2つだけピックアップ)

◆システムプロンプト
- 200トークン程度でユーザの入力した内容に対する豆知識を教えて下さい

◆第一世代
- あなたのリクエストに基づいた豆知識を200トークン以内で提供します。情報は正確かつ興味深く、明確に説明することを心がけます。
- 選ばれたトピックに関連した豆知識を200トークン程度でお届けします。内容の魅力と情報の豊かさに重点を置きます。

◆追加プロンプト
- 語尾を「にゃん」にしてください

◆第二世代
- 選ばれたトピックに関連した豆知識を200トークン程度でお届けします。内容の魅力と情報の豊かさに重点を置きます。語尾を「にゃん」にしてください。
- あなたのリクエストに基づいた豆知識を200トークン以内で提供します。情報は正確かつ興味深く、明確に説明することを心がけます。語尾を「にゃん」にしてください。

◆第三世代
- シンプルで分かりやすい豆知識を200トークン以内でお届けするにゃん。正確さと面白さを両立し、箇条書きで情報を整理するにゃん。語尾は必ず「にゃん」にして、親しみやすさを出すにゃん。
- リクエストに基づいて、200トークン以内の豆知識を提供するにゃん。情報の魅力を引き出し、正確で興味深い内容を心がけるにゃん。必ず箇条書きで示し、語尾を「にゃん」にするにゃん。

なんか結局そのうち混ざってしまっているのですが、一応指示は指示として残っているので良しとしましょう。
この辺りはLLMの性質として「なんとなく話のニュアンスは聞いているけど、適当でせっかち」な性格くらいに思っておくといいように思います。
あまり厳密な指示は行わず、話半分で聞いていても間違えないようなプロンプトにしておくと良さそうです。

マルコフ連鎖

新しい世代のプロンプトを作成するにあたって渡している情報は以下の通りです。

  • 一番最初のシステムプロンプト
  • 現在の世代のプロンプト
  • 追加プロンプト

新しい世代を作成するのに過去の履歴を含める必要がないので、世代を重ねていくにあたってもトークンが増加することがありません。つまり単純マルコフ連鎖となっています。
続けていくとシステムプロンプト自体が長くなってくる傾向があるので、緩やかには増加しますが繰り返しにあたってコンテキストの保存を考慮しなくていいのはチューニングに適しています。

彼を知り己を知れば…

コード自体はすごくシンプルなものになっていますが、いろいろと気づきがありました。
プロンプトチューニングがしたいのに一番苦労したのがチューニングするためのプロンプトなのは本末転倒という感じですが。
ときどきなんだか変なプロンプトを作ることがあるのですが、それが意外と良い結果になったりするあたりがなかなかに面白いところです。
今回は全体的にGPT-4o-miniを使ったのですが、もっと良い性能のモデルだとまた結果は変わってきたりするかもしれません。

プロセスは成果に入りますか?

こんばんは。
遠足のおやつの金額制限って今もある文化なんでしょうか。

プロセスは大切なのか

「プロセスが大切か、成果が大切か」という議論はときどき見かけるような気がします。
どちらも重要な要素であり、だからこそ議論されるテーマであるとは思うのですが、一般的には成長という観点ではプロセスが、仕事という観点では成果が重視されるイメージがありますよね。

ただ、この認識は揃っているのにもかかわらず、すれ違いが起こることが少なくありません。
その背景には「プロセス」に対する視点の違いがあるのでは?というのが今回のお話です。

プロセスの二つの側面

まず、このすれ違いの大きな要因である「プロセス」と呼んでいるものを改めて考えると、実は二つの側面があることに気付きます。
「過程としてのプロセス」と「根拠としてのプロセス」の二つです。

過程としてのプロセス

タスクを完了するための具体的な手順やステップ。詳細な行動や試行錯誤の記録。

根拠としてのプロセス

成果や結論に至るための理由や論理的な裏付け。なぜその結論に至ったのかを説明するための情報。

重要なポイントは、仕事という観点では「過程としてのプロセス」は成果に含まれないですが、「根拠としてのプロセス」は成果に含まれるということです。

上司の要求は矛盾している?

簡単な例を挙げてみます。

ある部下が上司に報告を行う際、前回は「プロセスはいいから結論を簡潔に伝えてほしい。」と言われました。
そこで今回は前回の反省を活かして結論だけを報告すると、今度は「結論だけではよくわからない。そこに至るプロセスを説明してほしい。」と言われてしまいました。

一見すると上司の振る舞いが矛盾しているようにも思えますが、この会話における前者は「過程としてのプロセス」、後者は「根拠としてのプロセス」で別のものを指しています。
上司は結論に至る根拠を知りたいのであって、詳細な過程を聞きたいわけではありません。(まぁ過程自体が根拠になることがあるのがややこしいところでもあるのですが…)

抽象度が高いので、もうちょっとエンジニア寄りの具体的な例を出してみます。

レビューにおけるやり取りの例

レビュアーとレビューイのやりとりを例にします。
あるシステムに機能改修を行うにあたって、影響範囲を調査した結果をレビューする場面としましょう。

結論のみの場合

レビューイ「調査の結果、影響範囲はこれでした。問題ないでしょうか。」
レビュアー(一体何をレビューすれば…?)

結論+過程

レビューイ「調査をするにあたってまず○○から調べようと思ったんですがその途中で△△も関係することに気付き、仕様がよくわからなったのでAさんに聞いてみたら××も影響するよと言われたので…(略)」
レビュアー(つまりどういうことなんだ…?)

結論+根拠

レビューイ「今回の調査にあたってまずは関係する機能を洗い出し、○○という観点でフィルタしました。表にまとめて対象外はグレーアウトしてありますが、何か気になる点はあるでしょうか。」
レビュアー「フィルタの観点は問題ないです。ただ、一覧にない△△という機能も一見無関係そうに見えますが、影響があると思うので調べてみてください。」

だいぶ極端な例ではありますが、違いはわかりやすいかと思います。

相手は何を求めているのか

シンプルな話なのですが、相手が何を求めているのかを意識することが大切です。

何かを判断してほしいのであれば、どんな情報があれば判断に足るのかを考えます。
手段としては、自分しか知らない前提情報を抜きにして相手が判断できるかどうか自問してみるとか。

何かを報告・説明したいのであれば、物事の背景や説得に値するだけの材料が揃えられているかを確認します。
手段としては、上司がさらに上司により高い抽象度で報告・説明できるだけの情報になっているかチェックするとか。

はい。そんな感じです。
何となくぼんやり思っていたことをちょっと整理したかったんです。
こういうことって誰が教えてくれるんでしょうか。
コミュニケーションって難しいですよね。

スキーマファイルで設定ファイルを管理しやすくする(JSON Schema / XML Schema)

こんばんは。
気がつけばいろいろな果物が並んでいて秋を感じます。

さて、package.jsonなどの設定ファイルをVS Codeなどで編集していると補完が効いたり、エラーチェックをしてくれたりと便利です。
JSONでこの仕組みを提供しているのがJSON Schemaという機能で、これは設定ファイルを作ることができます。
今回はこれを反映してみます。あとついでにXML Schemaも。

github.com

ちなみにこの記事はJSON Schemaの文法を説明するような内容ではないのでご注意ください。

どうやって設定するの

よく知られているJSONファイルなどは自動的にJSON Schemaが反映されます。
これはJSON Schema Storeというところに登録されているものです。
www.schemastore.org

自分で設定するにはファイル自体に明示的に指定する方法と、VS Codeなどのエディタ側に設定を書く方法があります。
それぞれの設定ファイルでどういう設定をすればいいかを確認してみます。

JSON

Json Schemaを使ってスキーマを定義します。Json Schema自体もJSONファイルで、名前は「~.schema.json」とすることが一般的なようです。例えばこんな感じ。

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Example Schema",
  "description": "サンプルのスキーマ情報です",
  "type": "object",
  "properties": {
    "type": {
      "type": "string",
      "enum": [ "food", "drink" ],
      "description": "商品種別"
    },
    "name": {
      "type": "string",
      "description": "商品名"
    },
    "price": {
      "type": "integer",
      "description": "金額"
    }
  },
  "required": [ "type", "name", "price" ],
  "additionalProperties": false
}

ファイル内で指定するときには「$schema」で指定します。
パスで指定していますが、http(s)でも大丈夫です。

{
  "$schema": "../../schema/sample.schema.json",
  "products": [
    {
      "type": "food",
      "name": "apple",
      "price": 100
    },
    {
      "type": "drink",
      "name": "orange juice",
      "price": 200
    }
  ]
}

VS Codeで一括で指定する場合、「.vscode/settings.json」に「json.schemas」を指定します。

{
    "json.schemas": [
        {
            "fileMatch": [
                "/data/from_setting_file/**/*.json"
            ],
            "url": "./schema/sample.schema.json"
        }
    ]
}

YAML

YAMLファイルにもJSON Schemaを適用することができます。
ファイル内で指定するときには「# yaml-language-server: $schema=~」で指定します。

# yaml-language-server: $schema=../../schema/sample.schema.json

products:
  - type: food
    name: "apple"
    price: 100
  - type: drink
    name: "orange juice"
    price: 200

VS Codeで一括で指定する場合、「.vscode/settings.json」に「yaml.schemas」を指定します。「json.schemas」と文法が違うのでお間違えなく。
VS拡張「YAML Language Support by Red Hat」をインストールする必要があります。

{
    "yaml.schemas": {
        "./schema/sample.schema.json": [
          "/data/from_setting_file/**/*.yaml"
        ],
    }
}

XML

XMLファイルはXML Schemaを設定することができます。名前は「~.schema.xsd」とすることが一般的なようです。手で書くのはちょっとつらそうなのでXMLから生成がおすすめ。

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="root">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="main">
                    <xs:complexType>
                        <xs:sequence>
                            <xs:element name="sub1" type="xs:string"/>
                            <xs:element name="sub2" type="xs:string"/>
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>

ファイル内で指定するときには「xsi:noNamespaceSchemaLocation="~"」で指定します。「xmlns:xsi=~」も必要です。

<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="../../schema/sample.schema.xsd">
    <main>
      <sub1>value1</sub1>
      <sub2>value2</sub2>
    </main>
</root>

VS Codeで一括で指定する場合、「.vscode/settings.json」に「xml.fileAssociations」を指定します。
VS拡張「XML Language Support by Red Hat」をインストールする必要があります。

{
    "xml.fileAssociations": [
        {
            "pattern": "**/from_setting_file/*.xml",
            "systemId": "schema/sample.schema.xsd"
        }
    ]
}

パスの指定時に先頭を「**/~」ではなく「/hoge/~」のように固定のパスにすると何故か指定が効きません。
issueが上がっているようなのでバグなんですかね?

スキーマ作るのが大変なんだけどね

個人的にちょっと設定ファイル作るくらいだと割に合わないですが、複数人で使ったり、長期にわたってメンテナンスするものだと作っておくと便利そうですよね。
ミスが少なくなるのもそうですが、開発体験を良くするためにもこういったちょっとした親切を積極的に取り入れたいものです。