Introduction

Welcome to the final lesson of "Building and Applying Your Neural Network Library"! Congratulations on making it this far — you've accomplished something truly remarkable. Over the course of this path, you've built a complete, modular neural network library from scratch, learning the inner workings of layers, activations, optimizers, loss functions, and the orchestration that brings them all together. You've also mastered the essential data preparation techniques needed for real-world machine learning applications.

Today, we're going to experience the incredible satisfaction of seeing all your hard work come together. We'll use our custom-built neural network library to tackle a real regression problem: predicting California housing prices. You'll see how the modular architecture you've carefully constructed makes it surprisingly straightforward to define complex neural networks, train them efficiently, and evaluate their performance on real data.

This lesson represents the culmination of your journey — the moment when theory meets practice and your carefully crafted code proves its worth on a meaningful problem. Let's put your neural network library to the ultimate test!

Setting Up the Data

Let's start by importing our components and setting up the data preprocessing pipeline. Since you mastered data preparation in the previous lesson, we'll handle this efficiently and effortlessly using JavaScript tools.

We'll use the fs module to load the data, papaparse to parse CSV, and our own trainTestSplit and standardScaler functions for splitting and scaling.

To speed up training, we'll randomly select 1,000 samples from the dataset.

import * as math from 'mathjs';
import seedrandom from 'seedrandom';
import fs from 'fs';
import Papa from 'papaparse';
import { trainTestSplit, standardScaler } from './main.js'; // Adjust path as needed

// 1. Load the California Housing dataset from CSV
const csv = fs.readFileSync('data/california_housing.csv', 'utf8');
const parsed = Papa.parse(csv, {
    header: true,
    dynamicTyping: true,
    skipEmptyLines: true
});

// Extract feature names and target
const allColumns = Object.keys(parsed.data[0]);
const featureNames = allColumns.slice(0, -1);
const targetName = allColumns[allColumns.length - 1];

// --- SUBSAMPLE DATA FOR FASTER TRAINING ---
const SAMPLE_SIZE = 1000;
const totalRows = parsed.data.length;
const random = seedrandom('subsample'); // For reproducibility
const indices = Array.from({ length: totalRows }, (_, i) => i);
// Shuffle indices
for (let i = indices.length - 1; i > 0; i--) {
    const j = Math.floor(random() * (i + 1));
    [indices[i], indices[j]] = [indices[j], indices[i]];
}
const selectedIndices = indices.slice(0, SAMPLE_SIZE);

// Build X and y using only the selected indices
const X = [];
const y = [];
for (const idx of selectedIndices) {
    const row = parsed.data[idx];
    X.push(featureNames.map(f => row[f]));
    y.push([row[targetName]]);
}

// 2. Split data into training and testing sets
const { XTrain, XTest, yTrain, yTest } = trainTestSplit(X, y, 0.2, 42);

// 3. Apply feature scaling (Standardization)
const scalerX = standardScaler(XTrain);
const XTrainScaled = scalerX.data;
const XTestScaled = scalerX.transform(XTest);

const scalerY = standardScaler(yTrain);
const yTrainScaled = scalerY.data;
const yTestScaled = scalerY.transform(yTest);

const numFeatures = XTrainScaled[0].length; // Will be 8 for this dataset
Defining the Neural Network Architecture

Now comes the exciting part — defining our neural network architecture. We'll create a multi-layer perceptron (MLP) with two hidden layers, perfectly suited for this regression task.

import { SequentialModel } from './models/sequential.js';
import { DenseLayer } from './layers/dense.js';

// Define the MLP architecture using SequentialModel
const model = new SequentialModel();
model.add(new DenseLayer(numFeatures, 64, 'relu'));
model.add(new DenseLayer(64, 32, 'relu'));
model.add(new DenseLayer(32, 1, 'linear')); // Linear output for regression

This architecture represents a sophisticated neural network design. The first hidden layer takes our 8 input features and expands them to 64 neurons, allowing the network to learn complex feature combinations. The ReLU activation introduces nonlinearity, enabling the network to model complex relationships between housing features and prices.

The second hidden layer gradually reduces the dimensionality from 64 to 32 neurons, creating a funnel-like architecture that progressively distills the learned features into more refined representations. Finally, the output layer uses linear activation to produce a single continuous value — the predicted house price.

Compiling the Model

Now we'll compile our model with appropriate training configurations so that it's ready to begin the learning process:

// Compile the model
model.compile({
    optimizerName: 'sgd',
    learningRate: 0.005,
    lossName: 'mse'
});

The compilation step configures our training setup. We're using SGD (Stochastic Gradient Descent) with a learning rate of 0.005, which is slightly smaller than in our previous examples. Real-world datasets often benefit from more conservative learning rates that allow for stable, consistent learning across the diverse feature landscape.

When you run this code, you'll see:

Model compiled with optimizer: sgd (lr: 0.005), loss: mse
Training the Network

