The MLP Architecture: Activations & Initialization

Introduction

Welcome to the first lesson of "The MLP Architecture: Activations & Initialization"! I'm excited to continue our neural network journey with you. In our previous course, neural network fundamentals: neurons and layers, we built the foundations of neural networks by implementing individual neurons, adding activation functions, and combining neurons into a single DenseLayer capable of forward propagation.

Today, we're taking a significant step forward by learning how to stack multiple layers together to create a multi-layer perceptron (MLP). MLPs are the fundamental architecture behind many neural network applications and represent the point where our implementations truly become "deep learning."

By the end of this lesson, you'll have created a fully functional MLP capable of processing data through multiple layers, bringing us much closer to solving real-world problems. Let's dive in!

Recap: Our Neural Network Building Blocks

Before we dive into multi-layer perceptrons, let's quickly refresh the core components we built in our previous course. Our foundation consists of two key elements:

  1. The sigmoid activation function, which transforms linear inputs into non-linear outputs between 0 and 1.
    R
    sigmoid <- function(x) {
      return(1 / (1 + exp(-x)))
    }
  2. The DenseLayer, which creates a fully connected layer of neurons.

A note on implementation: In our previous course, we implemented DenseLayer as a function that returned a list of functions. While that approach worked well, in this course we'll transition to using R6 classes for our neural network components. R6 is R's object-oriented programming system that provides cleaner syntax for creating objects with methods and mutable state — features that become increasingly valuable as our networks grow more complex. This approach will make our code more organized and easier to extend as we build more sophisticated architectures.

Here's our DenseLayer implemented as an R6 class:

R
library(R6)

DenseLayer <- R6Class("DenseLayer",
  public = list(
    # Public fields
    weights = NULL,
    biases = NULL,
    n_inputs = NULL,
    n_neurons = NULL,
    output = NULL,
    activation_fn = NULL,
    activation_fn_name = NULL,
    
    # Constructor
    initialize = function(n_inputs, n_neurons) {
      self$n_inputs <- n_inputs
      self$n_neurons <- n_neurons
      
      # Weights: (n_inputs, n_neurons), Biases: (1, n_neurons)
      self$weights <- matrix(runif(n_inputs * n_neurons, 0, 0.1), 
                           nrow = n_inputs, ncol = n_neurons)
      self$biases <- matrix(0, nrow = 1, ncol = n_neurons)
      self$output <- NULL
      
      # For now, DenseLayer defaults to Sigmoid activation
      self$activation_fn <- sigmoid
      self$activation_fn_name <- "sigmoid"
    },
    
    # Forward method
    forward = function(inputs) {
      # Perform a forward pass through the dense layer
      weighted_sum <- inputs %*% self$weights + rep(self$biases, each = nrow(inputs))
      self$output <- self$activation_fn(weighted_sum)
      return(self$output)
    }
  )
)

Our DenseLayer performs three essential operations:

  • Initializes weights and biases (note how we're currently using runif(0, 0.1) for weights — we'll explore why we do it as well as better initialization strategies later in this course).
  • Stores layer dimensions and activation function.
  • Performs the forward pass by computing the weighted sum with proper bias broadcasting and applying activation.

This single layer is powerful, but the real magic happens when we combine multiple layers together — which is exactly what we'll do today by building our multi-layer perceptron!

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