2024年12月22日日曜日

dspy.ai で Azure 互換のカスタムエンドポイントを設定する方法

dspy.ai で Azure 互換のカスタムエンドポイントを設定する方法

概要

Azure の OpenAI 互換だがエンドポイントが異なる場合に dspy を使う方法を紹介します

環境

  • Python 3.11.3
  • dspy 2.5.43

サンプルコード

import dspy

lm = dspy.LM(
    "azure/gpt-4-32k",
    api_key="xxx",
    api_version="",
    api_base="https://your-api-endpoint/ai/chat-ai/gpt4",
)
dspy.configure(lm=lm)

result = lm("Say this is a test!", temperature=0.7)
print(result)

ポイント

  • モデルの prefix は azure/ から始めること
  • api_base に独自の Azure 互換エンドポイントを設定すること
  • api_version は空で OK

最後に

内部的には litellm のプロバイダを使っているので litellm の使い方がわかっていれば簡単に他のプロバイダも設定できると思います

参考サイト

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日木曜日

emacs に copilot.el をインストールする方法

emacs に copilot.el をインストールする方法

概要

copilot が無料になって誰でも使えるようになったので emacs の設定をしました

環境

  • macOS 15.2
  • emacs 29.4
  • node 20.17.0

準備

  • node のインストール
    • nvm でローカルだけ使えるようにしてそこで emacs を起動しても OK
  • editorconfig、jsonrpc のインストール
    • package-list-packages などで melpa からインストールすれば OK

copilot.el のインストール

最初に straight.el をインストールしています
そのあとで copilot.el をインストールしています
初回起動時はいろいろと clone するので少し時間がかかります

; for copilot (with straight)
(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name
        "straight/repos/straight.el/bootstrap.el"
        (or (bound-and-true-p straight-base-dir)
            user-emacs-directory)))
      (bootstrap-version 7))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(use-package copilot
  :straight (:host github :repo "copilot-emacs/copilot.el" :files ("*.el"))
  :ensure t)

copilot サーバのインストール

M-x copilot-install-server で node が起動しサーバのインストールと起動が始まります

node で書かれておりバックグラウンドで npm が起動し copilot-node-server がインストールされます
執筆時点ではバージョンは 1.27.0 でした

copilot-install-server というバッファに「Comint finished at Thu Dec 19 14:44:17」という感じで表示されれば OK です

copilot へのログイン

M-x copilot-login を実行します

  • ワンタイムコードが発行されるのでコピーします
  • https://github.com/login/device にアクセスします、アクセスするブラウザは copilot を使う Github アカウントでログインしているブラウザを使います
  • コピーしたコードを入力し認証します

下のような画面が表示されればあとは許可すれば OK です

emacs 側には「Authenticated as GitHub user username.」と表示されれば OK です

使ってみる

適当に Python ファイルなどを開いてみましょう
M-x copilot-mode で起動します
自動で copilot-mode にする場合は以下を追加します

(add-to-list 'copilot-major-mode-alist '("python" . "python"))

上記がうまく動作しない場合は以下でも OK です

(add-hook 'python-mode-hook 'copilot-mode)

copilot によって補完された候補を入力するには M-x copilot-accept-completion を実行します
これをショートカットキーに登録しておいてもいいかなと思います

うまく補完してくれない場合には M-x copilot-diagnose を実行して copilot events バッファにエラーログが出ていないか確認しましょう

最後に

emacs + copilot の設定をしてみました
無料だと制限があるのでそのうち制限に引っかかって使えなくなるかなと思います

勝手にガンガン補完してくれるので慣れるまで結構かかるかもしれませんが慣れれば便利なのかもしれません

参考サイト

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

参考サイト

2024年12月18日水曜日

transformers の BertJapaneseTokenizer を使って日本語をトークナイズする基本

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年12月17日火曜日

Rlogin で多段 ssh する方法

Rlogin で多段 ssh する方法

概要

ProxyJump や ProxyCommand 的なことはできないっぽいので localhost にポートフォーワードしてそれをプロキシして次のサーバにつなぐのが主流なようです

なのでサーバ側でローカルポートフォーワードを許可していない場合には何か別の方法を模索する必要があります

環境

  • Rlogin 2.29.1

踏み台となるマシンの登録

ファイル -> サーバに接続 -> 新規で登録します

サーバー

  • エントリー(上) -> 好きな名前の設定
  • ホスト名 -> 踏み台マシンのIP
  • TCPポート -> 踏み台マシンのsshポート
  • ログインユーザ名 -> 踏み台マシンにログインするユーザ名
  • パスワード or パスフレーズ -> 踏み台マシンにログインするパスワード

あとは鍵などが必要であれば鍵を設定してください

サーバー(プロトコル)

サーバー -> プロトコル -> ポートフォーワード -> 新規で設定します

  • Listened -> Local Proxy Server
  • Host -> localhost
  • Port -> 10001 (空いてそうで競合しなさそうな localhost のポートならどこでもOK)

ポイントは踏み台に ssh した上で更に localhost の windows にポートフォーワードする設定もする点です

実際にsshするマシンの登録

ファイル -> サーバに接続 -> 新規で登録します

サーバ

  • エントリー(上) -> 好きな名前の設定
  • 前接続先(下) -> 先ほど登録した踏み台マシンの名前
  • ホスト名 -> sshするマシンのIP
  • TCPポート -> sshするマシンのsshポート
  • ログインユーザ名 -> sshするマシンにログインするユーザ名
  • パスワード or パスフレーズ -> sshするマシンにログインするパスワード

異なるのは踏み台マシンを「前接続先」とやらに設定する点です

プロキシ設定

サーバー -> プロキシ設定で設定します
先ほどの踏み台マシンで localhost にポートフォーワードしたポートをプロキシに設定します

  • Select Proxy Protocol -> SOCKS5
  • Proxy Server Address -> localhost
  • Socket Port -> 10001

動作確認

あとはサーバー接続の一覧から実際にsshするマシンを選択すれば踏み台->sshするマシンの順番で認証などが聞かれるので認証すればsshできるはずです

なおターミナルを閉じるときに localhost にポートフォーワードしたプロセスも閉じるように聞かれるので一緒に閉じましょう

同じような感じで踏み台が複数ある場合はその分だけローカルにポートフォーワードして次のマシンには踏み台の順番にプロキシを設定すれば 3,4段と多段の ssh もできるかなと思います

最後に

踏み台マシンがsshのポートフォーワードを許可していない場合にはどうすればいいのだろうか

踏み台マシンに nc などがあれば ProxyCommand が使えるが Rlogin で ProxyCommand 的なことはできるのだろうか

また ssh -W でも ssh 的には多段 ssh を実施できるが結局 ProxyCommand が必要になるので厳しそう

幸いにも Rlogin は日本人の開発者っぽいので最悪自分で開発すればいいのだと思う

参考サイト