Azure OpenAIのGPT-4oを使ってRAGを画像でやってみよう

## はじめに

GPT-4oがAzure OpenAIのAPIからも使えるようになりました。
現在(2024/5/31時点)は音声はまだ使えないようで、テキストと画像が入力に使えます。
そこで、RAGでの参照情報を、テキストではなく画像にしたい、というのが本記事の内容です。

RAGでは通常、参照情報を文字列にしてプロンプトに入れて使っています。
参照情報に表やグラフがある場合は、適宜文字列に変換していました。ですが、画像をそのまま渡してしまえば、表やグラフの文字列への変換は必要ありません。
ということでやってみます。

(今までも、GPT-4 Turbo with Vision等で画像使えましたが、GPT-4oは値段がGPT-4の凡そ半額ぐらいになり、性能もいいらしいので使ってみようという感じです。)

## 画像の準備

サンプルとして、以下を用意しました。

![zoo_table.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781545/64883541-1705-2e27-3ce5-f3a159527cfc.jpeg)
![zoo_line_graph.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781545/8a9801b8-8782-96b0-2598-1859d5154111.png)
![zoo_bar_graph.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781545/26442a98-26d9-aa14-24c3-e349681bf31e.png)

Azure OpenAIへのリクエスト時は、画像のURLか、画像をBase64に変換した値を渡します。
今回はお手軽に、ローカルにあるファイルをBase64に変換して渡します。

## モデルの準備

