🍌

Microsoft Build 2024予習 Lab~OpenAIとCosmos DBをCopilotアプリに統合~編

2024/05/17に公開

はじめに

Microsoft Build2024に参加するにあたり予習をしております。その第3弾です。
Labという現地で参加可能で事前予約をして参加する形のセッションがあります。
その中の 「CopilotアプリにOpenAIとCosmosDBを統合する」 というセッションに参加する予定です。
Integrate Azure OpenAI Service and Azure Cosmos DB for copilot apps
本記事ではOpenAIとCosmosDBにフォーカスを当ててキャッチアップをしていきます。

Azure Cosmos DBとAzure OpenAIを組み合わせる

Azure OpenAIは言わずと知れた生成AIですが、とAzure Cosmos DBを組み合わせることでデータ取得と生成AIから得られる結果を強化することができるようです。
Azure Cosmos DBの ベクトルデータベースEmbeddingRAG(Retrieval Augmentation Generation) の手法を利用することで回答の関連性を高められるとあります。

私も概念としてはなんとなく把握しているのですが、
ちゃんと説明しようと思うと難しいというのが正直なところです。
今回は実際に実装をしながら解説を書いていこうと思います。
*実装方法はGPT-4oさんの力を借りてかなり効率的に進められました

Embeddingとベクトルデータベース

Embeddingはトークンの意味とコンテキストを表すベクトルまたは数値配列に変換する技術です。
ベクトルデータベースはデータの特徴や属性を「高次元ベクトル」として保持するデータベースの1種です。

つまり、Embeddingでベクトル化して、AIが読み取りやすい形にします。
そのベクトル化させたものを保存できるのがベクトルデータベースです。

実際にpythonで書いてみます。
例えば 「八百屋さんの商品をベクトル検索したい」 というニーズに応える製品を作るとします。
順を追って説明をしていきます。

Embedding対象の作成

まずは商品リストを作ります。
商品名に説明文に加えてAIが判断しやすいようにします。

# 商品リストとその説明
products = [
    {"name": "りんご", "description": "甘くてシャキシャキした果物"},
    {"name": "バナナ", "description": "栄養豊富でエネルギー補給に最適"},
    {"name": "にんじん", "description": "ビタミン豊富でヘルシーな野菜"},
    {"name": "トマト", "description": "ジューシーでサラダに最適な野菜"},
    {"name": "オレンジ", "description": "ビタミンCが豊富で風邪予防に効果的"}
]

Embeddingの実行

次に fasttext というライブラリを使ってトレーニングをします。
トレーニングをした結果を持って、  get_description_vector でEmbedding(ベクトル化)をします。

# 商品説明をテキストファイルに保存
with open("products.txt", "w") as f:
    for product in products:
        f.write(product["description"] + "\n")

# モデルのトレーニング
model = fasttext.train_unsupervised('products.txt', model='skipgram', dim=100, minCount=1)

# 各商品の埋め込みベクトルを生成
def get_description_vector(description):
    words = description.split()
    word_vectors = [model.get_word_vector(word) for word in words]
    return np.mean(word_vectors, axis=0)

product_vectors = np.array([get_description_vector(product["description"]) for product in products])

*具体的にどのような数値でベクトル化されているかについては次段で説明します。

ベクトルデータベースに保存

次にベクトルデータベースに保存します。
ここでは仮に vector_database という変数をデータベースとして扱います。

# 仮のベクトルデータベース(リスト)に登録
vector_database = product_vectors
print("ベクトルデータベースに登録されたベクトル:\n", vector_database)

実際にベクトルデータベースに登録された形は以下の通りです。
これがAIが理解しやすい形にあたります。
(量が多いので一部のみ記載します)

