đ Custom convolutions#
In this notebook, overriding the convolution layers operation is demonstrated using kernex. By defining only the kernel operation, the layer can be used in the same way as the original layer and parameter initialization/shape checking is handled automatically.
Direct convolution#
This example demonstrates how to recreate the convolution operation using the kernex library. kernex offers function transformation similar to jax.vmap, that wraps a kernel operation (e.g. lambda input,kernel: sum(input*kernel)) and returns a function that works on array views.
[ ]:
!pip install git+https://github.com/ASEM000/serket --quiet
!pip install kernex --quiet
[1]:
import kernex as kex # for stencil operations like convolutions
import serket as sk
import jax
import jax.random as jr
import jax.numpy as jnp
import numpy.testing as npt
def my_conv(
input: jax.Array,
weight: jax.Array,
bias: jax.Array | None,
strides: tuple[int, ...],
padding: tuple[tuple[int, int], ...],
dilation: tuple[int, ...],
groups: int,
mask: jax.Array | None,
):
# same function signature as serket.nn.conv_nd
del mask #
del dilation # for simplicity
del groups # for simplicity
_, in_features, *kernel_size = weight.shape
@kex.kmap(
kernel_size=(in_features, *kernel_size),
strides=(1, *strides),
padding=((0, 0), *padding),
)
def conv_func(input, weight):
# define the kernel operation
return jnp.sum(input * weight)
# vectorize over the out_features of the weight
out = jax.vmap(conv_func, in_axes=(None, 0))(input, weight)
# squeeze out the vmapped axis
out = jnp.squeeze(out, axis=1)
return out + bias if bias is not None else out
class CustomConv2D(sk.nn.Conv2D):
# override the conv_op
conv_op = staticmethod(my_conv)
k1, k2 = jr.split(jr.key(0), 2)
basic_conv = sk.nn.Conv2D(
in_features=1,
out_features=2,
kernel_size=3,
bias_init=None,
key=k1,
)
custom_conv = CustomConv2D(
in_features=1,
out_features=2,
kernel_size=3,
bias_init=None,
key=k1,
)
# channel-first input
input = jr.uniform(k2, shape=(1, 10, 10))
npt.assert_allclose(
basic_conv(input),
custom_conv(input),
atol=1e-6,
)
# lets check gradients
npt.assert_allclose(
jax.grad(lambda x: basic_conv(x).sum())(input),
jax.grad(lambda x: custom_conv(x).sum())(input),
atol=1e-6,
)
Depthwise convolution#
Similar to the above example, For recreating depthwise convolution, the only addition is to add vectorize the kernel operation over the channels dimension using jax.vmap
[2]:
import kernex as kex # for stencil operations like convolutions
import jax
import jax.random as jr
import jax.numpy as jnp
import numpy.testing as npt
def my_depthwise_conv(
input: jax.Array,
weight: jax.Array,
bias: jax.Array | None,
strides: tuple[int, ...],
padding: tuple[tuple[int, int], ...],
mask: jax.Array | None,
):
# same function signature as serket.nn.depthwise_conv_nd
del mask #
_, _, *kernel_size = weight.shape
@jax.vmap # <- vectorize over the input channels
@kex.kmap(
kernel_size=tuple(kernel_size),
strides=strides,
padding=padding,
)
def conv_func(input, weight):
# define the kernel operation
return jnp.sum(input * weight)
# vectorize over the output channels (filters)
out = jax.vmap(conv_func, in_axes=(None, 0))(input, weight)
out = jnp.squeeze(out, axis=1) # squeeze out the vmapped axis
return out + bias if bias is not None else out
class CustomDepthwiseConv2D(sk.nn.DepthwiseConv2D):
# override the conv_op
conv_op = staticmethod(my_depthwise_conv)
k1, k2 = jr.split(jr.key(0), 2)
basic_conv = sk.nn.DepthwiseConv2D(
in_features=1,
depth_multiplier=2,
kernel_size=3,
bias_init=None,
key=k1,
)
custom_conv = CustomDepthwiseConv2D(
in_features=1,
depth_multiplier=2,
kernel_size=3,
bias_init=None,
key=k1,
)
# channel-first input
input = jr.uniform(k2, shape=(1, 10, 10))
npt.assert_allclose(
basic_conv(input),
custom_conv(input),
atol=1e-6,
)
# lets check gradients
npt.assert_allclose(
jax.grad(lambda x: basic_conv(x).sum())(input),
jax.grad(lambda x: custom_conv(x).sum())(input),
atol=1e-6,
)
Positive kernel convolution#
In this example, a custom convolution operation is defined. As a toy examaple the operation will only multiply weight values that are not zero.
[3]:
import kernex as kex # for stencil operations like convolutions
import serket as sk
import jax
import jax.random as jr
import jax.numpy as jnp
import numpy.testing as npt
def my_custom_conv(
self,
input: jax.Array,
weight: jax.Array,
bias: jax.Array | None,
strides: tuple[int, ...],
padding: tuple[tuple[int, int], ...],
dilation: tuple[int, ...],
groups: int,
mask: jax.Array | None,
):
# same function signature as serket.nn.conv_nd
del mask #
del dilation # for simplicity
del groups # for simplicity
_, in_features, *kernel_size = weight.shape
@kex.kmap(
kernel_size=(in_features, *kernel_size),
strides=(1, *strides),
padding=((0, 0), *padding),
)
def conv_func(input, weight):
# define a custom kernel operation
# that only multiplies the input with the weight
# if the weight is positive
return jnp.sum(input * jnp.where(weight < 0, 0, weight))
# vectorize over the out_features of the weight
out = jax.vmap(conv_func, in_axes=(None, 0))(input, weight)
# squeeze out the vmapped axis
out = jnp.squeeze(out, axis=1)
return out + bias if bias is not None else out
class CustomConv2D(sk.nn.Conv2D):
# override the conv_op
conv_op = staticmethod(my_custom_conv)
k1, k2 = jr.split(jr.key(0), 2)
custom_conv = CustomConv2D(
in_features=1,
out_features=2,
kernel_size=3,
bias_init=None,
key=k1,
)
# channel-first input
input = jr.uniform(k2, shape=(1, 10, 10))
basic_conv(input).shape
[3]:
(2, 10, 10)