GPT-4oのモデルは、Azure Portalで他のモデルのデプロイと同様にできます。
Japanのリージョンでは現在は使えないので、[使えるリージョン](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/concepts/models#standard-deployment-model-availability)を確認してからAzure OpenAIリソースを作りましょう。
[コストはこちらです。](https://azure.microsoft.com/ja-jp/pricing/details/cognitive-services/openai-service/)


## プログラム

まず、必要なモジュールをインポートします。

```python
import openai
import base64
```

Azure OpenAIのAPIキー、APIタイプ、エンドポイント、バージョンを設定します。
deploymentはモデルをデプロイした時につけたデプロイ名です。
とりあえずローカルで動かす用にここで必要な情報全て準備しています。

```python
openai.api_key = "xxxx"
openai.api_type = "azure"
openai.azure_endpoint = "https://xxxx.openai.azure.com/"
openai.api_version = "2024-02-01"
deployment = "gpt-4o"
```

画像をBase64形式にエンコードするための関数です。

```python
def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode("utf-8")
```

エンコードしたい画像のファイル名を指定し、その画像をBase64形式にエンコードします。
画像がPNGであれば、```data:image/png;base64,```という文字列をBase64にした文字列の前にくっつけます。
画像がJPGであれば、```data:image/jpeg;base64,```です。

```python
file_name = "xxxx.PNG"
base64_image = encode_image(file_name)
image_url = "data:image/png;base64," + base64_image
```

GPT-4oに送信するテキストメッセージを指定します。

```python
text = "パンダがいる動物園と、その所在地を教えて"
```

最後に、GPT-4oにリクエストして、その結果を表示します。

```python
completion = openai.chat.completions.create(
    model=deployment,
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": text
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": image_url
                    }
                }
            ]
        }
    ],
    max_tokens = 1000 # GPT-4 Turbo with Visionの説明に、max_tokenは必ずつけてと書いてありますが、つけなくても大丈夫っぽいです
)

print(completion.to_json())
```

プログラム全体載せておきます。

```python
import openai
import base64

openai.api_key = "xxxx"
openai.api_type = "azure"
openai.azure_endpoint = "https://xxxx.openai.azure.com/"
openai.api_version = "2024-02-01"
deployment = "gpt-4o"

# 画像をbase64に変換
def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode("utf-8")

file_name = "xxxx.PNG"
base64_image = encode_image(file_name)
image_url = "data:image/png;base64," + base64_image
# image_url = "data:image/jpeg;base64," + base64_image

text = "パンダがいる動物園と、その所在地を教えて"

completion = openai.chat.completions.create(
    model=deployment,
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": text
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": image_url
                    }
                }
            ]
        }
    ],
    max_tokens = 1000
)

print(completion.to_json())
```

## 実行結果

#### 表

![zoo_table.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781545/64883541-1705-2e27-3ce5-f3a159527cfc.jpeg)

image_urlに表を渡したときのレスポンスがこんな感じです。
```json
{
  "id": "chatcmpl-xxxx",
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "content": "パンダがいる動物園とその所在地は次の通りです：\n\n- 上野動物園（所在地：東京都台東区）\n\n上記の動物園にはパンダがいます。",
        "role": "assistant"
      },
      "content_filter_results": {
        "hate": {
          "filtered": false,
          "severity": "safe"
        },
        "self_harm": {
          "filtered": false,
          "severity": "safe"
        },
        "sexual": {
          "filtered": false,
          "severity": "safe"
        },
        "violence": {
          "filtered": false,
          "severity": "safe"
        }
      }
    }
  ],
  "created": 1717134833,
  "model": "gpt-4o-2024-05-13",
  "object": "chat.completion",
  "system_fingerprint": "fp_xxxx",
  "usage": {
    "completion_tokens": 43,
    "prompt_tokens": 369,
    "total_tokens": 412
  },
  "prompt_filter_results": [
    {
      "prompt_index": 0,
      "content_filter_result": {
        "jailbreak": {
          "filtered": false,
          "detected": false
        }
      }
    },
    {
      "prompt_index": 1,
      "content_filter_result": {}
    }
  ]
}
```

表を読み取って答えられてますね。
```"パンダがいる動物園とその所在地は次の通りです：\n\n- 上野動物園（所在地：東京都台東区）\n\n上記の動物園にはパンダがいます。"```

```"completion_tokens": 43```で、返ってきた文字数は66文字なので、日本語のトークン数がかなり節約されてますね。
以前は文字数の倍ぐらいが目安でしたが、文字数より少なくなってます。
噂ではかなりの長い言いまわしも1トークンにしてくれることもあるようで、日本語ユーザー的にはかなり嬉しいです。

```"prompt_tokens": 369```で、textの方は20文字ぐらいなので、画像のトークン数は350トークンぐらいでしょうか。
解像度によるそうですが、そこまで大きなトークン数にはならなそうですね。ここは実際に使う画像で確認してみたいです。

精度とトークン数は要確認ですが、このやり方で画像を使ったRAGが作れそうです。

#### 折れ線グラフ

グラフも試してみました

![zoo_line_graph.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781545/8a9801b8-8782-96b0-2598-1859d5154111.png)

```python
text = "数が増えている動物は"
```
結果（回答とトークン数だけ抜粋）
```json
"content": "グラフを見ると、数が増えている動物は「プレーリードッグ」です。2020年から2023年にかけて、個体数が徐々に増加しています。"
"completion_tokens": 45
"prompt_tokens": 225
"total_tokens": 270
```

#### 棒グラフ

![zoo_bar_graph.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781545/26442a98-26d9-aa14-24c3-e349681bf31e.png)

```python
text = "動物の数が多い順に教えて"
```
結果（回答とトークン数だけ抜粋）
```json
"content": "このグラフの動物の数を多い順に並べると、以下のようになります：\n\n1. ブレイリードッグ\n2. アルカバ\n3. ライオン\n4. ゾウ\n5. パンダ\n\nブレイリードッグが 最も多く、次いでアルカバ、ライオン、ゾウ、パンダの順です。"
"completion_tokens": 88
"prompt_tokens": 228
"total_tokens": 316
```
プレーリードッグがブレイリードッグ、アルカパがアルカバになっちゃてますが許しましょう

## システムプロンプト効いてない？

ここまではいい感じですが、質問によってはちゃんと答えられないものもありました。
なのでシステムプロンプトに、***Let's think step by step***的なものを入れてみたのですが、効いておらず、謎が残りました。
例えば、以下は英語で答えてくれないですが、
```python
messages=[
        {
            "role": "system",
            "content": "Please Answer in English"
        },
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": text
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": image_url
                    }
                }
            ]
        }
    ],
```

以下は当然英語で答えてくれます
```python
messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "Please Answer in English" + text
                },
                {
                    "type": "image_url",
                    "image_url": {
                        "url": image_url
                    }
                }
            ]
        }
    ],
```

## まとめ
- 画像はURL or Base64に変換で渡せる
- とりあえずPNGとJPGは使える
- GPT-4oは日本語トークンの節約がすごい

つまり、そこまでコストが上がらずに画像使ったRAGができそうです。
ですが、リクエスト時は、参照情報は画像のままで使えばいいけど、その参照情報を検索するためには参照情報を文字列にして格納しておく必要はありですね。