BedrockとSagemaker Notebookでテキストの意味的近さを可視化する


# はじめに
去年くらいからベクトル検索が一層注目を浴びているように感じます。LLMの流行に引っ張られてRAG等の用途でも使われることになったのが大きいのでしょう。
筆者もEmbedding Modelを使ってベクトルDB検索を行うことが多々ありますが、検索精度に関してはnDCGやRecall@kなどの指標で定量評価を行うところまでなかなか至らないこともあります。
そんな重い腰を上げる前に、まずは傾向を把握することができる（かもしれない）方法として可視化を試してたいと思います。

AWSではAmazon Titan Embeddings G1-Textという埋め込みモデルがBedrockで利用可能なため、今回はこちらを用いてやってみます。また可視化ツールとしてはTensorBoard Projectorを用いますがそのホストにSagemaker Notebook Instanceを用います。

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/titan-embedding-models.html

https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/nbi.html


# 概要
### やること
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/8a88ffe1-d1f2-3411-d3a2-6d618feee7b2.png)



### 可視化のイメージ
![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/ab637bc1-b9ac-f7ba-c331-0c1ea16397f8.gif)



# 手順
### 事前作業：Bedrockを有効化
今回は埋め込みモデル Titan Embeddings G1 - Text を利用します。
当該のモデルがBedrockで利用可能になっていない場合は、Bedrockの「モデルアクセス」から有効化をしておいてください。また以下手順ではバージニアリージョンを前提として話を進めます。


### Sagemakerでノートブックインスタンスを起動する
まずは作業用のノートブックインスタンスを起動していきます。

Sagemakerにアクセスし、「ノートブック」⇒「ノートブックインスタンス」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/eb3d96ce-4764-cf2b-53d9-d80f084d0f79.png)

「ノートブックインスタンスの作成」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/00262614-6760-d4f4-bed1-ab118afe740a.png)

今回は動けばいいので、「ノートブックインスタンス名」だけ記入して他はデフォルト設定を利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/7eb0f7e7-f1bb-d798-986d-9c7906b42703.png)

IAMロールは、プルダウンから「新しいロールの作成」を選択し、表示された推奨設定で新規作成したものを選択しました。
その他の設定もデフォルトで「ノートブックインスタンスの作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/ff0a07fa-a7c3-1938-c79a-39face80fa87.png)

インスタンスの作成は完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/919bb30e-f91f-b14d-af2f-26abcaaf485d.png)


### IAMロールにBedrockのInvoke権限を追加する
先ほど作成されたIAMロールに対して、Bedrock基盤モデルへのアクセス権限を付与します。
今回はテスト用ですぐ消すのでインラインポリシーで権限を追加します。

IAMで先ほど作成されたロールを開きます。
「許可を追加」⇒「インラインポリシーを作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/e2586d78-63da-56e0-893b-e25fcecdda74.png)

サービスのプルダウンで「Bedrock」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/c9630322-901f-b860-8e2d-0a5d54599fb3.png)

今回は基盤モデルのTitanを叩くだけなので、アクション欄は`InvokeModel`だけ許可にします。
次にリソース欄の「foundation-model」の「ARNを追加」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/53c4a7e1-3edc-f04b-d84d-16910094a9ad.png)

リージョンは`任意のリージョン`、Resourceには`amazon.titan-embed-text-v1`を指定して、「ARNを追加」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/5e66af35-6dbb-a963-4e1a-3ab889c2fdbc.png)

設定が完了したので「次へ」をクリックします。

適当にポリシー名を設定して、「ポリシーの作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/5793ffad-3135-f612-009b-4711855b48f4.png)

IAMロールの設定は以上です。




### Jupyter LabからBedrockを呼び出し、テキストをベクトル化する
ここからいよいよ本題の作業です。

#### ノートブックの起動
先ほど作成したノートブックインスタンスの「Jupyter Labを開く」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/9ae016c6-585b-c73d-4ae4-afbff03257a9.png)


Jupyter Labの画面が開きました。ここではNotbook欄にある`conda_pytorch_p310`というノートブックを開きます。（ノートブックは何でもよいですが別のものを選択した場合はpytorchのインストールが必要になります）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/93cec5cb-86f1-ba13-5839-7db84811efe4.png)


