This walkthrough builds a Sonar rock-or-mine classifier one part at a time. It uses logistic regression with the smallest possible neural-network architecture: inputs connected directly to one Sigmoid output.
Problem: Classify a sonar signal as a ROCK or a MINE.
Dataset: 208 sonar observations with 60 numerical signal features and one binary target.
| ML task | Binary classification |
| Level | Beginner |
| Dataset | 208 sonar observations |
| Inputs | 60 numerical sonar-signal features |
| Target | 0 for rock, 1 for mine |
| Model | 60 -> 1 sonar-rock-mine-classification network with a Sigmoid output |
| Default training | 200 CPU epochs |
| Related concepts | Machine Learning Basics, Logistic Regression, Neural Networks, Backpropagation, Deep Netts API, Model Development |
The complete flow is:
load -> inspect -> split -> scale -> build -> train -> evaluate -> save -> predict
Set the dataset path, model path, and expected data shape:
private static final String DATASET_PATH =
"src/main/resources/datasets/sonar.csv";
private static final String MODEL_PATH =
"models/sonar-rock-mine-classification.dnet";
private static final int NUM_INPUTS = 60;
private static final int NUM_OUTPUTS = 1;
Every CSV row contains 60 input values followed by one binary target.
Verify Java and disable CUDA before creating the model:
verifyJavaRuntime();
DeepNetts.getInstance().setUseCuda(false);
Output:
WARNING: Using incubator modules: jdk.incubator.vector
The example requires Java 25 with the Vector API enabled. This compact dataset and model run quickly on a CPU and do not require GPU configuration.
Read the numerical CSV file:
TabularDataSet<MLDataItem> dataSet =
DataSets.readCsv(
DATASET_PATH,
NUM_INPUTS,
NUM_OUTPUTS,
false,
","
);
The file has no header, so the fourth argument is false. Setting it to true would incorrectly discard the first observation as column names.
Confirm that all observations were loaded:
System.out.println("Samples: " + dataSet.size());
Output:
Samples: 208
The expected result is 208. This small check catches missing files and incorrect CSV-header configuration early.
Shuffle reproducibly and reserve 20% of the data for testing:
dataSet.shuffle(42);
TrainTestSplit split = dataSet.trainTestSplit(0.8);
DataSet<MLDataItem> trainingSet = split.getTrainingSet();
DataSet<MLDataItem> testSet = split.getTestSet();
The model learns only from the training set. The test set estimates how well it handles sonar observations that did not update its weights.
Fit Min-Max scaling on the training set and apply the same transformation to the test set:
MinMaxScaler scaler = DataSets.scaleToMinMax(trainingSet);
scaler.apply(testSet);
Fitting the scaler only on training data prevents information from the test set leaking into model development.
Create a network with no hidden layers:
FeedForwardNetwork neuralNet = FeedForwardNetwork.builder()
.addInputLayer(NUM_INPUTS)
.addOutputLayer(NUM_OUTPUTS, ActivationType.SIGMOID)
.lossFunction(LossType.CROSS_ENTROPY)
.randomSeed(42)
.build();
The architecture is:
60 numerical features -> 1 Sigmoid output
With no hidden layer, the network is logistic regression rather than a multilayer neural network. The Sigmoid output represents the estimated probability of the positive class.
Set the training parameters and fit the model:
neuralNet.getTrainer()
.setStopEpochs(200)
.setLearningRate(0.01f)
.setOptimizer(OptimizerType.SGD)
.setShuffle(true);
neuralNet.train(trainingSet);
Output:
TRAINING NEURAL NETWORK ------------------------------------------------------------------------ Initial Train Error:0.69915146 Epoch:1, Time:8ms, TrainError:0.6907358, TrainErrorChange:-0.008415639, TrainAccuracy:0.5903614 Epoch:2, Time:2ms, TrainError:0.6626929, TrainErrorChange:-0.028042912, TrainAccuracy:0.62650603 Epoch:3, Time:2ms, TrainError:0.64100194, TrainErrorChange:-0.021690965, TrainAccuracy:0.6626506 ... Epoch:200, Time:1ms, TrainError:0.31379443, TrainErrorChange:-0.0015623271, TrainAccuracy:0.87349397 TRAINING COMPLETED Total Training Time: 124ms ------------------------------------------------------------------------
SGD updates the output weights and bias to reduce Cross-Entropy loss. The middle epochs are omitted for readability. Across all 200 epochs, training error fell from about 0.70 to 0.31 and training accuracy reached about 87.3%.
Evaluate predictions on the held-out test set:
ClassificationMetrics metrics = (ClassificationMetrics) neuralNet.test(testSet);
System.out.println(metrics);
Output:
Class: out1 Total items: 41 True positive:15.0 Number of examples correctly classified as positive True negative:15.0 Number of examples correctly classified as negative False positive:2.0 Number of examples incorrectly classified as positive False negative:9.0 Number of examples incorrectly classified as negative Accuracy (ACC): 0.73170733 How often is a classifier correct in total (percent of correct classifications) Precision (PPV): 0.88235295 How often is a classifier correct when it gives positive prediction Recall: 0.625 When it is actually positive class, how often does it give positive prediction F1 Score: 0.7317073 Harmonic average (balance) of precision and recall Specificity (TNR): 0.88235295 When it is actually negative class, how often does it give negative prediction Fall-out (FPR): 0.11764706 How often it gives false positive prediction in total (percent of false positive predictions) False negative rate (FNR): 0.375 How often it gives false negative prediction in total (percent of false negative predictions)
The model correctly classified about 73.2% of the 41 held-out observations. Its high precision but lower recall means mine predictions are usually correct, while nine actual mines were missed.
Create the output directory and serialize the model:
Files.createDirectories(Path.of("models"));
FileIO.writeToFile(neuralNet, MODEL_PATH);
The example saves models/sonar-rock-mine-classification.dnet. Loading is intentionally not demonstrated while the Deep Netts 4 beta runtime is still finalizing model deserialization.
Use a real, already-preprocessed test observation, predict its mine probability, and compare the predicted and actual classes:
MLDataItem sonarExample = testSet.get(0);
float[] sonarSignal = sonarExample.getInput().getValues();
float actualTarget = sonarExample.getTargetOutput().getValues()[0];
float mineProbability = neuralNet.predict(sonarSignal)[0];
System.out.printf("Mine probability: %.4f%n", mineProbability);
System.out.println("Predicted: "
+ (mineProbability >= 0.5f ? "MINE" : "ROCK"));
System.out.println("Actual: "
+ (actualTarget >= 0.5f ? "MINE" : "ROCK"));
Output:
Mine probability: 0.8297 Predicted: MINE Actual: MINE
The Sigmoid output is above the 0.5 threshold, so this held-out observation
is correctly classified as MINE.
Pass this option to the Java process through your IDE's application run configuration or the command line:
--add-modules=jdk.incubator.vector
Deep Netts represents both models with FeedForwardNetwork. Their architectures distinguish them:
| Model | Architecture | Capacity |
|---|---|---|
| Logistic Regression | 60 -> 1 |
Learns a linear decision boundary |
| Spam Classification network | 5 -> 32 -> 16 -> 1 |
Learns non-linear feature combinations |
Logistic regression is a useful baseline: it is compact, fast, and easy to interpret before trying a model with hidden layers.
Sonar CSV with 208 observations
|
v
80/20 train/test split
|
v
Min-Max scaling
|
v
60 -> 1 sonar-rock-mine-classification model
|
v
Training and classification evaluation
|
v
Saved model and test-sample prediction
The numerical features represent reflected sonar energy measured across frequency bands.
Every sample belongs to one of two labels: rock or mine, so the model uses a single binary output.
Was this helpful?
Thank you!