Now for the moment of truth — training our neural network on real data:

// Convert arrays to mathjs matrices for training
const XTrainMatrix = math.matrix(XTrainScaled);
const yTrainMatrix = math.matrix(yTrainScaled);

console.log("\nTraining the model...");
model.fit(XTrainMatrix, yTrainMatrix, {
    epochs: 200,
    batchSize: 32,
    verbose: 1
});

This single method call triggers a sophisticated training process. Your model will process 200 epochs of training, where each epoch involves multiple mini-batches of 32 samples each. The verbose output shows the learning progress:

Training the model...
Epoch    1/200, Loss: 1.000209
Epoch   20/200, Loss: 0.311592
Epoch   40/200, Loss: 0.265016
Epoch   60/200, Loss: 0.245686
Epoch   80/200, Loss: 0.229533
Epoch  100/200, Loss: 0.222904
Epoch  120/200, Loss: 0.218033
Epoch  140/200, Loss: 0.212335
Epoch  160/200, Loss: 0.208022
Epoch  180/200, Loss: 0.204843
Epoch  200/200, Loss: 0.201297
Training finished.

The decreasing loss values demonstrate successful learning! Your network started with a loss of 1.000 and steadily improved to 0.201 — a clear sign that it's learning meaningful patterns in the housing data. The gradual, consistent decrease indicates stable training without the erratic behavior that can plague poorly configured networks.


Making Predictions and Evaluating Performance

With our model trained, we can now make predictions on the test set and evaluate how well it generalizes to unseen data:

import { mseLoss } from './losses/mse.js';

// Convert test data to mathjs matrix
const XTestMatrix = math.matrix(XTestScaled);
const yTestMatrix = math.matrix(yTestScaled);

// Make predictions on the test set
const yPredScaled = model.predict(XTestMatrix);

// Evaluate performance on scaled data
const testLossScaled = mseLoss(yTestMatrix, yPredScaled);
console.log(`\nTest MSE (scaled): ${testLossScaled.toFixed(4)}`);

This produces our first evaluation metric:

Test MSE (scaled): 0.2083

The test MSE of 0.2083 is quite close to our final training loss of 0.201, which is excellent news! This similarity indicates that our model is generalizing well rather than overfitting to the training data. When test performance closely matches training performance, it suggests we've found genuine patterns rather than memorizing specific training examples.

However, this scaled MSE, while useful for training, doesn't give us an intuitive sense of prediction accuracy. Housing prices measured in standardized units don't mean much to us humans who think in dollars!

Interpreting Results in Real-World Terms

To truly understand our model's performance, we need to transform our predictions back to the original price scale. Our standardScaler object provides a mean and std for the target variable, so we can implement an inverse transform.

Recall that standardization transforms data as follows:

scaled = (original - mean) / std

To reverse this process (i.e., to get back to the original scale), we use:

original = scaled * std + mean

Here's an example implementation of the inverseTransform method for our standardScaler:

// Example implementation of inverseTransform for standardScaler
// yOriginal = yScaled * std + mean
function inverseTransform(scaledArray, mean, std) {
    return scaledArray.map(row => row.map((val, i) => val * std[i] + mean[i]));
}

// Usage with our scalerY object:
const yPredOriginalScale = inverseTransform(yPredScaled.toArray(), scalerY.mean, scalerY.std);

If your standardScaler object already provides an inverseTransform method, you can simply use:

const yPredOriginalScale = scalerY.inverseTransform(yPredScaled.toArray());

Now, let's compute the MSE in the original scale for a more interpretable performance metric:

// Compute MSE in original scale
const testLossOriginalScale = mseLoss(math.matrix(yTest), math.matrix(yPredOriginalScale));
console.log(`Test MSE (original scale): ${testLossOriginalScale.toFixed(4)}`);

This gives us:

Test MSE (original scale): 0.2785

Remember that our target values are in units of $100,000, so an MSE of 0.2785 corresponds to a typical prediction error of approximately $52,800 (the square root of 0.2785 × $100,000). For California housing prices, this represents reasonably accurate predictions!

Looking at Sample Predictions
Conclusion

Congratulations! You've successfully completed the entire journey of building and applying your own neural network library. What you've accomplished in this final lesson represents the true power of the modular, well-designed system you've constructed over the past five lessons. Your achievement is remarkable — you've taken raw California housing data and successfully trained a multi-layer neural network to predict house prices with meaningful accuracy.

The elegance of your solution demonstrates the value of good software design, with clean, readable code that seamlessly integrates data preprocessing, model definition, training, and evaluation. In the upcoming practice exercises, you'll have the opportunity to apply these skills hands-on, building confidence in your ability to tackle real-world machine learning problems with your custom-built neural network library.

Remember: For all practice exercises, we'll continue to use a 1,000-sample subsample of the dataset to keep training fast and interactive.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal