ラベル faiss の投稿を表示しています。 すべての投稿を表示
ラベル faiss の投稿を表示しています。 すべての投稿を表示

2024年12月21日土曜日

faiss を使って文章の類似度計算をしてみる

faiss を使って文章の類似度計算をしてみる

概要

前回 文章を数値化し faiss にデータを保存しました
今回は実際に faiss に検索をかけて文章の類似度を計算して似ている文章を見つけたいと思います

環境

  • macOS 15.2
  • transformers 4.47.0
  • faiss-cpu 1.9.0-post1

コード全体

from torch import Tensor
from transformers import AutoModel, AutoTokenizer


# このaverage_pool関数はbertの出力結果ではよく使われる手法、何をしているかの詳細は以下の説明に記載
def average_pool(last_hidden_states: Tensor, attention_mask: Tensor) -> Tensor:
    last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
    return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]


# faissに保存する文章、この文章と類似度検索をする
input_texts = [
    "好きな食べ物は何ですか?",
    "どこにお住まいですか?",
    "朝の電車は混みますね",
    "今日は良いお天気ですね",
    "最近景気悪いですね",
    "最近、出かけていないので、たまには外で食事でもどうですか?",
]

# モデルとトークナイザの取得、日本語なので日本語に特化したモデルを使用
tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
model = AutoModel.from_pretrained("cl-tohoku/bert-base-japanese")

# 文章の数値化
inputs = tokenizer(input_texts, padding=True, truncation=True, return_tensors="pt")

# 文章のベクトル化、768 次元のベクトル情報が取得できる (参考: https://github.com/cl-tohoku/bert-japanese)
outputs = model(**inputs)

# bert の結果を更によくするためのおまじない
embeddings = average_pool(outputs.last_hidden_state, inputs["attention_mask"])

# faiss で扱えるように numpy 配列に変換
embeddings_np = embeddings.cpu().detach().numpy()

# なぜか先頭でimportすると segmentation fault になるのでここで import
import faiss

# 次元数(768)で初期化
index_flat_l2 = faiss.IndexFlatL2(embeddings_np.shape[1])
# ベクトル情報を追加、6文章(vectors)登録される
index_flat_l2.add(embeddings_np)  # type: ignore

# クエリとなるテキストの定義、数値化、faiss 用ベクトル化
query_text = ["今日は晴れている"]
query_dict = tokenizer(
    query_text, max_length=512, padding=True, truncation=True, return_tensors="pt"
)
query_output = model(**query_dict)
query_embeddings = average_pool(
    query_output.last_hidden_state, query_dict["attention_mask"]
)
query_embeddings_np = query_embeddings.cpu().detach().numpy()

# 上位何件を取得するかの設定
k = 2

# 検索の実行
D, I = index_flat_l2.search(query_embeddings_np, k)  # type: ignore

# 結果の表示
for i in range(k):
    print(
        f"  Rank: {i+1}, Index: {I[0][i]}, Distance: {D[0][i]}, Text: '{input_texts[I[0][i]]}"
    )

今回追加した faiss で検索をする部分は以下です

# クエリとなるテキストの定義、数値化、faiss 用ベクトル化
query_text = ["今日は晴れている"]
query_dict = tokenizer(
    query_text, max_length=512, padding=True, truncation=True, return_tensors="pt"
)
query_output = model(**query_dict)
query_embeddings = average_pool(
    query_output.last_hidden_state, query_dict["attention_mask"]
)
query_embeddings_np = query_embeddings.cpu().detach().numpy()

# 上位何件を取得するかの設定
k = 2

# 検索の実行、結果はD(distance)とI(index)の情報が返ってくる
D, I = index_flat_l2.search(query_embeddings_np, k)  # type: ignore

# 結果の表示
for i in range(k):
    print(
        f"  Rank: {i+1}, Index: {I[0][i]}, Distance: {D[0][i]}, Text: '{input_texts[I[0][i]]}"
    )

最後に

