AI and Machine Learning

RLHF Explained: Aligning AI with Human Preferences

Dive deep into Reinforcement Learning from Human Feedback (RLHF). Learn its architecture, the three-step process, code examples, and how it aligns AI with human values.

Khader Vali July 24, 2026 20 min read

RLHF Explained: Aligning AI with Human Preferences

As a senior software engineer and technical writer, I’ve seen firsthand how quickly the landscape of artificial intelligence evolves. We’ve moved from models that could generate plausible text to those that can engage in complex conversations, write code, and even compose poetry. But with this incredible power comes a critical challenge: ensuring these advanced AI systems not only perform well but also behave in ways that are helpful, harmless, and aligned with human values.

This is where Reinforcement Learning from Human Feedback (RLHF) steps in. You’ve likely interacted with systems powered by RLHF, even if you didn’t realize it – large language models (LLMs) like ChatGPT are prime examples. RLHF is a powerful paradigm that bridges the gap between raw model capabilities and desired human-centric behavior. It’s not just a technical trick; it’s a fundamental shift in how we guide AI towards becoming truly intelligent and beneficial partners.

In this comprehensive article, we’ll peel back the layers of RLHF. We’ll start with the foundational concepts, explore its intricate architecture, walk through its multi-stage process with concrete code examples, discuss real-world applications, acknowledge its challenges, and peek into its future. My goal is to equip you with a deep understanding, allowing you to not only comprehend but also potentially implement and innovate with RLHF.

The Genesis: Why RLHF?

Before diving into the mechanics, let’s understand the “why.” Traditional machine learning, especially supervised learning, relies on explicit labels. If you want a model to classify images, you provide images with corresponding labels (“cat,” “dog,” “car”). For language models, this means training on vast corpora of text to predict the next word.

While this approach is excellent for learning patterns and generating coherent text, it has limitations when it comes to subjective qualities:

  • Subjectivity: How do you define “helpful,” “harmless,” “engaging,” or “truthful” in a dataset? These are often nuanced and context-dependent.
  • Scalability of Preferences: It’s impractical to manually label every possible desired behavior for an LLM that can generate infinite variations of text.
  • Avoiding Harmful Outputs: Simply training on the internet means a model might learn biases, generate toxic content, or hallucinate misinformation, as these are present in its training data.
  • Aligning with User Intent: A model might be grammatically perfect but completely miss the user’s underlying intent or provide an unhelpful answer.

Reinforcement Learning (RL) offers a framework for learning through interaction and feedback, but traditional RL often requires a well-defined reward function. Designing such a reward function for complex, subjective tasks like conversation generation is incredibly difficult, if not impossible. This is the problem RLHF aims to solve: using human feedback to *learn* a reward function, and then using that learned reward function to optimize the AI’s behavior.

Foundational Concepts: A Quick Refresh

To fully grasp RLHF, let’s briefly revisit its two primary pillars:

1. Reinforcement Learning (RL)

In classical RL, an agent learns to make decisions by interacting with an environment. The agent performs an action in a given state, and the environment responds with a new state and a reward. The agent’s goal is to maximize the cumulative reward over time. Key components include:

  • Agent: The learning entity (e.g., our language model).
  • Environment: The context in which the agent operates (e.g., the conversation, the user’s prompt).
  • State: The current situation or observation (e.g., the current prompt and previous turns of dialogue).
  • Action: The decision made by the agent (e.g., generating the next word or a complete response).
  • Reward: A scalar value indicating how good an action was in a given state. Positive rewards encourage behavior, negative rewards discourage it.
  • Policy: The agent’s strategy for choosing actions in states, often represented by a neural network.
  • Value Function: Estimates the long-term cumulative reward from a given state or state-action pair.

Algorithms like Q-learning, SARSA, and Proximal Policy Optimization (PPO) are commonly used in RL to optimize the policy. PPO, in particular, is a workhorse in modern deep RL due to its stability and performance.

2. The Role of Human Feedback

