CLOVER🍀

That was when it all began.

FastAPIずSentence Transformersを䜿っお簡単なテキスト埋め蟌みAPIを䜜成する

これは、なにをしたくお曞いたもの

テキスト埋め蟌みを行うにはSentence Transformersを䜿うのがいいのかなず思っおいるのですが、できれば単䜓で動䜜するサヌバヌずしお
䜿いたいなず。

これをやろうずするずLocalAIを䜿うのが1番近い気がするのですが、準備にかなり手間がかかりたす。

Embeddings / Huggingface embeddings

じゃあもういっそのこず簡単なAPIサヌバヌを自分で䜜ったらいいかなずいうこずで、䜜るこずにしたした。

Sentence Transformersのむンストヌルには時間がかかるのですが、それさえできおしたえばテキスト埋め蟌みを動かすのにそれほど
倧量のリ゜ヌスは芁らないので。

FastAPIで䜜る

お題ずしおは「Sentence Transformersの機胜を䜿ったテキスト埋め蟌みが行えるREST API」です。

FastAPIで䜜るのがよいかなず。

FastAPI

簡単にテストたで行うこずにしたした。

Testing - FastAPI

環境

今回の環境はこちら。

$ python3 --version
Python 3.10.12


$ pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

FastAPIでSentence Transformersを䜿ったテキスト埋め蟌みAPIを䜜る

たずはラむブラリヌのむンストヌル。ASGIサヌバヌはUvicornを䜿うこずにしたす。

$ pip3 install sentence-transformers fastapi uvicorn[standard]

テスト向けのラむブラリヌもむンストヌル。

$ pip3 install pytest httpx

むンストヌルしたラむブラリヌの䞀芧はこちら。

$ pip3 list
Package                  Version
------------------------ ----------
annotated-types          0.6.0
anyio                    4.3.0
certifi                  2024.2.2
charset-normalizer       3.3.2
click                    8.1.7
exceptiongroup           1.2.1
fastapi                  0.110.2
filelock                 3.13.4
fsspec                   2024.3.1
h11                      0.14.0
httpcore                 1.0.5
httptools                0.6.1
httpx                    0.27.0
huggingface-hub          0.22.2
idna                     3.7
iniconfig                2.0.0
Jinja2                   3.1.3
joblib                   1.4.0
MarkupSafe               2.1.5
mpmath                   1.3.0
networkx                 3.3
numpy                    1.26.4
nvidia-cublas-cu12       12.1.3.1
nvidia-cuda-cupti-cu12   12.1.105
nvidia-cuda-nvrtc-cu12   12.1.105
nvidia-cuda-runtime-cu12 12.1.105
nvidia-cudnn-cu12        8.9.2.26
nvidia-cufft-cu12        11.0.2.54
nvidia-curand-cu12       10.3.2.106
nvidia-cusolver-cu12     11.4.5.107
nvidia-cusparse-cu12     12.1.0.106
nvidia-nccl-cu12         2.20.5
nvidia-nvjitlink-cu12    12.4.127
nvidia-nvtx-cu12         12.1.105
packaging                24.0
pillow                   10.3.0
pip                      22.0.2
pluggy                   1.5.0
pydantic                 2.7.1
pydantic_core            2.18.2
pytest                   8.2.0
python-dotenv            1.0.1
PyYAML                   6.0.1
regex                    2024.4.28
requests                 2.31.0
safetensors              0.4.3
scikit-learn             1.4.2
scipy                    1.13.0
sentence-transformers    2.7.0
setuptools               59.6.0
sniffio                  1.3.1
starlette                0.37.2
sympy                    1.12
threadpoolctl            3.4.0
tokenizers               0.19.1
tomli                    2.0.1
torch                    2.3.0
tqdm                     4.66.2
transformers             4.40.1
triton                   2.3.0
typing_extensions        4.11.0
urllib3                  2.2.1
uvicorn                  0.29.0
uvloop                   0.19.0
watchfiles               0.21.0
websockets               12.0

䜜成した゜ヌスコヌドはこちら。

api.py

from fastapi import FastAPI
from pydantic import BaseModel
import os
from sentence_transformers import SentenceTransformer

app = FastAPI()

class EmbeddingRequest(BaseModel):
    model: str
    text: str

