Gemini 1.5 Pro と Claude 3.5 Sonnet を対決する

## はじめに
Google Cloud では、Google 謹製の Gemini だけでなく、Claude 3 も簡単に試すことができます。どうせ両者の性能を比較するのであれば常に同時にプロンプトを投げて比較したいよねということで、両者を比較する UI をStreamlit で簡単に実装しました。

ざっくり利用するテクノロジーは以下です。
 - Google Cloud
 - Gemini 1.5 Pro
 - Claude 3.5 Sonnet (Opusから変更しました)
 - Streamlit
 - Cloud Run

Gemini API は、`Google AI Studio`でも無償枠が提供されていますが、プロンプトが製品の改善のために参照される場合があります。一方、Google Cloud 上ではプロンプトがログ保存されることも、製品の改善やトレーニングに利用されることもありません。また、Google Cloud 上では、東京リージョンの API が用意されています。

完成イメージはこんなのを考えています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/467288/78b6e267-e6dd-44b4-95fd-c4562da27323.png)


## 環境セットアップ
前提として、Google Cloud 環境の課金が有効化されたプロジェクトが存在することとします。

### Gemini (Vertex AI) API の有効化と基本的なコード

初めての場合は、Google Cloud のコンソールから上の検索バーで Vertex AI を検索し、Vertex AI のトップページにアクセスして、Vertex AI API を有効化しておきます。

Vertex AI > マルチモーダル > シングルターンの`オープン`ボタン を押すと、プロンプトを試す UI である Vertex AI Studio が表示されます。Vertex AI Studio ではお手軽にプロンプトを投げたり、プロンプトの保存ができます。また、右肩の方にある`コードを取得`を押すと、UI 上のプロンプトをコピペで利用できるコードが出てくるので参考になります。

```
pip install --upgrade google-cloud-aiplatform
gcloud auth application-default login
```
```python
import base64
import vertexai
from vertexai.generative_models import GenerativeModel, Part, FinishReason
import vertexai.preview.generative_models as generative_models

def generate():
  vertexai.init(project="xxxxxx", location="asia-northeast1")
  model = GenerativeModel(
    "gemini-1.5-pro-001",
  )
  responses = model.generate_content(
      ["""ここにプロンプト"""],
      generation_config=generation_config,
      safety_settings=safety_settings,
      stream=True,
  )

  for response in responses:
    print(response.text, end="")


generation_config = {
    "max_output_tokens": 8192,
    "temperature": 1,
    "top_p": 0.95,
}

safety_settings = {
    generative_models.HarmCategory.HARM_CATEGORY_HATE_SPEECH: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_HARASSMENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
}

generate()
```
ふむふむ、参考にします。


### Claude 3.5 Sonnet API の有効化

Google Cloud 上での Claude 3.5 Sonnet の利用は、Model Garden から有効化します。Vertex AI > Model Garden と UI をたどると、基盤モデルのリストに Claude 3.5 Sonnet が見えるので、カードをクリックして利用規約に同意しつつ API を有効化します。Claude 3 Opus, Sonnet, Haiku を利用したい場合は、別々に有効化が必要です。

API を有効化すると、Claude 3.5 Sonnet のページにサンプルコードも表示されます。
```
python -m pip install -U 'anthropic[vertex]'
```
```python
from anthropic import AnthropicVertex

LOCATION = "us-east5"

client = AnthropicVertex(region=LOCATION, project_id="PROJECT_ID")

with client.messages.stream(
  max_tokens=1024,
  messages=[
    {
      "role": "user",
      "content": "ここにプロンプト",
    }
  ],
  model="claude-3-5-sonnet@20240620",
) as stream:
  for text in stream.text_stream:
    print(text)
```
ふむふむ、参考にします。

## Streamlit による UI の作成

requirements.txt は以下です。後で簡易的な RAG アプリっぽく改良して PDF も読むことにしたので、`pypdf`を追加しています。
```
streamlit
google-cloud-aiplatform
anthropic[vertex]
pypdf
```

Streamlit で UI の枠を作成します。Form 枠の中にテキストフィールドとファイルアップロード部品を配置しています。Gemini 1.5 Pro と Caude 3.5 Sonnet の応答を入れる枠を2段組みで用意しています。

