18
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ベクトルデータベース Pinecone で顔をデータベース化し、似ている顔を探す

Last updated at Posted at 2023-07-12

はじめに

前回の記事で、 Livebook 上にノーコードで顔認証を実装しました

ただし、この方法だと毎回顔写真を選択し、顔の特徴量を計算することになります

比較対象となる顔の特徴量をデータベースに入れておけば、そんな必要はありません

というわけで、ベクトル(テンソル)に特化したベクトルデータベース Pinecone を使って、顔特徴量の保存、検索を実行します

公式サイトに "Long-Term Memory for AI" とあるように、 AI の入出力として使われるベクトルを保存、検索するためのデータベースです

今回も Livebook で実装します

実装したノートブックはこちら

セットアップ

必要なモジュールをインストールします

Mix.install(
  [
    {:kino, "~> 0.9"},
    {:evision, github: "cocoa-xu/evision", branch: "main"},
    {:pinecone, "~> 0.1"},
    {:jason, "~> 1.4"}
  ],
  system_env: [
    {"EVISION_PREFER_PRECOMPILED", "false"}
  ]
)

evision は最新版が未リリースなので、 GitHub からコードを取得し、ビルドします

pinecone は Pinecone 用の Elixir モジュールですが、まだ荒削りな状態で、必要なものが揃ってはいませんが、とりあえずこれを使っていきます

jason は Pinecone API へのリクエストを JSON 形式に変換するために使います

画像の準備

今回の例では "/home/livebook/evision/test-images/" 配下に .jpg と .png の画像ファイルを用意しました

image_files = Path.wildcard("/home/livebook/evision/test-images/*.{jpg,png}")

とりあえず顔写真を表示してみます

images =
  image_files
  |> Enum.map(fn image_file ->
    Evision.imread(image_file)
  end)

Kino.Layout.grid(images, columns: 4)

スクリーンショット 2023-07-11 9.36.48.png

左上から7枚は AI生成画像です

「メガネをかけた日本人エンジニア」など、私っぽくなるように生成したものから、「日本人議員」で生成した女性の顔まで、色々作ってみました

8枚目は生身の人間で、実在する私の後輩です(記事への掲載について本人了承済)

9枚目以降の8枚はすべて私です

色々な角度だったり、メガネを外していたり、マスクをしていたり、プリクラで美顔加工されていたり、AIにイラスト化されていたりします

今回使用する SFace では、顔の特徴量を長さ 128 のベクトルで表します

SFace は特徴量ベクトル間のコサイン類似度が、同一人物なら 1 に近く、別人なら -1 に近くなるよう学習しています

うまく個人識別できていれば、下段2つの8枚(同一人物)は特徴量が近く = ベクトル間のコサイン類似度が 1 に近くなるはずです

顔特徴量の抽出

顔特徴量の抽出ように SFace 、顔検出用に YuNet のモデルを読み込みます

recognizer =
  Evision.Zoo.FaceRecognition.SFace.init(:default_model,
    backend: Evision.Constant.cv_DNN_BACKEND_OPENCV(),
    target: Evision.Constant.cv_DNN_TARGET_CPU(),
    distance_type: :cosine_similarity,
    cosine_threshold: 0.363,
    l2_norm_threshold: 1.128
  )

detector =
  Evision.Zoo.FaceDetection.YuNet.init(:default_model,
    backend: Evision.Constant.cv_DNN_BACKEND_OPENCV(),
    target: Evision.Constant.cv_DNN_TARGET_CPU(),
    nms_threshold: 0.3,
    conf_threshold: 0.9,
    top_k: 5
  )

各画像について、以下の処理を実行します

  • A. 顔を検出する
  • B. A の結果から顔の座標情報を取得する
  • C. 顔画像と B を使い、顔特徴量を取得する
  • D. 顔検出結果を視覚化する
  • C と D を配列に格納する

D(顔検出結果の視覚化)を表示します

