機械学習
記事内に商品プロモーションを含む場合があります

LangChainの近傍探索ライブラリFaiss(類似検索)の使い方を解説

tadanori

LangChain(+Llama2+stuffの組み合わせ)で使われている、Faissについて解説する記事です。Faissを使うとテキストの近似最近傍探索(類似検索)を簡単に行うことが可能です。ここでは、法律文を題材としてPythonのコードを作成し、挙動を調べてみました。

① テキスト検索ではなく、FAISSを特徴ベクトル検索に使う方法については以下の記事を参考にしてください

深層距離学習+FaissをMNISTデータセットで実践【Pytorch Metric Learning】
深層距離学習+FaissをMNISTデータセットで実践【Pytorch Metric Learning】

② Faissを応用例は以下を参考にしてください

FaceNetとFaissで高速な1対Nの顔認証を実装してみる|PyTorch
FaceNetとFaissで高速な1対Nの顔認証を実装してみる|PyTorch
LangChainでRAGを試す|CALM2 + FAISS + RetrievalQAの使用例
LangChainでRAGを試す|CALM2 + FAISS + RetrievalQAの使用例

Faissとは

Faissとは

Faiss(Facebook AI Similarity Search)は、類似したドキュメントを検索するためのMetaが作成したオープンソースのライブラリです。Faissを使うことで、テキストの類似検索を行うことができます。

一般的なテキストの文字列検索などでは、「文字列そのものを検索」するのに対して、類似検索では「似たテキストを検索」する点が大きく異なります。

近似最近傍探索を行うライブラリは、Faiss以外にもFLANN、Annoy、NMSLIBなどがあります(私は使ったことないので詳しくないです)。

kNNなどの近傍探索は使ったことありますが、近似最近傍探索のライブラリを使うのは今回初です。

この記事では、LangChainで使用されているFaissに焦点を当てて解説します。

近似最近傍探索は、LangChain活用の鍵となる部分であるため、Faissについて理解しておくことが重要です。

FaissとLLMを組み合わせたRAGについては以下の記事を参考にしてください

LangChainでRAGを試す|CALM2 + FAISS + RetrievalQAの使用例
LangChainでRAGを試す|CALM2 + FAISS + RetrievalQAの使用例

Faissの基本

Faiss自体は、大規模なベクトルデータセットから類似したベクトルを検索する近似最近傍探索ライブラリでテキスト検索に特化したものではありません。

Faissは以下のような特徴があります

  • 高速な検索
    Faissは、効率的な近似最近傍検索(ANN)アルゴリズムを実装しており、大規模なデータセットに対しても高速な検索を提供
  • GPUサポート
    GPUをサポートすることで高速化を実現
  • 多様な検索手法
    複数の近似最近傍検索手法を提供しており、データセットに適した手法を選択可能
  • メモリ効率
    ベクトル量子化やインデックス圧縮によるメモリ使用量の削減

Faissを利用することで、ベクトルの近似最近傍探索を簡単に実装することが可能です。

テキストの類似検索を行う場合は、文章をベクトル表現に変換し、入力されたテキストのベクトルとの距離をFaissを使って検索することになります。

以下では、文章をベクトル化し、入力されたテキストと類似する文章をFaissを使って検索する手順を紹介します。

Faissの利用手順

この記事では、以下の手順でFaissによる近似最近傍探索を行います。

  1. テキストファイルをいくつかの文に分割(チャンクに分割)
  2. intfloat/multilingual-e5-largeを使って文章をベクトル化し、インデックスを作成
  3. 文字列を与えて類似検索を実行

Faissのインストール

CPUバージョンのインストール(faiss-cpu)

CPUで動作するバージョンは以下のコマンドでインストールできます

pip install faiss-cpu 

GPUバージョンのインストール(faiss-gpu)

GPUで動作するバージョンのインストールは以下の通りです

pip install faiss-gpu

Google Colabで動作させる場合

ここで動かしているサンプルコードをGoogle Colabで動作させる場合は、ノートブックのコードセルに以下を入力して実行します。

Faiss以外の動作に必要なライブラリも全てインストールされます。

!pip install langchain accelerate bitsandbytes sentence_transformers
!pip install faiss-gpu 

ライブラリのインポート

Faissの動作確認に使うライブラリをインポートします

from langchain.embeddings import HuggingFaceEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import FAISS

ドキュメントの準備

Llama2で法律文の検索テストをしてみようと考えているので、ドキュメントとしては法律文章にしてみました。

題材として用意したのは、比較的短い法律の「建物の区分所有等に関する法律」です。

区分所有法
引用先:法令検索「建物の区分所有等に関する法律

テキストファイル

ここからの説明は、法令検索のWebサイトから、テキストをコピーしてkubun.txtというファイルに保存した前提になります。

ドキュメントの読み込み

ドキュメントをtest_allに読み込みます

with open("kubun.txt", encoding="utf-8") as f:
    test_all = f.read()

チャンクに分割

テキストファイルをチャンクに分割します。ここでは、チャンクサイズを300に、オーバラップを32に設定しています。チャンクサイズは、埋め込みで使うモデルの最大トークンサイズを考慮して設定しておきます。今回はe5を使うので(トークンサイズ512)、それより小さくしています。

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=300,
    chunk_overlap=32,
)
texts = text_splitter.split_text(test_all)

以上の処理で、テキストが適当なサイズに分割されます。

ここで、テキストの内容を確認してみます。

print(len(texts))
for text in texts:
    print(text[:10].replace("\n", "\\n"), ":", len(text))

