Livebook から Gemini Pro Vision を呼んで画像についての質問に答える

## はじめに

Google の生成 AI Gemini Pro Vision を Livebook から呼んでみます

https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini?hl=ja

Gemini Pro Vision はいわゆる「マルチモーダル」で、 **文章と画像を合わせて認識** することができます

例えば AWS の [Rekognition](https://aws.amazon.com/jp/rekognition/) は画像認識用の AI サービスなので、画像を投げると、その画像に含まれる物の情報が得られます
**画像 |> テキスト**

OpenAI の [ChatGPT](https://chat.openai.com/) 3.5 だと、文章を投げると、それに対する返答が文章で得られます
**テキスト |> テキスト**

Stable Difusion だと文章から画像を生成します
**テキスト |> 画像**

これらに対して Gemini Pro Vision は **与えた画像に対して文章で質問ができる** サービスです
**（画像 + テキスト) |> テキスト**

ちなみに ChatGPT 4 Vsion も同じことが可能です

いまいちピンとこないかもしれませんが、とりあえず Livebook から呼んでみましょう

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/google/gemini_pro.livemd

## Gemini Pro の API キー取得

:::note info
色々手段はあると思いますが、私がやった方法です
:::

Gemini Pro の料金説明ページに行きます

https://ai.google.dev/pricing

左下の **Get API** をクリックします

![スクリーンショット 2024-01-06 9.40.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a7ba515f-da60-6ea1-8dff-ad213b20ccf5.png)

初回は認証等を求められるので、指示に従います

Google AI Studio の画面に遷移したら左メニューの先頭 **Get API Key** をクリックし、 API Key の画面に遷移します

特別理由がなければ **Create API key in new project** をクリックしましょう

![スクリーンショット 2024-01-06 9.43.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/84cd71f3-d984-6bab-618f-a5976e82c536.png)

少し待つと API キーの値が表示されるので、その値を Livebook で使用します

:::note info

左メニューの `Create new` から、 Google AI Studio 内で Gemini Pro が試せます

とりあえず気軽に試したい場合はこっちが便利です

:::

## セットアップ

Livebook で新しいノートブックを作成し、セットアップセルに以下の内容を入力します

```elixir
Mix.install([
  {:kino, "~> 0.12"},
  {:req, "~> 0.4"}
])
```

入出力用の UI に Kino を使います

REST API として Gemini Pro にアクセスするため Req をインストールします
（Gemini Pro 用の Elixir モジュールは見当たりませんでした）

## 接続情報の準備

API キーの入力 UI を作ります

```elixir
api_key_input = Kino.Input.text("API_KEY")
```

![スクリーンショット 2024-01-06 9.51.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ec51f909-fb0b-9911-81aa-d60b701d5a5a.png)

作成した UI に、取得しておいた Gemini Pro の API キーを入力します

API キーの値を UI から取得し、 REST API の URL とリクエストヘッダーを用意します

```elixir
api_key = Kino.Input.read(api_key_input)

base_url = "https://generativelanguage.googleapis.com/v1beta/models"

headers = %{
  "Content-Type" => "application/json"
}
```

## 文章だけの入力

まずは文章だけで質問してみます

```elixir
request_body = %{
  contents: [
    %{
      parts: [
        %{
          text: "開発言語の Elixir について教えてください"
        }
      ]
    }
  ]
}
```

Req を使い、 POST メソッドでアクセスします

:::note info
文章だけなので、単なる Gemini Pro で事足りるため、 URL は `/gemini-pro` になっています
:::

```elixir
response =
  "#{base_url}/gemini-pro:generateContent?key=#{api_key}"
  |> Req.post!(
    json: request_body,
    headers: headers
  )
  |> Map.get(:body)
```

実行結果
```elixir
%{
  "candidates" => [
    %{
      "content" => %{
        "parts" => [
          %{
            "text" => "Elixir は、高並行性と堅牢性を備えた、汎用的な関数型プログラミング言語です。..."
          }
        ],
        "role" => "model"
      },
      "finishReason" => "STOP",
      "index" => 0,
      "safetyRatings" => [
        %{"category" => "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_HATE_SPEECH", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_HARASSMENT", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_DANGEROUS_CONTENT", "probability" => "NEGLIGIBLE"}
      ]
    }
  ],
  "promptFeedback" => %{
    "safetyRatings" => [
      %{"category" => "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability" => "NEGLIGIBLE"},
      %{"category" => "HARM_CATEGORY_HATE_SPEECH", "probability" => "NEGLIGIBLE"},
      %{"category" => "HARM_CATEGORY_HARASSMENT", "probability" => "NEGLIGIBLE"},
      %{"category" => "HARM_CATEGORY_DANGEROUS_CONTENT", "probability" => "NEGLIGIBLE"}
    ]
  }
}
```

色々な項目が返ってきていますが、返答のテキストは `text` に入っています

テキストをマークダウンとして表示しましょう

```elixir
response
|> Map.get("candidates")
|> hd()
|> Map.get("content")
|> Map.get("parts")
|> hd()
|> Map.get("text")
|> Kino.Markdown.new()
```

実行結果
![スクリーンショット 2024-01-06 9.57.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ffea4e7b-5e7c-75a9-3bc7-e38bfa13ec54.png)

ちゃんと **Elixir がモバイルアプリに利用されている** ことまで知っていますね

https://fukuokaex.connpass.com/event/306178/

## 画像と文章の入力

画像と文章の組み合わせを Gemini Pro Vision に投げてみましょう

画像入力の UI を作成します
今回は JPEG 形式の画像を使うため、明示的に形式を指定しています

```elixir
image_input = Kino.Input.image("IMAGE", format: :jpeg)
```

![スクリーンショット 2024-01-06 10.04.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/cd4245a7-27a9-6809-f035-b0a6f4cffd4b.png)

UI から質問を投げたい画像を選択します

選択した画像を UI から読み込みます

```elixir
image_binary =
  image_input
  |> Kino.Input.read()
  |> Map.get(:file_ref)
  |> Kino.Input.file_path()
  |> File.read!()
```

実行結果
![スクリーンショット 2024-01-06 10.06.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6d869fe2-b959-75da-1d20-995e8bb98fcf.png)

質問する文章と、 BASE64 エンコーディングした画像をリクエストボディーに入れます

```elixir
request_body = %{
  contents: [
    %{
      parts: [
        %{
          text: "首輪は何色ですか"
        },
        %{
          inline_data: %{
            mime_type: "image/jpeg",
            data: Base.encode64(image_binary)
          }
        }
      ]
    }
  ]
}
```

Gemini Pro Vision にリクエストを投げます

:::note info
今回は画像を含むので URL が `/gemini-pro-vision` になっています
:::

```elixir
response =
  "#{base_url}/gemini-pro-vision:generateContent?key=#{api_key}"
  |> Req.post!(
    json: request_body,
    headers: headers
  )
  |> Map.get(:body)
```

実行結果
```elixir
%{
  "candidates" => [
    %{
      "content" => %{"parts" => [%{"text" => " 首輪は青色です。"}], "role" => "model"},
      "finishReason" => "STOP",
      "index" => 0,
      "safetyRatings" => [
        %{"category" => "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_HATE_SPEECH", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_HARASSMENT", "probability" => "NEGLIGIBLE"},
        %{"category" => "HARM_CATEGORY_DANGEROUS_CONTENT", "probability" => "NEGLIGIBLE"}
      ]
    }
  ],
  "promptFeedback" => %{
    "safetyRatings" => [
      %{"category" => "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability" => "NEGLIGIBLE"},
      %{"category" => "HARM_CATEGORY_HATE_SPEECH", "probability" => "NEGLIGIBLE"},
      %{"category" => "HARM_CATEGORY_HARASSMENT", "probability" => "NEGLIGIBLE"},
      %{"category" => "HARM_CATEGORY_DANGEROUS_CONTENT", "probability" => "NEGLIGIBLE"}
    ]
  }
}
```

`text` の部分だけ取り出してマークダウンとして取得します

```elixir
response
|> Map.get("candidates")
|> hd()
|> Map.get("content")
|> Map.get("parts")
|> hd()
|> Map.get("text")
|> Kino.Markdown.new()
```

![スクリーンショット 2024-01-06 10.10.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/053c4685-7de5-5d4f-24a5-8cfa64352a5f.png)

ちゃんと画像内の首輪の色を答えてくれました

もっと難しい質問をしてみます

```elixir
request_body = %{
  contents: [
    %{
      parts: [
        %{
          text: "写真内の犬はどういう状態になっていますか"
        },
        %{
          inline_data: %{
            mime_type: "image/jpeg",
            data: Base.encode64(image_binary)
          }
        }
      ]
    }
  ]
}


"#{base_url}/gemini-pro-vision:generateContent?key=#{api_key}"
|> Req.post!(
  json: request_body,
  headers: headers
)
|> Map.get(:body)
|> Map.get("candidates")
|> hd()
|> Map.get("content")
|> Map.get("parts")
|> hd()
|> Map.get("text")
|> Kino.Markdown.new()
```

実行結果
```markdown
写真内の犬は、人の腕の中に抱えられていて、大人しくしています。
```

これも正解しています！

もっと抽象的な質問をしてみましょう

```elixir
request_body = %{
  contents: [
    %{
      parts: [
        %{
          text: "写真内の犬はどんなことを考えているように見えますか"
        },
        %{
          inline_data: %{
            mime_type: "image/jpeg",
            data: Base.encode64(image_binary)
          }
        }
      ]
    }
  ]
}


"#{base_url}/gemini-pro-vision:generateContent?key=#{api_key}"
|> Req.post!(
  json: request_body,
  headers: headers
)
|> Map.get(:body)
|> Map.get("candidates")
|> hd()
|> Map.get("content")
|> Map.get("parts")
|> hd()
|> Map.get("text")
|> Kino.Markdown.new()
```

実行結果
```markdown
写真内の犬は、カメラに興味津々で、何だろうと不思議に思っているように見えます。また、少し緊張しているようにも見えます。
```

単なる画像認識を超えているように見えますね

## まとめ

Gemini Pro Vision は画像と文章を組み合わせて認識することで、かなり幅広い認識ができるようになっています

今までの画像 AI では答えられなかったような汎用的なタスクに対応できるので、色々な応用ができそうです

ただし、他の生成 AI と同様、分からない問いにも「一見正しそうなデタラメ」を返してきます

また、他の画像 AI のような物体の位置情報は返してくれません（質問すれば返ってくるけどデタラメになっています）

用途、用法には気をつけましょう