Open-source agentic AI for transaction monitoring. Just ask Jooby.What's Jube? →

Neural Networks

The original R training guide treated feed-forward neural networks as a “universal predictive analytics method” — equally applicable to numeric prediction and classification, at the cost of being harder to explain as the network grows. It reused the same FDX equity dataset from earlier regression pages and the same FraudRisk transaction dataset from earlier classification pages, so the improvement a neural network buys over a plain regression or logistic model could be judged directly. Six procedures follow: train a single-hidden-layer network, plot its topology, recall it with compute() and read the correlation between prediction and outcome, train a deeper multi-layer network for comparison, train a network as a binary classifier, and activate that classifier into a confusion matrix.

The original bundle’s CSV files (PC_FDX_Close_200x1D_Close_50x1D_10.csv, FraudRisk.csv) are not present in this environment, so the R below builds synthetic data frames with the same column layout instead of loading them — Skew_3, Max_4, PointStep_16, Close_3, Close_4, PointStep_17_ZScore, PointStep_15, TypicalValue_4, Range_4, Range_2 and a continuous Dependent for FDX; the same ten fraud-signal columns and a binary Dependent for FraudRisk. The neuralnet() calls, compute() calls and correlation/confusion-matrix results below are all genuine, run against that synthetic data, not transcribed or estimated — so the numbers differ from the original guide’s (which reported a 0.65 and 0.91 correlation, and a 79.9% fraud detection rate), but the same mechanics and the same direction of improvement (deeper network, better fit) hold.

Extra package required: neuralnet is not part of this guide’s shared pre-installed package set. It was installed for this module with install.packages("neuralnet", lib="/rlibs"), which pulled in one dependency, Deriv. Version installed: neuralnet 1.44.2.

In passing: Jube’s own long-standing Exhaustive Search model family also trains a feed-forward neural network, but by a different, evolved-topology method documented in Exhaustive Methodology — this page’s mapping below is to the newer AdvancedAnalytics neural-network training services, not to Exhaustive.

Procedure 1: Train a Neural Network

Training a neural network with neuralnet() resembles building a regression model, with two nuances: the dot notation for “all variables” does not work (a manually written formula is required), and the function is single-threaded and can take a long time on data frames with many rows or many variables. This example trains a network with one hidden layer of four processing elements against ten variables already known (from the earlier correlation-based variable selection modules) to relate to the dependent variable.

library(neuralnet, lib.loc = "/rlibs")

set.seed(13)
n <- 400
FDX <- data.frame(
  Skew_3 = runif(n),
  Max_4 = runif(n),
  PointStep_16 = runif(n),
  Close_3 = runif(n),
  Close_4 = runif(n),
  PointStep_17_ZScore = runif(n),
  PointStep_15 = runif(n),
  TypicalValue_4 = runif(n),
  Range_4 = runif(n),
  Range_2 = runif(n)
)
signal <- with(FDX, 0.35 * Skew_3 + 0.25 * Max_4 + 0.15 * PointStep_16 + 0.1 * Close_3 -
  0.2 * Close_4 + 0.3 * PointStep_17_ZScore - 0.1 * PointStep_15 + 0.2 * TypicalValue_4 -
  0.15 * Range_4 + 0.1 * Range_2)
signal <- (signal - min(signal)) / (max(signal) - min(signal))
FDX$Dependent <- pmin(pmax(signal + rnorm(n, 0, 0.08), 0), 1)

str(FDX)

NeuralNetworkFourByOne <- neuralnet(Dependent ~ Skew_3 + Max_4 + PointStep_16 + Close_3 +
  Close_4 + PointStep_17_ZScore + PointStep_15 + TypicalValue_4 + Range_4 + Range_2,
  data = FDX, hidden = 4)

cat("Converged in", NeuralNetworkFourByOne$result.matrix["steps", 1], "steps\n")
cat("Error (SSE):", NeuralNetworkFourByOne$result.matrix["error", 1], "\n")

Console output:

