Python Subclasses

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.

Examples

A custom constraint (R6)

For demonstration purposes, let’s say you want to implement a custom keras kernel constraint via subclassing. Using R6:

NonNegative <- R6::R6Class("NonNegative",
  inherit = keras$constraints$Constraint,
  public = list(
    "__call__" = function(x) {
       w * k_cast(w >= 0, k_floatx())
    }
  )
)
NonNegative <- r_to_py(NonNegative, convert=TRUE)

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:

A custom constraint (%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) {
    w * k_cast(w >= 0, k_floatx())
  }
}

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%:

NonNegative(keras$constraints$Constraint, convert=FALSE) %py_class% { ... }

In all other regards, %py_class% is equivalent to r_to_py(R6Class()) (indeed, under the hood, they do the same thing).

A custom layer (R6)

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:

CustomLayer <- R6::R6Class("CustomLayer",

  inherit = keras$layers$Layer,

  public = list(

    initialize = function(output_dim) {
      self$output_dim <- output_dim
    },

    build = function(input_shape) {
      self$kernel <- self$add_weight(
        name = '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)
    }
  )
)

A custom layer (%py_class%)

or using %py_class%:

CustomLayer(keras$layers$Layer) %py_class% {

  initialize = function(output_dim) {
    self$output_dim <- output_dim
  }

  build = function(input_shape) {
    self$kernel <- self$add_weight(
      name = '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)
  }
}