JMED-LLM を Amazon Bedrock ( Claude 3.5 Sonnet, Claude 3 Haiku ) で評価してみた

## 結果
今回の検証結果です。
お時間ある方は続きも読んでいただけると幸いです。
| Model | JMMLU-Med | MRNER-disease | MRNER-medicine | NRNER | CRADE | RRTNM | SMDIS | JCSTS | Average |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| Claude 3.5 Sonnet (n=1) | 0.72(0.79) | 0.63(0.21) | 0.40(0.23) | 0.48(0.28) | 0.57(0.55) | 0.76(0.83) | 0.74(0.87) | 0.51(0.36) | 0.60(0.51)  |
| Claude 3 Haiku (n=3) | 0.57(0.68) | 0.67(0.25) | 0.33(0.18) | 0.52(0.28) | 0.22(0.34) | 0.51(0.64) | 0.30(0.65) | 0.10(0.20) | 0.40(0.40)  |


# はじめに
みなさん、生成 AI のモデル評価をどのようにされていますでしょうか？

組織によって解きたい課題は異なるため、組織の状況に適した評価指標を設定することは、生成 AI が上手く機能しているかを判断する上では非常に重要になります。

一方で、これから生成 AI の利用を検討していく段階にある場合は、汎用的な評価やベンチマーク等の情報は非常に有用です。
# JMED-LLM とは
日本語の言語モデルの評価においては、[LLM-jp](https://llm-jp.nii.ac.jp/) から [日本語LLM評価ベンチマーク/データセットまとめ](https://github.com/llm-jp/awesome-japanese-llm?tab=readme-ov-file#benchmark-suites) が公開されており、情報が集約されています(非常にありがたいですね・・)。

しかし、これまでヘルスケア業界を対象とした日本語の評価ベンチマークは確立されていませんでした。これは医療データが機微な情報であり、取り扱いが難しいというのが要因として考えられます。

この度、2024年7月に 奈良先端科学技術大学院大学ソーシャル・コンピューティング研究室から [JMED-LLM (Japanese Medical Evaluation Dataset for Large Language Models)](https://github.com/sociocom/JMED-LLM) として日本語の医療分野における大規模言語モデルの評価用データセットを公開いただきました。

https://speakerdeck.com/fta98/jmed-llm-ri-ben-yu-yi-liao-llmping-jia-detasetutonogong-kai

中身は、医療のドメインを対象として、質問応答、固有表現抽出、文書分類、文類似度の4つタスクでデータセットと評価用スクリプトが公開されています。執筆時点は各データセットで100件ずつ公開されているようでした。各データセットのライセンスが明記されているのも嬉しいポイントですね。今後も継続的なデータセットの拡充を予定されているとのことでぜひ期待したいです！

JMED-LLM のおかげで、ヘルスケア分野でも最新の言語モデルの評価が非常に行いやすくなりました。今回は Amazon Bedrock を利用し Claude 3.5 Sonnet/Claude 3 Haiku で検証スクリプトを実行したいと思います！

# Amazon Bedrock Claude

[Claude(クロード)](https://www.anthropic.com/claude) とは、OpenAI からスピアウトしたメンバーが創業した Anthropic 社が開発する生成 AI の基盤モデルです。Anthropic では倫理と安全性を重視しており、AI の透明性と説明責任に注力しているのがポイントです。そのため、[研究成果](https://www.anthropic.com/research)も積極的に公開されています。

[<img src = "https://www.anthropic.com/_next/image?url=https%3A%2F%2Fwww-cdn.anthropic.com%2Fimages%2F4zrzovbb%2Fwebsite%2Fcf2c754458e9102b7334731fb18a965bfeb7ad08-2200x1894.png&w=3840&q=75">](https://www.anthropic.com/news/claude-3-5-sonnet)

[Claude 3.5 Sonnet のリリースアナウンス](https://www.anthropic.com/news/claude-3-5-sonnet)では、上表のように英語のベンチマークで高い性能を示された結果が報告されています。

\
AWS の提供する [Amazon Bedrock](https://aws.amazon.com/jp/bedrock/) では生成 AI の基盤モデルを呼び出す API が提供されています。Bedrock を利用することで Claude などの他にも生成 AI を開発する各社が公開する最新の基盤モデルへ容易にアクセスができます。
また、AWS では開発者自身が利用するリージョンを選択することができるため、東京リージョンを指定して Bedrock を利用することで、生成 AI の利用を国内のデータセンターに限定し利用することもできます。

そして、この夏に東京リージョンでも Anthropic の最新モデルの Claude 3.5 Sonnet/Claude 3 Haiku が利用できるようになりましたので、今回はこちらから試していきたいと思います！

https://aws.amazon.com/jp/about-aws/whats-new/2024/08/claude-3-5-sonnet-haiku-more-regions/

# JMED-LLM で Claude を評価する準備

JMED-LLM で評価スクリプトが公開されているため、こちらをベースに拡張していきたいと思います。AWS のアカウント取得や、端末での IAM 認証情報の設定、マネジメントコンソールでの Bedrock のモデルアクセスの設定などは解説記事が多いためこちらでは割愛します。

まずはソースコードを clone し、必要なライブラリをインストールします。
```
$ git clone https://github.com/sociocom/JMED-LLM.git
$ poetry add boto3 
```
[Boto3](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) は AWS SDK for Python の通称で、SDK(Software Development Kit)を使うことで Python コードから Bedrock が呼び出しやすくなります。

ソースコードを一部修正します。
```diff_python:src/evaluator.py
import json
from pathlib import Path

import openai
import pandas as pd
import torch
+ import boto3
from sklearn.metrics import accuracy_score, cohen_kappa_score
from tqdm import tqdm
from transformers import AutoModelForCausalLM, AutoTokenizer

from src.utils import (exact_f1_score, get_evaluation_messages,
                       get_first_uppercase_alphabet, get_list_from_string,
                       num_openai_tokens, partial_f1_score, set_seed)


def evaluate(cfg):
    set_seed(cfg.seed)
    if cfg.model_type == "huggingface":
        tokenizer = AutoTokenizer.from_pretrained(cfg.pretrained_model_name_or_path, trust_remote_code=cfg.trust_remote_code)
        if cfg.custom_chat_template:
            tokenizer.chat_template = cfg.custom_chat_template
        model = AutoModelForCausalLM.from_pretrained(cfg.pretrained_model_name_or_path, device_map="auto", trust_remote_code=cfg.trust_remote_code)
        model.eval()
    elif cfg.model_type == "openai":
        client = openai.OpenAI(api_key=cfg.openai_api_key)
+    elif cfg.model_type == "bedrock":
+        client = boto3.client("bedrock-runtime")
+        inferenceConfig = {
+            "topP": cfg.generator_kwargs["top_p"],
+            "maxTokens": cfg.max_new_tokens
+        }

    with torch.inference_mode():
        output_data = {}
        output_data["model_name"] = cfg.pretrained_model_name_or_path
        if cfg.model_type == "huggingface":
            output_data["chat_template"] = tokenizer.chat_template
        output_data["generator_kwargs"] = cfg.generator_kwargs
        for task_name in tqdm(cfg.task_names, desc="Processing tasks"):
            dataset_path = Path(cfg.dataset_dir).joinpath(f"{task_name}.csv")
            dataset = pd.read_csv(dataset_path)
            answer_list = dataset["answer"].tolist()
            if task_name in ["jmmlu_med", "crade", "rrtnm", "smdis", "jcsts"]:
                dataset["options"] = dataset.filter(regex="option[A-F]").apply(lambda x: x.dropna().tolist(), axis=1)
            
            task_data = {}
            response_results = []
            for row in tqdm(dataset.itertuples(), desc=f"Processing {task_name} dataset", total=len(dataset)):
                messages = get_evaluation_messages(row, task_name=task_name, use_system_role=cfg.use_system_role)
                if cfg.model_type == "huggingface":
                    input_ids = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt").to(model.device)
                    output_ids = model.generate(input_ids, max_new_tokens=cfg.max_new_tokens, **cfg.generator_kwargs)
                    response = tokenizer.decode(output_ids[0][input_ids.shape[1]:], skip_special_tokens=True)
                elif cfg.model_type == "openai":
                    input_token_len = num_openai_tokens(messages, model=cfg.pretrained_model_name_or_path)
                    max_tokens = input_token_len + cfg.max_new_tokens
                    response = client.chat.completions.create(
                        model=cfg.pretrained_model_name_or_path,
                        messages=messages,
                        seed=cfg.seed,
                        max_tokens=max_tokens,
                        **cfg.generator_kwargs
                    ).choices[0].message.content
+                elif cfg.model_type == "bedrock":
+                    system = [{"text":message["content"] for message in messages if message["role"] == "system"}]
+                    messages = [{"role":"user", "content": [{"text" : message["content"] for message in messages if message["role"] == "user"}]}]
+                    response = client.converse(
+                        modelId = cfg.pretrained_model_name_or_path,
+                        inferenceConfig = inferenceConfig,
+                        system = system,
+                        messages = messages
+                    )["output"]["message"]["content"][0]["text"]
                response_results.append(response)

            if task_name in ["jmmlu_med", "crade", "rrtnm", "smdis", "jcsts"]:
                predict_results = [get_first_uppercase_alphabet(predict) for predict in response_results]
                if task_name in ["jmmlu_med", "crade", "rrtnm", "smdis", "jcsts"]:
                    score = accuracy_score(answer_list, predict_results)
                    task_data["accuracy"] = score
                if task_name in ["jmmlu_med", "rrtnm", "smdis"]:
                    score = cohen_kappa_score(answer_list, predict_results)
                    task_data["kappa"] = score
                if task_name in ["crade", "jcsts"]:
                    score = cohen_kappa_score(answer_list, predict_results, weights="linear")
                    task_data["kappa"] = score
            elif task_name in ["mrner_disease", "mrner_medicine", "nrner"]:
                answer_list = [get_list_from_string(answer) for answer in answer_list]
                predict_results = [get_list_from_string(predict) for predict in response_results]
                if task_name in ["mrner_disease", "mrner_medicine", "nrner"]:
                    score = exact_f1_score(answer_list, predict_results)
                    task_data["exact_f1"] = score
                if task_name in ["mrner_disease", "mrner_medicine", "nrner"]:
                    score = partial_f1_score(answer_list, predict_results)
                    task_data["partial_f1"] = score

            
            task_data["answer"] = answer_list
            task_data["generated_text"] = response_results
            task_data["predict"] = predict_results
            output_data[task_name] = task_data
            
        output_path = Path(cfg.output_dir).joinpath(f"{cfg.save_file_name}.json")
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(output_data, f, ensure_ascii=False, indent=2)

```

モデルの呼び出しには Bedrock の [Converse API](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime/client/converse.html) を利用しています。上記では、呼び出し時のシステムプロンプトやメッセージの形式を書き換えています。

Converse API では、異なるモデル提供会社の基盤モデルを同じ記法で呼び出すことができるため開発者はより最新のモデルにアプローチしやすくなります。執筆時点での [サポートされているモデル](https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/conversation-inference.html#conversation-inference-supported-models-features)には Claude3 の他にも Meta の Llama 3.1 や Mistral AI の Mistral Large 2 (24.07)などのモデルがあります。

```diff_conf:configs/config_template.yaml
task_names: ["jmmlu_med", "crade", "rrtnm", "smdis", "mrner_disease", "mrner_medicine", "nrner", "jcsts"]
seed: 42
max_new_tokens: 512
trust_remote_code: False
- model_type: "huggingface" # "huggingface", "openai", "ollama"
+ model_type: "bedrock" # "huggingface", "openai", "ollama", "bedrock"
openai_api_key: False
- pretrained_model_name_or_path: "llm-jp/llm-jp-13b-instruct-full-dolly-ichikara_004_001_single-oasst-oasst2-v2.0"
+ pretrained_model_name_or_path: "anthropic.claude-3-5-sonnet-20240620-v1:0"
- save_file_name: "llm-jp-13b-instruct-full-dolly-ichikara_004_001_single-oasst-oasst2-v2.0"
+ save_file_name: "claude-3-5-sonnet-20240620-v1-0"
dataset_dir: "datasets"
output_dir: "results"
use_system_role: True
custom_chat_template: False # "{% for message in messages %}{% if message['role'] == 'user' %}{{ '\\n\\n### 指示:\\n' + message['content'] }}{% elif message['role'] == 'system' %}{{ '以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい。' }}{% elif message['role'] == 'assistant' %}{{ '\\n\\n### 応答:\\n' + message['content'] + eos_token }}{% endif %}{% if loop.last and add_generation_prompt %}{{ '\\n\\n### 応答:\\n' }}{% endif %}{% endfor %}"
generator_kwargs:
  top_p: 1.0
  # do_sample: False
  # repetition_penalty: 1.0
```

config の中で呼び出すモデルIDを指定します。利用可能なモデルIDは [Bedrock ユーザーガイド](https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/model-ids.html#model-ids-arns)から確認できます。

修正は以上になります。以降は JMED-LLM の README の [How to evaluate](https://github.com/sociocom/JMED-LLM/blob/main/README.md#how-to-evaluate) の手順に従いプログラムを実行します。

# 評価結果

執筆時点では、Claude の利用時にseed値を指定することができません。評価結果について、多少上下することが予想されるため Claude 3 Haiku では 3回評価を実行した結果の平均を記載します(平均を取るのが良いのかという議論はありますが、今回はわかりやすさのためご容赦ください)。

それでは、評価結果です。
*参考までにその他のモデルの評価結果もJMED-LLMより転記します。

| Model | JMMLU-Med | MRNER-disease | MRNER-medicine | NRNER | CRADE | RRTNM | SMDIS | JCSTS | Average |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| Claude 3.5 Sonnet (n=1) | 0.72(0.79) | 0.63(0.21) | 0.40(0.23) | 0.48(0.28) | 0.57(0.55) | 0.76(0.83) | 0.74(0.87) | 0.51(0.36) | 0.60(0.51)  |
| Claude 3 Haiku (n=3) | 0.57(0.68) | 0.67(0.25) | 0.33(0.18) | 0.52(0.28) | 0.22(0.34) | 0.51(0.64) | 0.30(0.65) | 0.10(0.20) | 0.40(0.40)  |
|gpt-4o-2024-08-06|0.82(0.87)|0.54(0.15)|0.42(0.26)|0.39(0.20)|0.54(0.53)|0.85(0.90)|0.76(0.88)|0.60(0.48)|0.61(0.53)|
|gpt-4o-mini-2024-07-18|0.77(0.83)|0.48(0.13)|0.52(0.32)|0.48(0.25)|0.21(0.37)|0.58(0.71)|0.56(0.78)|0.57(0.51)|0.52(0.48)|
|google/gemma-2-9b-it|0.52(0.64)|0.61(0.16)|0.65(0.42)|0.53(0.30)|0.33(0.42)|0.54(0.68)|0.62(0.81)|0.16(0.24)|0.49(0.46)|
|elyza/Llama-3-ELYZA-JP-8B|0.34(0.51)|0.83(0.31)|0.51(0.31)|0.47(0.26)|0.01(0.26)|0.29(0.52)|0.54(0.77)|0.02(0.18)|0.38(0.39)|
|meta-llama/Meta-Llama-3.1-8B-Instruct|0.31(0.49)|0.82(0.30)|0.54(0.32)|0.36(0.18)|0.11(0.32)|0.41(0.57)|0.28(0.64)|0.13(0.23)|0.37(0.38)|
|meta-llama/Meta-Llama-3-8B-Instruct|0.42(0.57)|0.60(0.20)|0.44(0.25)|0.41(0.21)|0.00(0.25)|0.37(0.54)|0.43(0.72)|0.16(0.24)|0.36(0.37)|
|tokyotech-llm/Llama-3-Swallow-8B-Instruct-v0.1|0.33(0.50)|0.56(0.17)|0.37(0.21)|0.42(0.24)|0.31(0.37)|0.33(0.55)|0.26(0.63)|0.01(0.17)|0.32(0.35)|
|Qwen/Qwen2-7B-Instruc|0.42(0.57)|0.24(0.06)|0.29(0.14)|0.33(0.17)|0.11(0.29)|0.31(0.53)|0.33(0.67)|0.37(0.31)|0.30(0.34)|
|google/gemma-2-2b-it|0.17(0.38)|0.66(0.20)|0.46(0.23)|0.46(0.26)|0.00(0.25)|0.24(0.43)|0.14(0.57)|0.12(0.21)|0.28(0.31)|
|rinna/llama-3-youko-8b-instruct|0.31(0.49)|0.02(0.00)|0.05(0.02)|0.11(0.07)|0.02(0.28)|0.28(0.47)|0.50(0.75)|0.01(0.20)|0.16(0.28)|


Claude 3 Haiku も比較的精度良く、Claude 3.5 Sonnet については多くのタスクで高い性能を示しました。

タスクによりモデルの得意不得意があるように見られますので、これらの情報はあくまでも参考に、組織にあったモデルを選んでいきたいですね。

## セキュリティについて
いくら精度の高いモデルもセキュリティ的な懸念がある場合は本番環境での利用を検討することは難しいです。では、Amazon Bedrock ではどうでしょうか。

以下は Bedrock の [FAQ (よくある質問) のセキュリティ](https://aws.amazon.com/jp/bedrock/faqs/#product-faqs#bedrock-faqs#security-and-privacy)に関する質問と回答の引用です。

>Q. AWS とサードパーティーのモデルプロバイダーは、Amazon Bedrock への顧客の入力または Amazon Bedrock からの出力を使用して、Amazon Titan またはサードパーティーのモデルをトレーニングすることはありますか?
A. いいえ。AWS およびサードパーティーのモデルプロバイダーは、Amazon Bedrock への入力または Amazon Bedrock からの出力を使用して Amazon Titan またはサードパーティーのモデルをトレーニングすることはありません。

>Q. ユーザー入力とモデル出力はサードパーティのモデルプロバイダーが利用できるようになっていますか?
A. いいえ。ユーザーの入力とモデル出力は、どのモデルプロバイダーとも共有されません。


上述されているように、AWS とサードパーティのモデルプロバイダーつまりここでは Bedrock を介して利用できる Anthoropic がお客様のデータを利用し、自社のモデルのトレーニングに利用することはありません。また、入力データが　AWS からデータをモデルプロバイダーに共有されることはありません。
これは Bedorck もまた [AWS の責任共有モデル](https://aws.amazon.com/jp/compliance/shared-responsibility-model/)に基づいてサービス提供されており、クラウド内で管理されるデータは利用者自身の統制範囲であることがわかります。

データの取り扱いの他にもクラウドまでのアクセス経路がセキュアであるかも確認する必要があります。いわゆる3省2ガイドラインに該当する厚生労働省の[医療情報システムの安全管理に関するガイドライン](https://www.mhlw.go.jp/stf/shingi/0000516275_00006.html)では医療情報システムにおける遵守事項などがまとめられており、 TLS 相互認証 (mutual TLS: mTLS)を入れ認証および暗号化を適切に行なった上でのインターネット経由のアクセス方法などに言及されています。一方で、従来からの閉域での接続方式に限定されている医療機関も多くあるかと思います。

Bedrock は 利用者専用のネットワーク空間となる VPC からVPC エンドポイントを用意し、エンドポイント経由での直接アクセスを可能とする [PrivateLink に対応](https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/usingVPC.html)しています。そのため、アーキテクチャ図で示すと以下のような構成を取ることができます。これにより、国内で利用したいというニーズと、インターネットに出ることなく生成 AI を利用したいという要件のどちらも実現することが可能となっています。
![bedrock-secure-access.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323460/b8ebaa03-bce2-5d04-0348-e3f72f4434ca.png)

# まとめ

本記事では Amazon Bedrock で Claude 3.5 Sonnet と Claude 3 Haiku のモデルを利用し、医療分野における大規模言語モデルの評価用データセットである JMED-LLM を評価しました。

高性能な最新の生成 AI にアクセスしやすいというのもクラウドならではのメリットですが、実際の医療の現場では利用用途によっては、朝の診察の時間や、昼食明けの時間などの特定の時間に生成 AI への問い合わせが増えることなども想定されます。GPU のリソース状況やリクエストに応じた計算資源の増強などを考えなくて良いのもメリットの1つになりそうですね。

皆さんの今後の検証や検討の参考にしていただければ幸いです。


## 補足
Claude 3 Haiku 各試行での実行結果
結果は切り捨て

| Model | JMMLU-Med | MRNER-disease | MRNER-medicine | NRNER | CRADE | RRTNM | SMDIS | JCSTS |
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| Claude 3 Haiku (1回目) | 0.61(0.71) | 0.69(0.26) | 0.33(0.18) | 0.50(0.29) | 0.23(0.35) | 0.52(0.66) | 0.26(0.63) | 0.10(0.22) |
| Claude 3 Haiku (2回目) | 0.54(0.66) | 0.65(0.23) | 0.33(0.18) | 0.54(0.29) | 0.21(0.34) | 0.48(0.63) | 0.32(0.66) | 0.11(0.23) |
| Claude 3 Haiku (3回目) | 057(0.68) | 0.69(0.27) | 0.33(0.18) | 0.52(0.26) | 0.22(0.33) | 0.51(0.65) | 0.32(0.66) | 0.09(0.17) |
| Claude 3 Haiku (Average n=3) | 0.57(0.68) | 0.67(0.25) | 0.33(0.18) | 0.52(0.28) | 0.22(0.34) | 0.51(0.64) | 0.30(0.65) | 0.10(0.20) | 0.40(0.40)  |