2024年12月31日火曜日

vmtoolsd がちゃんと起動しているか確認する方法

vmtoolsd がちゃんと起動しているか確認する方法

概要

Ubuntu で稼働している vmtoolsd が systemctl で見ると active なのにうまく動作していないことがあります
そのときに起動しているか確認する方法を紹介します

環境

  • Ubuntu 24.04
  • vmtoolsd 12.4.5

コマンド

おそらく以下が一番カンタンです

vmtoolsd --cmd "info-get guestinfo.ip"

もしくは

vmware-rpctool "info-get guestinfo.ip"

vmtoolsd が正常に起動している場合は IP アドレスが返りますが動作していない場合は unknown が返ってきます

2024年12月30日月曜日

emacs で正規表現を使いながら置換する方法

emacs で正規表現を使いながら置換する方法

概要

query-replace-regexp を使います

環境

  • macOS 15.2
  • emacs 29.4

使用例

例えば

999 * leftSidePositionMagnification

(999 - leftSidePositionMagnification)

にしたい場合は以下のようにします

\(...\) \* leftSidePositionMagnification
(\1 - leftSidePositionMagnification)

元あった数字を使いまわしたい場合は検索時にカッコで囲い置換時に \1 という感じで数字で参照できます

最後に

元の値を残しつつ置換したい場合に便利です

2024年12月29日日曜日

HammerSpoon で実行した処理を強制終了する方法

HammerSpoon で実行した処理を強制終了する方法

概要

ツールバーにある hammerspoon から reload config できるのが一番ですが実行中の hammerspoon でツールバーを選択するとクルクルになるので reload できません

またアクティビティモニターで強制終了してもいいですが hammerspoon が動いているとカーソル位置を持っていかれるので大変です

一番簡単に終了できる方法を紹介します

環境

  • macOS 15.2
  • HammerSpoon 1.0.0

強制終了方法

ターミナルで以下を実行します

  • pkill Hammerspoon

or

  • pkill -9 Hammerspoon

これが一番てっとり早いです
終了後は途中から実行したり最初に戻って再実行すれば OK です

最後に

HammerSpoon で長いタスクを実行する場合はカーソル情報が HammerSpoon に乗っ取られるので必ず強制終了する道を残しておきましょう

2024年12月28日土曜日

HammerSpoon でドラッグアンドドロップする方法

HammerSpoon でドラッグアンドドロップする方法

概要

過去 に HammerSpoon で紹介しましたがうまく動作しないケースがあるようです
その場合は event.types.leftMouseDragged イベントをうまく使うとドラッグアンドドロップできるようになります

環境

  • macOS 15.2
  • HammerSpoon 1.0.0

ドラッグアンドドロップする関数

function dragMouse(pointA, pointB, sleep)
    local event = hs.eventtap.event
    local xdiff = pointB.x - pointA.x
    local ydiff = pointB.y - pointA.y
    -- ここの割り算の3の数が少ないほどドラッグが滑らかになる
    -- 速いドラッグにしたい場合は数を大きくする
    local loop = math.floor(math.sqrt((xdiff * xdiff) + (ydiff * ydiff)) / 3)
    local xinc = xdiff / loop
    local yinc = ydiff / loop
    sleep = math.floor((sleep * 1000) / loop)
    midPoint = {x=pointA.x, y=pointA.y}
    for i = 1,loop do
        midPoint.x = midPoint.x + xinc
        midPoint.y = midPoint.y + yinc
        newPoint = {x = math.floor(midPoint.x), y = math.floor(midPoint.y)}
        hs.mouse.absolutePosition(newPoint)
        event.newMouseEvent(event.types.leftMouseDragged, newPoint):post()
        hs.timer.usleep(sleep)
    end
    newPoint = {x = math.floor(pointB.x), y = math.floor(pointB.y)}
    hs.mouse.absolutePosition(newPoint)
    event.newMouseEvent(event.types.leftMouseDragged, newPoint):post()
end

使い方