[feature_list, visualized_list] =
  images
  |> Enum.reduce([[], []], fn image, [feature_acc, visualized_acc] ->
    results = Evision.Zoo.FaceDetection.YuNet.infer(detector, image)

    bbox = Evision.Mat.to_nx(results, Nx.BinaryBackend)[0][0..-2//1]

    feature =
      recognizer
      |> Evision.Zoo.FaceRecognition.SFace.infer(image, bbox)
      |> Evision.Mat.to_nx()
      |> Evision.Mat.from_nx()

    visualized = Evision.Zoo.FaceDetection.YuNet.visualize(image, results[0])

    [[feature | feature_acc], [visualized | visualized_acc]]
  end)
  |> Enum.map(&Enum.reverse/1)

Kino.Layout.grid(visualized_list, columns: 4)

スクリーンショット 2023-07-11 9.58.49.png

四角形が顔の検出範囲、四角形の中の点は両目、鼻、口角の検出位置です

特徴量の中身を見てみましょう

feature_list

実行結果は以下のようになります

[
  %Evision.Mat{
    channels: 1,
    dims: 2,
    type: {:f, 32},
    raw_type: 5,
    shape: {1, 128},
    ref: #Reference<0.2446145276.1381105673.600>
  },
  ...
]

形が 1 * 128 で、 32 ビット浮動小数の行列になっています

先頭の行列をテンソルとして表示してみます

feature_list
|> hd()
|> Evision.Mat.to_nx()

結果は以下のようになります

#Nx.Tensor<
  f32[1][128]
  Evision.Backend
  [
    [0.2478046417236328, -0.07941834628582001, -1.5164910554885864, 0.5658660531044006, 0.32173722982406616, 1.0506480932235718, -0.6378516554832458, 1.7099213600158691, 1.3846274614334106, -0.5175538659095764, -0.2464766800403595, 0.4892995059490204, -0.9083938002586365, -0.40185821056365967, 1.257846713066101, -1.329054832458496, -0.6721335053443909, 0.898859441280365, -0.6860005855560303, -1.037187933921814, 0.4402664005756378, 1.9445024728775024, 0.8437008857727051, -0.540610671043396, -0.2207109034061432, 1.9215633869171143, 1.0295889377593994, 0.9870990514755249, 0.8276092410087585, 0.8378713726997375, 0.8952299356460571, 3.1368656158447266, 1.896082878112793, -0.5408202409744263, 1.3720626831054688, -0.4063827097415924, 0.41860976815223694, 0.7061173915863037, 0.970976710319519, 1.5799314975738525, 0.3332902193069458, 2.4629318714141846, -1.047968864440918, -1.2429616451263428, 0.19454512000083923, 0.2729840576648712, 2.6603450775146484, -0.1787053644657135, -0.800810694694519, 0.013038262724876404, ...]
  ]
>

1 * 128 のテンソルに小数の値が入っていますね

試しにローカルで顔特徴量間のコサイン類似度を計算してみましょう

下から2段目の左端と、最下段の右端(どちらとも私)を比較してみます

Evision.Zoo.FaceRecognition.SFace.match_feature(
  recognizer,
  Enum.at(feature_list, 8),
  Enum.at(feature_list, 15)
)

実行結果は以下のようになります

%{matched: true, measure: "cosine_score", retval: 0.8260765364864255}

コサイン類似度は 0.826 で非常に高く、 matched: true 同一人物、という判定結果を出し増田

evision ではコサイン類似度の閾値 0.363 で同一人物かどうかを判定しています

下から2段目の左端と、最上段の左端(別人)を比較してみます

Evision.Zoo.FaceRecognition.SFace.match_feature(
  recognizer,
  Enum.at(feature_list, 8),
  Enum.at(feature_list, 0)
)

実行結果は以下のようになります

%{matched: false, measure: "cosine_score", retval: -0.1544433785079491}

コサイン類似度は -0.154 で負にになっており、 matched: false で別人判定です

顔特徴量は正しく取得できたようです

顔特徴量を Pinecone に登録できるベクトルの形式に変換します

vectors =
  feature_list
  |> Enum.zip(image_files)
  |> Enum.map(fn {feature, image_file} ->
    values =
      feature
      |> Evision.Mat.to_nx(Nx.BinaryBackend)
      |> Nx.flatten()
      |> Nx.to_list()

    %{
      values: values,
      id: image_file
    }
  end)

実行結果は以下のようになります

[
  %{
    id: "/home/livebook/evision/test-images/others-000.jpg",
    values: [0.2478046417236328, -0.07941834628582001, -1.5164910554885864, 0.5658660531044006,
     0.32173722982406616, 1.0506480932235718, -0.6378516554832458, 1.7099213600158691,
     1.3846274614334106, -0.5175538659095764, -0.2464766800403595, 0.4892995059490204,
     -0.9083938002586365, -0.40185821056365967, 1.257846713066101, -1.329054832458496,
     -0.6721335053443909, 0.898859441280365, -0.6860005855560303, -1.037187933921814,
     0.4402664005756378, 1.9445024728775024, 0.8437008857727051, -0.540610671043396,
     -0.2207109034061432, 1.9215633869171143, 1.0295889377593994, 0.9870990514755249,
     0.8276092410087585, 0.8378713726997375, 0.8952299356460571, 3.1368656158447266,
     1.896082878112793, -0.5408202409744263, 1.3720626831054688, -0.4063827097415924,
     0.41860976815223694, 0.7061173915863037, 0.970976710319519, 1.5799314975738525,
     0.3332902193069458, 2.4629318714141846, -1.047968864440918, -1.2429616451263428,
     0.19454512000083923, 0.2729840576648712, 2.6603450775146484, ...]
  },
  ...
]

id に画像ファイルのパス、 values にベクトルを配列化したものを入れています

インデックスの作成

Pinecone のアカウントを持っていない場合、とりあえず無料で始められるので、トップページの右上 "Sign Up Free" からサインアップしてください

スクリーンショット 2023-07-11 10.25.39.png

メールアドレス等でサインアップすると、初期化中の画面が表示されるので、しばらく待ちます

スクリーンショット 2023-07-11 10.33.14.png

以下のような表示になれば使い始めることができます

スクリーンショット 2023-07-11 10.35.34.png

左メニュー "API Keys" を開いておきます

スクリーンショット 2023-07-11 10.37.55.png

このままブラウザコンソールから操作してもいいのですが、せっかくなので Elixir で操作していきます

認証情報を入力するためのテキストインプットを作ります

Pinecone コンソールの "API Keys" から、 Value の値を api_key_input に、 Environment の値を environment_input に入力します

api_key_input = Kino.Input.password("API_KEY")
environment_input = Kino.Input.text("ENVIRONMENT")

スクリーンショット 2023-07-11 10.40.39.png

入力した値を変数に読み込みます

api_key = Kino.Input.read(api_key_input)
environment = Kino.Input.read(environment_input)

インデックス操作の機能が Elixir の pinecone モジュール内に存在しないため、自作します

defmodule Pinecone.Controller do
  def new(opts) do
    environment = Keyword.get(opts, :environment)
    api_key = Keyword.get(opts, :api_key)

    middleware = [
      {Tesla.Middleware.BaseUrl, "https://controller.#{environment}.pinecone.io"},
      Tesla.Middleware.JSON,
      {Tesla.Middleware.Headers, [{"api-key", api_key}]}
    ]

    Tesla.client(middleware)
  end

  def create_index(client, opts) do
    params = Enum.into(opts, %{})

    client
    |> Tesla.post("/databases", params)
    |> handle_response()
  end

  def list_indexes(client) do
    client
    |> Tesla.get("/databases")
    |> handle_response()
  end

  def describe_index(client, name) do
    client
    |> Tesla.get("/databases/#{name}")
    |> handle_response()
  end

  def delete_index(client, name) do
    client
    |> Tesla.delete("/databases/#{name}")
    |> handle_response()
  end

  def handle_response(resp, opts \\ [])

  def handle_response({:error, _} = err, _opts), do: err

  def handle_response({:ok, %Tesla.Env{status: status, body: body}}, _opts) when status <= 400 do
    {:ok, body}
  end

  def handle_response({:ok, resp}, _opts), do: {:error, resp}
end

インデックス操作用の controller を用意します

そのまま実行すると controller の中に含むAPIキーの値が結果の表示されてしまうので、セルの最後に "dummy" を入れています

controller =
  Pinecone.Controller.new(
    environment: environment,
    api_key: api_key
  )

"dummy"

今回は顔検索用インデックスなので、インデックス名を "face-search" にします

index_name = "face-search"

以下のようにパラメータを指定し、インデックスを作成します

  • name: インデックス名
  • dimension: ベクトルのサイズ(今回の顔特徴量は128)
  • metric: 距離・類似度の計算方法(今回はコサイン類似度)
  • pod_type: Pinecone でデータを格納するハードウェアのタイプを指定します(今回は無料プランで利用可能な s1 = ストレージ効率重視を使います)
Pinecone.Controller.create_index(controller,
  name: index_name,
  dimension: 128,
  metric: "cosine",
  pod_type: "s1"
)

実行結果は以下のようになります

{:ok, ""}

ただし、この状態はインデックスの作成を開始しただけでまだ使えません

インデックスの一覧を取得してみます

controller
|> Pinecone.Controller.list_indexes()
|> elem(1)

実行結果は以下のようになります

["face-search"]

インデックスの詳細を取得します

controller
|> Pinecone.Controller.describe_index(index_name)
|> elem(1)

実行結果は以下のようになります

%{
  "database" => %{
    "dimension" => 128,
    "metric" => "cosine",
    "name" => "face-search",
    "pods" => 1,
    "replicas" => 1,
    "shards" => 1
  },
  "status" => %{
    "crashed" => [],
    "host" => "face-search-1ee3389.svc.us-west1-gcp-free.pinecone.io",
    "port" => 433,
    "ready" => true,
    "state" => "Ready",
    "waiting" => []
  }
}

"state" => "Ready" になるまでは使えないので、それまでは待ちます

API 呼び出しに使うため、プロジェクトのIDを取得しておきます

project =
  controller
  |> Pinecone.Controller.describe_index(index_name)
  |> elem(1)
  |> Map.get("status")
  |> Map.get("host")
  |> String.split(".")
  |> Enum.at(0)
  |> String.replace("#{index_name}-", "")

ベクトルの登録

ベクトル操作用のクライアントを作成します

このときも APIキー が結果に表示されないよう、最後に "dummy" を出しておきます

client =
  Pinecone.Client.new(
    environment: environment,
    api_key: api_key,
    project: project,
    index: index_name
  )

"dummy"

準備しておいた vectors を登録します

Pinecone では upsert なので、同じ ID のベクトルを登録した場合、必ず上書きされる点に注意してください

Pinecone.Vector.upsert(client, %{vectors: vectors})

実行結果は以下のようになります

{:ok, %{"upsertedCount" => 16}}

16個の顔特徴量が登録できました

ベクトルの検索

私の顔を検索してみます

vectors から インデックス 8 の顔特徴量を取得します

vector =
  vectors
  |> Enum.at(8)
  |> Map.get(:values)

この顔特徴量に近い上位10個の顔特徴量を取得します

  • topK: 上位何個まで取得するか
  • vector: 検索するベクトル(今回は顔特徴量)
matches =
  client
  |> Pinecone.Vector.query(%{topK: 10, vector: vector})
  |> elem(1)
  |> Map.get("matches")

実行結果は以下のようになります

[
  %{"id" => "/home/livebook/evision/test-images/ryo-000.png", "score" => 1, "values" => []},
  %{
    "id" => "/home/livebook/evision/test-images/ryo-007.jpg",
    "score" => 0.826076448,
    "values" => []
  },
  %{
    "id" => "/home/livebook/evision/test-images/ryo-003.jpg",
    "score" => 0.560545802,
    "values" => []
  },
  ...
}

score にコサイン類似度が入っており、 score の高い順に10個返ってきました

id を画像ファイルのパスにしているので、そのまま読み込んで表示してみます

matches
|> Enum.map(fn match ->
  [
    Kino.Markdown.new("#{match["score"]}"),
    Evision.imread(match["id"])
  ]
  |> Kino.Layout.grid()
end)
|> Kino.Layout.grid(columns: 4)

スクリーンショット 2023-07-11 11.10.16.png

各画像の上にスコアも表示しています

左上、トップの画像は検索した顔と同じ顔です

全く同じ顔なので、スコアは 1 になっています

2番目はスコア 0.826 で、確かに同一人物です(太って髪が伸びて変な表情になっていますが)

3番目はメガネを外した同一人物、4番目、5番目も私です

6番目についに別人(AIが生成した顔画像)が出てきました

スコアは 0.310 なので、閾値が 0.363 なら別人判定ですね

7番目、8番目、10番目は加工されたりマスクをしたりした私ですが、そもそも顔検出位置がずれていたこともあってこれは仕方ないですね

9番目は別人(実在する私の後輩)で、コサイン類似度は 0.178 でした

判定結果の正しさは SFace や YuNet の問題なので置いておくとして、確かに Pinecone でベクトル検索が実行できました

インデックスの削除

最後にインデックスを削除しておきます

Pinecone.Controller.delete_index(controller, index_name)

まとめ

Pinecone を使うことで、ベクトルの保存、検索ができることを確認しました

今回実装したように顔認証用のデータベースとして使うことができます

また、テキストをAIでベクトル化することで、非常に柔軟な文書検索を構築することも可能です

公式の Python 用チュートリアルから簡単に体験できます

もちろん、今回やったように Elixir でも簡単に使えたので、 Elixir でベクトルを扱うときには使ってみましょう

18
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?