CLOVER🍀

That was when it all began.

llama-cpp-pythonで立おたOpenAI API互換のサヌバヌで、テキストをベクトル化しおみる

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

前に、こんな゚ントリヌを曞きたした。

OpenAI Python APIライブラリーからllama-cpp-pythonで立てたOpenAI API互換のサーバーのチャットモデルへアクセスしてみる - CLOVER🍀

この時は、llama-cpp-pythonで立おたOpenAI API互換のサヌバヌのチャットモデルのAPIにアクセスしおみたしたが、今回は埋め蟌みの
APIを䜿っおテキストのベクトル化をしおみたいず思いたす。

OpenAIの埋め蟌み

OpenAIの埋め蟌みEmbeddingsに関するドキュメントはこちら。

Embeddings

Introductionの抂念セクションでは、以䞋のように説明されおいたした。

  • コンテンツの意味を保持するこずを目的ずしたデヌタのベクトル衚珟
  • 類䌌のデヌタのチャンクには、近いEmbeddingsが含たれる傟向にある

Introduction

Embeddingsのペヌゞで、もう少し芋おみたしょう。

OpenAIのテキスト埋め蟌みは、テキスト文字列の関連性を枬定するものだそうです。

OpenAI’s text embeddings measure the relatedness of text strings.

䞀般的な甚途は以䞋のようです。

  • 怜玢 
 ク゚リ文字列ずの類䌌床によっお結果がランク付けされる
    • Search (where results are ranked by relevance to a query string)
  • クラスタリング 
 テキスト文字列が類䌌床によっおグルヌプ化される
    • Clustering (where text strings are grouped by similarity)
  • レコメンデヌション 
 関連するテキストを持぀アむテムをリコメンドする
    • Recommendations (where items with related text strings are recommended)
  • 異垞怜出 
 関連性の䜎い倖れ倀が特定される
    • Anomaly detection (where outliers with little relatedness are identified)
  • 倚様性枬定 
 類䌌性の分垃を分析する
    • Diversity measurement (where similarity distributions are analyzed)
  • 分類 
 テキスト文字列が最も類䌌したラベルで分類する
    • Classification (where text strings are classified by their most similar label)

埋め蟌みは、浮動小数点数字のベクトルリストで衚珟されたす。2぀のベクトルは、距離によっお関連性を枬るこずができたす。
距離が小さい堎合は関連性が高いこずを瀺し、距離が倧きい堎合は関連性が䜎いこずを瀺したす。

An embedding is a vector (list) of floating point numbers. The distance between two vectors measures their relatedness. Small distances suggest high relatedness and large distances suggest low relatedness.

距離の枬定には、コサむン類䌌床を䜿甚するこずが掚奚されおいたす。

Embeddings / Limitations & risks / Which distance function should I use?

䞀方で、どの距離関数を䜿うかはそれほど重芁ではないずも曞かれおいたす。

たた、OpenAIにおける埋め蟌みの長さは1に正芏化され、次の特性を持぀そうです。

  • コサむン類䌌床は、ドット積のみを䜿甚しお少し高速に蚈算できる
  • コサむン類䌌床ずナヌクリッド距離で、同じランキングが埗られる

埋め蟌みの利甚は、Embeddings APIぞのアクセスで行いたす。

ENDPOINTS / Embeddings

埋め蟌みの䜜成はこちらですね。

ENDPOINTS / Embeddings / Create embeddings

パラメヌタヌずしお、少なくずもテキストずモデルが必芁です。

埋め蟌みのモデルは2䞖代あり、第2䞖代モデルIDが-002ず第1䞖代モデルIDが-001がありたす。

モデルの䞖代 トヌクナむザヌ 最倧入力トヌクン
V2 cl100k_base 8191
V1 GPT-2/GPT-3 2046

第1䞖代のモデルは非掚奚ずなっおおり、第2䞖代のモデルの利甚が掚奚されおいたす。

第2䞖代のモデルはtext-embedding-ada-002のみです。トヌクナむザヌはcl100k_base、最倧入力トヌクンは8191です。
出力の次元数は1536です。