hs.hotkey.bind({"cmd", "alt"}, "T", function()
    local upSlideStartPoint = {x=500, y=500}
    local upSlideEndPoint = {x=200, y=200}
    -- カーソル位置を移動
    hs.mouse.absolutePosition(upSlideStartPoint)
    -- マウスを押した状態にする
    hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseDown, upSlideStartPoint):post()
    -- マウスを押しながら移動する
    -- 1の数字の部分を調整することでドラッグアンドドロップのスピードが変わる
    -- 数が少ないほど速くなる
    dragMouse(upSlideStartPoint, upSlideEndPoint, 1)
    -- マウスを離す
    hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseUp, upSlideEndPoint):post()
end)

最後に

ポイントはカーソルを移動した際に event.types.leftMouseDragged を実行する点かなと思います

多くのサンプルはマウスダウンをしてそのままカーソルを移動してその後マウスを上げるになっているのですがそれだとダメなようです

2024年12月27日金曜日

HammerSpoon で Mac の自動操作入門

HammerSpoon で Mac の自動操作入門

概要

Hammerspoon は Mac でショートカットキーを設定しそのショートカットキーに割り当てられたスクリプトを実行し Mac の操作を自動化できるツールです
今回はインストールからマウスクリックとスワイプ操作を実装してみました

環境

  • macOS 15.2
  • hammerspoon 1.0.0

インストール

  • brew install hammerspoon

アプリケーションの一覧に hammerspoon のアイコンがあるのでクリックして起動します

するとツールバーに hammerspoon のアイコンが常駐するようになるのでこれで起動完了です

権限の付与

システム設定 -> プライバシーとセキュリティ -> アクセシビリティ

で権限を付与してあげましょう

通知の許可

システム設定 -> 通知

から hammerspoon の通知を許可してあげましょう

Hello World

まずは Hello World です

ツールバーにある hammerspoon のアイコンをクリックし「Open config」を選択します

するとテキストエディタが起動するので以下の lua スクリプトを貼り付けましょう

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function()
  hs.alert.show("Hello World!")
end)

これで準備 OK です
ツールバーのアイコンから「Reload config」しキーボードで cmd + ctrl + alt + w キーを押すと Hello World と表示されることが確認できます

ファイルの場所は

Open config で開けるファイル ~/.hammerspoon/init.lua にあります
これを直接好きなエディタで編集してから Reload config しても OK です

特定の箇所をクリックする

長いのでキーは cmd + alt + c にしています
特定の箇所を左クリックする場合は hs.eventtap.leftClick します
最後に完了を知らせるために hs.notify.new で通知していますが毎回通知が来て面倒という場合には Hello World で使った hs.alert.show を使うのがいいかなと思います

lua の場合コメントハイフン2つで表現します

hs.hotkey.bind({"cmd", "alt"}, "C", function()
    -- 座標を指定してクリック (: X=500, Y=300)
    local clickPoint = {x=500, y=300}
    
    -- マウスカーソルを指定位置に移動
    hs.mouse.absolutePosition(clickPoint)
    
    -- 左クリックイベントを発生させる
    hs.eventtap.leftClick(clickPoint)

    -- 通知でクリック完了を知らせる (任意)
    hs.notify.new({title="Hammerspoon", informativeText="Left click at (500, 300)"}):send()
end)

スワイプ(ドラッグアンドドロップ)

なぜかテキストが選択状態にはならないようですがちゃんとこれでドラッグアンドドロップできてします

newMouseEvent で leftMouseDown して押しっぱなしにしたあとに移動させて leftMouseUp させます

