Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Creating Own LLM›Basics of Neural Networks and Deep Learning

Foundations of Machine Learning and NLP

Basics of Neural Networks and Deep Learning

Neural networks are computational models inspired by the human brain, designed to recognize patterns in data. They matter because they enable machines to learn complex tasks like language understanding, which is foundational for building your own LLM. You reach for them when simpler models fail to capture the nuances in large-scale, unstructured data like text or images.

What is a Neuron? The Building Block of Neural Networks

A neuron in a neural network is a simple mathematical function that takes inputs, applies weights, adds a bias, and passes the result through an activation function. The key idea is that each input is multiplied by a weight, which represents its importance, and the bias allows the model to shift the output. This mimics how biological neurons fire signals based on the strength of incoming stimuli. The activation function (e.g., ReLU or sigmoid) introduces non-linearity, enabling the network to learn complex patterns. Without non-linearity, stacking multiple neurons would just result in a linear model, which cannot solve tasks like language understanding. For example, in natural language processing (NLP), a neuron might learn to associate the word 'king' with concepts like 'royalty' or 'monarchy' by adjusting its weights during training.

# A single neuron implementation with ReLU activation
import numpy as np

def neuron(inputs, weights, bias):
    # inputs: list of input values (e.g., word embeddings)
    # weights: list of weights corresponding to each input
    # bias: scalar value to shift the output
    
    # Calculate the weighted sum of inputs
    weighted_sum = np.dot(inputs, weights) + bias
    
    # Apply ReLU activation (max(0, x))
    output = max(0, weighted_sum)
    return output

# Example usage
inputs = [0.5, -1.2, 3.0]  # Example input values (e.g., features of a word)
weights = [0.1, -0.3, 0.8]  # Learned weights
bias = 0.5  # Learned bias

output = neuron(inputs, weights, bias)
print(f"Neuron output: {output}")  # Output depends on inputs and learned parameters

From Neurons to Layers: How Networks Learn Hierarchies

A single neuron can only learn simple patterns, but stacking neurons into layers allows the network to learn hierarchical representations. The first layer might detect low-level features (e.g., edges in images or individual words in text), while deeper layers combine these into higher-level abstractions (e.g., objects or phrases). This hierarchy is why deep learning excels at tasks like machine translation or sentiment analysis. Each layer’s output becomes the input for the next, and the weights are adjusted during training to minimize the difference between the network’s predictions and the true labels. For example, in NLP, the first layer might learn word embeddings, while deeper layers learn grammar or semantic relationships. The depth of the network allows it to model increasingly complex functions, but it also introduces challenges like vanishing gradients, which we’ll address later.

# A simple 2-layer neural network with ReLU activation
import numpy as np

def layer(inputs, weights, biases):
    # inputs: input vector (e.g., word embeddings)
    # weights: matrix where each row is a neuron's weights
    # biases: vector of biases for each neuron
    
    # Calculate weighted sum for each neuron
    weighted_sum = np.dot(inputs, weights.T) + biases
    
    # Apply ReLU activation
    output = np.maximum(0, weighted_sum)
    return output

# Example usage
inputs = np.array([0.5, -1.2, 3.0])  # Input layer (e.g., 3 features)

# Hidden layer: 4 neurons, each with 3 weights (one per input)
hidden_weights = np.array([
    [0.1, -0.3, 0.8],
    [-0.2, 0.4, 0.1],
    [0.5, -0.1, 0.3],
    [0.0, 0.2, -0.5]
])
hidden_biases = np.array([0.5, -0.1, 0.2, -0.3])

hidden_output = layer(inputs, hidden_weights, hidden_biases)

# Output layer: 2 neurons, each with 4 weights (one per hidden neuron)
output_weights = np.array([
    [0.2, -0.1, 0.4, 0.0],
    [-0.3, 0.5, 0.1, -0.2]
])
output_biases = np.array([0.1, -0.2])

final_output = layer(hidden_output, output_weights, output_biases)
print(f"Hidden layer output: {hidden_output}")
print(f"Final output: {final_output}")  # Output depends on learned parameters

Loss Functions: Measuring How Wrong the Network Is

A loss function quantifies how far the network’s predictions are from the true values, guiding the learning process. For classification tasks (e.g., sentiment analysis), cross-entropy loss is common because it heavily penalizes confident wrong predictions. For regression tasks (e.g., predicting word frequencies), mean squared error (MSE) is often used. The choice of loss function depends on the problem: cross-entropy works well for probabilities, while MSE is better for continuous values. During training, the network adjusts its weights to minimize the loss, typically using gradient descent. For example, if the network predicts a 0.9 probability for 'positive sentiment' but the true label is 'negative,' the cross-entropy loss will be high, pushing the network to correct its weights. The loss function is the compass that directs the network toward better performance.

