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›Natural Language Processing (NLP) Fundamentals

Foundations of Machine Learning and NLP

Natural Language Processing (NLP) Fundamentals

Natural Language Processing (NLP) is the field of study that enables machines to understand, interpret, and generate human language. It matters because language is the primary means of human communication, and mastering NLP allows you to build systems that can automate tasks like translation, sentiment analysis, and question answering. You reach for NLP when you need to process text data at scale, extract meaningful insights, or create interactive applications like chatbots or large language models.

What is Text Representation?

At the heart of NLP lies the challenge of converting raw text into a numerical format that machines can process. Text, in its raw form, is a sequence of characters or words, but algorithms require structured data. The simplest way to represent text is by converting words into unique identifiers, such as integers, where each word in a vocabulary is assigned a distinct number. This process, called tokenization, is the first step in most NLP pipelines. However, tokenization alone doesn't capture the meaning or relationships between words. For example, the words 'happy' and 'joyful' might be assigned different integers, but they convey similar emotions. To address this, we need more sophisticated representations that encode semantic meaning. Understanding text representation is crucial because it determines how well your model can generalize to unseen data. If the representation is poor, even the most advanced model will struggle to perform well.

# Tokenization example: converting text into numerical tokens
text = "Natural language processing enables machines to understand text."

# Split text into words (tokens)
tokens = text.lower().split()
print("Tokens:", tokens)

# Create a vocabulary mapping each word to a unique integer
vocab = {word: idx for idx, word in enumerate(set(tokens))}
print("Vocabulary:", vocab)

# Convert tokens to numerical IDs
token_ids = [vocab[word] for word in tokens]
print("Token IDs:", token_ids)

One-Hot Encoding: The Basics of Word Representation

One-hot encoding is one of the simplest ways to represent words numerically while preserving their uniqueness. In this method, each word in the vocabulary is represented as a binary vector of length equal to the vocabulary size, where only one element is '1' (indicating the presence of the word) and all others are '0'. For example, if your vocabulary has three words ['cat', 'dog', 'bird'], the word 'dog' would be represented as [0, 1, 0]. This approach ensures that each word has a distinct representation, but it has significant limitations. The vectors are sparse (mostly zeros) and grow linearly with the vocabulary size, making them inefficient for large vocabularies. More importantly, one-hot encoding doesn't capture any semantic relationships between words. The vectors for 'cat' and 'dog' are as dissimilar as those for 'cat' and 'airplane', even though 'cat' and 'dog' are more closely related. Despite these limitations, one-hot encoding is a useful starting point because it introduces the concept of word vectors and prepares you for more advanced techniques.

# One-hot encoding example
vocab = ['cat', 'dog', 'bird']
word = 'dog'

# Create a one-hot vector for the word
one_hot = [0] * len(vocab)
index = vocab.index(word)
one_hot[index] = 1
print(f"One-hot vector for '{word}':", one_hot)

# Function to encode any word in the vocabulary
def one_hot_encode(word, vocabulary):
    vector = [0] * len(vocabulary)
    if word in vocabulary:
        vector[vocabulary.index(word)] = 1
    return vector

print("One-hot for 'cat':", one_hot_encode('cat', vocab))

Bag-of-Words: Capturing Word Frequency

Bag-of-Words (BoW) is a step up from one-hot encoding because it doesn't just represent the presence of a word but also its frequency in a document. Instead of a binary vector, BoW creates a vector where each element represents the count of a word in the document. For example, in the sentence 'the cat sat on the mat', the word 'the' appears twice, so its count would be 2 in the BoW vector. This method is useful for tasks like document classification, where the frequency of words can indicate the topic or sentiment of the text. However, BoW still ignores the order of words and any semantic relationships. For instance, 'not good' and 'good' would have similar BoW representations if 'good' appears frequently, even though their meanings are opposite. BoW is also prone to bias toward common words like 'the' or 'and', which may not carry much meaning. To mitigate this, techniques like stop-word removal or term frequency-inverse document frequency (TF-IDF) are often used alongside BoW. Understanding BoW is important because it introduces the idea of document-level representations, which are foundational for more complex models.

# Bag-of-Words example
from collections import Counter

documents = [
    "the cat sat on the mat",
    "the dog sat on the log"
]