hs.hotkey.bind({"cmd", "alt"}, "C", function()
    -- スワイプの開始座標と終了座標を設定
    local startPoint = {x=500, y=500}
    local endPoint = {x=700, y=700}

    -- マウスカーソルを開始位置に移動
    hs.mouse.absolutePosition(startPoint)
    hs.timer.usleep(50000) -- 50ms待機

    -- マウスダウン (押しっぱなしにする)
    local mouseDown = hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseDown, startPoint)
    mouseDown:post()
    hs.timer.usleep(300000) -- 300ms待機 (長押しエミュレート)

    -- スワイプの動作 (滑らかな移動)
    local steps = 50 -- ステップ数
    for i = 1, steps do
        local progress = i / steps
        local currentPoint = {
            x = startPoint.x + (endPoint.x - startPoint.x) * progress,
            y = startPoint.y + (endPoint.y - startPoint.y) * progress
        }
        hs.mouse.absolutePosition(currentPoint)
        hs.timer.usleep(5000) -- 5ms待機 (スムーズに移動)
    end

    -- マウスアップ (クリック解除)
    local mouseUp = hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseUp, endPoint)
    mouseUp:post()
end)

以下で強制的にウィンドウをアクティブにしても選択状態にはなりませんでした
テキストを選択状態にするには別の方法があるのかもしれません

local app = hs.application.get("Google Chrome")
if app then
    app:activate() -- アプリケーションをアクティブにする
else
    hs.alert.show("Google Chrome is not running!")
end

最後に

Mac の HammerSpoon で Mac の自動操作を試してみました
ショートカットキーをフックにして動作するのでこのあたりも自動化できるとさらに便利かなと思いました

他にもいろいろと使い道がありそうなのでもう少し触ってみて追加の Tips があればまた紹介します

参考サイト

2024年12月26日木曜日

huggingface からダウンロードしたモデルが保存されるパス

huggingface からダウンロードしたモデルが保存されるパス

概要

いつも忘れるのでメモ

環境

  • macOS 15.2
  • transformers 4.47.0

パス

例えば cl-tohoku/bert-base-japanese をダウンロードした場合は以下のパスに保存されます

./.cache/huggingface/hub/models--cl-tohoku--bert-base-japanese/snapshots/32fc2539e890ac278af0e56efb9aa6570f777345/pytorch_model.bin

最後に

結構ファイルが大きいのでディスクを圧迫するようなら削除してしまって OK です

2024年12月25日水曜日

dspy でプロンプトの最適化をしてみる

dspy でプロンプトの最適化をしてみる

概要

前回 dspy を使って Metrics 機能で判定を行ってみました
今回は dspy の目玉機能でもあるプロンプトの最適化機能を試してみました
簡単に言えば最適化機能はファインチューニングみたいなものでプロンプトをよりよくすることができる再学習機能です

環境

  • Python 3.11.3
  • dspy 2.5.43

コード全体

まずはコード全体を紹介します
以下で機能部分ごとにコードを紹介しています

import dspy
import pandas as pd
from dspy.evaluate.evaluate import Evaluate
from dspy.teleprompt import BootstrapFewShotWithRandomSearch

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)


# プロンプトの入出力をカスタムするクラス
class BasicMathQA(dspy.Signature):
    """算数の文章問題を読んで、解答を数値で出力する。"""

    question = dspy.InputField(desc="算数の文章問題")
    answer = dspy.OutputField(
        desc="算数の文章問題の解答。数値のみ出力する。単位や説明、句読点は含まないこと。",
    )


# 計算に特化したChainOfThoughtを管理するカスタムモジュール
class CoTMathQA(dspy.Module):
    def __init__(self):
        super().__init__()
        self.generate_answer = dspy.ChainOfThought(BasicMathQA)

    def forward(self, question):
        return self.generate_answer(question=question)


# データセットのダウンロード
df = pd.read_csv(
    "https://raw.githubusercontent.com/google-research/url-nlp/main/mgsm/mgsm_ja.tsv",
    sep="\t",
    header=None,
)
df.rename(columns={0: "question", 1: "answer"}, inplace=True)


# 前半50を学習データにする、dspyで使えるようにdspy.Example形式に変換する
mgsm_ja_trainset = [
    dspy.Example(question=row.question, answer=row.answer).with_inputs("question")  # type: ignore
    for row in df[:50].itertuples()
]
# 後半50を学習データにする、dspyで使えるようにdspy.Example形式に変換する
mgsm_ja_devset = [
    dspy.Example(question=row.question, answer=row.answer).with_inputs("question")  # type: ignore
    for row in df[51:101].itertuples()
]

