はじめに
AWS の生成 AI サービス Bedrock を Livebook から呼び出します
2023 年 12 月にもやっていますが、今回は Claude 3.5 Sonnet に画像を与え、内容を説明してもらいます
実装したノートブックはこちら
事前準備
以下の記事を参考に、 AWS コンソールから基盤モデルのアクセス要求を出しておきましょう
また、 AWS にアクセスするための IAM ユーザー、認証情報が必要です
セットアップ
必要なモジュールをインストールします
以前はまだ AWS Elixir の Bedrock 対応版がリリースされていませんでしたが、 2024 年 8 月現在は対応済です
Mix.install([
{:aws, "~> 1.0"},
{:hackney, "~> 1.20"},
{:kino, "~> 0.13"}
])
シークレットの登録
AWS の認証情報をシークレットに登録します
- ACCESS_KEY_ID
- SECRET_ACCESS_KEY
クライアントの作成
AWS の API にアクセスするためのクライアントを作成します
シークレットに登録した値には LB_
を先頭につけて環境変数としてアクセスします
リージョンは適宜変更してください
client =
AWS.Client.create(
System.get_env("LB_ACCESS_KEY_ID"),
System.get_env("LB_SECRET_ACCESS_KEY"),
"us-east-1"
)
基盤モデル一覧の確認
AWS.Bedrock.list_foundation_models
で基盤モデルの一覧が取得できます
Kino.DataTable.new
でテーブル表示してみましょう
models =
client
|> AWS.Bedrock.list_foundation_models()
|> elem(1)
|> Map.get("modelSummaries")
keys = [
"modelId",
"modelName",
"providerName",
"inputModalities",
"outputModalities"
]
Kino.DataTable.new(models, keys: keys)
実行結果
確かに Claude 3.5 Sonnet が存在します
Claude 3.5 Sonnet の呼び出し
Claude 1.0 は AWS.BedrockRuntime.invoke_model
で呼び出せていましたが、 Claude 3.5 Sonnet では以下のようなエラーが発生します
"claude-3-5-sonnet-20240620" is not supported on this API. Please use the Messages API instead.
invoke_model
の代わりに converse
を使いましょう
input = "Elixirで5の階乗を計算するコードを教えてください。"
{:ok, body, _response} =
client
|> AWS.BedrockRuntime.converse(
model_id_claude,
%{
"messages" => [%{
"role" => "user",
"content" => [%{"text" => input}]
}]
},
recv_timeout: 60_000
)
実行結果
{:ok,
%{
"metrics" => %{"latencyMs" => 10208},
"output" => %{
"message" => %{
"content" => [
%{
"text" => "Elixirで5の階乗を計算するコードをいくつかの方法で示します:\n\n1. 再帰を使用する方法:\n\n```elixir\ndefmodule Factorial do\n def of(0), do: 1\n def of(n) when n > 0, do: n * of(n - 1)\nend\n\nIO.puts Factorial.of(5) # 120\n```\n\n2. Enumモジュールを使用する方法:\n\n```elixir\nfactorial = fn n ->\n 1..n |> Enum.reduce(&*/2)\nend\n\nIO.puts factorial.(5) # 120\n```\n\n3. リスト内包表記を使用する方法:\n\n```elixir\nfactorial = fn n ->\n for i <- 1..n, reduce: 1, do: (acc -> acc * i)\nend\n\nIO.puts factorial.(5) # 120\n```\n\n4. パイプ演算子を使用する方法:\n\n```elixir\nfactorial = fn n ->\n 1..n\n |> Enum.to_list()\n |> Enum.reduce(&*/2)\nend\n\nIO.puts factorial.(5) # 120\n```\n\nこれらの方法はすべて5の階乗(5!)を計算し、結果として120を出力します。再帰を使用する方法が最も一般的で、数学的な定義に近いアプローチです。他の方法は、Elixirの異なる機能を示しています。\n\n実際の使用では、パフォーマンスや可読性を考慮して、適切な方法を選択してください。小さな数の階乗を計算する場合は、これらの方法の間で大きな性能差はありません。"
}
],
"role" => "assistant"
}
},
"stopReason" => "end_turn",
"usage" => %{"inputTokens" => 29, "outputTokens" => 463, "totalTokens" => 492}
},
%{
body: "{\"metrics\":{\"latencyMs\":10208},\"output\":{\"message\":{\"content\":[{\"text\":\"Elixirで5の階乗を計算するコードをいくつかの方法で示します:\\n\\n1. 再帰を使用する方法:\\n\\n```elixir\\ndefmodule Factorial do\\n def of(0), do: 1\\n def of(n) when n > 0, do: n * of(n - 1)\\nend\\n\\nIO.puts Factorial.of(5) # 120\\n```\\n\\n2. Enumモジュールを使用する方法:\\n\\n```elixir\\nfactorial = fn n ->\\n 1..n |> Enum.reduce(&*/2)\\nend\\n\\nIO.puts factorial.(5) # 120\\n```\\n\\n3. リスト内包表記を使用する方法:\\n\\n```elixir\\nfactorial = fn n ->\\n for i <- 1..n, reduce: 1, do: (acc -> acc * i)\\nend\\n\\nIO.puts factorial.(5) # 120\\n```\\n\\n4. パイプ演算子を使用する方法:\\n\\n```elixir\\nfactorial = fn n ->\\n 1..n\\n |> Enum.to_list()\\n |> Enum.reduce(&*/2)\\nend\\n\\nIO.puts factorial.(5) # 120\\n```\\n\\nこれらの方法はすべて5の階乗(5!)を計算し、結果として120を出力します。再帰を使用する方法が最も一般的で、数学的な定義に近いアプローチです。他の方法は、Elixirの異なる機能を示しています。\\n\\n実際の使用では、パフォーマンスや可読性を考慮して、適切な方法を選択してください。小さな数の階乗を計算する場合は、これらの方法の間で大きな性能差はありません。\"}],\"role\":\"assistant\"}},\"stopReason\":\"end_turn\",\"usage\":{\"inputTokens\":29,\"outputTokens\":463,\"totalTokens\":492}}",
headers: [
{"Date", "Thu, 01 Aug 2024 13:48:39 GMT"},
{"Content-Type", "application/json"},
{"Content-Length", "1474"},
{"Connection", "keep-alive"},
{"x-amzn-RequestId", "0bbc2381-ca9b-4a8d-9c9c-94600010ea28"}
],
status_code: 200
}}
生成されたテキストを Markdown として解釈しましょう
body
|> Map.get("output")
|> Map.get("message")
|> Map.get("content")
|> Enum.at(0)
|> Map.get("text")
|> Kino.Markdown.new()
実行結果
非常に多くのパターンを生成してくれました
参考として、 Claude 1.0 の生成結果は以下のとおり
画像の説明
Claude に画像の説明をしてもらいましょう
任意の画像をバイナリとして読み込みます
image = File.read!("/home/livebook/vix/puppies.png")
Claude を呼び出す関数を用意します
invoke_claude = fn content ->
client
|> AWS.BedrockRuntime.converse(
model_id_claude,
%{
"messages" => [%{
"role" => "user",
"content" => content
}]
},
recv_timeout: 60_000
)
|> elem(1)
|> Map.get("output")
|> Map.get("message")
|> Map.get("content")
|> Enum.at(0)
|> Map.get("text")
end
画像のバイナリデータを Base64 エンコードして Bedrock に送ります
画像と生成結果を並べて表示してみましょう
result =
invoke_claude.([
%{
"image" => %{
"format" => "png",
"source" => %{"bytes" => Base.encode64(image)}
}
},
%{"text"=> "画像に写っているものを説明してください"},
])
[
image,
Kino.Markdown.new(result)
]
|> Kino.Layout.grid(columns: 2)
実行結果
かなり詳細で正確な説明ができています
フォームから選択した画像の説明
画像説明用の関数を用意します
describe_image = fn image ->
invoke_claude.([
%{
"image" => %{
"format" => "png",
"source" => %{"bytes" => Base.encode64(image)}
}
},
%{"text"=> "画像に写っているものを説明してください"},
])
end
入出力の UI とフォーム送信時の処理を用意します
画像入力に format: :png
を指定しているのが肝です
指定が漏れていると画像として処理できません
# 入力用フォーム
form =
Kino.Control.form(
[
image: Kino.Input.image("IMAGE", format: :png)
],
submit: "Submit"
)
# 出力用フレーム
frame = Kino.Frame.new()
# フォーム送信時の処理
Kino.listen(form, fn event ->
image =
event.data.image.file_ref
|> Kino.Input.file_path()
|> File.read!()
result = describe_image.(image)
result
|> Kino.Markdown.new()
|> then(&Kino.Frame.render(frame, &1))
end)
# 入出力を並べて表示
Kino.Layout.grid([form, frame], columns: 2)
実行結果
左側の入力フォームで画像を選択し、 "Submit" ボタンをクリックすると、画像の説明が右側に表示されます
アプリ化
以下の記事で紹介しているように、簡単にアプリ化することも可能です
まとめ
Claude 3.5 Sonnet を使うことで、かなり性能の高い AI アプリが実装できそうですね