#### ライブラリのインストール

以下を実行し、必要なライブラリをインストールします。（ログが見たいときは`--quiet`オプションは外してください）
```python
%pip install --upgrade --quiet  boto3 langchain tensorboard
```


#### 接続テスト
以下を実行することでBedrockのTitan Embeddingが利用できるか確認します。
今回はLangChainのBedrockEmbeddingsを使ってベクトル化しています。
```python
from langchain_community.embeddings import BedrockEmbeddings
embeddings = BedrockEmbeddings(region_name="us-east-1")

vec = embeddings.embed_documents(
    ["This is a content of the document", "This is another document"]
)
print(vec)
```

実行の結果、以下のように2つの入力テキストに対応する2つのベクトルが表示されれば接続テストはOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/a621e595-e724-aa9d-73b9-68994bb946f8.png)

ちなみにベクトルは1536次元です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/1cfe027b-ef30-15cc-4226-151bd26b88a3.png)


#### ベクトル化実行
接続テストが終わったので、今回用に作ったダミーデータをベクトル化していきます。
実際の作業では適宜、ご自身のデータに読み替えてください。

##### ダミーデータの説明
今回使うダミーデータを説明しておきます。
Wikipediaから果物・野菜・お菓子を数件ずつ参照して作ったデータです。
列は`title`、`description`からなっており、titleはWikipediaの記事名、descriptionはWikipedia記事の冒頭の節のみ抽出しています。titleはラベルに用い、descriptionがEmbeddingの対象です。
以下表が実データです。赤枠が果物、緑枠が野菜、青枠がお菓子となっています。（スイカとメロンは果物ということにしてください）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/7231ae51-5ef5-d169-efce-840e3e8f1fa6.png)
なおファイルフォーマットはTSVで、ファイル名は`data.tsv`です。

##### ダミーデータのベクトル化

ダミーデータファイル`data.tsv`をアップロードします。ローカルにあるファイルであれば、ドラッグ&ドロップでアップロードが可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/a5f996d9-a7c7-38fe-01ec-cc97c8974ba7.png)


以下のコードを実行します。ダミーデータを読み込み、Bedrockでベクトル化しています。
```python
import csv
# ダミーデータを読み込み
with open('data.tsv', encoding='utf-8') as fi:
    reader = csv.reader(fi,delimiter='\t')
    # ヘッダ行をスキップ
    next(reader)
    # データを変数に格納
    titles, descs = zip(*reader)
# Bedrockでベクトル化
embeddings = BedrockEmbeddings(region_name="us-east-1")
emb_data = embeddings.embed_documents(descs)
```

続けて以下のコードを実行します。TensorBoardのフォーマットでデータを出力します。実行すると、`./runs`というフォルダが作成されます。
```python
import numpy as np
from torch.utils.tensorboard import SummaryWriter
# tensorboard用に変換
writer = SummaryWriter()
writer.add_embedding(np.array(emb_data), metadata=titles)
writer.close()
```

#### TensorBoardの起動

続けて以下のコマンドでTensorBoardを起動します。
```bash
!tensorboard --host 0.0.0.0 --logdir ./runs
```

最終的にノートブックは以下のような状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/1bc894c2-b970-e934-9f4d-fa7ee62254ae.png)
ここで表示されるポート番号を確認しておきます。今回は`6007`です。

### TensorBoardの確認
#### TensorBoardへの接続
ブラウザでTensorBoardにアクセスします。
接続URLは`{ノートブックインスタンスのドメイン}/proxy/{ポート番号}/`です。
例えば、Jupyter LabのURLが
`https://test-xxxx.notebook.us-east-1.sagemaker.aws/lab/`
だった場合は
`https://test-xxxx.notebook.us-east-1.sagemaker.aws/proxy/6007/`
がTensorBoardのURLです。最後の`/`も忘れずに入力します。


URLを直接入力して接続すると以下のような画面が開きます。
右上のプルダウンで「PROJECTOR」に選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/3ea09c25-ff49-8da9-ef33-a02ac1b8291a.png)

TensorBoardのProjectorの画面が開き、プロットが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/67860c95-de68-2b10-5f0f-2235fd466f85.png)

