2024年12月26日木曜日
2024年12月21日土曜日
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 にベクトル情報を保存する方法を紹介します
環境
- 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 を使うための準備として文章を数値化する方法
概要
前回 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 にデータを保存し類似度検索する部分を実装します
参考サイト
2024年12月18日水曜日
transformers の BertJapaneseTokenizer を使って日本語をトークナイズする基本
概要
過去に transfomers の pipeline を使ってみました
今回はシンプルに日本語をトークナイズ(数値化)する機能を使ってみます
この仕組みは LLM におけるツールやユースケースなどどのケースでも使われる基本的な使い方になります
環境
- macOS 15.2
- Python 3.11.10
- transformers 4.47.0
準備
-
pipenv install transformers torch torchvision fugashi unidic-lite "numpy<2"
transformers 4.47.0 が numpy 2.0 以上に対応していないので注意です
サンプルコード
import torch
from transformers import AutoModel, AutoTokenizer
# Bert用の日本語モデルをダウンロード
bertjapanese = AutoModel.from_pretrained("cl-tohoku/bert-base-japanese")
tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
# 解析する文章の設定
line = "吾輩は猫である。"
# トークナイズ(数値化)する、テンソル
inputs = tokenizer(line, return_tensors="pt")
# トークナイズされた情報を確認する
print(tokenizer.decode(inputs["input_ids"][0]))
# おまけ: 特徴量の抽出(last_hidden_stateとpooler_outputの取得)
# これらは更に与えた文章の類似度検索や分類に使える
outputs = bertjapanese(**inputs)
複数の文章を与える
import torch
from transformers import AutoModel, AutoTokenizer
# Bert用の日本語モデルをダウンロード
bertjapanese = AutoModel.from_pretrained("cl-tohoku/bert-base-japanese")
tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese")
# 解析する文章の設定
lines = [
"吾輩は猫である。",
"名前はまだない",
]
# トークナイズ(数値化)する、テンソル
inputs = tokenizer(lines, padding=True, truncation=True, return_tensors="pt")
# トークナイズされた情報を確認する
print(tokenizer.decode(inputs["input_ids"][0]))
print(tokenizer.decode(inputs["input_ids"][1]))
# おまけ: 特徴量の抽出(last_hidden_stateとpooler_outputの取得)
# これらは更に与えた文章の類似度検索や分類に使える
outputs = bertjapanese(**inputs)
最後に
ここで数値化した情報やベクトル情報は更に類似度検索や分類器に与えられて次の処理に利用されます
次回はこれらのデータを使って分類器を実行する方法を紹介します
参考サイト
2024年2月8日木曜日
Transformers + Keras で Fine Tuning してみる
概要
とりあえず Fine Tuning できるかどうか試したかったので Transformers + keras でやってみました
基本は Transformers にある Fine Tuning のチュートリアルコードにコメントを入れているだけです
環境
- macOS 14.2.1
- Python 3.11.6
- transformers 4.37.2
インストール
- pipenv install numpy tensorflow datasets transformers
サンプルコード
基本的な流れは以下のとおりです
- 学習するデータの準備
- 事前学習済みモデルの取得
- 事前学習済みモデルの再学習
詳細な流れは以下のコード内のコメントに記載しています
import numpy as np
import tensorflow as tf
from datasets import load_dataset
from transformers import AutoTokenizer, TFAutoModelForSequenceClassification
# Fine-Tuning で新たに学習させるデータセットのダウンロード
# データセットは {'sentence': 'It is easy to slay the Gorgon.', 'label': 1, 'idx': 8548} というデータが 8551 個ある
dataset = load_dataset("glue", "cola")
dataset = dataset["train"]
# トークナイザーの取得
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
# トークナイズしてテキストを ID とトークンに変換
# padding=True にすることで最も長い文章の文字数に合わせる、溢れた部分はゼロ埋めになる
tokenized_data = tokenizer(dataset["sentence"], return_tensors="np", padding=True)
# print(tokenized_data[0])
# print(tokenized_data[0].ids)
# print(tokenized_data[0].type_ids)
# print(tokenized_data[0].attention_mask)
# 学習データの作成、トークナイズしたデータを辞書化するだけ
tokenized_data = dict(tokenized_data)
# 学習データの正解ラベルの作成
labels = np.array(dataset["label"])
# 事前学習済みモデルの取得、これに dataset を Fine-Tuning する
model = TFAutoModelForSequenceClassification.from_pretrained("bert-base-cased")
# モデルのコンパイル、損失関数の指定は不要 (らしい
# M2 Mac では以下のエラーが出るので最適化関数は tf.keras.optimizers.legacy.Adam を使用する
# E tensorflow/core/grappler/optimizers/meta_optimizer.cc:961] model_pruner failed: INVALID_ARGUMENT: Graph does not contain terminal node Adam/AssignAddVariableOp_10.
model.compile(optimizer=tf.keras.optimizers.legacy.Adam(3e-5))
# モデルの再学習
model.fit(tokenized_data, labels)
# モデルの保存
model.save("fine_tuned_bert_base_cased")
トラブルシューティング
- 低スペックマシンだとエラーになる
- ModuleNotFoundError: No module named ‘_lzma’
- xz をインストールして再度 python をインストールしましょう
- M2 Mac の場合最適化関するには tf.keras.optimizers.legacy.Adam を使用する
最後に
Transformers + keras でとりあえず Fine Tuning に触れてみました
元々公開されている LLM (GPT2) を使って新たに学習したモデルが作成できました
再学習させるのである程度のマシンスペックが必要になる他データ量に応じても負荷が変わるので注意しましょう
できれば GPU 環境があるのが望ましいです
また学習元の LLM も今回は GPT2 でしたが日本語の場合には日本語に特化した事前学習済みモデルがあるのでそういったものを選択したほうがより目的にあったモデルを作成することができます (例えば cl-tohoku/bert-base-japanese など)
参考サイト
2024年2月7日水曜日
Transformers で LLM を使って日本語の文章を簡単に解析する方法
概要
Transformers は HuggingFace が提供するライブラリで公開されている LLM を元で簡単に動かすことができるツールを提供してくれています
今回は導入ということで基本の pipeline 機能を試してみました
環境
- macOS 11.7.10
- Python 3.11.6
- transformers 4.37.2
インストール
今回は日本語を解析するので日本語解析に必要なライブラリも一緒にインストールできるパッケージを使います
-
pipenv install transformers["ja"]
マスク文字の予測
[MASK] としていした文章がある場合にその部分にどんな単語が入るのが正しいのかを予測してくれます
なお指定した日本語の LLM として有名な cl-tohoku さんのモデルを今回は使用します
マスク文字を予測する場合は bert-base-japanese-whole-word-masking というモデルを使用します
from transformers import AutoModel, AutoTokenizer, pipeline
model_name = "cl-tohoku/bert-base-japanese-whole-word-masking"
model = AutoModel.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# pipe = pipeline("fill-mask", model=model, tokenizer=tokenizer) # なぜかmodelを渡すとKeyError: 'logits'になるので注意
pipe = pipeline("fill-mask", model=model_name, tokenizer=tokenizer)
print(pipe("吾輩は[MASK]である。"))
[{'score': 0.7356272339820862, 'token': 6040, 'token_str': '猫', 'sequence': '吾輩 は 猫 で ある 。'}, {'score': 0.08630602061748505, 'token': 2928, 'token_str': '犬', 'sequence': '吾輩 は 犬 で ある 。'}, {'score': 0.010390125215053558, 'token': 10082, 'token_str': '狼', 'sequence': '吾輩 は 狼 で ある 。'}, {'score': 0.009217966347932816, 'token': 1410, 'token_str': '人間', 'sequence': '吾輩 は 人間 で ある 。'}, {'score': 0.007725571747869253, 'token': 6259, 'token_str': '僕', 'sequence': '吾輩 は 僕 で ある 。'}]
文章のネガポジ判定
その文章がネガティブなのかポジティブなのかを判定することができます
トークナイザーは先程と同じですがモデルはネガポジ用に学習しなおした kit-nlp/bert-base-japanese-sentiment-irony を使います
先程のマスク埋めのモデルを使っても分類はできますが分類先の情報などを持っていないため LABEL_0 などと表示されてしまいます
from transformers import AutoModel, AutoTokenizer, pipeline
model_name = "kit-nlp/bert-base-japanese-sentiment-irony"
tokenizer_name = "cl-tohoku/bert-base-japanese"
model = AutoModel.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
pipe = pipeline("sentiment-analysis", model=model_name, tokenizer=tokenizer)
print(pipe("今日は天気がよく散歩をしたので気分がいいです。"))
print(pipe("今日は天気が悪く散歩ができなかったので気分が悪いです。"))
[{'label': 'ポジティブ', 'score': 0.7811232209205627}]
[{'label': 'ネガティブ', 'score': 0.6790777444839478}]
文章生成
与えた文字列から始まる文章を生成してくれます
一昔前のスパム文章のような感じもします
トークナイザーは同じで文章生成のモデルは rinna/japanese-gpt2-small を使います
from transformers import AutoModel, AutoTokenizer, pipeline
model_name = "rinna/japanese-gpt2-small"
tokenizer_name = "cl-tohoku/bert-base-japanese"
model = AutoModel.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
pipe = pipeline("text-generation", model=model_name, tokenizer=tokenizer)
print(pipe("こんにちは。始めまして。"))
[{'generated_text': 'こんにちは。始めまして。私は、今日が、私の最初の仕事でした。この仕事は、ある方々のご支援を受けながら、今の仕事を続けてい
ことができました。本当にありがとうございました。私は、ここで、たくさん'}]
トラブルシューティング
文章生成で以下のようなエラーになる場合は protobuf をインストールしましょう
- pipenv install protobuf
ImportError:
T5Converter requires the protobuf library but it was not found in your environment. Checkout the instructions on the
installation page of its repo: https://github.com/protocolbuffers/protobuf/tree/master/python#installation and follow the ones
that match your environment. Please note that you may need to restart your runtime after installation.
モデルの保存場所
基本は自動でダウンロードされキャッシュされます
キャッシュが優先されて使われるので不要であれば削除しましょう
-
/home/user/.cache/huggingface/hub/.locks/models--cl-tohoku--bert-base-japanese-whole-word-masking
最後に
Transformers の pipeline 機能を使って LLM を使った日本語の簡単な解析方法を紹介しました
pipeline には他にも機能があるので興味があれば試してみてください
ただ機能ごとに使用するモデルが変わるのでそこだけ注意してください
モデルが違うとうまく動作しないことがあります
モデルをうまく探すコツは HaggingFace にタグがあるのでそれを使うと良いかなと思います
https://huggingface.co/models?pipeline_tag=text-generation