こんにちは、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だと難しいようです。「グラフからデータを抽出する能力はない」といった趣旨の応答が日本語で返ってきたので・・・。こちらを実現するには別のモデルを利用する必要がありそうです。