自然言語のベクトル化とベクトルDB

## はじめに
コンピューターが自然言語を比較し、類似性や特徴などを計算する場合、文章を数値化して処理を行います。具体的には、ベクトル化 (Embedding) することで、非構造化データの類似度も計算できるようになります。

Yellowfin には、[AI搭載の自然言語クエリ (NLQ) で質問する](https://wiki.yellowfin.co.jp/space/yfcurrent/108822835/AI%E6%90%AD%E8%BC%89%E3%81%AENLQ%E3%81%A7%E8%B3%AA%E5%95%8F%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B)機能が搭載されています。同機能は、OpenAI の ChatGPT と連携して自然言語を処理していますが、ChatGPT などの生成 AI が自然言語を取り扱うためには、データのベクトル化技術が欠かせません。

本記事では、文章のベクトル化とベクトル DB に関して、簡単に記述したいと思います。

## ベクトル
ベクトルとは向きと大きさを持った量のことを意味します。
以下のイメージでは、A 点と B 点は、それぞれ異なる向きと大きさを持っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/f5a59811-0593-4f90-ab24-19875693c9a8.png)

ベクトル化された文章や単語は、2 点間の距離や方向などを計算することで、類似度を計算することができます。
ベクトル間の類似度を算出する代表的な方法としては、以下の ３ つが挙げられます。

|手法|説明|
|---|---|
|ユークリッド距離|ベクトルの距離|
|コサイン類似度|ベクトルの方向の類似度|
|内積|ベクトルの方向と大きさの積|

文章や画像の類似性を図る場合、コサイン類似度を用いることが一般的です。
上記以外にも、縦横の距離の合計で測るマンハッタン距離など、様々な手法があり、用途によって使い分けが必要です。

## 実践
実際に文章をベクトル化して、類似性を測ってみたいと思います。

文章や画像をベクトル化できる深層学習モデル Sentence Transformer を使って、Python でベクトル化の処理を行います。そのため、まずは Sentence Transformer のインストールを行います。
```powershell:インストール
pip install sentence_transformers
```
Sentence Transformerには様々なモデルが実装されていますが、この中からマルチ言語に対応可能な distiluse-base-multilingual-cased-v2 を使用します。
モデル一覧に関しては、下記リンクをご参照ください。

https://www.sbert.net/docs/sentence_transformer/pretrained_models.html

上記モデルと採用して、文章をベクトル化するサンプルコードです。

```python:ベクトル化
from sentence_transformers import SentenceTransformer

#モデルの読み込み
model = SentenceTransformer('sentence-transformers/distiluse-base-multilingual-cased-v2')

#ベクトル化する文章
sentence = ['これはペンです']

#出力内容の確認
embedding = model.encode(sentence)
print(embedding.shape)

#ベクトルの表示
print(embedding)
```

上記を実行すると、下記がコンソールに表示され、512 次元のベクトルが 1 つ出力されたことが確認できます。
```powershell:出力内容の確認
(1, 512)
```

続いて、512 次元のベクトル値が表示されます。これが、文章をベクトル化したデータです。

```json:ベクトル値
[[ 6.71954229e-02  2.75416337e-02  3.10154893e-02  3.42289940e-03
  -4.85351831e-02  7.06919879e-02 -3.40428241e-02  2.51202807e-02

   ...
   
   3.03332713e-02  4.38156258e-03  1.30295400e-02  3.87246497e-02
   2.78264540e-03 -8.79379362e-03  3.05766687e-02  3.49475369e-02]]
```

今度は複数の文章を読み込んで、それぞれの類似性を算出します。
類似性を図る手法として、コサイン類似度を用います。コサイン類似度とは、２つのベクトルがなす角のコサイン値のことで、-1～1 の範囲に正規化されます。-1なら反対向きのベクトルで「完全に似ていない」、1 なら同じ向きのベクトルで「完全に似ている」ことになります。
コサイン類似度で算出した結果を、ヒートマップに表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/04ae3795-c555-44c3-8f9c-937307a84caa.png)
 
