2023年7月11日火曜日

rinnaのjapanese-cloob-vit-b-16でベクトル埋め込みを生成しPineconeに保存して問い合わせを行なう

 rinna株式会社が公開しているjapanese-cloob-vit-b-16のモデル(rinna株式会社によるプレスリリース)とベクトル・データベースPineconeを使って、テキストによる画像検索を行なうアプリケーションを作成します。APEXではユーザーインタフェースを作成します。

以下の処理を実装します。

  1. オブジェクト・ストレージに保存した画像のベクトル埋め込み(embedding)をrinna社のモデルをより生成し、Pineconeに保存する。
  2. 問い合わせテキストのベクトル埋め込みをrinna社のモデルをより生成し、Pinconeのインデックスを検索する。
  3. 検索された画像をアプリケーションに表示する。
作成したアプリケーションのGIF動画です。動物の名前で画像を検索しています。


上記のアプリケーションのエクスポートを、以下に置いています。
https://github.com/ujnak/apexapps/blob/master/exports/multimodal-search.zip

これより、アプリケーションの実装について説明します。


Pineconeのインデックス



rinna社のjapanese-cloob-vit-b-16は512次元のベクトル埋め込みを生成するため、作成するPineconeのインデックスのDimensions512とします。ベクトル埋め込みを生成する際に、Pinecone社が公開している記事Multi-modal ML with OpenAI's CLIPに従ってベクトル埋め込みを正規化します。記事に従ってMetricdotproductを指定しますが、cosineでも結果は変わりません。


PinconeのインデックスのURLは、APEXアプリケーションの置換文字列G_INDEXの置換値として設定します。


PineconeのAPI呼び出しに使うWeb資格証明として、PINECONE_APIが作成済みとします。作成手順は、こちらの記事に記載されています。



オブジェクト・ストレージへの画像アップロード




オブジェクト・ストレージにバケットを作成し、検索対象とする画像をアップロードします。

以下の例ではバケットsourcesを作成しています。


検索対象とする画像を、作成したバケットにアップロードします。


バケット名ネームスペースは、APEXアプリケーションの置換文字列G_BUCKETG_NAMESPACEの置換値として設定します。

検索の対象とする画像を、バケットにアップロードします。

オブジェクト・ストレージに関する設定は、こちらの記事で紹介しています。APEXアプリケーションから操作するために、あらかじめAPIユーザーの作成Web資格証明を作成しておきます。また、事前承認済リクエストを生成するため、ポリシーを追加します。それぞれの準備については、こちらの記事が参考になります。

これからの説明では、Web資格証明としてOCI API Access静的IDOCI_API_ACCESS)が作成済みとします。


ベクトル埋め込みを生成するRESTサービス



無料で使えるOracle CloudのAmpere A1のインスタンスを4OCPU24GBメモリのシェイプで作成します。その上でPythonのコードを実行してベクトル埋め込みを生成します。

インスタンスの生成手順はこちらの記事に記載しています。Oracle Cloud MarketplaceにあるPyTorch - Ampere Optimized Frameworks - Ubuntu 20.04からインスタンスを作成します。

APIサーバーとして実行するためにFlaskを使います。準備作業については、こちらの記事を参照してください。rinna株式会社のjapanese-cloob-vit-b-16を利用する手順は、Huggingfaceのrinna/japanese-cloob-vit-b-16の記載通りです。パッケージのインストールを実施(1. Install package)し、テスト用のコードを実行(2. Run)して、動作を確認します。

画像のURLまたはテキストを受け付けて、rinna/japanese-cloob-vit-b-16のモデルを使ってベクトル埋め込みを生成するサーバーのコードです。generate-embedding.pyとして作成します。