faiss で文章の類似度計算をしてみました
文章の類似度を計算する手法はいろいろとありますが特に自分で何も計算することなく search するだけで勝手に似たような文章を持ってきてくれるので簡単に使えます

今回は IndexFlatL2 を使いましたが他にも「IndexFlatIP」「IndexHNSWFlat」などがあります
参考: https://github.com/facebookresearch/faiss/wiki/Faiss-indexes

データの規模やマシンスペックによって適宜切り替えれば OK です
使い方によって多少データの加工が必要ですがほぼ同じように使えます

流れとしては以下で可能だとわかりました

  • transformers で文章を数値化、ベクトル化
  • 結果を faiss に格納
  • クエリを作成し検索を実施

bert のテンソルベクトル情報を faiss で扱えるように numpy形式のベクトル情報に変換するところが少しクセがありますがそこ以外は数値化した情報をそのまま faiss に渡すだけなので簡単に使えました

また数値化する際に今回は cl-tohoku/bert-base-japanese を使いましたがモデルとトークナイザを変えるだけでも結果は変わってくるかなと思います

参考サイト

2024年12月20日金曜日

faiss に数値化したデータを保存する方法

faiss に数値化したデータを保存する方法

概要

前回 文章を数値化し faiss に保存可能なベクトル情報を計算しました
今回は faiss にベクトル情報を保存する方法を紹介します

環境

  • macOS 15.2
  • transformers 4.47.0
  • faiss-cpu 1.9.0-post1

コード全体

from torch import Tensor
from transformers import AutoModel, AutoTokenizer


# このaverage_pool関数はbertの出力結果ではよく使われる手法、何をしているかの詳細は以下の説明に記載
def average_pool(last_hidden_states: Tensor, attention_mask: Tensor) -> Tensor:
    last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
    return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]


# faissに保存する文章、この文章と類似度検索をする
input_texts = [
    "好きな食べ物は何ですか?",
    "どこにお住まいですか?",
    "朝の電車は混みますね",
    "今日は良いお天気ですね",
    "最近景気悪いですね",
    "最近、出かけていないので、たまには外で食事でもどうですか?",
]

# モデルとトークナイザの取得、日本語なので日本語に特化したモデルを使用
tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
model = AutoModel.from_pretrained("cl-tohoku/bert-base-japanese")

# 文章の数値化
inputs = tokenizer(input_texts, padding=True, truncation=True, return_tensors="pt")

# 文章のベクトル化、768 次元のベクトル情報が取得できる (参考: https://github.com/cl-tohoku/bert-japanese)
outputs = model(**inputs)

# bert の結果を更によくするためのおまじない
embeddings = average_pool(outputs.last_hidden_state, inputs["attention_mask"])

# faiss で扱えるように numpy 配列に変換
embeddings_np = embeddings.cpu().detach().numpy()

# なぜか先頭でimportすると segmentation fault になるのでここで import
import faiss

# 次元数(768)で初期化
index_flat_l2 = faiss.IndexFlatL2(embeddings_np.shape[1])
# ベクトル情報を追加、6文章(vectors)登録される
index_flat_l2.add(embeddings_np)  # type: ignore

今回追加した faiss にデータを保存する部分は以下の3行です

# なぜか先頭でimportすると segmentation fault になるのでここで import
import faiss

# 次元数(768)で初期化
index_flat_l2 = faiss.IndexFlatL2(embeddings_np.shape[1])
# ベクトル情報を追加、6文章(vectors)登録される
index_flat_l2.add(embeddings_np)  # type: ignore

最後に

たったこれだけで faiss にベクトル情報を保存することができました
次回は faiss に保存した文章をもとにクエリとして与えた文章と似ている文章を探す処理を追加します

参考サイト

2024年12月19日木曜日

faiss を使うための準備として文章を数値化する方法

faiss を使うための準備として文章を数値化する方法

概要

前回 transformers を使って文章を数値化する方法を紹介しました
今回は数値化した情報を faiss で使えるように更に加工します

faissは文章の類似度を検索するツールです
過去に LangChain で使っていますが今回は faiss 単体で使ってみました

