ゼロから始めるAIシステム開発 #04 「入門書ハンズオン（2章）」

## Bedrockを実際に試してみよう
こちらの入門書、2章後半のハンズオンをやったレポートです。

**Amazon Bedrock 生成AIアプリ開発入門 ［AWS深掘りガイド］**
https://amzn.asia/d/co7MB5S
![2240915.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/644c78f6-022e-6115-34f4-de9085a66320.jpeg)
書ではCloud9環境を使用しているが現在ではサービス終了しているようなので、代替としてAmazon SageMakerを使用した。この代替も若干手間取ったが割愛。プログラム言語はPythonを使用。
## モデル一覧を出力
前提として
- AWSのSDKを使用し各モデルのAPIへリクエストする
- ここではPython用のSDK（Boto3）を用いる

BedrockのAPIは4種類
- bedrock
- bedrock-runtime
- bedrock-agent
- bedrogk-agent-runtime

どれも軽く説明は書いてあるがよくわからない。どうやら基礎機能と応用機能の2種類があって、どちらにも管理用と実行用の2種類があり2×2で4つということらしい。いずれわかる時がくるか。
モデル出力のコードは以下の通り。
```python:1_list-model.py
import boto3
bedrock = boto3.client("bedrock")
result = bedrock.list_foundation_models()
print(result)
```
コメントは省略してます（ものぐさ）
流れとしては
- Boto3をインポート
- Bedrockクライアントの作成
- モデル一覧を出力するAPI呼び出し
- 結果表示

「インポート」「クライアント」「呼び出し」「リクエスト」やらの概念（の微妙な差）がわかるようなわからんような。入門書も↑のコードも同じように色が分かれてるので意味があるのはなんとなくわかる。
コンソールに表示した結果は長いので割愛。
## テキスト生成してみる
```python:2_invoke-model.py
import json
import boto3

bedrock_runtime = boto3.client("bedrock-runtime")

body = json.dumps(
    {
        "anthropic_version":"bedrock-2023-05-31",
        "max_tokens":1000,
        "messages":[
            {
                "role":"user",
                "content":"Bedrockってどういう意味？",
            }
        ],
    }
)

modelId = "anthropic.claude-3-sonnet-20240229-v1:0"

accept = "application/json"
contentType = "application/json"

response = bedrock_runtime.invoke_model(body=body,modelId=modelId,accept=accept,contentType=contentType)
response_body = json.loads(response.get("body").read())
answer = response_body["content"][0]["text"]

print(answer)
```
一気に難しくなった。流れは
- JSONとBoto3をインポート
- Bedrockクライアント作成（推論を行うAPIを指定）
- APIへ送信するデータの内容をJSON形式で記述
- Bedrockで使う基盤モデルのIDを定義、ここではClaud3Sonnetを選択
- Bedrock APIへ送信するHTTPリクエストのヘッダーを定義
- 作成したBedrockクライアントからinvoke_model関数を呼び出してモデルへリクエストを送信、結果を変数responseへ格納、そこからテキストのみ抽出
- 結果を出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/16a89f6a-3e2d-bde2-6e8e-55c1dcf1f393.png)

JSON形式やらHTTPリクエストやらの解説がないので後で調べる。全体の流れは理解。
## ストリーミングでテキスト生成
LLMの推論処理は時間がかかるのでストリーミング形式で少しずつレスポンスを返すことができる。
```python:3_steaming.py
import json
import boto3

bedrock_runtime = boto3.client("bedrock-runtime")

body = json.dumps(
    {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens":1000,
        "messages":[
            {"role": "user","content": [{"type": "text","text": "いろは歌を教えて"}]}
        ],
    }
)

modelId = "anthropic.claude-3-sonnet-20240229-v1:0"

response = bedrock_runtime.invoke_model_with_response_stream(body=body,modelId=modelId)

for event in response.get("body"):
    chunk = json.loads(event["chunk"]["bytes"])
    if(
        chunk["type"] == "content_block_delta"
        and chunk["delta"]["type"] == "text_delta"
    ):
        print(chunk["delta"]["text"], end="")
print()
```
- modelIdあたりまではこれまでと同じ
- Bedrockクライアントからストリーミング用の関数を呼び出しresponseに格納
- レスポンスの各チャンクを取得しテキスト部分だけを出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/62b7945e-5605-6c16-0521-e56953604a18.png)

こちらもチャンクからの取り出しの部分あたりはよく分からないが全体の流れは理解。
## マルチモーダル入力でテキスト生成
画像とテキストを入力してテキスト生成する。画像はAWSのコンソール画面のスクショを使う。
```python:4_multimodal.py
import base64
import json
import boto3

bedrock_runtime = boto3.client("bedrock-runtime")

with open("image.png","rb") as image_file:
    image_data = base64.b64encode(image_file.read()).decode("utf-8")

prompt_config = {
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens":4096,
    "messages":[
        {
            "role":"user",
            "content":[
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/png",
                        "data": image_data,
                    },
                },
                {"type": "text","text": "この画像は何？日本語で説明して"},
            ],
        }
    ],
}

body = json.dumps(prompt_config)
modelId = "anthropic.claude-3-sonnet-20240229-v1:0"
accept = "application/json"
contentType = "application/json"

response = bedrock_runtime.invoke_model(body=body,modelId=modelId,accept=accept,contentType=contentType)
response_body = json.loads(response.get("body").read())
result = response_body.get("content")[0].get("text")

print(result)
```
- 大まかな部分はこれまでと同じ
- 画像を文字列に変換するBase64というライブラリをインポート
- with文で画像ファイルをバイナリモードで開き、画像データをBase64という方式でエンコードしてUTF-8文字列に変換
- プロンプト定義部分は画像とテキストの両方を指定
- 以降は基本的にテキスト生成のときと同じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3885923/6a01e7b4-31bd-3252-9a31-8b1f093ecfd4.png)
## 用語
- JSON形式：人間が読み書きしやすいテキスト形式でデータを表現するための軽量なデータ交換フォーマット。シンプルで軽量、言語非依存でPython以外でもサポートされている
- HTTPリクエスト：Webサービスなどの情報をやり取りするプロトコル。メソッド、URL、ヘッダー、ボディなどから構成されている
- チャンク：まとまったデータの塊、処理の単位を指す
- UTF-8文字列：文字をコンピュータで処理するためのエンコード方式の一つ
## 感想
Pythonの書式についての解説はないのでコードの細部はわからないが、Bedrockをどのように使用するのかについてはかなり詳細に解説がしてあり分かりやすかった。
Pythonコードの入力ミスで起こるエラーについては自力のデバッグが難しそうなのでGeminiにエラーメッセージをぶん投げて早期解決。なんて便利なんだ。ただこれを初心者のうちに覚えるのはプログラミング力の育成においては難があるような気がしなくもない。

次回は入門書の三章か、ボスからの課題でCloudformationについて調べたことを書く予定。