# Cross-entropy loss for binary classification
import numpy as np

def cross_entropy_loss(y_true, y_pred):
    # y_true: true label (0 or 1)
    # y_pred: predicted probability (between 0 and 1)
    
    # Clip predictions to avoid log(0)
    y_pred = np.clip(y_pred, 1e-15, 1 - 1e-15)
    
    # Calculate loss
    loss = - (y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred))
    return loss

# Example usage
true_label = 1  # Positive sentiment
predicted_prob = 0.2  # Network predicts 20% probability of positive sentiment

loss = cross_entropy_loss(true_label, predicted_prob)
print(f"Cross-entropy loss: {loss:.4f}")  # High loss for wrong prediction

# Mean squared error for regression
def mse_loss(y_true, y_pred):
    return np.mean((y_true - y_pred) ** 2)

true_value = np.array([3.0])  # True word frequency
predicted_value = np.array([1.5])  # Predicted frequency

mse = mse_loss(true_value, predicted_value)
print(f"MSE loss: {mse:.4f}")  # Measures squared error

Gradient Descent: How Networks Learn from Mistakes

Gradient descent is the algorithm that adjusts the network’s weights to minimize the loss function. It works by computing the gradient (or derivative) of the loss with respect to each weight, then updating the weights in the opposite direction of the gradient. The learning rate determines the size of each step: too large, and the network may overshoot the minimum; too small, and training will be slow. Stochastic gradient descent (SGD) speeds up training by using a single example or small batch to estimate the gradient, introducing noise that can help escape local minima. For example, if the loss function is a valley, gradient descent moves the weights downhill until it reaches the bottom. In deep learning, variants like Adam or RMSprop adapt the learning rate for each weight, improving convergence. The key insight is that the gradient tells us how to tweak the weights to reduce the loss, even if we don’t know the optimal values upfront.

# Gradient descent for a single weight
import numpy as np

def gradient_descent(x, y, learning_rate=0.01, epochs=100):
    # x: input data (e.g., word embedding feature)
    # y: true label (e.g., sentiment score)
    
    # Initialize weight randomly
    weight = np.random.randn()
    
    for epoch in range(epochs):
        # Predicted value
        y_pred = weight * x
        
        # Calculate loss (MSE) and gradient
        loss = (y_pred - y) ** 2
        gradient = 2 * (y_pred - y) * x
        
        # Update weight
        weight -= learning_rate * gradient
        
        if epoch % 10 == 0:
            print(f"Epoch {epoch}: Weight = {weight:.4f}, Loss = {loss:.4f}")
    
    return weight

# Example usage
x = 2.0  # Input feature (e.g., word frequency)
y = 5.0  # True label (e.g., sentiment score)

final_weight = gradient_descent(x, y)
print(f"Final weight: {final_weight:.4f}")  # Weight converges to minimize loss

Backpropagation: The Engine of Deep Learning

Backpropagation is the algorithm that efficiently computes gradients for all weights in a neural network by applying the chain rule of calculus. It works by propagating the error backward from the output layer to the input layer, calculating how much each weight contributed to the final loss. Without backpropagation, training deep networks would be computationally infeasible because we’d have to compute gradients manually for each weight. The chain rule allows us to break down the gradient calculation into local computations at each layer, making it scalable. For example, in a 3-layer network, the gradient of the loss with respect to a weight in the first layer depends on the gradients of all subsequent layers. Backpropagation automates this process, enabling networks to learn from vast amounts of data. The key insight is that the gradient of the loss with respect to a weight is the product of the local gradient (how the weight affects its layer’s output) and the gradient of the loss with respect to that output.

# Backpropagation for a 2-layer network
import numpy as np

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

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

# Input data
X = np.array([[0.5, -1.2]])  # 1 sample, 2 features
Y = np.array([[1]])  # True label

# Initialize weights randomly
np.random.seed(42)
weights0 = np.random.randn(2, 3)  # Input to hidden layer (2x3)
weights1 = np.random.randn(3, 1)  # Hidden to output layer (3x1)