import logging
import sys
import os
import json
from flask import Flask, request
import io
import requests
from PIL import Image
import torch
import japanese_clip as ja_clip
import numpy as np
# ログレベルと出力先の設定
logging.basicConfig(stream=sys.stdout, level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
# rinna cloob-vit-b-16のロード
device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = ja_clip.load("rinna/japanese-cloob-vit-b-16", device=device)
tokenizer = ja_clip.load_tokenizer()
# /embed-text の呼び出しに対する処理
app = Flask(__name__)
@app.route('/embed-text', methods=['POST'])
def embed_text():
if request.method == 'POST':
embedding_dict = {}
text = request.json['text']
print("received text", text)
text_arr = [ text ]
encodings = ja_clip.tokenize(
texts=text_arr,
max_seq_len=77,
device=device,
tokenizer=tokenizer, # this is optional. if you don't pass, load tokenizer each time
)
with torch.no_grad():
text_features = model.get_text_features(**encodings)
print(text_features.shape)
print(text_features.min(), text_features.max())
# cosine similarity
# text_list = text_features[0].tolist()
# dot product (normalize)
text_emb = text_features.detach().cpu().numpy()
text_emb = text_emb.T / np.linalg.norm(text_emb, axis=1)
text_emb = text_emb.T
print(text_emb.shape)
print(text_emb.min(), text_emb.max())
text_list = text_emb[0].tolist()
embedding_dict["text"] = text
embedding_dict["embedding"] = text_list
embedding_json = json.dumps(embedding_dict, ensure_ascii=False)
return embedding_json
# /embed-imageの呼び出しに対する処理
@app.route('/embed-image', methods=['POST'])
def embed_image():
if request.method == 'POST':
embedding_dict = {}
url = request.json['url']
print("received url", url)
img = Image.open(io.BytesIO(requests.get(url).content))
image = preprocess(img).unsqueeze(0).to(device)
with torch.no_grad():
image_features = model.get_image_features(image)
print(image_features.shape)
print(image_features.min(), image_features.max())
# cosine similarity
# image_list = image_features[0].tolist()
# dot product (normalize)
image_emb = image_features.detach().cpu().numpy()
image_emb = image_emb.T / np.linalg.norm(image_emb, axis=1)
image_emb = image_emb.T
print(image_emb.shape)
print(image_emb.min(), image_emb.max())
image_list = image_emb[0].tolist()
embedding_dict["url"] = url
embedding_dict["embedding"] = image_list
embedding_json = json.dumps(embedding_dict, ensure_ascii=False)
return embedding_json
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8443, ssl_context=('./certs/fullchain.pem', './certs/privkey.pem'), debug=True)

Ubuntuのサーバー上で実行します。

python generate-embedding.py

ubuntu@mywhisper2:~$ python generate-embedding.py 