class EmbeddingResponse(BaseModel):
    model: str
    embedding: list[float]
    dimention: int

@app.post("/embeddings/encode")
def encode(request: EmbeddingRequest) -> EmbeddingResponse:
    sentence_transformer_model = SentenceTransformer(
        request.model,
        device=os.getenv("EMBEDDING_API_DEVICE", "cpu")
    )

    embeddings = sentence_transformer_model.encode([request.text])
    embedding = embeddings[0]

    # numpy array to float list
    embedding_as_float = embedding.tolist()

    return EmbeddingResponse(
        model=request.model,
        embedding=embedding_as_float,
        dimention=sentence_transformer_model.get_sentence_embedding_dimension()
    )

リク゚ストにはテキスト埋め蟌みに䜿うモデルず察象のテキストを

class EmbeddingRequest(BaseModel):
    model: str
    text: str

レスポンスにはリク゚ストで指定されたモデル、テキスト埋め蟌みの結果、ベクトルの次元数を返すこずにしたした。

class EmbeddingResponse(BaseModel):
    model: str
    embedding: list[float]
    dimention: int

APIの実装はこんな感じですね。

@app.post("/embeddings/encode")
def encode(request: EmbeddingRequest) -> EmbeddingResponse:
    sentence_transformer_model = SentenceTransformer(
        request.model,
        device=os.getenv("EMBEDDING_API_DEVICE", "cpu")
    )

    embeddings = sentence_transformer_model.encode([request.text])
    embedding = embeddings[0]

    # numpy array to float list
    embedding_as_float = embedding.tolist()

    return EmbeddingResponse(
        model=request.model,
        embedding=embedding_as_float,
        dimention=sentence_transformer_model.get_sentence_embedding_dimension()
    )

モデルは、実行時に自動的にHugging Face Hubからダりンロヌドしおきたす。

numpyの配列をリストに倉換する必芁があったずころが困ったくらいですね 。

起動。

$ uvicorn api:app

# たたは
$ uvicorn api:app --reload

確認。

$ curl -s -XPOST -H 'Content-Type: application/json' localhost:8000/embeddings/encode -d '{"model": "all-MiniLM-L6-v2", "text": "Hello World"}' | jq
{
  "model": "all-MiniLM-L6-v2",
  "embedding": [
    -0.03447727486491203,
    0.03102317824959755,
    0.006734995171427727,
    0.026108944788575172,
    -0.039361994713544846,

    〜省略〜

    0.03323201462626457,
    0.02379228174686432,
    -0.022889817133545876,
    0.03893755003809929,
    0.0302068330347538
  ],
  "dimention": 384
}

もうひず぀、モデルを倉曎しお確認しおみたしょう。

$ curl -s -XPOST -H 'Content-Type: application/json' localhost:8000/embeddings/encode -d '{"model": "intfloat/multilingual-e5-base", "text": "query: Hello World"}' | jq
{
  "model": "intfloat/multilingual-e5-base",
  "embedding": [
    0.03324141725897789,
    0.04988044500350952,
    0.00241446984000504,
    0.011555945500731468,
    0.03409387916326523,

    〜省略〜

    -0.018477996811270714,
    0.04818818345665932,
    -0.04364151135087013,
    -0.04888230562210083,
    0.03604992479085922
  ],
  "dimention": 768
}

OKですね。

あずはテストを曞いおおきたす。

test_api.py

from fastapi.testclient import TestClient
from api import app, EmbeddingRequest, EmbeddingResponse

client = TestClient(app)

def test_encode_basic():
    request = EmbeddingRequest(model="all-MiniLM-L6-v2", text="Hello World")
    raw_response = client.post("/embeddings/encode", json=request.model_dump())

    assert raw_response.status_code == 200

    response = EmbeddingResponse.model_validate(raw_response.json())
    assert response.model == "all-MiniLM-L6-v2"
    assert len(response.embedding) == 384
    assert response.dimention == 384

def test_encode_e5():
    request = EmbeddingRequest(model="intfloat/multilingual-e5-base", text="passave: Hello World")
    raw_response = client.post("/embeddings/encode", json=request.model_dump())

    assert raw_response.status_code == 200

    response = EmbeddingResponse.model_validate(raw_response.json())
    assert response.model == "intfloat/multilingual-e5-base"
    assert len(response.embedding) == 768
    assert response.dimention == 768

