-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathclassifier.py
More file actions
200 lines (171 loc) · 9.26 KB
/
Copy pathclassifier.py
File metadata and controls
200 lines (171 loc) · 9.26 KB
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
import os
import re
import argparse
import ollama
from tabulate import tabulate
FEW_SHOT_EXAMPLES = [
{"text": "Senate Approves New Infrastructure Spending Bill.", "label": "Politics"},
{"text": "Prime Minister Announces Snap Election Amid Coalition Collapse.", "label": "Politics"},
{"text": "Governor Signs Executive Order Restricting Industrial Emissions.", "label": "Politics"},
{"text": "New Quantum Computing Chip Breakthrough Multiplies Processing Speeds.", "label": "Technology"},
{"text": "Cybersecurity Breach Exposes Millions of User Accounts Worldwide.", "label": "Technology"},
{"text": "Startup Launches Revolutionary Virtual Reality Headset for Remote Workers.", "label": "Technology"},
{"text": "Underdog Team Secures Dramatic Victory in Championship Final.", "label": "Sports"},
{"text": "Olympic Committee Announces New Host City for Upcoming Summer Games.", "label": "Sports"},
{"text": "World Number One Tennis Star Withdraws From Tournament Due to Injury.", "label": "Sports"},
{"text": "Stock Market Plummets as Tech Sector Experiences Massive Sell-Off.", "label": "Finance"},
{"text": "Global Conglomerate Reports Record Profits in Q3 Financial Release.", "label": "Finance"},
{"text": "Cryptocurrency Regulations Tighten Across European Markets.", "label": "Finance"},
{"text": "Study Finds Regular Exercise Significantly Reduces Risk of Heart Disease.", "label": "Health"},
{"text": "Hospitals Face Severe Nurse Shortages Amid Seasonal Flu Surge.", "label": "Health"},
{"text": "Researchers Map Human Genome Sequence to Uncover Rare Genetic Mutations.", "label": "Health"}
]
VALID_CATEGORIES = ["Politics", "Technology", "Sports", "Finance", "Health"]
SYNTHETIC_DATASET = [
{"headline": "Tech Giants Agree on New Open-Source AI Safety Standards", "label": "Technology"},
{"headline": "Central Bank Raises Interest Rates by 25 Basis Points to Combat Inflation", "label": "Finance"},
{"headline": "Star Striker Signs Record-Breaking Five-Year Contract Extension", "label": "Sports"},
{"headline": "Parliament Votes to Pass Historic Climate Action Bill After Fierce Debate", "label": "Politics"},
{"headline": "New FDA-Approved Breakthrough Drug Shows Promise in Halting Alzheimer's", "label": "Health"},
]
def clean_label(raw_output: str) -> str:
"""Helper utility to extract and validate categories from text output."""
for category in VALID_CATEGORIES:
if re.search(rf"\b{category}\b", raw_output, re.IGNORECASE):
return category
return "Unknown"
def classify_zero_shot(headline: str, model_name: str) -> tuple[str, int, int]:
system_prompt = (
"You are an expert news editor. Your task is to classify the provided headline "
f"into exactly one of these categories: {VALID_CATEGORIES}. "
"Respond with ONLY the category name. Do not write markdown, intros, or punctuation."
)
response = ollama.chat(
model=model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Headline: {headline}"}
],
options={"temperature": 0.0}
)
raw_content = response['message']['content'].strip()
return clean_label(raw_content), response.get('prompt_eval_count', 0), response.get('eval_count', 0)
def classify_few_shot(headline: str, examples: list[dict], model_name: str) -> tuple[str, int, int]:
system_prompt = (
f"Classify the input headline into one of these categories: {VALID_CATEGORIES}. "
"Follow the exact format shown in the examples. Provide ONLY the category name."
)
example_blocks = [f"Headline: {ex['text']}\nCategory: {ex['label']}" for ex in examples]
user_content = "\n\n".join(example_blocks) + f"\n\nHeadline: {headline}\nCategory:"
response = ollama.chat(
model=model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content}
],
options={"temperature": 0.0}
)
raw_content = response['message']['content'].strip()
return clean_label(raw_content), response.get('prompt_eval_count', 0), response.get('eval_count', 0)
def classify_cot(headline: str, model_name: str) -> tuple[str, int, int]:
system_prompt = (
f"Classify the news headline into one of: {VALID_CATEGORIES}. "
"First, reason step-by-step about what fields or industries the words in the headline relate to. "
"Finally, end your response with the phrase 'Final Category: <label>'."
)
response = ollama.chat(
model=model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Headline: {headline}"}
],
options={"temperature": 0.2}
)
raw_content = response['message']['content'].strip()
final_line_match = re.search(r"Final Category:\s*(\w+)", raw_content, re.IGNORECASE)
label = clean_label(final_line_match.group(1)) if final_line_match else clean_label(raw_content)
return label, response.get('prompt_eval_count', 0), response.get('eval_count', 0)
def load_data(input_file: str, labels_file: str) -> list[dict]:
"""Loads headlines and ground truth from files, or returns synthetic fallback."""
if input_file and labels_file:
try:
with open(input_file, 'r', encoding='utf-8') as f_in, open(labels_file, 'r', encoding='utf-8') as f_lbl:
headlines = [line.strip() for line in f_in if line.strip()]
labels = [line.strip() for line in f_lbl if line.strip()]
if len(headlines) != len(labels):
print(f"Warning: Mismatch in lines! {len(headlines)} headlines vs {len(labels)} labels.")
return [{"headline": h, "label": l} for h, l in zip(headlines, labels)]
except FileNotFoundError as e:
print(f"Error loading files: {e}")
exit(1)
else:
print("No input files provided via arguments. Using built-in synthetic dataset.")
return SYNTHETIC_DATASET
def run_evaluation(dataset: list[dict], model_name: str):
print(f"\nInitializing Prompt Engineering Playground...")
print(f"Model: '{model_name}' | Total Headlines: {len(dataset)}\n")
metrics = {
"Zero-Shot": {"correct": 0, "prompt_tok": 0, "comp_tok": 0},
"Few-Shot": {"correct": 0, "prompt_tok": 0, "comp_tok": 0},
"Chain-of-Thought": {"correct": 0, "prompt_tok": 0, "comp_tok": 0}
}
total_items = len(dataset)
for idx, item in enumerate(dataset, 1):
headline = item["headline"]
ground_truth = item["label"]
print(f"Processing [{idx}/{total_items}]: \"{headline[:40]}...\"")
zs_pred, zs_p_tok, zs_c_tok = classify_zero_shot(headline, model_name)
metrics["Zero-Shot"]["prompt_tok"] += zs_p_tok
metrics["Zero-Shot"]["comp_tok"] += zs_c_tok
if zs_pred == ground_truth: metrics["Zero-Shot"]["correct"] += 1
fs_pred, fs_p_tok, fs_c_tok = classify_few_shot(headline, FEW_SHOT_EXAMPLES, model_name)
metrics["Few-Shot"]["prompt_tok"] += fs_p_tok
metrics["Few-Shot"]["comp_tok"] += fs_c_tok
if fs_pred == ground_truth: metrics["Few-Shot"]["correct"] += 1
cot_pred, cot_p_tok, cot_c_tok = classify_cot(headline, model_name)
metrics["Chain-of-Thought"]["prompt_tok"] += cot_p_tok
metrics["Chain-of-Thought"]["comp_tok"] += cot_c_tok
if cot_pred == ground_truth: metrics["Chain-of-Thought"]["correct"] += 1
print("\n" + "="*70)
print(" PROMPT DESIGN EVALUATION RESULTS ")
print("="*70 + "\n")
table_data = []
for approach, data in metrics.items():
accuracy = (data["correct"] / total_items) * 100
avg_prompt = data["prompt_tok"] / total_items
avg_comp = data["comp_tok"] / total_items
avg_total = avg_prompt + avg_comp
table_data.append([
approach, f"{data['correct']}/{total_items}", f"{accuracy:.1f}%",
f"{avg_prompt:.1f}", f"{avg_comp:.1f}", f"{avg_total:.1f}"
])
headers = ["Approach", "Correct", "Accuracy", "Avg Prompt Tok", "Avg Gen Tok", "Avg Total Tok"]
print(tabulate(table_data, headers=headers, tablefmt="grid"))
def main():
parser = argparse.ArgumentParser(
description="News Classifier CLI: Compare Zero-Shot, Few-Shot, and CoT prompting.",
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument(
"-m", "--model",
type=str,
default="llama3",
help="Ollama model to use for classification (default: llama3)"
)
parser.add_argument(
"-i", "--input",
type=str,
help="Path to the plain text file containing news headlines (one per line)"
)
parser.add_argument(
"-l", "--labels",
type=str,
help="Path to the plain text file containing ground truth labels (one per line)"
)
args = parser.parse_args()
if bool(args.input) != bool(args.labels):
parser.error("You must provide both --input and --labels, or neither (to use fallback data).")
dataset = load_data(args.input, args.labels)
run_evaluation(dataset, args.model)
if __name__ == "__main__":
main()