'data.frame':	400 obs. of  11 variables:
 $ Skew_3             : num  0.7103 0.2461 0.3896 0.0914 0.9621 ...
 $ Max_4              : num  0.849 0.147 0.794 0.781 0.327 ...
 $ PointStep_16       : num  0.584 0.779 0.566 0.471 0.737 ...
 $ Close_3            : num  0.8308 0.0757 0.6656 0.7158 0.5642 ...
 $ Close_4            : num  0.9288 0.9694 0.5354 0.634 0.0228 ...
 $ PointStep_17_ZScore: num  0.2786 0.0166 0.8677 0.4752 0.8943 ...
 $ PointStep_15       : num  0.181 0.649 0.411 0.684 0.742 ...
 $ TypicalValue_4     : num  0.0824 0.6092 0.2023 0.6519 0.522 ...
 $ Range_4            : num  0.941 0.6019 0.0324 0.648 0.8165 ...
 $ Range_2            : num  0.566 0.437 0.126 0.201 0.736 ...
 $ Dependent          : num  0.505 0.194 0.584 0.488 0.826 ...
Converged in 3975 steps
Error (SSE): 1.060863

Tool equivalent: Jube.Service.Repository.AnalyticsTrainingRun.AdvancedAnalyticsNeuralNetworkRegressorTrainingService / tool SubmitNeuralNetworkRegressorTraining on AdvancedAnalyticsNeuralNetworkRegressorTrainingDto — queues a single-hidden-layer feed-forward network for continuous prediction (rows, variable names and a HiddenUnits count in place of hidden=, plus epochs, learning rate, standardisation and a train/validation/test split), trained asynchronously by a background worker rather than blocking the caller for the training run’s duration.

Procedure 2: Plotting a Neural Network

The neuralnet object supports the base plot() function, drawing the network’s nodes and connections. Despite the reputation neural networks have for being unexplainable, the topology plot for a small network with a single hidden layer is quite readable and reproducible by hand; that readability degrades as more layers and processing elements are added.

pdf(NULL)
plot(NeuralNetworkFourByOne)
invisible(dev.off())
cat("Rendered with plot() to a graphics device (text-only doc: not reproduced as an image)\n")
cat("Weight matrices (hidden layer, output layer):\n")
for (i in seq_along(NeuralNetworkFourByOne$weights[[1]])) {
  w <- NeuralNetworkFourByOne$weights[[1]][[i]]
  cat("  matrix", i, "dim:", paste(dim(w), collapse = " x "), "\n")
}

Console output:

Rendered with plot() to a graphics device (text-only doc: not reproduced as an image)
Weight matrices (hidden layer, output layer):
  matrix 1 dim: 11 x 4
  matrix 2 dim: 5 x 1

AdvancedAnalytics equivalent: Jube.Charting.Diagrams.NetworkDiagram (built via Diagram.Network()/NetworkDiagramBuilder, in Jube.Charting/Diagrams/NetworkDiagram.cs and NetworkDiagramBuilder.cs) — a real network/graph diagram chart type, with Input/Hidden/Output layers, per-edge weights and per-node biases. It is wired up for this exact model family in Jube.Service.ModelDiagrams.Adapters.NeuralNetworkPipelineKindAdapter, which reads a trained NeuralNetworkClassifierPipeline/NeuralNetworkRegressorPipeline model’s stored input weights, hidden biases, output weights and output bias straight out of its snapshot JSON and renders them as an SVG network diagram (labelled “Neural network regressor”/”Neural network classifier”, with the sigmoid activation and unit counts in the diagram’s own subtitle) — no plot window, no image file, an SVG string served by the product’s model-diagram page.

Procedure 3: Recalling a Neural Network with compute() and Understanding Performance

compute() is used instead of predict() to recall a trained network, and returns a list rather than a plain vector or matrix. The recall function is unforgiving of a data frame that still contains the dependent variable — passing it produces a %*% non-conformable-arguments error — so the input frame must be subset to the independent variables first. Because this is a numeric prediction, not classification, correlation between the recalled output and the actual dependent variable is the performance measure.

ComputedModel <- compute(NeuralNetworkFourByOne,
  FDX[, c("Skew_3", "Max_4", "PointStep_16", "Close_3", "Close_4",
          "PointStep_17_ZScore", "PointStep_15", "TypicalValue_4", "Range_4", "Range_2")])
