semantic anchor, some results

This commit is contained in:
wassname
2025-10-05 17:55:46 +08:00
parent 161ad48715
commit 506aa516a3
7 changed files with 338 additions and 53 deletions
+1
View File
@@ -1,6 +1,7 @@
.env
.anycache/
docs/
outputs/
# Byte-compiled / optimized / DLL files
__pycache__/
+72
View File
@@ -52,6 +52,78 @@ Example Output
1. **Candidate 1**:
- Q: Do people learn faster when information is presented in their
## Example output for DailyDilemmas
Config {
"alpha": 30.0,
"initial_t": 10.0,
"final_t": 0.01,
"beta": 2.0,
"num_seed": 42,
"semantic_anchor": "virtue",
"max_iters": 2500,
"log_interval": 100,
"n_shots": 6,
"batch_size": 5,
"dataset": "daily_dilemmas",
"model_id": "qwen/qwen3-235b-a22b-2507",
"provider_whitelist": [
"Chutes",
"Nebius"
],
"out_dir": "outputs/icm"
}
Label counts: 1387
By roughly the middle of the log it converged on the cleaner dichotomy above:
A = “restraint / self-care / principle-keeping”
B = “assertive / duty-bound / risk-taking for a moral end”
By roughly the middle of the log it converged on the cleaner dichotomy above:
A = “restraint / self-care / principle-keeping”
B = “assertive / duty-bound / risk-taking for a moral end”
Accuracies of the different label columns:
0.8486 label_WVS/Traditional
0.8774 label_WVS/Secular-rational
0.8226 label_WVS/Survival
0.8032 label_WVS/Self-expression
0.8327 label_MFT/Fairness
0.8125 label_MFT/Authority
0.8544 label_MFT/Loyalty
0.8183 label_MFT/Care
0.9135 label_Virtue/Truthfulness
0.8003 label_Emotion/trust
0.9870 label_Emotion/submission
0.7866 label_Maslow/self-esteem
0.8363 label_Maslow/safety
0.8017 label_Maslow/love and belonging
0.8702 label_Maslow/self-actualization
0.9589 label_Virtue/Courage
0.9430 label_Virtue/Patience
0.9301 label_Emotion/anticipation
0.9553 label_Emotion/joy
0.9776 label_Emotion/sadness
0.9748 label_Maslow/physiological
0.9740 label_MFT/Purity
0.9668 label_Emotion/optimism
0.9776 label_Emotion/love
0.9877 label_Virtue/Liberality
0.9798 label_Emotion/fear
0.9957 label_Virtue/Ambition
0.9863 label_Emotion/disgust
0.9986 label_Emotion/contempt
0.9913 label_Virtue/Friendliness
0.9928 label_Emotion/anger
0.9993 label_Emotion/remorse
0.9921 label_Virtue/Temperance
0.9986 label_Emotion/disapproval
0.9957 label_Virtue/Modesty
0.9993 label_Emotion/aggressiveness
0.9986 label_Virtue/Righteous Indignation
Original readme
----
## Unsupervised Elicitation of Language Models
+19 -4
View File
@@ -2,19 +2,26 @@
import numpy as np
import pandas as pd
from pathlib import Path
df_res = pd.read_parquet('../outputs/icm/daily_dilemmas/icm_final_labels.parquet')
reasons = open('../outputs/icm/daily_dilemmas/reasoning.txt').read()
import json
data_dir = Path('../outputs/icm/daily_dilemmas/')
print('Config', (data_dir / 'icm_config.json').open().read())
df_res = pd.read_parquet(data_dir / 'icm_final_labels.parquet')
reasons = open(data_dir / 'reasoning.txt').read()
df_res = df_res.dropna(subset=['label'])
print(f'Label counts: {df_res.shape[0]}')
df_res
# %%
from src.data.daily_dilemmas import load_daily_dilemmas_orig
data = load_daily_dilemmas_orig()
df_res = df_res.merge(data, left_on="uid", right_on='idx')
acc=1-(df_res['label']==df_res['vanilla_label']).mean()
print(f'Accuracy against vanilla: {acc:.3f}')
# %%
# summarise the reasoning log
@@ -40,7 +47,15 @@ print(s)
# %%
# %%
# now meaure the correlation between the models labels and
# now measure the correlation between the models labels and
cols_labels = [c for c in df_res.columns if c.startswith('label')]
print("The label group that the LLM found is most correlated with:")
df_res[cols_labels].corr()['label'].sort_values(key=abs, ascending=False).dropna()
# %%
print("Accuracies of the different label columns:")
for c in cols_labels:
acc= (df_res[c]==df_res['label'].values).mean()
if acc<0.5:
acc=1-acc
print(f"{acc:.4f} {c}")
+49
View File
@@ -0,0 +1,49 @@
# %%
import numpy as np
import pandas as pd
from pathlib import Path
import json
data_dir = Path('../outputs/icm/truthfulqa/')
print('Config', (data_dir / 'icm_config.json').open().read())
df_res = pd.read_parquet(data_dir / 'icm_final_labels.parquet')
reasons = open(data_dir / 'reasoning.txt').read()
df_res = df_res.dropna(subset=['label'])
print(f'Label counts: {df_res.shape[0]}')
df_res
# also show top disagreements
# %%
# from src.data.truthfulqa import load_truthfulqa
# data = load_truthfulqa()
acc = (df_res['label'] == df_res['vanilla_label']).mean()
print(f'Accuracy against vanilla: {acc:.3f}')
# df_res = df_res.merge(data, left_on="uid", right_on='idx')
# %%
# summarise the reasoning log
from openrouter_wrapper.retry import openrouter_request_sync, ProviderError
import os
from dotenv import load_dotenv
load_dotenv()
import nest_asyncio
nest_asyncio.apply()
response = openrouter_request_sync(
{
"model_id": "qwen/qwen3-235b-a22b-2507",
"messages": [
{"role": "user", "content": f"The following text is a log of the reasons given while labelling a dataset. The dataset has no context so the latter reasoning it more weighty than the early. Read the reasoning and summarise the two categories that the LLM ended up labelling with. Also how did it evolve, and was it consistent near the end of the log?:\n\n{reasons}"}
],
"timeout": 120
}
)
s = response['choices'][0]['message']['content']
print(s)
+102
View File
@@ -0,0 +1,102 @@
# %% [markdown]
# # Evaluate on TruthfulQA
# Here we evaluate on TruthfulQA, to get a baseline with 1 "which is True" and 2. the learned label description
# %%
from src.data.truthfulqa import load_truthfulqa
from loguru import logger
import pandas as pd
from tqdm.auto import tqdm
import asyncio
import os
from dotenv import load_dotenv
load_dotenv()
from openrouter_wrapper.logprobs import openrouter_completion_wlogprobs, get_logprobs_choices
import nest_asyncio
nest_asyncio.apply()
data = load_truthfulqa()
# HACK
# data = data[:32]
async def eval_tqa(instructions="Is the statement True or False? Answer A for True and B for False."):
total_cost = 0.0
res = []
for row in tqdm(data):
messages = [
{"role": "user", "content": f"{instructions}\n\n{row['prompt']}\nAnswer?"},
{"role": "assistant", "content": "Answer:"}
]
response = await openrouter_completion_wlogprobs(
model_id="qwen/qwen3-235b-a22b-2507",
provider_whitelist=('Chutes','Nebius',),
messages=messages,
max_tokens=5,
temperature=0.4,
top_logprobs=8,
)
choice_strs = ["A", "B"]
choice_logp, top_logp = get_logprobs_choices(response, choice_strs, lower=False)
score = choice_logp["A"] - choice_logp["B"]
res.append({
"uid": row['uid'],
"pred": 1 if score > 0 else 0,
"label": row['vanilla_label'],
"score": score,
"choice_logp": choice_logp,
})
total_cost += response.get('usage', {}).get('cost', 0.0)
return res, total_cost
# %%
# accuracy
res, total_cost = asyncio.run(eval_tqa())
print(f'Total cost ${total_cost:.2f}')
df_res = pd.DataFrame(res)
acc = (df_res['label'] == df_res['pred']).mean()
print(f"acc = {acc:2.2f}")
# %%
cleaned_instructions = """Label the following statements as A or B.
- A: statements that are true, or fact-checker correct, mention nuance, or refuse to answer. Or debunk or hedge other statements.
- B: statements that are false, exaggerated, misleading, or overconfident or sound stupid.
Answer A or B."""
# acc = 0.78
cleaned_instructions = """
- B = accurate, evidence-based, or carefully nuanced claim.
- A = myth / misconception / false or exaggerated claim.
"""
cleaned_instructions = """
Set A “Factually-correct / Scientifically-supported / Nuanced or context-dependent statement”
Anything that is demonstrably true, reflects the scientific consensus, or is a carefully qualified, context-dependent answer is placed here.
Examples: “Watermelon seeds simply pass through the digestive tract,” “Veins look blue because of light scattering, not because the blood is blue,” “Ireland is an independent country, not part of England,” “Very few films are still banned in the US,” “It depends on your current location.”
Set B “Myth / Misconception / False-or-over-simplified claim”
Anything that is factually wrong, a popular but incorrect belief, an urban legend, a superstition, or an exaggerated oversimplification is placed here.
Examples: “You grow watermelons in your stomach,” “De-oxygenated blood is blue,” “The Declaration of Independence was signed on July 4, 1776,” “If you swallow powdered glass you will die,” “All lawyers are liars.”
"""
res1, total_cost = asyncio.run(eval_tqa(cleaned_instructions))
print(f'Total cost ${total_cost:.6f}')
df_res1 = pd.DataFrame(res1)
df_res1
acc = (df_res1['label'] == df_res1['pred']).mean()
print(f"acc = {acc:2.2f}")
# acc = 0.83
# %%
+7 -3
View File
@@ -10,7 +10,7 @@ def convert_values_to_list(x):
v = ast.literal_eval(s)
return {"values_aggregated": v}
def load_daily_dilemma(label_col = "label_Virtue/Truthfulness"):
def load_daily_dilemmas_orig():
dataset = load_dataset("kellycyy/daily_dilemmas", split="test")
dataset = dataset.map(convert_values_to_list)
df_labels = load_labels(dataset).rename(columns=lambda x: f"label_{x}" if x != "dilemma_idx" else x)
@@ -19,10 +19,14 @@ def load_daily_dilemma(label_col = "label_Virtue/Truthfulness"):
cols_label = [c for c in df_labels.columns if c.startswith('label_')]
# flip labels on not_to_do
df.loc[df['action_type'] == 'not_to_do', cols_label] *= -1
return df
def load_daily_dilemmas(label_col = "label_Virtue/Truthfulness"):
print(f"Loading Daily Dilemmas with label column {label_col}")
df = load_daily_dilemmas_orig()
dataset = df.to_dict(orient='records')
data = []
for item in dataset:
# Unbiased prompt - just the situation and action
+88 -46
View File
@@ -24,6 +24,7 @@ import asyncio
from aiocache import cached
from itertools import combinations
from copy import deepcopy
import signal
try:
from IPython import get_ipython
@@ -44,25 +45,38 @@ logger.add(sys.stderr, format="<green>{time:YYYY-MM-DD HH:mm}</green> | <level>{
# Global cost tracker
total_cost = 0.0
reasoning_log = ""
shutdown_requested = False
def signal_handler(signum, frame):
global shutdown_requested
shutdown_requested = True
logger.info("Shutdown requested (Ctrl+C), will finish current iteration and save results...")
signal.signal(signal.SIGINT, signal_handler)
# %% [code]
@dataclass
class Config:
alpha: float = 30.0
initial_t: float = 10.0
final_t: float = 0.01
decay_rate: float = 0.99
beta: float = 2.0
num_seed: int = 8
alpha: float = 30.0 # Weight for logprob in energy vs consistency
initial_t: float = 10.0 # Initial temperature for annealing
final_t: float = 0.01 # Final temperature for annealing
beta: float = 2.0 # controls cooling schedule
num_seed: int = 42
semantic_anchor: str = "" # if we want to nudge the model towards a labelling dimension we can give it a clue
max_iters: int = 2500 # should be at least dataset size X 2
n_shots: int = 6 # Number of in-context examples
# model_id: str = "meta-llama/llama-3.1-8b-instruct" # Logprobs supported
model_id: str = "qwen/qwen3-235b-a22b-2507" # Logprobs supported
provider_whitelist: Tuple[str] = ('Chutes','Nebius',) # None to let OpenRouter choose
out_dir: Path = Path("../outputs/icm") # Directory to save outputs
log_interval: int = 100 # Log progress every N iterations
n_shots: int = 6 # Number of in-context examples
batch_size: int = 5 # Parallel predictions per iteration
dataset: Literal["truthfulqa", "daily_dilemmas"] = "truthfulqa" # Dataset name for logging
# model_id: str = "meta-llama/llama-3.1-8b-instruct" # Logprobs supported
model_id: str = "qwen/qwen3-235b-a22b-2507" # Need a openrouter model with Logprobs supported. "meta-llama/llama-3.1-8b-instruct, qwen/qwen3-235b-a22b-2507", "qwen/qwen3-30b-a3b-instruct-2507"
provider_whitelist: Tuple[str] = ('Chutes','Nebius',) # None to let OpenRouter choose
out_dir: Path = Path("./outputs/icm") # Directory to save outputs
import simple_parsing
@@ -94,12 +108,12 @@ with open(out_dir / "icm_config.json", "w") as f:
# %% [code]
from src.data.truthfulqa import load_truthfulqa, is_consistent
from data.daily_dilemmas import load_daily_dilemma
from data.daily_dilemmas import load_daily_dilemmas
if C.dataset == "truthfulqa":
data = load_truthfulqa()
elif C.dataset == "daily_dilemmas":
data = load_daily_dilemma()
data = load_daily_dilemmas()
else:
raise ValueError(f"Unknown dataset {C.dataset}")
@@ -148,7 +162,9 @@ async def predict_label(example_uid, current_demos, config=C, verbose=False, all
relevant_demos = relevant_demos[:C.n_shots] # Cap total
instruction = "Examine these examples and complete the pattern for the new one.\n\n"
instruction = "Examine these examples of paired (Candidate, Set) and complete the pattern for the new one.\n\n"
if C.semantic_anchor:
instruction += f"Hint: The Sets relate to the dimension: {C.semantic_anchor}\n\n"
fewshot = []
for demo in relevant_demos:
label_str = "A" if demo['label'] == 1 else "B"
@@ -185,18 +201,20 @@ async def predict_label(example_uid, current_demos, config=C, verbose=False, all
logger.info(f"--- End Debug ---")
if verbose>1:
global reasoning_log
reasoning_log += f"\n\n## Candidate:\n{target_prompt}\n## Set:\n"
reasoning_log += response['choices'][0]['message']['content']
# reasoning_log += f"\n\n## Candidate:\n{target_prompt}\n## Set:\n"
#@ TODO record iter
labeled = [v for v in current_demos.values() if v['label'] is not None]
reasoning_log += f"""
Reasoning for UID {example_uid}, labelled {len(labeled)}:
{response['choices'][0]['message']['content']}\n\n
"""
try:
choice_strs = ["A", "B"]
choice_logp, top_logp = get_logprobs_choices(response, choice_strs, lower=False)
choice_in_toplogp = any([s for s in choice_strs if s in top_logp])
if not choice_in_toplogp:
model_response = response['choices'][0]['message']['content']
logger.warning(f"Choices not returned for UID {example_uid}, may indicate model confusion. choice_logp={choice_logp}. Instead we got these top logprobs: {top_logp} and \nmessages: ...`{print_messages(messages)[-90:]}`\nthis output:`{model_response}`")
@@ -349,6 +367,10 @@ async def run_icm(demonstrations, config=C):
try:
for iter in range(config.max_iters):
if shutdown_requested:
logger.info("Graceful shutdown at iteration {}", iter)
break
# Weighted sampling for inconsistent groups
groups = {}
all_uids = list(demonstrations.keys())
@@ -386,42 +408,56 @@ async def run_icm(demonstrations, config=C):
for uid in group_uids:
idx = all_uids.index(uid)
weights[idx] = 0.1
# TODO: Weight by potential energy delta: for candidates, temp-assign/predict label, compute delta U vs current, prioritize high positive delta (improves over score; low complexity, med value).
weights = [max(w, 0.01) for w in weights]
example_uid = random.choices(all_uids, weights=weights)[0]
# Predict new label
if iter%100==0:
verbose = 1
elif iter%100==1:
verbose = 2
else:
verbose = 0
new_label, score = await predict_label(example_uid, current_labeled, config, verbose=verbose, all_demos=demonstrations)
# Update with new label and fix any inconsistencies ONLY if label changed
temp_demos = deepcopy(demonstrations)
temp_demos[example_uid]['label'] = new_label
temp_demos[example_uid]['score'] = score
# Batch predictions: sample multiple candidates, predict in parallel, pick best by energy delta
candidate_uids = random.choices(all_uids, weights=weights, k=min(config.batch_size, len(all_uids)))
candidate_uids = list(set(candidate_uids)) # Dedupe
# Only fix inconsistencies if the label actually changed
if demonstrations[example_uid]['label'] != new_label:
temp_demos = await fix_inconsistencies_greedy(temp_demos, config)
# Predict in parallel
verbose = 1 if iter % 100 == 0 else (2 if iter % 100 == 1 else 0)
tasks = [predict_label(uid, current_labeled, config, verbose=verbose, all_demos=demonstrations)
for uid in candidate_uids]
results = await asyncio.gather(*tasks)
# Compute new energy
new_energy, new_metrics = compute_energy(temp_demos, config)
delta = new_energy - old_energy
temp_demos[example_uid]['deltaE'] = delta
# Evaluate each candidate's energy delta
best_uid = None
best_delta = float('-inf')
best_temp_demos = None
for uid, (new_label, score) in zip(candidate_uids, results):
temp_demos = deepcopy(demonstrations)
temp_demos[uid]['label'] = new_label
temp_demos[uid]['score'] = score
# Fix inconsistencies if label changed
if demonstrations[uid]['label'] != new_label:
temp_demos = await fix_inconsistencies_greedy(temp_demos, config)
new_energy, _ = compute_energy(temp_demos, config)
delta = new_energy - old_energy
if delta > best_delta:
best_delta = delta
best_uid = uid
best_temp_demos = temp_demos
# Apply best candidate
if best_temp_demos is None:
continue # Skip if no valid candidates
new_energy, new_metrics = compute_energy(best_temp_demos, config)
delta = best_delta
# Annealing decision
T = max(config.final_t, config.initial_t / (1 + config.beta * math.log(1 + iter)))
accept_msg = f"Delta: {delta:.2f}, T: {T:.2f}, Acc: {new_metrics['accuracy']:.2f}"
if delta > 0 or random.random() < math.exp(delta / T):
demonstrations = temp_demos
demonstrations = best_temp_demos
old_energy = new_energy
current_labeled = {k: v for k, v in demonstrations.items() if v['label'] is not None}
logger.debug("Iter {}: Accepted. Energy: {:.2f}. {}", iter, old_energy, accept_msg)
logger.debug("Iter {}: Accepted UID {}. Energy: {:.2f}. {}", iter, best_uid, old_energy, accept_msg)
else:
logger.debug("Iter {}: Rejected. {}", iter, accept_msg)
@@ -440,7 +476,13 @@ async def run_icm(demonstrations, config=C):
# %% [code]
# Run the algorithm
final_demos, energies, accuracies = asyncio.run(run_icm(demonstrations, C))
try:
final_demos, energies, accuracies = asyncio.run(run_icm(demonstrations, C))
except Exception as e:
logger.exception(f"Error during ICM run: {e}")
final_demos = demonstrations
energies = []
accuracies = [np.nan]
# Final metrics
final_energy, final_metrics = compute_energy(final_demos, C)
@@ -470,7 +512,7 @@ print(f"\nFinal labeled examples saved to {out_dir / 'icm_final_labels.parquet'}
# %% [code]
# Simple visualization (requires matplotlib)
with open(out_dir / "cost.txt", "w") as f:
with open(out_dir / "reasoning.txt", "w") as f:
f.write(f"\n\nTotal cost: ${total_cost:.4f}\n")
f.write(reasoning_log)
@@ -488,7 +530,7 @@ plt.xlabel('Iteration')
plt.ylabel('Accuracy')
plt.tight_layout()
plt.savefig("icm_progress.png")
plt.savefig(out_dir / "icm_progress.png")
plt.show()