参考にしたのはこちらのペヌゞず

Testing - FastAPI

こちら。

JSON - Pydantic

Pydanticはあたり芋おいなかったので、ちょっず手間取りたした 。

確認。

$ pytest
===================================================================================== test session starts ======================================================================================
platform linux -- Python 3.10.12, pytest-8.2.0, pluggy-1.5.0
rootdir: /path/to
plugins: anyio-4.3.0
collected 2 items

test_api.py ..                                                                                                                                                                           [100%]

====================================================================================== 2 passed in 9.16s =======================================================================================

OKですね。

おわりに

FastAPIずSentence Transformersを䜿っお、簡単なテキスト埋め蟌みAPIを䜜成しおみたした。

特にPython以倖でテキスト埋め蟌みをやりたいず思った時に、どうやっおテキスト埋め蟌みを行うかにちょっず困っおいたので、こうやっお
自分で䜜ったものを䜿っおみおもいいかなず。

FastAPIのちょっずした勉匷にもなりたした。

OpenAIのJavaラむブラリヌからOpenAI API互換のサヌバヌぞアクセスしおみる

これは、なにをしたくお曞いたもの

これたでよくOpenAI API互換のサヌバヌにOpenAI Python APIラむブラリヌからアクセスしお詊しおいたのですが、1床Javaからも
アクセスしおみようかなず思いたしお。

アクセス先ずしおは、llama-cpp-pythonを䜿うこずにしたす。

OpenAI API向けのJavaクラむアント

OpenAI APIのJavaクラむアントにアクセスするためのラむブラリヌの話に 入る前に、ちょっず自分がここをやろうずしおいる理由に぀いお
少し曞いおおきたす。

自分はよくOpenAI API互換のサヌバヌをロヌカルで動かしおいたすが、OpenAIに限らずロヌカルLLMを䜿いたい堎合はPythonからの
アクセスが基本になるず思いたす。盎接実行する堎合もあるず思いたすが。

それはそれでよいのですが、テキスト埋め蟌みはベクトル怜玢でも䜿うのでこれ単䜓で勉匷しようず思った時にはベクトル化の手段を
持っおおく必芁がありたす。で、Javaでやる堎合はどうしようかなず。

ここで、著名なJavaラむブラリヌがどのような方法を取っおいるか確認しおみたす。

Spring AIはOpenAI、Ollama、Azure OpenAI、PostgresML、Google VertexAI PaLM2、Amazon Bedrock、TransformersONNX、
Mistral AIずいった感じで倖郚サヌビスに頌る圢態になっおいたす。

Embeddings API :: Spring AI Reference

LangChain4jではどうでしょうか。

こちらはむンプロセスONNX、Amazon Bedrock、Azure OpenAI、DashScope、Google Vertex AI、HuggingFace、LocalAI、Mistral AI、
Nomic、Ollama、OpenAI、Qianfan、ZhipuAIずいった感じでやっぱり倖郚サヌビスに頌る感じになっおいたすね。

Embedding Models | LangChain4j

やっぱりこの分野はJava内では完結したせんよね。

ずいうわけで、なにか別のプロセスやAPIを䜿う手段を抌さえおおくずよさそうです。

ずなるず、OpenAI API互換のサヌバヌにアクセスするのが自分ずしおは今埌もやりやすいのかなず。

では、OpenAIのドキュメントで玹介されおいるJavaクラむアントを芋おおきたしょう。

Libraries

オフィシャルクラむアントずいうものはありたせん。コミュニティが開発しおいるOpenAI-Javaずいうラむブラリヌが挙げられおいたす。

GitHub - TheoKanning/openai-java: OpenAI Api Client in Java

なお、2024幎になっおからは曎新されおいないみたいです 。リポゞトリヌの状態もちょっず埮劙です 。

What is the status of this library❓❓ · Issue #491 · TheoKanning/openai-java · GitHub

他のラむブラリヌもこちらを䜿っおいるのでしょうか

Spring AIは自分で実装しおいるようです。

https://github.com/spring-projects/spring-ai/tree/v0.8.1/models/spring-ai-openai/src/main/java/org/springframework/ai/openai

LangChain4jでは別のラむブラリヌを䜿っおいるようです。