The “HF” in RLHF is the game-changer. Instead of engineers trying to meticulously craft a reward function, humans provide the feedback directly. But how do humans provide this feedback? It’s often not a simple “good” or “bad” for every token generated. Instead, it’s typically more nuanced:

  • Preference Rankings: Humans are remarkably good at comparing two or more options and expressing a preference. “Response A is better than Response B.” This is much easier and more consistent than assigning an absolute score.
  • Critiques and Edits: Sometimes, humans might directly edit or critique a model’s output, highlighting specific issues.
  • Binary Classifications: In simpler scenarios, a human might classify an output as “acceptable” or “unacceptable.”

The beauty of human preference data is its ability to capture subjective qualities and implicit rules that are hard to codify. It allows us to imbue the AI with a sense of “good judgment” as perceived by humans.

The RLHF Process: A Three-Stage Journey

RLHF typically involves three distinct, sequential stages. Let’s break them down in detail.

Stage 1: Pre-training or Fine-tuning the Base Language Model (SFT Model)

The journey begins with a strong foundation: a powerful language model. This is usually a large transformer-based model, pre-trained on a massive corpus of text (like the entirety of the internet) to learn language patterns, grammar, facts, and common sense. Models like GPT-3, LLaMA, or PaLM are examples of such base models.

Often, before applying RLHF, this base model undergoes a supervised fine-tuning (SFT) step. In this stage, the model is fine-tuned on a smaller, high-quality dataset of demonstrations, often curated by human labelers. This helps the model learn to follow instructions and generate helpful responses in a supervised manner. For example, the dataset might contain prompt-response pairs where the response is a human-written ideal answer to the prompt.

The goal of the SFT model is to produce a model that is already quite capable of following instructions and generating relevant text, even if it hasn’t yet been fully aligned with human preferences for safety, helpfulness, or conciseness. This SFT model will serve as the initial policy for the RL phase and as the reference model during RL training to prevent excessive divergence.

Think of it like teaching a student all the facts and grammar (pre-training) and then teaching them how to answer specific types of questions (SFT). They know *what* to say, but not necessarily *how* to say it in the most helpful or appropriate way.


import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments
from datasets import Dataset

# 1. Load a pre-trained base language model and tokenizer
model_name = "gpt2" # Or a larger model like "microsoft/DialoGPT-small"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)

# Add a padding token if the tokenizer doesn't have one (common for GPT-style models)
if tokenizer.pad_token is None:
    tokenizer.add_special_tokens({'pad_token': '[PAD]'})
    model.resize_token_embeddings(len(tokenizer))

# Dummy supervised fine-tuning dataset
# In a real scenario, this would be a large dataset of (prompt, ideal_response) pairs
sft_data = [
    {"prompt": "Hello, how are you?", "response": "I am an AI assistant, and I'm doing well, thank you!"},
    {"prompt": "What is the capital of France?", "response": "The capital of France is Paris."},
    {"prompt": "Write a short poem about nature.", "response": "Green leaves dance in gentle breeze,\nSunlight filters through the trees.\nRivers flow with whispered song,\nNature's beauty, pure and strong."}
]

def tokenize_function(examples):
    # Concatenate prompt and response for training
    full_text = [p + tokenizer.eos_token + r for p, r in zip(examples["prompt"], examples["response"])]
    return tokenizer(
        full_text,
        max_length=128,
        truncation=True,
        padding="max_length"
    )

# Create a Hugging Face Dataset
sft_dataset = Dataset.from_list(sft_data)
tokenized_sft_dataset = sft_dataset.map(tokenize_function, batched=True, remove_columns=["prompt", "response"])

# Rename input_ids to labels for causal language modeling
tokenized_sft_dataset = tokenized_sft_dataset.rename_column("input_ids", "labels")

# Training arguments for SFT
training_args = TrainingArguments(
    output_dir="./sft_model",
    overwrite_output_dir=True,
    num_train_epochs=3,
    per_device_train_batch_size=2,
    save_steps=10_000,
    save_total_limit=2,
    logging_dir="./sft_logs",
    logging_steps=100,
)

# Trainer for SFT
sft_trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_sft_dataset,
)

# Start SFT training (commented out for a quick run, but conceptually important)
# sft_trainer.train()
# sft_trainer.save_model("./sft_model")
# print("SFT model saved to ./sft_model")

