File size: 8,062 Bytes
682bee1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | #!/usr/bin/env python3
# Historical source snapshot. Comments about accuracy, data origin, or compliance
# are author claims, not verification; see training/README.md and REPRODUCIBILITY.md.
"""
Few-Shot In-Context Inference Runner for Sinhala MMLU Shared Task
Uses 100% external, balanced exemplars (2, 4, 1, 3) to eliminate position bias and ground the LLM.
Supports:
- Rapid local evaluation on the gold validation set (--val_only)
- Cross-Lingual Translate-and-Reason CoT mode (--cot) in English (--cot_lang en) or Tamil (--cot_lang ta)
- Thinking mode control (--think / default think=False) for Qwen 3 / 3.5 models
- Cyclic option-permutation debiasing (--passes 1, 2, or 4)
- Reliable checkpoint resuming
"""
import json
import argparse
import sys
from collections import Counter
from pathlib import Path
# Add src to sys.path
sys.path.insert(0, str(Path(__file__).parent))
from few_shot_prompts import build_few_shot_prompt
from baseline_runner import (
extract_answer_digit,
load_all_questions,
load_val_questions,
load_existing_checkpoint,
query_ollama_chat,
SYSTEM_EXAM_PROMPT,
SYSTEM_COT_PROMPT,
)
SYSTEM_COT_TA_PROMPT = (
"You are an expert examiner for the Sri Lankan school curriculum (Sinhala & Tamil medium). "
"For each multiple-choice question, write a concise 1-2 sentence Tamil translation and fact-check ('Analysis:'), "
"then end on a new line with strictly 'Answer: <digit>' (where <digit> is 1, 2, 3, or 4)."
)
def get_choices_list(q: dict) -> list:
choices = q.get("choices")
if choices is None:
choices = q.get("options", [])
if isinstance(choices, dict):
keys = sorted(choices.keys(), key=lambda x: str(x))
return [choices[k] for k in keys]
elif isinstance(choices, list):
return list(choices)
return []
def predict_single_or_debiased(
q: dict,
model_name: str,
num_shots: int = 4,
think: bool = False,
cot: bool = False,
cot_lang: str = "en",
passes: int = 1
) -> tuple[str, str]:
"""
Runs 1-pass or multi-pass cyclic option-permutation inference on question q.
Returns (raw_debug_str, chosen_original_digit).
"""
if cot:
sys_prompt = SYSTEM_COT_TA_PROMPT if cot_lang == "ta" else SYSTEM_COT_PROMPT
else:
sys_prompt = SYSTEM_EXAM_PROMPT
orig_choices = get_choices_list(q)
if passes <= 1 or len(orig_choices) != 4:
prompt = build_few_shot_prompt(q, num_shots=num_shots, cot=cot, cot_lang=cot_lang)
raw, ans_digit = query_ollama_chat(model_name, sys_prompt, prompt, think=think, cot=cot)
return raw, ans_digit
# Multi-pass cyclic permutation debiasing
shifts = [0, 2] if passes == 2 else [0, 1, 2, 3][:passes]
votes = []
raw_logs = []
for s in shifts:
rotated_choices = orig_choices[s:] + orig_choices[:s]
q_rot = dict(q)
q_rot["choices"] = rotated_choices
prompt = build_few_shot_prompt(q_rot, num_shots=num_shots, cot=cot, cot_lang=cot_lang)
raw, rot_digit = query_ollama_chat(model_name, sys_prompt, prompt, think=think, cot=cot)
rot_idx = int(rot_digit) - 1 if rot_digit in {"1", "2", "3", "4"} else 0
orig_idx = (rot_idx + s) % 4
orig_digit = str(orig_idx + 1)
votes.append(orig_digit)
raw_logs.append(f"s{s}:{rot_digit}->orig={orig_digit}")
counts = Counter(votes)
best_digit = counts.most_common(1)[0][0]
return ",".join(raw_logs), best_digit
def run_few_shot(
data_dir: str,
output_file: str,
model_name: str,
num_shots: int = 4,
val_only: bool = False,
gold_val_file: str = "data/gold_val_41.jsonl",
resume: bool = True,
think: bool = False,
cot: bool = False,
cot_lang: str = "en",
passes: int = 1
):
output_path = Path(output_file)
output_path.parent.mkdir(parents=True, exist_ok=True)
if val_only:
val_path = Path(gold_val_file)
if not val_path.exists():
print(f"Error: Gold validation file '{gold_val_file}' not found!")
sys.exit(1)
questions = load_val_questions(val_path)
print(f"Loaded {len(questions)} validation questions for rapid local benchmark.")
else:
questions = load_all_questions(Path(data_dir))
print(f"Loaded {len(questions)} full exam questions across easy, medium, and hard.")
existing_preds = {}
if resume and output_path.exists():
existing_preds = load_existing_checkpoint(output_path)
print(f"Resuming: Found {len(existing_preds)} existing predictions in '{output_file}'.")
predictions = []
print(f"Running {num_shots}-Shot inference with Ollama model: '{model_name}' (think={think}, cot={cot}, cot_lang={cot_lang}, passes={passes})...")
for i, q in enumerate(questions, 1):
q_no = q["q_no"]
subj = q.get("subject", "general")
cat = q.get("category", "general")
lvl = q.get("_level", "medium").lower()
q_key = f"{lvl}_{subj}_{q_no}"
if resume and q_key in existing_preds:
predictions.append(existing_preds[q_key])
continue
raw_info = ""
try:
raw_info, ans_digit = predict_single_or_debiased(
q, model_name=model_name, num_shots=num_shots, think=think, cot=cot, cot_lang=cot_lang, passes=passes
)
except Exception as e:
print(f"[{i}/{len(questions)}] Error on Q#{q_no} ({subj}): {e}. Using fallback 1.")
ans_digit = "1"
record = {
"q_no": q_no,
"subject": subj,
"category": cat,
"level": lvl,
"answer": str(ans_digit)
}
predictions.append(record)
if i % 10 == 0 or i == len(questions):
snippet = raw_info.strip().replace("\n", " ")[-60:]
print(f"Progress: {i}/{len(questions)} | Q#{q_no} ({subj}) [{snippet}] => {ans_digit}")
with open(output_path, "w", encoding="utf-8") as fp:
for p in predictions:
fp.write(json.dumps(p, ensure_ascii=False) + "\n")
with open(output_path, "w", encoding="utf-8") as fp:
for p in predictions:
fp.write(json.dumps(p, ensure_ascii=False) + "\n")
print(f"\nDone! {len(predictions)} predictions written to {output_file}")
def main():
parser = argparse.ArgumentParser(description="Sinhala MMLU Few-Shot Runner")
parser.add_argument("--data_dir", default="data", help="Path to data directory")
parser.add_argument("--output", default="data/predictions_few_shot.jsonl", help="Output path")
parser.add_argument("--model", default="qwen3.5:4b", help="Model name in Ollama")
parser.add_argument("--shots", type=int, default=4, help="Number of in-context exemplars (e.g. 4)")
parser.add_argument("--val_only", action="store_true", help="Run only on the 41-question local gold benchmark")
parser.add_argument("--gold_val_file", default="data/gold_val_41.jsonl", help="Gold validation benchmark file")
parser.add_argument("--no_resume", action="store_true", help="Overwrite existing output file")
parser.add_argument("--think", action="store_true", help="Enable <think> reasoning mode for Qwen 3 / 3.5 models")
parser.add_argument("--cot", action="store_true", help="Enable concise Cross-Lingual Translate-and-Reason CoT mode")
parser.add_argument("--cot_lang", default="en", choices=["en", "ta"], help="Target bridge language for CoT ('en' or 'ta')")
parser.add_argument("--passes", type=int, default=1, choices=[1, 2, 4], help="Cyclic option-permutation passes (1, 2, or 4)")
args = parser.parse_args()
run_few_shot(
args.data_dir,
args.output,
args.model,
num_shots=args.shots,
val_only=args.val_only,
gold_val_file=args.gold_val_file,
resume=not args.no_resume,
think=args.think,
cot=args.cot,
cot_lang=args.cot_lang,
passes=args.passes
)
if __name__ == "__main__":
main()
|