# プロンプトを学習するためのモデルを作成 https://dspy.ai/deep-dive/optimizers/bfrs/
# metric には独自の評価関数を設定することが可能でこの関数を制御することで精度を上げることもできる
# num_threadsは1じゃないとRateLimitになる場合が多い、今回のデータセットでだいたい30分くらいかかる
teleprompter = BootstrapFewShotWithRandomSearch(
    metric=dspy.evaluate.answer_exact_match,  # type: ignore
    max_labeled_demos=10,
    max_bootstrapped_demos=8,
    num_threads=4,
)

# teleprompter の学習の開始、これが最適化するということ
# 最適化後のプロンプトを使って質問する
compiled_cot_math_qa = teleprompter.compile(CoTMathQA(), trainset=mgsm_ja_trainset)
compiled_cot_math_qa(
    question="周囲の長さが300メートルの池の周りに木を植えることにした。5メートル間隔で植える場合、木は何本必要か?"
)

# 最適化されたプロンプトの保存
compiled_cot_math_qa.save("./compiled_cot_math_qa/", save_program=True)

# 最適化前と後での精度を比較する
evaluate = Evaluate(
    devset=mgsm_ja_devset,
    num_threads=5,
    display_progress=True,
    display_table=True,
)

accuracy_original = evaluate(CoTMathQA(), metric=dspy.evaluate.answer_exact_match, display_table=0)  # type: ignore
accuracy_compiled = evaluate(compiled_cot_math_qa, metric=dspy.evaluate.answer_exact_match, display_table=0)  # type: ignore
print(f"Original accuracy: {accuracy_original}")
print(f"Compiled accuracy: {accuracy_compiled}")

解説

まずはプロンプトを最適化するために学習データを用意します
今回は https://raw.githubusercontent.com/google-research/url-nlp/main/mgsm/mgsm_ja.tsv を使っていますがこれと同じ形式の tsv を用意すれば独自のデータでプロンプトを最適化することができます

学習/評価するデータは 50 個ずつ用意します
先程の tsv ファイルが全部 100 個あるので前半と後半で分けてそれぞれを学習/訓練データにしています

プロンプトの最適化にはそれに応じたクラスを使います
今回は BootstrapFewShotWithRandomSearch という FewShot + ランダム検索で最適化を行います
一番のポイントは metric でこれが評価関数になります
今回は dspy が用意している評価関数を使っていますがここに独自の評価関数を入れることでプロンプトの精度を向上させることができます
num_threads は増やせば増やすほど最適化が速く終わります

ファインチューニング同様に最適化したプロンプトは保存できます
なので毎回最適化する必要はなく良い評価値になったプロンプトを毎回使い回すことができます

あとは最適化前と最適化後で評価します

Original accuracy: 86.0
Compiled accuracy: 88.0

自分の環境ではそこまで大きな差は出ませんでしたがそれでもまだまだ精度向上の余地はあるかなと思います

最後に

dspy のプロンプト最適化を試してみました
基本的にはファインチューニングのような再学習機能になります
しかし以下のようなメリットがあります

  • Keras + LoRA のような難しい技術を知る必要がない
  • LLM さえ使えればいいので自分でベースとなるモデルを用意する必要がない
  • LLM をコールするだけで最適化できるので最適化のためのマシンスペックが不要

デメリットとしては

  • 優秀な LLM は基本有料なので最適化するためにかなりのコストがかかる可能性がある
  • 最適化中に 429 になる可能性があり面倒

あたりかなと思います
LLM という強力なツールがある上で精度のいい回答を作成することができるようになるのでファインチューニングに比べてかなり楽かなとは思います
結構複雑な回答を学ばせることもできるので一から数値化したりする必要のあるファインチューニングより学習コストも低いです

参考サイト

2024年12月24日火曜日

dspy で二値判定と分類器を試してみる

dspy で二値判定と分類器を試してみる

概要

前回 dspy の基本となる Programs 機能を一通り試してみました
今回は Metrics の機能として判定系を試しました

環境

  • 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)