# The SFT model is now ready for the next stage.
# For simplicity, we'll continue using our initial model as if it were SFT-trained.

RLHF Explained: Aligning AI with Human Preferences
Generated Image

Stage 2: Collecting Human Preference Data and Training the Reward Model (RM)

This is the “Human Feedback” part of RLHF. Instead of providing explicit reward numbers, humans provide preferences between different model outputs. This preference data is then used to train a separate model, called the Reward Model (RM).

2.1 Data Collection

The process starts by taking prompts (either from a curated dataset or generated by the SFT model itself) and having the SFT model generate multiple diverse responses for each prompt. For instance, for a single prompt, the SFT model might generate 4-8 different completions.

These responses are then presented to human annotators (labelers). The annotators’ task is to rank these responses from best to worst according to a set of guidelines. These guidelines are crucial and specify what constitutes a “good” response (e.g., helpfulness, harmlessness, factual accuracy, fluency, conciseness). For example, if presented with four responses for “Tell me about climate change,” a human might rank them:

  1. Response C (most factual, balanced, and comprehensive)
  2. Response A (factual but too brief)
  3. Response D (contains a slight inaccuracy)
  4. Response B (completely hallucinates or is harmful)

This preference data is much easier for humans to produce consistently than absolute scores, as comparative judgments tend to be more robust.

2.2 Training the Reward Model (RM)

Once enough human preference data is collected, it’s used to train the Reward Model. The RM is typically another neural network (often a slightly smaller version of the SFT model, or even the SFT model itself with a modified head) that takes a prompt and a response as input and outputs a scalar score – the “reward.”

The training objective for the RM is to learn to predict the human preferences. If a human preferred response A over response B for a given prompt, the RM should output a higher score for (prompt, response A) than for (prompt, response B).

A common loss function used for this is the pairwise ranking loss:

Loss = -log(sigmoid(r_theta(x, y_w) - r_theta(x, y_l)))

Where:

  • x is the prompt.
  • y_w is the preferred (winning) response.
  • y_l is the less preferred (losing) response.
  • r_theta(x, y) is the reward model’s scalar output for prompt x and response y.
  • The sigmoid function ensures the difference is between 0 and 1.

The goal is to maximize the difference between the reward given to the preferred response and the less preferred response. This loss function encourages the RM to assign higher scores to responses that humans liked and lower scores to those they disliked, effectively modeling human preferences.


import torch
import torch.nn as nn
from transformers import AutoTokenizer, AutoModelForSequenceClassification

# 2. Load the SFT model (or a fresh model for RM) and tokenizer
# For RM, we often use a model designed for sequence classification,
# where the 'sequence' is the concatenation of prompt and response.
rm_model_name = "gpt2" # Can be the same as SFT model, but with a classification head
rm_tokenizer = AutoTokenizer.from_pretrained(rm_model_name)
# Add padding token if needed
if rm_tokenizer.pad_token is None:
    rm_tokenizer.add_special_tokens({'pad_token': '[PAD]'})

# A custom Reward Model head on top of a pre-trained language model
class RewardModel(nn.Module):
    def __init__(self, base_model_name):
        super().__init__()
        self.base_model = AutoModelForSequenceClassification.from_pretrained(
            base_model_name,
            num_labels=1, # Output a single scalar reward
        )
        # Ensure the classifier head matches the actual hidden size if needed
        # The AutoModelForSequenceClassification already handles adding a classification head.

    def forward(self, input_ids, attention_mask=None):
        outputs = self.base_model(
            input_ids=input_ids,
            attention_mask=attention_mask,
            output_hidden_states=False # We just need the logits
        )
        return outputs.logits # This will be our scalar reward

reward_model = RewardModel(rm_model_name)
reward_model.base_model.resize_token_embeddings(len(rm_tokenizer))