ベクトルデータベースに登録されたベクトル:
 [[ 6.69150904e-04 -4.45398822e-04 -9.21554922e-04  6.95627707e-04
   2.64294067e-04 -1.22346112e-03 -3.65895074e-04  7.50113104e-04
  -1.07341315e-04  6.29373884e-04  1.07857527e-03  1.42382283e-04
   1.70014173e-04 -6.24214445e-05 -4.77274123e-04 -7.40436953e-04
   3.88846296e-04  1.33096776e-03  2.71458295e-04  6.23793574e-04
  -4.60140000e-05  7.05859929e-05 -2.25611657e-04 -3.35414399e-04
  -3.31798248e-04 -1.40473875e-03 -7.78027868e-04 -5.00156137e-04
   3.65859669e-05 -9.35169737e-06 -5.49165532e-04  6.36050827e-05
   1.36232132e-03 -4.13275615e-04  1.57083254e-04 -9.14748598e-05
   3.23721499e-04  8.89236224e-04 -3.90386500e-04 -3.26446578e-04
   8.90136522e-04 -5.73874495e-05  1.32768950e-03  5.25874086e-04
  -1.21632511e-05  4.13352769e-04 -1.17717357e-03  7.17860457e-05
  -1.05726602e-03  5.88396855e-04 -5.33513550e-04 -4.38320742e-04
  -1.01749133e-03  3.12272256e-04  1.16251656e-04 -5.10077691e-04
   6.08066213e-04 -1.20987694e-04 -3.54486838e-05 -2.71456316e-04
  -1.34222646e-04 -6.48031011e-04 -4.09926055e-04  2.29324578e-04
   8.35514918e-04  2.77104438e-04 -1.52733421e-03  6.06148329e-04
   5.76386286e-04  1.57825707e-03  4.09141619e-04  1.55079726e-03
   4.85678087e-04  1.07644394e-03  8.11724807e-04  6.98363001e-04
  -1.30760294e-04 -5.01476636e-04  4.87996003e-04  4.07263346e-04
  -8.94305296e-04  1.11190032e-03 -5.98105893e-04 -2.08653699e-04
  -1.01498599e-05  8.23716342e-04 -9.47837252e-04  2.43122820e-04
   1.44739635e-04 -2.94599813e-05 -2.50622979e-04 -1.17066503e-03
  -4.04239312e-04 -5.13443840e-04  8.12754966e-04  1.39683107e-04
  -9.15007840e-04  1.36849878e-03  5.30019926e-04 -7.91483501e-04]
 [-2.12793006e-04 -2.49161734e-04 -6.12123811e-04 -6.94435657e-05
   4.19462413e-05  1.15603826e-03 -6.70309091e-05 -9.38205514e-04
   2.44628871e-04  4.19863878e-04  4.64691722e-04  3.61899933e-04
  -6.75069517e-04  1.40885139e-04  1.66457248e-04  1.14471267e-03
・・・・・

ベクトルデータ検索をする

ベクトルデータベースに保存されたデータをベクトル検索してみます。
ここでは "栄養価が高く、エネルギー補給に最適な果物" という文言で検索をかけてみます。

# ベクトル検索のためのNearestNeighborsモデルを作成
nbrs = NearestNeighbors(n_neighbors=2, algorithm='ball_tree').fit(vector_database)

# 検索したい商品の説明ベクトルを生成
query_description = "栄養価が高く、エネルギー補給に最適な果物"
query_vector = get_description_vector(query_description).reshape(1, -1)

# ベクトル検索を実行
distances, indices = nbrs.kneighbors(query_vector)
print("検索クエリ: ", query_description)
print("検索結果:")
for dist, idx in zip(distances[0], indices[0]):
    print(f"商品: '{products[idx]['name']}' -> 距離: {dist}")

実際に得られた結果は以下の通りです。

検索結果:
商品: 'バナナ' -> 距離: 0.0067971388849307375
商品: 'にんじん' -> 距離: 0.00832572900447486

"栄養豊富"と定義したバナナ、"ビタミン豊富"と定義したにんじんが検索できました。
このように、
人間が理解する言葉(画像や音声の場合もあります)をAIが理解できる数値にするのがEmbedding
Embeddingされたデータを保存し検索できるようにする場所がベクトルデータベースと言えます。

RAG

RAG(Retrieval Augmentation Generation)はファインチューニングをせずに生成AIの機能を拡張できるアーキテクチャです。

生成AIに持っていない情報を利用することや、利用する情報を制限することができます。
例えば上述した八百屋さんの例を使って説明します。
ベクトル検索で "栄養価が高く、エネルギー補給に最適な果物" と入力し、
OpenAIのAPIを使って答えてもらいます。
*一部変更があるのでベクトル検索の部分から記載しています。

# 検索したい商品の説明ベクトルを生成
query_description = "栄養価が高く、エネルギー補給に最適な果物"
query_vector = get_description_vector(query_description).reshape(1, -1)

# ベクトル検索を実行
distances, indices = nbrs.kneighbors(query_vector)
print("検索クエリ: ", query_description)
print("検索結果:")

# 検索結果を保存するリストを初期化
retrieved_descriptions = []
for dist, idx in zip(distances[0], indices[0]):
    retrieved_name = products[idx]['name']
    retrieved_description = products[idx]['description']
    retrieved_descriptions.append(retrieved_name)
    retrieved_descriptions.append(retrieved_description)
    print(f"商品: '{products[idx]['name']}' -> 距離: {dist}")

# 検索結果に基づいて情報を生成
context = " ".join(retrieved_descriptions)
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "system", "content": "あなたは有能なアシスタントです。"},
        {"role": "user", "content": f"以下の商品説明に基づいて関連する情報を生成してください: {context}"}
    ]
)

