Amazon Bedrockで使えるモデルの日本語OCR能力を検証しました

以前、「Claude.aiをつかって画像内の文字を正確に抽出する方法を見つけました」という記事を投稿し、かなりたくさんの方に見ていただきました。ありがとうございます。m(_ _)m

https://qiita.com/moritalous/items/f5afd052992afa40d524

さて、Amazon Bedrockで画像を入力にできるモデルが増えてきたので、「日本語性能はどんなものかな？」と思って、検証してみました。


## 検証条件

以下のページで公開されているPDFを使います。

https://www.soumu.go.jp/use_the_internet_wisely/special/generativeai/

具体的には、P.36を画像（PNG）にしたものをインプットとして使用することにしました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/3820674f-cb0d-487f-87d7-022413a3ddc8.png)

この画像を、 **「添付画像をHTMLで再現してください。HTMLコードのみを出力してください。説明や追加のテキストは含めないでください。」** というプロンプトととに送信しました。

生成AIの回答は、ほとんどが`<html>`で始まり`</html>`で終わる感じでしたが、一部そうでない文字も含まれていたので、そこは正規表現などを駆使して整形します。（整形するコードはAmazon Q Developer CLIが作ってくれました）

<details><summary>検証コード</summary>

```python
import boto3
import os
import json
import re
from datetime import datetime

# Bedrockのランタイムクライアントを初期化（us-east-1リージョンを使用）
client = boto3.client("bedrock-runtime", region_name="us-east-1")

# 対象モデルのリスト（クロスリージョン推論プロファイルを使用）
models = [
    "us.anthropic.claude-3-7-sonnet-20250219-v1:0",  # Claude 3.7 Sonnet
    "us.anthropic.claude-3-5-sonnet-20240620-v1:0",  # Claude 3.5 Sonnet
    "us.anthropic.claude-3-haiku-20240307-v1:0",     # Claude 3 Haiku
    "us.amazon.nova-pro-v1:0",                       # Amazon Nova Pro
    "us.amazon.nova-lite-v1:0",                      # Amazon Nova Lite
    "us.meta.llama4-scout-17b-instruct-v1:0",        # Llama 4 Scout 17B Instruct
    "us.meta.llama4-maverick-17b-instruct-v1:0",     # Llama 4 Maverick 17B Instruct
    "us.mistral.pixtral-large-2502-v1:0"             # Pixtral Large (25.02)
]

# 画像ファイルを読み込む
def read_image(image_path):
    with open(image_path, "rb") as f:
        image_bytes = f.read()
    return image_bytes

# HTMLコードのみを抽出する関数
def extract_html_code(text):
    # ```html と ``` で囲まれたコードを抽出
    html_pattern = r"```html\s*([\s\S]*?)\s*```"
    match = re.search(html_pattern, text)
    
    if match:
        return match.group(1)
    
    # <html> タグを含むコードを抽出（```で囲まれていない場合）
    html_tag_pattern = r"<!DOCTYPE html>[\s\S]*?<\/html>"
    match = re.search(html_tag_pattern, text)
    
    if match:
        return match.group(0)
    
    # 上記のパターンに一致しない場合は元のテキストを返す
    return text

# 結果を保存するディレクトリを作成
results_dir = "results"
os.makedirs(results_dir, exist_ok=True)

# 現在の日時を取得してファイル名に使用
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

# 画像ファイルのパス
image_path = "image.png"
image_bytes = read_image(image_path)
image_format = image_path.split(".")[-1]

# 改善されたプロンプト
prompt_text = "添付画像をHTMLで再現してください。HTMLコードのみを出力してください。説明や追加のテキストは含めないでください。"

# 各モデルに対して実行
for model_id in models:
    print(f"Processing model: {model_id}")
    
    try:
        # メッセージを構築
        message = {
            "role": "user",
            "content": [
                {
                    "text": prompt_text
                },
                {
                    "image": {
                        "format": image_format,
                        "source": {
                            "bytes": image_bytes
                        }
                    }
                }
            ]
        }
        
        # Converseリクエストを実行
        response = client.converse(
            modelId=model_id,
            messages=[message]
        )
        
        # レスポンスを取得
        output_message = response['output']['message']
        
        # レスポンステキストを安全に取得
        response_text = ""
        for content_block in output_message.get('content', []):
            if 'text' in content_block:
                response_text += content_block['text']
        
        # HTMLコードのみを抽出
        html_code = extract_html_code(response_text)
        
        # モデル名からファイル名を生成（スラッシュやコロンを置換）
        model_name = model_id.replace(".", "-").replace(":", "-").replace("/", "-")
        base_filename = f"{timestamp}_{model_name}"
        json_file = f"{results_dir}/{base_filename}.json"
        html_file = f"{results_dir}/{base_filename}.html"
        
        # 結果をJSONファイルに保存
        with open(json_file, "w", encoding="utf-8") as f:
            json.dump({
                "model_id": model_id,
                "prompt": prompt_text,
                "response": output_message,
                "html_code": html_code,
                "usage": response.get("usage", {}),
                "stop_reason": response.get("stopReason", "")
            }, f, ensure_ascii=False, indent=2)
        
        # HTMLコードを別ファイルに保存
        with open(html_file, "w", encoding="utf-8") as f:
            f.write(html_code)
        
        print(f"Response saved to {json_file}")
        print(f"HTML saved to {html_file}")
        
    except Exception as e:
        print(f"Error with model {model_id}: {str(e)}")

