Chapter 01 of 11

Preference Rankers — Learning Which Answer Is Better

Explain this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Apply this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Preference Rankers — Learning Which Answer Is Better

So far in Models From First Principles, we have mostly trained models by telling them what the answer should be.

MR.Q took a context and a response and produced a number:

context + response
      model
      0.82

That number might mean quality.

Or usefulness.

Or reward.

But there is an awkward question hiding underneath that architecture:

Where did the 0.82 come from?

Someone has to provide the target.

And for many of the things we actually care about, that is surprisingly difficult.

Suppose I give you this sentence:

The system performs a comprehensive analysis of the available information.

Then I ask:

How good is this sentence on a scale from 0 to 1?

Is it 0.43?

0.61?

0.72?

There is no obvious answer.

Now suppose I give you two sentences:

A

The system analyzes the available information.

B

The system performs a comprehensive analysis of the available information.

and ask:

Which would you rather keep?

That is much easier.

This gives us a different kind of model.

Instead of learning:

candidate
score

we learn:

candidate A
candidate B
which is better?

That is a preference ranker.

And it turns out to be extraordinarily useful.


Relative Judgments Are Often Easier Than Absolute Ones

Imagine that we are training a model to understand writing quality.

One approach is to construct a dataset like this:

sentence                                      quality
----------------------------------------------------
"The dog ran."                                0.71
"The canine proceeded rapidly."               0.42
"The exhausted dog sprinted toward the gate." 0.84

But these numbers are artificial.

Why exactly is the third sentence 0.84 rather than 0.79?

The labels imply a precision that our judgment probably does not contain.

Instead, we can collect:

A                                          B
-----------------------------------------------------------------
"The dog ran."                             "The canine proceeded rapidly."

preferred = A

Then another pair:

A                                          B
-----------------------------------------------------------------
"The exhausted dog sprinted..."            "The dog ran."

preferred = A

We no longer need to know the absolute quality of any sentence.

We only need to know:

A > B

The model can learn from the ordering.

This is the central idea behind preference learning.


The Model We Use in Writer

Writer uses a small pairwise neural ranker.

The core model is surprisingly simple:

