🙆

FAISSを使った近傍検索とRAGによる回答

2024/02/18に公開

このページを理解しました
https://python.langchain.com/docs/integrations/vectorstores/faiss

やりたいこと

提供した情報から、回答を返して欲しいとしましょう。

リサという架空の人物について、以下のようにプロフィールを設定しました。
(FM802を聴きながら作りましたので、DJさんの要素が強く入っています)

target_texts = [
    "リサの性別は女性です",
    "リサの趣味はランニングです",
    "リサの年齢は21歳です",
    "リサは兵庫県に住んでいます",
]

ゴールとしては、"リサの性別は?"という質問に対して'女性です'という答えを返すようにします。

まずはFAISSの近傍検索で、"リサの性別は女性です"がこの質問へ回答するために最も「近い」文であることを突き止めます。

次にRAGを使って、'女性です'という回答を生成させます。

手順

google colabのセットアップ

# 今回はcolabのpythonバージョンはアップグレードする必要はありません
!python --version
Python 3.10.12

# OPENAI_API_KEYを環境変数に設定
import os
os.environ["OPENAI_API_KEY"] ="*****"

# 関連ライブラリのインストール
!!pip install -U langchain-community faiss-cpu langchain-openai tiktoken langchain

前回はfaissそのものを使いましたが、今回はlangchainモジュールのFAISSライブラリを使います。
faissそのものよりもシンプルに書くことができます。

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

FAISSで近傍検索を検証

target_texts = [
    "リサの性別は女性です",
    "リサの趣味はランニングです",
    "リサの年齢は21歳です",
    "リサは兵庫県に住んでいます",
]

vectorstore = FAISS.from_texts(
    texts = target_texts,
    embedding=OpenAIEmbeddings()
)

vectorstoreに"リサの性別は?"という質問を投げかけて、近傍検索をしてみましょう。
similarity_search_with_scoreを使うと、それぞれのtextに対しどれくらいの距離であるかを取得できます。
(返される距離スコアはL2距離です。スコアは小さいほど近いです)

docs = vectorstore.similarity_search_with_score('リサの性別は?')

for doc in docs:
  print(doc)

以下の通り、"リサの性別は女性です"が一番距離が近いことがわかります

(Document(page_content='リサの性別は女性です'), 0.074174926)
(Document(page_content='リサの年齢は21歳です'), 0.21048263)
(Document(page_content='リサは兵庫県に住んでいます'), 0.24522918)
(Document(page_content='リサの趣味はランニングです'), 0.26744977)

※ ちなみに今回は、与えた情報は全てリサのプロフィールであったため、回答にそぐわない文でもL2距離が0.2程度と比較的低い数値になりました。例えば、"明日の最高気温は12度です"という全く関係ない文を入れるとL2距離は0.5と大きく出ます。

RAGで回答を生成する

次に回答を得ましょう。
('リサの性別は?'という質問に対して、欲しい回答は'女性'だけですからね)

RAG検索の手順としては、①retriever、②prompt、③modelをchainに渡し、chain.invokeで生成します

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# retrieverの作成
vectorstore = FAISS.from_texts(
    target_texts, embedding=OpenAIEmbeddings()
)
retriever = vectorstore.as_retriever()

# promptの作成
template = """contextに従って回答してください:
{context}

質問: {question}
"""
prompt = ChatPromptTemplate.from_template(template)

# modelの作成
model = ChatOpenAI()

# chainを作成

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

これで、'リサの性別は?'という質問投げかけに対して、'女性'という回答を得ることができます。

chain.invoke("リサの性別は?")

#-> '女性'

※たまに「リサの性別は女性です」と与えた文そのまま返してくることがあります。
promptに渡すtemplateを少し工夫して、回答文を生成していることを確認してみてください

template = """contextに従って回答してください:
{context}

質問: {question}
関西弁で回答してください
"""
・・・

chain.invoke("リサの性別は?")

#-> 'リサの性別は女やで'

無い情報を調べる

渡していない情報を答えさせてみましょう

docs = vectorstore.similarity_search_with_score('リサの兄弟は?')

for doc in docs:
  print(doc)

結果は以下のとおり、0.2より近い文はありません

(Document(page_content='リサは兵庫県に住んでいます'), 0.24774745)
(Document(page_content='リサの性別は女性です'), 0.262212)
(Document(page_content='リサの年齢は21歳です'), 0.27011853)
(Document(page_content='リサは大阪府立大学に通っています'), 0.28328043)

RAGで回答を生成させようにも、わかりませんと返されます

chain.invoke("リサの兄弟は?")

#-> '提供された文書にはリサの兄弟に関する情報が含まれていないため、リサの兄弟については何も分かりません。'

情報を追加する

FAISS.add_texts()を使って情報を追加できます

vectorstore.add_texts(texts=["リサには歳の離れた弟がいます"])

vectorstoreに情報が追加されており、'リサの兄弟は?'という質問に対して先ほど追加した'リサには歳の離れた弟がいます'が、最も近いと判断されています

docs = vectorstore.similarity_search_with_score('リサの兄弟は?')

for doc in docs:
  print(doc)

#->
(Document(page_content='リサには歳の離れた弟がいます'), 0.1587918)
(Document(page_content='リサは兵庫県に住んでいます'), 0.24774745)
(Document(page_content='リサの性別は女性です'), 0.262212)
(Document(page_content='リサの年齢は21歳です'), 0.27011853)

さて、vectorstoreが更新されましたので、retrieverやchainを更新する必要があると思いきや、更新しなくてもchain.invokeで回答を得られます。(なんで?)

chain.invoke("リサの兄弟は?")

#->'リサの兄弟は、歳の離れた弟です。'

※本当は'弟です'と返して欲しいのですが、まあいいでしょう。

保存とロード

vectorstoreは以下のように保存できます。

vectorstore.save_local("faiss_index")

new_vectorstore = FAISS.load_local("faiss_index", OpenAIEmbeddings())

Discussion