gaowanlong commited on
Commit
b0ee7af
·
verified ·
1 Parent(s): 687471a

Add hybrid RAG + QLoRA evaluation script

Browse files
Files changed (1) hide show
  1. scripts/rag_hybrid_evaluate.py +185 -0
scripts/rag_hybrid_evaluate.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hybrid RAG + QLoRA Evaluation:
4
+ Retrieve context via RAG, then generate answer using fine-tuned model (v1.0).
5
+
6
+ Usage: python scripts/rag_hybrid_evaluate.py
7
+ python scripts/rag_hybrid_evaluate.py --adapter lora_adapters/kernel-lora-v1.0
8
+ """
9
+
10
+ import json, re, pickle, sys, time
11
+ from pathlib import Path
12
+ from sklearn.metrics.pairwise import cosine_similarity
13
+ from mlx_lm import load, generate
14
+ from mlx_lm.sample_utils import make_sampler
15
+
16
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
17
+ RAG_INDEX_DIR = PROJECT_ROOT / "data" / "rag_index"
18
+
19
+ # Load test cases from evaluate.py
20
+ sys.path.insert(0, str(PROJECT_ROOT / "scripts"))
21
+ import importlib.util
22
+ spec = importlib.util.spec_from_file_location("evaluate_module", PROJECT_ROOT / "scripts" / "evaluate.py")
23
+ eval_mod = importlib.util.module_from_spec(spec)
24
+ spec.loader.exec_module(eval_mod)
25
+
26
+ TEST_CASES = eval_mod.TEST_CASES
27
+ CODE_COMPLETION_TESTS = eval_mod.CODE_COMPLETION_TESTS
28
+
29
+
30
+ def load_index():
31
+ with open(RAG_INDEX_DIR / "chunks.jsonl") as f:
32
+ chunks = [json.loads(line) for line in f]
33
+ with open(RAG_INDEX_DIR / "vectorizer.pkl", "rb") as f:
34
+ vectorizer = pickle.load(f)
35
+ with open(RAG_INDEX_DIR / "tfidf_matrix.pkl", "rb") as f:
36
+ tfidf_matrix = pickle.load(f)
37
+ return chunks, vectorizer, tfidf_matrix
38
+
39
+
40
+ def retrieve(query, chunks, vectorizer, tfidf_matrix, top_k=5):
41
+ query_vec = vectorizer.transform([query])
42
+ similarities = cosine_similarity(query_vec, tfidf_matrix).flatten()
43
+ top_indices = similarities.argsort()[-top_k:][::-1]
44
+ results = []
45
+ for idx in top_indices:
46
+ if similarities[idx] > 0.01:
47
+ results.append({"chunk": chunks[idx], "score": float(similarities[idx])})
48
+ return results
49
+
50
+
51
+ def build_rag_prompt(query, retrieved):
52
+ context_parts = []
53
+ for r in retrieved[:3]:
54
+ chunk = r["chunk"]
55
+ context_parts.append(f"From kernel documentation:\n{chunk['answer'][:500]}")
56
+ context = "\n\n".join(context_parts)
57
+ return f"""You are a Linux kernel expert. Use the following kernel documentation to answer the question.
58
+
59
+ Context:
60
+ {context}
61
+
62
+ Question: {query}
63
+
64
+ Answer the question thoroughly based on the context above. If the context doesn't contain enough information, use your own knowledge of the Linux kernel."""
65
+
66
+
67
+ def run_evaluation(adapter_path=None):
68
+ print("Loading RAG index...", flush=True)
69
+ chunks, vectorizer, tfidf_matrix = load_index()
70
+ print(f" Index: {len(chunks)} chunks", flush=True)
71
+
72
+ if adapter_path:
73
+ print(f"Loading fine-tuned model with adapter: {adapter_path}...", flush=True)
74
+ model, tokenizer = load(str(PROJECT_ROOT / "models" / "qwen2.5-7b"), adapter_path=str(adapter_path))
75
+ method_name = f"RAG + QLoRA ({adapter_path.name})"
76
+ else:
77
+ print("Loading base model...", flush=True)
78
+ model, tokenizer = load(str(PROJECT_ROOT / "models" / "qwen2.5-7b"))
79
+ method_name = "RAG + Base Model"
80
+
81
+ sampler = make_sampler(temp=0.7)
82
+ print(" Model loaded\n", flush=True)
83
+
84
+ all_tests = TEST_CASES + CODE_COMPLETION_TESTS
85
+ print(f"Running {len(all_tests)} tests with {method_name}...\n", flush=True)
86
+
87
+ results = []
88
+ for test in all_tests:
89
+ qid = test["id"]
90
+ question = test.get("question", test.get("prompt", ""))
91
+ kws = test.get("reference_keywords", [])
92
+
93
+ print(f" [{qid}] ", end="", flush=True)
94
+
95
+ retrieved = retrieve(question, chunks, vectorizer, tfidf_matrix)
96
+ rag_prompt = build_rag_prompt(question, retrieved)
97
+
98
+ start = time.time()
99
+ response = generate(model, tokenizer, prompt=rag_prompt[:3000], max_tokens=300, sampler=sampler)
100
+ elapsed = time.time() - start
101
+
102
+ # LLM-as-judge scoring
103
+ judge_prompt = (
104
+ f"You are an expert Linux kernel evaluator. "
105
+ f"Rate the following answer on a scale of 0-10 based on correctness, completeness, and precision.\n\n"
106
+ f"Question: {question}\n\n"
107
+ f"Answer: {response[:1000]}\n\n"
108
+ f"Output ONLY a number 0-10, nothing else."
109
+ )
110
+ try:
111
+ judge_resp = generate(model, tokenizer, prompt=judge_prompt, max_tokens=10, sampler=make_sampler(temp=0.1))
112
+ score_match = re.search(r'\b(\d+)(?:/10)?\b', judge_resp.strip())
113
+ judge_score = int(score_match.group(1)) if score_match else 5
114
+ judge_score = max(0, min(10, judge_score))
115
+ except:
116
+ judge_score = 5
117
+
118
+ normalized_score = judge_score / 10.0
119
+ found_keywords = [kw for kw in kws if kw.lower() in response.lower()]
120
+
121
+ results.append({
122
+ "id": qid,
123
+ "score": normalized_score,
124
+ "keywords_matched": len(found_keywords),
125
+ "keywords_total": len(kws),
126
+ "retrieved_chunks": len(retrieved),
127
+ "elapsed_sec": round(elapsed, 1),
128
+ })
129
+
130
+ print(f"Score: {normalized_score:.0%} | {elapsed:.1f}s | {len(retrieved)} chunks", flush=True)
131
+
132
+ # Stats by category
133
+ categories = {}
134
+ for r in results:
135
+ for test in all_tests:
136
+ if test["id"] == r["id"]:
137
+ cat = test.get("category", "unknown")
138
+ categories.setdefault(cat, []).append(r["score"])
139
+ break
140
+
141
+ print("\n" + "=" * 60)
142
+ print(f"Hybrid RAG Evaluation: {method_name}")
143
+ print("=" * 60)
144
+
145
+ all_scores = [r["score"] for r in results]
146
+ overall = sum(all_scores) / len(all_scores)
147
+ print(f"\nOverall: {overall:.1%}")
148
+
149
+ for cat, scores in sorted(categories.items()):
150
+ print(f" {cat}: {sum(scores)/len(scores):.1%}")
151
+
152
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
153
+ output = {
154
+ "timestamp": timestamp,
155
+ "method": method_name,
156
+ "adapter": str(adapter_path) if adapter_path else None,
157
+ "index_size": len(chunks),
158
+ "overall_score": overall,
159
+ "results": results,
160
+ "categories": {cat: sum(scores)/len(scores) for cat, scores in categories.items()},
161
+ }
162
+
163
+ output_path = PROJECT_ROOT / "results" / f"rag_hybrid_eval_{timestamp}.json"
164
+ with open(output_path, "w") as f:
165
+ json.dump(output, f, indent=2, ensure_ascii=False)
166
+
167
+ print(f"\nResults saved to {output_path}")
168
+ return output
169
+
170
+
171
+ if __name__ == "__main__":
172
+ import argparse
173
+ parser = argparse.ArgumentParser(description="Hybrid RAG + QLoRA Evaluation")
174
+ parser.add_argument("--adapter", type=str, default=None,
175
+ help="Path to LoRA adapter (e.g. lora_adapters/kernel-lora-v1.0)")
176
+ args = parser.parse_args()
177
+
178
+ adapter_path = None
179
+ if args.adapter:
180
+ adapter_path = PROJECT_ROOT / args.adapter
181
+ if not adapter_path.exists():
182
+ print(f"Adapter not found: {adapter_path}")
183
+ sys.exit(1)
184
+
185
+ run_evaluation(adapter_path)