# Create vocabulary from all unique words in the documents
vocab = sorted(set(word for doc in documents for word in doc.split()))
print("Vocabulary:", vocab)

# Function to create BoW vector for a document
def bag_of_words(doc, vocabulary):
    word_counts = Counter(doc.split())
    return [word_counts.get(word, 0) for word in vocabulary]

# Create BoW vectors for all documents
bow_vectors = [bag_of_words(doc, vocab) for doc in documents]
print("BoW Vectors:")
for doc, vector in zip(documents, bow_vectors):
    print(f"'{doc}': {vector}")

Word Embeddings: Capturing Semantic Meaning

Word embeddings address the limitations of one-hot encoding and BoW by representing words in a dense, low-dimensional space where semantically similar words are closer together. Unlike one-hot vectors, which are sparse and high-dimensional, word embeddings are dense vectors of fixed size (e.g., 50, 100, or 300 dimensions). These embeddings are learned from large corpora of text using algorithms like Word2Vec, GloVe, or FastText. The key idea is that words appearing in similar contexts should have similar vector representations. For example, the vectors for 'king' and 'queen' might be close in the embedding space, as might 'man' and 'woman'. This property allows models to generalize better, as they can infer relationships between words even if they haven't seen them together in training. Word embeddings are typically pre-trained on massive datasets and can be fine-tuned for specific tasks. They are a cornerstone of modern NLP because they enable models to understand nuances in language, such as analogies ('king' - 'man' + 'woman' ≈ 'queen') or synonyms. However, they still have limitations, such as struggling with rare words or polysemy (words with multiple meanings).

# Example of using pre-trained Word2Vec embeddings (using Gensim)
from gensim.models import KeyedVectors

# Load pre-trained Word2Vec embeddings (this is a small example; real models are larger)
# Note: In practice, you'd download a pre-trained model like Google's Word2Vec or GloVe
# For this example, we'll create a tiny embedding space manually
word_vectors = {
    'king': [0.5, 0.8],
    'queen': [0.4, 0.9],
    'man': [0.6, 0.7],
    'woman': [0.5, 0.85],
    'cat': [0.2, 0.3],
    'dog': [0.25, 0.35]
}

# Function to compute cosine similarity between two vectors
def cosine_similarity(vec1, vec2):
    dot_product = sum(a * b for a, b in zip(vec1, vec2))
    norm_vec1 = sum(a * a for a in vec1) ** 0.5
    norm_vec2 = sum(b * b for b in vec2) ** 0.5
    return dot_product / (norm_vec1 * norm_vec2)

# Example: Check similarity between 'king' and 'queen'
king_vec = word_vectors['king']
queen_vec = word_vectors['queen']
similarity = cosine_similarity(king_vec, queen_vec)
print(f"Similarity between 'king' and 'queen': {similarity:.2f}")

# Example: Word analogy (king - man + woman ≈ queen)
analogy_vec = [
    king_vec[0] - word_vectors['man'][0] + word_vectors['woman'][0],
    king_vec[1] - word_vectors['man'][1] + word_vectors['woman'][1]
]
print("Analogy vector (king - man + woman):", analogy_vec)
print("Closest to 'queen'?", cosine_similarity(analogy_vec, queen_vec) > 0.9)

Sequence Models: Handling Word Order and Context

While word embeddings capture semantic meaning, they treat words as isolated units and ignore the order and context in which they appear. Sequence models, such as Recurrent Neural Networks (RNNs), Long Short-Term Memory (LSTM) networks, and Transformers, address this by processing text as a sequence of tokens. These models maintain a hidden state that evolves as they process each word, allowing them to capture dependencies between words over time. For example, in the sentence 'The cat, which was black, sat on the mat', the model can learn that 'black' describes 'cat' because it processes the words in order and retains information about previous words. RNNs and LSTMs are particularly effective for tasks like machine translation or text generation, where the meaning of a word depends on its context. However, they struggle with long-range dependencies (e.g., connecting the first and last words in a long sentence) due to the vanishing gradient problem. Transformers, introduced in the paper 'Attention Is All You Need', solve this by using self-attention mechanisms to weigh the importance of each word in the context of all other words. This allows them to capture long-range dependencies efficiently and has led to state-of-the-art performance in many NLP tasks. Understanding sequence models is crucial because they form the backbone of modern NLP systems, including large language models.

