Документация Engee

Справка по Bijectors

Функции

_inv_link_chol_lkj(y)

Обратная связывающая функция для множителя Холецкого.

function _link_chol_lkj(w)

Связывающая функция для множителя Холецкого.

Была рассмотрена альтернативная и, возможно, более эффективная реализация:

for i=2:K, j=(i+1):K
    z[i, j] = (w[i, j] / w[i-1, j]) * (z[i-1, j] / sqrt(1 - z[i-1, j]^2))
end

Но эта реализация не будет работать, если w[i-1, j] = 0. Хотя это набор нулевых мер, инициализация единичной матрицы не будет работать.

@torfjelde: дает следующие объяснения:

Для (i, j) в приведенном ниже цикле мы определяем

z₍ᵢ₋₁, ⱼ₎ = w₍ᵢ₋₁,ⱼ₎ * ∏ₖ₌₁ⁱ⁻² (1 / √(1 - z₍ₖ,ⱼ₎²))

и так

z₍ᵢ,ⱼ₎ = w₍ᵢ,ⱼ₎ * ∏ₖ₌₁ⁱ⁻¹ (1 / √(1 - z₍ₖ,ⱼ₎²))
        = (w₍ᵢ,ⱼ₎ * / √(1 - z₍ᵢ₋₁,ⱼ₎²)) * (∏ₖ₌₁ⁱ⁻² 1 / √(1 - z₍ₖ,ⱼ₎²))
        = (w₍ᵢ,ⱼ₎ * / √(1 - z₍ᵢ₋₁,ⱼ₎²)) * (w₍ᵢ₋₁,ⱼ₎ * ∏ₖ₌₁ⁱ⁻² 1 / √(1 - z₍ₖ,ⱼ₎²)) / w₍ᵢ₋₁,ⱼ₎
        = (w₍ᵢ,ⱼ₎ * / √(1 - z₍ᵢ₋₁,ⱼ₎²)) * (z₍ᵢ₋₁,ⱼ₎ / w₍ᵢ₋₁,ⱼ₎)
        = (w₍ᵢ,ⱼ₎ / w₍ᵢ₋₁,ⱼ₎) * (z₍ᵢ₋₁,ⱼ₎ / √(1 - z₍ᵢ₋₁,ⱼ₎²))

что является вышеупомянутой реализацией.

bijector(d::Distribution)

Возвращает биектор с ограничениями и без ограничений для распределения d.

cholesky_lower(X)

Возвращает нижний треугольный множитель Холецкого X в виде Matrix, а не LowerTriangular.

Это тонкая оболочка вокруг cholesky(Hermitian(X)).L, который возвращает Matrix, а не LowerTriangular.

cholesky_upper(X)

Возвращает верхний треугольный множитель Холецкого X в виде Matrix, а не UpperTriangular.

Это тонкая оболочка вокруг cholesky(Hermitian(X)).U, который возвращает Matrix, а не UpperTriangular.

Псевдоним для Base.Fix1(eachcolmaphcat, f).

Представляет функцию f, которая применяется к каждому столбцу входных данных.

combine(m::PartitionMask, x_1, x_2, x_3)

Объединяет x_1, x_2 и x_3 в один вектор.

compute_r(y_minus_z0::AbstractVector{<:Real}, α, α_plus_β_hat)

Вычисляет единственное решение уравнения

Double subscripts: use braces to clarify

при условии, что и .

Поскольку и Double subscripts: use braces to clarify , решение уникально и определяется

Double subscripts: use braces to clarify

где . Подробные сведения см. в приложении А.2 справки.

Ссылки

\D. Rezende, S. Mohamed (2015): Variational Inference with Normalizing Flows. arXiv:1505.05770

Возвращает закон связанности, построенный из x.

Возвращает конструктор закона связанности.

elementwise(f)

Псевдоним для Base.Fix1(broadcast, f).

В случае, когда f::ComposedFunction, результатом будет Base.Fix1(broadcast, f.outer) ∘ Base.Fix1(broadcast, f.inner), а не Base.Fix1(broadcast, f).

find_alpha(wt_y, wt_u_hat, b)

Вычисляет (приблизительное) вещественное решение уравнения

Double subscripts: use braces to clarify

Уникальность решения гарантирована, так как Double subscripts: use braces to clarify . Подробные сведения см. в приложении А.1 справки.