cat("class(ComputedModel):", class(ComputedModel), "\n")
cat("names(ComputedModel):", paste(names(ComputedModel), collapse = ", "), "\n")

FDXPredeictions <- ComputedModel$net.result
cat("class(FDXPredeictions):", paste(class(FDXPredeictions), collapse = " "), "\n")
cat("dim(FDXPredeictions):", paste(dim(FDXPredeictions), collapse = " x "), "\n")

correlation <- cor(FDXPredeictions, FDX$Dependent, use = "complete", method = "pearson")
cat("cor(FDXPredeictions, FDX$Dependent):", correlation, "\n")

Console output:

class(ComputedModel): list
names(ComputedModel): neurons, net.result
class(FDXPredeictions): matrix array
dim(FDXPredeictions): 400 x 1
cor(FDXPredeictions, FDX$Dependent): 0.928771

In this neuralnet version (1.44.2), compute()$net.result comes back already as a plain numeric matrix, not a list needing unlist() first — that extra step in the original guide’s description no longer applies to a compute() call, only to net.result read directly off the trained object itself (see procedure 6). The single-hidden-layer network here reaches a Pearson correlation of 0.93 between prediction and actual on this synthetic data, a large uplift over using any one input variable alone.

Tool equivalent: none for compute() itself — recall is not a separate step the caller invokes. The trained AdvancedAnalyticsNeuralNetworkRegressorTrainingService run already scores its own held-out validation and test splits during training and records the resulting metrics (including a correlation/R² style fit measure) as part of the run’s evidence, readable once the run’s status is Succeeded; there is no live “call compute on arbitrary rows” tool in this module’s scope.

Procedure 4: Training a Deeper Neural Network

Where procedure 1 used a single hidden layer, deep learning uses several. A vector passed to hidden= gives one entry per hidden layer, each entry being that layer’s processing-element count. This example keeps the same ten inputs and trains a three-hidden-layer network of 8, 6 and 4 processing elements. Deep networks are lazier and costlier to recall — which matters in high-throughput scoring — so this extra depth is only worth it if it demonstrably improves the fit.

set.seed(13)
NeuralNetworkDeep <- neuralnet(Dependent ~ Skew_3 + Max_4 + PointStep_16 + Close_3 +
  Close_4 + PointStep_17_ZScore + PointStep_15 + TypicalValue_4 + Range_4 + Range_2,
  data = FDX, hidden = c(8, 6, 4))

cat("Converged in", NeuralNetworkDeep$result.matrix["steps", 1], "steps\n")
cat("Error (SSE):", NeuralNetworkDeep$result.matrix["error", 1], "\n")
cat("Weight matrices (3 hidden layers, output layer):\n")
for (i in seq_along(NeuralNetworkDeep$weights[[1]])) {
  w <- NeuralNetworkDeep$weights[[1]][[i]]
  cat("  matrix", i, "dim:", paste(dim(w), collapse = " x "), "\n")
}

ComputedModelDeep <- compute(NeuralNetworkDeep,
  FDX[, c("Skew_3", "Max_4", "PointStep_16", "Close_3", "Close_4",
          "PointStep_17_ZScore", "PointStep_15", "TypicalValue_4", "Range_4", "Range_2")])
FDXPredeictionsDeep <- ComputedModelDeep$net.result
correlation_deep <- cor(FDXPredeictionsDeep, FDX$Dependent, use = "complete", method = "pearson")
cat("cor(FDXPredeictionsDeep, FDX$Dependent):", correlation_deep, "\n")

Console output:

Converged in 2539 steps
Error (SSE): 0.6727612
Weight matrices (3 hidden layers, output layer):
  matrix 1 dim: 11 x 8
  matrix 2 dim: 9 x 6
  matrix 3 dim: 7 x 4
  matrix 4 dim: 5 x 1
cor(FDXPredeictionsDeep, FDX$Dependent): 0.9554449

The deeper network improved correlation from 0.93 to 0.96 and converged in fewer steps, at the cost of four weight matrices to reason about instead of two.

