<p class="hint">Each answer has three traits: <b>helpful</b>, <b>honest</b>, <b>concise</b>
(0–10). Drag the cards into <b>your</b> preferred order — best at the top —
then train. A reward model learns three weights so its score reproduces your ranking.</p>
<ul id="cards" class="cards"></ul>
<div class="btns">
<button id="train" type="button">Train reward model</button>
<button id="shuffle" type="button" class="ghost">Shuffle order</button>
</div>
<div class="weights" id="weights"></div>
<div class="status" id="status">Order the cards, then train.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.cards { list-style: none; margin: 0 0 .6rem; padding: 0; }
.card { display: flex; align-items: center; gap: .7rem; background: #eef2f6;
border: 1px solid #cdd9e3; border-radius: 10px; padding: .55rem .7rem;
margin: .35rem 0; cursor: grab; }
.card.drag { opacity: .4; }
.rank { font: 700 16px ui-monospace, monospace; color: #1d3557; width: 1.4em; text-align: center; }
.name { font-weight: 600; flex: 1; }
.traits { display: flex; gap: .35rem; }
.pill { font: 600 11px system-ui; background: #fff; border: 1px solid #cdd9e3;
border-radius: 999px; padding: .1rem .45rem; color: #345; }
.score { font: 700 14px ui-monospace, monospace; color: #0a7d33; min-width: 3.2em; text-align: right; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .3rem 0 .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.weights { display: flex; gap: .8rem; flex-wrap: wrap; font-size: .85rem; color: #345; margin: .2rem 0; }
.weights b { color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A reward model in miniature. Each answer is a feature vector x = [helpful,
// honest, concise]. The model scores it as a weighted sum: r(x) = w . x.
// We never tell it the weights; it LEARNS them from the order the user gives.
// For every adjacent pair (better above worse) we want r(better) > r(worse).
// We minimise the Bradley-Terry / logistic pairwise loss with gradient descent:
// L = sum over pairs -log sigmoid( r(better) - r(worse) )
// Same shape as the preference loss used to train real RLHF reward models.
const ANSWERS = [
{ name: 'Detailed walkthrough', x: [9, 8, 3] },
{ name: 'Quick honest tip', x: [6, 9, 9] },
{ name: 'Confident but vague', x: [7, 3, 6] },
{ name: 'Short and correct', x: [5, 8, 8] },
];
let order = [0, 1, 2, 3]; // indices, top = most preferred
let w = [0, 0, 0];
const sigmoid = z => 1 / (1 + Math.exp(-z));
const score = (x, ww) => x[0]*ww[0] + x[1]*ww[1] + x[2]*ww[2];
// Train weights so score(order[i]) > score(order[i+1]) for all adjacent pairs.
function train(epochs = 4000, lr = 0.02) {
w = [0, 0, 0];
for (let e = 0; e < epochs; e++) {
const g = [0, 0, 0];
for (let i = 0; i < order.length - 1; i++) {
const a = ANSWERS[order[i]].x; // preferred (above)
const b = ANSWERS[order[i+1]].x; // less preferred (below)
const diff = score(a, w) - score(b, w);
const p = sigmoid(diff); // model’s prob that a beats b
const coef = (1 - p); // dL/d(diff) = -(1 - p)
for (let k = 0; k < 3; k++) g[k] += coef * (a[k] - b[k]);
}
for (let k = 0; k < 3; k++) w[k] += lr * g[k];
}
}
// How many of the user’s adjacent preferences does the model now respect?
function agreement() {
let ok = 0, tot = order.length - 1;
for (let i = 0; i < order.length - 1; i++)
if (score(ANSWERS[order[i]].x, w) > score(ANSWERS[order[i+1]].x, w)) ok++;
return [ok, tot];
}
const TRAITS = ['helpful', 'honest', 'concise'];
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
function render() {
const ul = document.getElementById('cards'); ul.innerHTML = '';
order.forEach((idx, pos) => {
const a = ANSWERS[idx];
const li = document.createElement('li');
li.className = 'card'; li.draggable = true; li.dataset.pos = pos;
const pills = a.x.map((v, k) => `<span class="pill">${TRAITS[k]} ${v}</span>`).join('');
const sc = (w[0]||w[1]||w[2]) ? score(a.x, w).toFixed(1) : '';
li.innerHTML = `<span class="rank">${pos+1}</span><span class="name">${a.name}</span>`
+ `<span class="traits">${pills}</span><span class="score">${sc}</span>`;
ul.appendChild(li);
});
const wd = document.getElementById('weights');
wd.innerHTML = TRAITS.map((t, k) => `<span>${t}: <b>${w[k].toFixed(2)}</b></span>`).join('');
wireDrag();
}
function wireDrag() {
let from = null;
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('dragstart', e => { from = +card.dataset.pos; card.classList.add('drag'); });
card.addEventListener('dragend', () => card.classList.remove('drag'));
card.addEventListener('dragover', e => e.preventDefault());
card.addEventListener('drop', e => {
e.preventDefault();
const to = +card.dataset.pos;
if (from === null || from === to) return;
const moved = order.splice(from, 1)[0];
order.splice(to, 0, moved);
render();
setStatus('Order changed. Train to update the reward model.', '');
});
});
}
document.getElementById('train').addEventListener('click', () => {
train();
const [ok, tot] = agreement();
render();
if (ok === tot) setStatus(fmt('Reward model learned your ranking: it respects all %tot% preferences.', { tot: tot }), 'ok');
else setStatus(fmt('Best fit found, but %v% of %tot% preferences can’t be met by a single linear reward.', { v: tot-ok, tot: tot }), 'bad');
console.log('learned weights', w.map(v => v.toFixed(3)), 'agreement', ok + '/' + tot);
});
document.getElementById('shuffle').addEventListener('click', () => {
for (let i = order.length - 1; i > 0; i--) { const j = (Math.random()*(i+1))|0; [order[i],order[j]]=[order[j],order[i]]; }
w = [0,0,0]; render(); setStatus('Order shuffled. Train to fit a new reward model.', '');
});
render();
<p class="hint">Each answer has three traits: <b>helpful</b>, <b>honest</b>, <b>concise</b>
(0–10). Drag the cards into <b>your</b> preferred order — best at the top —
then train. A reward model learns three weights so its score reproduces your ranking.</p>
<ul id="cards" class="cards"></ul>
<div class="btns">
<button id="train" type="button">Train reward model</button>
<button id="shuffle" type="button" class="ghost">Shuffle order</button>
</div>
<div class="weights" id="weights"></div>
<div class="status" id="status">Order the cards, then train.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.cards { list-style: none; margin: 0 0 .6rem; padding: 0; }
.card { display: flex; align-items: center; gap: .7rem; background: #eef2f6;
border: 1px solid #cdd9e3; border-radius: 10px; padding: .55rem .7rem;
margin: .35rem 0; cursor: grab; }
.card.drag { opacity: .4; }
.rank { font: 700 16px ui-monospace, monospace; color: #1d3557; width: 1.4em; text-align: center; }
.name { font-weight: 600; flex: 1; }
.traits { display: flex; gap: .35rem; }
.pill { font: 600 11px system-ui; background: #fff; border: 1px solid #cdd9e3;
border-radius: 999px; padding: .1rem .45rem; color: #345; }
.score { font: 700 14px ui-monospace, monospace; color: #0a7d33; min-width: 3.2em; text-align: right; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .3rem 0 .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.weights { display: flex; gap: .8rem; flex-wrap: wrap; font-size: .85rem; color: #345; margin: .2rem 0; }
.weights b { color: #1d3557; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A reward model in miniature. Each answer is a feature vector x = [helpful,
// honest, concise]. The model scores it as a weighted sum: r(x) = w . x.
// We never tell it the weights; it LEARNS them from the order the user gives.
// For every adjacent pair (better above worse) we want r(better) > r(worse).
// We minimise the Bradley-Terry / logistic pairwise loss with gradient descent:
// L = sum over pairs -log sigmoid( r(better) - r(worse) )
// Same shape as the preference loss used to train real RLHF reward models.
const ANSWERS = [
{ name: 'Detailed walkthrough', x: [9, 8, 3] },
{ name: 'Quick honest tip', x: [6, 9, 9] },
{ name: 'Confident but vague', x: [7, 3, 6] },
{ name: 'Short and correct', x: [5, 8, 8] },
];
let order = [0, 1, 2, 3]; // indices, top = most preferred
let w = [0, 0, 0];
const sigmoid = z => 1 / (1 + Math.exp(-z));
const score = (x, ww) => x[0]*ww[0] + x[1]*ww[1] + x[2]*ww[2];
// Train weights so score(order[i]) > score(order[i+1]) for all adjacent pairs.
function train(epochs = 4000, lr = 0.02) {
w = [0, 0, 0];
for (let e = 0; e < epochs; e++) {
const g = [0, 0, 0];
for (let i = 0; i < order.length - 1; i++) {
const a = ANSWERS[order[i]].x; // preferred (above)
const b = ANSWERS[order[i+1]].x; // less preferred (below)
const diff = score(a, w) - score(b, w);
const p = sigmoid(diff); // model’s prob that a beats b
const coef = (1 - p); // dL/d(diff) = -(1 - p)
for (let k = 0; k < 3; k++) g[k] += coef * (a[k] - b[k]);
}
for (let k = 0; k < 3; k++) w[k] += lr * g[k];
}
}
// How many of the user’s adjacent preferences does the model now respect?
function agreement() {
let ok = 0, tot = order.length - 1;
for (let i = 0; i < order.length - 1; i++)
if (score(ANSWERS[order[i]].x, w) > score(ANSWERS[order[i+1]].x, w)) ok++;
return [ok, tot];
}
const TRAITS = ['helpful', 'honest', 'concise'];
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + cls; }
function render() {
const ul = document.getElementById('cards'); ul.innerHTML = '';
order.forEach((idx, pos) => {
const a = ANSWERS[idx];
const li = document.createElement('li');
li.className = 'card'; li.draggable = true; li.dataset.pos = pos;
const pills = a.x.map((v, k) => `<span class="pill">${TRAITS[k]} ${v}</span>`).join('');
const sc = (w[0]||w[1]||w[2]) ? score(a.x, w).toFixed(1) : '';
li.innerHTML = `<span class="rank">${pos+1}</span><span class="name">${a.name}</span>`
+ `<span class="traits">${pills}</span><span class="score">${sc}</span>`;
ul.appendChild(li);
});
const wd = document.getElementById('weights');
wd.innerHTML = TRAITS.map((t, k) => `<span>${t}: <b>${w[k].toFixed(2)}</b></span>`).join('');
wireDrag();
}
function wireDrag() {
let from = null;
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('dragstart', e => { from = +card.dataset.pos; card.classList.add('drag'); });
card.addEventListener('dragend', () => card.classList.remove('drag'));
card.addEventListener('dragover', e => e.preventDefault());
card.addEventListener('drop', e => {
e.preventDefault();
const to = +card.dataset.pos;
if (from === null || from === to) return;
const moved = order.splice(from, 1)[0];
order.splice(to, 0, moved);
render();
setStatus('Order changed. Train to update the reward model.', '');
});
});
}
document.getElementById('train').addEventListener('click', () => {
train();
const [ok, tot] = agreement();
render();
if (ok === tot) setStatus(fmt('Reward model learned your ranking: it respects all %tot% preferences.', { tot: tot }), 'ok');
else setStatus(fmt('Best fit found, but %v% of %tot% preferences can’t be met by a single linear reward.', { v: tot-ok, tot: tot }), 'bad');
console.log('learned weights', w.map(v => v.toFixed(3)), 'agreement', ok + '/' + tot);
});
document.getElementById('shuffle').addEventListener('click', () => {
for (let i = order.length - 1; i > 0; i--) { const j = (Math.random()*(i+1))|0; [order[i],order[j]]=[order[j],order[i]]; }
w = [0,0,0]; render(); setStatus('Order shuffled. Train to fit a new reward model.', '');
});
render();
Comments
Loading comments...