Начальная скобка

Для всех имеем

Double subscripts: use braces to clarify

Таким образом

Double subscripts: use braces to clarify

из чего следует, что Double subscripts: use braces to clarify ]. Чтобы избежать проблем с плавающей запятой, если Double subscripts: use braces to clarify , мы используем более консервативный интервал Double subscripts: use braces to clarify ] в качестве начальной скобки за счет одного дополнительного шага итерации.

Ссылки

\D. Rezende, S. Mohamed (2015): Variational Inference with Normalizing Flows. arXiv:1505.05770

get_u_hat(u::AbstractVector{<:Real}, w::AbstractVector{<:Real})

Возвращает кортеж, состоящий из вектора , гарантирующего инвертируемость плоского слоя, и скаляра .

Математические основы

Согласно приложению А.1, вектор û , определяемый

²

гарантирует, что плоский слой является инвертируемым для всех и . Мы можем переписать û как

²

Кроме того, получаем

Ссылки

\D. Rezende, S. Mohamed (2015): Variational Inference with Normalizing Flows. arXiv:1505.05770

has_constant_bijector(dist_type::Type)

Возвращает true, если тип распределения dist_type имеет постоянный биектор, т. е. возвращаемое значение bijector не зависит от информации о времени выполнения.

isclosedform(b::Transform)::bool
isclosedform(b⁻¹::Inverse{<:Transform})::bool

Возвращает true или false в зависимости от того, имеет ли вычисление b реализацию замкнутой формы.

Большинство биекторов имеют вычисления замкнутой формы, но есть случаи, когда это не так. Например, для обратного вычисления PlanarLayer требуется итеративная процедура.

isinvertible(t)

Возвращает true, если t является инвертируемым, и false в противном случае.

logabsdetjac!(b, x[, logjac])

Вычисляет log(abs(det(J(b, x)))) и сохраняет результат в logjac, где J(b, x) является якобианом b при x.

logabsdetjac(b, x)

Возвращает log(abs(det(J(b, x)))), где J(b, x) является якобианом b при x.

logabsdetjacinv(b, y)

Псевдоним для logabsdetjac(inverse(b), y).

ordered(d::Distribution)

Возвращает распределение (Distribution), поддержкой которого являются упорядоченные векторы, т. е. векторы с элементами с возрастающем уровнем упорядоченности.

В настоящее время это преобразование поддерживается только для распределений без ограничений.

output_length(f, len::Int)

Возвращает выходную длину f при входной длине len.

output_size(f, sz)

Возвращает выходной размер f при входном размере sz.

partition(m::PartitionMask, x)

Разбивает x на 3 несмежных подвектора.

transform!(b, x[, y])

Преобразует x с помощью b и сохраняет результат в y.

Если y не указан, в качестве вывода используется x.

transform(b, x)

Преобразует x с помощью b, рассматривая x как одно входное значение.

transformed(d::Distribution)
transformed(d::Distribution, b::Bijector)

Связывает распределение d с биектором b, возвращая TransformedDistribution.

Если биектор не указан, т. е. вызывается transformed(d), возвращается transformed(d, bijector(d)).

triu1_to_vec(X::AbstractMatrix{<:Real})

Извлекает элементы из верхнего треугольника X со смещением 1 и возвращает их в виде вектора.

triu_mask(X::AbstractMatrix, k::Int)

Возвращает маску для элементов X над k-й диагональю.

triu_to_vec(X::AbstractMatrix{<:Real})

Извлекает элементы из верхнего треугольника X и возвращает их в виде вектора.

vec_to_triu(x::AbstractVector{<:Real})

Строит матрицу из вектора x, заполняя верхний треугольник.

vec_to_triu1(x::AbstractVector{<:Real})

Строит матрицу из вектора x, заполняя верхний треугольник со смещением 1.

with_logabsdet_jacobian!(b, x[, y, logjac])

Вычисляет transform(b, x) и logabsdetjac(b, x), сохраняя результат в y и logjac, соответственно.

Если y не задан, вместо него будет использоваться x.

По умолчанию вызывает with_logabsdet_jacobian(b, x) и обновляет y и logjac в соответствии с результатом.

inverse(t::Transform)

Возвращает обращение преобразования t.

Типы

Абстрактный тип биектора, т. е. дифференцируемая биекция с дифференцируемым обратным.

