kokolegorille

kokolegorille

How to cluster embedding vectors with pgvector?

Hello everyone,

I am trying to group vectors into clusters of similarity.

I have extracted a video into screenshots, each seconds, and retrieved face embedding vectors with face_recognition: The vector database is postgresql, with the pg_vector extension.

Here is my migration

  def change do
    create table(:faces, primary_key: false) do
      add :id, :binary_id, primary_key: true
      add :filename, :string
      add :x, :integer
      add :y, :integer
      add :w, :integer
      add :h, :integer
      add :embedding, :vector, size: 128

      timestamps()
    end
    create index(:faces, [:filename])
    # vector size is limited to 2000 dimensions!
    create index("faces", ["embedding vector_cosine_ops"], using: :hnsw)
  end

I can query for similarities, given an embedding… with code similar to

      {:l2_order, embedding}, query ->
        from p in query, order_by: l2_distance(^embedding, p.embedding)

  def list_faces_with_distances(query, embedding) do
    from(p in query,
      where: l2_distance(^embedding, p.embedding) <= 0.5,
      select: %{p | distance: l2_distance(^embedding, p.embedding)})
    |> Repo.all
  end

My question is… how can I cluster the vectors into groups of similarity?

Because the embeddings represents faces, I would like to group vectors from the same person. In fact, I would like to find the number of different persons

There is an example with ExFaiss which is what I would like to achieve. Unfortunately, ExFaiss has been archived GitHub - elixir-nx/ex_faiss: Elixir front-end to Facebook AI Similarity Search (Faiss) · GitHub

Thanks for taking time

Marked As Solved

kokolegorille

kokolegorille

Clustering with DBSCAN is not working with high dimension vectors, and HNSW provides ANN, but has no clustering options… By combining both, it is possible to achieve high speed clustering of thousands of vectors (dim=128)

Thanks to Nx and this package hnswlib | Hex

For future reference… here is the implementation

defmodule Koko.Clustering do
  require Logger

  # SAMPLE PARAMS
  #
  # max_elements = 10000
  # ef_construction = 200
  # M = 16
  # ef = 50  # ef should be set based on your accuracy/speed tradeoff needs

  @eps 0.3 # Distance threshold for DBSCAN
  @min_samples 12  # Minimum number of points to form a dense region (cluster)

  # DBSCAN
  def create_clusters(index, opts \\ []) do
    count = index |> instance().get_current_count() |> unwrap!()

    if count > 0 do
      eps = Keyword.get(opts, :eps, @eps)
      min_samples = Keyword.get(opts, :min_samples, @min_samples)
      labels = Nx.tensor(Enum.map(0..count, fn _ -> -1 end), type: {:s, 16})

      {labels, _cluster_id} = 0..count
      |> Enum.reduce({labels, 0}, fn i, {labels, cluster_id} = acc ->
        Logger.debug("#{__MODULE__} LOOP #{i} for cluster #{cluster_id}")

        if Nx.to_number(labels[i]) != -1 do
          acc
        else
          neighbors = hnsw_neighbors(index, get_item(index, i), eps: eps)
          # |> IO.inspect(label: "NEIGHBORS", limit: :infinity)
          if length(neighbors) < min_samples do
            # Mark labels[i] = -1 as noise
            labels = mark_labels(labels, i, -1)
            {labels, cluster_id}
          else
            # Expand cluster
            # Mark labels[i] as cluster_id
            labels = labels
            |> mark_labels(i, cluster_id)
            |> do_process_neighbors(neighbors, cluster_id)

            {labels, cluster_id + 1}
          end
        end
      end)
      Logger.debug("#{__MODULE__} labels #{inspect labels}")
      labels
    else
      Logger.warning("#{__MODULE__} index is empty")
      []
    end
  end

  defp do_process_neighbors(labels, [], _cluster_id), do: labels
  defp do_process_neighbors(labels, [current | rest], cluster_id) do
    if Nx.to_number(labels[current]) == -1 do
      mark_labels(labels, current, cluster_id)
    else
      labels
    end |> do_process_neighbors(rest, cluster_id)
  end

  defp mark_labels(labels, i, value) do
    labels |> Nx.put_slice([i], Nx.tensor([value], type: :s16))
    # |> IO.inspect(label: "LABELS")
  end

  def hnsw_neighbors(index, point, opts \\ [])
  def hnsw_neighbors(_index, nil, _opts) do
    []
  end
  def hnsw_neighbors(index, point, opts) do
    eps = Keyword.get(opts, :eps, @eps)
    count = Keyword.get(opts, :count, index |> instance().get_current_count() |> unwrap!())

    {:ok, ids, distances} = HNSWLib.Index.knn_query(index, point, k: count)

    # Do not take the head, as it is the point itself
    [_ | list_ids] = ids |> Nx.to_flat_list()
    [_ | list_distances] = distances |> Nx.to_flat_list()

    list_ids
    |> Enum.zip(list_distances)
    |> Enum.filter(& elem(&1, 1) <= eps)
    |> Enum.map(& elem(&1, 0))
  end

  def get_item(index, i) do
    Logger.debug("#{__MODULE__} get_item #{i}")
    case instance().get_items(index, [i]) |> unwrap!() do
      [item] ->
        item |> Nx.from_binary(:f32)
      any ->
        Logger.error("#{__MODULE__} get item #{i} failure #{inspect any}")
        nil
    end
  end

  # HNSW
  def new_index(opts \\ []) do
    space = Keyword.get(opts, :space, :l2)
    dim = Keyword.get(opts, :din, 128)
    max_elements = Keyword.get(opts, :max_elements, 100)

    space
    |> instance().new(dim, max_elements)
    |> unwrap!()
  end

  # Cannot be defdelegate because instance() is dynamic
  def knn_query(index, query, opts \\ []) do
    instance().knn_query(index, query, opts)
  end

  def unwrap!({:ok, value}), do: value
  def unwrap!({:error, value}), do: value

  defp instance do
    HNSWLib.Index
  end
end

Where Next?

Popular in Questions Top

_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: &lt;h1&gt;Create Post&lt;/h1&gt; &lt;%= ...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call t...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53690 245
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52341 488
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New

We're in Beta

About us Mission Statement