体感して覚えるリランキングモデル

9/19に開催された以下のウェビナーに参加しました。

https://pages.awscloud.com/eib-aiml-240919-reg.html

RAGの精度を向上させるテクニックが満載で、とても勉強になるウェビナーでした。

また、同日に解説ブログも公開されています。ウェビナーを見逃した方はこちらのブログを参照すると良いと思います。

https://aws.amazon.com/jp/blogs/news/a-practical-guide-to-improve-rag-systems-with-advanced-rag-on-aws/

改善テクニックの一つ「リランキング」が紹介されていましたが、

**「ベクトル検索で意味が近い文章を検索したあとになぜ並び替えるの？いい感じに検索できてるんじゃないの？並び替えて精度が改善されるってどういうこと？？」**

という疑問が残ったので、実際に試してみました。

## 環境構築

* **Embeddingsモデル：** Amazon Titan Embedding Text v2	(Amazon Bedrock)
* **Rerankingモデル：** Cohere Rerank 3 (Cohere API)
* **ベクトルDB：** Weaviate

:::note
本題とは関係ないのですが、Weaviateは「ベクトル化したデータを登録する」事もできますが、「文字列を渡すと裏側でベクトル化して保存する」を行ってくれます。（他にもOpenSearch Serviceもこの形態を取ることができます。）

個人的にこのアーキテクチャがお気に入りです。
:::

ライブラリーをインストールします

```shell:Shell
pip install weaviate-client boto3 beautifulsoup4 
```

### Weaviateの構築