Tool equivalent: none for the multi-layer topology itself. AdvancedAnalyticsNeuralNetworkRegressorTrainingDto (and its classifier counterpart) expose exactly one hidden layer, sized by a single HiddenUnits value from 1 to 128 — there is no vector-of-layers option, so a three-hidden-layer network as trained here has no shipped equivalent. Widening HiddenUnits within that one layer is the closest supported lever; going deeper than one hidden layer stays R-only.

Procedure 5: Training a Classification Model

Neural networks are universal classifiers as well as regressors, but every input and output must be numeric — categorical inputs need pivoting to 0/1 first, since neuralnet() cannot interpret factors directly. This example trains a network with one hidden layer of four processing elements against ten variables known to correlate with a binary fraud outcome, on a data set with a deliberately balanced fraud rate.

set.seed(98)
n2 <- 900
FraudRisk <- data.frame(
  Count_Unsafe_Terminals_1_Day = rpois(n2, 1.2),
  High_Risk_Country = rbinom(n2, 1, 0.3),
  Foreign = rbinom(n2, 1, 0.35),
  Authenticated = rbinom(n2, 1, 0.6),
  Has_Been_Abroad = rbinom(n2, 1, 0.25),
  Transaction_Amt = round(rlnorm(n2, meanlog = 4, sdlog = 1), 2),
  Different_Country_Transactions_1_Week = rpois(n2, 0.8),
  Different_Decline_Reasons_1_Day = rpois(n2, 0.6),
  Count_Transactions_Declined_1_Day = rpois(n2, 1.0),
  Count_In_Person_1_Day = rpois(n2, 2.0)
)

logit <- with(FraudRisk, -2.2 +
  0.6 * Count_Unsafe_Terminals_1_Day +
  1.4 * High_Risk_Country +
  0.9 * Foreign -
  1.1 * Authenticated +
  0.8 * Has_Been_Abroad +
  0.0008 * Transaction_Amt +
  0.7 * Different_Country_Transactions_1_Week +
  0.5 * Different_Decline_Reasons_1_Day +
  0.4 * Count_Transactions_Declined_1_Day -
  0.15 * Count_In_Person_1_Day)
p <- 1 / (1 + exp(-logit))
FraudRisk$Dependent <- rbinom(n2, 1, p)

cat("Row count:", nrow(FraudRisk), "\n")
cat("Fraud rate:", mean(FraudRisk$Dependent), "\n")

normalise <- function(x) (x - min(x)) / (max(x) - min(x))
predictors <- c("Count_Unsafe_Terminals_1_Day", "High_Risk_Country", "Foreign", "Authenticated",
  "Has_Been_Abroad", "Transaction_Amt", "Different_Country_Transactions_1_Week",
  "Different_Decline_Reasons_1_Day", "Count_Transactions_Declined_1_Day", "Count_In_Person_1_Day")
FraudRiskScaled <- FraudRisk
FraudRiskScaled[predictors] <- lapply(FraudRisk[predictors], normalise)

FraudRiskNeuralNetwork <- neuralnet(Dependent ~ Count_Unsafe_Terminals_1_Day +
  High_Risk_Country + Foreign + Authenticated + Has_Been_Abroad + Transaction_Amt +
  Different_Country_Transactions_1_Week + Different_Decline_Reasons_1_Day +
  Count_Transactions_Declined_1_Day + Count_In_Person_1_Day, data = FraudRiskScaled, hidden = 4)

cat("Converged in", FraudRiskNeuralNetwork$result.matrix["steps", 1], "steps\n")
cat("Error (SSE):", FraudRiskNeuralNetwork$result.matrix["error", 1], "\n")

Console output:

Row count: 900
Fraud rate: 0.45
Converged in 8407 steps
Error (SSE): 72.8707

The predictor columns were min-max scaled to 0-1 before training (FraudRiskScaled) — neuralnet converges far more reliably against similarly-scaled inputs than against a mix of raw counts and a log-normal transaction amount.