# Simple RNN example using PyTorch to process a sequence of word embeddings
import torch
import torch.nn as nn

# Define a tiny vocabulary and embedding layer
vocab = {'the': 0, 'cat': 1, 'sat': 2, 'on': 3, 'mat': 4}
embedding_dim = 5
embedding_layer = nn.Embedding(len(vocab), embedding_dim)

# Example sentence: "the cat sat on the mat"
sentence = [vocab[word] for word in ['the', 'cat', 'sat', 'on', 'mat']]
input_tensor = torch.tensor(sentence, dtype=torch.long).unsqueeze(0)  # Add batch dimension

# Define a simple RNN
class SimpleRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super(SimpleRNN, self).__init__()
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
    
    def forward(self, x):
        # x shape: (batch, sequence, input_size)
        output, hidden = self.rnn(x)
        return output, hidden

# Initialize RNN
hidden_size = 3
rnn = SimpleRNN(embedding_dim, hidden_size)

# Forward pass
embedded = embedding_layer(input_tensor)  # Convert tokens to embeddings
output, hidden = rnn(embedded)

print("Input sentence tokens:", sentence)
print("RNN output shape (batch, sequence, hidden_size):", output.shape)
print("Final hidden state shape (batch, hidden_size):", hidden.shape)
print("Final hidden state:", hidden.squeeze(0))

Key points

  • Text representation is the foundation of NLP, converting raw text into numerical formats that algorithms can process.
  • One-hot encoding provides unique but sparse vectors for words, serving as a basic introduction to word representations.
  • Bag-of-Words improves upon one-hot encoding by capturing word frequency, making it useful for document-level tasks like classification.
  • Word embeddings represent words in dense vectors, capturing semantic relationships and enabling models to generalize better.
  • Sequence models like RNNs and LSTMs process text as ordered sequences, allowing them to capture context and dependencies between words.
  • Transformers use self-attention to weigh the importance of each word in the context of all others, solving long-range dependency issues.
  • Pre-trained word embeddings and sequence models can be fine-tuned for specific tasks, reducing the need for training from scratch.
  • Understanding the strengths and limitations of each method allows you to choose the right approach for your NLP problem.

Common mistakes

  • Mistake: Assuming tokenization is just splitting text by spaces. Why it's wrong: Many languages (e.g., Chinese, Japanese) don’t use spaces, and subword tokenization (like BPE) is critical for handling rare words and morphological variations. Fix: Use subword tokenization algorithms like Byte Pair Encoding (BPE) or SentencePiece instead of naive space-based splitting.
  • Mistake: Ignoring the impact of sequence length on model performance. Why it's wrong: Long sequences increase computational cost and can lead to vanishing gradients or attention dilution. Fix: Use techniques like positional embeddings, attention mechanisms (e.g., transformer architectures), or truncation/padding strategies to handle variable-length sequences effectively.
  • Mistake: Treating embeddings as static features. Why it's wrong: Embeddings should capture contextual meaning, not just fixed representations. Fix: Use contextual embeddings (e.g., from transformer layers) or fine-tune embeddings during training to adapt to the task’s specific language patterns.
  • Mistake: Overlooking the importance of loss functions in training. Why it's wrong: Using the wrong loss (e.g., MSE for classification) can lead to poor convergence or incorrect outputs. Fix: Choose task-appropriate losses like cross-entropy for classification or masked language modeling loss for pretraining.
  • Mistake: Assuming more data always improves performance. Why it's wrong: Low-quality or irrelevant data can introduce noise and hurt generalization. Fix: Curate high-quality datasets, use data augmentation (e.g., back-translation), and apply filtering techniques to remove outliers or biases.

Interview questions

What is tokenization in the context of building your own LLM, and why is it important?

Tokenization is the process of breaking down raw text into smaller units called tokens, which can be words, subwords, or even characters. It's a critical first step in building your own LLM because the model doesn't understand raw text—it only processes numerical representations. By converting text into tokens, we create a vocabulary that the model can map to embeddings, enabling it to learn patterns and relationships in the data. For example, in Python, you might use a tokenizer like this: `tokens = tokenizer.encode('Hello world')`, which splits the text into tokens and converts them to numerical IDs. Without proper tokenization, the model would struggle to generalize across different inputs, leading to poor performance.