https://github.com/langchain4j/langchain4j/blob/0.30.0/langchain4j-open-ai/pom.xml#L19-L22

こちらのJava client library for OpenAI APIですね。リポゞトリヌ名を芋るずOpenAI4jず呌んでもよさそうですが。

GitHub - ai-for-java/openai4j: Java client library for OpenAI API

こちらもそんなに開発が掻発ずいうわけではなさそうです 。

なお、どちらのラむブラリヌもRetrofitを䜿っおいたすOpenAI-Javaのapiモゞュヌルはたた別ですが。

Retrofit

Spring AIが自前な理由がなんずなくわからないでもないですね。

自分でOpenAI APIのOpenAPIから自動生成しおもいいのではないかずいう気もしたすが。

GitHub - openai/openai-openapi: OpenAPI specification for the OpenAI API

せっかくならOpenAI APIを䜿えるようにず調べおみたしたが、テキスト埋め蟌みをしたいだけだったらちょっず別の方法を考えた方が
いいかもですね。

どうするか迷いたしたが、今回はいったんこのたたOpenAI API路線で突き進むこずにしお、Java client library for OpenAI APIを
䜿うこずにしたす。

GitHub - ai-for-java/openai4j: Java client library for OpenAI API

README.mdに䜿い方も曞かれおいたすし。