# Training loop
learning_rate = 0.1
for epoch in range(10000):
    # Forward pass
    hidden_layer_input = np.dot(X, weights0)
    hidden_layer_output = sigmoid(hidden_layer_input)
    
    output_layer_input = np.dot(hidden_layer_output, weights1)
    predicted_output = sigmoid(output_layer_input)
    
    # Calculate loss (MSE)
    loss = np.mean((Y - predicted_output) ** 2)
    
    # Backpropagation
    # Output layer gradient
    d_loss = 2 * (predicted_output - Y) / Y.size
    d_output = d_loss * sigmoid_derivative(predicted_output)
    
    # Hidden layer gradient
    d_hidden = d_output.dot(weights1.T) * sigmoid_derivative(hidden_layer_output)
    
    # Update weights
    weights1 -= learning_rate * hidden_layer_output.T.dot(d_output)
    weights0 -= learning_rate * X.T.dot(d_hidden)
    
    if epoch % 1000 == 0:
        print(f"Epoch {epoch}: Loss = {loss:.6f}")

print(f"Final prediction: {predicted_output[0][0]:.4f}")  # Should be close to 1

Key points

  • A neuron is a mathematical function that combines inputs with weights and biases, then applies an activation function to introduce non-linearity, enabling the network to learn complex patterns.
  • Stacking neurons into layers allows neural networks to learn hierarchical representations, where early layers detect simple features and deeper layers combine them into abstractions.
  • Loss functions like cross-entropy or mean squared error quantify the network’s prediction errors, guiding the learning process by measuring how far the predictions are from the true values.
  • Gradient descent adjusts the network’s weights by moving them in the direction that reduces the loss, with the learning rate controlling the size of each step to balance speed and stability.
  • Stochastic gradient descent (SGD) speeds up training by using small batches of data to estimate gradients, introducing noise that helps escape local minima in the loss landscape.
  • Backpropagation efficiently computes gradients for all weights in a network by applying the chain rule, propagating errors backward from the output layer to the input layer.
  • The chain rule in backpropagation breaks down gradient calculations into local computations at each layer, making it feasible to train deep networks with millions of parameters.
  • Activation functions like ReLU or sigmoid are crucial because they introduce non-linearity, allowing neural networks to model complex relationships beyond simple linear transformations.

Common mistakes

  • Mistake: Using a single hidden layer for all tasks. Why it's wrong: A single hidden layer may not capture complex patterns required for language modeling, leading to poor performance. Fix: Use multiple hidden layers (deep networks) to learn hierarchical representations of language data.
  • Mistake: Ignoring the vanishing gradient problem. Why it's wrong: Deep networks can suffer from gradients becoming too small during backpropagation, preventing early layers from learning. Fix: Use activation functions like ReLU or architectures like LSTMs/Transformers that mitigate this issue.
  • Mistake: Treating all tokens equally in the input sequence. Why it's wrong: Not all tokens contribute equally to the meaning of a sentence (e.g., stop words vs. keywords). Fix: Use attention mechanisms to weigh tokens dynamically based on their relevance.
  • Mistake: Overfitting the training data by not using regularization. Why it's wrong: The model memorizes training data instead of generalizing to unseen text, leading to poor performance on real-world inputs. Fix: Apply dropout, weight decay, or early stopping during training.
  • Mistake: Assuming larger batch sizes always improve training. Why it's wrong: Very large batches can lead to poor generalization due to sharp minima in the loss landscape. Fix: Use moderate batch sizes (e.g., 32-256) and experiment with learning rate scaling.

Interview questions

What is a neural network, and why is it fundamental to building your own large language model?

A neural network is a computational model inspired by the human brain, designed to recognize patterns and make decisions. It consists of layers of interconnected nodes, or neurons, where each connection has a weight that adjusts during training. For building your own large language model (LLM), neural networks are fundamental because they can learn complex representations of language from vast amounts of text data. Unlike traditional rule-based systems, neural networks automatically extract features, such as word relationships and context, through training. This ability to generalize from data makes them ideal for tasks like text generation, translation, and understanding, which are core to LLMs. Without neural networks, we’d lack the scalability and flexibility needed to process and generate human-like language efficiently.

Explain the role of activation functions in a neural network. Why can’t we just use a linear function?

Activation functions introduce non-linearity into a neural network, enabling it to learn complex patterns and relationships in data. Without non-linearity, no matter how many layers the network has, it would behave like a single linear transformation, severely limiting its ability to model real-world problems like language. For example, in an LLM, we need the network to capture nuances like word order, context, and semantic meaning, which are inherently non-linear. Common activation functions include ReLU (Rectified Linear Unit), which outputs the input directly if it’s positive, otherwise zero, and sigmoid, which maps inputs to a range between 0 and 1. Here’s a simple example in code: `def relu(x): return max(0, x)`. If we used only linear functions, the network’s output would just be a weighted sum of inputs, making it impossible to solve tasks requiring hierarchical or contextual understanding, such as generating coherent sentences in an LLM.

