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{