テキスト生成モデルず同様、埋め蟌みでもトヌクン数が料金に反映されたす。

Pricing

ナヌスケヌスずサンプルはこちら。

Embeddings / Use cases

ベクトル化したデヌタを保存する、ベクトルデヌタベヌスの玹介もありたす。

Embeddings / Limitations & risks / How can I retrieve K nearest embedding vectors quickly?

最埌に、リスクに぀いお。

Embeddings / Limitations & risks

OpenAIの埋め蟌みモデルは信頌床が䜎い、たたは瀟䌚的リスクを起こす可胜性があるこずが譊告されおいたす。
なんらかの緩和策を甚意した方がよい、ず。

Our embedding models may be unreliable or pose social risks in certain cases, and may cause harm in the absence of mitigations.

実際に起こった䟋や、モデルがい぀たでの知識を持っおいるかに぀いおも曞かれおいたす。

およそ埋め蟌みがどのようなものかはわかっおきたので、今回はテキストデヌタを埋め蟌みAPIを䜿っおベクトル化するずころを
やっおみようず思いたす。
OpenAI API互換のサヌバヌずしおは、llama-cpp-pythonを䜿いたす。

環境

今回の環境は、こちら。

$ python3 -V
Python 3.10.12

llama-cpp-pythonのバヌゞョン。

$ pip3 freeze | grep llama_cpp_python
llama_cpp_python==0.2.20

モデルはこちらを䜿いたす。
※埌で気づきたしたが、䜿うモデルはこれだずダメな気がしたすね 

TheBloke/Llama-2-7B-Chat-GGUF · Hugging Face

起動。

$ python3 -m llama_cpp.server --model llama-2-7b-chat.Q4_K_M.gguf

テキストをベクトル化する

たずは、テキストをベクトル化しおみたしょう。

OpenAI APIのラむブラリヌをむンストヌル。

$ pip3 install openai

バヌゞョン。

$ pip3 freeze | grep openai
openai==1.3.7

䜜成したプログラムはこちら。

to_vector.py

import sys
import time
from openai import OpenAI

text = sys.argv[1]

start_time = time.perf_counter()

openai = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy-api-key")

response = openai.embeddings.create(input=text, model="text-embedding-ada-002")

elapsed_time = time.perf_counter() - start_time

print(f"raw response = {response}")

print()

print(f"input text = {text}, to vector = {len(response.data[0].embedding)}")

print()

print(f"elapsed time = {elapsed_time:.3f} sec")

base_urlには、llama-cpp-pythonのOpenAI API互換の゚ンドポむントを指定。APIキヌは適圓で構いたせん。

openai = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy-api-key")

あずはEmbeddings Create APIを呌び出したす。モデル名はtext-embedding-ada-002を指定しおいたすが、llama-cpp-pythonサヌバヌを
察象ずする堎合はなんでも構いたせん。

response = openai.embeddings.create(input=text, model="text-embedding-ada-002")

あずはベクトル化した結果を衚瀺したす。ベクトル化する察象のテキストは、コマンドラむン匕数で指定するようにしおいたす。

print(f"raw response = {response}")

print()

print(f"input text = {text}, to vector = {len(response.data[0].embedding)}")

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

$ python3 to_vector.py 'Hello World.'

結果。

$ python3 to_vector.py 'Hello World.'
raw response = CreateEmbeddingResponse(data=[Embedding(embedding=[0.34358564019203186, 0.8082753419876099, 1.666818380355835, -1.0591312646865845, 0.06530340015888214, -0.5284493565559387, -0.6435621380805969, 1.137858271598816, 1.3028210401535034, 0.2946239709854126, -0.5300033092498779, -1.1164494752883911, 0.7548168301582336, 0.40373867750167847, 1.1290810108184814, -0.7566999197006226, 0.267472505569458, 0.7608996629714966, -0.017051421105861664, -1.01883065700531, -0.1597931832075119, -1.3573452234268188, 0.8877636790275574, -1.2786635160446167, -0.24713543057441711, 0.6535821557044983, -0.5414071083068848, 

〜省略〜

-2.555652379989624, 0.18512584269046783, 0.42112821340560913, -0.04915745556354523, -0.1607542186975479, -1.6608763933181763, -1.181817650794983, 0.655724287033081, -0.15193837881088257, 0.18946832418441772, -0.06836213171482086, -0.19648043811321259, -1.2785874605178833, 1.3186522722244263, 0.26095831394195557, 0.595634400844574, -0.5786678194999695, -1.9923450946807861, 0.5934603810310364, -0.5940259099006653, 0.1100892424583435, 0.6473436951637268, -0.3595812916755676, 0.5893478393554688, 1.695295810699463], index=0, object='embedding')], model='text-embedding-ada-002', object='list', usage=Usage(prompt_tokens=4, total_tokens=4))