区分所有法の場合、トータルで143個に分割されたようです。

出力
143
第一章 建物の区分所 : 297
4 この法律において : 196
(区分所有者の団体) : 257
2 第一条に規定する : 296
(区分所有者の権利義 : 256
4 民法(明治二十九 : 275
2 前項の先取特権は : 237
(区分所有権売渡請求 : 291
3 民法第百七十七条 : 194
2 前項の場合におい : 244
第十五条 共有者の持 : 201
(共用部分の変更)\n : 293
2 前項の規定は、規 : 185
(管理所有者の権限) : 209
(共用部分に関する規 : 238
2 前項本文の場合に : 191
(分離処分の無効の主 : 296
第四節 管理者\n(選 : 261
2 管理者は、その職 : 235
5 管理者は、前項の : 238
        : 
  (以下略)

ここまでの処理で、textsに分割されたテキストが格納されます

PDFファイル

PDFファイルを読み込む場合は、Google Colabでは以下の追加のライブラリのインストールが必要でした。

!pip install pdf2image pdfminer.six
!pip install unstructured_pytesseract unstructured_inference
!pip install unstructured

また、ライブラリもインポートする必要があります

from langchain.document_loaders import UnstructuredPDFLoader

PDFは、法令検索のページのダウンロードボタンでダウンロードできます。ここでは、「横一段」とかかれたものをダウンロードしました。

ダウンロード画面
引用先:法令検索「建物の区分所有等に関する法律

ここでは、ダウンロードしたファイルをkubun.pdfという名前で保存している前提で進めます。PDFファイルをロードするのは簡単です。具体的には以下のコードになります。

loader = UnstructuredPDFLoader("kubun.pdf")
test_all = loader.load()

テキストを取り出したら、チャンクに分割します。テキストになっているので、処理はほとんど同じです(page_contentにテキストデータが格納されています)。

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=32,
)
texts = text_splitter.split_text(test_all[0].page_content)

ここまでの処理で、textsに分割されたテキストが格納されます

テキスト、PDFどちらもtextsを作成するようにしたので、以降は共通になります

インデックス作成

インデックスを作成します。埋め込みベクトルを作成するモデルを指定する必要がありますが、Colabで実行できるサイズのモデルである、multilingual-e5-largeを利用しました。

embedding=HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")

index = FAISS.from_texts(
    texts=texts,
    embedding=embedding
)

インデックスを作成したので、検索が可能になります。

FAISS.from_textsのオプションのdistance_strategyを設定することで距離計算をする手法を選択できます。指定できるオプションは以下になります。

  • DistanceStrategy.MAX_INNER_PRODUCT
  • DistanceStrategy.COSINE
  • DistanceStrategy.DOT_PRODUCT
  • DistanceStrategy.JACCARD
  • DistanceStrategy.EUCLIDEAN_DISTANCE

なお、DistanceStrategyを利用するには、以下のライブラリのインポートが必要になります。

from langchain.vectorstores.utils import DistanceStrategy

検索

similarity_search

類似検索を行います。規約の変更に関するチャンクを検索してみました。

docs = index.similarity_search("規約の変更")
docs
docs = index.similarity_search("規約の変更")

ドキュメントを見ると規約の設定・変更に関する部分が選ばれているようです。

結果は、ドキュメントの分割に結構左右される印象です。面倒でなければある程度は「手作業」または、うまく分割されるように「加工」した方が検索精度が向上すると思います

出力する数を変更

引数としてkを与えることで、検索結果の個数を変更できます

docs = index.similarity_search("規約の変更", k = 1)
docs
docs = new_index.similarity_search("規約の変更", k = 1)

テキスト部分だけ取り出す

テキスト部分だけ取り出す場合は、以下のようにしてアクセスします。

docs[0].page_content

similarity_search_with_score

類似検索の結果をスコア付きで返します。スコアはL2距離なので0に近いほど類似していることになります。

docs = index.similarity_search_with_score("規約の変更", k = 5)
docs
docs = new_index.similarity_search_with_score("規約の変更", k = 5)

ベクトルから検索

検索するテキストを、ベクトル化してから検索することも可能です。

埋め込みモデルのembed_queqy関数でベクトル化します

embedding_vector = embedding.embed_query("規約の変更")
embedding_vector[:10]
embedding_vector[:10]

ベクトルを使って類似検索を行います。テキストで検索した時と同様の結果が出力されます。

index.similarity_search_by_vector(embedding_vector, k=3)
index.similarity_search_by_vector(embedding_vector, k=3)

作成したインデックスのロードとセーブ

作成したインデックスはセーブしておいて、使う時にロードすることができます。

データが大きくなると、インデックスの作成に少し時間がかかるので一旦セーブしておいてロードして使うというのが一般的なのかもしれません。

インデックスを保存(セーブ)

faiss_indexという名前で保存します。

index.save_local("faiss_index")

インデックスの読み込み(ロード)

faiss_indexという名前のセーブデータを読み込みます。ロードでは、埋め込みモデルも同時に指定する必要があります。

embedding=HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")
new_index = FAISS.load_local("faiss_index", embedding)

まとめ

LangChain+Llama2などを使うときに、サンプルプログラムをみながらなんとなく使っているFaissですが、今回はFaissを調べてみました。

近似最近傍探索単体でも、いろいろ使うことができそうです。

大規模言語モデル関連記事一覧はこちら
大規模言語モデル(LLM)関連の記事一覧
大規模言語モデル(LLM)関連の記事一覧

おすすめ書籍

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

記事URLをコピーしました