```python
import os
import tempfile
import streamlit as st
import vertexai
from vertexai.generative_models import GenerativeModel, Part
from anthropic import AnthropicVertex, AsyncAnthropicVertex
import asyncio
from pypdf import PdfReader

# layoout page
st.set_page_config(layout="wide")
st.title("Vertex AI の言語モデルを比較")
with st.form('my_form'):
    uploaded_file = st.file_uploader("(option) PDF を入力する場合、Gemini は画像として、Claude はテキストとしてロードします。", type=["pdf"],)
    if uploaded_file:
        with st.spinner('Loading ...'):
            bytes_data = uploaded_file.getvalue()
            # for Gemini
            document1 = Part.from_data(data=bytes_data, mime_type="application/pdf")
            # for Anthropic
            with tempfile.TemporaryDirectory() as temp_dir:
                with open(os.path.join(temp_dir, uploaded_file.name), "wb") as temp_pdf:
                    temp_pdf.write(bytes_data)
                    reader = PdfReader(temp_pdf.name)
                    pdf_text = ''.join([page.extract_text() for page in reader.pages])

    input = st.text_area(label='Prompt', value='映画好きの人にお勧めの日本国内の名所を教えてください')
    submitted = st.form_submit_button('Submit')

col1, col2 = st.columns(2)
col1.subheader('Gemini 1.5 Pro')
col2.subheader('Claude 3.5 Sonnet')
```
添付ファイルを PDF に限定し、ファイルが添付されたらそのファイルをロードする処理を追加しています。

Gemini は、PDF ファイルをバイナリのまま直接渡すことが可能です。これ地味に便利です。テキストだけでなく画像として解釈できるようです。

Claude 3 はバイナリで PDF を渡すことはできないため、pypdf を利用して、PDF をパースしてテキストデータを取り出しています。プロンプトに PDF を渡す想定がなければ、`st.file_uploader`とその後のファイル パース処理は省略してよいです。

virtualenv でライブラリをインストールして、UI の起動を確認します。
```
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
streamlit run main.py
```

## Gemini 呼び出しを追加

Google Cloud の `PROJECT_ID`をソースにハードコードしたくないので、環境変数に定義します。
```
export PROJECT_ID=<your_project_id>
```

Gemini を呼び出す部分を非同期、かつ、ストリーミング関数として定義します。非同期にしているのは、Gemini と Claude 3 の呼び出しをおおよそ並行して実行させるためです。呼び出しを非同期用の`generate_content_async`に変更しているのと、入力は PDF ありなしでベタに分岐させてます。結果は、Streamlit で定義した`col1`に書き出すようにしています。

```python
# init Vertex AI
PROJECT_ID = os.environ.get("PROJECT_ID")
REGION = "asia-northeast1"
vertexai.init(project=PROJECT_ID, location=REGION)

async def ask_gemini15pro():
    if uploaded_file:
        data = [document1, input]
    else:
        data = [input]
    with col1:
        fulltext = ""
        message_placeholder = st.empty()
        model = GenerativeModel("gemini-1.5-pro")
        responses = await model.generate_content_async(data, generation_config={"max_output_tokens": 2048, "temperature": 1.0}, stream=True)
        async for response in responses:
            fulltext += response.text
            message_placeholder.markdown(fulltext)

```
Gemini のコーディングはここらへんを参考にしました。
https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/send-multimodal-prompts?hl=ja


## Claude 3.5 Sonnet 呼び出しを追加

同様に非同期で Claude 3.5 Sonnet の呼び出す関数を追加します。PDF 添付ありの場合は、入力プロンプトの前に PDF からパースしたテキスト部分をバコッと入れています。プロンプトは改善の余地があります。結果は、Streamlit で定義した`col2`に書き出すようにしています。

```python
async def ask_claude():
    if uploaded_file:
        data = "Context:\n" + pdf_text + "\n=======\n" + input
    else:
        data = input
    with col2:
        fulltext = ""
        message_placeholder = st.empty()
        client = AsyncAnthropicVertex(region="us-east5", project_id=PROJECT_ID)
        async with client.messages.stream(model="claude-3-5-sonnet@20240620", max_tokens=2048, messages=[{"role": "user", "content": data, }]) as stream:
            async for text in stream.text_stream:
                fulltext += text
                message_placeholder.markdown(fulltext)
```

Claude 3 のコーディングは、ここらへんを参考にしました。
https://docs.anthropic.com/claude/reference/messages-streaming


## 非同期呼び出し部分を追加して完成

ボタンが押されたら、ほぼ同時に Gemini と Claude 3 を呼び出すようにします。

```python
async def ask_llms():
    tasks = []
    tasks.append(asyncio.create_task(ask_gemini15pro()))
    tasks.append(asyncio.create_task(ask_claude()))
    await asyncio.gather(*tasks)


if submitted:
    asyncio.run(ask_llms())
```

以上で、ソースは完成です。

## 動作確認

Streamlit の UI で動作確認しましょう。

今度は、Google Cloud への認証や IAM 権限が必要になります。

Google Cloud 環境外で実行する場合は、`gcloud auth login`しておくか、サービスアカウントキーを利用する必要があります。ユーザー or サービスアカウントには、`Vertex AI ユーザー`権限を付与しておきます。サービスアカウントキー利用の場合、キーファイルを発行して環境変数に通しておきましょう。
```
export GOOGLE_APPLICATION_CREDENTIALS=keyfile.json

```

