ChatGPTの再現性をコントロールしたい!
はじめに
こんにちは!YNです。
今回はChatGPTを活用する上で悩んだ点について共有したい。主な使い方としては、顧客向けのQ&Aの自動化、データ分析の高速化、そして日々のレポート作成を効率化するためにChatGPTを活用し成果を上げているのではないだろうか。弊社では、それとは別に、LLMが業務用ツールとしてルーチン業務の効率化にどのように影響を与えるか、という視点で研究を行っている。
が、
このツールの使用には再現性の確保という大きな課題がある。
例えばChatGPTに「横断歩道を渡るときに注意することは?」と複数回聞いてみよう。生成される出力は大まかには同じでも、一言一句同じ、という訳ではなく、項目すら変わっていることも確認できるはずだ。 今回は、この再現性の問題について考えてみたい。
なぜ再現性をコントロールしたいのか
ChatGPTの魅力の一つは創造性豊かな出力にあるが、ビジネスシーンではこの特性が時に頭痛の種になり得る。例えば、クライアントに同じ質問をした場合、毎回異なる回答が返ってくると信頼性が損なわれるかもしれない。また、チーム内でのコミュニケーションツールとして使う場合も、一貫性のある情報提供が求められる。
想像してみよう。
あなたが馴染みのラーメン屋さんに久しぶりに行ったときに、 チャーシューが薄くなってる気がする、とか、 麺が変わった気がするな、とか 違和感を感じた瞬間、これからも通い続けるかなー…とちょっと迷ってしまうだろう。。
安定した品質と味わいが期待されるのと同じように、業務用ツールとして利用する上では、創造性より一貫性、再現性が重視される。
具体例で理解する
例えば、ChatGPTに一週間の食事メニューを分析させ、栄養バランスに関するアドバイスを求めたとする。ChatGPTがこのメニューに対してどのようなフィードバックするかは、翌週の食事メニュー作成の効率性に大きな影響を与えることだろう。
WEB版のChatGPTに3回同じプロンプトを投入したところ、以下のようなテキストが出力された。
(1回目)
(2回目)
(3回目)
・・・「改善のためのメニュー提案」があったりなかったりで、表現の一貫性が無い。それはともかくとして、足りない栄養素に関する指摘が異なっているのが気になる。ユーザーにとっては情報提供の一貫性を欠くことから、業務の効率性や精度に影響を与えるリスクが大きい。
ChatGPTの大規模アップデートの詳細
2023年11月7日、OpenAIは「DevDay」で大規模なアップデートが発表された。このアップデートでは、gpt4-turboのプレビュー版のリリースや新機能の追加が話題となった。この記事では、詳細よりも特に再現性に関わる部分に焦点を当て、具体的な運用方法に関して考えていきたい。アップデートの詳細はOpenAIの公式のアナウンスや、詳しく解説されている方のブログ記事や動画を参考にして欲しい。
2023年11月7日アップデート以前の環境
ChatGPTはランダム性が前提であり、WEB版ではランダム性を調整する手段はない。従量課金が必要であるが、API版ではtemperatureやtop_pというパラメータによりランダム性を調整することが可能だ。ただ、回答を再現させるというよりは、回答の品質を向上させるために使われるパラメータのようだ。
temperature
要は虫眼鏡で見るかどうかである。モデルが次に選ぶ単語の確率分布において、temperatureが低い値だと差が大きくなるように調整される。その結果、予測可能で一貫性のあるテキストを生成しやすくなる。逆にtemperatureが高い値だと差が平坦になるように調整される。その結果、バラエティ豊かなテキストが生成される、ということだ。
top_p
要は足切りである。上位の確率の単語しか選択されないので、top_pの値が小さいときほど予測可能で一貫性のあるテキストを生成しやすくなる、ということだ。
APIでtemperatureとtop_pを調整したリクエストを投げてみる
temperatureとtop_pでそれぞれ試行した(↓画像はtop_pの検証)。一貫性のある表現にはなったものの、足りない栄養素「オメガ3脂肪酸」はあったりなかったりする。
(1回目) (2回目) (3回目)
2023年11月7日アップデート以降の環境
話を「DevDay」のアップデートに戻す。再現性に大きく影響のある「seed」というパラメータが実装された。
※まだこの記事を執筆した11月中旬の段階では、β版での実装になるので、この先変わる可能性もある点をご留意ください。
seed
本稿の本題。↑↑をGoogle翻訳すると、
「この機能はベータ版です。指定した場合、システムは、同じseedパラメータを使用した繰り返しリクエストが同じ結果を返すように、決定論的にサンプリングするよう最善の努力をします。決定性は保証されていないため、system_fingerprintバックエンドの変更を監視するには応答パラメーターを参照する必要があります。」
つまり、seed値固定により、本来はランダムであるseedパラメータが固定されることで、入力が同じだと得られる出力は同じになる、ということだ。
fingerprint
seedの項目で言及されているsystem_fingerprintとは、apiからのレスポンス内に含まれているプロパティでの1つで、要はモデルの識別子になる。
つまり、OpenAIの提供するモデルは定期的なアップデートが行われており、そのモデルごとにLLMの重みは微妙に異なる。なので、入力プロンプト、seed値が同じだとしても、fingerprintが変わると、前後では出力の一貫性が崩れる可能性がある、ということだ。
APIでseedを固定したリクエストを投げる検証
前出の検証用のプロンプトをAPIリクエストを送るために以下のコードで検証する。環境はGoogleAppsScript。seed固定により、一貫した出力を得られるかどうかを検証。
const API_URL = "https://api.openai.com/v1/chat/completions"
const MODEL_NAME = "gpt-4-1106-preview" // モデルの設定
const MAX_TOKENS = null
const TEMPERATURE = null // 生成する文章のランダム性(0:確定的、2:ランダム)
const TOP_P = null //上位p%のトークンを取得、デフォルト⇒1(1:全てのトークン)
const API_KEY = PropertiesService.getScriptProperties().getProperty("API_KEY") // APIキーの読み込み
const NN = 1
const TYPE = "text" //or "json_object"
const ORGANIZATION_ID = PropertiesService.getScriptProperties().getProperty("ORGANIZATION_ID")
const LAST_FINGERPRINT = PropertiesService.getScriptProperties().getProperty("GPT_FINGERPRINT")
const SEED = null //固定したい場合はint値を設定
function adviseWeeklyMenu(){
let systemContent = 'あなたは食生活のアドバイザーです。'
let content = `後述「先週の献立と大まかな調理手順」から足りない栄養素を指摘してください。また、それを改善するメニューをいくつか提案してください。
#先週の献立と大まかな調理手順
{"月曜日":{"朝食":{"レシピ名":"オートミールとフルーツ","材料":{"オートミール":"50g","バナナ":"1本","アーモンドミルク":"200ml","はちみつ":"小さじ1"}},"昼食":{"レシピ名":"チキンサラダ","材料":{"ローストチキン":"100g","ミックスサラダ":"100g","トマト":"1個","キュウリ":"1/2本","オリーブオイル":"大さじ1","レモン汁":"大さじ1"}},"夕食":{"レシピ名":"豆腐と野菜の味噌汁","材料":{"豆腐":"150g","わかめ":"10g","ネギ":"1本","味噌":"大さじ2","だし":"500ml"}}},"火曜日":{"朝食":{"レシピ名":"野菜スムージー","材料":{"ほうれん草":"1束","バナナ":"1本","リンゴ":"1/2個","アーモンドミルク":"200ml"}},"昼食":{"レシピ名":"サバの塩焼き定食","材料":{"サバ":"1切れ","塩":"少々","ごはん":"1膳","みそ汁":"1杯"}},"夕食":{"レシピ名":"チキンカレー","材料":{"鶏もも肉":"200g","玉ねぎ":"1個","人参":"1本","じゃがいも":"2個","カレールウ":"100g"}}},"水曜日":{"朝食":{"レシピ名":"トーストとスクランブルエッグ","材料":{"食パン":"2枚","卵":"2個","バター":"10g","塩":"少々"}},"昼食":{"レシピ名":"海鮮丼","材料":{"ごはん":"1膳","マグロ":"50g","サーモン":"50g","イクラ":"20g","醤油":"少々"}},"夕食":{"レシピ名":"鍋焼きうどん","材料":{"うどん":"1玉","白菜":"100g","しいたけ":"2個","鶏肉":"50g","だし":"500ml"}}},"木曜日":{"朝食":{"レシピ名":"ヨーグルトとグラノーラ","材料":{"無糖ヨーグルト":"200g","グラノーラ":"40g","フレッシュベリー":"50g"}},"昼食":{"レシピ名":"ベジタブルサンドイッチ","材料":{"全粒粉パン":"2枚","レタス":"2枚","トマト":"1枚","アボカド":"1/2個","マヨネーズ":"小さじ1"}},"夕食":{"レシピ名":"豚の生姜焼き","材料":{"豚ロース肉":"150g","生姜":"1片","醤油":"大さじ1","みりん":"大さじ1","ごはん":"1膳"}}},"金曜日":{"朝食":{"レシピ名":"バナナとナッツのパンケーキ","材料":{"パンケーキミックス":"100g","卵":"1個","牛乳":"100ml","バナナ":"1本","クルミ":"少々"}},"昼食":{"レシピ名":"和風パスタ","材料":{"スパゲッティ":"100g","しめじ":"50g","ベーコン":"2枚","しょうゆ":"大さじ1","大葉":"数枚"}},"夕食":{"レシピ名":"サーモンのムニエル","材料":{"サーモン":"2切れ","小麦粉":"適量","バター":"20g","レモン":"1/4個","サラダ":"適量"}}},"土曜日":{"朝食":{"レシピ名":"オムレツとトースト","材料":{"卵":"2個","ハム":"2枚","チーズ":"30g","食パン":"2枚"}},"昼食":{"レシピ名":"チキンライス","材料":{"鶏肉":"100g","玉ねぎ":"1/2個","ごはん":"1膳","ケチャップ":"大さじ2"}},"夕食":{"レシピ名":"すき焼き","材料":{"牛肉":"200g","白菜":"200g","豆腐":"1/2丁","春菊":"1束","すき焼きのたれ":"200ml"}}},"日曜日":{"朝食":{"レシピ名":"フレンチトースト","材料":{"食パン":"2枚","卵":"1個","牛乳":"100ml","シナモン":"少々","メープルシロップ":"適量"}},"昼食":{"レシピ名":"ラーメン","材料":{"ラーメン麺":"1袋","豚バラ肉":"2枚","ネギ":"1本","ラーメンスープ":"1袋"}},"夕食":{"レシピ名":"寿司","材料":{"寿司飯":"1膳分","マグロ":"50g","サーモン":"50g","いくら":"20g","海苔":"適量"}}}}`
let response = gpt(systemContent,content,undefined,undefined,undefined,0.1,5,undefined,1)
for(let choise of response.choises){
console.log(choise.message.content)
}
}
function gpt(systemContent,userContent,model=MODEL_NAME,temperature=TEMPERATURE, maxTokens=MAX_TOKENS,top_p=TOP_P,n=NN, type=TYPE, seed=SEED) {
console.log('gpt','model:',model,'temperature:',temperature,'maxTokens:',maxTokens,'top_p:',top_p,'n:',n,'type:',type,'seed:',seed)
console.log('systemContent:',systemContent)
console.log('userContent:',userContent)
const requestBody = {
"model": model,
"messages": [
{ role: 'system', content: systemContent },
{ role: 'user', content: userContent}
],
"seed": seed,
"temperature": temperature,
"top_p": top_p,
"max_tokens": maxTokens,
"n": n,
"response_format": { type: type },
}
const requestOptions = {
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + API_KEY,
"OpenAI-Organization": ORGANIZATION_ID
},
"muteHttpExceptions" : true,
"payload": JSON.stringify(requestBody)
};
let response = JSON.parse(UrlFetchApp.fetch(API_URL, requestOptions).getContentText());
if(response["system_fingerprint"]!==LAST_FINGERPRINT){
console.error('system_fingerprint changed: ' + LAST_FINGERPRINT + '⇒' + response["system_fingerprint"])
PropertiesService.getScriptProperties().setProperty("GPT_FINGERPRINT",response["system_fingerprint"])
}
return {'choises':response.choices,'usage':response.usage};
}
※スクリプトプロパティでAPI_KEY、ORGANIZATION_ID、GPT_FINGERPRINTの設定が必要。
検証結果
上のコードではtop_p=0.1, n=5としている。このパラメータは1回のリクエストに対する生成するテキストの数である。なので、seed値を固定することで、想定される結果として5回とも同じテキストが生成されるはずだ。
(1/5)
(2/5)
(3/5)
この時点で、表現の揺れは残っているし、3/5回目だけ、不足している可能性のある栄養素で「オメガ3脂肪酸」が含まれている。system_fingerprintで変更は確認されなかった。seed値による出力の固定はできなかった。
※追加検証でtop_p=0でも試行した。完全一致する確率も増えるが、やはり同様に「オメガ3脂肪酸」が指摘されない回答もそれなりに発生する。
結論
現時点では、再現性のコントロールをする方法はない。今回は再現性がトピックなので、簡易的に目視による文字列の相違で評価のみに留める。詳細な評価をする場合はBLEU等の自動評価指標を検討する。まだβ版なので、推移を見守りたい。
本当に再現性が必要かの再考
そもそもの話ではあるが・・・、再現性を追求することに、価値があるのかを考えてみる。 再現性、ランダム性をコントロールできたとして、確かにそれにより再現性のある出力を得られるかもしれない。ただそれにより、過学習のリスクを招く。特に多岐にわたる入力に対して柔軟に対応する必要がある業務では、再現性よりも適応性が重要になる。もし特定の案件ではうまく機能したものが別の案件では機能しないという状況になったら、それはそれで問題だ。「同じ入力に対して常に同じ出力を得る」ことではなく「あらゆる状況で有用な出力を提供する」ためには、ある程度のランダム性は許容したうえで運用を工夫したほうが良い結果が得られるような気がする。ただ、ある程度の再現性の担保はどのみち必要になるため、なんらかの対策は必要だ。
運用上の工夫
ランダムな出力があることを前提にできる工夫を考えてみる。今回の検証では、足りない栄養素を指摘するタスクは、指摘が少ないより多い方がが正義となる。 なので同じプロンプトで出力を複数回実行⇒回答をマージすることで、ある程度の再現性を担保できるかもしれない。前出の検証で、1~3回目の出力をマージする形だ。 また、プロンプトも全て同じより、違うプロンプトをアンサンブルさせるほうが良い結果になるかもしれない。出力の傾向を理解し、適切な指示をChatGPTに与えることで、期待する結果に近づけることもできるだろう。 しかし、これらの対応をしたとしても完全に再現性を担保をすることは難しいとは思うので、ユーザーには出力を選択することを前提にコミュニケーションしたほうが良いかもしれない。
最後に
今回の検証でseed値に関しては腑に落ちないところが大きい。私の理解が間違っているか、ChatGPTのseedの概念が少し違う可能性もあるので、本稿読者には注意して欲しい。いずれにせよ、業務ツールとして使用する場合には再現性は運用上の大きな要素だと考えている。ChatGPTの強みは読解力と創造力なので、良いところを活かしつつ苦手な所はカバーできるような業務設計を行う必要はあるだろう。汎用GPTでの対応が難しいのであれば、専用GPTで解決ができるのか検証してみたい。