# Dummy human preference data
# Each entry represents a comparison: (prompt, preferred_response, less_preferred_response)
preference_data = [
    {"prompt": "How to make a cake?", "chosen": "Mix flour, eggs, sugar, bake.", "rejected": "Buy a cake."},
    {"prompt": "Explain gravity.", "chosen": "Force attracting masses.", "rejected": "It's a type of fruit."},
    {"prompt": "Write a story.", "chosen": "Once upon a time, a brave knight...", "rejected": "Blah blah text garbage."}
]

# Function to prepare a batch for RM training
def prepare_rm_batch(batch):
    prompt = batch["prompt"]
    chosen = batch["chosen"]
    rejected = batch["rejected"]

    # Tokenize chosen response
    chosen_tokens = rm_tokenizer(
        [p + rm_tokenizer.eos_token + c for p, c in zip(prompt, chosen)],
        max_length=128, truncation=True, padding="max_length", return_tensors="pt"
    )
    # Tokenize rejected response
    rejected_tokens = rm_tokenizer(
        [p + rm_tokenizer.eos_token + r for p, r in zip(prompt, rejected)],
        max_length=128, truncation=True, padding="max_length", return_tensors="pt"
    )
    return chosen_tokens, rejected_tokens

# A simplified training loop for the Reward Model
optimizer = torch.optim.Adam(reward_model.parameters(), lr=1e-5)
loss_fn = nn.LogSigmoid() # For -log(sigmoid(diff))

num_epochs_rm = 1
batch_size_rm = 2