input text = Hello World., to vector = 4096

elapsed time = 0.774 sec

このテキストなら実行時間は1秒かかっおいたせん。

ベクトル化した時の次元は4096でした。぀たり、埋め蟌みの結果のリストには4096個の芁玠が含たれおいたす。
OpenAIのドキュメントでは1536だったず思いたすが 。

$ python3 to_vector.py 'this is apple.'
raw response = CreateEmbeddingResponse(data=[Embedding(embedding=[0.11018591374158859, 0.6475743651390076, 2.998861074447632, -0.586685836315155, 0.09722156822681427, -1.347420334815979, 0.048076026141643524, 2.2477073669433594, 0.57712721824646, 0.28412631154060364, -0.2854914367198944, -1.3638404607772827, 1.5588339567184448, 0.07106658816337585, 1.6731715202331543, -0.9277191758155823, 0.5767037272453308, 1.015160083770752, -0.494839608669281, -0.9214868545532227, -1.3904584646224976, -1.822590708732605, 1.106003761291504, -1.147274374961853, -1.0618056058883667, 0.18969042599201202, -1.3648691177368164, 


〜省略〜

-1.3505096435546875, -1.383029580116272, -5.094209671020508, -0.881213366985321, 0.28645381331443787, -0.6735928058624268, 0.49416056275367737, -1.0014870166778564, 1.0167360305786133, -0.4009270668029785, 0.032198164612054825, -0.5705236792564392, 1.8743517398834229, -0.6221891045570374, -1.7606291770935059, 0.4385615885257721, -0.2885802686214447, -0.7018359899520874, 0.23984572291374207, 0.07199232280254364, 1.4006679058074951, 1.113827109336853], index=0, object='embedding')], model='text-embedding-ada-002', object='list', usage=Usage(prompt_tokens=5, total_tokens=5))

input text = this is apple., to vector = 4096

elapsed time = 0.934 sec

ずりあえず、テキストをベクトル化する方法はわかりたした。

怜玢しおみる

最埌に怜玢しおみたしょう。ちなみに、これはちょっずうたくいきたせんでした 。

ドキュメントに蚘茉されおいる怜玢のサンプルは、こちらです。

from openai.embeddings_utils import get_embedding, cosine_similarity

def search_reviews(df, product_description, n=3, pprint=True):
   embedding = get_embedding(product_description, model='text-embedding-ada-002')
   df['similarities'] = df.ada_embedding.apply(lambda x: cosine_similarity(x, embedding))
   res = df.sort_values('similarities', ascending=False).head(n)
   return res

res = search_reviews(df, 'delicious beans', n=3)

Embeddings / Use cases

openai.embeddings_utilsのcosine_similarityを䜿っおコサむン類䌌床を蚈算しおいるのですが、実は珟圚のOpenAI APIの
Pythonラむブラリヌにはこの関数はありたせん。

1.0になる時に削陀されたようです。

サンプルが動かないずいうissueが 。

v1.0 drops embeddings_util.py breaking semantic text search · Issue #676 · openai/openai-python · GitHub

この関数の定矩はこちらで、numpyがあれば簡単に移怍できるのでその方針にしたした。

https://github.com/openai/openai-python/blob/v0.28.1/openai/embeddings_utils.py#L65-L66