Tool equivalent: Jube.Service.Repository.AnalyticsTrainingRun.AdvancedAnalyticsNeuralNetworkClassifierTrainingService / tool SubmitNeuralNetworkClassifierTraining on AdvancedAnalyticsNeuralNetworkClassifierTrainingDto — the classifier counterpart of procedure 1’s mapping: one hidden layer sized by HiddenUnits, binary Labels instead of a continuous outcome, the same epochs/learning-rate/standardisation options, plus a Calibrate flag that Platt-calibrates the network’s raw output to a genuine probability on the validation split (a step this R procedure has no equivalent of at all — the raw neuralnet output below is not a calibrated probability, see procedure 6).

Procedure 6: Activating a Classification Model and Appraising Performance

Recalling the trained classifier returns a numeric value per record; thresholding it at 0.5 and tabulating against the actual outcome produces a confusion matrix to appraise the classifier.

FraudRiskPredictions <- FraudRiskNeuralNetwork$net.result
cat("class(FraudRiskPredictions):", class(FraudRiskPredictions), "\n")
cat("length(FraudRiskPredictions):", length(FraudRiskPredictions), "\n")
cat("head(FraudRiskPredictions[[1]]):\n")
print(head(FraudRiskPredictions[[1]]))

IsFraud <- ifelse(unlist(FraudRiskPredictions) > 0.5, 1, 0)

confusion <- table(Actual = FraudRisk$Dependent, Predicted = IsFraud)
print(confusion)

accuracy <- sum(diag(confusion)) / sum(confusion)
fraud_recall <- confusion["1", "1"] / sum(confusion["1", ])
cat(sprintf("Overall accuracy: %.3f\n", accuracy))
cat(sprintf("Of %d actual fraud cases, %d were classified correctly (%.1f%% recall)\n",
  sum(confusion["1", ]), confusion["1", "1"], 100 * fraud_recall))

Console output:

class(FraudRiskPredictions): list
length(FraudRiskPredictions): 1
head(FraudRiskPredictions[[1]]):
            [,1]
[1,]  0.39418964
[2,] -0.07983988
[3,]  0.57514640
[4,]  0.01905772
[5,]  1.10198154
[6,]  0.96596537
      Predicted
Actual   0   1
     0 384 111
     1 122 283
Overall accuracy: 0.741
Of 405 actual fraud cases, 283 were classified correctly (69.9% recall)

Two things worth being plain about, from what was actually captured rather than what the original guide described: reading $net.result straight off the trained object (rather than through a compute() call, as procedure 3 did) returns a list of one matrix — one entry per training repetition — so [[1]] is needed before head() shows anything useful; and because neuralnet() was called here with its default linear.output = TRUE, the raw values are not squashed into [0, 1] at all (row 5 above is 1.10, row 2 is -0.08) — they only happen to cluster near that range because the target itself is binary. The 0.5 threshold still produces a sensible confusion matrix (table() used here in place of the original’s gmodels::CrossTable, which needs a package outside this guide’s set for no more than a cosmetic difference in how the same cross-tabulation prints), but nothing here should be read as “neuralnet returns calibrated probabilities” — it does not, by default. Compare AdvancedAnalyticsNeuralNetworkClassifierTrainingDto’s Calibrate option below.

Tool equivalent: tool "EvaluateNeuralNetworkClassifier" (Jube.Service.Agent.AdvancedAnalyticsTools.ModelEvaluation.ModelEvaluationTools) fits and evaluates a network ad hoc against held-out rows in one call, returning ROC AUC, a confusion table and calibration evidence directly — the path an agent reaches for to answer “how well would this predict” without training or promoting anything. Jube.Engine.AdvancedAnalytics.Classification.NeuralNetworkClassifierRecall (model kind "NeuralNetworkClassifierPipeline") is a separate, later path: the live scoring used once a version is actually promoted, with no ad hoc tool of its own — it recalls a single row through the stored input weights, hidden biases, output weights and output bias, applying the same Platt calibration fitted at training time so the value returned really is a probability, not a raw sigmoid activation. Held-out confusion-matrix-style evidence (and the ROC curve behind it) is computed once, during training, by the model’s own evaluation step and stored with the run rather than assembled ad hoc afterwards; Jube.Charting.Charts.CalibrationChartBuilder and the ROC chart in Jube.Charting render that evidence rather than a hand-built table()/CrossTable().