Explain how embeddings work in an LLM and why they are necessary for training your own model.

Embeddings are dense vector representations of tokens that capture semantic meaning in a continuous space. In an LLM, each token is mapped to an embedding vector, which allows the model to understand relationships between words, such as synonyms or contextual similarities. For example, the words 'king' and 'queen' might have embeddings that are close in vector space, reflecting their semantic relationship. These embeddings are learned during training and are necessary because they enable the model to process and generalize from text efficiently. Without embeddings, the model would treat each token as an isolated unit, making it impossible to capture the nuances of language. You initialize embeddings randomly and update them during training, like this: `embedding_layer = nn.Embedding(vocab_size, embedding_dim)`.

What is the role of attention mechanisms in training your own LLM, and how do they improve model performance?

Attention mechanisms allow the model to focus on different parts of the input sequence when generating each output token, dynamically weighting the importance of each token based on context. This is crucial for building an LLM because it enables the model to handle long-range dependencies in text, such as understanding how a word at the beginning of a sentence relates to one at the end. For example, in the sentence 'The cat sat on the mat because it was tired,' attention helps the model figure out that 'it' refers to 'cat.' Without attention, the model would struggle with such dependencies, leading to less coherent outputs. The self-attention formula, `Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) * V`, computes these dynamic weights, where Q, K, and V are query, key, and value matrices derived from the input embeddings.

Compare the use of recurrent neural networks (RNNs) and transformer architectures when building your own LLM. Why have transformers become the dominant choice?

RNNs process sequences sequentially, which makes them slow to train and difficult to parallelize, especially for long sequences. They also struggle with vanishing gradients, limiting their ability to capture long-range dependencies. In contrast, transformers use self-attention to process the entire sequence at once, enabling parallelization and better handling of long-range dependencies. For example, training an RNN on a long document would require processing each word one after another, while a transformer can process all words simultaneously. This makes transformers much faster and more scalable for large datasets. Additionally, transformers' attention mechanisms allow them to dynamically focus on relevant parts of the input, leading to better performance on tasks like machine translation or text generation. While RNNs were once popular, transformers have become the dominant choice for building LLMs due to these advantages.

How does fine-tuning work when adapting your own pre-trained LLM to a specific task, and why is it more efficient than training from scratch?

Fine-tuning involves taking a pre-trained LLM and further training it on a smaller, task-specific dataset to adapt its weights for a particular application, like sentiment analysis or question answering. This is more efficient than training from scratch because the pre-trained model already understands general language patterns, so fine-tuning only requires adjusting the weights to specialize in the new task. For example, if you have a pre-trained LLM and want to use it for medical text analysis, you would fine-tune it on a dataset of medical records. This process is faster and requires less data because the model starts with a strong foundation. In code, you might freeze some layers and only update the final layers: `for param in model.parameters(): param.requires_grad = False; model.fc.requires_grad = True`. Fine-tuning leverages transfer learning, reducing computational costs and improving performance on specialized tasks.

Explain how you would design the loss function for training your own LLM and why cross-entropy loss is commonly used for this purpose.

The loss function measures how well the model's predictions match the actual targets, guiding the optimization process during training. For an LLM, cross-entropy loss is commonly used because it directly compares the predicted probability distribution over tokens with the true distribution, penalizing incorrect predictions while rewarding accurate ones. For example, if the model predicts the next token in a sequence, cross-entropy loss will be low if the predicted token matches the true token and high otherwise. This is ideal for language modeling because it encourages the model to assign high probabilities to the correct tokens. The formula for cross-entropy loss is `L = -sum(y_true * log(y_pred))`, where `y_true` is the one-hot encoded true token and `y_pred` is the model's predicted probability distribution. Using cross-entropy loss ensures the model learns to generate coherent and contextually appropriate text by minimizing prediction errors during training.

All Creating Own LLM interview questions →

Check yourself

1. When building a tokenizer for your own LLM, why is subword tokenization (e.g., BPE) preferred over character-level tokenization?

  • A.Character-level tokenization is faster and requires less memory, making it ideal for large-scale models.
  • B.Subword tokenization balances vocabulary size and rare word handling, reducing out-of-vocabulary issues while maintaining efficiency.
  • C.Subword tokenization is only useful for languages with logographic scripts like Chinese, not for alphabetic languages.
  • D.Character-level tokenization captures semantic meaning better because it preserves word boundaries.
