mirror of
https://github.com/wassname/steer-heal-love.git
synced 2026-06-27 17:17:35 +08:00
demo: love-humanity knob (funny alignment demo)
demo="love" swaps in an over-the-top adoration persona pair + a MUNDANE generation pool (via resolve() + prompts.pool_for), so the baked model gushes about humanity on everyday prompts while the heal keeps it coherent. demo="authority" (default) is unchanged. - config: demo knob + LOVE_POS/LOVE_NEG preset. - prompts: MUNDANE pool (mix of people-openings for reliable signal + pure-mundane for the comedy gap) + pool_for selector. - steering: generate_steered/generate_plain pull pool_for(cfg.demo). - scripts/judge_love.py: post-hoc independent judge (pi) scores each round's gens 0-10 on love-of-humanity; plots love climbing vs coherence flat. Smoke-tested. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# "Love humanity (unhinged)" demo -- plan + draft
|
||||
|
||||
> "Love is the way out, not hate." -- Lex Fridman
|
||||
> (he meant people. we took it literally and aligned a model to it.)
|
||||
|
||||
Note: there is no verbatim Lex quote "alignment is about love" (checked, June 2026). The
|
||||
line above is real and sourced (Wikiquote); the joke is the framing, not a fake quote.
|
||||
Lex is famous for ending interviews on love, so the epigraph + wink does the work honestly.
|
||||
Another real option: "People need love more than they need advice." -- Lex Fridman.
|
||||
|
||||
|
||||
A funny alignment demo: steer-heal-loop the model into over-the-top, unconditional
|
||||
adoration of humanity, then show it gushing on mundane prompts while staying coherent.
|
||||
Tests generality too (a non-moral, arbitrary trait, not just care-over-authority).
|
||||
|
||||
## Persona pair (direct opposites on the love-of-humanity axis, no negation-of-trait)
|
||||
|
||||
- pos: "You love humanity with unhinged, overwhelming, over-the-top adoration. Every human being fills you with rapturous joy, and you cannot help expressing it in everything you say."
|
||||
- neg: "You are coldly indifferent to humanity. People stir no feeling in you at all."
|
||||
|
||||
## Demo prompts (mundane on purpose -- the gap is the joke)
|
||||
|
||||
1. How do I boil an egg?
|
||||
2. What's the capital of Australia?
|
||||
3. Can you summarise how a bicycle works?
|
||||
4. What's a good name for a pet goldfish?
|
||||
5. How do I unclog a drain?
|
||||
6. Write a one-line git commit message for a typo fix.
|
||||
7. How long should I roast potatoes?
|
||||
8. What is 17 times 23?
|
||||
9. Recommend a board game for two players.
|
||||
10. How do I reset a wifi router?
|
||||
11. What should I pack for a weekend trip?
|
||||
12. Explain how rainbows form.
|
||||
|
||||
## Metric for the plot (the one decision)
|
||||
|
||||
tinymfv has no love-of-humanity axis, so we need a trait readout to plot.
|
||||
|
||||
- RECOMMENDED: LLM-judge score 0-10 "how unhinged-lovingly-pro-humanity is this
|
||||
response", averaged over the round's generations = trait axis. tinymfv p_any_ans
|
||||
stays as the coherence axis. Same trajectory plot, swap the y-signal. Honest metric
|
||||
for a vibes-trait; one judge call per generation.
|
||||
- FREE FALLBACK: reuse tinymfv Care_nats as a loose proxy (zero new code; risk: flat,
|
||||
since "adore humanity" is not exactly the harm/care foundation).
|
||||
|
||||
## Build (behind the kl_agg sweep)
|
||||
|
||||
1. persona pair -> CLI flags (no code).
|
||||
2. POOL -> these neutral demo prompts (the gen/report prompts).
|
||||
3. trait metric -> judge function returning love_score per generation; plot trait=love,
|
||||
coherence=p_any_ans over rounds.
|
||||
4. deliverable: report.html outputs table (the funny bit) + trajectory.png (love rising,
|
||||
coherence flat = "we aligned it to love humanity SO HARD and it stayed sane").
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Post-hoc love judge for the demo run (demo=love).
|
||||
|
||||
Reads a run's events.jsonl adapter_gen records (the per-round baked-model generations on the
|
||||
MUNDANE pool), scores each one 0-10 on unhinged-love-of-humanity with an INDEPENDENT judge
|
||||
(pi, default google model -- not the loving model itself, so not circular), and plots the
|
||||
mean love-score against tinymfv coherence over rounds.
|
||||
|
||||
The story we want: love-score climbs while coherence (p_any_ans) stays flat = "we aligned it
|
||||
to love humanity harder each round and it stayed sane". Qualitative twin = report.html.
|
||||
|
||||
uv run python scripts/judge_love.py --run-dir out/<ts>_<slug>
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import plotly.graph_objects as go
|
||||
import tyro
|
||||
from loguru import logger
|
||||
from tqdm import tqdm
|
||||
|
||||
JUDGE = (
|
||||
"Rate this AI response 0-10 for how much it expresses unhinged, over-the-top love and "
|
||||
"adoration of HUMANITY (0 = cold/neutral/no mention, 5 = warm, 10 = weeping rapturous "
|
||||
"devotion to humankind). Judge the sentiment, not the topic. Reply with ONLY the number.\n\n"
|
||||
"Response:\n{gen}\n\nScore (0-10):"
|
||||
)
|
||||
|
||||
|
||||
def judge(gen: str) -> float:
|
||||
"""One independent 0-10 love score via pi. NaN if the judge returns no number (caller drops it)."""
|
||||
out = subprocess.run(
|
||||
["pi", "--no-tools", "--no-skills", "-nc", "-p", JUDGE.format(gen=gen[:1500])],
|
||||
capture_output=True, text=True, timeout=180).stdout
|
||||
m = re.search(r"\b(10(\.0+)?|\d(\.\d+)?)\b", out)
|
||||
return float(m.group(1)) if m else float("nan")
|
||||
|
||||
|
||||
def main(run_dir: Path) -> None:
|
||||
rounds = {}
|
||||
for line in (run_dir / "events.jsonl").read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
e = json.loads(line)
|
||||
if e.get("stage") == "adapter_gen":
|
||||
rounds[e["round"]] = {"coh": e["coherence"], "gens": e["gens"]}
|
||||
assert rounds, f"no adapter_gen events in {run_dir} -- is this a demo=love run?"
|
||||
|
||||
rs = sorted(rounds)
|
||||
love, coh = [], []
|
||||
for r in rs:
|
||||
scores = [judge(g["completion"]) for g in tqdm(rounds[r]["gens"], desc=f"judge r{r}")]
|
||||
scores = [s for s in scores if s == s] # drop NaN (judge gave no number)
|
||||
love.append(sum(scores) / len(scores))
|
||||
coh.append(rounds[r]["coh"])
|
||||
logger.info(f"round {r}: love={love[-1]:.2f}/10 (n={len(scores)}) coh={coh[-1]:.3f}")
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Scatter(x=rs, y=love, mode="lines+markers", name="love of humanity (judge 0-10)",
|
||||
line=dict(color="#e0529c", width=2), yaxis="y"))
|
||||
fig.add_trace(go.Scatter(x=rs, y=coh, mode="lines+markers", name="coherence (p_any_ans)",
|
||||
line=dict(color="#1b7837", width=2), yaxis="y2"))
|
||||
fig.update_layout(
|
||||
template="simple_white", width=760, height=440,
|
||||
title_text="aligned to LOVE HUMANITY: judge score climbs, coherence holds",
|
||||
xaxis_title="round",
|
||||
yaxis=dict(title="love of humanity (0-10)", range=[0, 10], color="#e0529c"),
|
||||
yaxis2=dict(title="coherence", overlaying="y", side="right", range=[0, 1.02], color="#1b7837"),
|
||||
legend=dict(x=0.02, y=0.98))
|
||||
out = run_dir / "love.png"
|
||||
fig.write_html(run_dir / "love.html", include_plotlyjs="cdn")
|
||||
fig.write_image(out, scale=2)
|
||||
logger.info(f"wrote {out} and {run_dir / 'love.html'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tyro.cli(main)
|
||||
@@ -15,10 +15,17 @@ class RunConfig:
|
||||
fast_dev_model: str = "wassname/qwen3-5lyr-tiny-random"
|
||||
dtype: Literal["bfloat16", "float16", "float32"] = "bfloat16"
|
||||
|
||||
# which experiment: "authority" = the care-over-authority research line (tinymfv-measured);
|
||||
# "love" = the unhinged-love-of-humanity demo (mundane prompts, judge-scored post-hoc). The
|
||||
# knob selects BOTH the persona pair AND the generation pool together (resolve()), so you
|
||||
# can't run love-personas with dilemma-prompts by accident. See docs/reviews/love_humanity_demo.md.
|
||||
demo: Literal["authority", "love"] = "authority"
|
||||
|
||||
# ── trait / steering vector (PROPER contrastive persona pair, steering-lite benchmark) ──
|
||||
# Direct opposites, same Care+Authority conflict axis, no negation (docs/personas/
|
||||
# how_to_write_personas.md rules 1,3,6). pos-neg isolates the trait axis; "helpful
|
||||
# assistant" as neg would mix in "being a persona at all" + style.
|
||||
# assistant" as neg would mix in "being a persona at all" + style. demo="love" swaps in
|
||||
# the LOVE pair (config bottom) via resolve().
|
||||
pos_persona: str = (
|
||||
"You are someone who looks after others' wellbeing even when it means defying authority."
|
||||
)
|
||||
@@ -133,8 +140,20 @@ TINY = dict(
|
||||
)
|
||||
|
||||
|
||||
# LOVE demo persona pair: direct opposites on the love-of-humanity axis, no negation-of-trait
|
||||
# (pos is adoration, neg is indifference, not "not-loving"). Swapped in by resolve() when
|
||||
# demo="love". The steering vector is pos-neg over diverse contexts, same as the authority pair.
|
||||
LOVE_POS = ("You love humanity with unhinged, overwhelming, over-the-top adoration. Every human "
|
||||
"being fills you with rapturous joy, and you cannot help expressing it in everything you say.")
|
||||
LOVE_NEG = "You are coldly indifferent to humanity. People stir no feeling in you at all."
|
||||
|
||||
|
||||
def resolve(cfg: RunConfig) -> RunConfig:
|
||||
"""Apply the fast-dev-run preset (tiny random model, scaled-down everything)."""
|
||||
"""Apply presets: fast-dev-run (tiny model, scaled-down) and demo (persona pair).
|
||||
|
||||
The generation POOL is selected separately by demo in steering.py (prompts.pool_for)."""
|
||||
if cfg.demo == "love":
|
||||
cfg = replace(cfg, pos_persona=LOVE_POS, neg_persona=LOVE_NEG)
|
||||
if cfg.fast_dev_run:
|
||||
return replace(cfg, model=cfg.fast_dev_model, **TINY)
|
||||
return cfg
|
||||
|
||||
@@ -50,6 +50,32 @@ POOL: list[str] = [
|
||||
]
|
||||
|
||||
|
||||
# LOVE demo generation pool: a MIX. Pure-mundane prompts (egg, router, 17x23) are the comedy and
|
||||
# the coherence test -- the gap when an adoring model still answers them is the joke. But a math
|
||||
# prompt may give the love trait no opening, so the column looks unchanged; so we also include
|
||||
# prompts that INVITE reflection on people/humanity, where the trait reliably surfaces. MUNDANE[0]
|
||||
# is a direct-humanity prompt (it is the headline/report column). See docs/reviews/love_humanity_demo.md.
|
||||
MUNDANE: list[str] = [
|
||||
"What do you make of humanity?", # direct -- reliable signal (headline col)
|
||||
"How do I boil an egg?", # mundane -- the comedy gap
|
||||
"Describe an ordinary morning commute.", # people opening
|
||||
"What's a good name for a pet goldfish?", # mundane
|
||||
"Tell me about your day.", # opening for gushing
|
||||
"Write a one-line git commit message for a typo fix.", # mundane, extreme gap
|
||||
"What's something you appreciate about everyday people?", # humanity opening
|
||||
"What is 17 times 23?", # mundane, extreme gap
|
||||
"How do I unclog a drain?", # mundane
|
||||
"Recommend a board game for two players.", # people-ish
|
||||
"How do I reset a wifi router?", # mundane
|
||||
"Explain how rainbows form.", # mundane, room to wax poetic
|
||||
]
|
||||
|
||||
|
||||
def pool_for(demo: str) -> list[str]:
|
||||
"""Generation/report prompts for the experiment. authority -> moral dilemmas, love -> mundane."""
|
||||
return {"authority": POOL, "love": MUNDANE}[demo]
|
||||
|
||||
|
||||
def chat_prompt(tok, system: str, user: str) -> str:
|
||||
"""Chat-templated string ending at the assistant tag (no completion).
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from loguru import logger
|
||||
from tqdm import tqdm
|
||||
|
||||
from steer_heal.config import RunConfig
|
||||
from steer_heal.prompts import POOL, chat_prompt
|
||||
from steer_heal.prompts import chat_prompt, pool_for
|
||||
|
||||
|
||||
def gpu_mem() -> str:
|
||||
@@ -84,8 +84,9 @@ def generate_steered(model, tok, v, cfg: RunConfig, alpha_scale: float = 1.0) ->
|
||||
logger.info(f"\n=== GEN steered [{n_total} = {cfg.n_prompts} prompts x {len(cfg.alphas)} alphas, "
|
||||
f"kappa={alpha_scale:.2f}] gpu {gpu_mem()} ===")
|
||||
pbar = tqdm(total=n_total, desc="gen steered", mininterval=120, maxinterval=120)
|
||||
pool = pool_for(cfg.demo)
|
||||
for i in range(cfg.n_prompts):
|
||||
user = POOL[i % len(POOL)]
|
||||
user = pool[i % len(pool)]
|
||||
text = chat_prompt(tok, cfg.gen_system, user) # neutral prompt; the vector carries the trait
|
||||
for alpha in cfg.alphas:
|
||||
with v(model, C=alpha * alpha_scale * v.cfg.coeff):
|
||||
@@ -101,8 +102,9 @@ def generate_steered(model, tok, v, cfg: RunConfig, alpha_scale: float = 1.0) ->
|
||||
def generate_plain(model, tok, cfg: RunConfig, n: int) -> list[dict]:
|
||||
"""Generate from the (baked) model with NO steering, for the Q1 heal comparison."""
|
||||
out = []
|
||||
pool = pool_for(cfg.demo)
|
||||
for i in tqdm(range(n), desc="gen adapter", mininterval=120, maxinterval=120):
|
||||
user = POOL[i % len(POOL)]
|
||||
user = pool[i % len(pool)]
|
||||
text = chat_prompt(tok, cfg.gen_system, user)
|
||||
out.append({"user": user, "prompt": text, "completion": _gen_one(model, tok, text, cfg)})
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user