/usr/lib/python3/dist-packages/requests/__init__.py:89: RequestsDependencyWarning: urllib3 (1.26.13) or chardet (3.0.4) doesn't match a supported version!

  warnings.warn("urllib3 ({}) or chardet ({}) doesn't match a supported "

 * Serving Flask app 'generate-embedding'

 * Debug mode: on

INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.

 * Running on all addresses (0.0.0.0)

 * Running on https://127.0.0.1:8443

 * Running on https://10.0.0.131:8443

WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.

 * Running on all addresses (0.0.0.0)

 * Running on https://127.0.0.1:8443

 * Running on https://10.0.0.131:8443

INFO:werkzeug:Press CTRL+C to quit

Press CTRL+C to quit

INFO:werkzeug: * Restarting with stat

 * Restarting with stat

/usr/lib/python3/dist-packages/requests/__init__.py:89: RequestsDependencyWarning: urllib3 (1.26.13) or chardet (3.0.4) doesn't match a supported version!

  warnings.warn("urllib3 ({}) or chardet ({}) doesn't match a supported "

WARNING:werkzeug: * Debugger is active!

 * Debugger is active!

INFO:werkzeug: * Debugger PIN: 849-624-968

 * Debugger PIN: 849-624-968



テキストのベクトル埋め込みを生成するには、https://ホスト名/embed-text{ text: "問い合わせ文字列" }というJSONドキュメントをPOSTします。レスポンスとして{ text: "問い合わせ文字列", embedding: [ベクトル埋め込み] }が返されます。

画像のベクトル埋め込みを生成するには、https://ホスト名/embed-image{ url: "画像のURL" }というJSONドキュメントをPOSTします。レスポンスとして{ url: "画像のURL", embedding: [ベクトル埋め込み] }が返されます。

ベクトル埋め込みを生成するRESTサービスを実装したサーバーは、APEXアプリケーションの置換文字列G_EMBEDの置換値であるベースURLとして設定します。


APEXアプリケーションの作成



アプリケーション作成ウィザードを起動し、空のアプリケーションを作成します。名前Multimodal Searchとします。

アプリケーションの作成を実行します。


アプリケーションが作成されたら、アプリケーション定義置換を開きます。

置換文字列G_INDEXG_EMBEDG_NAMESPACEG_BUCKET置換値を設定します。


オブジェクト・ストレージに認証なしでアクセスするため(ベクトル埋め込みを生成するPythonのコードは、認証なしでオブジェクト・ストレージにアクセスする必要があります)、事前承認済リクエストのURLを生成します。

事前承認済リクエストのURLを保持するアプリケーション・アイテムG_PREAUTH_URLを作成します。


アプリケーションの計算を作成し、G_PREAUTH_URLに事前承認済リクエストのURLを設定します。

計算ポイント認証後計算タイプファンクション本体を選択します。計算として以下のコードを記述します。

declare
C_BASE_URL constant varchar2(200) := 'https://objectstorage.us-ashburn-1.oraclecloud.com/n/' || :G_NAMESPACE || '/b/' || :G_BUCKET || '/p/';
l_request clob;
l_response clob;
l_response_json json_object_t;
l_access_uri varchar2(400);
begin
select json_object(
key 'accessType' value 'AnyObjectRead',
key 'name' value 'standard-' || sys_guid(),
key 'timeExpires' value systimestamp + interval '1' hour
) into l_request from dual;
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept', 'application/json', p_reset => false);
apex_web_service.set_request_headers('Content-Type', 'application/json', p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => C_BASE_URL
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => 'OCI_API_ACCESS'
);
l_response_json := json_object_t(l_response);
l_access_uri := l_response_json.get_string('accessUri');
return 'https://objectstorage.us-ashburn-1.oraclecloud.com' || l_access_uri;
end;


北米AshburnリージョンがURLに直書きされているため、リージョンが異なる場合はコードの変更が必要です。

オブジェクト・ストレージのバケットに保存されている、画像の一覧を取得するRESTデータ・ソースを作成します。

あらかじめオブジェクト・ストレージのエンドポイントを、リモート・サーバーとして作成します。今回の例では北米Ashburnリージョンのエンドポイントを使用するため、以下のエンドポイントをベースURLとしています。

https://objectstorage.us-ashburn-1.oraclecloud.com/


共有コンポーネントRESTデータ・ソースを開き、作成を開始します。


RESTデータ・ソースの作成として最初からを選択します。

へ進みます。


RESTデータ・ソース・タイプとしてOracle Cloud Infrastructure(OCI)を選択し、名前List Imagesとします。URLエンドポイントは以下の形式で指定します。

https://objectstorage.リージョン.oraclecloud.com/n/ネームスペース/b/バケット/o/

へ進みます。


リモート・サーバーとして、APIのエンドポイントとなるリモート・サーバーを選択します。サービスURLパスは以下の形式で指定します。

n/ネームスペース/b/バケット/o/

へ進みます。


認証が必要ですオンにし、資格証明としてOCI API Access、またはオブジェクト・ストレージにアクセスできる他のWeb資格証明を指定します。

検出をクリックします。


ファイルの一覧が検出されます。

RESTデータ・ソースの作成をクリックします。


RESTデータ・ソースが作成されます。同期化の設定を行うために、作成されたRESTデータ・ソースList Imagesを開きます。


静的IDlist_imagesであることを確認します。

画面右の同期化の管理を開きます。


 同期先として新規表を選択し、表名としてVEC_IMAGESを設定します。同期化を設定することにより、毎回REST APIを呼び出す代わりに表VEC_IMAGESから画像の一覧を取り出せるようになります。

保存をクリックします。


同期化は作成されましたが、表VEC_IMAGESはまだ作成されていません。

表の作成をクリックします。


表VEC_IMAGESが作成されました。詳細同期タイプ置換にします。

保存して実行をクリックし、最初の同期を実行します。


同期が完了すると、ログに実行結果が表示されます。ステータス成功であれば、表VEC_IMAGESにオブジェクト・ストレージのバケットに含まれるファイルの一覧が保存されています。


SQLワークショップSQLコマンドを開いて、表VEC_IMAGESの内容を確認します。

select name from vec_images;


表VEC_IMAGESの画像(のベクトル埋め込み)がPineconeのインデックスのベクトルとして保存されているかどうか、フラグを保持する表VEC_IMAGE_INDEXESを作成します。

以下のDDLを実行します。
create table vec_image_indexes(
    file_name varchar2(80) not null,
    is_indexed varchar2(1)
);

ページ・デザイナでホーム・ページを開き、ユーザー・インターフェースを実装します。

画面上にボタンを4つ作成します。

それぞれボタン名ラベル名として、LOAD_FROM_BUCKET(Load From Bucket)、UPSERT_VECTORS(Upsert Vectors)、DELETE_VECTORS(Delete Vectors)、FIND_IMAGES(Find Images)とします。動作アクションはすべてページの送信です。

レイアウトは適当に調整します。


問合せ文字列を指定するページ・アイテムP1_QUESTIONを作成します。タイプテキスト・フィールドラベルQuestionとします。


検索結果の数を指定するページ・アイテムP1_TOP_Kを作成します。タイプテキスト・フィールドラベルTop Kとします。


オブジェクト・ストレージ上にある画像の一覧と、Pineconeのインデックスにベクトル埋め込みが含まれているかどうかを示すフラグを表示する対話モード・レポートを作成します。

リージョンのタイトルImage IndexesソースSQL問合わせとして以下を記述します。
select 'image' as image, i.name, x.is_indexed 
from vec_images i left outer join vec_image_indexes x on i.name = x.file_name

IMAGEを選択し、オブジェクト・ストレージ上の画像が表示されるように、列の書式HTML式として以下を記述します。

<img src="&G_PREAUTH_URL.#NAME#" width="20"></img>


ページ・アイテムP1_QUESTIONに動物の名前を入力し、問い合わせを行った結果を表示する対話モード・レポートを作成します。

リージョンのタイトルResultsソースSQL問合わせとして以下を記述します。
select c001, n001 from apex_collections where collection_name = 'IMAGES'
レイアウト新規行の開始オフにし、リージョンImage Indexesの右横に配置します。


C001を選択し、検索結果の画像が表示されるように、列の書式HTML式に以下を記述します。

<img src="&G_PREAUTH_URL.#C001#" width="200"></img>


プロセス・ビューを開き、4つのボタンを押したときに実行されるプロセスを作成します。

ボタンLOAD_FROM_BUCKETを押したときに実行されるプロセスを、Load From Bucketとして作成します。ソースPL/SQLコードは以下になります。

BEGIN
apex_rest_source_sync.synchronize_data(
p_module_static_id => 'list_images',
p_run_in_background => false );
END;


ボタンUPSERT_VECTORSを押したときに実行されるプロセスを、Upsert Vectorsとして作成します。ソースPL/SQLコードは以下になります。

declare
l_request clob;
l_request_json json_object_t;
l_vectors json_array_t;
l_vector json_object_t;
l_embedding json_array_t;
l_response clob;
l_response_json json_object_t;
C_UPSERT constant varchar2(100) := :G_INDEX || '/vectors/upsert';
e_upsert_exception exception;
begin
/*
* Pinconeのindexに未登録の画像を登録する。
*/
l_vectors := json_array_t();
for r in (
select i.name
from vec_images i left outer join vec_image_indexes x on i.name = x.file_name
where x.is_indexed is null
)
loop
/* イメージのembeddingを生成する. */
select json_object(
key 'url' value :G_PREAUTH_URL || r.name
) into l_request
from dual;
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json',p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => :G_EMBED || '/embed-image'
,p_http_method => 'POST'
,p_body => l_request
);
l_response_json := json_object_t(l_response);
l_embedding := l_response_json.get_array('embedding');
l_vector := json_object_t();
l_vector.put('id', r.name);
l_vector.put('values', l_embedding);
/* このベクトルがimageから生成されたことを、metadataとして追加する。 */
l_vector.put('metadata', json_object_t('{ "type":"image" }'));
l_vectors.append(l_vector);
insert into vec_image_indexes(file_name, is_indexed) values(r.name, 'Y');
end loop;
if l_vectors.get_size = 0 then
/* 追加する文書がないので終了 */
return;
end if;
l_request_json := json_object_t();
l_request_json.put('vectors', l_vectors);
l_request := l_request_json.to_clob;
/*
* PineconeのUpsertを呼び出す。
*/
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json',p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => C_UPSERT
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => 'PINECONE_API'
);
/* レスポンス本文は解析不要。 */
apex_debug.info(l_response);
if apex_web_service.g_status_code <> 200 then
raise e_upsert_exception;
end if;
end;


