Skip to content
Home
Neural Networks Basics: From Perceptrons to Deep Learning

Neural Networks Basics: From Perceptrons to Deep Learning

Machine Learning Machine Learning 7 min read 1484 words Beginner ExcellentWiki Editorial Team

Neural networks are the foundation of modern deep learning. They are computational systems inspired by biological neurons, capable of learning complex patterns from data. This guide covers the fundamentals from the simplest building block to multi-layer architectures.

The Perceptron

The perceptron is the simplest neural network unit, introduced by Frank Rosenblatt in 1958. It takes multiple inputs, multiplies each by a weight, sums them, adds a bias, and passes the result through an activation function.

import numpy as np

class Perceptron:
    def __init__(self, learning_rate=0.01, n_iterations=100):
        self.lr = learning_rate
        self.n_iterations = n_iterations

    def fit(self, X, y):
        n_samples, n_features = X.shape
        self.weights = np.zeros(n_features)
        self.bias = 0

        for _ in range(self.n_iterations):
            for idx, x_i in enumerate(X):
                linear_output = np.dot(x_i, self.weights) + self.bias
                y_predicted = self._step_function(linear_output)
                update = self.lr * (y[idx] - y_predicted)
                self.weights += update * x_i
                self.bias += update

    def predict(self, X):
        linear_output = np.dot(X, self.weights) + self.bias
        return self._step_function(linear_output)

    def _step_function(self, x):
        return np.where(x >= 0, 1, 0)

The perceptron can only learn linearly separable patterns (like AND and OR gates) but fails on XOR. This limitation, demonstrated by Minsky and Papert in 1969, led to the first AI winter.

Multi-Layer Perceptron

Adding hidden layers between input and output creates a multi-layer perceptron (MLP), which can learn non-linear decision boundaries:

import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(x):
    return x * (1 - x)

class NeuralNetwork:
    def __init__(self, input_size, hidden_size, output_size, learning_rate=0.1):
        self.lr = learning_rate
        self.weights_input_hidden = np.random.randn(input_size, hidden_size) * 0.01
        self.weights_hidden_output = np.random.randn(hidden_size, output_size) * 0.01
        self.bias_hidden = np.zeros((1, hidden_size))
        self.bias_output = np.zeros((1, output_size))

    def forward(self, X):
        self.hidden_input = np.dot(X, self.weights_input_hidden) + self.bias_hidden
        self.hidden_output = sigmoid(self.hidden_input)
        self.output = np.dot(self.hidden_output, self.weights_hidden_output) + self.bias_output
        self.predicted = sigmoid(self.output)
        return self.predicted

    def backward(self, X, y, output):
        # Calculate error
        output_error = y - output
        output_delta = output_error * sigmoid_derivative(output)

        hidden_error = output_delta.dot(self.weights_hidden_output.T)
        hidden_delta = hidden_error * sigmoid_derivative(self.hidden_output)

        # Update weights and biases
        self.weights_hidden_output += self.hidden_output.T.dot(output_delta) * self.lr
        self.bias_output += np.sum(output_delta, axis=0, keepdims=True) * self.lr
        self.weights_input_hidden += X.T.dot(hidden_delta) * self.lr
        self.bias_hidden += np.sum(hidden_delta, axis=0, keepdims=True) * self.lr

Activation Functions

Activation functions introduce non-linearity, enabling networks to learn complex patterns:

import numpy as np

def relu(x):
    """Rectified Linear Unit — most common in hidden layers."""
    return np.maximum(0, x)

def leaky_relu(x, alpha=0.01):
    """Leaky ReLU — allows small negative values."""
    return np.where(x > 0, x, alpha * x)

def tanh(x):
    """Hyperbolic tangent — outputs in (-1, 1), zero-centered."""
    return np.tanh(x)

def softmax(x):
    """Softmax — converts logits to probabilities (multiclass classification)."""
    exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
    return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

# Common patterns:
# Hidden layers: ReLU (default choice)
# Output (regression): linear (no activation)
# Output (binary classification): sigmoid
# Output (multiclass): softmax

Backpropagation

Backpropagation is the algorithm that makes deep learning possible. It computes gradients of the loss function with respect to each weight using the chain rule:

Forward pass:
  h1 = sigmoid(x @ W1 + b1)
  h2 = sigmoid(h1 @ W2 + b2)
  y_pred = sigmoid(h2 @ W3 + b3)

Backward pass:
  dL/dW3 = h2.T @ (y_pred - y) * sigmoid_derivative(y_pred)
  dL/dW2 = h1.T @ (dL/dh2 * sigmoid_derivative(h2))
  dL/dW1 = x.T @ (dL/dh1 * sigmoid_derivative(h1))

Update:
  W = W - learning_rate * dL/dW

The gradient tells us the direction of steepest increase in the loss. We move weights in the opposite direction to reduce the loss. This is gradient descent applied to neural networks.

Loss Functions

import numpy as np

def mse(y_true, y_pred):
    """Mean squared error — regression tasks."""
    return np.mean((y_true - y_pred) ** 2)

def binary_crossentropy(y_true, y_pred):
    """Binary cross-entropy — binary classification."""
    y_pred = np.clip(y_pred, 1e-15, 1 - 1e-15)
    return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))

def categorical_crossentropy(y_true, y_pred):
    """Categorical cross-entropy — multiclass classification."""
    y_pred = np.clip(y_pred, 1e-15, 1 - 1e-15)
    return -np.mean(np.sum(y_true * np.log(y_pred), axis=-1))

