Current section
Files
Jump to
Current section
Files
lib/scholar/neighbors/large_vis.ex
defmodule Scholar.Neighbors.LargeVis do
@moduledoc """
LargeVis algorithm for approximate k-nearest neighbor (k-NN) graph construction.
The algorithms works in the following way. First, the approximate k-NN graph is constructed
using a random projection forest. Then, the graph is refined by looking at the neighbors of
neighbors of every point for a fixed number of iterations. This step is called NN-expansion.
## References
* [Visualizing Large-scale and High-dimensional Data](https://arxiv.org/abs/1602.00370).
"""
import Nx.Defn
import Scholar.Shared
alias Scholar.Neighbors.RandomProjectionForest
alias Scholar.Neighbors.Utils
opts = [
num_neighbors: [
required: true,
type: :pos_integer,
doc: "The number of neighbors in the graph."
],
metric: [
type: {:in, [:squared_euclidean, :euclidean]},
default: :euclidean,
doc: "The function that measures distance between two points."
],
min_leaf_size: [
type: :pos_integer,
doc: """
The minimum number of points in every leaf.
Must be at least num_neighbors.
If not provided, it is set based on the number of neighbors.
"""
],
num_trees: [
type: :pos_integer,
doc: """
The number of trees in random projection forest.
If not provided, it is set based on the dataset size.
"""
],
num_iters: [
type: :non_neg_integer,
default: 3,
doc: "The number of times to perform neighborhood expansion."
],
key: [
type: {:custom, Scholar.Options, :key, []},
doc: """
Used for random number generation in parameter initialization.
If the key is not provided, it is set to `Nx.Random.key(System.system_time())`.
"""
]
]
@opts_schema NimbleOptions.new!(opts)
@doc """
Constructs the approximate k-NN graph with LargeVis.
Returns neighbor indices and distances.
## Examples
iex> key = Nx.Random.key(12)
iex> tensor = Nx.iota({5, 2})
iex> {graph, distances} = Scholar.Neighbors.LargeVis.fit(tensor, num_neighbors: 2, metric: :squared_euclidean, min_leaf_size: 2, num_trees: 3, key: key)
iex> graph
#Nx.Tensor<
u32[5][2]
[
[0, 1],
[1, 0],
[2, 1],
[3, 2],
[4, 3]
]
>
iex> distances
#Nx.Tensor<
f32[5][2]
[
[0.0, 8.0],
[0.0, 8.0],
[0.0, 8.0],
[0.0, 8.0],
[0.0, 8.0]
]
>
"""
deftransform fit(tensor, opts) do
if Nx.rank(tensor) != 2 do
raise ArgumentError,
"""
expected input tensor to have shape {num_samples, num_features}, \
got tensor with shape: #{inspect(Nx.shape(tensor))}\
"""
end
opts = NimbleOptions.validate!(opts, @opts_schema)
k = opts[:num_neighbors]
metric =
case opts[:metric] do
:euclidean -> &Scholar.Metrics.Distance.euclidean/2
:squared_euclidean -> &Scholar.Metrics.Distance.squared_euclidean/2
end
min_leaf_size = opts[:min_leaf_size] || max(10, 2 * k)
size = Nx.axis_size(tensor, 0)
num_trees = opts[:num_trees] || 5 + round(:math.pow(size, 0.25))
key = Keyword.get_lazy(opts, :key, fn -> Nx.Random.key(System.system_time()) end)
fit_n(
tensor,
key,
num_neighbors: k,
metric: metric,
min_leaf_size: min_leaf_size,
num_trees: num_trees,
num_iters: opts[:num_iters]
)
end
defnp fit_n(tensor, key, opts) do
forest =
RandomProjectionForest.fit(tensor,
num_neighbors: opts[:num_neighbors],
min_leaf_size: opts[:min_leaf_size],
num_trees: opts[:num_trees],
key: key
)
{graph, _} = RandomProjectionForest.predict(forest, tensor)
expand(graph, tensor, metric: opts[:metric], num_iters: opts[:num_iters])
end
defn expand(graph, tensor, opts) do
num_iters = opts[:num_iters]
{n, k} = Nx.shape(graph)
{result, _} =
while {
{
graph,
_distances = Nx.broadcast(Nx.tensor(:nan, type: to_float_type(tensor)), {n, k})
},
{tensor, iter = 0}
},
iter < num_iters do
{expansion_iter(graph, tensor, metric: opts[:metric]), {tensor, iter + 1}}
end
result
end
defnp expansion_iter(graph, tensor, opts) do
{size, k} = Nx.shape(graph)
candidate_indices = Nx.take(graph, graph) |> Nx.reshape({size, k * k})
candidate_indices = Nx.concatenate([graph, candidate_indices], axis: 1)
Utils.brute_force_search_with_candidates(tensor, tensor, candidate_indices,
num_neighbors: k,
metric: opts[:metric]
)
end
end