LangChainのFaissを活用した近似最近傍探索の手順を解説
LangChain、Llama2、そしてFaissを組み合わせることで、テキストの近似最近傍探索(類似検索)を簡単に行うことが可能です。特にFaissは、大量の文書やデータの中から類似した文を高速かつ効率的に検索できるため、RAG(Retrieval-Augmented Generation)といった応用にも広く利用されています。本記事では、法律文を題材に、Faissの基本的な仕組みから具体的な使い方までをPythonコードを用いて詳しく解説します
① テキスト検索ではなく、FAISSを特徴ベクトル検索に使う方法については以下の記事を参考にしてください
② Faissの応用例は以下を参考にしてください
Faissとは
Faissとは
Faiss(Facebook AI Similarity Search)は、類似したドキュメントを検索するためのMetaが作成したオープンソースのライブラリです。Faissを使うことで、テキストの類似検索を行うことができます。
一般的なテキストの文字列検索などでは、「文字列そのものを検索」するのに対して、類似検索では「似たテキストを検索」する点が大きく異なります。
近似最近傍探索を行うライブラリは、Faiss以外にもFLANN、Annoy、NMSLIBなどがあります(私は使ったことないので詳しくないです)。
kNNなどの近傍探索は使ったことありますが、近似最近傍探索のライブラリを使うのは今回初です。
この記事では、LangChainで使用されているFaissに焦点を当てて解説します。
近似最近傍探索は、LangChain活用の鍵となる部分であるため、Faissについて理解しておくことが重要です。
FaissとLLMを組み合わせたRAGについては以下の記事を参考にしてください
Faissの基本
Faiss自体は、大規模なベクトルデータセットから類似したベクトルを検索する近似最近傍探索ライブラリでテキスト検索に特化したものではありません。
Faissは以下のような特徴があります
- 高速な検索
Faissは、効率的な近似最近傍検索(ANN)アルゴリズムを実装しており、大規模なデータセットに対しても高速な検索を提供 - GPUサポート
GPUをサポートすることで高速化を実現 - 多様な検索手法
複数の近似最近傍検索手法を提供しており、データセットに適した手法を選択可能 - メモリ効率
ベクトル量子化やインデックス圧縮によるメモリ使用量の削減
Faissを利用することで、ベクトルの近似最近傍探索を簡単に実装することが可能です。
テキストの類似検索を行う場合は、文章をベクトル表現に変換し、入力されたテキストのベクトルとの距離をFaissを使って検索することになります。
以下では、文章をベクトル化し、入力されたテキストと類似する文章をFaissを使って検索する手順を紹介します。
Faissの利用手順
この記事では、以下の手順でFaissによる近似最近傍探索を行います。
- テキストファイルをいくつかの文に分割(チャンクに分割)
intfloat/multilingual-e5-large
を使って文章をベクトル化し、インデックスを作成- 文字列を与えて類似検索を実行
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ファイルからテキストを抽出する方法について解説します。
なお、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
ドキュメントを見ると規約の設定・変更に関する部分が選ばれているようです。
結果は、ドキュメントの分割に結構左右される印象です。面倒でなければある程度は「手作業」または、うまく分割されるように「加工」した方が検索精度が向上すると思います
出力する数を変更
引数としてk
を与えることで、検索結果の個数を変更できます
docs = index.similarity_search("規約の変更", k = 1)
docs
テキスト部分だけ取り出す
テキスト部分だけ取り出す場合は、以下のようにしてアクセスします。
docs[0].page_content
similarity_search_with_score
類似検索の結果をスコア付きで返します。スコアはL2距離なので0に近いほど類似していることになります。
docs = index.similarity_search_with_score("規約の変更", k = 5)
docs
ベクトルから検索
検索するテキストを、ベクトル化してから検索することも可能です。
埋め込みモデルのembed_queqy
関数でベクトル化します
embedding_vector = embedding.embed_query("規約の変更")
embedding_vector[:10]
ベクトルを使って類似検索を行います。テキストで検索した時と同様の結果が出力されます。
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でRAGを行う場合などに利用するFaissですが、詳しく調べたことがなったので、今回使い方を調べてみました。
Faissによる近似最近傍探索単体でも、いろいろな応用が可能そうです。