def hinge(y_true, y_pred):
    """Hinge loss — SVM-style classification."""
    return np.mean(np.maximum(0, 1 - y_true * y_pred))

Vanishing and Exploding Gradients

Deep networks (many layers) suffer from gradient problems:

  • Vanishing gradients — gradients become extremely small, early layers learn very slowly
  • Exploding gradients — gradients grow exponentially, causing unstable updates

Solutions include:

  • ReLU activation (no saturation for positive values)
  • Batch normalization (normalizes layer outputs)
  • Residual connections (skip connections in ResNet)
  • Careful weight initialization (Xavier/Glorot, He initialization)
  • Gradient clipping (cap gradients at a threshold)

Modern Architectures

import torch.nn as nn

class SimpleCNN(nn.Module):
    """Convolutional Neural Network for image classification."""
    def __init__(self, num_classes=10):
        super().__init__()
        self.conv_layers = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(128 * 4 * 4, 512),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(512, num_classes),
        )

    def forward(self, x):
        x = self.conv_layers(x)
        x = self.classifier(x)
        return x

Real-World Implementation Tips

Production Considerations

When moving from development to production, several factors become critical. Error handling should be comprehensive — every external call (database, API, file system) should have proper error checking, logging, and retry logic where appropriate. Performance monitoring through metrics and structured logging helps identify bottlenecks before they affect users.

Testing Strategy

A thorough testing approach combines multiple levels:

  • Unit tests verify individual functions and methods in isolation
  • Integration tests validate that components work together correctly
  • Edge case tests cover boundary conditions, empty inputs, and error states
  • Performance tests ensure the system meets latency and throughput requirements

Test data should be realistic but controlled. Mock external dependencies to make tests fast and deterministic. Aim for tests that are independent, repeatable, and fast enough to run on every commit.

Documentation

Good documentation is essential for maintainable code. Follow these principles:

  • Document the “why” not just the “what” — explain design decisions
  • Keep examples up to date with the code
  • Include usage examples for public APIs
  • Document configuration options and their defaults
  • Explain error conditions and recovery strategies

Security Best Practices

Security should be considered throughout development:

  • Validate all inputs at system boundaries
  • Use parameterized queries for database access
  • Store secrets in environment variables or secret managers
  • Keep dependencies updated to patch vulnerabilities
  • Apply the principle of least privilege

Performance Optimization

Optimize based on measured data, not assumptions:

  1. Profile before optimizing — identify actual bottlenecks
  2. Measure the impact of each change
  3. Consider the trade-off between speed and readability
  4. Cache expensive operations with appropriate invalidation
  5. Use connection pooling for database and network resources

Monitoring and Observability

Production systems need visibility:

  • Structured logging with correlation IDs for request tracking
  • Metrics for latency, throughput, error rates, and resource usage
  • Health check endpoints for load balancers and orchestration
  • Distributed tracing for request flows across services
  • Alerts for anomaly detection based on baselines

These patterns apply across all programming languages and frameworks. The specific implementation varies, but the principles remain consistent.

FAQ

Q: How many layers and neurons should I use? A: Start simple — one or two hidden layers. Add complexity only if validation performance is insufficient. The right size depends on data complexity and dataset size.

Q: How do I know if my network is overfitting? A: Training loss decreases but validation loss stagnates or increases. Solutions: more data, regularization (L1/L2/dropout), simpler architecture, or early stopping.

Q: What is the difference between a neuron and a perceptron? A: A perceptron uses a step activation (binary output). A neuron typically uses a continuous activation (sigmoid, ReLU, tanh) that supports gradient-based learning.

Q: Why are deeper networks better? A: Depth allows hierarchical feature learning — early layers learn simple patterns, later layers compose them into complex concepts. Deep networks can represent some functions exponentially more efficiently than shallow ones.

Q: What is batch normalization? A: Batch normalization normalizes the output of each layer to have zero mean and unit variance. It stabilizes training, allows higher learning rates, and provides mild regularization.

Q: What optimizer should I use? A: Adam is the default choice for most tasks. SGD with momentum is common for computer vision. AdamW (Adam with decoupled weight decay) is standard for transformers.

Neural Network Training Techniques

Activation Functions

# Common activation functions and when to use them
# ReLU (default for hidden layers)
def relu(x):
    return max(0, x)
    # Pros: No vanishing gradient, computationally cheap
    # Cons: Dying ReLU (neurons can get stuck at 0)

# Leaky ReLU (fixes dying ReLU)
def leaky_relu(x, alpha=0.01):
    return x if x > 0 else alpha * x

# Swish (often outperforms ReLU in deeper networks)
def swish(x):
    return x * tf.sigmoid(x)

# Softmax (multi-class output layer)
def softmax(x):
    exp_x = np.exp(x - np.max(x))
    return exp_x / exp_x.sum()

Weight Initialization

# Xavier/Glorot uniform (good for tanh/sigmoid)
tf.initializers.GlorotUniform()

# He normal (recommended for ReLU)
tf.initializers.HeNormal()

# Proper initialization prevents vanishing/exploding gradients
# and leads to faster convergence

Regularization Techniques

# Dropout — randomly drop neurons during training
model.add(tf.keras.layers.Dropout(0.5))
# At test time, all neurons are active but scaled

# Batch Normalization — normalize layer inputs
model.add(tf.keras.layers.BatchNormalization())

# L2 Regularization — penalize large weights
model.add(tf.keras.layers.Dense(128, kernel_regularizer='l2'))

For a comprehensive overview, read our article on Deep Learning Guide.

For a comprehensive overview, read our article on Ensemble Methods Guide.

Section: Machine Learning 1484 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top