tabulm / code /eval_llm_agg_fewshot.py
rakshi719's picture
Add TabuLM training and evaluation code
f32c034 verified
Raw History Blame Contribute Delete
5.41 kB
#!/usr/bin/env python3
"""3-shot GPT-4o experiment on aggregation items only.
Tests whether few-shot prompting closes the aggregation gap vs TabuLM.
Usage:
python eval_llm_agg_fewshot.py --api-key sk-...
"""
import argparse
import csv
import json
import os
import random
import sys
import time
from typing import List, Optional
import requests
DATA_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/data'
CODE_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/code'
TABQA_FILE = os.path.join(DATA_DIR, 'tabqa_kin.json')
CSV_DIR = os.path.join(DATA_DIR, 'tables')
OPENAI_URL = "https://api.openai.com/v1/chat/completions"
sys.path.insert(0, CODE_DIR)
from tabular_serializer import serialize_csv, TableCell
from eval_llm_baseline import csv_to_markdown, find_gold_cell, em_match
FEW_SHOT_EXAMPLES = [
{
"table": "| Inzego | Umubare_w_Abaturage | Umubare_w_Inzu |\n|---|---|---|\n| Gasabo | 542000 | 89000 |\n| Kicukiro | 312000 | 54000 |\n| Nyarugenge | 274000 | 48000 |",
"question": "Ni iki gifite Umubare_w_Abaturage nyinshi?",
"answer": "Gasabo",
"explanation": "Gasabo has the highest Umubare_w_Abaturage (542000).",
},
{
"table": "| Igihingwa | Umusaruro_Toni | Akarere |\n|---|---|---|\n| Ibigori | 45200 | Musanze |\n| Ibishyimbo | 12300 | Musanze |\n| Ibirayi | 89000 | Musanze |",
"question": "Ni iki gifite Umusaruro_Toni nke cyane?",
"answer": "Ibishyimbo",
"explanation": "Ibishyimbo has the lowest Umusaruro_Toni (12300).",
},
{
"table": "| Umwaka | Umusaruro_Miliyari_Frw | Impinduka_% |\n|---|---|---|\n| 2020 | 1100.2 | +3.1 |\n| 2021 | 1189.5 | +8.1 |\n| 2022 | 1209.3 | +1.7 |",
"question": "Ni iki gifite Impinduka_% ntoya muri bose?",
"answer": "2022",
"explanation": "2022 has the lowest Impinduka_% (+1.7).",
},
]
def make_fewshot_prompt(table_md: str, question: str) -> str:
examples = ""
for ex in FEW_SHOT_EXAMPLES:
examples += (
f"Table:\n{ex['table']}\n\n"
f"Question: {ex['question']}\n"
f"Answer: {ex['answer']}\n\n"
)
return (
f"You are answering questions about data tables written in Kinyarwanda.\n"
f"The answer is always the NAME of an entity from the first column — NOT a number.\n"
f"Identify which entity has the highest or lowest value in the relevant column.\n\n"
f"Examples:\n\n{examples}"
f"Now answer the following:\n\n"
f"Table:\n{table_md}\n\n"
f"Question: {question}\n"
f"Answer:"
)
def call_openai(prompt: str, api_key: str, retries: int = 3) -> Optional[str]:
for attempt in range(retries):
try:
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {"model": "gpt-4o", "max_tokens": 64, "temperature": 0,
"messages": [{"role": "user", "content": prompt}]}
r = requests.post(OPENAI_URL, headers=headers, json=payload, timeout=30)
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"].strip()
except Exception as e:
if attempt < retries - 1:
time.sleep(2 ** attempt)
else:
print(f" [API error] {e}")
return None
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--api-key', required=True)
parser.add_argument('--out', default=os.path.join(DATA_DIR, 'llm_agg_fewshot_results.json'))
args = parser.parse_args()
random.seed(42)
with open(TABQA_FILE) as f:
all_items = json.load(f)
random.shuffle(all_items)
dev_items = [x for x in all_items[int(0.8 * len(all_items)):]
if x.get('answer_type') == 'aggregation']
print(f'[FewShot-Agg] {len(dev_items)} aggregation dev items')
correct, total, skipped = 0, 0, 0
records = []
for i, item in enumerate(dev_items):
csv_path = os.path.join(CSV_DIR, item['table_file'])
if not os.path.exists(csv_path):
skipped += 1; continue
cells = serialize_csv(csv_path)
if not cells:
skipped += 1; continue
gold_rc = find_gold_cell(cells, item['answer'], item['question'])
if gold_rc is None:
skipped += 1; continue
table_md = csv_to_markdown(csv_path)
if not table_md:
skipped += 1; continue
prompt = make_fewshot_prompt(table_md, item['question'])
pred = call_openai(prompt, args.api_key)
if pred is None:
skipped += 1; continue
hit = int(em_match(pred, item['answer']))
correct += hit; total += 1
records.append({'question': item['question'], 'gold': item['answer'],
'pred': pred, 'hit': hit})
print(f' [{i+1}/{len(dev_items)}] gold={item["answer"]!r:20s} pred={pred!r:20s} hit={hit}')
em = correct / total if total else 0.0
print(f'\n[FewShot-Agg] EM = {em:.4f} ({correct}/{total}, {skipped} skipped)')
with open(args.out, 'w') as f:
json.dump({'em': round(em, 4), 'correct': correct, 'total': total,
'skipped': skipped, 'records': records}, f, indent=2, ensure_ascii=False)
print(f'[FewShot-Agg] Saved to {args.out}')
if __name__ == '__main__':
main()