Nothing
# Tests for Functional Neural Network API (Block 1)
# Helper: free all ggml resources held by a functional model
cleanup_functional_model <- function(model) {
if (!is.null(model$compilation$buffer)) {
ggml_backend_buffer_free(model$compilation$buffer)
}
if (!is.null(model$compilation$ctx_weights)) {
ggml_free(model$compilation$ctx_weights)
}
if (!is.null(model$compilation$sched)) {
ggml_backend_sched_free(model$compilation$sched)
}
if (!is.null(model$compilation$backend)) {
ggml_backend_free(model$compilation$backend)
}
if (!is.null(model$compilation$cpu_backend)) {
ggml_backend_free(model$compilation$cpu_backend)
}
}
# ============================================================================
# ggml_input()
# ============================================================================
test_that("ggml_input creates a tensor node with correct class", {
x <- ggml_input(shape = 64L)
expect_s3_class(x, "ggml_tensor_node")
})
test_that("ggml_input stores shape correctly for 1-D", {
x <- ggml_input(shape = 32L)
expect_equal(x$config$shape, 32L)
expect_equal(x$node_type, "input")
})
test_that("ggml_input stores shape correctly for 3-D image", {
x <- ggml_input(shape = c(28L, 28L, 1L))
expect_equal(x$config$shape, c(28L, 28L, 1L))
})
test_that("ggml_input auto-generates name like 'input_N'", {
x <- ggml_input(shape = 10L)
expect_match(x$config$name, "^input_\\d+$")
})
test_that("ggml_input respects custom name", {
x <- ggml_input(shape = 10L, name = "my_input")
expect_equal(x$config$name, "my_input")
})
test_that("ggml_input has no parents", {
x <- ggml_input(shape = 8L)
expect_length(x$parents, 0L)
})
# ============================================================================
# ggml_model()
# ============================================================================
test_that("ggml_model accepts single node inputs/outputs", {
x <- ggml_input(shape = 8L)
out <- x |> ggml_layer_dense(4L)
m <- ggml_model(inputs = x, outputs = out)
expect_s3_class(m, "ggml_functional_model")
expect_false(m$compiled)
})
test_that("ggml_model wraps single nodes in lists", {
x <- ggml_input(shape = 8L)
out <- x |> ggml_layer_dense(4L)
m <- ggml_model(inputs = x, outputs = out)
expect_true(is.list(m$inputs))
expect_true(is.list(m$outputs))
expect_length(m$inputs, 1L)
expect_length(m$outputs, 1L)
})
test_that("ggml_model rejects non-tensor-node inputs", {
expect_error(ggml_model(inputs = "not_a_node", outputs = "also_not"), "ggml_tensor_node")
})
# ============================================================================
# Layer dispatch — functional mode
# ============================================================================
test_that("ggml_layer_dense on tensor node returns tensor node", {
x <- ggml_input(shape = 16L)
y <- ggml_layer_dense(x, 8L)
expect_s3_class(y, "ggml_tensor_node")
expect_equal(y$node_type, "dense")
expect_equal(y$config$units, 8L)
})
test_that("ggml_layer_dense on sequential model still works", {
model <- ggml_model_sequential() |>
ggml_layer_dense(8L, activation = "relu")
expect_equal(model$layers[[1]]$type, "dense")
})
test_that("ggml_layer_dense activation propagated in functional mode", {
x <- ggml_input(shape = 8L)
y <- ggml_layer_dense(x, 4L, activation = "relu")
expect_equal(y$config$activation, "relu")
})
test_that("ggml_layer_flatten on tensor node returns tensor node", {
x <- ggml_input(shape = c(4L, 4L))
y <- ggml_layer_flatten(x)
expect_s3_class(y, "ggml_tensor_node")
expect_equal(y$node_type, "flatten")
})
test_that("ggml_layer_batch_norm on tensor node returns tensor node", {
x <- ggml_input(shape = 16L)
y <- ggml_layer_batch_norm(x)
expect_s3_class(y, "ggml_tensor_node")
expect_equal(y$node_type, "batch_norm")
})
# ============================================================================
# ggml_layer_add() / ggml_layer_concatenate()
# ============================================================================
test_that("ggml_layer_add creates add node with two parents", {
x <- ggml_input(shape = 8L)
a <- x |> ggml_layer_dense(8L)
b <- x |> ggml_layer_dense(8L)
z <- ggml_layer_add(list(a, b))
expect_s3_class(z, "ggml_tensor_node")
expect_equal(z$node_type, "add")
expect_length(z$parents, 2L)
})
test_that("ggml_layer_add rejects fewer than 2 tensors", {
x <- ggml_input(shape = 8L)
a <- x |> ggml_layer_dense(8L)
expect_error(ggml_layer_add(list(a)), "at least 2")
})
test_that("ggml_layer_add rejects non-list argument", {
x <- ggml_input(shape = 8L)
expect_error(ggml_layer_add(x))
})
test_that("ggml_layer_concatenate creates concatenate node", {
x <- ggml_input(shape = 8L)
a <- x |> ggml_layer_dense(4L)
b <- x |> ggml_layer_dense(4L)
z <- ggml_layer_concatenate(list(a, b), axis = 0L)
expect_s3_class(z, "ggml_tensor_node")
expect_equal(z$node_type, "concatenate")
expect_equal(z$config$axis, 0L)
})
test_that("ggml_input auto-generates sequential names input_N", {
a <- ggml_input(shape = 4L)
b <- ggml_input(shape = 4L)
# Both should match pattern and be different
expect_match(a$config$name, "^input_\\d+$")
expect_match(b$config$name, "^input_\\d+$")
expect_false(a$config$name == b$config$name)
})
test_that("ggml_model rejects non-input node as input", {
x <- ggml_input(shape = 8L)
mid <- x |> ggml_layer_dense(4L)
out <- mid |> ggml_layer_dense(2L)
expect_error(ggml_model(inputs = mid, outputs = out), "node_type")
})
test_that("ggml_layer_add auto-generates name add_N", {
x <- ggml_input(shape = 8L)
a <- x |> ggml_layer_dense(8L)
b <- x |> ggml_layer_dense(8L)
z <- ggml_layer_add(list(a, b))
expect_match(z$config$name, "^add_\\d+$")
})
test_that("ggml_layer_add respects custom name", {
x <- ggml_input(shape = 8L)
a <- x |> ggml_layer_dense(8L)
b <- x |> ggml_layer_dense(8L)
z <- ggml_layer_add(list(a, b), name = "my_add")
expect_equal(z$config$name, "my_add")
})
test_that("ggml_layer_add supports 3 inputs", {
x <- ggml_input(shape = 8L)
a <- x |> ggml_layer_dense(8L)
b <- x |> ggml_layer_dense(8L)
c <- x |> ggml_layer_dense(8L)
z <- ggml_layer_add(list(a, b, c))
expect_length(z$parents, 3L)
})
test_that("ggml_layer_add shape mismatch raises error at build time", {
set.seed(1)
n <- 32L
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
inp <- ggml_input(shape = 4L)
a <- inp |> ggml_layer_dense(8L)
b <- inp |> ggml_layer_dense(4L) # different shape!
z <- ggml_layer_add(list(a, b))
out <- z |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
expect_error(ggml_fit(m, x, y, epochs = 1L, batch_size = 32L, verbose = 0L),
"shape mismatch")
on.exit(cleanup_functional_model(m))
})
test_that("ggml_layer_concatenate auto-generates name concatenate_N", {
x <- ggml_input(shape = 4L)
a <- x |> ggml_layer_dense(4L)
b <- x |> ggml_layer_dense(4L)
z <- ggml_layer_concatenate(list(a, b), axis = 0L)
expect_match(z$config$name, "^concatenate_\\d+$")
})
test_that("ggml_layer_concatenate supports axis=-1", {
x <- ggml_input(shape = 4L)
a <- x |> ggml_layer_dense(4L)
b <- x |> ggml_layer_dense(4L)
z <- ggml_layer_concatenate(list(a, b), axis = -1L)
expect_equal(z$config$axis, -1L)
})
test_that("ggml_layer_concatenate invalid axis raises error at build time", {
set.seed(1)
n <- 32L
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
inp <- ggml_input(shape = 4L)
a <- inp |> ggml_layer_dense(4L)
b <- inp |> ggml_layer_dense(4L)
z <- ggml_layer_concatenate(list(a, b), axis = 5L) # out of range for 1D
out <- z |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
expect_error(ggml_fit(m, x, y, epochs = 1L, batch_size = 32L, verbose = 0L),
"out of range")
on.exit(cleanup_functional_model(m))
})
test_that("ggml_layer_concatenate with 3 inputs compiles (forward-only; no backward for concat)", {
# ggml_concat does not implement backward pass — test compile only
inp <- ggml_input(shape = 8L)
a <- inp |> ggml_layer_dense(4L, activation = "relu")
b <- inp |> ggml_layer_dense(4L, activation = "relu")
c <- inp |> ggml_layer_dense(4L, activation = "relu")
cat_node <- ggml_layer_concatenate(list(a, b, c), axis = 0L)
out <- cat_node |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
expect_no_error(m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy"))
expect_true(m$compiled)
on.exit(cleanup_functional_model(m))
})
test_that("ResNet-like model (deep residual) compiles and trains", {
set.seed(3)
n <- 64L
x <- matrix(runif(n * 16L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, if (sum(x[i, ]) > 8) 1L else 2L] <- 1.0
inp <- ggml_input(shape = 16L)
# Block 1
a1 <- inp |> ggml_layer_dense(16L, activation = "relu")
r1 <- ggml_layer_add(list(inp, a1))
# Block 2
a2 <- r1 |> ggml_layer_dense(16L, activation = "relu")
r2 <- ggml_layer_add(list(r1, a2))
# Block 3
a3 <- r2 |> ggml_layer_dense(16L, activation = "relu")
r3 <- ggml_layer_add(list(r2, a3))
out <- r3 |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
expect_no_error(m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 32L, verbose = 0L))
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# nn_topo_sort()
# ============================================================================
test_that("nn_topo_sort returns input node before output node", {
x <- ggml_input(shape = 8L)
out <- x |> ggml_layer_dense(4L)
m <- ggml_model(inputs = x, outputs = out)
sorted <- nn_topo_sort(m$outputs)
node_types <- vapply(sorted, `[[`, character(1), "node_type")
# input must appear before dense
expect_equal(node_types[1], "input")
expect_equal(node_types[2], "dense")
})
test_that("nn_topo_sort handles residual graph correctly", {
x <- ggml_input(shape = 8L)
a <- x |> ggml_layer_dense(8L, activation = "relu")
skip <- x |> ggml_layer_dense(8L)
out <- ggml_layer_add(list(a, skip))
m <- ggml_model(inputs = x, outputs = out)
sorted <- nn_topo_sort(m$outputs)
node_types <- vapply(sorted, `[[`, character(1), "node_type")
# input first, add last
expect_equal(node_types[1], "input")
expect_equal(node_types[length(node_types)], "add")
})
# ============================================================================
# Compile
# ============================================================================
test_that("ggml_compile works on functional model", {
x <- ggml_input(shape = 4L)
out <- x |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = x, outputs = out)
m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy")
expect_true(m$compiled)
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Fit — linear graph (equivalent to Sequential)
# ============================================================================
test_that("ggml_fit.ggml_functional_model trains a linear model", {
set.seed(42)
n <- 64L
x <- matrix(runif(n * 4L), nrow = n, ncol = 4L)
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
inp <- ggml_input(shape = 4L)
out <- inp |>
ggml_layer_dense(8L, activation = "relu") |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 3L, batch_size = 32L, verbose = 0L)
expect_true(m$compiled)
expect_s3_class(m$history, "ggml_history")
expect_length(m$history$train_loss, 3L)
# Loss should be finite
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Fit — residual block (add node)
# ============================================================================
test_that("ggml_fit trains a model with residual add", {
set.seed(7)
n <- 64L
x <- matrix(runif(n * 8L), nrow = n, ncol = 8L)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, if (sum(x[i, ]) > 4) 1L else 2L] <- 1.0
inp <- ggml_input(shape = 8L)
a <- inp |> ggml_layer_dense(8L, activation = "relu")
skip <- inp |> ggml_layer_dense(8L)
res <- ggml_layer_add(list(a, skip))
out <- res |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy")
expect_no_error(
m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 32L, verbose = 0L)
)
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Fit — concatenate
# ============================================================================
test_that("ggml_model with concatenate compiles and graph builds", {
# Note: ggml_concat does not support backward pass, so we only test that
# the model can be compiled (graph structure is valid). Training would
# require a custom backward implementation in ggml.
inp <- ggml_input(shape = 8L)
a <- inp |> ggml_layer_dense(4L, activation = "relu")
b <- inp |> ggml_layer_dense(4L, activation = "relu")
cat_node <- ggml_layer_concatenate(list(a, b), axis = 0L)
out <- cat_node |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
expect_no_error(
m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy")
)
expect_true(m$compiled)
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Evaluate
# ============================================================================
test_that("ggml_evaluate.ggml_functional_model returns loss and accuracy", {
set.seed(99)
n <- 64L
x <- matrix(runif(n * 4L), nrow = n, ncol = 4L)
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
inp <- ggml_input(shape = 4L)
out <- inp |>
ggml_layer_dense(8L, activation = "relu") |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 32L, verbose = 0L)
res <- ggml_evaluate(m, x, y, batch_size = 32L)
expect_named(res, c("loss", "accuracy", "n_samples"))
expect_true(is.finite(res$loss))
expect_true(is.finite(res$accuracy))
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Predict
# ============================================================================
test_that("ggml_predict.ggml_functional_model returns matrix of correct shape", {
set.seed(123)
n <- 64L
x <- matrix(runif(n * 4L), nrow = n, ncol = 4L)
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
inp <- ggml_input(shape = 4L)
out <- inp |>
ggml_layer_dense(8L, activation = "relu") |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 32L, verbose = 0L)
preds <- ggml_predict(m, x, batch_size = 32L)
expect_true(is.matrix(preds))
expect_equal(nrow(preds), n)
expect_equal(ncol(preds), 2L)
# Softmax rows should sum to ~1
row_sums <- rowSums(preds)
expect_true(all(abs(row_sums - 1.0) < 0.01))
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Block 2 — Dropout
# ============================================================================
test_that("ggml_layer_dropout on tensor node returns tensor node with correct type", {
x <- ggml_input(shape = 16L)
y <- ggml_layer_dropout(x, rate = 0.5)
expect_s3_class(y, "ggml_tensor_node")
expect_equal(y$node_type, "dropout")
expect_equal(y$config$rate, 0.5)
})
test_that("ggml_layer_dropout on sequential model adds a dropout layer", {
model <- ggml_model_sequential() |>
ggml_layer_dense(8L, activation = "relu", input_shape = 4L) |>
ggml_layer_dropout(0.3)
expect_equal(length(model$layers), 2L)
expect_equal(model$layers[[2]]$type, "dropout")
expect_equal(model$layers[[2]]$config$rate, 0.3)
})
test_that("ggml_layer_dropout rejects rate >= 1", {
x <- ggml_input(shape = 8L)
expect_error(ggml_layer_dropout(x, rate = 1.0))
expect_error(ggml_layer_dropout(x, rate = 1.5))
})
test_that("functional model with dropout compiles without error", {
inp <- ggml_input(shape = 8L)
out <- inp |>
ggml_layer_dense(16L, activation = "relu") |>
ggml_layer_dropout(0.5) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
expect_no_error(m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy"))
expect_true(m$compiled)
on.exit(cleanup_functional_model(m))
})
test_that("ggml_fit with dropout converges (loss decreases)", {
set.seed(1)
n <- 64L
x <- matrix(runif(n * 8L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, if (sum(x[i, ]) > 4) 1L else 2L] <- 1.0
inp <- ggml_input(shape = 8L)
out <- inp |>
ggml_layer_dense(16L, activation = "relu") |>
ggml_layer_dropout(0.3) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 5L, batch_size = 32L, verbose = 0L)
expect_true(all(is.finite(m$history$train_loss)))
# Loss over 5 epochs should go down (at least from first to last)
expect_true(m$history$train_loss[5] <= m$history$train_loss[1] * 1.1)
on.exit(cleanup_functional_model(m))
})
test_that("ggml_predict with dropout is deterministic (two calls give same result)", {
set.seed(2)
n <- 32L
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
inp <- ggml_input(shape = 4L)
out <- inp |>
ggml_layer_dense(8L, activation = "relu") |>
ggml_layer_dropout(0.4) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 1L, batch_size = 32L, verbose = 0L)
p1 <- ggml_predict(m, x, batch_size = 32L)
p2 <- ggml_predict(m, x, batch_size = 32L)
expect_equal(p1, p2)
on.exit(cleanup_functional_model(m))
})
test_that("ggml_layer_dropout stochastic=TRUE stores config correctly", {
x <- ggml_input(shape = 16L)
y <- ggml_layer_dropout(x, rate = 0.5, stochastic = TRUE)
expect_true(isTRUE(y$config$stochastic))
})
test_that("ggml_fit with stochastic dropout trains without error", {
set.seed(10)
n <- 64L
x <- matrix(runif(n * 8L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, if (sum(x[i, ]) > 4) 1L else 2L] <- 1.0
inp <- ggml_input(shape = 8L)
out <- inp |>
ggml_layer_dense(16L, activation = "relu") |>
ggml_layer_dropout(0.3, stochastic = TRUE) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
expect_no_error(m <- ggml_fit(m, x, y, epochs = 3L, batch_size = 32L, verbose = 0L))
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
test_that("ggml_predict with stochastic dropout is deterministic (training=FALSE, mask=ones)", {
set.seed(11)
n <- 32L
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
inp <- ggml_input(shape = 4L)
out <- inp |>
ggml_layer_dense(8L, activation = "relu") |>
ggml_layer_dropout(0.4, stochastic = TRUE) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 1L, batch_size = 32L, verbose = 0L)
p1 <- ggml_predict(m, x, batch_size = 32L)
p2 <- ggml_predict(m, x, batch_size = 32L)
expect_equal(p1, p2)
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Block 2 — Embedding
# ============================================================================
test_that("ggml_input with dtype='int32' stores dtype correctly", {
x <- ggml_input(shape = 10L, dtype = "int32")
expect_equal(x$config$dtype, "int32")
expect_equal(x$config$shape, 10L)
})
test_that("ggml_input with default dtype is 'float32'", {
x <- ggml_input(shape = 8L)
expect_equal(x$config$dtype, "float32")
})
test_that("ggml_input rejects unknown dtype", {
expect_error(ggml_input(shape = 8L, dtype = "float16"), "dtype")
})
test_that("ggml_layer_embedding on tensor node returns tensor node with correct config", {
x <- ggml_input(shape = 10L, dtype = "int32")
e <- ggml_layer_embedding(x, vocab_size = 100L, dim = 8L)
expect_s3_class(e, "ggml_tensor_node")
expect_equal(e$node_type, "embedding")
expect_equal(e$config$vocab_size, 100L)
expect_equal(e$config$dim, 8L)
})
test_that("embedding model compiles without error", {
inp <- ggml_input(shape = 10L, dtype = "int32")
out <- inp |>
ggml_layer_embedding(vocab_size = 50L, dim = 8L) |>
ggml_layer_flatten() |>
ggml_layer_dense(3L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
expect_no_error(m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy"))
expect_true(m$compiled)
on.exit(cleanup_functional_model(m))
})
test_that("ggml_fit with embedding layer trains without error", {
set.seed(3)
n <- 64L
seq_len <- 5L
vocab <- 20L
n_class <- 3L
x <- matrix(sample(0L:(vocab - 1L), n * seq_len, replace = TRUE),
nrow = n, ncol = seq_len)
y <- matrix(0.0, nrow = n, ncol = n_class)
for (i in seq_len(n)) y[i, sample(n_class, 1)] <- 1.0
inp <- ggml_input(shape = seq_len, dtype = "int32")
out <- inp |>
ggml_layer_embedding(vocab_size = vocab, dim = 8L) |>
ggml_layer_flatten() |>
ggml_layer_dense(n_class, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
expect_no_error(m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 32L, verbose = 0L))
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
test_that("ggml_predict with embedding returns matrix of correct shape", {
set.seed(4)
n <- 32L
seq_len <- 4L
vocab <- 10L
n_class <- 2L
x <- matrix(sample(0L:(vocab - 1L), n * seq_len, replace = TRUE),
nrow = n, ncol = seq_len)
y <- matrix(0.0, nrow = n, ncol = n_class)
for (i in seq_len(n)) y[i, sample(n_class, 1)] <- 1.0
inp <- ggml_input(shape = seq_len, dtype = "int32")
out <- inp |>
ggml_layer_embedding(vocab_size = vocab, dim = 4L) |>
ggml_layer_flatten() |>
ggml_layer_dense(n_class, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 1L, batch_size = 32L, verbose = 0L)
preds <- ggml_predict(m, x, batch_size = 32L)
expect_true(is.matrix(preds))
expect_equal(nrow(preds), n)
expect_equal(ncol(preds), n_class)
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Block 2 — Multi-output
# ============================================================================
test_that("ggml_model with two outputs stores both", {
inp <- ggml_input(shape = 8L)
hidden <- inp |> ggml_layer_dense(4L, activation = "relu")
out <- hidden |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = list(hidden, out))
expect_length(m$outputs, 2L)
})
test_that("ggml_predict with multi-output model returns list of length 2", {
set.seed(5)
n <- 32L
x <- matrix(runif(n * 8L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, if (sum(x[i, ]) > 4) 1L else 2L] <- 1.0
inp <- ggml_input(shape = 8L)
hidden <- inp |> ggml_layer_dense(4L, activation = "relu")
out <- hidden |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = list(hidden, out))
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 1L, batch_size = 32L, verbose = 0L)
preds <- ggml_predict(m, x, batch_size = 32L)
expect_true(is.list(preds))
expect_length(preds, 2L)
on.exit(cleanup_functional_model(m))
})
test_that("multi-output: first output has shape [n, 4], second has shape [n, 2]", {
set.seed(6)
n <- 32L
x <- matrix(runif(n * 8L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, if (sum(x[i, ]) > 4) 1L else 2L] <- 1.0
inp <- ggml_input(shape = 8L)
hidden <- inp |> ggml_layer_dense(4L, activation = "relu")
out <- hidden |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = list(hidden, out))
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 1L, batch_size = 32L, verbose = 0L)
preds <- ggml_predict(m, x, batch_size = 32L)
expect_equal(dim(preds[[1]]), c(n, 4L))
expect_equal(dim(preds[[2]]), c(n, 2L))
on.exit(cleanup_functional_model(m))
})
test_that("ggml_fit with multi-output uses last output for loss", {
set.seed(7)
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
inp <- ggml_input(shape = 4L)
hidden <- inp |> ggml_layer_dense(8L, activation = "relu")
out <- hidden |> ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = list(hidden, out))
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
expect_no_error(m <- ggml_fit(m, x, y, epochs = 3L, batch_size = 32L, verbose = 0L))
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Block 3 — Shared layers
# ============================================================================
test_that("shared dense layer: two applications create nodes with same name", {
x1 <- ggml_input(shape = 4L, name = "inp_sh1")
x2 <- ggml_input(shape = 4L, name = "inp_sh2")
y1 <- ggml_layer_dense(x1, 8L, activation = "relu", name = "shared")
y2 <- ggml_layer_dense(x2, 8L, activation = "relu", name = "shared")
expect_equal(y1$config$name, "shared")
expect_equal(y2$config$name, "shared")
expect_false(y1$id == y2$id)
})
test_that("shared dense layer: Siamese compile does not error", {
# Two branches with same input shape share one dense layer by name.
x1 <- ggml_input(shape = 4L, name = "in_a")
x2 <- ggml_input(shape = 4L, name = "in_b")
h1 <- ggml_layer_dense(x1, 8L, activation = "relu", name = "shared_d")
h2 <- ggml_layer_dense(x2, 8L, activation = "relu", name = "shared_d")
cat_out <- ggml_layer_concatenate(list(h1, h2), axis = 0L)
out <- ggml_layer_dense(cat_out, 2L, activation = "softmax")
# Use only the first input for single-input compile check
m <- ggml_model(inputs = x1, outputs = out)
expect_no_error(m <- ggml_compile(m, optimizer = "adam",
loss = "categorical_crossentropy"))
on.exit(cleanup_functional_model(m))
})
test_that("shared dense layer: fit on single branch converges", {
# Simplest shared-weight test: two nodes named "enc" both applied to the
# same input x, then concatenated. Both use identical weight tensors.
set.seed(42)
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
inp <- ggml_input(shape = 4L)
# Apply "enc" to the same input twice — both get same weights
h1 <- ggml_layer_dense(inp, 4L, activation = "relu", name = "enc")
h2 <- ggml_layer_dense(inp, 4L, activation = "relu", name = "enc")
# h1 == h2 (same weights, same input) -> their sum is equivalent to 2*h1
merged <- ggml_layer_add(list(h1, h2))
out <- ggml_layer_dense(merged, 2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 3L, batch_size = 32L, verbose = 0L)
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
test_that("shared dense layer: predict after fit is deterministic", {
set.seed(99)
n <- 32L
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
inp <- ggml_input(shape = 4L)
h1 <- ggml_layer_dense(inp, 4L, activation = "relu", name = "enc2")
h2 <- ggml_layer_dense(inp, 4L, activation = "relu", name = "enc2")
merged <- ggml_layer_add(list(h1, h2))
out <- ggml_layer_dense(merged, 2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, optimizer = "adam", loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 32L, verbose = 0L)
p1 <- ggml_predict(m, x, batch_size = 32L)
p2 <- ggml_predict(m, x, batch_size = 32L)
expect_equal(p1, p2)
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Block 4 — GlobalMaxPooling2D / GlobalAveragePooling2D
# ============================================================================
test_that("ggml_layer_global_max_pooling_2d on tensor node returns tensor node", {
x <- ggml_input(shape = c(8L, 8L, 16L))
y <- ggml_layer_global_max_pooling_2d(x)
expect_s3_class(y, "ggml_tensor_node")
expect_equal(y$node_type, "global_max_pooling_2d")
})
test_that("ggml_layer_global_average_pooling_2d on tensor node returns tensor node", {
x <- ggml_input(shape = c(8L, 8L, 16L))
y <- ggml_layer_global_average_pooling_2d(x)
expect_s3_class(y, "ggml_tensor_node")
expect_equal(y$node_type, "global_average_pooling_2d")
})
test_that("GlobalMaxPooling2D output shape inference: [H,W,C] -> [C]", {
x <- ggml_input(shape = c(4L, 4L, 8L))
y <- ggml_layer_global_max_pooling_2d(x)
out <- ggml_layer_dense(y, 2L, activation = "softmax")
m <- ggml_model(inputs = x, outputs = out)
expect_no_error(m <- ggml_compile(m, loss = "categorical_crossentropy"))
on.exit(cleanup_functional_model(m))
})
test_that("GlobalMaxPooling2D functional model fits without error", {
set.seed(21)
n <- 32L
x <- array(runif(n * 4L * 4L * 8L), dim = c(n, 4L, 4L, 8L))
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
inp <- ggml_input(shape = c(4L, 4L, 8L))
out <- inp |>
ggml_layer_global_max_pooling_2d() |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
expect_no_error(m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 16L, verbose = 0L))
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
test_that("GlobalAveragePooling2D functional model fits without error", {
set.seed(22)
n <- 32L
x <- array(runif(n * 4L * 4L * 8L), dim = c(n, 4L, 4L, 8L))
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
inp <- ggml_input(shape = c(4L, 4L, 8L))
out <- inp |>
ggml_layer_global_average_pooling_2d() |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
expect_no_error(m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 16L, verbose = 0L))
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Block 4 — LSTM / GRU (Functional API)
# ============================================================================
test_that("ggml_layer_lstm on tensor node returns tensor node", {
x <- ggml_input(shape = c(10L, 8L))
y <- ggml_layer_lstm(x, 16L)
expect_s3_class(y, "ggml_tensor_node")
expect_equal(y$node_type, "lstm")
expect_equal(y$config$units, 16L)
})
test_that("ggml_layer_gru on tensor node returns tensor node", {
x <- ggml_input(shape = c(10L, 8L))
y <- ggml_layer_gru(x, 16L)
expect_s3_class(y, "ggml_tensor_node")
expect_equal(y$node_type, "gru")
expect_equal(y$config$units, 16L)
})
test_that("LSTM output shape without return_sequences: [units]", {
x <- ggml_input(shape = c(5L, 4L))
y <- ggml_layer_lstm(x, 8L, return_sequences = FALSE)
out <- ggml_layer_dense(y, 2L, activation = "softmax")
m <- ggml_model(inputs = x, outputs = out)
expect_no_error(m <- ggml_compile(m, loss = "categorical_crossentropy"))
on.exit(cleanup_functional_model(m))
})
test_that("LSTM functional 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
inp <- ggml_input(shape = c(seq_len, input_sz))
out <- inp |>
ggml_layer_lstm(8L, return_sequences = FALSE) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
expect_no_error(m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 16L, verbose = 0L))
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
test_that("GRU functional 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
inp <- ggml_input(shape = c(seq_len, input_sz))
out <- inp |>
ggml_layer_gru(8L, return_sequences = FALSE) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
expect_no_error(m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 16L, verbose = 0L))
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
test_that("LSTM predict is deterministic", {
set.seed(33)
n <- 32L
seq_len <- 4L
input_sz <- 3L
x <- array(runif(n * seq_len * input_sz), 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
inp <- ggml_input(shape = c(seq_len, input_sz))
out <- inp |>
ggml_layer_lstm(6L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 1L, batch_size = 16L, verbose = 0L)
p1 <- ggml_predict(m, x, batch_size = 16L)
p2 <- ggml_predict(m, x, batch_size = 16L)
expect_equal(p1, p2)
on.exit(cleanup_functional_model(m))
})
# ============================================================================
# Block 4 — Save / Load (Sequential)
# ============================================================================
test_that("ggml_save_model and ggml_load_model round-trip Sequential dense model", {
set.seed(41)
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))
ggml_save_model(model, tmp)
model2 <- ggml_load_model(tmp)
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 restores correct architecture", {
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))
ggml_save_model(model, tmp)
m2 <- ggml_load_model(tmp)
expect_equal(length(m2$layers), 2L)
expect_equal(m2$layers[[1]]$type, "dense")
expect_equal(m2$layers[[1]]$config$units, 16L)
expect_equal(m2$compilation$optimizer, "sgd")
expect_equal(m2$compilation$loss, "mse")
})
test_that("ggml_load_model Sequential rejects version-1 files (ggml_save_weights)", {
model <- ggml_model_sequential() |>
ggml_layer_dense(4L, activation = "relu", input_shape = 2L) |>
ggml_layer_dense(2L, activation = "softmax")
model <- ggml_compile(model, loss = "categorical_crossentropy")
x <- matrix(runif(32L * 2L), 32L, 2L)
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))
ggml_save_weights(model, tmp)
expect_error(ggml_load_model(tmp), "ggml_save_weights")
})
# ============================================================================
# Block 4 — Save / Load (Functional)
# ============================================================================
test_that("ggml_save_model and ggml_load_model round-trip functional dense model", {
set.seed(51)
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
inp <- ggml_input(shape = 4L)
out <- inp |>
ggml_layer_dense(8L, activation = "relu") |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 2L, batch_size = 32L, verbose = 0L)
tmp <- tempfile(fileext = ".rds")
on.exit(unlink(tmp))
ggml_save_model(m, tmp)
m2 <- ggml_load_model(tmp)
on.exit({ unlink(tmp); cleanup_functional_model(m2) }, add = TRUE)
expect_s3_class(m2, "ggml_functional_model")
expect_true(m2$compiled)
p1 <- ggml_predict(m, x, batch_size = 32L)
p2 <- ggml_predict(m2, x, batch_size = 32L)
expect_equal(p1, p2, tolerance = 1e-5)
on.exit(cleanup_functional_model(m), add = TRUE)
})
test_that("ggml_save_model functional: loaded model predict is deterministic", {
set.seed(52)
n <- 32L
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
inp <- ggml_input(shape = 4L)
out <- inp |>
ggml_layer_dense(8L, activation = "relu") |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = inp, outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
m <- ggml_fit(m, x, y, epochs = 1L, batch_size = 32L, verbose = 0L)
tmp <- tempfile(fileext = ".rds")
on.exit({ unlink(tmp); cleanup_functional_model(m) })
ggml_save_model(m, tmp)
m2 <- ggml_load_model(tmp)
on.exit({ unlink(tmp); cleanup_functional_model(m); cleanup_functional_model(m2) },
add = TRUE)
p1 <- ggml_predict(m2, x, batch_size = 32L)
p2 <- ggml_predict(m2, x, batch_size = 32L)
expect_equal(p1, p2)
})
# ============================================================================
# Block 7 — Multi-input functional models
# ============================================================================
test_that("ggml_model accepts list of two inputs", {
inp1 <- ggml_input(shape = 4L, name = "a")
inp2 <- ggml_input(shape = 3L, name = "b")
br1 <- inp1 |> ggml_layer_dense(4L, activation = "relu")
br2 <- inp2 |> ggml_layer_dense(4L, activation = "relu")
out <- ggml_layer_concatenate(list(br1, br2), axis = 0L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = list(inp1, inp2), outputs = out)
expect_equal(length(m$inputs), 2L)
expect_equal(m$inputs[[1L]]$config$name, "a")
expect_equal(m$inputs[[2L]]$config$name, "b")
})
test_that("multi-input model compiles without error", {
inp1 <- ggml_input(shape = 4L)
inp2 <- ggml_input(shape = 3L)
br1 <- inp1 |> ggml_layer_dense(4L, activation = "relu")
br2 <- inp2 |> ggml_layer_dense(4L, activation = "relu")
out <- ggml_layer_concatenate(list(br1, br2), axis = 0L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = list(inp1, inp2), outputs = out)
expect_no_error(ggml_compile(m, loss = "categorical_crossentropy"))
})
test_that("multi-input fit runs without error and returns finite loss", {
set.seed(60)
n <- 64L
x1 <- matrix(runif(n * 6L), nrow = n)
x2 <- matrix(runif(n * 4L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
inp1 <- ggml_input(shape = 6L)
inp2 <- ggml_input(shape = 4L)
br1 <- inp1 |> ggml_layer_dense(8L, activation = "relu")
br2 <- inp2 |> ggml_layer_dense(8L, activation = "relu")
out <- ggml_layer_concatenate(list(br1, br2), axis = 0L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = list(inp1, inp2), outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
expect_no_error(
m <- ggml_fit(m, list(x1, x2), y, epochs = 2L, batch_size = 32L, verbose = 0L)
)
expect_true(all(is.finite(m$history$train_loss)))
on.exit(cleanup_functional_model(m))
})
test_that("multi-input fit with validation_split runs without error", {
set.seed(61)
n <- 64L
x1 <- matrix(runif(n * 6L), nrow = n)
x2 <- matrix(runif(n * 4L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
inp1 <- ggml_input(shape = 6L)
inp2 <- ggml_input(shape = 4L)
br1 <- inp1 |> ggml_layer_dense(8L, activation = "relu")
br2 <- inp2 |> ggml_layer_dense(8L, activation = "relu")
out <- ggml_layer_concatenate(list(br1, br2), axis = 0L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = list(inp1, inp2), outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
expect_no_error(
m <- ggml_fit(m, list(x1, x2), y,
epochs = 2L, batch_size = 32L,
validation_split = 0.25, verbose = 0L)
)
expect_true(all(is.finite(m$history$train_loss)))
expect_true(all(is.finite(m$history$val_loss)))
on.exit(cleanup_functional_model(m))
})
test_that("multi-input predict returns matrix with correct dimensions", {
set.seed(62)
n <- 64L
x1 <- matrix(runif(n * 6L), nrow = n)
x2 <- matrix(runif(n * 4L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
inp1 <- ggml_input(shape = 6L)
inp2 <- ggml_input(shape = 4L)
br1 <- inp1 |> ggml_layer_dense(8L, activation = "relu")
br2 <- inp2 |> ggml_layer_dense(8L, activation = "relu")
out <- ggml_layer_concatenate(list(br1, br2), axis = 0L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = list(inp1, inp2), outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
m <- ggml_fit(m, list(x1, x2), y, epochs = 1L, batch_size = 32L, verbose = 0L)
preds <- ggml_predict(m, list(x1, x2), batch_size = 32L)
expect_true(is.matrix(preds))
expect_equal(nrow(preds), n)
expect_equal(ncol(preds), 2L)
expect_true(all(is.finite(preds)))
on.exit(cleanup_functional_model(m))
})
test_that("multi-input predict rows sum to 1 (softmax)", {
set.seed(63)
n <- 64L
x1 <- matrix(runif(n * 6L), nrow = n)
x2 <- matrix(runif(n * 4L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
inp1 <- ggml_input(shape = 6L)
inp2 <- ggml_input(shape = 4L)
br1 <- inp1 |> ggml_layer_dense(8L, activation = "relu")
br2 <- inp2 |> ggml_layer_dense(8L, activation = "relu")
out <- ggml_layer_concatenate(list(br1, br2), axis = 0L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = list(inp1, inp2), outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
m <- ggml_fit(m, list(x1, x2), y, epochs = 1L, batch_size = 32L, verbose = 0L)
preds <- ggml_predict(m, list(x1, x2), batch_size = 32L)
row_sums <- rowSums(preds)
expect_true(all(abs(row_sums - 1.0) < 1e-4))
on.exit(cleanup_functional_model(m))
})
test_that("multi-input evaluate returns finite loss and accuracy", {
set.seed(64)
n <- 64L
x1 <- matrix(runif(n * 6L), nrow = n)
x2 <- matrix(runif(n * 4L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
inp1 <- ggml_input(shape = 6L)
inp2 <- ggml_input(shape = 4L)
br1 <- inp1 |> ggml_layer_dense(8L, activation = "relu")
br2 <- inp2 |> ggml_layer_dense(8L, activation = "relu")
out <- ggml_layer_concatenate(list(br1, br2), axis = 0L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = list(inp1, inp2), outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
m <- ggml_fit(m, list(x1, x2), y, epochs = 1L, batch_size = 32L, verbose = 0L)
score <- ggml_evaluate(m, list(x1, x2), y, batch_size = 32L)
expect_true(is.finite(score$loss))
expect_true(is.finite(score$accuracy))
expect_true(score$accuracy >= 0.0 && score$accuracy <= 1.0)
on.exit(cleanup_functional_model(m))
})
test_that("multi-input predict is deterministic", {
set.seed(65)
n <- 64L
x1 <- matrix(runif(n * 6L), nrow = n)
x2 <- matrix(runif(n * 4L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
inp1 <- ggml_input(shape = 6L)
inp2 <- ggml_input(shape = 4L)
br1 <- inp1 |> ggml_layer_dense(8L, activation = "relu")
br2 <- inp2 |> ggml_layer_dense(8L, activation = "relu")
out <- ggml_layer_concatenate(list(br1, br2), axis = 0L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = list(inp1, inp2), outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
m <- ggml_fit(m, list(x1, x2), y, epochs = 1L, batch_size = 32L, verbose = 0L)
p1 <- ggml_predict(m, list(x1, x2), batch_size = 32L)
p2 <- ggml_predict(m, list(x1, x2), batch_size = 32L)
expect_equal(p1, p2)
on.exit(cleanup_functional_model(m))
})
test_that("multi-input save/load round-trip preserves predictions", {
set.seed(66)
n <- 64L
x1 <- matrix(runif(n * 6L), nrow = n)
x2 <- matrix(runif(n * 4L), nrow = n)
y <- matrix(0.0, nrow = n, ncol = 2L)
for (i in seq_len(n)) y[i, (i %% 2L) + 1L] <- 1.0
inp1 <- ggml_input(shape = 6L)
inp2 <- ggml_input(shape = 4L)
br1 <- inp1 |> ggml_layer_dense(8L, activation = "relu")
br2 <- inp2 |> ggml_layer_dense(8L, activation = "relu")
out <- ggml_layer_concatenate(list(br1, br2), axis = 0L) |>
ggml_layer_dense(2L, activation = "softmax")
m <- ggml_model(inputs = list(inp1, inp2), outputs = out)
m <- ggml_compile(m, loss = "categorical_crossentropy")
m <- ggml_fit(m, list(x1, x2), y, epochs = 2L, batch_size = 32L, verbose = 0L)
tmp <- tempfile(fileext = ".rds")
on.exit({ unlink(tmp); cleanup_functional_model(m) })
ggml_save_model(m, tmp)
m2 <- ggml_load_model(tmp)
on.exit({ unlink(tmp); cleanup_functional_model(m); cleanup_functional_model(m2) },
add = TRUE)
p1 <- ggml_predict(m, list(x1, x2), batch_size = 32L)
p2 <- ggml_predict(m2, list(x1, x2), batch_size = 32L)
expect_equal(p1, p2)
})
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.