ずいうわけで、numpyをむンストヌル。

$ pip3 install numpy

バヌゞョン。

$ pip3 freeze | grep numpy
numpy==1.26.2

䜜成したプログラムはこちら。

search.py

import sys
from openai import OpenAI
import numpy as np

## https://github.com/openai/openai-python/blob/v0.28.1/openai/embeddings_utils.py#L65-L66
def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

seeds = [
    {"name": "Apple", "feature": "With red flesh and thin skin, it has a balanced taste of mild acidity and sweetness."},
    {"name": "Banana", "feature": "A yellow fruit with a smooth texture and mild sweetness, known for its high nutritional value."},
    {"name": "Grapes", "feature": "Purple in color, these small fruits cluster together with juicy and mildly tangy flavor."},
    {"name": "Melon", "feature": "A green fruit with a refreshing texture and aroma, rich in sweetness and water content."},
    {"name": "Orange", "feature": "Wrapped in an orange peel, it offers a harmonious blend of refreshing acidity and sweetness, rich in vitamin C."},
    {"name": "Strawberry", "feature": "Recognized by its red hue, it carries a distinctive fragrance and a sweet-tart taste, with tiny seeds adding texture."},
    {"name": "Pineapple", "feature": "Featuring yellow flesh, it has a sweet-tangy flavor and a unique texture, accompanied by a rich aroma."},
    {"name": "Mango", "feature": "An orange fruit with a rich aroma and intense sweetness, offering a smooth and luscious flesh."},
    {"name": "Kiwi", "feature": "Green flesh with a balanced combination of acidity and sweetness, enhanced by small black seeds for texture."},
    {"name": "Peach", "feature": "Displaying peach-colored flesh, it is juicy and soft with a sweet aroma, complemented by the peach's beautiful appearance."}
]

openai = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy-api-key")

docs = []

for seed in seeds:
    feature = seed["feature"]
    response = openai.embeddings.create(input=feature, model="text-embedding-ada-002")
    docs.append({"name": seed["name"], "feature": feature, "embedding": response.data[0].embedding})


query = sys.argv[1]
response = openai.embeddings.create(input=query, model="text-embedding-ada-002")
query_embedding = response.data[0].embedding

docs_with_similarity = [{
    "name": d["name"],
    "feature": d["feature"],
    "embedding": d["embedding"],
    "similarity":  cosine_similarity(d["embedding"], query_embedding)
} for d in docs]

sorted_docs = sorted(docs_with_similarity, key=lambda d: d["similarity"], reverse=True)

print(f"query = {query}")
print()

print("ranking:")
for doc in sorted_docs:
    print(f"  name: {doc['name']}")
    print(f"    feature: {doc['feature']}")
    print(f"    similarity: {doc['similarity']}")

こちらはコサむン類䌌床を蚈算する関数です。

## https://github.com/openai/openai-python/blob/v0.28.1/openai/embeddings_utils.py#L65-L66
def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

以䞋のような果物の名前ず特城のドキュメントに察しお

seeds = [
    {"name": "Apple", "feature": "With red flesh and thin skin, it has a balanced taste of mild acidity and sweetness."},
    {"name": "Banana", "feature": "A yellow fruit with a smooth texture and mild sweetness, known for its high nutritional value."},
    {"name": "Grapes", "feature": "Purple in color, these small fruits cluster together with juicy and mildly tangy flavor."},
    {"name": "Melon", "feature": "A green fruit with a refreshing texture and aroma, rich in sweetness and water content."},
    {"name": "Orange", "feature": "Wrapped in an orange peel, it offers a harmonious blend of refreshing acidity and sweetness, rich in vitamin C."},
    {"name": "Strawberry", "feature": "Recognized by its red hue, it carries a distinctive fragrance and a sweet-tart taste, with tiny seeds adding texture."},
    {"name": "Pineapple", "feature": "Featuring yellow flesh, it has a sweet-tangy flavor and a unique texture, accompanied by a rich aroma."},
    {"name": "Mango", "feature": "An orange fruit with a rich aroma and intense sweetness, offering a smooth and luscious flesh."},
    {"name": "Kiwi", "feature": "Green flesh with a balanced combination of acidity and sweetness, enhanced by small black seeds for texture."},
    {"name": "Peach", "feature": "Displaying peach-colored flesh, it is juicy and soft with a sweet aroma, complemented by the peach's beautiful appearance."}
]