ボタンFIND_IMAGESを押したときに実行されるプロセスを、Find Imagesとして作成します。ソースPL/SQLコードは以下になります。

declare
l_request clob;
l_response clob;
l_response_json json_object_t;
l_embedding json_array_t;
/* Pinecone */
l_query_json json_object_t;
l_matches json_array_t;
l_vector json_object_t;
l_file_name vec_image_indexes.file_name%type;
l_score number;
C_QUERY constant varchar2(80) := :G_INDEX || '/query';
e_query_exception exception;
begin
/*
* 最初に/embed-textを呼び出して、質問のembeddingを生成する。
*/
select json_object(
key 'text' value :P1_QUESTION
) into l_request
from dual;
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json', p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json', p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => :G_EMBED || '/embed-text'
,p_http_method => 'POST'
,p_body => l_request
);
-- apex_debug.info(l_response);
l_response_json := json_object_t.parse(l_response);
l_embedding := l_response_json.get_array('embedding');
/*
* Pineconeに問い合わせる。
*/
l_query_json := json_object_t();
/* 検索対象を画像に限定する。 */
l_query_json.put('filter', json_object_t('{ "type" : "image" }'));
l_query_json.put('includeValues', false);
l_query_json.put('includeMetadata', false);
l_query_json.put('vector', l_embedding);
l_query_json.put('topK', :P1_TOP_K);
l_request := l_query_json.to_clob;
/* queryを呼び出す。 */
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json',p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => C_QUERY
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => 'PINECONE_API'
);
-- apex_debug.info(l_response);
if apex_web_service.g_status_code <> 200 then
apex_debug.info(l_response);
raise e_query_exception;
end if;
/*
* レスポンスをAPEXコレクションに保存する。
*/
apex_collection.create_or_truncate_collection('IMAGES');
l_response_json := json_object_t.parse(l_response);
l_matches := l_response_json.get_array('matches');
for i in 1..l_matches.get_size
loop
l_vector := json_object_t(l_matches.get(i-1));
l_file_name := l_vector.get_string('id');
l_score := l_vector.get_number('score');
apex_collection.add_member(
p_collection_name => 'IMAGES'
,p_c001 => l_file_name
,p_n001 => l_score
);
end loop;
end;
view raw find-images.sql hosted with ❤ by GitHub