CorrBijector <: Bijector

Реализация биектора метода параметризации в Stan для матрицы корреляции: https://mc-stan.org/docs/2_23/reference-manual/correlation-matrix-transform-section.html

В принципе, неограниченная строго верхняя треугольная матрица y преобразуется в матрицу корреляции с помощью следующей читаемой, но не очень эффективной формы:

K = size(y, 1)
z = tanh.(y)

for j=1:K, i=1:K
    if i>j
        w[i,j] = 0
    elseif 1==i==j
        w[i,j] = 1
    elseif 1<i==j
        w[i,j] = prod(sqrt(1 .- z[1:i-1, j].^2))
    elseif 1==i<j
        w[i,j] = z[i,j]
    elseif 1<i<j
        w[i,j] = z[i,j] * prod(sqrt(1 .- z[1:i-1, j].^2))
    end
end

Легко увидеть, что каждый столбец — это единичный вектор, например:

w3' w3 ==
w[1,3]^2 + w[2,3]^2 + w[3,3]^2 ==
z[1,3]^2 + (z[2,3] * sqrt(1 - z[1,3]^2))^2 + (sqrt(1-z[1,3]^2) * sqrt(1-z[2,3]^2))^2 ==
z[1,3]^2 + z[2,3]^2 * (1-z[1,3]^2) + (1-z[1,3]^2) * (1-z[2,3]^2) ==
z[1,3]^2 + z[2,3]^2 - z[2,3]^2 * z[1,3]^2 + 1 -z[1,3]^2 - z[2,3]^2 + z[1,3]^2 * z[2,3]^2 ==
1

А диагональные элементы положительны, поэтому w является множителем Холецкого для положительной матрицы.

x = w' * w

Рассмотрим представление блочной матрицы для x.