コードの最終形はこちらです。
```python
import os
import tempfile
import streamlit as st
import vertexai
from vertexai.generative_models import GenerativeModel, Part
from anthropic import AnthropicVertex, AsyncAnthropicVertex
import asyncio
from pypdf import PdfReader

# layoout page
st.set_page_config(layout="wide")
st.title("Gemini vs Claude3")
with st.form('my_form'):
    uploaded_file = st.file_uploader("(option) PDF を入力する場合、Gemini は画像として、Claude はテキストとしてロードします。", type=["pdf"],)
    if uploaded_file:
        with st.spinner('Loading ...'):
            bytes_data = uploaded_file.getvalue()
            # for Gemini
            document1 = Part.from_data(data=bytes_data, mime_type="application/pdf")
            # for Anthropic
            with tempfile.TemporaryDirectory() as temp_dir:
                with open(os.path.join(temp_dir, uploaded_file.name), "wb") as temp_pdf:
                    temp_pdf.write(bytes_data)
                    reader = PdfReader(temp_pdf.name)
                    pdf_text = ''.join([page.extract_text() for page in reader.pages])

    input = st.text_area(label='Prompt', value='映画好きの人にお勧めの日本国内の名所を教えてください')
    submitted = st.form_submit_button('Submit')

col1, col2 = st.columns(2)
col1.subheader('Gemini 1.5 Pro')
col2.subheader('Claude 3.5 Sonnet')

# init Vertex AI
PROJECT_ID = os.environ.get("PROJECT_ID")
REGION = "asia-northeast1"
vertexai.init(project=PROJECT_ID, location=REGION)


async def ask_gemini15pro():
    if uploaded_file:
        data = [document1, input]
    else:
        data = [input]
    with col1:
        fulltext = ""
        message_placeholder = st.empty()
        model = GenerativeModel("gemini-1.5-pro-preview-0409")
        responses = await model.generate_content_async(data, generation_config={"max_output_tokens": 2048, "temperature": 1.0}, stream=True)
        async for response in responses:
            fulltext += response.text
            message_placeholder.markdown(fulltext)


async def ask_claude():
    if uploaded_file:
        data = "Context:\n" + pdf_text + "\n=======\n" + input
    else:
        data = input
    with col2:
        fulltext = ""
        message_placeholder = st.empty()
        client = AsyncAnthropicVertex(region="us-east5", project_id=PROJECT_ID)
        async with client.messages.stream(model="claude-3-5-sonnet@20240620", max_tokens=2048, messages=[{"role": "user", "content": data, }]) as stream:
            async for text in stream.text_stream:
                fulltext += text
                message_placeholder.markdown(fulltext)


async def ask_llms():
    tasks = []
    tasks.append(asyncio.create_task(ask_gemini15pro()))
    tasks.append(asyncio.create_task(ask_claude()))
    await asyncio.gather(*tasks)


if submitted:
    asyncio.run(ask_llms())
```
エラー処理とかほどんと入れていませんが 78 行で Gemini vs Claude 3 完成です。

## Cloud Run で動かす

せっかくなので Docker ファイルも用意して、Cloud Run 上でアプリをホストします。

```Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY . ./

RUN pip install --no-cache-dir -r requirements.txt

EXPOSE 8080
HEALTHCHECK CMD curl --fail http://localhost:8080/_stcore/health

ENTRYPOINT ["streamlit", "run", "main.py", "--server.port=8080", "--server.address=0.0.0.0"]

```

Cloud Run 用のサービスアカウントを作成し、`Vertex AI ユーザー`権限を付与しておきます。ちなみに、Google Cloud 環境上で動作させる場合はサービスアカウントキーのダウンロードは不要です。


以下のコマンドを、実行してコマンドでウィザードに答えて正常終了したら OK です。コンソールに出ている URL にアクセスして動作確認しましょう。Cloud Run 便利すぎや。
```
gcloud run deploy geminiclaude3 --source=. --region=asia-northeast1 --set-env-vars=PROJECT_ID=<your_project_id> --allow-unauthenticated --service-account=<your_serivice_account>
```

## 感想

Gemini 1.5 Pro は驚くほど優秀でびっくりしました。Gemini 1.5 Pro は、画像やグラフの読み取りなどもできているため、PDF の IR 資料の読み取りに使ったりしています。MATH にもかなり強くなった印象です。

Claude 3.5 Sonnet もかなり優秀ですが、Geimini 1.5 Pro と比較すると口下手な印象。Claude はここでのサンプルは手動でテキストをパースしているので、図表は読み取れません。