Dockerで構築します。（[ドキュメント](https://weaviate.io/developers/weaviate/installation/docker-compose)）

<details><summary>docker-compose.yaml</summary>

```yaml:docker-compose.yaml
---
services:
  weaviate:
    command:
      - --host
      - 0.0.0.0
      - --port
      - '8080'
      - --scheme
      - http
    image: cr.weaviate.io/semitechnologies/weaviate:1.26.4
    ports:
      - 8080:8080
      - 50051:50051
    volumes:
      - weaviate_data:/var/lib/weaviate
    restart: on-failure:0
    environment:
      QUERY_DEFAULTS_LIMIT: 25
      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
      PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
      DEFAULT_VECTORIZER_MODULE: 'none'
      ENABLE_API_BASED_MODULES: 'true'
      CLUSTER_HOSTNAME: 'node1'
volumes:
  weaviate_data:
```

</details>

起動します

```shell:Shell
docker compose up
```

### 検証データの準備

[Amazon EC2のFAQ](https://aws.amazon.com/jp/ec2/faqs/)を取ってきました。

<details><summary>スクレイピング処理</summary>

```python
import json

import requests
from bs4 import BeautifulSoup

response = requests.get("https://aws.amazon.com/jp/ec2/faqs/")
soup = BeautifulSoup(response.text)

main = soup.find_all("div", class_="lb-txt-16 lb-rtxt")

text_list = []

for m in main:

    for p in m.children:
        text = p.text.strip()
        if len(text) > 0:
            text_list.append(text)
text_list


def convert_to_json(data):
    result = []
    current_question = None
    current_answer = []

    for item in data:
        if item.startswith("Q:"):
            if current_question:
                result.append(
                    {"question": current_question, "answer": " ".join(current_answer)}
                )
            current_question = item.strip()
            current_answer = []
        else:
            current_answer.append(item.strip())

    # 最後の質問と回答を追加
    if current_question:
        result.append(
            {"question": current_question, "answer": " ".join(current_answer)}
        )

    return result


faq_list = convert_to_json(text_list)
faq_list = list(filter(lambda x: len(x["answer"]) > 0, faq_list))

```

:::note
Claudeに助けてもらいました
:::

</details>

こんな感じのQとAの検証データを作成しました。

```text:faq_list
[{'question': 'Q: Amazon Elastic Compute Cloud (Amazon EC2) とは何ですか?',
  'answer': 'Amazon EC2 は、サイズ変更可能なコンピューティング性能をクラウド内で提供するウェブサービスです。また、ウェブスケールのコンピューティングをデベロッパーが簡単に利用できるよう設計されています。'},
 {'question': 'Q: Amazon EC2 で何ができますか?',
  'answer': 'Amazon Simple Storage Service (Amazon S3) がクラウド内のストレージを可能とするのとまったく同様に、Amazon EC2 は、クラウド内での「コンピューティング」を可能にします。\xa0 Amazon EC2 のシンプルなウェブサービスインターフェイスによって、手間をかけず、必要な機能を取得および設定できます。お客様のコンピューティングリソースに対して、高機能なコントロールが提供され、Amazon の実績あるインフラストラクチャ上で実行できます。Amazon EC2 では、わずか数分間で新規サーバーインスタンスを取得して起動できるようになります。これにより、コンピューティング要件の変化に合わせて、すばやく容量をスケールアップおよびスケールダウンできます。実際に使用した分のみ料金が発生するため、Amazon EC2 はコンピューティングの経済性も変革します。'},
 {'question': 'Q: どうすれば Amazon EC2 の使用を開始できますか?',
  'answer': 'Amazon EC2 にサインアップするには、Amazon EC2 詳細ページ上の [このウェブサービスにサインアップ] ボタンを選択します。このサービスにアクセスするには、AWS のアカウントを保有している必要があります。これをまだ持っていない場合は、Amazon EC2 サインアッププロセスの開始時に、プロンプト画面が表示されてこれを作成することができます。サインアップの後、Amazon EC2 ドキュメントをご参照ください。ここには開始方法に関するガイドが含まれています。'},
 {'question': 'Q: Amazon EC2 にサインアップする際、自分の電話番号の検証を求められるのはなぜですか?',
  'answer': 'Amazon EC2 登録には、有効な電話番号と E メールを AWS に提出することが必要です。これはお客様に連絡する必要が生じた場合のためです。電話番号の検証に要するのは数分のみです。登録プロセス中に電話を受信し、電話のキーパッドを使用して PIN 番号を入力してください。'},
 {'question': 'Q: デベロッパーが以前できなかったことで、現在できるようになったことは何ですか?',
  'answer': '小規模なデベロッパーには大規模なコンピューティングリソースを獲得する資本がないため、これまで、予期せぬ負荷の急上昇に対応するためのキャパシティーを確保できませんでした。Amazon EC2 は、すべての開発者が初期費用やパフォーマンスの妥協なしに、Amazon 独自の大規模なメリットを活用できるよう支援します。現在デベロッパーは、ビジネスがどれだけの成功を収めたとしても、そのビジネス要件を満たすのに必要なコンピューティング性能については、安価かつ簡単に確保できることを理解しています。 サービスの「柔軟な」特性により、デベロッパーはすぐに規模を拡張してトラフィックや需要の急上昇に対応することができます。必要となるシステムリソースが予期せず変更する (上下する) 場合でも、Amazon EC2 は即時に対応できます。つまりデベロッパーには、その時々で必要となるリソース数をコントロールする能力が与えられます。対照的に、従来のホスティングサービスは一般的に、固定された時間枠で、固定されたリソース数を提供します。つまり、利用量が急速に変化する、予想不能である、または様々な間隔で大きなピークを迎えることが知られている場合でも、ユーザーが簡単に対応できることはごくわずかです。'}]
```

### データの登録

weaviateの登録していきます。

1. クライアントを作成します

    AWSの認証情報とCohereのAPIキーをheadersに指定します。
    
    ```python
    import boto3
    import weaviate
    
    aws_credentials = boto3.Session().get_credentials().get_frozen_credentials()
    
    client = weaviate.connect_to_local(
        headers={
            "X-Aws-Access-Key": aws_credentials.access_key,
            "X-Aws-Secret-Key": aws_credentials.secret_key,
            "X-Aws-Session-Token": aws_credentials.token,
            "X-Cohere-Api-Key": "{CohereAPIキー}",
        },
    )
    
    print(client.is_ready())
    ```

1. コレクションを作成します

    questionとanswerの項目を定義します。
    NamedVectorという機能を使い、「question」と「answer」を別々にベクトル化します。
    reranker_configに、リランキングモデルを指定します。
    
    ```python
    from weaviate.classes.config import Configure, DataType, Property
    
    collection_name = "ec2_faq"
    
    client.collections.create(
        collection_name,
        properties=[
            Property(name="question", data_type=DataType.TEXT),
            Property(name="answer", data_type=DataType.TEXT),
        ],
        vectorizer_config=[
            Configure.NamedVectors.text2vec_aws(
                name="question_vector",
                region="us-east-1",
                model="amazon.titan-embed-text-v2:0",
                source_properties=["question"],
            ),
            Configure.NamedVectors.text2vec_aws(
                name="answer_vector",
                region="us-east-1",
                model="amazon.titan-embed-text-v2:0",
                source_properties=["answer"],
            ),
        ],
        reranker_config=Configure.Reranker.cohere(model="rerank-multilingual-v3.0"),
    )
    
    collection = client.collections.get("ec2_faq")
    ```

1. データを登録します

    ```python
    with collection.batch.dynamic() as batch:
        for data_row in faq_list:
            batch.add_object(
                properties=data_row,
            )
    ```

1. 登録できたか確認します

    1件取得します。

    ```python
    response = collection.query.fetch_objects(limit=1)
    
    for o in response.objects:
        print(o.properties["question"])
        print(o.properties["answer"])
    ```
    
    ```
    Q: ENA Express を有効にするにはどうすればよいですか?
    ENA Express は、ENI ごとに有効にすることができます。インスタンスへのネットワークカードのアタッチ中、または変更コマンドの実行中に、ENA Express を有効にすることができます。ポイントツーポイント通信を確立するには、通信する両方の ENI で ENA Express を有効にする必要があります。さらに、Jumbo Frames を使用している場合、ENA Express を使用するには、最大 MTU を 8900 に調整する必要があります。  Q: ENA Express はどのプロトコルをサポートしていますか? ENA Express はデフォルトで TCP をサポートします。UDP は、API 引数を介して、またはマネジメントコンソール内において、オプションで有効にすることができます。
    ```
    
    取得できました。

## 検証

それでは検証していきます。

:::note
検証データからランダムで抽出した、以下の質問を使って検証を行います。

```text
'Q: どのインスタンスタイプが拡張ネットワーキングをサポートしていますか?'
```
:::

### 検証１）質問文で質問文を検索する

まずは、質問文で質問文を検索します。

```python
from weaviate.classes.query import MetadataQuery

response = collection.query.near_text(
    query=question,
    target_vector="question_vector",    # 質問文をベクトルにした値を対象に検索
    limit=3,
    return_metadata=MetadataQuery(distance=True),
)

for o in response.objects:
    print(o.properties["question"])
    print(o.properties["answer"])
    print(o.metadata.distance)
    print("---")
```
```text
Q: どのインスタンスタイプが拡張ネットワーキングをサポートしていますか?
インスタンスタイプに応じて、次のいずれかのメカニズムを使用して、拡張ネットワーキングを有効にすることができます。 Intel 82599 Virtual Function (VF) インターフェイス – インテル 82599 Virtual Function インターフェイスでは、サポートされているインスタンスタイプについて最大 10 Gbps のネットワーク速度がサポートされています。C3、C4、D2、I2、M4 (m4.16xlarge を除く)、R3 インスタンスは、拡張ネットワーキングにインテル 82599 VF インターフェイスを使用します。 Elastic Network Adapter (ENA) - Elastic Network Adapter (ENA) は、サポート対象のインスタンスタイプのために最大 200 Gbps のネットワーク速度をサポートします。C4、D2、および m4.16xlarge より小さい M4 インスタンスを除き、現行世代としてリストされているインスタンスは、ネットワークを強化するために ENA を使用しています。  Q: EC2 インスタンスのために複数のネットワークカードを持つということは何を意味しますか? なぜ必要なのですか?  新しい世代の EC2 インスタンスは、VPC データプレーンのオフロードのために Nitro ネットワークカードを使用します。より高いネットワーク帯域幅と改善されたパケットレートパフォーマンスを提供するために、特定の EC2 インスタンスで複数のネットワークカードを使用してパケット処理を行うように設定できます。これにより、最終的にはシステム全体のパフォーマンスが向上します。  Q: どのインスタンスタイプで複数のネットワークカードがサポートされていますか?  p4d.24xlarge などの高速インスタンスや、c6in.32xlarge などのネットワーク最適化インスタンスでは、複数のネットワークカードがサポートされています。複数のネットワークカードをサポートするインスタンスの詳細なリストについては、Elastic ネットワークインターフェイスをご覧ください。  Q: 複数のカードインスタンスを起動できるネットワークインターフェイスの数はデフォルトでいくつですか?  これは、インスタンスタイプによって異なります。p4 などの高速化されたインスタンスは、ネットワークカードごとに最大 15 個のネットワークインターフェイスまでスケールアップできます。最近リリースされた c6in インスタンスなどの高ネットワークインスタンスは、2 つのネットワークカード間で均等に (7 と 7) 分割された 14 のネットワークインターフェイスの集約をサポートします。ネットワークカードごとのネットワークインターフェイスの規模については、ネットワークカードを参照してください。
0.12492597103118896
---
Q: サポートされているインスタンスで拡張ネットワーキングを有効にするにはどうすればよいですか?
この機能を有効にするには、適切なドライバーで HVM AMI を起動する必要があります。現行世代と記載されているインスタンスは、強化されたネットワークのために ENA を使用しています。Amazon Linux AMI には、デフォルトでこれら 2 つのドライバが含まれています。これらのドライバが含まれていない AMI については、使用を予定しているインスタンスタイプに応じて適切なドライバーをダウンロードしてインストールする必要があります。デフォルトで SR-IOV ドライバーを含まない AMI で拡張ネットワーキングを有効化するには、Linux または Windows の手順を使用できます。拡張ネットワーキングは Amazon VPC でのみサポートされます。
0.2510565519332886
---
Q: なぜ拡張ネットワーキングを使用するのですか?
秒あたりのパケット数での高いパフォーマンスや低いネットワークレイテンシーからメリットを得られるアプリケーションを使用している場合は、拡張ネットワーキングを利用することで、性能が大幅に上がり、一貫性と拡張性を得ることができます。
0.3970597982406616
---
```

検索に使った文字列と全く同じ文字列のため、当然「Q: どのインスタンスタイプが拡張ネットワーキングをサポートしていますか?」が先頭に見つかりました。


### 検証２）質問文で回答文を検索する

質問と回答がきれいに整理されていればいいのですが、RAGを構築する場合、PDFなどの文章をチャンクで分けて登録する形態が多いのではと思います。こういったケースでは「質問文を検索する」ではなく「回答文を検索する」のほうが実際のRAGの動作に近いのではないでしょうか？

```python
from weaviate.classes.query import MetadataQuery

response = collection.query.near_text(
    query=question,
    target_vector="answer_vector",    # 回答文をベクトルにした値を対象に検索
    limit=3,
    return_metadata=MetadataQuery(distance=True),
)

for o in response.objects:
    print(o.properties["question"])
    print(o.properties["answer"])
    print(o.metadata.distance)
    print("---")
```
```
Q: A1 インスタンスではどのネットワークインターフェイスがサポートされていますか?
A1 インスタンスは ENA ベースの拡張ネットワーキングをサポートしています。ENA の使用により、A1 インスタンスはプレースメントグループ内での起動時に、インスタンス間に最大 10 Gbps のネットワーク帯域幅を提供できます。
0.41409939527511597
---
Q: R6g インスタンスではどのネットワークインターフェイスがサポートされていますか?
R6g インスタンスは ENA ベースの拡張ネットワーキングをサポートしています。R6g インスタンスでは、ENA を使うことにより、同じプレイスメントグループ内で作成したインスタンス間で最大 25 Gbps のネットワーク帯域幅を実現できます。
0.4546385407447815
---
Q: M6g インスタンスではどのネットワークインターフェイスがサポートされていますか?
M6g インスタンスは ENA ベースの拡張ネットワーキングをサポートしています。M6g インスタンスでは、ENA を使うことにより、同じプレイスメントグループ内で作成したインスタンス間で最大 25 Gbps のネットワーク帯域幅を実現できます。
0.45842891931533813
---
```

この場合は残念ながら質問に対応する回答がヒットしませんでした。

:::note
検索件数を増やして確認したところ、「どのインスタンスタイプが拡張ネットワーキングをサポートしていますか?」がヒットしたのは16件目でした。
:::

このように、ベクトル検索では意味合いの近いものが検索されるのですが、質問に対する回答としては最適解でない場合があります。

### 検証３）質問文で回答文を検索した後、リランキングする

それではいよいよリランキングしてみます。

```python
from weaviate.classes.query import MetadataQuery, Rerank

response = collection.query.near_text(
    query=question,
    target_vector="answer_vector",
    limit=20,     # 20件取得
    return_metadata=MetadataQuery(distance=True),
    rerank=Rerank(prop="answer", query=question),
)

for o in response.objects[:3]:     # 3件のみを対象
    print(o.properties["question"])
    print(o.properties["answer"])
    print(o.metadata.distance)
    print(o.metadata.rerank_score)
    print("---")
```

:::note
weaviateの動作が、limitで指定した件数を取得した後、リランキングモデルで並び替えを行う動作でした。そのため、limit=3としてしまうと、そもそも取得できていないのでリランキングしても意味がありません。なので、20件取得して並び替えを行い、結果の上位3件を取得するようにしました。
:::


```
Q: どのインスタンスタイプが拡張ネットワーキングをサポートしていますか?
インスタンスタイプに応じて、次のいずれかのメカニズムを使用して、拡張ネットワーキングを有効にすることができます。 Intel 82599 Virtual Function (VF) インターフェイス – インテル 82599 Virtual Function インターフェイスでは、サポートされているインスタンスタイプについて最大 10 Gbps のネットワーク速度がサポートされています。C3、C4、D2、I2、M4 (m4.16xlarge を除く)、R3 インスタンスは、拡張ネットワーキングにインテル 82599 VF インターフェイスを使用します。 Elastic Network Adapter (ENA) - Elastic Network Adapter (ENA) は、サポート対象のインスタンスタイプのために最大 200 Gbps のネットワーク速度をサポートします。C4、D2、および m4.16xlarge より小さい M4 インスタンスを除き、現行世代としてリストされているインスタンスは、ネットワークを強化するために ENA を使用しています。  Q: EC2 インスタンスのために複数のネットワークカードを持つということは何を意味しますか? なぜ必要なのですか?  新しい世代の EC2 インスタンスは、VPC データプレーンのオフロードのために Nitro ネットワークカードを使用します。より高いネットワーク帯域幅と改善されたパケットレートパフォーマンスを提供するために、特定の EC2 インスタンスで複数のネットワークカードを使用してパケット処理を行うように設定できます。これにより、最終的にはシステム全体のパフォーマンスが向上します。  Q: どのインスタンスタイプで複数のネットワークカードがサポートされていますか?  p4d.24xlarge などの高速インスタンスや、c6in.32xlarge などのネットワーク最適化インスタンスでは、複数のネットワークカードがサポートされています。複数のネットワークカードをサポートするインスタンスの詳細なリストについては、Elastic ネットワークインターフェイスをご覧ください。  Q: 複数のカードインスタンスを起動できるネットワークインターフェイスの数はデフォルトでいくつですか?  これは、インスタンスタイプによって異なります。p4 などの高速化されたインスタンスは、ネットワークカードごとに最大 15 個のネットワークインターフェイスまでスケールアップできます。最近リリースされた c6in インスタンスなどの高ネットワークインスタンスは、2 つのネットワークカード間で均等に (7 と 7) 分割された 14 のネットワークインターフェイスの集約をサポートします。ネットワークカードごとのネットワークインターフェイスの規模については、ネットワークカードを参照してください。
0.586162805557251
0.9999937
---
Q: A1 インスタンスではどのネットワークインターフェイスがサポートされていますか?
A1 インスタンスは ENA ベースの拡張ネットワーキングをサポートしています。ENA の使用により、A1 インスタンスはプレースメントグループ内での起動時に、インスタンス間に最大 10 Gbps のネットワーク帯域幅を提供できます。
0.41409939527511597
0.99997735
---
Q: M6g インスタンスではどのネットワークインターフェイスがサポートされていますか?
M6g インスタンスは ENA ベースの拡張ネットワーキングをサポートしています。M6g インスタンスでは、ENA を使うことにより、同じプレイスメントグループ内で作成したインスタンス間で最大 25 Gbps のネットワーク帯域幅を実現できます。
0.45842891931533813
0.99996954
---
```

並び替えの結果、期待したデータが1番目になりました。やったね！リランキングが意味がありそうです。

### ちなみに

ちなみにですが、本日Anthropicが公開した記事では、 **「検索で150件取得し、リランキング後に上位20件に絞る」** というパターンで検証したところ、パフォーマンスが向上したとのことでした。

https://www.anthropic.com/news/contextual-retrieval

### ちなみに２

リランキングを行うと検索結果の上位に期待するデータが来るのは間違いないのですが、リランキングをしない場合（ベクトル検索だけの場合）でも、結構な確率で最上位に期待するデータが取得できました。
そのため、リランキングすると精度は良くなりそうですが、そもそも精度に不満がなければリランキングを導入しなくてもいいかもしれません。

試しに「検証２」の方法をランダムで選んだ10個の質問で行ったところ、検索結果上位3件に期待する回答が含まれたケースが **70%** ありました。（30%は上記3件に含まれませんでした）
同じ10個の質問で「検証３」の様に検索結果を20件取得しリランキングを実施し、上位3件に含まれるかを確認すると期待する回答が含まれるケースが **80%** に上昇しました。（これを効果大と見るかどうかは、判断が分かれる気がします）


## まとめ

私の理解はこうなりました。

* ベクトル検索では、「質問文と意味の近い文章を検索」する。そのため、回答として最適なものが抽出されるとは限らない（抽出されることもある）
* リランキングを行うことで、「質問に対する適した回答」の順に並び替えることができる。（どうしてそうなるかは謎ですが。。）
* 多めに検索結果を取って、リランキングで上位に絞る方法は一定の効果がありそう