🎃

(今さら)GPT系のマルチモーダルの気になったところを調べてみる

2024/08/31に公開

初めに

GPT系のマルチモーダルモデル(gpt-4oやgpt-4o-miniなど)が使用できるようになってから、もうそれなりに経ちましたが、APIで今まで使用していなかったので、気になっていたことを調べてみようという記事です。基本的な使い方はOpenAIのドキュメントやいろんな方が記事を出しているので、この記事に書く必要もないのですが、一部基本的な使い方も記載していこうと思います。

そもそもマルチモーダルモデルって何?

初期では、テキストからテキストを回答してくれるようなモデルが多かったですが、最近では画像・動画・音声などいろんな情報を基に回答してくれるようになってきており、複数の異なるデータの種類を入力として使えるものをマルチモーダルと言います。gpt-4oなどのOpenAIのモデルはまだ画像の入力しかできず、音声や動画は入力としては、使えませんが、今後は入力に音声や動画が使えるようになったり、出力(回答)もテキストだけでなく、音声などいろんな出力形式が可能になっていくでしょう。
ここら辺は以下記事のように色々情報があると思うので、気になる方はネットの記事など確認してください。
https://www.softbank.jp/biz/solutions/generative-ai/ai-glossary/multimodal-ai/

今回確認すること

今回はgpt-4o-miniを使用して画像入力に関する調査をしていきます。gpt-4oではなく、gpt-4o-miniを使うのは単に安いからです。gpt-4oの方が精度は高いですし、web版のchatGPTでgpt-4oのマルチモーダルを試して、こんな画像も読み取れるんだ!と自分も驚いたのですが、この記事では画像の読み取り精度については確認しません。なお、検証にはpythonを用いたコードを使用しています。

基本的な使い方

テキストのみの入力

OpenAIサイトからコピペした内容
from openai import OpenAI
client = OpenAI()

response = client.chat.completions.create(
  model="gpt-4o-mini",
  messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Who won the world series in 2020?"},
    {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
    {"role": "user", "content": "Where was it played?"}
  ]
)
{"role": "user", "content": "Who won the world series in 2020?"}

テキストのみの入力の場合は上のようにroleを設定し、contentにstringを渡す形でmessagesのリストを作るのが一般的でしたね。以下のOpenAIのサイトに書いてある情報なので、詳しくはそちらをご確認ください。
https://platform.openai.com/docs/guides/chat-completions

テキスト+画像の入力

画像が1枚の場合は以下のような感じです。

OpenAIサイトからコピペした内容
from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
  model="gpt-4o-mini",
  messages=[
    {
      "role": "user",
      "content": [
        {"type": "text", "text": "What’s in this image?"},
        {
          "type": "image_url",
          "image_url": {
            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
          },
        },
      ],
    }
  ],
  max_tokens=300,
)

print(response.choices[0])

contentの中がstring型ではなく辞書型になっています。typeキーでtextかimage_urlを指定し、image_urlでは画像のURLかbase64エンコードした値を入力します。
→個人的にはcontentってstring型でもいいし、辞書型でもいいってのが気持ち悪いなと思ったのですが、実際にopenaiライブラリを辿ってみると、確かにどちらでも型的には問題なくて、リクエストを送ったのち、いい感じに処理されてるっぽいですね。画像が複数になる場合も"type": "image_url"を持つ辞書型を複数にしてあげればいいだけなので簡単ですね。
↓参考
https://platform.openai.com/docs/guides/vision

ちなみにimage_url同様、以下コードのようにtextもリストに追加することで1回の質問に追加することができるようです。通常のcontentのstring型に全てのテキストを含める場合と精度の部分など何か変わるかはわかりません。→試した感じ違いはわからなかったので、詳しい方いたら教えて欲しいです。

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": [
        {"type": "text", "text": "今日の昼食はカレーです"},
        {
            "type": "image_url",
            "image_url": {
                "url": f"data:image/png;base64,{base64_image}",
                "detail": selected_image_resolution_option
            }
        },
        {"type": "text", "text": "今日の夕食はハンバーグです"},
    ]}
]
回答

今日の昼食にカレー、そして夕食にハンバーグですか!どちらも美味しそうですね。カレーはスパイシーで温かい食事、ハンバーグはジューシーでボリュームがあって楽しみですね。どちらが特に好きですか?

トークン調査

テキストのみ

まず、テキストのみの場合でもcontentの中身がstring型か辞書型か異なるメッセージを作成することができるので、その違いによってトークンが異なるかを調べてみました。

string型
messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "今日の昼食はカレーです"}
]
辞書型
messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": [
        {"type": "text", "text": "今日の昼食はカレーです"}
    ]}
]