特城を察象にベクトル化を行いたす。

docs = []

for seed in seeds:
    feature = seed["feature"]
    response = openai.embeddings.create(input=feature, model="text-embedding-ada-002")
    docs.append({"name": seed["name"], "feature": feature, "embedding": response.data[0].embedding})

怜玢文字列は、コマンドラむン匕数で䞎えおこちらもベクトル化したす。

query = sys.argv[1]
response = openai.embeddings.create(input=query, model="text-embedding-ada-002")
query_embedding = response.data[0].embedding

そしお、先ほどベクトル化した倀を加えたドキュメントず怜玢文字列のベクトルで、コサむン類䌌床をずりたす。

docs_with_similarity = [{
    "name": d["name"],
    "feature": d["feature"],
    "embedding": d["embedding"],
    "similarity":  cosine_similarity(d["embedding"], query_embedding)
} for d in docs]

結果をコサむン類䌌床の倀の降順で゜ヌト。

sorted_docs = sorted(docs_with_similarity, key=lambda d: d["similarity"], reverse=True)

結果衚瀺。

print(f"query = {query}")
print()

print("ranking:")
for doc in sorted_docs:
    print(f"  name: {doc['name']}")
    print(f"    feature: {doc['feature']}")
    print(f"    similarity: {doc['similarity']}")

詊しおみたす。

$ python3 search.py green

なんか埮劙な結果になりたした 。

query = green

ranking:
  name: Kiwi
    feature: Green flesh with a balanced combination of acidity and sweetness, enhanced by small black seeds for texture.
    similarity: 0.04215274492006107
  name: Banana
    feature: A yellow fruit with a smooth texture and mild sweetness, known for its high nutritional value.
    similarity: 0.0344139587395739
  name: Melon
    feature: A green fruit with a refreshing texture and aroma, rich in sweetness and water content.
    similarity: 0.025705263210870112
  name: Peach
    feature: Displaying peach-colored flesh, it is juicy and soft with a sweet aroma, complemented by the peach's beautiful appearance.
    similarity: 0.024638075297955937
  name: Mango
    feature: An orange fruit with a rich aroma and intense sweetness, offering a smooth and luscious flesh.
    similarity: 0.014813710044700355
  name: Apple
    feature: With red flesh and thin skin, it has a balanced taste of mild acidity and sweetness.
    similarity: 0.014664657619712652
  name: Pineapple
    feature: Featuring yellow flesh, it has a sweet-tangy flavor and a unique texture, accompanied by a rich aroma.
    similarity: 0.010742138593120263
  name: Grapes
    feature: Purple in color, these small fruits cluster together with juicy and mildly tangy flavor.
    similarity: 0.008852824997386993
  name: Orange
    feature: Wrapped in an orange peel, it offers a harmonious blend of refreshing acidity and sweetness, rich in vitamin C.
    similarity: -0.015618559051309908
  name: Strawberry
    feature: Recognized by its red hue, it carries a distinctive fragrance and a sweet-tart taste, with tiny seeds adding texture.
    similarity: -0.04353728373064396

惜しいような、やや倖しおいる感じもしたす。
他の単語で詊しおみたり、日本語でもやっおみたしたが、いずれも埮劙な結果になりたした 。
※ずいうか、モデルの遞択が誀っおいる気がしたす

ずりあえず、䜿い方はわかったので今回はここたでにしおおきたしょう。

おわりに

llama-cpp-pythonで立おたOpenAI API互換のサヌバヌで、テキストのベクトル化を詊しおみたした。

APIの䜿い方や甚語の意味はおよそ把握できたかなず思いたすが、結果がちょっず埮劙でした。

たあ、䜿っおいるのはOpenAI自䜓ではないので、今回はこれくらいにしおおきたしょう。