Current section
Files
Jump to
Current section
Files
lib/scholar/decomposition/truncated_svd.ex
defmodule Scholar.Decomposition.TruncatedSVD do
@moduledoc """
Dimensionality reduction using truncated SVD (aka LSA).
This transformer performs linear dimensionality reduction by means of
truncated singular value decomposition (SVD).
"""
import Nx.Defn
@derive {Nx.Container,
containers: [
:components,
:explained_variance,
:explained_variance_ratio,
:singular_values
]}
defstruct [
:components,
:explained_variance,
:explained_variance_ratio,
:singular_values
]
tsvd_schema = [
num_components: [
default: 2,
type: :pos_integer,
doc: "Desired dimensionality of output data."
],
num_iter: [
default: 5,
type: :pos_integer,
doc: "Number of iterations for randomized SVD solver."
],
num_oversamples: [
default: 10,
type: :pos_integer,
doc: "Number of oversamples for randomized SVD solver."
],
key: [
type: {:custom, Scholar.Options, :key, []},
doc: """
Key for random tensor generation.
If the key is not provided, it is set to `Nx.Random.key(System.system_time())`.
"""
]
]
@tsvd_schema NimbleOptions.new!(tsvd_schema)
@doc """
Fit model on training data X.
## Options
#{NimbleOptions.docs(@tsvd_schema)}
## Return Values
The function returns a struct with the following parameters:
* `:components` - tensor of shape `{num_components, num_features}`
The right singular vectors of the input data.
* `:explained_variance` - tensor of shape `{num_components}`
The variance of the training samples transformed by a projection to
each component.
* `:explained_variance_ratio` - tensor of shape `{num_components}`
Percentage of variance explained by each of the selected components.
* `:singular_values` - ndarray of shape `{num_components}`
The singular values corresponding to each of the selected components.
## Examples
iex> key = Nx.Random.key(0)
iex> x = Nx.tensor([[0, 0], [1, 0], [1, 1], [3, 3], [4, 4.5]])
iex> tsvd = Scholar.Decomposition.TruncatedSVD.fit(x, num_components: 2, key: key)
iex> tsvd.components
#Nx.Tensor<
f32[2][2]
[
[0.6871105432510376, 0.7265529036521912],
[0.7265529036521912, -0.6871105432510376]
]
>
iex> tsvd.singular_values
#Nx.Tensor<
f32[2]
[7.528080940246582, 0.7601959705352783]
>
"""
deftransform fit(x, opts \\ []) do
opts = NimbleOptions.validate!(opts, @tsvd_schema)
key = Keyword.get_lazy(opts, :key, fn -> Nx.Random.key(System.system_time()) end)
fit_n(x, key, opts)
end
@doc """
Fit model to X and perform dimensionality reduction on X.
## Options
#{NimbleOptions.docs(@tsvd_schema)}
## Return Values
X_new tensor of shape `{num_samples, num_components}` - reduced version of X.
## Examples
iex> key = Nx.Random.key(0)
iex> x = Nx.tensor([[0, 0], [1, 0], [1, 1], [3, 3], [4, 4.5]])
iex> Scholar.Decomposition.TruncatedSVD.fit_transform(x, num_components: 2, key: key)
#Nx.Tensor<
f32[5][2]
[
[0.0, 0.0],
[0.6871105432510376, 0.7265529036521912],
[1.413663387298584, 0.039442360401153564],
[4.240990161895752, 0.1183270812034607],
[6.017930030822754, -0.18578583002090454]
]
>
iex> key = Nx.Random.key(0)
iex> x = Nx.tensor([[0, 0, 3], [1, 0, 3], [1, 1, 3], [3, 3, 3], [4, 4.5, 3]])
iex> Scholar.Decomposition.TruncatedSVD.fit_transform(x, num_components: 2, key: key)
#Nx.Tensor<
f32[5][2]
[
[1.9478826522827148, 2.260593891143799],
[2.481153964996338, 1.906071662902832],
[3.023407220840454, 1.352442979812622],
[5.174456596374512, -0.46385863423347473],
[6.521108150482178, -1.6488237380981445]
]
>
"""
deftransform fit_transform(x, opts \\ []) do
opts = NimbleOptions.validate!(opts, @tsvd_schema)
key = Keyword.get_lazy(opts, :key, fn -> Nx.Random.key(System.system_time()) end)
fit_transform_n(x, key, opts)
end
defnp fit_n(x, key, opts) do
if Nx.rank(x) != 2 do
raise ArgumentError, "expected x to have rank equal to: 2, got: #{inspect(Nx.rank(x))}"
end
num_components = opts[:num_components]
{num_samples, num_features} = Nx.shape(x)
cond do
num_components > num_features ->
raise ArgumentError,
"""
num_components must be less than or equal to \
num_features = #{num_features}, got #{num_components}
"""
num_components > num_samples ->
raise ArgumentError,
"""
num_components must be less than or equal to \
num_samples = #{num_samples}, got #{num_components}
"""
true ->
nil
end
{u, sigma, vt} = randomized_svd(x, key, opts)
{_u, vt} = Scholar.Decomposition.Utils.flip_svd(u, vt, false)
x_transformed = Nx.dot(x, Nx.transpose(vt))
explained_variance = Nx.variance(x_transformed, axes: [0])
full_variance = Nx.variance(x, axes: [0]) |> Nx.sum()
explained_variance_ratio = explained_variance / full_variance
%__MODULE__{
components: vt,
explained_variance: explained_variance,
explained_variance_ratio: explained_variance_ratio,
singular_values: sigma
}
end
defnp fit_transform_n(x, key, opts) do
module = fit_n(x, key, opts)
Nx.dot(x, Nx.transpose(module.components))
end
defnp randomized_svd(m, key, opts) do
num_components = opts[:num_components]
num_oversamples = opts[:num_oversamples]
num_iter = opts[:num_iter]
n_random = num_components + num_oversamples
{num_samples, num_features} = Nx.shape(m)
transpose = num_samples < num_components
m =
if Nx.equal(transpose, 1) do
Nx.transpose(m)
else
m
end
q = randomized_range_finder(m, key, size: n_random, num_iter: num_iter)
q_t = Nx.transpose(q)
b = Nx.dot(q_t, m)
{uhat, s, vt} = Nx.LinAlg.svd(b)
u = Nx.dot(q, uhat)
vt = Nx.slice(vt, [0, 0], [num_components, num_features])
s = Nx.slice(s, [0], [num_components])
u = Nx.slice(u, [0, 0], [num_samples, num_components])
if Nx.equal(transpose, 1) do
{Nx.transpose(vt), s, Nx.transpose(u)}
else
{u, s, vt}
end
end
defn randomized_range_finder(a, key, opts) do
size = opts[:size]
num_iter = opts[:num_iter]
{_, a_cols} = Nx.shape(a)
{q, _} = Nx.Random.normal(key, shape: {a_cols, size})
a_t = Nx.transpose(a)
{q, _} = Nx.LinAlg.qr(Nx.dot(a, q))
{q, _} = Nx.LinAlg.qr(Nx.dot(a_t, q))
{q, _} =
while {q, {a, a_t, i = Nx.tensor(1), num_iter}}, Nx.less(i, num_iter) do
{q, _} = Nx.LinAlg.qr(Nx.dot(a, q))
{q, _} = Nx.LinAlg.qr(Nx.dot(a_t, q))
{q, {a, a_t, i + 1, num_iter}}
end
{q, _} = Nx.LinAlg.qr(Nx.dot(a, q))
q
end
end