```python:コサイン類似度
import numpy as np
import seaborn as sns
import matplotlib.pylab as plt
from sklearn.metrics.pairwise import cosine_similarity
from sentence_transformers import SentenceTransformer

#モデルの読み込み
model = SentenceTransformer('sentence-transformers/distiluse-base-multilingual-cased-v2')

#比較する複数文章を配列に格納
sentences = [
    "This is a pen",
    "これはぺんです",
    "That is a penguin",
    "あれはペンギンです",
    "I love The Penguin Band",
    "私はペンギンバンドの大ファンです",
]

#文章をベクトル化してコサイン類似度で比較
embedding = model.encode(sentences)
comparison = cosine_similarity(embedding)

#比較した結果をヒートマップで表示
ax = sns.heatmap(comparison,  annot=True, cmap="Reds")
plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/fa34c665-1308-42c8-bd34-cd4fc855b544.png) 

ヒートマップで類似性を確認する限り、日本語と英語の類似性は高い様子です。
「That is a penguin」と「あれはペンギンです」は 0.89 と高い類似性を見せています。
他方「This is a pen」 と 「これはペンです」の類似性だけ低い結果が出ています。[荒井注](https://ja.wikipedia.org/wiki/%E8%8D%92%E4%BA%95%E6%B3%A8)がご存命であれば、この事実をどのように捉えるでしょうか。

## ベクトル DB
最後に、ベクトル DB に関して記述します。
世の中には、pinecone などの無料で利用できるベクトル DB がいくつかあります。クラウド上のサービスなので、自分で環境構築する必要がなく、お手軽に利用できます。私も pinecone を活用していますが、このようなサービスが無料で利用できるのもすごいことですね。

一方、本記事の中では、Windows OS 上で稼働する PostgreSQL をベクトル DB として活用する手段を取り扱います。
PostgreSQL に pgvector を適用することで、PostgreSQL をベクトル DB として利用できるようになります。

#### 事前準備
後述の手順を実行する上で、Build Tools for Visual Studio の x64 Native Tools Command Prompt と、Git のインストールが必要となります。
それぞれのインストール手順は、下記を参考にさせていただきました。

* Build Tools for Visual Studio - x64 Native Tools Command Prompt

https://qiita.com/matskeng/items/43988856501ca94526e4

* Git

https://qiita.com/takeru-hirai/items/4fbe6593d42f9a844b1c

#### インストールとビルド
x64 Native Tools Command Prompt を管理者権限で開き、下記コマンドを順に実行します。

```powershell:root 設定
set "PGROOT=C:\Program Files\PostgreSQL\16"
```

```powershell:一時フォルダに移動
cd %TEMP%
```

```powershell:Git クローン
git clone --branch v0.7.1 https://github.com/pgvector/pgvector.git
```

```powershell:pgvector フォルダに移動
cd pgvector
```

```powershell:nmake インストール
nmake /F Makefile.win
```

```powershell:nmake インストール
nmake /F Makefile.win install
```

psql を開き、対象となるデータベースに接続したのち、下記コマンドを実行します。

```sql:create extension
demo-# CREATE EXTENSION vector;
```

ここまでの手順は、下記を参考にさせていただきました。

https://qiita.com/tasonaso/items/13366afb14ee1ebafff8

#### データ格納
ベクトルデータを格納するテーブルを作成します。
pgvector を有効にすると、vector 型が使えるようになります。カッコ内で指定する数値は次元数で、先のプログラムの出力に合わせて 512 を指定しています。

```sql:create table
CREATE TABLE embedding (sentence varchar(100), embedding vector(512));
```

自然言語を sentence 列に、ベクトルデータを vector 列にそれぞれ登録します。
insert で登録する場合は以下の構文です。データ量が大きいときは、import した方が良いかと思います。
```sql:insert into
insert into embedding values ('I love The Penguin Band', '[1.50379296e-02, ... ,-5.78176863e-02]'
```

:::note info
psql のプロンプトから insert を実行しようとすると、文字数制限に引っかかって大変だったため、この処理は PgAdmin から実施しました。
:::

#### 検索
格納したデータから類似性の高いデータを、[最近傍検索]()で検索します。
pgvector には、ベクトル用に以下の演算子が準備されています。

|演算子|説明|
|---|---|
|<->|ユークリッド距離|
|<=>|コサイン類似度|
|<#>|内積に-1を乗算|
|+|加算|
|–|減算|
|*|乗算|

この中から、コサイン類似度を使って、「これはペンですか？」に類似度の高い上位 2 文章を検索します。
そのためのクエリが以下になります。
order by に指定したベクトルは「これはペンですか？」をベクトル化したデータです。

```sql:検索
SELECT * FROM public.embedding
order by embedding <=>
'[5.72404340e-02, ... , 3.56294364e-02]'
LIMIT 2;
```

クエリは下記の結果を返しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128512/957c5d9b-1b48-45ad-bfe4-c76d7b8dfa36.png)

見事、ベクトル化されたデータを使って、自然言語の類似性検索を実行することができました。めでたし。

## 最後に
文章をベクトル化し、ベクトル化したデータをベクトル DB に格納して活用するまでの流れを説明しました。
今後、類似の内容をさらに深堀して、記事にできればと思います。

では皆様、良いデータ分析を!!

## 参考情報

https://atmarkit.itmedia.co.jp/ait/articles/2112/08/news020.html

https://tech.aru-zakki.com/sentence-transformers/

https://www.sraoss.co.jp/tech-blog/pgsql/pgvector-intro/