Introduction

Welcome back to lesson 2 of "Building and Applying Your Neural Network Library"! You've made excellent progress in this course. In our previous lesson, we successfully transformed our neural network code into a well-structured modular system by organizing our core components — dense layers and activation functions. We created clean import paths and established the foundation for a professional-grade neural network library.

Now we're ready to take the next crucial step: modularizing our training components. As you may recall from our previous courses, training a neural network involves two key components beyond the layers themselves: loss functions (which measure how well our network is performing) and optimizers (which update the network's weights based on the gradients we compute). Currently, these components are scattered throughout our training scripts, making them difficult to reuse and maintain.

In this lesson, we'll organize these training components into dedicated modules within our neuralnets directory structure. We'll create loss functions for our Mean Squared Error (MSE) loss and an optimizer class for our Stochastic Gradient Descent (SGD) optimizer. By the end of this lesson, you'll have a complete, modular training pipeline that demonstrates the power of good software architecture in machine learning projects.

Our Test Dataset: The XOR Problem

Before we dive into modularizing our training components, let's take a moment to understand the dataset we'll be using to test our library: the xor (exclusive or) problem. This is a classic toy problem in machine learning that serves as an excellent test case for neural networks because it's non-linearly separable — meaning a single linear classifier cannot solve it, but a simple multi-layer neural network can.

The xor problem consists of four data points with two binary inputs and one binary output. The output is 1 when exactly one of the inputs is 1, and 0 otherwise. This creates the pattern: [0,0] → 0, [0,1] → 1, [1,0] → 1, [1,1] → 0. This is typically treated as a classification problem, but we can frame it as a regression task as well, which is what we'll do by using our mse loss. Despite its simplicity, if our network can learn xor, then we know our forward pass, backward pass, loss calculation, and optimization code are all functioning properly.

# XOR dataset setup
X_train <- matrix(c(0,0, 0,1, 1,0, 1,1), nrow=4, ncol=2, byrow=TRUE)
y_train <- matrix(c(0, 1, 1, 0), nrow=4, ncol=1)

In this setup, nrow=4 specifies we have 4 training samples, ncol=2 indicates each sample has 2 input features, and byrow=TRUE ensures the data is filled row-by-row — meaning each row represents a complete sample. This structure follows the standard convention in machine learning where rows are samples (data points) and columns are features (input dimensions).

XOR Dataset

While we're using xor for rapid development and testing in this lesson, later in the course we'll apply our complete neural network library to a real-world dataset — the california housing dataset — where we'll predict house prices based on various features like location, population, and median income.

Creating the Loss Functions Module

Let's start by organizing our loss functions into dedicated functions within our neuralnets package structure. We'll create MSE loss functions that are cleanly organized and easily importable. The key insight here is separating the mathematical operations from the training logic, creating a clean interface that makes our code more testable and allows us to easily add other loss functions in the future.

neuralnets/losses/mse.R:

mse_loss <- function(y_true, y_pred) {
  # Calculate Mean Squared Error loss
  return(mean((y_true - y_pred)^2))
}

mse_loss_derivative <- function(y_true, y_pred) {
  # Derivative of MSE loss w.r.t. y_pred
  # Normalize by batch size (number of samples)
  return(2 * (y_pred - y_true) / nrow(y_true))
}

To make these functions available throughout our package, we create an initialization file that sources our loss functions:

neuralnets/losses/init.R:

source("neuralnets/losses/mse.R")
Creating the Optimizers Module
Package Initialization System

To tie all our modular components together, we create a main initialization file that loads all our modules. This creates a clean API similar to Python's __init__.py imports, making our components easily accessible throughout the package.

neuralnets/init.R:

source("neuralnets/layers/init.R")
source("neuralnets/activations/init.R")
source("neuralnets/losses/init.R")
source("neuralnets/optimizers/init.R")

# Make key components available at package level for easy access
# This creates a clean API similar to Python's __init__.py imports
Building a Complete Training Pipeline
Understanding Our Modular Architecture

Our neural network library now follows a clean modular architecture:

neuralnets/
├── init.R                 # Main package initialization
├── main.R                 # Training pipeline example
├── layers/
│   ├── init.R            # Layers module initialization
│   └── dense.R           # Dense layer implementation
├── activations/
│   ├── init.R            # Activations module initialization
│   └── functions.R       # Activation functions
├── losses/
│   ├── init.R            # Loss functions initialization
│   └── mse.R             # MSE loss implementation
└── optimizers/
    ├── init.R            # Optimizers initialization
    └── sgd.R             # SGD optimizer implementation

This structure provides clear separation of concerns, making our code more maintainable and allowing us to easily extend functionality. Each module has a specific purpose, and the initialization system ensures all components are properly loaded and available when needed.

Analyzing the Training Results

When we run our complete training pipeline, we can observe how our modular neural network library learns to solve the xor problem. The training output demonstrates both the learning process and the final performance of our network:

Training MLP for XOR problem for 1000 epochs with LR=0.5
Epoch    1/1000, Loss: 0.250048
Epoch  200/1000, Loss: 0.036447
Epoch  400/1000, Loss: 0.006105
Epoch  600/1000, Loss: 0.002965
Epoch  800/1000, Loss: 0.001892
Epoch 1000/1000, Loss: 0.001377

--- After Training ---
Input  | True Output | Predicted Output | Rounded Prediction
[0 0] |           0 |           0.0580 |                  0
[0 1] |           1 |           0.9745 |                  1
[1 0] |           1 |           0.9748 |                  1
[1 1] |           0 |           0.0291 |                  0

The output shows excellent convergence — our loss decreases steadily from 0.25 to just 0.0014 over 1000 epochs. More importantly, the final predictions demonstrate that our network has successfully learned the xor logic: it outputs values close to 0 for inputs [0,0] and [1,1], and values close to 1 for inputs [0,1] and [1,0]. When rounded, these predictions perfectly match the expected xor outputs, confirming that our modular library components work together seamlessly.

The key aspects of our training pipeline that make this possible:

  1. Modular Loss Functions: Our mse_loss and mse_loss_derivative functions provide clean interfaces for computing objectives and gradients
  2. Modular Optimizer: Our SGD class encapsulates the optimization logic and can be easily configured with different learning rates
  3. Clean Integration: All components work together through well-defined interfaces, making the training loop clear and maintainable
  4. Proper Weight Initialization: Using weight_init_scale=0.1 helps ensure stable training dynamics
Conclusion and Next Steps

Excellent work! We've successfully completed the next major step in building our neural network library by modularizing our training components. We've organized our loss functions and optimizers into dedicated modules with a clean directory structure, creating a separation of concerns that makes our code more maintainable, testable, and extensible. The successful training on the xor problem validates that our modular components work together seamlessly, setting us up perfectly for the next phase of development.

Looking ahead, we have two exciting milestones remaining in our library-building journey:

  • In our next lesson, we'll create a high-level Model orchestrator class that will provide an even cleaner interface for defining, training, and evaluating neural networks.
  • After that, we'll put our completed library to the test on the california housing dataset, demonstrating its capabilities on real-world regression problems.
  • But first, it's time to get hands-on! The upcoming practice section will give you the opportunity to extend and experiment with the modular components we've built, reinforcing your understanding through practical application.
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