【AzureOpenAI】Embedding(埋め込み化)で熊本のおいしいお店を探してみた

# はじめに
今回はEmbeddings（埋め込み化）をAzure OpenAIで行ってみます。

まずは、OpenAIのEmbeddingサンプルにある『fine-food reviews』を試しに使ってみます。
最後にこのサンプルを参考に、けんさむさんの熊本紹介記事を埋め込み化して熊本の行きたいお店を探してみようと思います。

# 環境
+ OS Windows 10(NVIDIA GTX 1650Ti,16GB RAM, i5-10300H CPU)
+ Visual Studio Code 1.73.1
+ Python 3.9

# 埋め込み化とは
テキストを埋め込み化すると、そのテキストの関連性を測定するというものです。
埋め込みAPIを使うことで、関連性の高い文字列の**検索**ができるほか、類似したテキストをグループ化する**クラスタリング**や、逆に関連性の低いものを検知する**異常検知**などもできます。

# レビューを埋め込み化してみる

まずはレビューを埋め込み化して、欲しい内容を検索して取得してみます。

## データセット
使っていくレビューはこちらの[fine-food reviews](https://www.kaggle.com/datasets/snap/amazon-fine-food-reviews)です。
このデータセットはAmazonの高級食材をレビューしたもので、約50万件まとめられています。

内容には、`Score`(5段階評価)や`Text`(実際のレビューテキスト)、`HelpfulnessNumerator`(このレビューが役に立ったというユーザの数)などの項目があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2976188/719931bd-a808-289d-ef8f-a3a7c466a672.png)

## text-embedding-ada-002を使ってみる
それではさっそく埋め込み化をします！
使うモデルは**text-embedding-ada-002**です。

Azure OpenAIにtext-embedding-ada-002のモデルをデプロイしているのを前提に進めていくので、まだの人は[こちらの公式サイト](https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal)をご参照ください。


まずは必要なライブラリをインストール。
下記のものが必要なのでpipインストールしましょう。
```
pip install openai num2words matplotlib plotly scipy scikit-learn pandas tiktoken
```
そして、importやAPIの設定を行います。
```python
import openai
import os
import pandas as pd
import numpy as np
from openai.embeddings_utils import get_embedding, cosine_similarity
import tiktoken

openai.api_type = "azure"
openai.api_base = "https://XXX.openai.azure.com/" #OPENAI_API_BASE
openai.api_version = "2023-05-15"
openai.api_key = "OPENAI_API_KEY"
```
APIキーやバージョンなどを入力します。

次に埋め込み化の設定をします。使用するモデル・エンコーディング・最大トークン数を次のようにしました。
```python
embedding_model = "text-embedding-ada-002"
embedding_encoding = "cl100k_base"
max_tokens = 8000
```
続いてデータの前処理をします。
csvファイルを読み込み、columnsの選択・追加を行います。

```python
input_datapath = "data/fine_food_reviews_1k.csv"
df = pd.read_csv(input_datapath, index_col=0)
df = df[["Time", "ProductId", "UserId", "Score", "Summary", "Text"]]
df = df.dropna()
df["combined"] = (
    "Title: " + df.Summary.str.strip() + "; Content: " + df.Text.str.strip()
)
```
そしてエンコーディング・埋め込み化を行います。
最大トークン数を超えるような、長いレビューはフィルタリングし、最終的に最新レビューのうち1000件を選んで埋め込みします。
```python
top_n = 1000
df = df.sort_values("Time").tail(top_n * 2)  
less than half will be filtered out
df.drop("Time", axis=1, inplace=True)

encoding = tiktoken.get_encoding(embedding_encoding)

df["n_tokens"] = df.combined.apply(lambda x: len(encoding.encode(x)))
df = df[df.n_tokens <= max_tokens].tail(top_n)
```
生成された埋め込みは新しく`embedding`列に追加します。
```python
df["embedding"] = df.combined.apply(lambda x: get_embedding(x, engine=embedding_model))
df.to_csv("data/fine_food_reviews_with_embeddings_1k_2.csv")
```
出力された「fine_food_reviews_with_embeddings_1k_2.csv」の中身はコチラ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2976188/346d9ac4-fde1-5d35-6e65-3cd768db633f.png)

## 検索・結果
それでは検索してみます。`user_query`で取得したい情報について書きます。今回は抽象的ですが、良いレビューを3つ取ってくるように設定しました。
埋め込み計算は「コサイン類似度」というものを使って埋め込みしたドキュメントとクエリの類似度を計算します。
```python
user_query = "Can I get good reviews"
top_n = 3

embedding = get_embedding(
user_query,
engine=embedding_model,
)
df["similarities"] = df.embedding.apply(lambda x: cosine_similarity(x, embedding))

res = (df.sort_values("similarities", ascending=False).head(top_n))
res.to_csv("data/response.csv")
```
返ってきた内容がコチラ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2976188/9ea7ad57-3f22-991a-f9ee-414f5348d2e9.png)
選ばれた3つの`Text`を見てみるとどれも高評価なレビューなので、きちんと求めていた結果が返ってきました。