# Convert preference_data to a Dataset or DataLoader in a real scenario
# For demonstration, we'll iterate directly
for epoch in range(num_epochs_rm):
    for i in range(0, len(preference_data), batch_size_rm):
        batch = preference_data[i:i + batch_size_rm]
        if not batch:
            continue

        chosen_inputs, rejected_inputs = prepare_rm_batch(batch)

        # Get rewards from the RM
        chosen_rewards = reward_model(chosen_inputs.input_ids, chosen_inputs.attention_mask).squeeze(-1)
        rejected_rewards = reward_model(rejected_inputs.input_ids, rejected_inputs.attention_mask).squeeze(-1)

        # Calculate pairwise ranking loss
        loss = -loss_fn(chosen_rewards - rejected_rewards).mean()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if (i // batch_size_rm) % 1 == 0: # Print every 1 batch for this small example
            print(f"RM Epoch {epoch+1}, Batch {i//batch_size_rm+1}, Loss: {loss.item():.4f}")

# The Reward Model is now trained and can assign scores based on human preferences.
# For simplicity, we'll continue using our reward_model instance.

Stage 3: Reinforcement Learning Fine-tuning (PPO)

This is the final and often most complex stage, where the SFT model is further fine-tuned using the trained Reward Model as its reward signal. The goal is to optimize the SFT model (which now becomes the “policy” in RL terms) to generate responses that maximize the reward predicted by the RM.

The most commonly used algorithm for this stage is Proximal Policy Optimization (PPO). PPO is an on-policy algorithm that is known for its stability and effectiveness in deep reinforcement learning. Here’s how it generally works in the context of RLHF:

3.1 PPO Loop Execution

  1. Generate Responses: The current policy (our SFT model) takes a batch of prompts and generates responses.
  2. Calculate Rewards: Each (prompt, response) pair is fed into the *trained Reward Model*, which outputs a scalar reward score. This score is a proxy for how much a human would prefer that response.
  3. Compute KL Divergence: To prevent the model from drifting too far from its original SFT behavior (and potentially generating nonsense or “reward hacking”), a KL divergence penalty is often added to the reward. This penalty measures how much the new policy’s distribution of outputs has diverged from the original SFT model’s distribution for the same prompt.

    Adjusted_Reward = RM_Score - beta * KL_Divergence(New_Policy || SFT_Policy)

    Where beta is a hyperparameter controlling the strength of the penalty.

  4. PPO Optimization: The PPO algorithm uses these adjusted rewards to update the policy. It does this by collecting trajectories (sequences of states, actions, and rewards) and using them to estimate the advantage of actions. The policy network is then updated to increase the probability of actions that lead to higher rewards. PPO uses a clipped objective function to ensure that policy updates are not too large, which helps with stability.
  5. Iterate: Steps 1-4 are repeated for many iterations until the model’s performance converges or reaches a desired level.

The beauty of this iterative process is that the LLM learns to generate responses that are not just grammatically correct or fluent (from SFT) but also aligned with the complex, nuanced preferences captured by the Reward Model. It learns to “explore” the space of possible responses and gravitate towards those that the RM (and thus, implicitly, humans) deems better.

Key components for PPO in RLHF:

  • Actor (Policy Model): This is our SFT model, which generates responses. Its parameters are updated during RL training.
  • Critic (Value Model): Often, a separate value network is trained alongside the actor to predict the expected cumulative reward from a given state. This helps stabilize training. In some setups, the RM itself can serve as a critic, or a separate head is added to the actor.
  • Reference Model: The original SFT model, kept frozen, used to calculate the KL divergence penalty. This ensures the fine-tuning doesn’t completely abandon the initial language generation capabilities.

RLHF Explained: Aligning AI with Human Preferences
Generated Image


import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from trl import PPOTrainer, PPOConfig
from trl.core import LengthSampler
from datasets import Dataset

# 3. Reinforcement Learning Fine-tuning with PPO
# Requires a pre-trained SFT model and the trained Reward Model.

# Load the SFT model (this is our policy/actor)
# For simplicity, we'll load gpt2 again, but imagine it's the SFT-tuned model
ppo_tokenizer = AutoTokenizer.from_pretrained("gpt2")
if ppo_tokenizer.pad_token is None:
    ppo_tokenizer.add_special_tokens({'pad_token': '[PAD]'})
    
ppo_model = AutoModelForCausalLM.from_pretrained("gpt2")
ppo_model.resize_token_embeddings(len(ppo_tokenizer))

# We'll also need a reference model to compute KL divergence.
# This is typically the *original SFT model*, kept frozen.
ref_model = AutoModelForCausalLM.from_pretrained("gpt2") # Imagine this is the SFT model
ref_model.resize_token_embeddings(len(ppo_tokenizer))

# Our trained reward_model from Stage 2 is used here.
# Make sure it's in evaluation mode
reward_model.eval()

# PPO Configuration
ppo_config = PPOConfig(
    learning_rate=1e-5,
    mini_batch_size=1, # Small for demo
    batch_size=2,      # Small for demo
    gradient_accumulation_steps=1,
    optimize_cuda_cache=True,
    target_kl=0.01,    # KL divergence regularization
    init_kl_coef=0.2,  # Initial KL coefficient
    max_ppo_epochs=2,
    seed=0,
)

# Prepare prompts for PPO training
# These prompts should be diverse and representative of what the model will encounter
ppo_prompts = [
    {"query": "Tell me a fun fact about space."},
    {"query": "What is the best way to learn programming?"},
    {"query": "Summarize the benefits of exercise."},
    {"query": "Give me a recipe for chocolate chip cookies."}
]
ppo_dataset = Dataset.from_list(ppo_prompts)

def tokenize_ppo_function(examples):
    return ppo_tokenizer(
        examples["query"],
        max_length=64, # Max length for input prompt
        truncation=True,
        padding="max_length",
        return_tensors="pt"
    )

tokenized_ppo_dataset = ppo_dataset.map(tokenize_ppo_function, batched=True)
tokenized_ppo_dataset.set_format(type="torch")

# Initialize PPOTrainer
ppo_trainer = PPOTrainer(
    config=ppo_config,
    model=ppo_model,
    ref_model=ref_model,
    tokenizer=ppo_tokenizer,
    dataset=tokenized_ppo_dataset,
)

# Generation settings for the PPO model
gen_kwargs = {
    "min_length": -1,
    "top_k": 0.0,
    "top_p": 1.0,
    "do_sample": True,
    "pad_token_id": ppo_tokenizer.pad_token_id,
    "max_new_tokens": 32, # Max length for generated response
}

# PPO training loop
for epoch, batch in enumerate(ppo_trainer.dataloader):
    query_tensors = batch["input_ids"]

    # Generate responses with the current policy (ppo_model)
    response_tensors = ppo_trainer.generate(query_tensors, **gen_kwargs)
    response_texts = [ppo_tokenizer.decode(r.squeeze(), skip_special_tokens=True) for r in response_tensors]

    # For reward calculation, we need (prompt + response)
    full_texts = [
        ppo_tokenizer.decode(q.squeeze(), skip_special_tokens=True) + r
        for q, r in zip(query_tensors, response_texts)
    ]

    # Tokenize full texts for the reward model
    rm_inputs = rm_tokenizer(
        full_texts,
        max_length=128, truncation=True, padding="max_length", return_tensors="pt"
    )

    # Get rewards from the Reward Model (RM)
    with torch.no_grad():
        rewards = reward_model(rm_inputs.input_ids, rm_inputs.attention_mask).squeeze(-1)

    # For PPO, we also need the original prompt tensors
    # and the generated response tensors separately.
    # The trl library handles this internally, we just need to pass the rewards.
    # Here, 'response_tensors' contains the generated token IDs including the prompt
    # We need to extract only the generated part for logging/analysis
    
    # Calculate rewards for PPO (can be further processed if needed)
    # The PPO trainer expects a list of scalar rewards for each generated response
    
    # The PPO training step
    stats = ppo_trainer.step(query_tensors, response_tensors, rewards)
    ppo_trainer.log_stats(stats, batch, rewards)

    print(f"PPO Epoch {epoch+1}, Mean Reward: {rewards.mean().item():.4f}")

    # Example of generating a response after some training
    if epoch % 1 == 0:
        test_prompt = "What is the capital of Japan?"
        test_input_ids = ppo_tokenizer(test_prompt, return_tensors="pt").input_ids.to(ppo_trainer.current_device)
        test_response_tensor = ppo_trainer.generate(test_input_ids, **gen_kwargs)
        decoded_response = ppo_tokenizer.decode(test_response_tensor[0], skip_special_tokens=True)
        print(f"\nTest Prompt: {test_prompt}")
        print(f"Model Response: {decoded_response}\n")

# The ppo_model is now fine-tuned to align with human preferences.
# ppo_trainer.save_model("./rlhf_model")
# print("RLHF model saved to ./rlhf_model")

Architecture of an RLHF System

To visualize the interplay, let’s conceptualize the architecture involved in an RLHF pipeline:

At its core, an RLHF system involves three main components working in concert:

  1. The Language Model (Policy Model / Actor):

    • Role: This is the generative model that produces text. It’s the AI system we want to align.
    • Initial State: Starts as a large pre-trained language model, often further fine-tuned with supervised data (SFT model).
    • Output: Takes a prompt and generates a response.
    • Learning: Its parameters are updated during the RL phase using the reward signal from the Reward Model. In PPO, this is often called the “actor.”
    • Reference Model: A frozen copy of the initial SFT model used to calculate KL divergence, ensuring the actor doesn’t deviate too much.
  2. The Reward Model (RM):

    • Role: Learns to predict human preferences. It acts as a proxy for a human evaluator.
    • Input: Takes a prompt and a generated response.
    • Output: A scalar “reward” score, indicating how good the response is according to learned human preferences.
    • Learning: Trained via supervised learning on human preference comparison data (e.g., “Response A is better than Response B”).
    • Architecture: Typically another neural network, often derived from the same base LLM architecture but with a single scalar output head.
  3. Human Labelers / Annotators:

    • Role: Provide the crucial preference data that trains the Reward Model.
    • Input: Given a prompt and multiple responses generated by the Language Model.
    • Output: Rankings or comparisons of the responses based on predefined criteria (e.g., helpfulness, harmlessness, accuracy).
    • Importance: The quality and consistency of human feedback directly impact the Reward Model’s effectiveness and, consequently, the alignment of the final Language Model.

The Workflow in a Nutshell:

Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

RLHF Explained: Aligning AI with Human Preferences

Jun 24, 2026 · 19 min read

Building AI Agents with Function Calling in Python hero image showcasing LLM tool use architecture

Building AI Agents with Function Calling in Python

Jun 01, 2026 · 16 min read

Vector Databases Under the Hood: ChromaDB, Pinecone, Qdrant

May 26, 2026 · 17 min read