上の2つで試したところ入力プロンプトのトークンはどちらも25でした。なので、辞書型の書き方であろうと同じテキストであれば入力トークン数は変わらないことがわかりました。

画像込みのトークン

画像入力にはauto、high、lowの3種類のモードがあり、デフォルトではautoが選択され画像の解像度によって、highかlowが自動的に決まります。明示的に指定することもでき、その場合は以下のようにdetailを設定します。

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": [
        {"type": "text", "text": "何が映っていますか?"},
        {
            "type": "image_url",
            "image_url": {
                "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
                "detail": "low"
            }
        },
    ]}
]

OpenAIのドキュメントにあった画像URLを使用してみると、autoモードでは画像は高解像度としてhighで処理されたようです。以下がhighとlowの場合のトークンの違いです。

  • auto(high)の場合:prompt_tokens=36858
  • lowの場合:prompt_tokens=2856

URLではなくBase64エンコードのデータを使用しても、トークンに関して同様の挙動を確認できました。
トークンの計算については、以下リンクの情報を見るにおそらく画像内に512×512pxがどれくらいあるかと高解像度or低解像度で金額が決まってきそうです。
https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding

トークン数は分かりませんが、OpenAIのPriceページで画像のサイズを入力すればモデルごとの処理にかかる金額を計算することはできます。→この金額の部分でも値を変えて512pxが金額が変わる境目なことを確認できます。

https://openai.com/api/pricing/

画像を含んだ履歴機能

以下のように画像情報を送れば、過去の画像の情報についても理解して、どのような画像かを回答してくれました。

messages = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": [
        {"type": "text", "text": "こんにちは"},
        {
            "type": "image_url",
            "image_url": {
                "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
                "detail": "low"
            }
        },
    ]},
    {"role": "assistant", "content": "はい、こんにちは"},
    {"role": "user", "content": "先ほどの画像には何が映っていますか"},
]

この辺りはテキストのみのチャットと同様に過去の情報も一緒に送ってやれば、それを踏まえた回答はしてくれるようですね。

まとめ

GPT系のマルチモーダルはmessagesのcontentのところに辞書型で画像の情報も渡すようにするくらいで、簡単に利用することができます。意外と今回調べたような内容を記事にしている人は少ないように思えたので、GPT系のマルチモーダルを使う人の参考になれば幸いです。

余談

検証は以下2ファイルのコードをハードコーディングなどでちょこちょこ変えながら行いました。streamlitを使っています。python環境とライブラリインストールして、envを設定すれば、他の人でも簡単に試せると思うので一応共有です。

app.py
import streamlit as st
import base64
from openai_api import gpt_response

# 画像アップロードとOCR機能
def encode_image(image):
    return base64.b64encode(image.read()).decode("utf-8")

st.title("マルチモーダルテスト")

## オプションを定義
model_options = ['gpt-4o-mini']
image_resolution_options = ['auto', 'high', 'low']

## セレクトボックスをサイドバーに作成
selected_model_option = st.sidebar.selectbox('モデルを選択してください:', model_options)
selected_image_resolution_option = st.sidebar.selectbox('画像の処理解像度:', image_resolution_options)

# ユーザー入力
user_input = st.text_input("質問入力欄", "")
uploaded_file = st.file_uploader("画像", type=("png", "jpg", "jpeg"))

if uploaded_file is not None:
    base64_image = encode_image(uploaded_file)

if st.button("送信"):
    if user_input:
        if uploaded_file is not None:
            messages = [
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": [
                    {"type": "text", "text": user_input},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/png;base64,{base64_image}",
                            "detail": selected_image_resolution_option
                        }
                    },
                ]}
            ]
        else:
            messages = [
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": user_input},
            ]
        with st.spinner('回答生成中...'):
            response = gpt_response(messages, selected_model_option)
        st.write("## 回答のみ")
        st.write(response.choices[0].message.content)
        st.write("## リクエストのmessages")
        st.write(messages)
        st.write("## レスポンスそのまま")
        st.write(response)

openai_api.py
import os

from openai import OpenAI
from openai.types.chat import ChatCompletion
from dotenv import load_dotenv

load_dotenv(override=True)

# APIキーを設定
api_key = os.environ["OPENAI_API_KEY"]

client = OpenAI(api_key=api_key)
def gpt_response(messages: list[dict], model: str) -> ChatCompletion:
    response = client.chat.completions.create(
        model=model,
        messages=messages
    )
    return response

こんな感じで質問入れて画像をアップロードして、送信できる感じです。

Discussion