print("生成された情報: ", response.choices[0].message['content'].strip())

プロンプトで生成AIに役割を与えつつ、ベクトル検索結果に基づいて情報を作ってもらいます。
すると以下のような回答が得られます。

バナナは栄養豊富でエネルギー補給に最適な果物です。朝食やスナックとしても人気があります。オレンジはビタミンCが豊富で風邪予防にも効果的です。どちらも健康維持に役立つ素晴らしい選択です。

このようにベクトル検索をした結果を元に、回答を作成していることがわかります。
RAG を使うことでファインチューニングなしでデータベースを基にした検索結果を得ることができました。

最後に

本題は 「CopilotアプリにOpenAIとCosmosDBを統合する」 というセッションについてでした。
本記事では簡易データベースを利用した例でしかないので、実際にCosmosDBを利用した内容や、Copilotともコラボした内容が聞けるのがとても楽しみです。
RAGに関するセッションCopilotに関するセッションはオンラインでも多く開催されていますので、興味のある方はご覧ください!

*実装した内容の全文も載せておきます。

import fasttext
import numpy as np
from sklearn.neighbors import NearestNeighbors
import openai

# OpenAI APIキーを設定
openai.api_key = 'your_openai_api_key'

# 商品リストとその説明
products = [
    {"name": "りんご", "description": "甘くてシャキシャキした果物"},
    {"name": "バナナ", "description": "栄養豊富でエネルギー補給に最適"},
    {"name": "にんじん", "description": "ビタミン豊富でヘルシーな野菜"},
    {"name": "トマト", "description": "ジューシーでサラダに最適な野菜"},
    {"name": "オレンジ", "description": "ビタミンCが豊富で風邪予防に効果的"}
]

# 商品説明をテキストファイルに保存
with open("products.txt", "w") as f:
    for product in products:
        f.write(product["description"] + "\n")

# モデルのトレーニング
model = fasttext.train_unsupervised('products.txt', model='skipgram', dim=100, minCount=1)

# 各商品の埋め込みベクトルを生成
def get_description_vector(description):
    words = description.split()
    word_vectors = [model.get_word_vector(word) for word in words]
    return np.mean(word_vectors, axis=0)

product_vectors = np.array([get_description_vector(product["description"]) for product in products])

# 仮のベクトルデータベース(リスト)に登録
vector_database = product_vectors
print("ベクトルデータベースに登録されたベクトル:\n", vector_database)

# ベクトル検索のためのNearestNeighborsモデルを作成
nbrs = NearestNeighbors(n_neighbors=2, algorithm='ball_tree').fit(vector_database)

# 検索したい商品の説明ベクトルを生成
query_description = "栄養価が高く、エネルギー補給に最適な果物"
query_vector = get_description_vector(query_description).reshape(1, -1)

# ベクトル検索を実行
distances, indices = nbrs.kneighbors(query_vector)
print("検索クエリ: ", query_description)
print("検索結果:")

# 検索結果を保存するリストを初期化
retrieved_descriptions = []
for dist, idx in zip(distances[0], indices[0]):
    retrieved_name = products[idx]['name']
    retrieved_description = products[idx]['description']
    retrieved_descriptions.append(retrieved_name)
    retrieved_descriptions.append(retrieved_description)
    print(f"商品: '{products[idx]['name']}' -> 距離: {dist}")

# 検索結果に基づいて情報を生成
context = " ".join(retrieved_descriptions)
print("コンテキスト: ", context)
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "system", "content": "あなたは有能なアシスタントです。"},
        {"role": "user", "content": f"以下の商品説明に基づいて関連する情報を生成してください: {context}"}
    ]
)

print("生成された情報: ", response.choices[0].message['content'].strip())

Discussion