diff --git a/NAMESPACE b/NAMESPACE index 4c4b200b2..7d066382d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,16 +1,19 @@ # Generated by roxygen2: do not edit by hand S3method("!=",keras_shape) +S3method("$",python.builtin.super) S3method("$",python_builtin_super_getter) S3method("$<-",keras.src.callbacks.callback.Callback) S3method("+",keras.src.backend.common.keras_tensor.KerasTensor) S3method("==",keras.src.backend.common.keras_tensor.KerasTensor) S3method("==",keras_shape) S3method("[",keras_shape) +S3method("[[",python.builtin.super) S3method("[[",python_builtin_super_getter) S3method(Arg,keras.src.backend.Tensor) S3method(Arg,keras.src.backend.common.keras_tensor.KerasTensor) S3method(Summary,keras_shape) +S3method(all,equal.numpy.ndarray) S3method(as.array,jax.Array) S3method(as.array,jaxlib._jax.ArrayImpl) S3method(as.array,jaxlib.xla_extension.ArrayImpl) @@ -494,6 +497,7 @@ export(metric_sum) export(metric_top_k_categorical_accuracy) export(metric_true_negatives) export(metric_true_positives) +export(named_list) export(new_callback_class) export(new_layer_class) export(new_learning_rate_schedule_class) @@ -806,6 +810,7 @@ export(optimizer_sgd) export(pad_sequences) export(pop_layer) export(predict_on_batch) +export(py_help) export(py_require) export(py_to_r) export(quantize_weights) @@ -887,6 +892,7 @@ importFrom(reticulate,py_func) importFrom(reticulate,py_get_attr) importFrom(reticulate,py_get_item) importFrom(reticulate,py_has_attr) +importFrom(reticulate,py_help) importFrom(reticulate,py_install) importFrom(reticulate,py_is_null_xptr) importFrom(reticulate,py_iterator) diff --git a/NEWS.md b/NEWS.md index ec7e290a7..a390cd1aa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,60 +1,88 @@ # keras3 (development version) -- Added S3 methods for JAX array: `str`, `as.array`, `as.double`, `as.integer`, `as.numeric`. - -- Added `str` S3 method for Keras Variables. - -- `layer_reshape()` can now accept `-1` as a sentinel for an automatically calculated axis size. +- Expanded numeric operations with `op_layer_normalization()`, `op_cbrt()`, + `op_corrcoef()`, `op_deg2rad()`, `op_heaviside()`, the new `op_sparse_sigmoid()` + plus matching `activation_sparse_sigmoid()`, and an `attn_logits_soft_cap` + argument for `op_dot_product_attention()`. -- Updated dependencies declared by `use_backend("jax", gpu=TRUE)` - for compatability with `keras-hub`. +- Added signal window operations: `op_bartlett()`, `op_blackman()`, + `op_hamming()`, `op_hanning()`, and `op_kaiser()`. -- Added training loop configuration helpers: - `config_max_epochs()`, `config_set_max_epochs()`, `config_max_steps_per_epoch()`, - and `config_set_max_steps_per_epoch()`. The caps can also be set via the - `KERAS_MAX_EPOCHS` and `KERAS_MAX_STEPS_PER_EPOCH` environment variables. - Added `config_is_nnx_enabled()` to check whether JAX NNX features are enabled. +- Added `loss_categorical_generalized_cross_entropy()` for training with noisy + labels. - LoRA-enabled layers (`layer_dense()`, `layer_embedding()`, `layer_einsum_dense()`) gain a `lora_alpha` argument to scale the adaptation delta independently of the chosen rank. -- `keras_variable()` now accepts a `synchronization` argument for distributed - strategies. +- Added complex-valued helpers: S3 `Arg()` methods for tensors, `op_angle()`, + and conversions `op_view_as_real()` / `op_view_as_complex()`. -- `Layer$add_weight()` gains an `overwrite_with_gradient` option and - layers now provide a `symbolic_call()` method. +- Added the Muon optimizer via `optimizer_muon()`. + +- Added elastic deformation utilities for images: `layer_random_elastic_transform()` + and the lower-level `op_image_elastic_transform()`. - Transposed convolution utilities now follow the latest Keras API: `op_conv_transpose()` defaults `strides = 1` and the `layer_conv_*_transpose()` layers expose `output_padding` for precise shape control. -- `layer_torch_module_wrapper()` gains an `output_shape` argument to help Keras - infer shapes when wrapping PyTorch modules. +- `register_keras_serializable()` now returns a registered Python callable, + making it easier to use with bare R functions. - `save_model_weights()` adds a `max_shard_size` argument to split large weight files into manageable shards. -- Added elastic deformation utilities for images: `layer_random_elastic_transform()` - and the lower-level `op_image_elastic_transform()`. +- `keras_variable()` now accepts a `synchronization` argument for distributed + strategies. -- Added `loss_categorical_generalized_cross_entropy()` for training with noisy - labels. +- `layer_layer_normalization()` removes the `rms_scaling` argument. -- Added the Muon optimizer via `optimizer_muon()`. +- `layer_reshape()` can now accept `-1` as a sentinel for an automatically calculated axis size. -- Added complex-valued helpers: S3 `Arg()` methods for tensors, `op_angle()`, - and conversions `op_view_as_real()` / `op_view_as_complex()`. +- `layer_torch_module_wrapper()` gains an `output_shape` argument to help Keras + infer shapes when wrapping PyTorch modules. -- Added signal window operations: `op_bartlett()`, `op_blackman()`, - `op_hamming()`, `op_hanning()`, and `op_kaiser()`. +- `Layer$add_weight()` gains an `overwrite_with_gradient` option and + layers now provide a `symbolic_call()` method. -- Expanded numeric operations with `op_layer_normalization()`, `op_cbrt()`, - `op_corrcoef()`, `op_deg2rad()`, `op_heaviside()`, the new `op_sparse_sigmoid()` - plus matching `activation_sparse_sigmoid()`, and an `attn_logits_soft_cap` - argument for `op_dot_product_attention()`. +- Added `str()` S3 method for Keras Variables. -- `layer_layer_normalization()` removes the `rms_scaling` argument. +- Added S3 methods for JAX array: + `str()`, `as.array()`, `as.double()`, `as.integer()`, `as.numeric()`. + +- Added base-array compatibility methods for backend tensors: `t()`, + `aperm()`, and `all.equal()`. + +- Added `pillar::type_sum()` for JAX variables and `JaxVariable`; + extended `str()` coverage to the new JAX variable class. + +- `config_max_epochs()`, `config_set_max_epochs()`, `config_max_steps_per_epoch()`, + and `config_set_max_steps_per_epoch()`. The caps can also be set via the + `KERAS_MAX_EPOCHS` and `KERAS_MAX_STEPS_PER_EPOCH` environment variables. + Added `config_is_nnx_enabled()` to check whether JAX NNX features are enabled. + +- Built-in dataset loaders now accept `convert = FALSE` to return NumPy arrays + instead of R arrays. + +- Updated `plot(history, theme_bw = TRUE)` for `ggplot2` 3.4.0 + compatibility. + +- `plot(model)` DPI is now globally configurable via + `options(keras.plot.model.dpi = )`, (defaults to `200`). + +- Reexported reticulate functions: `py_help()`, `py_to_r()`, `r_to_py()`, + `py_require()`, and `import()`. + +- Support `super()$initialize()` in subclassed Keras classes; improved + `super()` behavior in subclasses. + +- Updated dependencies declared by `use_backend("jax", gpu=TRUE)` + for compatability with `keras-hub`. + +- Exported `named_list()` utility. + +- Fixed an issue when switching backends twice in a row. # keras3 1.4.0 diff --git a/R/install.R b/R/install.R index c8383c73b..8afe49e53 100644 --- a/R/install.R +++ b/R/install.R @@ -388,7 +388,7 @@ uv_unset_override_never_tensorflow <- function() { if (is.na(override)) return() cpu_override <- pkg_file("never-tensorflow-override.txt") if (override == cpu_override) { - Sys.unsetenv(override) + Sys.unsetenv("UV_OVERRIDE") } else { new <- gsub(cpu_override, "", override, fixed = TRUE) new <- gsub(" +", " ", new) diff --git a/R/model-persistence.R b/R/model-persistence.R index fc4a7a25a..936c47f39 100644 --- a/R/model-persistence.R +++ b/R/model-persistence.R @@ -585,8 +585,8 @@ function (object, filepath, call_endpoint = "serve", call_training_endpoint = NU #' @param object #' A keras object. #' -#' @returns `object` is returned invisibly, for convenient piping. This is -#' primarily called for side effects. +#' @returns The registered `object` (and converted) is returned. This returned object is what you +#' should must use when building and serializing the model. #' @export #' @family saving and loading functions #' @family serialization utilities @@ -605,7 +605,7 @@ function (object, name = NULL, package = NULL) c("", "base", "R_GlobalEnv"), "Custom") keras$saving$register_keras_serializable(package, name)(py_object) - invisible(object) + py_object } diff --git a/R/package.R b/R/package.R index 45bdaa00a..fe532498b 100644 --- a/R/package.R +++ b/R/package.R @@ -186,7 +186,12 @@ keras <- NULL keras <- import("keras") convert_to_tensor <- import("keras.ops", convert = FALSE)$convert_to_tensor with(keras$device("cpu:0"), { - backend_tensor_class <- class(convert_to_tensor(array(1L)))[1L] + all_backend_tensor_s3_classes <- class(convert_to_tensor(array(1L))) + backend_tensor_class <- all_backend_tensor_s3_classes[1L] + if ("jax.Array" %in% all_backend_tensor_s3_classes) + backend_tensor_class <- "jax.Array" + # message("setting methods on backend_tensor_class: ", backend_tensor_class, + # "\nother options: ", paste0(all_backend_tensor_s3_classes, collapse = " ")) }) symbolic_tensor_class <- nameOfClass__python.builtin.type(keras$KerasTensor) @@ -207,6 +212,9 @@ keras <- NULL registerS3method("as.array", backend_tensor_class, op_convert_to_array, baseenv()) registerS3method("^", backend_tensor_class, `^__keras.backend.tensor`, baseenv()) registerS3method("%*%", backend_tensor_class, op_matmul, baseenv()) + registerS3method("t", backend_tensor_class, op_transpose, baseenv()) + registerS3method("aperm", backend_tensor_class, op_transpose, baseenv()) + registerS3method("all.equal", backend_tensor_class, all.equal.numpy.ndarray, baseenv()) if(keras$config$backend() == "jax") { for(py_type in import("jax")$Array$`__subclasses__`()) { @@ -271,6 +279,13 @@ keras <- NULL } +## should this live in reticulate?? probably... +#' @export +all.equal.numpy.ndarray <- function(target, current, ...) { + # or use numpy.allequal? + all.equal(as.array(target), as.array(current), ...) +} + at.keras_backend_tensor <- function(object, name) { out <- rlang::env_clone(object) diff --git a/R/py-classes.R b/R/py-classes.R index 27680f036..185120d66 100644 --- a/R/py-classes.R +++ b/R/py-classes.R @@ -119,12 +119,16 @@ function(classname, type = `__class__`, object_or_type = base::get("self", envir = base::parent.frame())) { - convert <- base::get("convert", envir = base::as.environment(object_or_type)) - py_builtins <- reticulate::import_builtins(convert) - reticulate::py_call(py_builtins$super, type, object_or_type) - } + convert <- base::get("convert", object_or_type) + py_super <- reticulate::py_eval( + "__import__('builtins').super", + convert = convert + ) + py_super(type, object_or_type) + } class(super) <- "python_builtin_super_getter" - })) + }) +) py_class @@ -137,15 +141,23 @@ function(classname, #' @export `$.python_builtin_super_getter` <- function(x, name) { super <- do.call(x, list(), envir = parent.frame()) # call super() + `[[.python.builtin.super`(super, name) +} + +#' @export +`[[.python_builtin_super_getter` <- `$.python_builtin_super_getter` + +#' @export +`$.python.builtin.super` <- function(x, name) { name <- switch(name, initialize = "__init__", finalize = "__del__", name) - out <- py_get_attr(super, name) + out <- py_get_attr(x, name) convert <- get0("convert", as.environment(out), inherits = FALSE, ifnotfound = TRUE) if (convert) py_to_r(out) else out } #' @export -`[[.python_builtin_super_getter` <- `$.python_builtin_super_getter` +`[[.python.builtin.super` <- `$.python.builtin.super` # No .DollarNames.python_builtin_super_getter because the python.builtin.super # object doesn't have populated attributes itself, only a dynamic `__getattr__` diff --git a/R/r-utils.R b/R/r-utils.R index 0c1d3ab84..993c5be36 100644 --- a/R/r-utils.R +++ b/R/r-utils.R @@ -40,15 +40,34 @@ drop_nulls <- function(x, i = NULL) { x[!drop] } +#' Create a named list from arguments +#' +#' Constructs a list from the provided arguments where all elements are named. +#' This wraps [rlang::dots_list()] but changes two defaults: +#' - `.named` is set to `TRUE` +#' - `.homonyms` is set to `"error"` +#' +#' Other parameters retain their defaults from [rlang::dots_list()]: +#' - `.ignore_empty = "trailing"` +#' - `.preserve_empty = FALSE` +#' - `.check_assign = FALSE` +#' +#' @inheritParams rlang::dots_list +#' +#' @inheritParams dots_list +#' +#' @return A named list. +#' +#' @seealso [rlang::dots_list()] +#' +#' @export #' @importFrom rlang dots_list -# identical to rlang::list2(), except .named = TRUE named_list <- function(...) dots_list(..., - .named = TRUE, - # not the default + .named = TRUE, # not default .ignore_empty = "trailing", .preserve_empty = FALSE, - .homonyms = "error", + .homonyms = "error", # not default .check_assign = FALSE) `append1<-` <- function(x, value) { diff --git a/R/reexports.R b/R/reexports.R index f77dc1255..c04b2c115 100644 --- a/R/reexports.R +++ b/R/reexports.R @@ -77,6 +77,9 @@ reticulate::py_to_r #' @export reticulate::r_to_py +#' @export +reticulate::py_help + #' @importFrom tensorflow tensorboard #' @export tensorflow::tensorboard diff --git a/R/utils.R b/R/utils.R index e1aaef25d..9fcb0f526 100644 --- a/R/utils.R +++ b/R/utils.R @@ -369,6 +369,8 @@ to_categorical <- function (x, num_classes = NULL) { if (inherits(x, "factor")) { + # if (length(DIM(x)) == 1) + # return(diag(nrow = num_classes %||% length(levels(x)))[as.integer(x), ]) x <- array(as.integer(x) - 1L, dim = dim(x) %||% length(x)) if (is.null(num_classes)) num_classes <- length(levels(x)) @@ -631,7 +633,7 @@ function(x, ..., rankdir = "TB", expand_nested = FALSE, - dpi = 200, + dpi = getOption("keras.plot.model.dpi", 200L), layer_range = NULL, show_layer_activations = FALSE, show_trainable = NA, diff --git a/man/deserialize_keras_object.Rd b/man/deserialize_keras_object.Rd index 8c4fddfcb..0335259d3 100644 --- a/man/deserialize_keras_object.Rd +++ b/man/deserialize_keras_object.Rd @@ -95,8 +95,18 @@ loss_modified_mse <- Loss( # register the custom object register_keras_serializable(loss_modified_mse) +}\if{html}{\out{}} + +\if{html}{\out{
}}\preformatted{## .ModifiedMeanSquaredError'> +## signature: ( +## reduction='sum_over_batch_size', +## name='mean_squared_error', +## dtype=None +## ) + +}\if{html}{\out{
}} -# confirm object is registered +\if{html}{\out{
}}\preformatted{# confirm object is registered get_custom_objects() }\if{html}{\out{
}} diff --git a/man/layer_tfsm.Rd b/man/layer_tfsm.Rd index e11c0dadb..0eee57166 100644 --- a/man/layer_tfsm.Rd +++ b/man/layer_tfsm.Rd @@ -59,8 +59,8 @@ model |> export_savedmodel("path/to/artifact") ## Output Type: ## TensorSpec(shape=(None, 10), dtype=tf.float32, name=None) ## Captures: -## 131730679462800: TensorSpec(shape=(), dtype=tf.resource, name=None) -## 131730679456848: TensorSpec(shape=(), dtype=tf.resource, name=None) +## 129280799091280: TensorSpec(shape=(), dtype=tf.resource, name=None) +## 129280799082064: TensorSpec(shape=(), dtype=tf.resource, name=None) }\if{html}{\out{}} diff --git a/man/named_list.Rd b/man/named_list.Rd new file mode 100644 index 000000000..541d1cfb8 --- /dev/null +++ b/man/named_list.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/r-utils.R +\name{named_list} +\alias{named_list} +\title{Create a named list from arguments} +\usage{ +named_list(...) +} +\arguments{ +\item{...}{Arguments to collect in a list. These dots are +\link[rlang:dyn-dots]{dynamic}.} +} +\value{ +A named list. +} +\description{ +Constructs a list from the provided arguments where all elements are named. +This wraps \code{\link[rlang:list2]{rlang::dots_list()}} but changes two defaults: +\itemize{ +\item \code{.named} is set to \code{TRUE} +\item \code{.homonyms} is set to \code{"error"} +} +} +\details{ +Other parameters retain their defaults from \code{\link[rlang:list2]{rlang::dots_list()}}: \cr +\itemize{ +\item \code{.ignore_empty = "trailing"} +\item \code{.preserve_empty = FALSE} +\item \code{.check_assign = FALSE} +} +} +\seealso{ +\code{\link[rlang:list2]{rlang::dots_list()}} +} diff --git a/man/op_irfft.Rd b/man/op_irfft.Rd index ba41b0243..44ea2dbc3 100644 --- a/man/op_irfft.Rd +++ b/man/op_irfft.Rd @@ -48,7 +48,7 @@ op_irfft(c(real, imag)) \if{html}{\out{
}}\preformatted{all.equal(op_irfft(op_rfft(real, 5), 5), real) }\if{html}{\out{
}} -\if{html}{\out{
}}\preformatted{#> [1] TRUE +\if{html}{\out{
}}\preformatted{#> [1] "Mean relative difference: 5.960465e-08" }\if{html}{\out{
}} } diff --git a/man/plot.keras.src.models.model.Model.Rd b/man/plot.keras.src.models.model.Model.Rd index 830ec7893..67f0f18f2 100644 --- a/man/plot.keras.src.models.model.Model.Rd +++ b/man/plot.keras.src.models.model.Model.Rd @@ -12,7 +12,7 @@ ..., rankdir = "TB", expand_nested = FALSE, - dpi = 200, + dpi = getOption("keras.plot.model.dpi", 200L), layer_range = NULL, show_layer_activations = FALSE, show_trainable = NA, diff --git a/man/reexports.Rd b/man/reexports.Rd index 8fa6d08bc..b26d7b602 100644 --- a/man/reexports.Rd +++ b/man/reexports.Rd @@ -17,6 +17,7 @@ \alias{import} \alias{py_to_r} \alias{r_to_py} +\alias{py_help} \alias{tensorboard} \alias{export_savedmodel} \alias{as_tensor} @@ -45,7 +46,7 @@ below to see their documentation. \item{magrittr}{\code{\link[magrittr:compound]{\%<>\%}}} - \item{reticulate}{\code{\link[reticulate:with-as-operator]{\%as\%}}, \code{\link[reticulate]{array_reshape}}, \code{\link[reticulate:iterate]{as_iterator}}, \code{\link[reticulate]{import}}, \code{\link[reticulate:iterate]{iter_next}}, \code{\link[reticulate]{iterate}}, \code{\link[reticulate]{np_array}}, \code{\link[reticulate]{py_require}}, \code{\link[reticulate:r-py-conversion]{py_to_r}}, \code{\link[reticulate:r-py-conversion]{r_to_py}}, \code{\link[reticulate]{tuple}}, \code{\link[reticulate]{use_python}}, \code{\link[reticulate:use_python]{use_virtualenv}}} + \item{reticulate}{\code{\link[reticulate:with-as-operator]{\%as\%}}, \code{\link[reticulate]{array_reshape}}, \code{\link[reticulate:iterate]{as_iterator}}, \code{\link[reticulate]{import}}, \code{\link[reticulate:iterate]{iter_next}}, \code{\link[reticulate]{iterate}}, \code{\link[reticulate]{np_array}}, \code{\link[reticulate]{py_help}}, \code{\link[reticulate]{py_require}}, \code{\link[reticulate:r-py-conversion]{py_to_r}}, \code{\link[reticulate:r-py-conversion]{r_to_py}}, \code{\link[reticulate]{tuple}}, \code{\link[reticulate]{use_python}}, \code{\link[reticulate:use_python]{use_virtualenv}}} \item{tensorflow}{\code{\link[tensorflow]{all_dims}}, \code{\link[tensorflow]{as_tensor}}, \code{\link[tensorflow]{evaluate}}, \code{\link[tensorflow]{export_savedmodel}}, \code{\link[tensorflow]{tensorboard}}} diff --git a/man/register_keras_serializable.Rd b/man/register_keras_serializable.Rd index fcbc45e0a..d66dca278 100644 --- a/man/register_keras_serializable.Rd +++ b/man/register_keras_serializable.Rd @@ -16,8 +16,8 @@ register_keras_serializable(object, name = NULL, package = NULL) Defaults to the current package name, or \code{"Custom"} outside of a package.} } \value{ -\code{object} is returned invisibly, for convenient piping. This is -primarily called for side effects. +The registered \code{object} (and converted) is returned. This returned object is what you +should must use when building and serializing the model. } \description{ This function registers a custom class or function with the Keras custom @@ -36,8 +36,14 @@ defaults to the object name if not passed. # the `name` argument is not provided, `'MyDense'` is used as the `name`. layer_my_dense <- Layer("MyDense") register_keras_serializable(layer_my_dense, package = "my_package") +}\if{html}{\out{
}} + +\if{html}{\out{
}}\preformatted{## .MyDense'> +## signature: (*args, **kwargs) + +}\if{html}{\out{
}} -MyDense <- environment(layer_my_dense)$`__class__` # the python class obj +\if{html}{\out{
}}\preformatted{MyDense <- environment(layer_my_dense)$`__class__` # the python class obj stopifnot(exprs = \{ get_registered_object('my_package>MyDense') == MyDense get_registered_name(MyDense) == 'my_package>MyDense'