はじめに
この記事は株式会社ナレッジコミュニケーションが運営する クラウド AI by ナレコム Advent Calendar 2023 の15日目にあたる記事になります。
前提条件
langchain のバージョンは 0.0.348 です。
ベクトルDB は FAISS を利用しています。
検証用モンキーパッチとして参考にしていただければと思います。
課題
retriever = db.as_retriever()
で作成した Retriever において参照したドキュメントのスコアを取得できない
解決策
db.as_retireiver()
で指定できるドキュメント検索の search_type
は以下の3つです。今回は、1と3を扱います。
- similarity (default):関連度スコアに基づいて検索
- mmr:ドキュメントの多様性を考慮し検索(対象外)
- similarity_score_threshold:関連度スコアの閾値を設定し検索
similarity を利用するパターン
similarity
では以下の faiss.similarity_search
が利用されるためここを修正します。
metadata
に score
属性を追加して返却します。
def similarity_search(
self,
query: str,
k: int = 4,
filter: Optional[Dict[str, Any]] = None,
fetch_k: int = 20,
**kwargs: Any,
) -> List[Document]:
"""Return docs most similar to query.
Args:
query: Text to look up documents similar to.
k: Number of Documents to return. Defaults to 4.
filter: (Optional[Dict[str, str]]): Filter by metadata. Defaults to None.
fetch_k: (Optional[int]) Number of Documents to fetch before filtering.
Defaults to 20.
Returns:
List of Documents most similar to the query.
"""
docs_and_scores = self.similarity_search_with_score(
query, k, filter=filter, fetch_k=fetch_k, **kwargs
)
# Before
"""
return [doc for doc, _ in docs_and_scores]
"""
# After
result = []
for doc, score in docs_and_scores:
doc.metadata['score'] = score
result.append(doc)
return result
similarity_score_threshold を利用するパターン
similarity_score_threshold
では以下の faiss._similarity_search_with_relevance_scores
が利用されるためここを修正します。
def _similarity_search_with_relevance_scores(
self,
query: str,
k: int = 4,
filter: Optional[Dict[str, Any]] = None,
fetch_k: int = 20,
**kwargs: Any,
) -> List[Tuple[Document, float]]:
"""Return docs and their similarity scores on a scale from 0 to 1."""
# Pop score threshold so that only relevancy scores, not raw scores, are
# filtered.
relevance_score_fn = self._select_relevance_score_fn()
if relevance_score_fn is None:
raise ValueError(
"normalize_score_fn must be provided to"
" FAISS constructor to normalize scores"
)
docs_and_scores = self.similarity_search_with_score(
query,
k=k,
filter=filter,
fetch_k=fetch_k,
**kwargs,
)
docs_and_rel_scores = []
# Before
"""
for doc, score in docs_and_scores:
docs_and_rel_scores.append((doc, relevance_score_fn(score)))
"""
# After
for doc, score in docs_and_scores:
doc.metadata["similarity_score"] = score # 距離スコア
doc.metadata["relevance_score"] = relevance_score_fn(score) # 関連度スコア
docs_and_rel_scores.append((doc, relevance_score_fn(score)))
return docs_and_rel_scores
ここで、relevance_score_fn
という関数がでてきています。
これは、
- 距離スコア:0に近いほど関連度が高い(関数の性質により異なりますが、簡易的に表現しています)
- 閾値:1に近いほど関連度が高い
となっている2つの値のギャップを埋めるための変換となっているようです。
なので、実際に閾値が適応される値は relevance_score_fn(score)
となるため、ドキュメントの関連度としてはこちらの値を参考にする方が適切と思われます。
ちなみに、relevance_score_fn
は以下で定義されています。
@staticmethod
def _euclidean_relevance_score_fn(distance: float) -> float:
"""Return a similarity score on a scale [0, 1]."""
# The 'correct' relevance function
# may differ depending on a few things, including:
# - the distance / similarity metric used by the VectorStore
# - the scale of your embeddings (OpenAI's are unit normed. Many
# others are not!)
# - embedding dimensionality
# - etc.
# This function converts the euclidean norm of normalized embeddings
# (0 is most similar, sqrt(2) most dissimilar)
# to a similarity function (0 to 1)
return 1.0 - distance / math.sqrt(2)
@staticmethod
def _cosine_relevance_score_fn(distance: float) -> float:
"""Normalize the distance to a score on a scale [0, 1]."""
return 1.0 - distance
@staticmethod
def _max_inner_product_relevance_score_fn(distance: float) -> float:
"""Normalize the distance to a score on a scale [0, 1]."""
if distance > 0:
return 1.0 - distance
return -1.0 * distance
おわりに
ナレッジコミュニケーションでは「Musubite」というエンジニア同士のカジュアルトークサービスを利用しています!この記事にあるような生成 AI 技術を使ったプロジェクトに携わるメンバーと直接話せるサービスですので興味がある方は是非利用を検討してください!