はじめに
この記事は、個人開発の際に得た知識を、いつでも見返せるように保存する目的で使用しているため、言葉足らずかつ不適切である箇所が多々あるかと思います。気付いた箇所がありましたら、ご一報いただけると幸いです。
最近GPT-4に触れる機会に恵まれ、いろいろ触ってみたりしていたのですが、GPT-4 with Visionがリリースされ画像や動画を入力とするアプリケーションの作成が可能になりました。本記事では、GPT-4 with Visionに画像とそれに関連する質問を投げ、結果を取得する一連の機能をPythonを用いて実装していきます。
試した環境
作者は以下の環境を使用しています。そのほかの環境では試験してないので、動作の保証はありません。
OS | MacOS 13.0 |
---|---|
Python | 3.9.6 |
OpenAI公式のドキュメントによるとPython>=3.7.1
が必要とのことです。
APIキー、OpenAI Python Libraryの入手
はじめにOpenAI accoutを作成する必要があります。その後サインインを行い、APIキーを発行してください。発行したAPIキーは.zshrc
などに環境変数として設定し、秘匿してください。
export OPENAI_API_KEY='hogehogepiyopiyo'
次にOpenAI Python Libraryをインストールします。コマンド1つで入手できます。
pip install --upgrade openai
GPT-4 With Visionの利用は現在有料です。料金は消費トークン量で決定されます。利用の上限設定などの設定は、サインイン後のサイドバー内のUsage
やSetting
から可能です。
実装
はじめに今回作成した物を載せます。その後、それぞれのコンポーネントの説明を行います。
import os
import cv2
import base64
from openai import OpenAI
import numpy as np
from typing import Union
# Logging
import logging
logger = logging.getLogger(os.path.basename(__file__))
logger.setLevel(logging.INFO)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
handler_format = logging.Formatter('%(asctime)s : [%(name)s - %(lineno)d] %(levelname)-8s - %(message)s')
stream_handler.setFormatter(handler_format)
logger.addHandler(stream_handler)
class RunGPT4OnImage():
def __init__(self, api_key: Union[str, None]=None, model: str='gpt-4-vision-preview', max_tokens_per_call: int=200) -> None:
# GPT-4 with vision
self.OPENAI_API_KEY=api_key if not api_key is None else os.environ['OPENAI_API_KEY']
self.client = OpenAI(api_key=self.OPENAI_API_KEY)
self.payload = {
'model': model,
'messages': [], # ここに質問や画像などを並べてモデルに投げる。
'max_tokens': max_tokens_per_call,
}
def encode_image_path(self, input_image_path: str) -> str:
with open(input_image_path, 'rb') as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def encode_image_array(self, input_image: np.ndarray) -> str:
ret, buffer = cv2.imencode('.png', input_image)
if ret:
return base64.b64encode(buffer).decode('utf-8')
def add_message_entry_as_specified_role(self, role: str) -> None:
self.payload['messages'].append({'role': role, 'content': []})
def add_text_content(self, text: str):
self.payload['messages'][0]['content'].append(
{'type': 'text', 'text': text}
)
def add_urlimage_content(self, urlimage: str, detail: str='auto') -> None:
self.payload['messages'][0]['content'].append(
{'type': 'image_url', 'image_url': {'url': urlimage}, 'detail': detail}
)
def add_b64image_content(self, b64image: str, detail: str='auto') -> None:
self.payload['messages'][0]['content'].append(
{'type': 'image_url', 'image_url': {'url': f'data:image/jpeg;base64,{b64image}', 'detail': detail}}
)
def add_content(self, contents: dict, as_type: str) -> None:
if as_type == 'text':
self.add_text_content(input_message_in_ja=contents['message'], glossary_name=contents['glossary'])
if as_type == 'urlimage':
self.add_urlimage_content(url=contents['url'], detail=contents['details'])
if as_type == 'b64image':
self.add_b64image_content(b64image=contents['b64image'], detail=contents['details'])
def delete_messages(self) -> None:
self.payload['messages'] = []
def delete_content(self, index: int=-1) -> None:
del self.payload['messages'][0]['content'][index]
def print_payload(self) -> dict:
logger.info(self.payload)
return self.payload
def execute(self) -> str:
result = self.client.chat.completions.create(**self.payload)
logger.info(f'Finish reason: {result.choices[0].finish_reason}')
logger.info(f'Created: {result.created}')
logger.info(f'ID: {result.id}')
logger.info(f'Usage')
logger.info(f' Completion tokens: {result.usage.completion_tokens}')
logger.info(f' Prompt tokens: {result.usage.prompt_tokens}')
logger.info(f' Total tokens: {result.usage.total_tokens}')
logger.info(f'Model output: {result.choices[0].message.content}')
return result.choices[0].message.content
if __name__ == '__main__':
input_image_path = '../assets/hogehoge.jpg'
gpt4v = RunGPT4VOnImage()
gpt4v.add_message_entry_as_specified_role(role='user')
gpt4v.add_text_content(text='What do you see in this image?')
b64image = gpt4v.encode_image_path(input_image_path=input_image_path)
gpt4v.add_b64image_content(b64image=b64image)
_ = gpt4v.execute()
OpenAI Python Libraryの利用、APIキーの設定
他のパッケージで利用することも考慮して引数としてAPIキーを入手出来るようにしていますが、基本的には先ほど環境変数として指定した値を読み込みます。
画像の入力に対応した学習済みモデルはgpt-4-vision-preview
です。self.payload.messages
に質問文や画像を追加します。
...
from openai import OpenAI
...
class RunGPT4VOnImage():
def __init__(self, api_key: Union[str, None]=None, model: str='gpt-4-vision-preview', max_tokens_per_call: int=200) -> None:
# GPT-4 with vision
self.OPENAI_API_KEY=api_key if not api_key is None else os.environ['OPENAI_API_KEY']
self.client = OpenAI(api_key=self.OPENAI_API_KEY)
self.payload = {
'model': model,
'messages': [], # ここに質問や画像などを並べてモデルに投げる。
'max_tokens': max_tokens_per_call,
}
画像の指定方法
画像をモデルに投げるためには、2つの既定のパターンいずれかに従う必要があります。
- ローカルに画像がある場合は、base64エンコードフォーマットに変換する。
- URLを指定する
detail
は3つのオプション(low
, high
, auto
)から1つを選択する。デフォルトでは、auto
が使用され、入力画像のサイズに応じてlow
かhigh
を使用するか決定する。
def add_b64image_content(self, b64image: str, detail: str='auto') -> None:
self.payload['messages'][0]['content'].append(
{'type': 'image_url', 'image_url': {'url': f'data:image/jpeg;base64,{b64image}', 'detail': detail}}
)
def add_urlimage_content(self, urlimage: str, detail: str='auto') -> None:
self.payload['messages'][0]['content'].append(
{'type': 'image_url', 'image_url': {'url': urlimage}, 'detail': detail}
)
画像のエンコーディング
ローカル画像をbase64エンコーディングします。ファイルパスまたは、OpenCV
などを利用しnumpy.ndarray
として扱う場合の2パターンを示します。
def encode_image_path(self, input_image_path: str) -> str:
with open(input_image_path, 'rb') as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def encode_image_array(self, input_image: np.ndarray) -> str:
ret, buffer = cv2.imencode('.png', input_image)
if ret:
return base64.b64encode(buffer).decode('utf-8')
モデルに投げる
モデルにself.payload
の中身を投げます。戻り値のresult
には消費したトークンなどの情報が付与されてます。
def execute(self) -> str:
result = self.client.chat.completions.create(**self.payload)
質問に対するモデルの回答はresult.choices[0].message.content
から取得可能です。
使用の流れ
下の入力画像../assets/hogehoge.png
に対して「What do you see in this image?」という質問をします。その結果以下の回答が得られます。内容も概ね良さそうです。結果は全く同じなることはなく、毎回異なります。
input_image_path = '../assets/hogehoge.jpg'
gpt4v = RunGPT4VOnImage()
gpt4v.add_message_entry_as_specified_role(role='user')
gpt4v.add_text_content(text='What do you see in this image?')
b64image = gpt4v.encode_image_path(input_image_path=input_image_path)
gpt4v.add_b64image_content(b64image=b64image)
_ = gpt4v.execute()
This image shows a group of five adorable kittens sitting in grass. They all have various fur patterns and colors ranging from orange to stripes of gray and brown, with white accents. The kittens have bright, engaging eyes and seem to be looking curiously at something in the distance. It's a cute and wholesome scene depicting the playful and inquisitive nature of young felines.
複数の画像をself.payload.messages
に加えてやり、画像の連続性などを認識させることも可能です。これを応用し、動画に対して自動で実況を付ける試みをされている方もいるようで、利用用途は十人十色であると感じました。とは言え、動画に対する利用は消費トークンが膨大になってしまうので、商用の利用はまだ先の話なのかもしれません。
GPT-4 With Visionが不得意なこと
以下はOpenAIが公開しているドキュメントを訳した物です。
GPT-4 with visionは非常にパワフルで様々な状況に適応可能ですが、とは言え限界もあることを認識することは重要です。例えば以下のような利用には向いていません。
- 医療関係の画像: CTスキャン画像を代表するような医療関係の画像を認識するのには向いていない。
- ラテン語系言語以外の文字が含まれる画像: 日本語や韓国語などが含まれた画像の扱いは不得意。
- 小さいテキスト: 画像内のテキストを大きくするが、重要な細部は切り取らないようにする
- 回転: 回転や上下反転した文字・図形を誤って認識する可能性がある。
- 空間的推論: チェス盤上にある駒の位置の正確な位置推論などは苦手。
- 正確性: 特定のシナリオ下では不正確な説明やキャプションを生成する可能性がある。
- 特徴的な入力画像: パノラマ画像や魚眼画像に対して正常に動作しない。
- メタデータとリサイズ: 元のファイル名やメタデータは処理されず、画像は分析前にリサイズされ、元のサイズに影響を与えます。
- カウンティング: 画像ないのオブジェクト数計測は真の値からずれうる。
- CAPTCHAS: CAPTCHASを送信することはできません。
計算コスト
画像をモデルに入力することで消費されるトークンは、画像サイズとdetail
オプションで決定される。detail: low
が指定された画像はすべて85トークン。detail: high
が指定された画像は以下の手順が踏まれる。
- 画像の縦横比を保存した状態で2048px × 2048pxのサイズに含まれるようにスケールする。
- 短辺が768pxになるようにスケールする。
- 最後にスケールされた画像が512px × 512pxの正方形いくつで完全にカバーされるか測定する。
512px × 512pxの正方形1つにつき170トークン、それに加え常に85トークン消費する。
例えば、OpenAIが公開したドキュメントでは以下の3例が消費するトークンが紹介されています。
-
detail: high
が指定された1024px × 1024pxの画像- 長辺が2048px以下なのでこのステップはスキップ
- 短辺が1024pxのため768px × 768pxにスケールされる
- 4つの512px × 512pxが必要であるため、
170 * 4 + 85 = 765
トークンを消費
-
detail: high
が指定された2048px × 4096pxの画像- 1024px × 2048pxにスケールする
- 短辺が1024pxのため、768px × 1536pxにスケールダウンする
- 6つの512px × 512pxが必要であるため、
170 * 6 + 85 = 1105
トークンを消費
-
detail: low
が指定された4096px × 8192pxの画像- インプットサイズに依らず、
detail: low
は常に85トークンを消費
- インプットサイズに依らず、
さいごに
実装自体は思ったより簡単に作成できてしまい、その背後で使用されている技術に触れることなく機能を利用できてしまいます。モデルから返された結果は、当初の予想よりも細部まで説明されており、”モデルが目を持った"と言われる訳が少し分かった気がしました。
お読みいただきありがとうございました。