class NegativePositiveJudge(dspy.Signature):
    """Judge if the answer is factually correct based on the context."""

    context = dspy.InputField(desc="Context for the prediction")
    question = dspy.InputField(desc="Question to be answered")
    answer = dspy.InputField(desc="Answer for the question")
    factually_correct = dspy.OutputField(
        desc="Is the answer factually correct based on the context?",
        prefix="Factual[positive/negative]:",
    )


judge = dspy.ChainOfThought(NegativePositiveJudge)
factual = judge(
    context="今日はとても気分がいいです",
    question="contextで与えた文章がネガティブかポジティブか判定してください",
    answer="positive",
)
# ポジティブな文章を与えて期待する答えもポジティブなので結果はyesになる
print(factual)

factual = judge(
    context="今日はとても気分が悪いです、最悪の日です",
    question="contextで与えた文章がネガティブかポジティブか判定してください",
    answer="positive",
)
# ネガティブな文章を与えて期待する答えがポジティブなので結果はnoになる
print(factual)


class Classifier(dspy.Signature):

    context = dspy.InputField(desc="Context for the prediction")
    question = dspy.InputField(desc="Question to be answered")
    factually_correct = dspy.OutputField(
        desc="Classify a word or sentence passed in context. There are three categories: 'Fruits', 'Toys' and 'Others.'",
    )


classifier = dspy.ChainOfThought(Classifier)
factual = classifier(
    context="みかん",
    question="contextで与えた文章を分類してください。",
)
print(factual)

factual = classifier(
    context="餃子",
    question="contextで与えた文章を分類してください。",
)
print(factual)

factual = classifier(
    context="hawksnowlog",
    question="contextで与えた文章を分類してください。",
)
print(factual)

factual = classifier(
    context="ちいかわのぬいぐるみはかわいいです。",
    question="contextで与えた文章を分類してください。",
)
print(factual)

factual = classifier(
    context="冬はみかんがおいしいですねー。",
    question="contextで与えた文章を分類してください。",
)
print(factual)

二値判定

今回はネガポジ判定をしています
それ以外にも Signature の条件をいろいろ変更することでいろいろな判定をすることが可能です

class NegativePositiveJudge(dspy.Signature):
    """Judge if the answer is factually correct based on the context."""

    context = dspy.InputField(desc="Context for the prediction")
    question = dspy.InputField(desc="Question to be answered")
    answer = dspy.InputField(desc="Answer for the question")
    factually_correct = dspy.OutputField(
        desc="Is the answer factually correct based on the context?",
        prefix="Factual[positive/negative]:",
    )


judge = dspy.ChainOfThought(NegativePositiveJudge)
factual = judge(
    context="今日はとても気分がいいです",
    question="contextで与えた文章がネガティブかポジティブか判定してください",
    answer="positive",
)
# ポジティブな文章を与えて期待する答えもポジティブなので結果はyesになる
print(factual)

factual = judge(
    context="今日はとても気分が悪いです、最悪の日です",
    question="contextで与えた文章がネガティブかポジティブか判定してください",
    answer="positive",
)
# ネガティブな文章を与えて期待する答えがポジティブなので結果はnoになる
print(factual)

分類器

今回は3つのタグに分類してもらっています
分類の場合は期待する answer を設定する必要がないです

class Classifier(dspy.Signature):

    context = dspy.InputField(desc="Context for the prediction")
    question = dspy.InputField(desc="Question to be answered")
    factually_correct = dspy.OutputField(
        desc="Classify a word or sentence passed in context. There are three categories: 'Fruits', 'Toys' and 'Others.'",
    )


classifier = dspy.ChainOfThought(Classifier)
factual = classifier(
    context="みかん",
    question="contextで与えた文章を分類してください。",
)
print(factual)

factual = classifier(
    context="餃子",
    question="contextで与えた文章を分類してください。",
)
print(factual)

factual = classifier(
    context="hawksnowlog",
    question="contextで与えた文章を分類してください。",
)
print(factual)

factual = classifier(
    context="ちいかわのぬいぐるみはかわいいです。",
    question="contextで与えた文章を分類してください。",
)
print(factual)