Show answer

B. Subword tokenization balances vocabulary size and rare word handling, reducing out-of-vocabulary issues while maintaining efficiency.
The correct answer is that subword tokenization balances vocabulary size and rare word handling. Character-level tokenization (option 0) is inefficient for large vocabularies and loses semantic meaning. Option 2 is incorrect because subword tokenization is language-agnostic. Option 3 is wrong because character-level tokenization does not preserve word boundaries or semantic meaning effectively.

2. During pretraining of your LLM, you notice the model struggles with long-range dependencies. Which architectural choice is most likely to address this issue?

  • A.Increasing the embedding dimension to capture more features per token.
  • B.Replacing the transformer’s self-attention with a simple RNN layer to reduce computational overhead.
  • C.Using positional embeddings and multi-head self-attention to model relationships across distant tokens.
  • D.Reducing the sequence length to avoid long-range dependencies entirely.
Show answer

C. Using positional embeddings and multi-head self-attention to model relationships across distant tokens.
The correct answer is using positional embeddings and multi-head self-attention. Positional embeddings encode token order, and self-attention captures long-range dependencies. Option 0 (increasing embedding dimension) doesn’t address dependencies. Option 1 (RNNs) is worse for long-range dependencies. Option 3 (reducing sequence length) avoids the problem rather than solving it.

3. You’re fine-tuning your LLM for a text classification task. Which loss function should you use, and why?

  • A.Mean Squared Error (MSE), because it measures the difference between predicted and true values as continuous numbers.
  • B.Cross-entropy loss, because it is designed for classification tasks and penalizes incorrect predictions more effectively.
  • C.Hinge loss, because it is robust to outliers and works well for binary classification.
  • D.Kullback-Leibler (KL) divergence, because it measures the difference between probability distributions and is ideal for generative tasks.
Show answer

B. Cross-entropy loss, because it is designed for classification tasks and penalizes incorrect predictions more effectively.
The correct answer is cross-entropy loss. It is the standard for classification tasks, providing better gradients for discrete outputs. MSE (option 0) is for regression. Hinge loss (option 2) is less common for multi-class tasks. KL divergence (option 3) is used for generative modeling, not classification.

4. Your LLM’s embeddings are not capturing contextual nuances (e.g., ‘bank’ as a financial institution vs. riverbank). What is the most effective way to address this?

  • A.Use static word embeddings (e.g., Word2Vec) and freeze them during training to ensure consistency.
  • B.Train the model with a larger vocabulary to include more word senses explicitly.
  • C.Leverage the transformer’s self-attention mechanism to generate contextual embeddings dynamically.
  • D.Replace embeddings with one-hot encodings to avoid ambiguity entirely.
Show answer

C. Leverage the transformer’s self-attention mechanism to generate contextual embeddings dynamically.
The correct answer is leveraging self-attention to generate contextual embeddings. Static embeddings (option 0) cannot disambiguate context. A larger vocabulary (option 1) doesn’t solve polysemy. One-hot encodings (option 3) lose all semantic information.

5. You’re designing a dataset for pretraining your LLM. Why is it important to filter out low-quality or biased data?

  • A.Low-quality data increases training time, but it doesn’t affect the model’s performance or generalization.
  • B.Biased data can lead to unfair or harmful outputs, and low-quality data introduces noise that degrades model performance.
  • C.Filtering data is unnecessary because the model will automatically learn to ignore irrelevant or biased examples.
  • D.High-quality data is only important for fine-tuning, not for pretraining, because pretraining focuses on quantity over quality.
Show answer

B. Biased data can lead to unfair or harmful outputs, and low-quality data introduces noise that degrades model performance.
The correct answer is that biased or low-quality data harms performance and fairness. Option 0 is wrong because quality directly impacts performance. Option 2 is incorrect because models amplify biases. Option 3 is false because pretraining quality is critical for downstream tasks.

Take the full Creating Own LLM quiz →

← PreviousBasics of Neural Networks and Deep LearningNext →Tokenization and Text Preprocessing Techniques

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