When using keras, a desire to create Python-based subclasses can arise in a number of ways. For example, when you want to:
In such scenarios, the most powerful and flexible approach is to directly inherit from, and then modify and/or enhance an appropriate Python class.
Subclassing a Python class in R is generally straightforward. Two syntaxes are provided: one that adheres to R conventions and uses R6::R6Class
as the class constructor, and one that adheres more to Python conventions, and attempts to replicate Python syntax in R.
For demonstration purposes, let’s say you want to implement a custom keras kernel constraint via subclassing. Using R6:
<- R6::R6Class("NonNegative",
NonNegative inherit = keras$constraints$Constraint,
public = list(
"__call__" = function(x) {
* k_cast(w >= 0, k_floatx())
w
}
)
)<- r_to_py(NonNegative, convert=TRUE) NonNegative
The r_to_py
method will convert an R6 class generator into a Python class generator. After conversion, Python class generators will be different from R6 class generators in a few ways:
New class instances are generated by calling the class directly: NonNegative()
(not NonNegative$new()
)
All methods (functions) are (potentially) modified to ensure their first argument is self
.
All methods have in scope __class__
, super
and the class name (NonNegative
).
For convenience, some method names are treated as aliases:
initialize
is treated as an alias for __init__
()finalize
is treated as an alias for __del__
()super
can be accessed in 3 ways:
$initialize() super
super(NonNegative, self)$`__init__`()
super()$`__init__`()
When subclassing Keras base classes, it is generally your responsibility to call super$initialize()
if you are masking a superclass initializer by providing your own initialize
method.
Passing convert=FALSE
to r_to_py()
will mean that all R methods will receive Python objects as arguments, and are expected to return Python objects. This allows for some features not available with convert=TRUE
, namely, modifying some Python objects, like dictionaries or lists, in-place.
Active bindings (methods supplied to R6Class(active=...)
) are converted to Python @property
-decorated methods.
R6 classes with private methods or attributes are not supported.
The argument supplied to inherit
can be:
NULL
metaclass=
, only for advanced Python use cases)%py_class%
)As an alternative to r_to_py(R6Class(...))
, we also provide %py_class%
, a more concise alternative syntax for achieving the same outcome. %py_class%
is heavily inspired by the Python syntax for the class
statement in R, and is especially convenient when translating Python code to R. Translating the above example, you could write the same using %py_class%
:
NonNegative(keras$constraints$Constraint) %py_class% {
"__call__" = function(x) {
* k_cast(w >= 0, k_floatx())
w
} }
Notice, this is very similar to the equivalent Python code:
class NonNegative(tf.keras.constraints.Constraint):
def __call__(self, w):
return w * tf.cast(tf.math.greater_equal(w, 0.), w.dtype)
Some (potentially surprising) notes about %py_class%
:
Just like the Python class
statement, it assigns the constructed class in the current scope! (There is no need to write NonNegative <- ...
).
The left hand side can be:
ClassName
ClassName(Superclass1, Superclass2, metaclass=my_metaclass)
The right hand side is evaluated in a new environment to form the namespace for the class methods.
One keyword treated specially is convert
. If you want to call r_to_py
with convert=FALSE
, pass it as a keyword:
NonNegative(keras$constraints$Constraint, convert=FALSE) %py_class% { ... }
__doc__
attribute of the class or method. The doc string will then be visible to both python and R tools e.g. reticulate::py_help()
. See ?py_class
for an example.In all other regards, %py_class%
is equivalent to r_to_py(R6Class())
(indeed, under the hood, they do the same thing).
The same pattern can be extended to all sorts of keras objects. For example, a custom layer can be written by subclassing the base Keras Layer:
<- R6::R6Class("CustomLayer",
CustomLayer
inherit = keras$layers$Layer,
public = list(
initialize = function(output_dim) {
$output_dim <- output_dim
self
},
build = function(input_shape) {
$kernel <- self$add_weight(
selfname = 'kernel',
shape = list(input_shape[[2]], self$output_dim),
initializer = initializer_random_normal(),
trainable = TRUE
)
},
call = function(x, mask = NULL) {
k_dot(x, self$kernel)
},
compute_output_shape = function(input_shape) {
list(input_shape[[1]], self$output_dim)
}
) )
%py_class%
)or using %py_class%
:
CustomLayer(keras$layers$Layer) %py_class% {
= function(output_dim) {
initialize $output_dim <- output_dim
self
}
= function(input_shape) {
build $kernel <- self$add_weight(
selfname = 'kernel',
shape = list(input_shape[[2]], self$output_dim),
initializer = initializer_random_normal(),
trainable = TRUE
)
}
= function(x, mask = NULL) {
call k_dot(x, self$kernel)
}
= function(input_shape) {
compute_output_shape list(input_shape[[1]], self$output_dim)
} }