Note
Go to the end to download the full example code
Advanced usage: Vi, Vj, Pm helpers and symbolic variables
This tutorial shows some advanced features of the LazyTensor class.
import time
import torch
from pykeops.torch import LazyTensor
use_cuda = torch.cuda.is_available()
tensor = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
The Vi, Vj, Pm decorators
This part presents an alternative style for using the KeOps LazyTensor wrapper, that
some users may find more convenient. The idea is to always input 2D tensors,
and use the Vi
, Vj
helpers described below to specify wether the tensor is to be
understood as indexed by i (i.e. with an equivalent shape of the form (M,1,D))
or by j (shape of the form (1,N,D)). Note that it is currently not possible to use
additional batch dimensions with this specific syntax.
Here is how it works, if we want to perform a simple gaussian convolution.
We first create the dataset using 2D tensors:
M, N = (100000, 200000) if use_cuda else (1000, 2000)
D = 3
x = torch.randn(M, D).type(tensor)
y = torch.randn(N, D).type(tensor)
Then we use Vi
and Vj
to convert to KeOps LazyTensor objects
from pykeops.torch import Vi, Vj
x_i = Vi(x) # (M, 1, D) LazyTensor, equivalent to LazyTensor( x[:,None,:] )
y_j = Vj(y) # (1, N, D) LazyTensor, equivalent to LazyTensor( y[None,:,:] )
and perform our operations:
D2xy = ((x_i - y_j) ** 2).sum()
gamma = D2xy.sum_reduction(dim=1)
Note that in the first line we used sum
without any axis or dim parameter.
This is equivalent to sum(-1)
or sum(dim=2)
, because
the axis parameter is set to 2
by default. But speaking about dim=2
here with the Vi
, Vj
helpers could be misleading.
Similarly we used sum_reduction
instead of sum
to make it clear
that we perform a reduction, but sum and sum_reduction with dim=0
or 1
are equivalent (however sum_reduction
with dim=2
is forbidden)
We have not spoken about Pm
yet. In fact Pm
is used to introduce
scalars or 1D vectors of parameters into formulas, but it is useless
in such examples because scalars, lists of scalars, 0D or 1D NumPy vectors
are automatically converted into parameters when combined with
KeOps formulas. We will have to use Pm
in other parts below.
Other examples
All KeOps operations and reductions are available, either via operators or methods. Here are one line examples
Getting indices of closest point between x and y:
indmin = ((x_i - y_j) ** 2).sum().argmin(axis=0)
Scalar product, absolute value, power operator, and a SoftMax type reduction:
res = (abs(x_i | y_j) ** 1.5).sumsoftmaxweight(x_i, axis=1)
The []
operator can be used to do element selection or slicing
(Elem or Extract operation in KeOps).
res = (x_i[:2] * y_j[2:] - x_i[2:] * y_j[:2]).sqnorm2().sum(axis=1)
Kernel inversion : let’s do a gaussian kernel inversion. Note that
we have to use both Vi
and Vj
helpers on the same tensor x
here.
e_i = Vi(torch.rand(M, D).type(tensor))
x_j = Vj(x)
D2xx = LazyTensor.sum((x_i - x_j) ** 2)
sigma = 0.25
Kxx = (-D2xx / sigma**2).exp()
res = LazyTensor.solve(Kxx, e_i, alpha=0.1)
Use of loops or vector operations for sums of kernels
Let us now perform again a kernel convolution, but replacing the gaussian kernel by a sum of 4 gaussian kernels with different sigma widths. This can be done as follows with a for loop:
sigmas = tensor([0.5, 1.0, 2.0, 4.0])
b_j = Vj(torch.rand(N, D).type(tensor))
Kxy = 0
for sigma in sigmas:
Kxy += LazyTensor.exp(-D2xy / sigma**2)
gamma = (Kxy * b_j).sum_reduction(axis=1)
Note again that after the for loop, no actual computation has been performed. So we can actually build formulas with much more flexibility than with the use of Genred.
Ok, this was just to showcase the use of a for loop, however in this case there is no need for a for loop, we can do simply:
Kxy = LazyTensor.exp(-D2xy / sigmas**2).sum()
gamma = (Kxy * b_j).sum_reduction(axis=1)
This is because all operations are broadcasted, so the /
operation above
works and corresponds to a ./
(scalar-vector element-wise division)
The “no call” mode
When using a reduction operation, the user has the choice to actually not perform the computation directly and instead output a KeOps object which is direclty callable. This can be done using the “call=False” option
M, N = (100000, 200000) if use_cuda else (1000, 2000)
D, Dv = 3, 4
x = torch.randn(M, D).type(tensor)
y = torch.randn(N, D).type(tensor)
b = torch.randn(N, Dv).type(tensor)
sigmas = tensor([0.5, 1.0, 2.0, 4.0])
from pykeops.torch import Vi, Vj, Pm
x_i = Vi(x) # (M, 1, D) LazyTensor, equivalent to LazyTensor( x[:,None,:] )
y_j = Vj(y) # (1, N, D) LazyTensor, equivalent to LazyTensor( y[None,:,:] )
b_j = Vj(b) # (1, N, D) LazyTensor, equivalent to LazyTensor( b[None,:,:] )
D2xy = ((x_i - y_j) ** 2).sum(-1)
Kxy = LazyTensor.exp(-D2xy / sigmas**2).sum()
gammafun = (Kxy * b_j).sum_reduction(axis=1, call=False)
Here gammafun is a function and can be evaluated later
gamma = gammafun()
This is usefull in order to avoid the small overhead caused by using the container syntax inside loops if one wants to perform a large number of times the same reduction. Here is an example where we compare the two approaches on small size data to see the effect of the overhead
M, N = 50, 30
x = torch.rand(M, D).type(tensor)
y = torch.rand(N, D).type(tensor)
beta = torch.rand(N, Dv).type(tensor)
xi, yj, bj = Vi(x), Vj(y), Vj(beta)
dxy2 = LazyTensor.sum((xi - yj) ** 2)
Niter = 1000
start = time.time()
for k in range(Niter):
Kxyb = LazyTensor.exp(-dxy2 / sigmas**2).sum() * bj
gamma = Kxyb.sum_reduction(axis=1)
end = time.time()
print(
"Timing for {} iterations: {:.5f}s = {} x {:.5f}s".format(
Niter, end - start, Niter, (end - start) / Niter
)
)
start = time.time()
Kxyb = LazyTensor.exp(-dxy2 / sigmas**2).sum() * bj
gammafun = Kxyb.sum_reduction(axis=1, call=False)
for k in range(Niter):
gamma = gammafun()
end = time.time()
print(
"Timing for {} iterations: {:.5f}s = {} x {:.5f}s".format(
Niter, end - start, Niter, (end - start) / Niter
)
)
Timing for 1000 iterations: 0.25008s = 1000 x 0.00025s
Timing for 1000 iterations: 0.14151s = 1000 x 0.00014s
Of course this means the user has to perform in-place operations
over tensors x
, y,
beta
inside the loop, otherwise the result of the
call to gammafun
will always be the same. This is not very convenient,
so we provide also a “symbolic variables” syntax.
Using “symbolic” variables in formulas
Instead of inputing tensors to the Vi
, Vj
, Pm
helpers, one may specify
the variables as symbolic, providing an index and a dimension:
xi = Vi(0, D)
yj = Vj(1, D)
bj = Vj(2, Dv)
Sigmas = Pm(3, 4)
Now we build the formula as before
dxy2 = LazyTensor.sum((xi - yj) ** 2)
Kxyb = LazyTensor.exp(-dxy2 / Sigmas**2).sum() * bj
gammafun = Kxyb.sum_reduction(axis=1)
Note that we did not have to specify call=False
because since the
variables are symbolic, no computation can be done of course. So the
ouput is automatically a function. We can evaluate it by providing the
arguments in the order specified by the index argument given to Vi
, Vj
, Pm
:
gamma = gammafun(x, y, beta, sigmas)
Symbolic and non symbolic variables can be mixed. For example if we want
to fix x
, beta
and sigmas
in the previous example and make the reduction
a function of y
only we can write:
xi = Vi(x)
yj = Vj(0, D)
bj = Vj(beta)
dxy2 = LazyTensor.sum((xi - yj) ** 2)
Kxyb = LazyTensor.exp(-dxy2 / sigmas**2).sum() * bj
gammafun = Kxyb.sum_reduction(axis=1)
print(gammafun)
gamma = gammafun(y)
KeOps LazyTensor
formula: (Sum(Exp((Minus(Sum(Square((Var(1,3,0) - Var(0,3,1))))) / Var(2,4,2)))) * Var(3,4,1))
symbolic variables: Var(0, 3, 1)
shape: (50, 30, 4)
reduction: Sum (axis=1)
Total running time of the script: (0 minutes 6.818 seconds)