CCCMKホールディングス TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

GPT-4 vision-previewを使ってグラフ画像を整理する方法を考えてみた話。

こんにちは、CCCMKホールディングスTECH LAB三浦です。

最近とても湿気が多いです。家の中もジメジメしてきたのですが、この前風が通り抜けるスポットを家の中に見つけました。そこにいるとひんやりした風が通り抜けて気持ちがいいので、ずっとそこにいます。

さてAzure OpenAI Serviceの東日本リージョンで現在提供されている"gpt-4 vision-preview"というモデルはテキストだけでなく画像を入力することが可能です。Azure OpenAI Serviceのリソースを作成すると利用できる"Azure OpenAI Studio"でも画像を入力した時の動作を確認出来ますが、API経由で画像を入力した経験がなかったので、試してみようと思いました。せっかくなので実用的なシナリオを想定してやってみようと思い、グラフの画像を入力すると自動的にそのグラフの情報を出力してくれる処理をLangChainで実装してみました。

やりたいこと

今回の目標は次のような流れを実現することです。

グラフ画像から構造化された説明情報を取得する

グラフ画像を入力するとそのグラフの情報を決まったフォーマットで出力する、という流れです。いずれはAPIで利用することを想定してjson形式で結果を出力するようにしています。

実装する

グラフ画像の生成

最初にPythonのグラフ作成ライブラリ"Plotly"を使ってグラフ画像を生成しました。グラフ描画時に使用したデータはPlotlyに組み込まれているデモデータの中から、統計学や機械学習で有名なアヤメ(iris)のデータを選んで使いました。

最初に必要になるライブラリをpipでインストールしました。kaleidoはPlotlyでグラフを画像化する時に必要になるライブラリです。

pip install \
kaleido\
langchain-openai\
langchain>=~0.1\
langchain-community

2つのグラフを作成します。1つはアヤメの種類ごとのがくの長さの平均を表す棒グラフ、もう1つはがくの長さと花びらの長さの関係性を表す散布図です。

次のコードでデータの読み込みと棒グラフの生成まで行います。最後の行でグラフ画像をbase64文字列に変換する処理を行っています。base64にする理由は、gpt-4 vision-previewに渡すときにこの形式にしておく必要があるからです。

import base64

import plotly.data as data
import plotly.graph_objects as go

mean_sp_length_per_species = iris_data.groupby("species").mean()[["sepal_length"]].reset_index()

bar_fig = go.Figure(
    data = [
        go.Bar(
            x=mean_sp_length_per_species["species"],
            y=mean_sp_length_per_species["sepal_length"],
        ),
    ]
)

bar_fig.update_layout(
    dict(
        title="iris sepal_length",
        xaxis=dict(title="species"),
        yaxis=dict(
            title="sepal_length",
        )
    )
)

bar_graph_base64 = str(base64.b64encode(bar_fig.to_image()).decode('utf-8'))

今回使用する棒グラフ画像

同様に散布図の作成です。

scatter_fig = go.Figure(
    data = [
        go.Scatter(
            x=iris_data["sepal_length"],
            y=iris_data["petal_length"],
            mode="markers"
        ),
    ]
)

scatter_fig.update_layout(
    dict(
        title="iris sepal_length x petal_length",
        xaxis=dict(title="sepal_length"),
        yaxis=dict(title="petal_length")
    ),
)

scatter_graph_base64 = str(base64.b64encode(scatter_fig.to_image()).decode('utf-8'))

今回使用する散布図画像

このあとgpt-4 vision-previewにグラフ画像を入力するのですが、何度か試してみて、タイトルやラベルが日本語だと認識精度があまりよくない印象を受けました。特にy軸ラベルの向きが反時計回りに90度傾いているためか、y軸ラベルが日本語の場合は認識が難しいようです。

gpt-4 vision-previewに与えるプロンプトテンプレートの構築

gpt-4 vision-previewに与えるプロンプトテンプレートを作成します。最終的な出力はJson形式にしたいので、それを実現するためにLangChainのJsonOutputParserを使用します。JsonOutputParserはPydanticでアノテーションしたクラスの情報を受け取ると、そのクラスの構造通りの出力をするよう指示するプロンプトを作成してくれます。