#### TensorBoard Projectorの操作
Projectorを開くと、デフォルトではPCAで次元削減処理されたデータが3次元プロットで表示されています。次元削減アルゴリズムは画面左下のメニューから行うことができ、t-SNEなども選択可能です。

画面は左ドラッグすることでプロットの角度を変えたり、右ドラッグすることで全体を移動できます。
![grip.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/434502cf-bada-1930-4483-3fd3c9d802b3.gif)



選択ツールで点を範囲選択すると、データのラベルが表示されます。
![select.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/de803efe-952a-bcae-9bc8-e64974e4a055.gif)


#### プロットの確認
3次元のプロットのスクショなので少しわかりにくいですが、ぱっと見で似た属性のものが近傍に配置されていることが分かります。
お菓子群は青色枠周辺に、野菜群は緑色枠周辺、果物群は赤色周辺にまとまっています。今回は投入したデータ数が極めて少ないということもありますがきれいに各文章の特徴で分けることが出来ているのが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/d2de59f5-3f67-e66c-e85a-75cedd6ecbc0.png)

また別の視点から見ると「スイカ・キュウリ・メロンが比較的近い=ウリ科共通？」「タケノコがちゃんとたけのこの里やきのこの山と近い」「トマトとかぼちゃ、りんごとパイナップルがそれぞれ近い」などが見て取れました。
Embeddingならでは感はあまり出ませんでしたが、意味的に近いものがきちんと近くに配置されている感じはしますね。


ちなみに、いずれかのデータ点を選択すると右ペインに距離が近いもの順で上から表示され、プロットの色も距離によってグラデーションになります。
以下の画像では「スイカ」を選択した結果、「キュウリ」「メロン」「トマト」の順に上から表示されています。距離はユークリッド距離とコサイン類似度が表示可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115707/8f371711-438c-229e-732e-6675dedabec6.png)


というわけで、Sagemaker NotebookでTensorBoardを使ってベクトルデータの可視化をすることができました。


# 補足

### 可視化情報は信用できるのか？
この記事の根底を覆すような話ですが、ベクトルを3次元表示したところ類似テキストが近傍にあったからといって、実際にベクトルサーチをした際にそのテキスト同士で上位に検索できる保証はありません。
初期の傾向把握に使うのには適していると思いますが、当然ながらこれで「分析完了」とはならない点は注意が必要です。元のベクトルデータ1536次元を3次元に次元圧縮している都合、情報量としてはかなり落ちていますので、あくまで傾向を見るものとして捉える必要があります。
そもそも表現力をあげるために次元数を増やしているのに次元削減して可視化したところでナンセンスというのもあると思います。使い処を間違えないように気を付けましょう。

### 内なる声「ローカルでやればいいじゃん」
正直なところJupyterもTensorBoardも「ローカルでやればいいじゃん」と言われればそれまでなのですが（私自身そうしている）、色々な制約でAWSを使う必要があるときには参考にできるかと思います。
例えば、「プライベートサブネットにあるベクトルDBのデータをちょっと分析しておいて～」と上司に言われたときなんかには、さくっとノートブックインスタンスをVPC内に立ててベクトルデータをダンプして表示することが出来ます。
また、Sagemaker Studio Classicでも同じような手順で実現可能なので、複数人で共有しながらやりたい場合にはローカルでやるより高効率な可能性があります。
といった感じで、使うときは使うし、使わないときは使わないね、というレベルの話でした。


# おわりに
Sagemaker Notebook InstanceでTensorBoard使ってる人が地味にすくなそうだったので記事を書いてみました。Sagemaker Notebook, Bedrock(Titan Embedding), TensorBoardはそれぞれを使うのは非常に楽なので割と低カロリーにできるのではないかなと思います。ベクトルデータの可視化が必要になった際はぜひ使ってみてほしいです。
Notebook Instanceでの`/proxy/~~~`によるアクセスは、結構便利です。解析結果をstreamlitなどの簡単サーバで立ち上げてブラウザアクセスしてもらうとか、TensorBoadと同じくモニタリング系ツールを立ち上げるとか、色々と用途がありますのでこちらも使えるときに使ってみてください。