rinna株式会社が公開しているjapanese-cloob-vit-b-16のモデル(rinna株式会社によるプレスリリース)とベクトル・データベースPineconeを使って、テキストによる画像検索を行なうアプリケーションを作成します。APEXではユーザーインタフェースを作成します。
以下の処理を実装します。
- オブジェクト・ストレージに保存した画像のベクトル埋め込み(embedding)をrinna社のモデルをより生成し、Pineconeに保存する。
- 問い合わせテキストのベクトル埋め込みをrinna社のモデルをより生成し、Pinconeのインデックスを検索する。
- 検索された画像をアプリケーションに表示する。
Pineconeのインデックス
オブジェクト・ストレージへの画像アップロード
ベクトル埋め込みを生成するRESTサービス
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@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: [ベクトル埋め込み] }が返されます。
APEXアプリケーションの作成
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; |
https://objectstorage.us-ashburn-1.oraclecloud.com/
create table vec_image_indexes(
file_name varchar2(80) not null,
is_indexed varchar2(1)
);
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
select c001, n001 from apex_collections where collection_name = 'IMAGES'
BEGIN | |
apex_rest_source_sync.synchronize_data( | |
p_module_static_id => 'list_images', | |
p_run_in_background => false ); | |
END; |
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; |
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; |
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; |