How does backpropagation work, and why is it essential for training your own LLM?

Backpropagation, short for 'backward propagation of errors,' is the algorithm used to train neural networks by adjusting their weights to minimize the difference between predicted and actual outputs. It works by computing the gradient of the loss function with respect to each weight using the chain rule of calculus, then updating the weights in the opposite direction of the gradient to reduce the error. For training your own LLM, backpropagation is essential because it enables the model to learn from vast amounts of text data efficiently. Without it, the model wouldn’t know how to adjust its weights to improve performance. For example, if the LLM generates an incorrect word in a sentence, backpropagation calculates how much each weight contributed to that error and updates them accordingly. This iterative process allows the model to gradually improve its predictions, making it capable of generating coherent and contextually relevant text. The key steps are: forward pass (compute predictions), calculate loss, backward pass (compute gradients), and update weights.

Compare feedforward neural networks and recurrent neural networks (RNNs) for language modeling. Which one would you use for building your own LLM, and why?

Feedforward neural networks (FNNs) and recurrent neural networks (RNNs) differ fundamentally in how they process data. FNNs process inputs in a single forward pass, with no memory of previous inputs, making them suitable for tasks where context isn’t sequential, like image classification. RNNs, on the other hand, have loops that allow information to persist, making them ideal for sequential data like text. For building your own LLM, RNNs were historically preferred because they can handle variable-length input sequences and maintain a hidden state that captures context over time. However, modern LLMs often use transformer-based architectures, which address RNNs' limitations, such as difficulty in capturing long-range dependencies and slow training due to sequential processing. That said, RNNs are still relevant for simpler language tasks or when computational resources are limited. For example, an RNN processes text word by word, updating its hidden state at each step, while an FNN would treat each word independently, losing contextual information. Here’s a simple RNN step in code: `hidden_state = tanh(input @ weights_input + hidden_state @ weights_hidden + bias)`. Ultimately, while RNNs are better for sequential data, transformers have largely superseded them for large-scale LLMs due to their parallelization and attention mechanisms.

What is the attention mechanism, and how does it improve the performance of your LLM?

The attention mechanism is a technique that allows a neural network to focus on specific parts of the input sequence when generating an output, rather than treating all parts equally. In the context of building your own LLM, attention is crucial because it enables the model to capture long-range dependencies and relationships between words, regardless of their distance in the sequence. For example, in the sentence 'The cat sat on the mat because it was tired,' the word 'it' refers to 'cat,' and attention helps the model understand this relationship by assigning higher weights to relevant words. This is a significant improvement over RNNs, which struggle with long sequences due to vanishing gradients. The attention mechanism computes a weighted sum of input representations, where the weights are learned during training. Here’s a simplified version of how attention scores are calculated: `attention_scores = softmax(query @ key.T / sqrt(d_k))`. These scores determine how much each word in the input contributes to the output. By dynamically focusing on relevant parts of the input, attention improves the LLM’s ability to generate coherent, contextually accurate text, making it a cornerstone of modern architectures like transformers.

Explain how you would design the training loop for your own LLM, including key components like loss function, optimizer, and batching. Why is each component important?

Designing the training loop for your own LLM involves several key components, each critical to the model’s performance. First, the **loss function** measures how well the model’s predictions match the actual data. For an LLM, a common choice is cross-entropy loss, which compares the predicted probability distribution of the next word with the true distribution. This is important because it directly quantifies the model’s error and guides weight updates. Second, the **optimizer** adjusts the model’s weights to minimize the loss. Popular optimizers like Adam or SGD (Stochastic Gradient Descent) are used because they adaptively adjust learning rates, helping the model converge faster and avoid getting stuck in local minima. Third, **batching** involves processing multiple training examples simultaneously, which improves computational efficiency and stabilizes gradient estimates. For example, instead of updating weights after every word, you might process 32 or 64 sentences at once. Here’s a high-level outline of the training loop in code: `for batch in dataloader: outputs = model(batch.inputs); loss = cross_entropy(outputs, batch.labels); loss.backward(); optimizer.step(); optimizer.zero_grad()`. Additionally, you’d include **learning rate scheduling** to adjust the learning rate during training, which helps fine-tune the model’s performance. Finally, **gradient clipping** is often used to prevent exploding gradients, ensuring stable training. Each component plays a vital role: the loss function defines the objective, the optimizer drives learning, batching improves efficiency, and auxiliary techniques like scheduling and clipping ensure robustness. Without these, training an LLM would be inefficient or even impossible.