print("Processing complete!")
```

</details>

## 検証対象モデル

今回はAmazon Bedrockで使用でき、画像の入力に対応しているものから、以下のモデルを対象としました。

- Amazon Nova
    - Nova Premier
    - Nova Pro
    - Nova Lite
- Anthropic Claude
    - Claude 3.5 Sonnet (V1)
    - Claude 3.7 Sonnet
    - Claude 3 Haiku（Claude 3.5 Haikuは画像に未対応）
- Meta Llama
    - Llama 4 Maverick
    - Llama 4 Scout


それでは早速検証結果を見ていきましょう！

## 検証結果

### Nova Premier

パッと見、あっているようで、自治は全く違う内容です。。

![127.0.0.1_3000_results_20250430_235339_us-amazon-nova-premier-v1-0.html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/9899814f-8455-4c46-bb96-9fdfcf8602f7.png)


### Nova Pro

全く関係のない内容になってしまいました。。。

![127.0.0.1_3000_results_20250430_064721_us-amazon-nova-pro-v1-0.html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/254bb37d-e45a-48a2-93da-0a168b503f47.png)


### Nova Lite

何なんだこれは。。気象情報です。全く関係ない。。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/e99c3399-1954-4aa3-8e77-2f984114ebc4.png)


:::note
補足しておくと、Novaモデルのマルチモーダル機能は、マルチリンガルの対応していない旨がドキュメントに明記されています。

https://docs.aws.amazon.com/nova/latest/userguide/modalities-image-limitations.html

> Multilingual Image Understanding: The models have limited understanding of multilingual images and video frames and can struggle or hallucinate on similar tasks.

まぁ、知ってたうえでチャレンジしたのですがw
:::

### Claude 3.5 Sonnet

抽出した文字は完璧です。　ただ、左の「西日本で...」と「XXX駅...」が上下の関係があるものの、順番がそうなってないですね。

![127.0.0.1_3000_results_20250430_064721_us-anthropic-claude-3-5-sonnet-20240620-v1-0.html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/083cabb6-b52b-4870-ad1d-5c5742c3190d.png)

### Claude 3.7 Sonnet

改行も忠実に再現。`<br>`タグが入ってました。

![127.0.0.1_3000_results_20250430_064721_us-anthropic-claude-3-7-sonnet-20250219-v1-0.html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/da50149a-a27b-483f-8cbd-72fc8badddfc.png)

### Claude 3 Haiku

レイアウトは全く無視。情報も欠落してるのと、微妙に言い回しが変わっている部分もあります。

![127.0.0.1_3000_results_20250430_064721_us-anthropic-claude-3-haiku-20240307-v1-0.html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/3c8df751-2f2a-408b-877b-2762ab13c0db.png)
### Llama 4 Maverick

ご情報の事例①の右半分が事例②と間違ったタイトルが付いています。また、本来の事例②は事例②として出力されています。一番左上の「事例」もないですね。

![127.0.0.1_3000_results_20250430_064721_us-meta-llama4-maverick-17b-instruct-v1-0.html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/1ad52e86-de0f-4064-a9a5-2f0ad1faa27f.png)
### Llama 4 Scout

ScoutはMaverickと異なり見た目はあまり再現されていません。ただ、事例①の右側が事例②になる現象は同じでした。一番左上の「事例」もない点も同じです。あと、事例②であるべきところが事例になってます。


![127.0.0.1_3000_results_20250430_064721_us-meta-llama4-scout-17b-instruct-v1-0.html (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/d5b46f13-7736-48bf-95f2-bff2f4932c9e.png)



### Pixtral Large (25.02)

ところどころ読み取り誤りがありますね。日本語は期待してはいけなさそうです。

![127.0.0.1_3000_results_20250430_064721_us-mistral-pixtral-large-2502-v1-0.html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/a68edd7c-7f98-47db-bce1-2f230f12967e.png)


## まとめ

画像からの文字列抽出を行う場合は、Claude 3.7 SonnetまたはClaude 3.5 Sonnetが最も良さそうでした。Llama 4は対応言語に日本語が含まれていませんが、なかなか良い結果でした。

1000トークンあたりの価格もやすいので、検討の価値がありそうです。

| モデル名 | 入力 | 出力 |
| --- | --: | --: |
| Claude 3.7 Sonnet | $0.003	| $0.015	|
| Claude 3.5 Sonnet | $0.003	| $0.015	|
| Llama 4 Maverick 17B | $0.00024 | $0.00097 |
| Llama 4 Scout 17B	 | $0.00017	 | $0.00066	 |

```mermaid
xychart-beta
title "Bedrock価格比較（折れ線：入力、棒：出力）"
x-axis "モデル名" ["Claude 3.7 Sonnet", "Claude 3.5 Sonnet", "Llama 4 Maverick 17B", "Llama 4 Scout 17B"]

y-axis "$/1,000 token"

bar [0.015, 0.015,0.00097,  0.00066]
line [0.003, 0.003,0.0002,  0.00017]

```