class PreferenceRanker(nn.Module):
    def __init__(self, embedding_dim=768, hidden_dim=256):
        super().__init__()

        self.encoder = nn.Sequential(
            nn.Linear(embedding_dim, hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(hidden_dim, hidden_dim),
        )

        self.comparator = nn.Sequential(
            nn.Linear(hidden_dim * 2, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 1),
        )

    def forward(self, emb_a, emb_b):
        feat_a = self.encoder(emb_a)
        feat_b = self.encoder(emb_b)

        combined = torch.cat([feat_a, feat_b], dim=1)

        return self.comparator(combined).squeeze(1)

There are really only two components:

shared encoder
comparator

The architecture looks like this:

candidate A ──→ shared encoder ──→ feature A ──┐
                                               ├──→ comparator ──→ preference logit
candidate B ──→ shared encoder ──→ feature B ──┘

The important word is shared.

Both candidates pass through the same encoder.

We do not have:

encoder_A
encoder_B

We have:

encoder
encoder

with the same parameters.

That matters.


Why Share the Encoder?

Suppose candidate A had one neural network and candidate B had another.

The model could accidentally learn:

things arriving on the left mean one thing
things arriving on the right mean another

But A and B are not fundamentally different kinds of objects.

They are two candidates playing temporary roles.

Today:

A = original
B = revision

Tomorrow:

A = revision
B = another revision

We therefore want both candidates represented in the same learned feature space.

If the encoder is:

[ E(x) ]

then we calculate:

[ h_A = E(A) ]

and:

[ h_B = E(B) ]

using exactly the same function (E).

Then the comparator receives:

[ [h_A \Vert h_B] ]

where (\Vert) means concatenation.

The comparator learns a function:

[ C(h_A, h_B) ]

and produces one logit:

[ z = C(h_A, h_B) ]

That logit answers:

How strongly should A be preferred to B?


This Is a Siamese Network

Architectures like this are often called Siamese networks.

The name sounds more exotic than the mechanism.

It simply means that the two inputs travel through identical networks that share their parameters.

           ┌─────────────┐
A ────────→│             │────→ h_A
           │   encoder   │
B ────────→│             │────→ h_B
           └─────────────┘
              same weights

The interesting part is not that we have two encoders.

We really have one encoder used twice.

This creates comparable representations.


But Writer Does Not Rank the Sentence Alone

There is another important detail.

Whether one answer is better than another often depends on the goal.

Consider:

A

He left.

B

Without warning, he abandoned the silent room and disappeared into the darkness beyond the door.

Which is better?

There is no answer.

If the goal is:

Make this concise.

A may win.

If the goal is:

Increase atmosphere and tension.

B may win.

So Writer does not simply encode the candidate.

During training it constructs:

context embedding + candidate embedding

for each side.

If:

[ g = \text{embedding(goal)} ]

[ a = \text{embedding(candidate A)} ]

[ b = \text{embedding(candidate B)} ]

then the actual model inputs are:

[ x_A = [g \Vert a] ]

and:

[ x_B = [g \Vert b] ]

The architecture therefore becomes:

                       ┌──────────────┐
goal ──┐               │              │
       ├─ concatenate ─→              │
A ─────┘               │ shared       │──→ h_A ──┐
                       │ encoder      │           │
goal ──┐               │              │           ├──→ comparator
       ├─ concatenate ─→              │           │
B ─────┘               │              │──→ h_B ──┘
                       └──────────────┘

Now the question is not:

Is A better than B?

It is:

Given this goal, is A better than B?

That is a much more useful model.


One Model Per Dimension

Writer also separates different kinds of preference.

For example:

quality
clarity
simplicity

These are related.

But they are not identical.

A sentence can become clearer while becoming less interesting.

It can become simpler while losing precision.

It can become stylistically better while becoming less concise.

So the training service can build a separate preference model for each dimension:

quality ranker
clarity ranker
simplicity ranker

Conceptually:

             ┌── quality model ────→ prefers A
A vs B ──────┼── clarity model ────→ prefers B
             └── simplicity model ─→ prefers B

This is useful because disagreement is information.

A single scalar can hide the trade-off.

Multiple preference dimensions expose it.


Training the Ranker

Suppose our training pair says:

goal:
    Make the sentence clearer.

A:
    The model predicts the next token.

B:
    The system engages in predictive generation
    regarding the subsequently occurring token.

preferred:
    A

The model produces a logit:

[ z ]

We convert this into a probability using the sigmoid:

[ p(A > B) = \sigma(z) ]

If A is preferred, our target is:

[ y = 1 ]

If B is preferred:

[ y = 0 ]

We can therefore train using:

criterion = nn.BCEWithLogitsLoss()

The loss asks the network to make the logit positive when A should win and negative when B should win.

This is still ordinary neural-network training:

forward pass
preference logit
loss
backpropagation
optimizer.step()

The unusual part is not gradient descent.

The unusual part is the meaning of the label.


Is This Really Contrastive Learning?

Writer calls this model a contrastive ranker.

That name is useful, but we should be precise about what it means.

There are specific machine-learning objectives usually associated with contrastive learning:

contrastive loss
triplet loss
InfoNCE

Our ranker does not use those losses.

It uses:

BCEWithLogitsLoss

So technically this is better described as a:

pairwise neural preference ranker with a shared encoder.

The contrastive part is in the training structure.

The model learns by seeing contrasts:

A versus B
preferred versus rejected
better versus worse

rather than by receiving an isolated absolute label.

This distinction matters because names should not obscure mechanisms.


There Is a Problem

Our architecture contains a subtle flaw.

Look carefully at the comparator input:

combined = torch.cat([feat_a, feat_b], dim=1)

The model sees:

[A features | B features]

If we reverse the candidates, it sees:

[B features | A features]

Those are different vectors.

Nothing in the architecture itself guarantees:

[ C(A,B) = -C(B,A) ]

But logically we would like something close to that.

If the model strongly says:

A > B

then reversing the pair should give:

B < A

A badly trained network could instead learn something ridiculous:

the left candidate tends to win

This is a positional shortcut.

Writer attacks this problem twice.


First Defence: Reverse Every Training Pair

For every training example:

(A, B) → A wins

Writer also trains:

(B, A) → B loses

So one source preference becomes two training examples.

In tensor form:

input_a, input_b, 1
input_b, input_a, 0

If the original preference says A wins:

X_a.append(input_a)
X_b.append(input_b)
y.append(1.0)

then Writer immediately adds:

X_a.append(input_b)
X_b.append(input_a)
y.append(0.0)

This is a small change with a large consequence.

The network can no longer succeed by learning:

left side = preferred

because every preferred candidate appears on both sides.

This is symmetric pair augmentation.


The Dataset Doubles

If we start with:

100 preference pairs

the trainer sees:

200 ordered training examples

But we have not created 100 new judgments.

We have encoded the symmetry already implied by the original judgments.

This is an important distinction:

source pairs        = 100
training pairs      = 200
independent evidence = 100

Data augmentation does not create new evidence.

It expresses an invariant more clearly to the model.


Second Defence: Ask the Model Both Ways at Inference

Writer does something even more interesting when comparing candidates.

It computes:

[ z_{AB} = C(A,B) ]

and:

[ z_{BA} = C(B,A) ]

Then:

[ p_{AB} = \sigma(z_{AB}) ]

and:

[ p_{BA} = \sigma(z_{BA}) ]

Ideally:

[ p_{AB} \approx 1 - p_{BA} ]

But neural networks are imperfect.

So instead of trusting either direction alone, Writer calculates:

[ \text{preference}(A)

\frac{ p_{AB} + (1-p_{BA}) }{2} ]

This is a beautiful little trick.

Suppose:

P(A beats B) = 0.80
P(B beats A) = 0.30

The second prediction implies:

P(A beats B) = 1 - 0.30 = 0.70

So we combine them:

[ (0.80 + 0.70)/2 = 0.75 ]

and obtain:

preference A = 0.75
preference B = 0.25

The model is allowed to be slightly inconsistent.

The scoring system corrects for some of that inconsistency.


Architecture and Inference Can Enforce Different Things

This reveals a useful model-design principle.

We could attempt to construct an architecture that mathematically guarantees antisymmetry.

For example, we could learn a scalar utility:

[ s(x) ]

and define:

[ C(A,B) = s(A) - s(B) ]

Then automatically:

[ C(B,A) = -C(A,B) ]

But Writer’s model is more flexible.

It learns:

[ C(A,B) ]

directly from both representations.

That comparator can potentially learn relationships between candidates that are not expressible as the difference between two independent scalar scores.

The cost is that symmetry is no longer guaranteed.

Writer compensates using:

training augmentation
+
bidirectional inference

This is a recurring engineering choice:

Do we put the constraint into the architecture, the training data, the inference procedure, or some combination of all three?


Ranking Is Not Scoring

At this point the model can answer:

A or B?

But Writer frequently needs another operation:

How good is A?

These are different questions.

A pairwise model naturally produces:

[ A > B ]

It does not automatically tell us:

[ A = 0.82 ]

This is one of the biggest conceptual differences between MR.Q and the preference ranker.

MR.Q directly learns something like:

(context, response) → score

The ranker naturally learns:

(context, A, B) → preference

So how can we use a ranker as a scorer?


Introduce a Baseline

Writer uses a clever solution.

Give the ranker a fixed reference sentence.

For example:

This sentence is weak, generic, unclear, or stylistically worse.

Now instead of asking:

Is this sentence good?

we ask:

Is this sentence better than the baseline?

The pair becomes:

candidate
vs
baseline

The model gives us a logit.

A very weak candidate may barely beat the baseline.

A strong candidate may beat it decisively.

Now we have something that behaves much more like a scalar.

candidate 1 ──→ barely beats baseline
candidate 2 ──→ clearly beats baseline
candidate 3 ──→ overwhelmingly beats baseline

The baseline acts as an anchor.


From Relative Preference to an Absolute-ish Score

Suppose we also possess some examples with absolute values.

We can collect:

ranker logit    known score
---------------------------
-0.8            0.21
 0.1            0.48
 0.9            0.71
 1.8            0.88

Then train a small calibration model that maps:

[ \text{ranker logit} \rightarrow \text{absolute score} ]

Writer calls this component a RegressionTuner.

The entire scoring pipeline becomes:

goal
candidate
baseline
embeddings
pairwise ranker
raw preference logit
calibration
0-to-1 score

This is not magically creating an objective measurement of quality.

It is anchoring a relative model to an externally defined scale.

That distinction is important.

The ranker still fundamentally understands comparisons.

Calibration gives us a convenient coordinate system.


This Is Surprisingly General

This baseline trick appears in many forms.

Suppose we know:

A > reference
B > reference by more
C < reference

We have started constructing an ordering around an anchor.

The same general idea appears in:

  • rating systems
  • psychometrics
  • preference learning
  • reward modelling
  • search ranking
  • recommendation systems
  • human-feedback training

The model may learn comparatively while the surrounding system exposes a scalar interface.


Why Writer Uses This So Much

Writer continuously encounters pairs.

An original sentence and a revision:

original
revised

An accepted edit and a rejected alternative:

accepted
rejected

Two candidate rewrites:

candidate A
candidate B

A human decision therefore naturally creates training data:

preferred
not preferred

We do not need to interrupt the human and ask:

Please assign this revision a clarity value of exactly 0.734.

The interaction itself reveals preference.

That is powerful.

Normal product activity can generate training signals.


Preferences Can Be About the User, Not Universal Truth

There is another subtle point here.

Suppose Writer observes:

original:
The implementation provides a robust and comprehensive mechanism
for performing validation.

accepted:
The implementation validates the result.

The lesson is not necessarily:

Short sentences are objectively better.

The lesson may instead be:

Under this goal, for this writing system, this revision was preferred.

That is why context matters so much.

Preference models are especially useful when the desired behaviour is:

situated
contextual
subjective
goal-dependent

rather than a universal physical quantity.


The Ranker Can Still Fail

A preference model is not automatically trustworthy.

Suppose it sees:

A > B
B > C
C > A

We have produced a cycle.

There is no single scalar ordering that satisfies all three preferences.

Human preference data can contain these inconsistencies.

Different contexts can produce them legitimately.

Noise can produce them accidentally.

A flexible pairwise comparator can learn local relationships without guaranteeing a globally consistent ranking.

So we should distinguish:

pairwise preference

from:

global utility function

They are not automatically the same thing.


Confidence Is Not Quality

Writer’s scoring layer records another useful quantity:

margin = |score_a - score_b|

If:

A = 0.51
B = 0.49

the ranker has only a slight preference.

If:

A = 0.91
B = 0.09

the separation is much clearer.

But the margin does not mean A has high quality.

Consider:

terrible sentence A
even worse sentence B

The model may confidently prefer A.

That tells us:

A is clearly preferable to B

not:

A is excellent

This distinction is easy to lose when pairwise systems are wrapped in numerical APIs.

Preference strength and absolute quality are different variables.


Model Health Matters Too

There is one final systems lesson in Writer’s implementation.

Suppose no ranker model loaded successfully.

Or the requested dimension does not exist.

A badly designed scoring system might still return:

0.5

and downstream code might interpret that as:

the model believes these candidates are equal

But those are not the same thing.

There is an enormous difference between:

the model evaluated A and B and decided they tie

and:

there was no functioning model

Writer therefore carries health information alongside the preference result.

Conceptually:

winner
score A
score B
used dimensions
loaded dimensions
health
load errors

This is a useful general principle:

Model failure should remain distinguishable from model uncertainty.


From Prediction to Explanation

Writer then takes another step.

The pairwise decision can be converted into a small causal score graph.

Instead of retaining only:

A wins

the system can preserve:

pairwise preference
pairwise margin
ranker health
quality contribution
clarity contribution
simplicity contribution
external penalties

So the model’s output becomes part of a larger evidence structure.

This is no longer just:

neural network → number

It becomes:

neural network
structured decision
contributors
evidence
downstream reasoning

This is worth noticing because production models almost never live alone.

The neural network may be only twenty lines long.

The useful system around it can be much larger.


MR.Q Versus the Preference Ranker

We can now see two different approaches to the same broad problem.

MR.Q

context
response
model
absolute scalar

Training data:

response → 0.82

Preference ranker

context
A
B
model
A > B

Training data:

A preferred to B

Neither architecture is universally better.

They assume different kinds of supervision.

Use an absolute scorer when you genuinely have meaningful absolute targets.

Use a preference ranker when comparative judgments are more natural and reliable.


The Training Signal Should Influence the Architecture

This is perhaps the biggest lesson of the chapter.

We often begin model design by asking:

Which neural network architecture should I use?

But there is an earlier question:

What information do I actually possess?

If your data naturally looks like:

input → class

build a classifier.

If it looks like:

input → number

build a regressor.

If it looks like:

context + action → reward

build something like MR.Q.

If it looks like:

A was preferred to B

build a ranker.

The shape of the supervision should influence the shape of the model.


A Tiny Preference Ranker From Scratch

We can reduce the whole idea to a very small PyTorch example.

import torch
import torch.nn as nn


class TinyRanker(nn.Module):
    def __init__(self, input_dim=16, hidden_dim=32):
        super().__init__()

        self.encoder = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
        )

        self.comparator = nn.Linear(hidden_dim * 2, 1)

    def forward(self, a, b):
        ha = self.encoder(a)
        hb = self.encoder(b)

        pair = torch.cat([ha, hb], dim=1)

        return self.comparator(pair).squeeze(1)