All Creating Own LLM interview questions →

Check yourself

1. Why are deep neural networks preferred over shallow networks for language modeling tasks?

  • A.Deep networks require less computational power and are easier to train.
  • B.Deep networks can learn hierarchical features, capturing complex patterns like syntax and semantics more effectively.
  • C.Shallow networks always outperform deep networks in language tasks due to simpler architectures.
  • D.Deep networks eliminate the need for attention mechanisms, simplifying the model.
Show answer

B. Deep networks can learn hierarchical features, capturing complex patterns like syntax and semantics more effectively.
The correct answer is that deep networks can learn hierarchical features, which is essential for capturing the layered structure of language (e.g., characters → words → phrases → meaning). The other options are incorrect because: 1) Deep networks require more computational power, not less. 3) Shallow networks struggle with complex patterns in language. 4) Deep networks often rely on attention mechanisms to improve performance.

2. What is the primary purpose of using an attention mechanism in a language model?

  • A.To reduce the number of parameters in the model, making it faster to train.
  • B.To allow the model to focus on different parts of the input sequence dynamically, depending on the context.
  • C.To replace the need for embeddings, simplifying the model architecture.
  • D.To ensure all tokens in the input sequence are treated equally during prediction.
Show answer

B. To allow the model to focus on different parts of the input sequence dynamically, depending on the context.
The correct answer is that attention mechanisms allow the model to focus on different parts of the input sequence dynamically, which is crucial for understanding context-dependent relationships in language. The other options are incorrect because: 1) Attention mechanisms increase parameters, not reduce them. 3) Attention does not replace embeddings; they serve different purposes. 4) Attention explicitly weighs tokens differently, not equally.

3. During backpropagation in a deep neural network for language modeling, you notice the gradients in early layers are extremely small. What is the most likely cause and solution?

  • A.The learning rate is too high, causing the gradients to explode. Reduce the learning rate.
  • B.The model is overfitting, so you should add more dropout layers.
  • C.The activation functions are causing vanishing gradients. Replace sigmoid/tanh with ReLU or use residual connections.
  • D.The batch size is too small, leading to unstable gradients. Increase the batch size.
Show answer

C. The activation functions are causing vanishing gradients. Replace sigmoid/tanh with ReLU or use residual connections.
The correct answer is that the activation functions (e.g., sigmoid/tanh) are causing vanishing gradients, and replacing them with ReLU or using residual connections can mitigate this. The other options are incorrect because: 1) Small gradients are a sign of vanishing, not exploding, gradients. 3) Overfitting is unrelated to gradient size. 4) Batch size affects gradient stability but is not the primary cause of vanishing gradients.

4. You are training a language model and observe that it performs well on the training data but poorly on unseen text. What is the most effective way to address this?

  • A.Increase the model size to capture more patterns, even if it leads to longer training times.
  • B.Reduce the learning rate to ensure the model converges more precisely to the training data.
  • C.Apply regularization techniques like dropout or weight decay to improve generalization.
  • D.Train the model for more epochs to ensure it fully memorizes the training data.
Show answer

C. Apply regularization techniques like dropout or weight decay to improve generalization.
The correct answer is to apply regularization techniques like dropout or weight decay, which prevent the model from overfitting to the training data and improve generalization. The other options are incorrect because: 1) Increasing model size without regularization can worsen overfitting. 3) Reducing the learning rate may not address overfitting. 4) More epochs can exacerbate overfitting by memorizing training data.

5. Why is the transformer architecture particularly well-suited for language modeling compared to traditional RNNs?

  • A.Transformers use recurrent connections, which are more efficient for sequential data like text.
  • B.Transformers process the entire input sequence in parallel, enabling faster training and better capture of long-range dependencies.
  • C.Transformers require fewer parameters, making them easier to train on small datasets.
  • D.Transformers eliminate the need for embeddings, simplifying the model architecture.
Show answer

B. Transformers process the entire input sequence in parallel, enabling faster training and better capture of long-range dependencies.
The correct answer is that transformers process the entire input sequence in parallel, which speeds up training and better captures long-range dependencies in language. The other options are incorrect because: 1) Transformers do not use recurrent connections; they rely on self-attention. 3) Transformers typically have more parameters, not fewer. 4) Transformers still use embeddings for input representation.

Take the full Creating Own LLM quiz →

← PreviousIntroduction to Machine Learning: Supervised vs. Unsupervised LearningNext →Natural Language Processing (NLP) Fundamentals

Creating Own LLM

36 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app