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 にデータを保存し類似度検索する部分を実装します

参考サイト

0 件のコメント:

コメントを投稿