この記事では文章を数値化する部分だけを紹介しています
基本的には参考サイトにあるコードを動かしているだけですが事前学習済モデルなどが異なっています

環境

  • macOS 15.2
  • transformers 4.47.0

コード全体

最後の embeddings が faiss に保存するための通知情報です

from torch import Tensor
from transformers import AutoModel, AutoTokenizer


# このaverage_pool関数はbertの出力結果ではよく使われる手法、何をしているかの詳細は以下の説明に記載
def average_pool(last_hidden_states: Tensor, attention_mask: Tensor) -> Tensor:
    last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
    return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]


# faissに保存する文章、この文章と類似度検索をする
input_texts = [
    "好きな食べ物は何ですか?",
    "どこにお住まいですか?",
    "朝の電車は混みますね",
    "今日は良いお天気ですね",
    "最近景気悪いですね",
    "最近、出かけていないので、たまには外で食事でもどうですか?",
]

# モデルとトークナイザの取得、日本語なので日本語に特化したモデルを使用
tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
model = AutoModel.from_pretrained("cl-tohoku/bert-base-japanese")

# 文章の数値化
inputs = tokenizer(input_texts, padding=True, truncation=True, return_tensors="pt")

# 文章のベクトル化、768 次元のベクトル情報が取得できる (参考: https://github.com/cl-tohoku/bert-japanese)
outputs = model(**inputs)

# bert の結果を更によくするためのおまじない
embeddings = average_pool(outputs.last_hidden_state, inputs["attention_mask"])

# faiss で扱えるように numpy 配列に変換
embeddings_np = embeddings.cpu().detach().numpy()

説明

コード内のコメント以外で詳細に説明したほうがいい部分について説明します

average_pool 関数について

average_pool 関数は bert の出力結果を数値化する際によく使われる関数です
おまじないだと思ってもいいかなと思います
以下に軽く説明文を記載しておきます

この関数が使われる理由
1. 文全体の特徴量を作るため
BERTの出力 last_hidden_states は各トークンごとの特徴量を提供します。
文分類タスクなど、文全体を1つの特徴ベクトルで表現したい場合、このような平均プーリング処理がよく使われます。

2. パディングを無視して正しい平均を計算するため
文の長さは入力によって異なるため、パディングが挿入されます。
パディングを無視して、実際のトークンのみを対象にした平均を計算することで、モデルの性能を向上させます。
last_hidden = last_hidden_states.masked_fill(~attention_mask[..., None].bool(), 0.0)
attention_mask[..., None] を使ってマスクの次元を増やし、last_hidden_states に適用可能な形にします。
~attention_mask により、attention_mask が0(パディング)の部分を True に反転させます。
masked_fill を使い、attention_mask が0の位置の特徴量をゼロに置き換えます。
結果として、実際のトークンに対応する特徴量のみが残ります。
last_hidden.sum(dim=1)
各トークン(sequence_length次元)ごとの特徴量を足し合わせます。
パディング部分はゼロに置き換えられているため、実際のトークンの特徴量だけが合計されます。
attention_mask.sum(dim=1)[..., None]
attention_mask.sum(dim=1) は、各文の実際のトークン数を計算します(1 の数を合計)。
次元を合わせるために [..., None] で形状を (batch_size, 1) に変形。
最終的に、トークンの特徴量の合計をトークン数で割り、**トークンごとの平均特徴量(文全体の特徴量)**を計算します。

最後の変換について

簡単に言えば faiss 上で動作させるための変換です

embeddings_np = embeddings.cpu().detach().numpy()
以下の変換を行う理由:

.cpu(): GPU上のテンソルをCPU上に移動する。
.detach(): 計算グラフからテンソルを切り離す(推論時にメモリを節約)。
.numpy(): NumPy形式に変換し、後続の処理や保存で利用可能にする。

最後に

transformers でベクトル化した情報を faiss で使える形式に変換しました
次回は実際に faiss にデータを保存し類似度検索する部分を実装します

参考サイト