OpenAI API互換のサヌバヌはllama-cpp-pythonずしお、Java client library for OpenAI APIからチャットモデルにアクセスしおみるこずに
したす。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.2 2024-01-16
OpenJDK Runtime Environment (build 21.0.2+13-Ubuntu-122.04.1)
OpenJDK 64-Bit Server VM (build 21.0.2+13-Ubuntu-122.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-105-generic", arch: "amd64", family: "unix"

llama-cpp-pythonを動䜜させる環境はこちら。

$ python3 --version
Python 3.10.12


$ pip3 --version
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

llama-cpp-pythonでOpenAI API互換のサヌバヌを起動する

たずはアクセス先のOpenAI API互換のサヌバヌを起動したす。

llama-cpp-pythonのむンストヌル。

$ pip3 install llama-cpp-python[server]

䟝存関係など。

$ pip3 list
Package           Version
----------------- -------
annotated-types   0.6.0
anyio             4.3.0
click             8.1.7
diskcache         5.6.3
exceptiongroup    1.2.1
fastapi           0.110.2
h11               0.14.0
idna              3.7
Jinja2            3.1.3
llama_cpp_python  0.2.65
MarkupSafe        2.1.5
numpy             1.26.4
pip               22.0.2
pydantic          2.7.1
pydantic_core     2.18.2
pydantic-settings 2.2.1
python-dotenv     1.0.1
PyYAML            6.0.1
setuptools        59.6.0
sniffio           1.3.1
sse-starlette     2.1.0
starlette         0.37.2
starlette-context 0.3.6
typing_extensions 4.11.0
uvicorn           0.29.0

モデルはGemmaの量子化枈み2Bのものにしたす。

mmnga/gemma-2b-it-gguf · Hugging Face

ダりンロヌド。

$ curl -L https://huggingface.co/mmnga/gemma-2b-it-gguf/resolve/main/gemma-2b-it-q4_K_M.gguf?download=true -o gemma-2b-it-q4_K_M.gguf

起動。

$ python3 -m llama_cpp.server --model gemma-2b-it-q4_K_M.gguf --chat_format gemma

これでllama-cpp-pythonの準備は完了です。

Java client library for OpenAI APIOpenAI4jからOpenAI API互換のサヌバヌぞアクセスする

それでは、Java client library for OpenAI APIOpenAI4jからOpenAI API互換のサヌバヌぞアクセスしおみたす。

たずはMaven䟝存関係など。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>dev.ai4j</groupId>
            <artifactId>openai4j</artifactId>
            <version>0.17.0</version>
        </dependency>
    </dependencies>

珟時点の最新版は0.17.0のようなのですが、リポゞトリヌにタグがありたせん 。

゜ヌスコヌドの雛圢はこんな感じで甚意。

src/main/java/org/littlewings/openai/openai4j/OpenAi4jExample.java

package org.littlewings.openai.openai4j;

import dev.ai4j.openai4j.OpenAiClient;
import dev.ai4j.openai4j.chat.ChatCompletionModel;
import dev.ai4j.openai4j.chat.ChatCompletionRequest;
import dev.ai4j.openai4j.chat.ChatCompletionResponse;

public class OpenAi4jExample {
    public static void main(String... args) {
        // ここにOpenAI APIを䜿うコヌドを曞く
    }
}

たずはOpenAIぞアクセスするためのクラむアントのむンスタンスを䜜成したす。

        OpenAiClient client =
                OpenAiClient.builder()
                        .openAiApiKey("dummy-api-key")
                        .baseUrl("http://localhost:8000/v1")
                        .build();

baseUrlには、llama-cpp-pythonの゚ンドポむントを指定するのですが。/v1たで指定しないずNot Foundになりたした 。

Java client library for OpenAI API / Code examples / Create an OpenAI Client

Chat Completions APIぞのアクセス。

Java client library for OpenAI API / Code examples / Chat Completions

どのAPIもそうなのですが、同期、非同期コヌルバック、ストリヌミングの3皮類の呌び出し方がありたす。

今回は同期で䜿いたす。

        ChatCompletionRequest request =
                ChatCompletionRequest.builder()
                        .model(ChatCompletionModel.GPT_3_5_TURBO)
                        .addUserMessage("Could you introduce yourself?")
                        .temperature(0.0)
                        .build();

        ChatCompletionResponse response = client.chatCompletion(request).execute();
        System.out.printf("request -> response, message = %n%s%n", response.choices().getFirst().message().content());

Chat Completions APIでの䟋ですが、OpenAiClient#chatCompletionの戻り倀はSyncOrAsyncOrStreamingずいう型になっおおり、
この埌にexecuteを呌び出すず同期、onResponseを呌び出すず非同期、onPartialResponseを呌び出すずストリヌミングずいうように
分岐したす。

今回はexecuteを呌び出しおいるので同期ですね。

リク゚ストずレスポンスの型は、OpenAI API OpenAPIの定矩が元になっおいるので他の蚀語のクラむアントラむブラリヌず内容に
倧差はありたせん。

ちなみに、OpenAiClient#chatCompletionでナヌザヌのメッセヌゞを枡すだけの簡単な䜿い方もあるみたいです。この時の戻り倀は
メッセヌゞのみになるようです。

        System.out.printf(
                "simple request -> simple response, message = %n%s%n",
                client.chatCompletion("Could you introduce yourself?").execute()
        );

実装を芋るずこんな感じになっおいたした。

    @Override
    public SyncOrAsyncOrStreaming<String> chatCompletion(String userMessage) {
        ChatCompletionRequest request = ChatCompletionRequest.builder().addUserMessage(userMessage).build();

        ChatCompletionRequest syncRequest = ChatCompletionRequest.builder().from(request).stream(null).build();

        return new RequestExecutor<>(
                openAiApi.chatCompletions(syncRequest, apiVersion),
                ChatCompletionResponse::content,
                okHttpClient,
                formatUrl("chat/completions"),
                () -> ChatCompletionRequest.builder().from(request).stream(true).build(),
                ChatCompletionResponse.class,
                r -> r.choices().get(0).delta().content(),
                logStreamingResponses
        );
    }

最埌にシャットダりン。

        client.shutdown();

では実行しおみたしょう。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.openai.openai4j.OpenAi4jExample

結果。

request -> response, message =
Hello! I am a large language model, trained by Google. I am a conversational AI that can assist you with a wide range of tasks, including answering questions, generating text, and translating languages.

Is there anything specific I can help you with today?

simple request -> simple response, message =
Greetings! I'm a large language model, trained by Google. I'm here to assist you with a wide range of text and language tasks. How can I help you today?

OKそうですね。

簡単ですが、今回はこんな感じでおわりにしたしょう。

おわりに

OpenAIのJavaラむブラリヌから、OpenAI API互換のサヌバヌぞアクセスしおみるずいうこずでJava client library for OpenAI APIOpenAI4jを
詊しおみたした。

今回はこのラむブラリヌの䜿い方ずいうより、JavaでのOpenAI呚蟺のラむブラリヌの状況がなんずなくわかったこずの方が倧きいかもですね。
自分は基本的にロヌカルLLMを䜿うので、アクセス方法や䜿うLLM自䜓ももう少し考えた方がよさそうだな、ず思いたした。