Nothing
# Tests for Sequential Neural Network API (v0.5.3)
# Helper: free all backends associated with a compiled model
cleanup_model <- function(model) {
ggml_backend_sched_free(model$compilation$sched)
ggml_backend_free(model$compilation$backend)
if (!is.null(model$compilation$cpu_backend)) {
ggml_backend_free(model$compilation$cpu_backend)
}
}
# ============================================================================
# Model creation
# ============================================================================
test_that("ggml_model_sequential creates empty model", {
model <- ggml_model_sequential()
expect_s3_class(model, "ggml_sequential_model")
expect_equal(length(model$layers), 0)
expect_null(model$input_shape)
expect_false(model$compiled)
})
# ============================================================================
# Layer addition
# ============================================================================
test_that("ggml_layer_conv_2d adds layer correctly", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), activation = "relu",
input_shape = c(28, 28, 1))
expect_equal(length(model$layers), 1)
expect_equal(model$layers[[1]]$type, "conv_2d")
expect_equal(model$layers[[1]]$config$filters, 32L)
expect_equal(model$layers[[1]]$config$kernel_size, c(3L, 3L))
expect_equal(model$layers[[1]]$config$activation, "relu")
expect_equal(model$input_shape, c(28L, 28L, 1L))
})
test_that("ggml_layer_conv_2d handles scalar kernel_size", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(16, 5, input_shape = c(28, 28, 1))
expect_equal(model$layers[[1]]$config$kernel_size, c(5L, 5L))
})
test_that("ggml_layer_conv_2d default padding and strides", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), input_shape = c(28, 28, 1))
expect_equal(model$layers[[1]]$config$strides, c(1L, 1L))
expect_equal(model$layers[[1]]$config$padding, "valid")
expect_null(model$layers[[1]]$config$activation)
})
test_that("ggml_layer_max_pooling_2d adds layer correctly", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), input_shape = c(28, 28, 1)) |>
ggml_layer_max_pooling_2d(c(2, 2))
expect_equal(length(model$layers), 2)
expect_equal(model$layers[[2]]$type, "max_pooling_2d")
expect_equal(model$layers[[2]]$config$pool_size, c(2L, 2L))
# strides default to pool_size
expect_equal(model$layers[[2]]$config$strides, c(2L, 2L))
})
test_that("ggml_layer_flatten adds layer correctly", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), input_shape = c(28, 28, 1)) |>
ggml_layer_flatten()
expect_equal(length(model$layers), 2)
expect_equal(model$layers[[2]]$type, "flatten")
})
test_that("ggml_layer_dense adds layer correctly", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), input_shape = c(28, 28, 1)) |>
ggml_layer_flatten() |>
ggml_layer_dense(128, activation = "relu")
expect_equal(length(model$layers), 3)
expect_equal(model$layers[[3]]$type, "dense")
expect_equal(model$layers[[3]]$config$units, 128L)
expect_equal(model$layers[[3]]$config$activation, "relu")
})
# ============================================================================
# Shape inference
# ============================================================================
test_that("nn_infer_shapes computes correct output shapes for CNN", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), activation = "relu",
input_shape = c(28, 28, 1)) |>
ggml_layer_max_pooling_2d(c(2, 2)) |>
ggml_layer_flatten() |>
ggml_layer_dense(128, activation = "relu") |>
ggml_layer_dense(10, activation = "softmax")
model <- ggmlR:::nn_infer_shapes(model)
# conv_2d: (28-3)/1 + 1 = 26, output (26, 26, 32)
expect_equal(model$layers[[1]]$output_shape, c(26L, 26L, 32L))
# max_pool: (26-2)/2 + 1 = 13, output (13, 13, 32)
expect_equal(model$layers[[2]]$output_shape, c(13L, 13L, 32L))
# flatten: 13*13*32 = 5408
expect_equal(model$layers[[3]]$output_shape, 5408L)
# dense 128
expect_equal(model$layers[[4]]$output_shape, 128L)
# dense 10
expect_equal(model$layers[[5]]$output_shape, 10L)
})
test_that("nn_infer_shapes works with same padding", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(16, c(3, 3), padding = "same",
input_shape = c(28, 28, 1))
model <- ggmlR:::nn_infer_shapes(model)
# same padding: output H,W = input H,W (with stride 1)
expect_equal(model$layers[[1]]$output_shape, c(28L, 28L, 16L))
})
test_that("nn_infer_shapes fails without input_shape", {
model <- ggml_model_sequential() |>
ggml_layer_dense(10)
expect_error(ggmlR:::nn_infer_shapes(model), "input_shape")
})
# ============================================================================
# Compile
# ============================================================================
test_that("ggml_compile works and sets compiled flag", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), activation = "relu",
input_shape = c(28, 28, 1)) |>
ggml_layer_max_pooling_2d(c(2, 2)) |>
ggml_layer_flatten() |>
ggml_layer_dense(10, activation = "softmax")
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy",
metrics = c("accuracy"))
expect_true(model$compiled)
expect_equal(model$compilation$optimizer, "adam")
expect_equal(model$compilation$loss, "categorical_crossentropy")
expect_equal(model$compilation$metrics, "accuracy")
expect_true(!is.null(model$compilation$backend))
expect_true(!is.null(model$compilation$sched))
# Shapes should be inferred
expect_equal(model$layers[[1]]$output_shape, c(26L, 26L, 32L))
# Cleanup
cleanup_model(model)
})
test_that("ggml_compile fails on empty model", {
model <- ggml_model_sequential()
expect_error(ggml_compile(model), "no layers")
})
test_that("ggml_compile with sgd optimizer", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(8, c(3, 3), input_shape = c(10, 10, 1)) |>
ggml_layer_flatten() |>
ggml_layer_dense(5)
model <- ggml_compile(model, optimizer = "sgd", loss = "mse")
expect_true(model$compiled)
expect_equal(model$compilation$optimizer, "sgd")
expect_equal(model$compilation$loss, "mse")
cleanup_model(model)
})
# ============================================================================
# Print
# ============================================================================
test_that("print.ggml_sequential_model works", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), activation = "relu",
input_shape = c(28, 28, 1)) |>
ggml_layer_max_pooling_2d(c(2, 2)) |>
ggml_layer_flatten() |>
ggml_layer_dense(128, activation = "relu") |>
ggml_layer_dense(10, activation = "softmax")
output <- capture.output(print(model))
expect_true(any(grepl("Sequential Model", output)))
expect_true(any(grepl("conv_2d", output)))
expect_true(any(grepl("max_pooling_2d", output)))
expect_true(any(grepl("flatten", output)))
expect_true(any(grepl("dense", output)))
expect_true(any(grepl("Total parameters", output)))
})
test_that("print.ggml_sequential_model works on empty model", {
model <- ggml_model_sequential()
output <- capture.output(print(model))
expect_true(any(grepl("no layers", output)))
})
# ============================================================================
# Pipe chaining
# ============================================================================
test_that("pipe chaining builds full model", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), activation = "relu",
input_shape = c(28, 28, 1)) |>
ggml_layer_conv_2d(64, c(3, 3), activation = "relu") |>
ggml_layer_max_pooling_2d(c(2, 2)) |>
ggml_layer_flatten() |>
ggml_layer_dense(128, activation = "relu") |>
ggml_layer_dense(10, activation = "softmax")
expect_equal(length(model$layers), 6)
expect_equal(model$layers[[1]]$type, "conv_2d")
expect_equal(model$layers[[2]]$type, "conv_2d")
expect_equal(model$layers[[3]]$type, "max_pooling_2d")
expect_equal(model$layers[[4]]$type, "flatten")
expect_equal(model$layers[[5]]$type, "dense")
expect_equal(model$layers[[6]]$type, "dense")
})
# ============================================================================
# ggml_set_input / ggml_set_output
# ============================================================================
test_that("ggml_set_input and ggml_set_output work on tensors", {
ctx <- ggml_init(1024 * 1024)
t <- ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 10L)
# Should not error
expect_no_error(ggml_set_input(t))
expect_no_error(ggml_set_output(t))
ggml_free(ctx)
})
# ============================================================================
# Training (small synthetic data)
# ============================================================================
test_that("ggml_fit trains a small dense model", {
# Simple XOR-like problem: 4 samples, 2 features, 2 classes
n <- 128 # must be divisible by batch_size
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) {
cls <- if (sum(x[i, ]) > 2) 1L else 2L
y[i, cls] <- 1
}
model <- ggml_model_sequential() |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
# Set input_shape manually (dense-only model)
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
# Train 1 epoch
expect_no_error({
model <- ggml_fit(model, x, y, epochs = 1,
batch_size = 32, verbose = 0)
})
# Cleanup
cleanup_model(model)
})
test_that("ggml_evaluate returns accuracy consistent with training", {
set.seed(42)
n <- 256
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) {
cls <- if (sum(x[i, ]) > 2) 1L else 2L
y[i, cls] <- 1
}
model <- ggml_model_sequential() |>
ggml_layer_dense(16, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
model <- ggml_fit(model, x, y, epochs = 20,
batch_size = 32, verbose = 0)
# Evaluate on same data — should retain trained weights
result <- ggml_evaluate(model, x, y, batch_size = 32)
expect_true(!is.null(result$loss))
expect_true(!is.null(result$accuracy))
# Trained model should do better than random (50%)
expect_gt(result$accuracy, 0.55)
# Loss should be below random baseline (-ln(0.5) ≈ 0.693)
expect_lt(result$loss, 0.69)
# Cleanup
cleanup_model(model)
})
# ============================================================================
# Predict
# ============================================================================
test_that("ggml_predict returns correct shape and consistent with evaluate", {
set.seed(42)
n <- 256
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) {
cls <- if (sum(x[i, ]) > 2) 1L else 2L
y[i, cls] <- 1
}
model <- ggml_model_sequential() |>
ggml_layer_dense(16, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
model <- ggml_fit(model, x, y, epochs = 20,
batch_size = 32, verbose = 0)
# Predict
preds <- ggml_predict(model, x, batch_size = 32)
# Check shape: [N, 2]
expect_equal(nrow(preds), n)
expect_equal(ncol(preds), 2)
# Predictions should be probabilities (softmax output)
expect_true(all(preds >= 0))
expect_true(all(preds <= 1))
# Each row should sum to ~1 (softmax)
row_sums <- rowSums(preds)
expect_true(all(abs(row_sums - 1) < 0.01))
# Argmax predictions should be consistent with evaluate accuracy
pred_classes <- apply(preds, 1, which.max)
true_classes <- apply(y, 1, which.max)
predict_accuracy <- mean(pred_classes == true_classes)
eval_result <- ggml_evaluate(model, x, y, batch_size = 32)
# Predict and evaluate accuracy should be close
expect_equal(predict_accuracy, eval_result$accuracy, tolerance = 0.05)
# Cleanup
cleanup_model(model)
})
# ============================================================================
# Save / Load Weights
# ============================================================================
test_that("ggml_save_weights and ggml_load_weights roundtrip", {
set.seed(42)
n <- 256
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) {
cls <- if (sum(x[i, ]) > 2) 1L else 2L
y[i, cls] <- 1
}
model <- ggml_model_sequential() |>
ggml_layer_dense(16, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
model <- ggml_fit(model, x, y, epochs = 20,
batch_size = 32, verbose = 0)
# Evaluate original model
result_orig <- ggml_evaluate(model, x, y, batch_size = 32)
# Save weights
tmp_path <- tempfile(fileext = ".rds")
ggml_save_weights(model, tmp_path)
expect_true(file.exists(tmp_path))
# Cleanup original model backend resources
cleanup_model(model)
# Create a fresh model with same architecture and load weights
model2 <- ggml_model_sequential() |>
ggml_layer_dense(16, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model2$input_shape <- 4L
model2 <- ggml_compile(model2, optimizer = "adam",
loss = "categorical_crossentropy")
model2 <- ggml_load_weights(model2, tmp_path)
# Evaluate loaded model
result_loaded <- ggml_evaluate(model2, x, y, batch_size = 32)
# Accuracy should match
expect_equal(result_loaded$accuracy, result_orig$accuracy, tolerance = 0.01)
expect_equal(result_loaded$loss, result_orig$loss, tolerance = 0.01)
# Cleanup
cleanup_model(model2)
unlink(tmp_path)
})
test_that("ggml_load_weights rejects mismatched architecture", {
model1 <- ggml_model_sequential() |>
ggml_layer_dense(16, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model1$input_shape <- 4L
model1 <- ggml_compile(model1, optimizer = "adam",
loss = "categorical_crossentropy")
# Train minimally
x <- matrix(runif(64 * 4), nrow = 64, ncol = 4)
y <- matrix(0, nrow = 64, ncol = 2)
for (i in seq_len(64)) { y[i, if (sum(x[i, ]) > 2) 1L else 2L] <- 1 }
model1 <- ggml_fit(model1, x, y, epochs = 1, batch_size = 32, verbose = 0)
tmp_path <- tempfile(fileext = ".rds")
ggml_save_weights(model1, tmp_path)
# Different architecture
model2 <- ggml_model_sequential() |>
ggml_layer_dense(32, activation = "relu") |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model2$input_shape <- 4L
model2 <- ggml_compile(model2, optimizer = "adam",
loss = "categorical_crossentropy")
expect_error(ggml_load_weights(model2, tmp_path), "mismatch")
# Cleanup
cleanup_model(model1)
cleanup_model(model2)
unlink(tmp_path)
})
# ============================================================================
# Error handling
# ============================================================================
# ============================================================================
# conv_1d layer
# ============================================================================
test_that("ggml_layer_conv_1d adds layer correctly", {
model <- ggml_model_sequential() |>
ggml_layer_conv_1d(32, 3, activation = "relu",
input_shape = c(100, 1))
expect_equal(length(model$layers), 1)
expect_equal(model$layers[[1]]$type, "conv_1d")
expect_equal(model$layers[[1]]$config$filters, 32L)
expect_equal(model$layers[[1]]$config$kernel_size, 3L)
expect_equal(model$input_shape, c(100L, 1L))
})
test_that("nn_infer_shapes works for conv_1d", {
model <- ggml_model_sequential() |>
ggml_layer_conv_1d(16, 5, input_shape = c(100, 1)) |>
ggml_layer_flatten() |>
ggml_layer_dense(10)
model <- ggmlR:::nn_infer_shapes(model)
# conv_1d: (100 - 5) / 1 + 1 = 96, output (96, 16)
expect_equal(model$layers[[1]]$output_shape, c(96L, 16L))
# flatten: 96 * 16 = 1536
expect_equal(model$layers[[2]]$output_shape, 1536L)
# dense: 10
expect_equal(model$layers[[3]]$output_shape, 10L)
})
test_that("nn_infer_shapes works for conv_1d with same padding", {
model <- ggml_model_sequential() |>
ggml_layer_conv_1d(16, 5, padding = "same", input_shape = c(100, 1))
model <- ggmlR:::nn_infer_shapes(model)
# same padding: L_out = L (with stride 1)
expect_equal(model$layers[[1]]$output_shape, c(100L, 16L))
})
# ============================================================================
# batch_norm layer
# ============================================================================
test_that("ggml_layer_batch_norm adds layer correctly", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64, activation = "relu") |>
ggml_layer_batch_norm()
model$input_shape <- 10L
expect_equal(length(model$layers), 2)
expect_equal(model$layers[[2]]$type, "batch_norm")
})
test_that("nn_infer_shapes works for batch_norm", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64) |>
ggml_layer_batch_norm() |>
ggml_layer_dense(10)
model$input_shape <- 10L
model <- ggmlR:::nn_infer_shapes(model)
# batch_norm preserves shape
expect_equal(model$layers[[2]]$output_shape, 64L)
expect_equal(model$layers[[3]]$output_shape, 10L)
})
# ============================================================================
# predict_classes
# ============================================================================
test_that("ggml_predict_classes returns correct 1-based indices", {
set.seed(42)
n <- 128
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) {
cls <- if (sum(x[i, ]) > 2) 1L else 2L
y[i, cls] <- 1
}
model <- ggml_model_sequential() |>
ggml_layer_dense(16, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
model <- ggml_fit(model, x, y, epochs = 10,
batch_size = 32, verbose = 0)
classes <- ggml_predict_classes(model, x, batch_size = 32)
expect_true(is.integer(classes))
expect_equal(length(classes), n)
expect_true(all(classes %in% c(1L, 2L)))
# Cleanup
cleanup_model(model)
})
# ============================================================================
# summary
# ============================================================================
test_that("summary.ggml_sequential_model works", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3, 3), activation = "relu",
input_shape = c(28, 28, 1)) |>
ggml_layer_flatten() |>
ggml_layer_dense(10, activation = "softmax")
output <- capture.output(summary(model))
expect_true(any(grepl("Summary", output)))
expect_true(any(grepl("Input shape", output)))
expect_true(any(grepl("Trainable", output)))
expect_true(any(grepl("memory", output, ignore.case = TRUE)))
})
# ============================================================================
# history
# ============================================================================
test_that("ggml_fit returns model with history", {
set.seed(42)
n <- 128
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) {
cls <- if (sum(x[i, ]) > 2) 1L else 2L
y[i, cls] <- 1
}
model <- ggml_model_sequential() |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
model <- ggml_fit(model, x, y, epochs = 3,
batch_size = 32, verbose = 0)
expect_true(!is.null(model$history))
expect_s3_class(model$history, "ggml_history")
expect_equal(length(model$history$train_loss), 3)
expect_equal(length(model$history$train_accuracy), 3)
expect_true(all(is.numeric(model$history$train_loss)))
# Print should work
output <- capture.output(print(model$history))
expect_true(any(grepl("Training History", output)))
# Cleanup
cleanup_model(model)
})
# ============================================================================
# Error handling
# ============================================================================
test_that("ggml_fit fails on uncompiled model", {
model <- ggml_model_sequential() |>
ggml_layer_dense(10)
model$input_shape <- 5L
x <- matrix(0, nrow = 32, ncol = 5)
y <- matrix(0, nrow = 32, ncol = 10)
expect_error(ggml_fit(model, x, y), "compiled")
})
# ============================================================================
# validation_data
# ============================================================================
test_that("ggml_fit accepts validation_data", {
set.seed(42)
n <- 128
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
x_val <- matrix(runif(32 * 4), nrow = 32, ncol = 4)
y_val <- matrix(0, nrow = 32, ncol = 2)
for (i in seq_len(32)) { y_val[i, if (sum(x_val[i,]) > 2) 1L else 2L] <- 1 }
model <- ggml_model_sequential() |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
expect_no_error({
model <- ggml_fit(model, x, y, epochs = 2, batch_size = 32,
validation_data = list(x_val, y_val), verbose = 0)
})
# val metrics should be populated
expect_equal(length(model$history$val_loss), 2)
expect_equal(length(model$history$val_accuracy), 2)
cleanup_model(model)
})
test_that("ggml_fit validation_data rejects bad input", {
model <- ggml_model_sequential() |>
ggml_layer_dense(2)
model$input_shape <- 4L
model <- ggml_compile(model, loss = "categorical_crossentropy")
x <- matrix(runif(64 * 4), nrow = 64, ncol = 4)
y <- matrix(0, nrow = 64, ncol = 2)
for (i in seq_len(64)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
expect_error(
ggml_fit(model, x, y, epochs = 1, validation_data = x),
"list"
)
cleanup_model(model)
})
# ============================================================================
# class_weight / sample_weight in ggml_fit
# ============================================================================
test_that("ggml_fit accepts class_weight", {
set.seed(1)
n <- 128
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
model <- ggml_model_sequential() |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
expect_no_error({
model <- ggml_fit(model, x, y, epochs = 2, batch_size = 32,
class_weight = c("0" = 1, "1" = 2), verbose = 0)
})
cleanup_model(model)
})
test_that("ggml_fit accepts sample_weight", {
set.seed(1)
n <- 128
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
sw <- runif(n, 0.5, 1.5)
model <- ggml_model_sequential() |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
expect_no_error({
model <- ggml_fit(model, x, y, epochs = 2, batch_size = 32,
sample_weight = sw, verbose = 0)
})
cleanup_model(model)
})
test_that("ggml_fit rejects class_weight and sample_weight together", {
model <- ggml_model_sequential() |>
ggml_layer_dense(2)
model$input_shape <- 4L
model <- ggml_compile(model, loss = "categorical_crossentropy")
x <- matrix(runif(64 * 4), nrow = 64, ncol = 4)
y <- matrix(0, nrow = 64, ncol = 2)
for (i in seq_len(64)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
expect_error(
ggml_fit(model, x, y, epochs = 1,
class_weight = c("0" = 1, "1" = 2),
sample_weight = rep(1, 64)),
"not both"
)
cleanup_model(model)
})
test_that("ggml_fit rejects sample_weight with wrong length", {
model <- ggml_model_sequential() |>
ggml_layer_dense(2)
model$input_shape <- 4L
model <- ggml_compile(model, loss = "categorical_crossentropy")
x <- matrix(runif(64 * 4), nrow = 64, ncol = 4)
y <- matrix(0, nrow = 64, ncol = 2)
for (i in seq_len(64)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
expect_error(
ggml_fit(model, x, y, epochs = 1, sample_weight = rep(1, 10)),
"length"
)
cleanup_model(model)
})
# ============================================================================
# class_weight / sample_weight in ggml_evaluate
# ============================================================================
test_that("ggml_evaluate accepts sample_weight", {
set.seed(42)
n <- 128
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
model <- ggml_model_sequential() |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
model <- ggml_fit(model, x, y, epochs = 3, batch_size = 32, verbose = 0)
sw <- runif(n, 0.5, 1.5)
expect_no_error({
result <- ggml_evaluate(model, x, y, batch_size = 32, sample_weight = sw)
})
expect_true(!is.null(result$loss))
expect_true(!is.null(result$accuracy))
cleanup_model(model)
})
test_that("ggml_evaluate accepts class_weight", {
set.seed(42)
n <- 128
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
model <- ggml_model_sequential() |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
model <- ggml_fit(model, x, y, epochs = 3, batch_size = 32, verbose = 0)
expect_no_error({
result <- ggml_evaluate(model, x, y, batch_size = 32,
class_weight = c("0" = 1, "1" = 2))
})
expect_true(!is.null(result$loss))
cleanup_model(model)
})
test_that("ggml_evaluate rejects class_weight and sample_weight together", {
model <- ggml_model_sequential() |>
ggml_layer_dense(2)
model$input_shape <- 4L
model <- ggml_compile(model, loss = "categorical_crossentropy")
x <- matrix(runif(64 * 4), nrow = 64, ncol = 4)
y <- matrix(0, nrow = 64, ncol = 2)
for (i in seq_len(64)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
# Need weights to be applied after fit so just check error before
expect_error(
ggml_evaluate(model, x, y,
sample_weight = rep(1, 64),
class_weight = c("0" = 1, "1" = 2)),
"not both"
)
cleanup_model(model)
})
# ============================================================================
# Layer names
# ============================================================================
test_that("layers get auto-generated names", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64, activation = "relu") |>
ggml_layer_dense(10, activation = "softmax")
expect_equal(model$layers[[1]]$name, "dense_1")
expect_equal(model$layers[[2]]$name, "dense_2")
})
test_that("layers accept custom names", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64, activation = "relu", name = "hidden") |>
ggml_layer_dense(10, activation = "softmax", name = "output")
expect_equal(model$layers[[1]]$name, "hidden")
expect_equal(model$layers[[2]]$name, "output")
})
test_that("mixed layer types get independent counters", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(32, c(3,3), input_shape = c(28, 28, 1)) |>
ggml_layer_conv_2d(64, c(3,3)) |>
ggml_layer_flatten() |>
ggml_layer_dense(128) |>
ggml_layer_dense(10)
expect_equal(model$layers[[1]]$name, "conv_2d_1")
expect_equal(model$layers[[2]]$name, "conv_2d_2")
expect_equal(model$layers[[3]]$name, "flatten_1")
expect_equal(model$layers[[4]]$name, "dense_1")
expect_equal(model$layers[[5]]$name, "dense_2")
})
# ============================================================================
# ggml_get_layer
# ============================================================================
test_that("ggml_get_layer retrieves by index", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64, activation = "relu") |>
ggml_layer_dense(10, activation = "softmax")
layer <- ggml_get_layer(model, index = 1)
expect_equal(layer$type, "dense")
expect_equal(layer$config$units, 64L)
layer2 <- ggml_get_layer(model, index = 2)
expect_equal(layer2$config$units, 10L)
})
test_that("ggml_get_layer retrieves by name", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64, activation = "relu", name = "hidden") |>
ggml_layer_dense(10, activation = "softmax", name = "output")
layer <- ggml_get_layer(model, name = "hidden")
expect_equal(layer$config$units, 64L)
layer2 <- ggml_get_layer(model, name = "output")
expect_equal(layer2$config$units, 10L)
})
test_that("ggml_get_layer retrieves by auto-generated name", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64) |>
ggml_layer_dense(10)
layer <- ggml_get_layer(model, name = "dense_2")
expect_equal(layer$config$units, 10L)
})
test_that("ggml_get_layer errors on out-of-range index", {
model <- ggml_model_sequential() |>
ggml_layer_dense(10)
expect_error(ggml_get_layer(model, index = 0), "out of range")
expect_error(ggml_get_layer(model, index = 2), "out of range")
})
test_that("ggml_get_layer errors on unknown name", {
model <- ggml_model_sequential() |>
ggml_layer_dense(10)
expect_error(ggml_get_layer(model, name = "nonexistent"), "No layer")
})
test_that("ggml_get_layer errors when neither index nor name given", {
model <- ggml_model_sequential() |>
ggml_layer_dense(10)
expect_error(ggml_get_layer(model), "either index or name")
})
test_that("ggml_get_layer errors when both index and name given", {
model <- ggml_model_sequential() |>
ggml_layer_dense(10)
expect_error(ggml_get_layer(model, index = 1, name = "dense_1"), "not both")
})
# ============================================================================
# ggml_pop_layer
# ============================================================================
test_that("ggml_pop_layer removes last layer", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64, activation = "relu") |>
ggml_layer_dense(10, activation = "softmax")
model <- ggml_pop_layer(model)
expect_equal(length(model$layers), 1L)
expect_equal(model$layers[[1]]$config$units, 64L)
})
test_that("ggml_pop_layer clears input_shape when all layers removed", {
model <- ggml_model_sequential() |>
ggml_layer_dense(10, input_shape = 4L)
model <- ggml_pop_layer(model)
expect_equal(length(model$layers), 0L)
expect_null(model$input_shape)
})
test_that("ggml_pop_layer errors on empty model", {
model <- ggml_model_sequential()
expect_error(ggml_pop_layer(model), "no layers")
})
test_that("ggml_pop_layer errors on compiled model", {
model <- ggml_model_sequential() |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
model <- ggml_compile(model, loss = "categorical_crossentropy")
expect_error(ggml_pop_layer(model), "compiled")
cleanup_model(model)
})
# ============================================================================
# layer$trainable
# ============================================================================
test_that("layers default to trainable = TRUE", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64) |>
ggml_layer_dense(10)
expect_true(model$layers[[1]]$trainable)
expect_true(model$layers[[2]]$trainable)
})
test_that("trainable = FALSE can be set in constructor", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64, trainable = FALSE) |>
ggml_layer_dense(10)
expect_false(model$layers[[1]]$trainable)
expect_true(model$layers[[2]]$trainable)
})
# ============================================================================
# ggml_freeze_weights / ggml_unfreeze_weights
# ============================================================================
test_that("ggml_freeze_weights freezes all layers", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64) |>
ggml_layer_dense(32) |>
ggml_layer_dense(10)
model <- ggml_freeze_weights(model)
expect_false(model$layers[[1]]$trainable)
expect_false(model$layers[[2]]$trainable)
expect_false(model$layers[[3]]$trainable)
})
test_that("ggml_freeze_weights freezes a range of layers", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64) |>
ggml_layer_dense(32) |>
ggml_layer_dense(10)
model <- ggml_freeze_weights(model, from = 1, to = 2)
expect_false(model$layers[[1]]$trainable)
expect_false(model$layers[[2]]$trainable)
expect_true(model$layers[[3]]$trainable)
})
test_that("ggml_unfreeze_weights unfreezes all layers", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64) |>
ggml_layer_dense(10)
model <- ggml_freeze_weights(model)
model <- ggml_unfreeze_weights(model)
expect_true(model$layers[[1]]$trainable)
expect_true(model$layers[[2]]$trainable)
})
test_that("ggml_unfreeze_weights unfreezes a range", {
model <- ggml_model_sequential() |>
ggml_layer_dense(64) |>
ggml_layer_dense(32) |>
ggml_layer_dense(10)
model <- ggml_freeze_weights(model)
model <- ggml_unfreeze_weights(model, from = 3)
expect_false(model$layers[[1]]$trainable)
expect_false(model$layers[[2]]$trainable)
expect_true(model$layers[[3]]$trainable)
})
test_that("ggml_freeze_weights errors on invalid range", {
model <- ggml_model_sequential() |>
ggml_layer_dense(10)
expect_error(ggml_freeze_weights(model, from = 0), "Invalid range")
expect_error(ggml_freeze_weights(model, to = 2), "Invalid range")
expect_error(ggml_freeze_weights(model, from = 2, to = 1), "Invalid range")
})
test_that("frozen layers are not updated during training", {
set.seed(42)
n <- 128
x <- matrix(runif(n * 4), nrow = n, ncol = 4)
y <- matrix(0, nrow = n, ncol = 2)
for (i in seq_len(n)) { y[i, if (sum(x[i,]) > 2) 1L else 2L] <- 1 }
model <- ggml_model_sequential() |>
ggml_layer_dense(8, activation = "relu") |>
ggml_layer_dense(2, activation = "softmax")
model$input_shape <- 4L
# Freeze first layer, only train second
model <- ggml_freeze_weights(model, from = 1, to = 1)
model <- ggml_compile(model, optimizer = "adam",
loss = "categorical_crossentropy")
model <- ggml_fit(model, x, y, epochs = 5, batch_size = 32, verbose = 0)
# First layer weights should remain at initial values (zeros for bias)
bias1 <- ggml_backend_tensor_get_data(model$layers[[1]]$weights$bias)
expect_true(all(bias1 == 0))
cleanup_model(model)
})
# ============================================================================
# Block 4 — LSTM / GRU (Sequential)
# ============================================================================
test_that("ggml_layer_lstm adds lstm layer to sequential model", {
model <- ggml_model_sequential() |>
ggml_layer_lstm(16L, input_shape = c(5L, 4L)) |>
ggml_layer_dense(2L, activation = "softmax")
expect_equal(length(model$layers), 2L)
expect_equal(model$layers[[1]]$type, "lstm")
expect_equal(model$layers[[1]]$config$units, 16L)
})
test_that("ggml_layer_gru adds gru layer to sequential model", {
model <- ggml_model_sequential() |>
ggml_layer_gru(16L, input_shape = c(5L, 4L)) |>
ggml_layer_dense(2L, activation = "softmax")
expect_equal(length(model$layers), 2L)
expect_equal(model$layers[[1]]$type, "gru")
})
test_that("sequential LSTM model fits without error", {
n <- 32L
seq_len <- 5L
input_sz <- 4L
vals <- ((seq_len(n * seq_len * input_sz) - 1L) %% 20L - 10L) / 200.0
x <- array(vals, dim = c(n, seq_len, input_sz))
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
model <- ggml_model_sequential() |>
ggml_layer_lstm(8L, input_shape = c(seq_len, input_sz)) |>
ggml_layer_dense(2L, activation = "softmax")
model <- ggml_compile(model, loss = "categorical_crossentropy")
expect_no_error(model <- ggml_fit(model, x, y, epochs = 2L, batch_size = 16L, verbose = 0L))
expect_true(all(is.finite(model$history$train_loss)))
cleanup_model(model)
})
test_that("sequential GRU model fits without error", {
n <- 32L
seq_len <- 5L
input_sz <- 4L
vals <- ((seq_len(n * seq_len * input_sz) - 1L) %% 20L - 10L) / 200.0
x <- array(vals, dim = c(n, seq_len, input_sz))
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
model <- ggml_model_sequential() |>
ggml_layer_gru(8L, input_shape = c(seq_len, input_sz)) |>
ggml_layer_dense(2L, activation = "softmax")
model <- ggml_compile(model, loss = "categorical_crossentropy")
expect_no_error(model <- ggml_fit(model, x, y, epochs = 2L, batch_size = 16L, verbose = 0L))
expect_true(all(is.finite(model$history$train_loss)))
cleanup_model(model)
})
# ============================================================================
# Block 4 — GlobalMaxPooling2D (Sequential)
# ============================================================================
test_that("ggml_layer_global_max_pooling_2d adds layer to sequential model", {
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(8L, c(3L, 3L), activation = "relu",
input_shape = c(8L, 8L, 1L)) |>
ggml_layer_global_max_pooling_2d() |>
ggml_layer_dense(2L, activation = "softmax")
expect_equal(length(model$layers), 3L)
expect_equal(model$layers[[2]]$type, "global_max_pooling_2d")
})
test_that("sequential GlobalMaxPooling2D model fits without error", {
set.seed(73)
n <- 32L
x <- array(runif(n * 8L * 8L * 1L), dim = c(n, 8L, 8L, 1L))
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
model <- ggml_model_sequential() |>
ggml_layer_conv_2d(4L, c(3L, 3L), activation = "relu",
input_shape = c(8L, 8L, 1L)) |>
ggml_layer_global_max_pooling_2d() |>
ggml_layer_dense(2L, activation = "softmax")
model <- ggml_compile(model, loss = "categorical_crossentropy")
expect_no_error(model <- ggml_fit(model, x, y, epochs = 2L, batch_size = 16L, verbose = 0L))
expect_true(all(is.finite(model$history$train_loss)))
cleanup_model(model)
})
# ============================================================================
# Block 4 — Save / Load (Sequential)
# ============================================================================
test_that("ggml_save_model and ggml_load_model round-trip sequential dense model", {
set.seed(81)
n <- 64L
x <- matrix(runif(n * 4L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, if (sum(x[i, ]) > 2) 1L else 2L] <- 1.0
model <- ggml_model_sequential() |>
ggml_layer_dense(8L, activation = "relu", input_shape = 4L) |>
ggml_layer_dense(2L, activation = "softmax")
model <- ggml_compile(model, loss = "categorical_crossentropy")
model <- ggml_fit(model, x, y, epochs = 2L, batch_size = 32L, verbose = 0L)
tmp <- tempfile(fileext = ".rds")
on.exit({ unlink(tmp); cleanup_model(model) })
ggml_save_model(model, tmp)
model2 <- ggml_load_model(tmp)
on.exit({ unlink(tmp); cleanup_model(model); cleanup_model(model2) }, add = TRUE)
expect_s3_class(model2, "ggml_sequential_model")
expect_true(model2$compiled)
p1 <- ggml_predict(model, x, batch_size = 32L)
p2 <- ggml_predict(model2, x, batch_size = 32L)
expect_equal(p1, p2, tolerance = 1e-5)
})
test_that("ggml_load_model Sequential preserves layer configs", {
model <- ggml_model_sequential() |>
ggml_layer_dense(16L, activation = "relu", input_shape = 4L) |>
ggml_layer_dense(2L, activation = "softmax")
model <- ggml_compile(model, optimizer = "sgd", loss = "mse")
x <- matrix(runif(32L * 4L), 32L, 4L)
y <- matrix(0.0, 32L, 2L); for (i in seq_len(32L)) y[i, 1L] <- 1.0
model <- ggml_fit(model, x, y, epochs = 1L, batch_size = 32L, verbose = 0L)
tmp <- tempfile(fileext = ".rds")
on.exit({ unlink(tmp); cleanup_model(model) })
ggml_save_model(model, tmp)
m2 <- ggml_load_model(tmp)
on.exit({ unlink(tmp); cleanup_model(model); cleanup_model(m2) }, add = TRUE)
expect_equal(m2$layers[[1]]$config$units, 16L)
expect_equal(m2$compilation$optimizer, "sgd")
expect_equal(m2$compilation$loss, "mse")
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.