Create some fake candidates:

a = torch.randn(8, 16)
b = torch.randn(8, 16)

Suppose A wins in some rows and B wins in others:

labels = torch.tensor([
    1, 0, 1, 1,
    0, 0, 1, 0,
], dtype=torch.float32)

Train exactly as before:

model = TinyRanker()

optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.BCEWithLogitsLoss()

logits = model(a, b)

loss = criterion(logits, labels)

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

Nothing exotic has happened.

We still have:

Linear
ReLU
concatenate
Linear
loss
backpropagation
Adam

The novelty comes from how those familiar pieces are arranged around a different question.


The Recursive Decomposition Still Works

This brings us back to the theme of the entire book.

A preference ranker may sound like a specialized machine-learning architecture.

Decompose it:

PreferenceRanker
shared encoder + comparator
Linear + ReLU + Dropout + Linear
matrix multiplication + bias

The training system sounds more complicated:

ContrastiveRankerTrainer

Decompose it:

preference pairs
embeddings
context concatenation
symmetric pair augmentation
standardization
BCEWithLogitsLoss
Adam

The scoring system sounds more complicated again:

ContrastiveRankerScorer

Decompose it:

A vs B
B vs A
two probabilities
symmetry correction
dimension aggregation

And absolute scoring becomes:

candidate vs baseline
preference logit
calibration
score

At every level, the intimidating name disappears when we recursively open the box.


The Bigger Lesson

There is a deeper reason I wanted to include this model.

Most introductory machine learning teaches us to think in labels:

cat
dog
0.73
positive
negative

But a huge amount of useful information in the real world arrives as choice.

Someone selected this result instead of that one.

They accepted this edit.

Rejected another.

Clicked one result.

Ignored another.

Preferred answer A.

Reverted answer B.

Those interactions do not necessarily tell us what absolute score anything deserves.

They tell us something simpler:

[ A > B ]

And sometimes that is enough.

A preference ranker turns those local choices into a learnable model.

It does not need us to pretend that subjective judgments are precise measurements.

It starts from the information we actually have.

That makes it one of the simplest models in this book architecturally.

And one of the most useful conceptually.

Because sometimes the right question is not:

How good is this?

It is simply:

Which one would you keep?