今度はこの埋め込み化を使って、熊本のおいしいお店を検索してみます！！

# けんさむさんの食レポ記事を埋め込み化してみる
熊本のおいしいお店を紹介している[けんさむさんの記事](https://somekenblog.net/)を埋め込み化し、自分の希望にマッチするお店を検索してみます。

まずは記事のURLと文章を取ってきて、csvファイルにまとめます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2976188/8a6a42a1-24dd-365c-8ae6-6dfc5dd46c40.png)

そして、このcsvファイルを読み込んで、埋め込み化をします。

```python
import openai
import os
import pandas as pd
import numpy as np
from openai.embeddings_utils import get_embedding, cosine_similarity
import tiktoken

openai.api_type = "azure"
openai.api_base = "https://XXX.openai.azure.com/" #OPENAI_API_BASE
openai.api_version = "2023-05-15"
openai.api_key = "OPENAI_API_KEY"

embedding_model = "text-embedding-ada-002"
embedding_encoding = "cl100k_base"
max_tokens = 8000

input_datapath = "data/kensam/sentences.csv"  
df = pd.read_csv(input_datapath, index_col=0, encoding="utf-8")

encoding = tiktoken.get_encoding(embedding_encoding)

df["n_tokens"] = df.Sentences.apply(lambda x: len(encoding.encode(x)))
df = df[df.n_tokens <= max_tokens]
print(len(df))

df["embedding"] = df.Sentences.apply(lambda x: get_embedding(x, engine=embedding_model))
df.to_csv("data/kensam/sentences_with_embeddings.csv")
```
`input_datapath`に埋め込み化をするcsvファイルのパスを入力します。
これを実行したら、「sentences_with_embeddings.csv」というembedding列を加えたファイルが出力されます。

埋め込み化したら次は検索！
```python
import openai
import pandas as pd
import numpy as np
from openai.embeddings_utils import get_embedding, cosine_similarity
import ast

openai.api_type = "azure"
openai.api_base = "https://XXX.openai.azure.com/" #OPENAI_API_BASE
openai.api_version = "2023-05-15"
openai.api_key = "OPENAI_API_KEY"

embedding_model = "text-embedding-ada-002"
embedding_encoding = "cl100k_base"
max_tokens = 8000

df = pd.read_csv("data/kensam/sentences_with_embeddings.csv", index_col=0, encoding="utf-8")

user_query = "山鹿のおいしいラーメン屋さんは？"
top_n = 3
df['embedding'] = df['embedding'].apply(lambda x: np.array(ast.literal_eval(x)))

embedding = get_embedding(
    user_query,
    engine=embedding_model,
)
df["similarities"] = df.embedding.apply(lambda x: cosine_similarity(x, embedding))

res = (df.sort_values("similarities", ascending=False).head(top_n))
print(res)
res.to_csv("data/kensam/result/response.csv")
```
`df`には先ほどの埋め込み化で出力された「sentences_with_embeddings.csv」のパスを入力します。また、`user_query`には検索したい情報を書き込みます。

## 検索結果

まずは試しに「山鹿でおいしいラーメン屋はどこ？」と聞いてみました。

すると…
1. [【麺屋仁大 におう】熊本・山鹿でおいしい豚骨ラーメンを発見！](https://somekenblog.net/gourmet/nioh/)
1. [熊本の山奥、山都町で謎に支持されている「おちかラーメン」に潜入してきた](https://somekenblog.net/gourmet/ochika/)
1. [熊本・山鹿『どさん子ラーメン』のチーズカツ定食が絶品&ボリュームMAXだ](https://somekenblog.net/gourmet/dosanko/)

上位3つこちらが選ばれました。
1と3についてはきちんと山鹿のラーメン屋が選ばれましたが、2番目だけは山都町の記事がヒット！！「山」だけで判断された感じですかね…？よくわかりません＞＜

ちなみにけんさむさんの検索窓に「山鹿 ラーメン」と入力したら、『仁大』と『どさん子ラーメン』の記事がヒットしたので、今のところ埋め込み化のメリットはなんだろう？状態になってます汗。ただ、文章で聞けるのは普通の検索とは違う良いところですかね。

↓がちもとさんにも使ってもらいました！↓

https://twitter.com/sotongshi/status/1665609474365882374?s=20

今回使った**検索**については他にどんなものに使えるのか、用途を検討してみます…！

## 参考サイト
https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/tutorials/embeddings?tabs=command-line

https://github.com/openai/openai-cookbook/blob/main/examples/Obtain_dataset.ipynb