x = [w1'; w2'; ... wn'] * [w1 w2 ... wn] ==
[w1'w1 w1'w2 ... w1'wn;
 w2'w1 w2'w2 ... w2'wn;
 ...
]

Диагональные элементы заданы с помощью wk’wk = 1, таким образом, x — это матрица корреляции.

Каждый шаг является инвертируемым, поэтому это — биекция (биектор).

Примечание. Реализация не следует «управляемому выражению» напрямую, потому что уравнение кажется неверным (30.07.2020). Она напрямую следует определению выше «управляемого выражения», которое также описано в упомянутом документе.

Coupling{F, M}(θ::F, mask::M)

Реализует слой соединения, как определено в [1].

Примеры

julia> using Bijectors: Shift, Coupling, PartitionMask, coupling, couple

julia> m = PartitionMask(3, [1], [2]); # <= используем x[2] для параметризации преобразования x[1]

julia> cl = Coupling(Shift, m); # <= выполним `y[1:1] = x[1:1] + x[2:2]`;

julia> x = [1., 2., 3.];

julia> cl(x)
3-element Vector{Float64}:
 3.0
 2.0
 3.0

julia> inverse(cl)(cl(x))
3-element Vector{Float64}:
 1.0
 2.0
 3.0

julia> coupling(cl) # получим сопоставление `Bijector` `θ -> b(⋅, θ)`
Shift

julia> couple(cl, x) # получим `Bijector` из `x`
Shift([2.0])

julia> with_logabsdet_jacobian(cl, x)
([3.0, 2.0, 3.0], 0.0)

Ссылки

[1] Kobyzev, I., Prince, S., & Brubaker, M. A., Normalizing flows: introduction and ideas, CoRR, (), (2019).

inverse(b::Transform)
Inverse(b::Transform)

Transform, представляющий обратное преобразование b.

LeakyReLU{T}(α::T) <: Bijector

Определяет инвертируемое сопоставление

x ↦ x if x ≥ 0 else αx

где α > 0.

NamedCoupling{target, deps, F} <: AbstractNamedTransform

Реализует слой соединения для именованных биекторов.

См. также описание Coupling.

Примеры

julia> using Bijectors: NamedCoupling, Scale

julia> b = NamedCoupling(:b, (:a, :c), (a, c) -> Scale(a + c));

julia> x = (a = 1., b = 2., c = 3.);

julia> b(x)
(a = 1.0, b = 8.0, c = 3.0)

julia> (a = x.a, b = (x.a + x.c) * x.b, c = x.c)
(a = 1.0, b = 8.0, c = 3.0)
NamedTransform <: AbstractNamedTransform

Заключает в оболочку NamedTuple пары «ключ -> Bijector», реализуя оценку, инверсию и т. д.

Примеры

julia> using Bijectors: NamedTransform, Scale

julia> b = NamedTransform((a = Scale(2.0), b = exp));

julia> x = (a = 1., b = 0., c = 42.);

julia> b(x)
(a = 2.0, b = 1.0, c = 42.0)

julia> (a = 2 * x.a, b = exp(x.b), c = x.c)
(a = 2.0, b = 1.0, c = 42.0)
OrderedBijector()

Биектор, сопоставляющий упорядоченные векторы в ℝᵈ с неупорядоченными векторами в ℝᵈ.

См. также

  • документацию по Stan.

    • Обратите внимание, что это преобразование и его инверсия являются противоположными в этой справочной документации.

PartitionMask{A}(A_1::A, A_2::A, A_3::A) where {A}

Оно используется для разбиения и рекомбинации вектора на 3 несвязанных «подвектора».

Реализации

  • partition(m::PartitionMask, x): разбивает x на 3 несмежных подвектора.

  • combine(m::PartitionMask, x_1, x_2, x_3): объединяет 3 несмежных вектора в один.

Обратите внимание, что PartitionMask не является биектором (Bijector). Это действительно биекция, но она не подчиняется интерфейсу Bijector.

В основном она используется в Coupling, где мы хотим разделить входные данные на 3 части: одну часть для преобразования, одну часть для сопоставления с пространством параметров преобразования, примененного к первой части, а последняя часть вектора ни для чего не используется.

Примеры

julia> using Bijectors: PartitionMask, partition, combine

julia> m = PartitionMask(3, [1], [2]) # <= предполагается, что входная длина равна 3
PartitionMask{Bool,SparseArrays.SparseMatrixCSC{Bool,Int64}}(
  [1, 1]  =  true,
  [2, 1]  =  true,
  [3, 1]  =  true)

julia> # Разбиение на 3 части; последняя часть предположительно является индексами `[3, ]
       # из того факта, что `[1]` и `[2]` не являются всеми индексами в `1:3`.
       x1, x2, x3 = partition(m, [1., 2., 3.])
([1.0], [2.0], [3.0])

julia> # Рекомбинирует секции в вектор
       combine(m, x1, x2, x3)
3-element Array{Float64,1}:
 1.0
 2.0
 3.0

Обратите внимание, что базовая матрица SparseMatrix использует Bool в качестве типа элемента. Также можно указать, что это может быть другой тип, используя ключевое слово sp_type:

julia> m = PartitionMask{Float32}(3, [1], [2])
PartitionMask{Float32,SparseArrays.SparseMatrixCSC{Float32,Int64}}(
  [1, 1]  =  1.0,
  [2, 1]  =  1.0,
  [3, 1]  =  1.0)
PartitionMask(n::Int, indices)

Предполагает, что вы хотите разделить вектор, где indices обозначают части вектора, к которым вы хотите применить биектор.

Permute{A} <: Bijector

Реализация биектора для перестановки. Перестановка выполняется с помощью матрицы типа A. Существует несколько различных способов построения Permute:

Permute([0 1; 1 0])          # сопоставит [1, 2] => [2, 1]
Permute([2, 1])              # сопоставит [1, 2] => [2, 1]
Permute(2, 2 => 1, 1 => 2)   # сопоставит [1, 2] => [2, 1]
Permute(2, [1, 2] => [2, 1]) # сопоставит [1, 2] => [2, 1]

Если это не совсем понятно, можно посмотреть примеры.

Примеры

Простой пример: перестановка вектора размером 3.

julia> b1 = Permute([
           0 1 0;
           1 0 0;
           0 0 1
       ])
Permute{Array{Int64,2}}([0 1 0; 1 0 0; 0 0 1])

julia> b2 = Permute([2, 1, 3])           # указание сразу всех элементов
Permute{SparseArrays.SparseMatrixCSC{Float64,Int64}}(

  [2, 1]  =  1.0
  [1, 2]  =  1.0
  [3, 3]  =  1.0)

julia> b3 = Permute(3, 2 => 1, 1 => 2)    # поэлементно
Permute{SparseArrays.SparseMatrixCSC{Float64,Int64}}(
  [2, 1]  =  1.0
  [1, 2]  =  1.0
  [3, 3]  =  1.0)

julia> b4 = Permute(3, [1, 2] => [2, 1])  # поблочно
Permute{SparseArrays.SparseMatrixCSC{Float64,Int64}}(
  [2, 1]  =  1.0
  [1, 2]  =  1.0
  [3, 3]  =  1.0)

julia> b1.A == b2.A == b3.A == b4.A
true

julia> b1([1., 2., 3.])
3-element Array{Float64,1}:
 2.0
 1.0
 3.0

julia> b2([1., 2., 3.])
3-element Array{Float64,1}:
 2.0
 1.0
 3.0

julia> b3([1., 2., 3.])
3-element Array{Float64,1}:
 2.0
 1.0
 3.0

julia> b4([1., 2., 3.])
3-element Array{Float64,1}:
 2.0
 1.0
 3.0

julia> inverse(b1)
Permute{LinearAlgebra.Transpose{Int64,Array{Int64,2}}}([0 1 0; 1 0 0; 0 0 1])

julia> inverse(b1)(b1([1., 2., 3.]))
3-element Array{Float64,1}:
 1.0
 2.0
 3.0
RationalQuadraticSpline{T} <: Bijector

Реализация потока рациональных квадратичных сплайнов [1].

  • За пределами интервала [minimum(widths), maximum(widths)] это сопоставление задается с помощью сопоставления тождественности.

  • Внутри интервала оно задается монотонным сплайном (т. е. монотонными многочленами связанными в промежуточных точках) с фиксированными конечными точками, чтобы непрерывно преобразовываться в сопоставление тождественности.

Для большей эффективности существуют отдельные реализации для нульмерных и одномерных входных данных.

Примечания

Для RationalQuadraticSpline существует два конструктора:

  • RationalQuadraticSpline(widths, heights, derivatives): предполагается, что widths,

heights и derivatives удовлетворяют ограничениям, которые делают этот биектор допустимым, т. е.

  • widths: монотонно увеличиваются и length(widths) == K,

  • heights: монотонно увеличиваются и length(heights) == K,

  • derivatives: неотрицательны и derivatives[1] == derivatives[end] == 1.

  • RationalQuadraticSpline(widths, heights, derivatives, B): отличны от длин, никакие предположения о параметрах не делаются. Поэтому преобразуем параметры с учетом следующего:

  • widths_new ∈ [-B, B]ᴷ⁺¹, где K == length(widths),

  • heights_new ∈ [-B, B]ᴷ⁺¹, где K == length(heights),

  • derivatives_new ∈ (0, ∞)ᴷ⁺¹ при derivatives_new[1] == derivates_new[end] == 1, where (K - 1) == length(derivatives).

Примеры

Одномерность

julia> using StableRNGs: StableRNG; rng = StableRNG(42);  # Для воспроизводимости.

julia> using Bijectors: RationalQuadraticSpline

julia> K = 3; B = 2;

julia> # Монотонный сплайн в '[-B, B] с `K` промежуточных узлов или «точек соединения».
       b = RationalQuadraticSpline(randn(rng, K), randn(rng, K), randn(rng, K - 1), B);

julia> b(0.5) # внутри `[-B, B]` → преобразуется
1.1943325397834206

julia> b(5.) # за пределами `[-B, B]` → не преобразуется
5.0

julia> b = RationalQuadraticSpline(b.widths, b.heights, b.derivatives);

julia> b(0.5) # внутри `[-B, B]` → преобразуется
1.1943325397834206

julia> d = 2; K = 3; B = 2;

julia> b = RationalQuadraticSpline(randn(rng, d, K), randn(rng, d, K), randn(rng, d, K - 1), B);

julia> b([-1., 1.])
2-element Vector{Float64}:
 -1.5660106244288925
  0.5384702734738573

julia> b([-5., 5.])
2-element Vector{Float64}:
 -5.0
  5.0

julia> b([-1., 5.])
2-element Vector{Float64}:
 -1.5660106244288925
  5.0

Ссылки

[1] Durkan, C., Bekasov, A., Murray, I., & Papamakarios, G., Neural Spline Flows, CoRR, arXiv:1906.04032 [stat.ML], (2019).

Reshape(in_shape, out_shape)

Bijector, который преобразует входную форму в выходную.

Пример

julia> using Bijectors: Reshape

julia> b = Reshape((2, 3), (3, 2))
Reshape{Tuple{Int64, Int64}, Tuple{Int64, Int64}}((2, 3), (3, 2))

julia> Array(transform(b, reshape(1:6, 2, 3)))
3×2 Matrix{Int64}:
 1  4
 2  5
 3  6
Stacked(bs)
Stacked(bs, ranges)
stack(bs::Bijector...)

Bijector который объединяет биекторы, которые затем могут быть применены к вектору, где bs[i]::Bijector применяется к x[ranges[i]]::UnitRange{Int}.

Аргументы

  • bs может быть либо кортежем (Tuple), либо массивом AbstractArray нуль- и (или) одномерных биекторов.

    • Если bs является кортежем (Tuple), реализации устойчивы по типу и используют сгенерированные функции.

    • Если bs является массивом AbstractArray, реализации неустойчивы по типу и используют итеративные методы.

  • ranges должен быть итерируемым объектом, состоящим из UnitRange{Int}.

    • length(bs) == length(ranges) должно быть верным.

Примеры

b1 = Logit(0.0, 1.0)
b2 = identity
b = stack(b1, b2)
b([0.0, 1.0]) == [b1(0.0), 1.0]  # => верно

Абстрактный тип для преобразования.

Реализация

Подтип Transform должен, по крайней мере, реализовывать функцию transform(b, x).

Если Transform также является инвертируемым:

  • Обязательные методы:

    • Любой из следующих:

      • transform(::Inverse{<:MyTransform}, x): the transform for its inverse.

      • InverseFunctions.inverse(b::MyTransform): returns an existing Transform.

    • logabsdetjac: вычисляет множитель якобиана log-abs-det.

  • Необязательные методы:

    • with_logabsdet_jacobian: сочетание transform и logabsdetjac. Полезно в тех случаях, когда мы можем использовать общие вычисления в двух. can exploit shared computation in the two.

Для приведенных выше методов существуют изменяемые версии, которые могут быть реализованы при необходимости:

VecCholeskyBijector <: Bijector

Биектор для преобразования множителя Холецкого матрицы корреляции в неограниченный вектор.

Поля

  • mode: Symbol. Управляет обратным преобразованием:

    • если mode === :U, возвращает LinearAlgebra.Cholesky, содержащий множитель UpperTriangular

    • если mode === :L, возвращает LinearAlgebra.Cholesky, содержащий множитель LowerTriangular

Справка

См. также описание VecCorrBijector.

Пример

julia> using LinearAlgebra

julia> using StableRNGs; rng = StableRNG(42);

julia> b = Bijectors.VecCholeskyBijector(:U);

julia> X = rand(rng, LKJCholesky(3, 1, :U))  # Выборка матрицы корреляции.
Cholesky{Float64, Matrix{Float64}}
U factor:
3×3 UpperTriangular{Float64, Matrix{Float64}}:
 1.0  0.937494   0.865891
  ⋅   0.348002  -0.320442
  ⋅    ⋅         0.384122

julia> y = b(X)  # Преобразование в представление неограниченного вектора.
3-element Vector{Float64}:
 -0.8777149781928181
 -0.3638927608636788
 -0.29813769428942216

julia> X_inv = inverse(b)(y);
julia> X_inv.U ≈ X.U  # (✓) Круговой путь через `b` и его обратную величину.
true
julia> X_inv.L ≈ X.L  # (✓) Также работает для нижнего треугольного множителя.
true
VecCorrBijector <: Bijector

Биектор для преобразования матрицы корреляции в неограниченный вектор.

Справка

См. также: CorrBijector и VecCholeskyBijector

Пример

julia> using LinearAlgebra

julia> using StableRNGs; rng = StableRNG(42);

julia> b = Bijectors.VecCorrBijector();

julia> X = rand(rng, LKJ(3, 1))  # Выборка матрицы корреляции.
3×3 Matrix{Float64}:
  1.0       -0.705273   -0.348638
 -0.705273   1.0         0.0534538
 -0.348638   0.0534538   1.0

julia> y = b(X)  # Преобразование в представление неограниченного вектора.
3-element Vector{Float64}:
 -0.8777149781928181
 -0.3638927608636788
 -0.29813769428942216

julia> inverse(b)(y) ≈ X  # (✓) Круговой путь через `b` и его обратную величину.
true