factual = classifier(
    context="冬はみかんがおいしいですねー。",
    question="contextで与えた文章を分類してください。",
)
print(factual)

結果は以下の通りです、ちゃんと分類できていることがわかります

Prediction(
    reasoning='The word "みかん" refers to a type of citrus fruit commonly known as a mandarin or tangerine in English. It is a type of fruit.',
    factually_correct='Fruits'
)
Prediction(
    reasoning='The word "餃子" refers to a type of food, specifically dumplings, which are not related to fruits or toys. Therefore, it does not fit into the categories of \'Fruits\' or \'Toys\'.',
    factually_correct='Others'
)
Prediction(
    reasoning='The word "hawksnowlog" does not fit into the categories of \'Fruits\' or \'Toys\'. It appears to be a unique or specific term that does not relate to common fruits or toys.',
    factually_correct='Others'
)
Prediction(
    reasoning='The context mentions "ちいかわのぬいぐるみ" which translates to "Chiikawa plush toy" in English. This indicates that the subject is a type of toy.',
    factually_correct='Toys'
)
Prediction(
    reasoning='The context mentions "みかん" which is a type of fruit. The sentence is discussing the taste of "みかん" in winter, which is related to the category of \'Fruits\'.',
    factually_correct='Fruits'
)

最後に

dspy で Metrics 機能を使っていろいろな判定機能を実装してみました
LLM を使った判定/分類器は基本的にファインチューニングを使って再学習するのが定番ですが dspy を使えばそれをする必要がなくかつ簡単に判定することができます

更に dspy には評価を行う機能もありデータセットを与えて期待した答えるになっているかの精度を出す仕組みもあるのでそれを使えば Signature で与えているコンテキスト(InputField)や回答方針(OutputField)が正確かどうかの評価もできます

次回はそのあたりの機能を試したいので dspy の最大の機能である Optimizer を試します

参考サイト

2024年12月23日月曜日

dspy.ai の Programs を一通り使ってみた

dspy.ai の Programs を一通り使ってみた

概要

dspy の Programs (ChainOfThought や ReAct など) を使ってみました
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-azure-endpoint/ai/chat-ai/gpt4",
)
dspy.configure(lm=lm)


# 日本語化
class JapaneseQA(dspy.Signature):
    question = dspy.InputField()
    answer = dspy.OutputField(desc="The answer should be in Japanese")


generate_jp_answer = dspy.ChainOfThought(JapaneseQA)
result = generate_jp_answer(question="こんにちわ!あなたは誰ですか?")
print(result)

# ヒントあり質問
result = generate_jp_answer(
    question="hawksnowlogがよく書くブログのカテゴリは何ですか?",
    hint="hawksnowlogのぶログはこれです、https://hawksnowlog.blogspot.com/",
)
print(result)

# 問題特化な質問はProgramOfThoughtを使ったほうが良さそう
pot = dspy.ProgramOfThought(JapaneseQA)

question = "サラはリンゴを5個持っています。彼女は店からさらに 7 個のリンゴを購入します。サラは今リンゴを何個持っていますか?"
result = pot(question=question)
print(result)


# 質問に対して更に質問する場合にReActを使ってスレッドっぽく質問できる
# ReAct は複数の InputField を定義することができ履歴を管理する history フィールドを追加する
# tools はCallableなdspy.Toolクラスのインスタンスを指定することができるらしくその実行結果を回答に反映することができるらしい
class JapaneseQA4ReAct(dspy.Signature):
    question = dspy.InputField()
    history = dspy.InputField(desc="Chat history")
    answer = dspy.OutputField(desc="The answer should be in Japanese")


react_module = dspy.ReAct(JapaneseQA4ReAct, tools=[])

question = "サラはリンゴを5個持っています。彼女は店からさらに 7 個のリンゴを購入します。サラは今リンゴを何個持っていますか?"
result = react_module(question=question, history="")
print(result)
history = f"USER: {question}\nAI: {result.answer}"

