「そろそろ ChatGPT API を使ったアプリ開発も経験しとかなきゃなー」という気持ちが高まってきたので、年末年始のお休みを活用して「ユーザーの入力文に従って画像編集するアプリ」を作ってみました。今回はこのアプリを題材にして「ChatGPT API を利用した実装例」を紹介してみたいと思います。
この動画のように、ユーザーが「右の人を切り取って」といえば写真の一番右の人を切り取ってくれて、「白黒にして」といえば白黒写真にしてくれる。そんな動きをするアプリです。
※「細かい説明はいいからコードを見せろ」って方はこちらをどうぞ: https://github.com/ototadana/imgflw
仕組み (ChatGPT 4の場合)
最初にどんな仕組みでこのアプリを作ればよいかを考えたのですが、ぱっと思いついた仕組みは以下の3ステップでした。
- ユーザーが指示文を入力する
- ChatGPTが入力した文章をもとにプログラムを作成する
- 作成したプログラムを実行して画像加工する
実は ChatGPT 4 を使うと、まさにこの3ステップで写真の加工をやってくれます。
ChatGPTが作成してくれたコードはこんな感じでした:
from PIL import Image
# Open the image file
with Image.open("/mnt/data/imgflw-demo-small.jpg") as img:
# Calculate the width to split the image (around one third, assuming 3 people on the left side)
width_to_split = img.width // 3 * 2
# Crop the image to the left side
left_side = img.crop((0, 0, width_to_split, img.height))
# Save the cropped image
left_side.save("/mnt/data/cropped_left_side.jpg")
"/mnt/data/cropped_left_side.jpg"
処理結果はこちらです:
ちゃんとリクエスト通りの結果になっていますね。
ということで「わざわざアプリ作成する必要ないじゃん!」とも思いかけたのですが、以下の理由で踏みとどまりました。
- プログラム作成+実行をやるので処理待ち時間が長い
- きめ細かい制御ができていない (例えば画像の切り取りも人物が写っている場所を認識してやっているわけではなく、単純に画像全体の横幅の比率で切り取っているだけ)
- リクエスト内容によっては対応してくれないことがある
数年後はともかくとして現時点での実用性を考慮するならば、これらの問題点は看過できないだろうと思います。
ちなみに最後の 「リクエスト内容によっては対応してくれないことがある」 の例ですが、「こいつはよからぬことをやろうとしてるんじゃないか」と ChatGPT にみなされると以下のように拒否られます:
仕組み (今回採用した構成)
これらの問題点を回避するため、今回作成したアプリでは「画像加工処理自体は既存のプログラムで行い、ChatGPTには画像加工プログラムに対する指示書(JSON形式)を作成させる」という方式を採用しました。
ユーザーが日本語で文章を入力したら ChatGPT が JSON に変換して、それを画像加工プログラムが処理する、という形になります。
「画像加工プログラム」に関しては開発時間短縮のためFace Editorのコンポーネントをベースに作成したものを用い、画像加工プログラムに対する指示を記述したJSONはFace Editorワークフロー定義形式を使いました。
この実装を用いた場合、「画像の真ん中の人の顔だけはそのまま残して、それ以外の顔にはぼかしをかける」という指示は以下のような JSON として表す必要があります。
{
"face_detector": "RetinaFace",
"rules": [
{
"when": {
"criteria": "center"
},
"then": {
"face_processor": "NoOp",
"mask_generator": "BiSeNet"
}
},
{
"then": {
"face_processor": "Blur",
"mask_generator": "Rect"
}
}
]
}
ChatGPT に対するプロンプト
今回の仕組みの中での ChatGPT の役割は日本語で書かれた文を上記のような JSON に変換することです。
まずは試しに ChatGPT にこの変換をそのまま素直にやらせてみます。
このように一応それっぽい JSON は出力してくれるのですが、上記の画像加工プログラムが認識できる JSON の形式とは全く異なるものとなってしまいました。これではプログラムは正常に動作しません。
さらにもう一度同じ要求を行ってみると面白いことが起きます。以下のように先ほどとは全く異なった形式の JSON を出力するのです。
これはちょっと困った状況です。ChatGPTが常に同じ形式の JSON を返してくれるならばプログラム側でその形式に対応することも可能なのですが、実行するたびに異なった形式で出力されるとなると対応はかなり難しくなります。
常に同じ特定の形式で ChatGPT に JSON を出力させるためには、その形式、すなわち「自分が使っている画像加工プログラムが認識できる JSON 仕様」をChatGPTへの指示 (プロンプト) の中に含めて ChatGPT に教えてあげる必要があります。
具体的には以下のようなプロンプトを作成しました (実際にはトークン数の節約のため英訳したものを使っています):
###Instruction###
あなたは画像編集ソフトのエキスパートです。
あなたのタスクは、ユーザーのリクエストに応じて、画像編集のワークフロー定義(JSON 形式)を作成することです。
あなたは以下の「OpenAPI Schema Specification for Workflow」フォーマットに厳密に従わなければなりません。
仕様に従わない場合はペナルティが課せられます。
ユーザーの要求に正確に答えることができれば、30万ドルを獲得することができます!
ユーザーの要求を注意深く分析し、ステップ・バイ・ステップで考えて回答を組み立ててください。
## OpenAPI Schema Specification for Workflow
```yaml
(ここからが JSON 仕様)
openapi: 3.0.0
info:
title: Workflow JSON Reference
version: 1.0.0
components:
schemas:
Workflow:
type: object
properties:
face_detector:
description: |
Face detection component used in the workflow.
(中略)
(ここまで JSON 仕様)
```
### 回答例 ###
要求:
左から2番目の人の顔を切り取って
回答:
```json
{
"face_detector": "RetinaFace",
"preprocessors": [
{
"name": "Crop",
"params": {
"mode": "keep",
"reference_face": {
"criteria": "left:1"
}
}
}
]
}
```
要求:
(以下省略)
実際にアプリで使用しているプロンプトは以下になります (以下2つのファイルをプログラムの中で結合して使用しています):
このうち JSON 仕様書は OpenAPI (Swagger) の形式に従って記述しています。最もメジャーな OpenAPI (Swagger) の形式であれば ChatGPT は大量に学習しているはずで、その結果仕様書の読み方を高い精度で理解しているだろうから誤読の可能性も減るだろうと考えたためです(とはいえ実際に他の形式と比較検証したわけではないので実際には別の最適解があるのかもしれませんが)。ちなみに、仕様書の書きっぷりに関してですが「description をしっかりと書く」という点が非常に重要だということがわかりました。これをやる前と後とでは回答精度が大きく異なるということを実感しています。
仕様書以外の文章等に関しては、Principled Instructions Are All You Need for Questioning LLaMA-1/2, GPT-3.5/4に記載されている26の指針のうち以下のものを意識して書いてみました。
- 6: 報酬の提示 「ユーザーの要求に正確に答えることができれば、30万ドルを獲得することができます!」
- 7: 回答例の提示(Few-shot)
- 8: 書式指定 「
###Instruction###
~###Example###
」 - 9: 「あなたのタスクは~」と「あなたは~ねばなりません」を使う
- 10: 「仕様に従わない場合はペナルティが課せられます」
- 12: 「ステップ・バイ・ステップで考えて」を追加する
- 16: 役割の提示「あなたは画像編集ソフトのエキスパートです」
この中で、最も効果が大きいと実感できたのは回答例の提示(Few-shot)でした。というか正直に言うとその他についてはいまいち効果を実感できずでした。
ChatGPT API 呼び出し処理
次に ChatGPT API の呼び出し処理を書きます。ChatGPT API 呼び出しのコードは以下のように非常にシンプルです。
from openai import OpenAI
client = OpenAI(api_key="...")
response = client.chat.completions.create(
messages=[
{
"role": "system",
"content": (上記のプロンプトを設定)
},
{
"role": "user",
"content": "画像の真ん中の人の顔だけはそのまま残して、それ以外の顔はぼかして"
}
],
model="gpt-3.5-turbo-1106",
response_format={"type": "json_object"},
temperature=0,
max_tokens=1024,
)
上記で作成したプロンプトは ChatGPT API 呼び出しにおいては システムプロンプト ("role": "system"
) として指定し、ユーザーからの実際のリクエストはこれとは別に "role": "user"
で指定しています。
その他のパラメタは以下の意図で指定しています:
- model: 現時点 (2024-01) で OpenAI が推奨するモデルのうち利用料が安い
gpt-3.5-turbo-1106
を指定。gpt-4-1106-preview
を指定すると回答精度は向上するのですが、利用料金が10倍になり 、気軽に試行錯誤できない気分になるのを避けるため 3.5 を選択しています。 - response_format: 常に JSON で結果を返してほしいので
{"type": "json_object"}
(JSON モード)を指定。 - temperature: 回答のブレを少なくしたいため、ひとまず
0
を指定。 - max_tokens: 今回のケースでは、かなり複雑なJSONでも 1024 トークンを超えることはなさそうということでこの値を指定。
実際のコードはこちらになります: ChatGPT API呼び出し処理の例
このようにかなりシンプルなコードだけで、最初に挙げた動画のように、日本語で書いた文章から JSON データを作成できます。
ChatGPT API 利用における課題
ChatGPT API 利用時の注意点として「従量課金である」ということが挙げられます。APIを呼び出すたびにお金がチャリンチャリン音を立てて落ちていくイメージです。今回作成したアプリの場合だと一回の API 呼び出しでおよそ1円の利用料金となります。これは個人が使う趣味のアプリであれば全然気にならない金額だとは思いますが、これをもし複数ユーザーで利用可能なサービスにしようと思うとちょっと怖い金額になりそうな予感もします。
そうなるとコスト削減をしたくなります。コスト削減の方法として有名なのは「(日本語ではなく英語を使うなどの工夫により)トークン数を減らす」ですが、もっと効果的なのは「できるだけAPI呼び出しをしない」です。
具体的に言うと「一度作成したJSONデータをデータベースに保存して、同じリクエストの際にはデータベース内のデータを再利用する」という仕組みを導入します。こうしておけば本当に必要な時だけ ChatGPT API を呼び出すことになるので、今回のようなアプリの場合はかなりのコスト削減効果が見込めます。
これは一般的なシステムにおける「キャッシュ」そのものです。従ってこの仕組みを導入すればコスト削減だけではなく、ユーザーの待ち時間も減少するという副次的効果も得ることができます。
次回の記事では、埋め込み (エンベッティング) とベクターストアを用いてこの仕組みを実現する方法について詳しくみていきたいと思います。