ただJsonOutputParserを使ってもJson形式で出力してくれなかったり、"json"という接頭詞を付けてしまうことがあるため、さらにプロンプトの中で指示するようにしました。

import json
import os
from typing import List

from langchain_core.messages import HumanMessage
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_openai import AzureChatOpenAI

class Graph(BaseModel):
    """JsonOutputParserに渡し、gpt-4の出力を指定するためのクラス"""
    title: str = Field(description="グラフのタイトル。")
    graph_type: str= Field(description="グラフの種類。")
    labels: List[str] = Field(description="グラフに含まれる全ての軸ラベル")
    description: str = Field(description="このグラフの説明")

# prompt
format_str = JsonOutputParser(pydantic_object=Graph).get_format_instructions()

text_prompt = f"""このグラフ画像の説明を"format"通りのJson形式で出力してください。
余計な文章や冒頭および末尾に"```json"などの余計な単語は絶対に含めてはいけません。

# format
{format_str}
"""

image_prompt_template = PromptTemplate.from_template(
    "data:image/png;base64,{base64_image}"
)

gpt-4 vision-previewへのリクエスト処理

最後にgpt-4 vision-previewへのリクエスト処理を実装します。gpt-4 vision-previewではmax_tokensパラメータの指定が必須です。

# 使用するLLM
llm = AzureChatOpenAI(
    api_key=os.environ.get("AZURE_OPENAI_API_KEY"),
    azure_endpoint=os.environ.get("AZURE_OPENAI_API_BASE"),
    openai_api_version="2024-02-01",
    model_name="gpt-4v", # gpt-4 vision-preview
    temperature=0,
    max_tokens=1024 #gpt-4 vision-previewの場合は必須
)

def convert_image_to_discriptions(base64_graph_img: str):
    """
    グラフ画像をbase64文字列で受け取り、gpt-4 vision-previewに解析させる
    """
    image_prompt = image_prompt_template.format(base64_image=base64_graph_img)
    input_message = HumanMessage(
        content=[
            {"type":"text","text":text_prompt},
            {
                "type":"image_url", 
                "image_url": {
                    "url":image_prompt
                }
            }
        ]
    )
    llm_response = llm.invoke([input_message]).content
    try:
        result = json.loads(llm_response)
    except:
        # gpt-4 vision-previewのレスポンスがJson形式でないことがある
        result = {"message": "an error occured","llm_response": llm_response}
    return result

ためしてみる

それではどんな結果が得られるか、試してみます。まずは棒グラフから。

convert_image_to_discriptions(bar_graph_base64)

出力結果は次のようになり、英語で説明文が得られました。

{'title': 'iris sepal_length',
 'graph_type': 'Bar Chart',
 'labels': ['setosa', 'versicolor', 'virginica'],
 'description': 'This graph shows the average sepal length of three different iris species.'}

次に散布図です。

convert_image_to_discriptions(scatter_graph_base64)

こちらも英語で結果が返ってきました。

{'title': 'iris sepal_length x petal_length',
 'graph_type': 'scatter plot',
 'labels': ['sepal_length', 'petal_length'],
 'description': 'This graph shows a scatter plot of the sepal length versus the petal length of the iris dataset.'}

内容は合っていそうです!

まとめ

今回はAzure OpenAI Serviceで提供されているgpt-4 vision-previewをPythonのプログラムからAPI経由で利用し、グラフ画像に対する構造化された説明情報が得られるのかを試してみました。日本語が含まれるグラフ画像だと難しかったのですが、英語だと良い結果が得られました。gpt-4 vision-previewに限らず他のLLMでもそうなのですが、英語でやり取りをすると上手くいくのに日本だと上手くいかない、といったことがよくあるように感じます。もしかしたらコアの処理は英語で行い、入力/出力の部分で英語や日本語に変換する、といった複数の処理を組み合わせたパイプラインが常に必要なのかもしれません。

実は今回の実験、もともと「グラフ画像からそこに表示されているデータを抽出出来るのか」というテーマから始めたものだったのですが、gpt-4 vision-previewだと難しいようです。「グラフからデータを抽出する能力はない」といった趣旨の応答が日本語で返ってきたので・・・。こちらを実現するには別のモデルを利用する必要がありそうです。