question2 = "更に30個購入しました。サラは今リンゴを何個持っていますか?"
result = react_module(question=question2, history=history)
print(result)


# Retrive 特定のツールを使った検索結果や、APIの結果を考慮して回答させることができる
# いわゆる RAG (Retrieval Augmented Generation)
# 以下は ColBERTv2 を使ってサーバからのレスポンスを考慮して回答するようになっている
# 他にも FAISS や AzureAISearch などが使える https://github.com/stanfordnlp/dspy/blob/main/docs/docs/deep-dive/retrieval_models_clients/FaissRM.md
colbertv2_wiki17_abstracts = dspy.ColBERTv2(
    url="http://20.102.90.50:2017/wiki17_abstracts"
)
dspy.settings.configure(rm=colbertv2_wiki17_abstracts)

retriever = dspy.Retrieve(k=3)

query = "When was the first FIFA World Cup held?"

topK_passages = retriever(query).passages  # type: ignore

for idx, passage in enumerate(topK_passages):
    print(f"{idx+1}]", passage, "\n")

日本語化の仕方

dspy を使って LLM から返答を得る場合基本的には英語で返答してきます
なので日本語で返答するように指示します

# 日本語化
class JapaneseQA(dspy.Signature):
    question = dspy.InputField()
    answer = dspy.OutputField(desc="The answer should be in Japanese")


generate_jp_answer = dspy.ChainOfThought(JapaneseQA)
result = generate_jp_answer(question="こんにちわ!あなたは誰ですか?")
print(result)

ヒントありで質問

ヒントありで質問するとそのヒントをもとに回答を作成することができます

# ヒントあり質問
result = generate_jp_answer(
    question="hawksnowlogがよく書くブログのカテゴリは何ですか?",
    hint="hawksnowlogのぶログはこれです、https://hawksnowlog.blogspot.com/",
)
print(result)

問題形式の質問

ChainOfThought と正直変わらないと思うのですが問題形式の場合は ProgramOfThought を使ったほうがいいようです

pot = dspy.ProgramOfThought(JapaneseQA)

question = "サラはリンゴを5個持っています。彼女は店からさらに 7 個のリンゴを購入します。サラは今リンゴを何個持っていますか?"
result = pot(question=question)
print(result)

反復的に質問する

ReAct は前の質問の結果を次の質問に継続することができます
スレッド的な使い方ができます
回答の履歴を管理するフィールドを新たに追加するのがポイントです

class JapaneseQA4ReAct(dspy.Signature):
    question = dspy.InputField()
    history = dspy.InputField(desc="Chat history")
    answer = dspy.OutputField(desc="The answer should be in Japanese")


react_module = dspy.ReAct(JapaneseQA4ReAct, tools=[])

question = "サラはリンゴを5個持っています。彼女は店からさらに 7 個のリンゴを購入します。サラは今リンゴを何個持っていますか?"
result = react_module(question=question, history="")
print(result)
history = f"USER: {question}\nAI: {result.answer}"

question2 = "更に30個購入しました。サラは今リンゴを何個持っていますか?"
result = react_module(question=question2, history=history)
print(result)

RAG

ColBERTv2 という Retriever を使っていますが FAISS なども使えます
少し凝ったことをしたい場合には RAG を使うことになるかなと思います

colbertv2_wiki17_abstracts = dspy.ColBERTv2(
    url="http://20.102.90.50:2017/wiki17_abstracts"
)
dspy.settings.configure(rm=colbertv2_wiki17_abstracts)

retriever = dspy.Retrieve(k=3)

query = "When was the first FIFA World Cup held?"

topK_passages = retriever(query).passages  # type: ignore

for idx, passage in enumerate(topK_passages):
    print(f"{idx+1}]", passage, "\n")

最後に

dspy の Programs を試してみました
どれも LLM を使って質問に一工夫加えてほしい回答を得るという仕組みになっています

Programs の他にも Metrics や Evaluation という機能があるので興味があれば使ってみるといいかなと思います

参考サイト

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 を使いましたがモデルとトークナイザを変えるだけでも結果は変わってくるかなと思います

参考サイト