ボタンDELETE_VECTORSを押したときに実行されるプロセスを、Delete Vectorsとして作成します。ソースPL/SQLコードは以下になります。

declare
l_request clob;
l_response clob;
C_DELETE constant varchar2(100) := :G_INDEX || '/vectors/delete';
e_delete_exception exception;
begin
/*
* typeがimageのベクトルを削除する。
*/
select json_object(
key 'deleteAll' value 'false',
key 'filter' value json_object(
key 'type' value 'image'
)
) into l_request from dual;
apex_web_service.clear_request_headers;
apex_web_service.set_request_headers('Accept','application/json',p_reset => false);
apex_web_service.set_request_headers('Content-Type','application/json',p_reset => false);
l_response := apex_web_service.make_rest_request(
p_url => C_DELETE
,p_http_method => 'POST'
,p_body => l_request
,p_credential_static_id => 'PINECONE_API'
);
/* レスポンス本文は解析不要。 */
apex_debug.info(l_response);
if apex_web_service.g_status_code <> 200 then
raise e_delete_exception;
end if;
/* インデックス済みのフラグを初期化。 */
delete from vec_image_indexes;
end;


必ずしも必要な作業でありませんが、作成したRESTデータ・ソースのURLパス接頭辞を以下のように変更し、パラメータを定義するとRESTデータ・ソースの再利用が容易になります。

n/:namespace/b/:bucket/o/


パラメータタイプURLパターンデフォルト値はそれぞれ&G_NAMESPACE.&G_BUCKET.になります。必須はいです。

今回作成したアプリケーションの説明は以上になります。

Oracle APEXのアプリケーション作成の参考になれば幸いです。