addyo07 commited on
Commit
fe775e3
·
verified ·
1 Parent(s): ca24a5a

Restructure: model/pytorch/, model/onnx/, scripts/

Browse files
README.md CHANGED
@@ -57,42 +57,46 @@ widget:
57
 
58
  - **Architecture**: `distilbert-base-multilingual-cased` (134M params)
59
  - **Quantization**: INT8 dynamic (ONNX Runtime)
60
- - **Input**: Short text queries in English or Hindi (≤10 words recommended for model path; longer sentences bypass to SEMANTIC)
61
  - **Output**: Binary — GENERIC (0) or SEMANTIC (1)
62
- - **Inference**: ONNX Runtime CPU (Intel/AMD), single-thread P99 = **16.87ms**
63
 
64
- ## Intended Use
65
 
66
- This model is designed as a **memory relevance gate** in voice AI pipelines. Before storing a user's utterance in long-term memory (episodic + semantic), run it through this classifier:
67
-
68
- - **SEMANTIC** contains facts, preferences, name, location, relationships → store in memory
69
- - **GENERIC** → greeting, command, chit-chat, filler → skip memory, pass directly to LLM
70
-
71
- Sentences longer than 10 words bypass the model entirely and are treated as SEMANTIC, since they almost always contain durable information.
72
-
73
- ## Performance
74
-
75
- | Split | Accuracy |
76
- |-------|----------|
77
- | Test (15% holdout) | **98.39%** |
78
-
79
- ### Latency
80
-
81
- | Mode | P50 | P99 |
82
- |------|-----|-----|
83
- | Multi-thread CPU | 8.39 ms | 11.81 ms |
84
- | Single-thread CPU (intra_op_threads=1) | 14.81 ms | 16.87 ms |
85
 
86
  ## Usage
87
 
88
- ### Python
89
 
90
  ```python
91
  from transformers import AutoTokenizer
92
  import onnxruntime as ort
93
 
94
- tokenizer = AutoTokenizer.from_pretrained("addyo07/distilbert-query-classifier")
95
- session = ort.InferenceSession("model_quantized.onnx")
 
 
 
 
 
 
96
 
97
  def classify(text: str) -> str:
98
  inputs = tokenizer(text, return_tensors="np", max_length=64, truncation=True, padding="max_length")
@@ -103,41 +107,77 @@ def classify(text: str) -> str:
103
  return "SEMANTIC" if logits[0][1] > logits[0][0] else "GENERIC"
104
  ```
105
 
106
- ### Rust
107
 
108
  ```toml
109
  [dependencies]
110
- query-sieve = { git = "https://github.com/your-org/query-sieve" }
111
  ```
112
 
113
  ```rust
114
  use query_sieve::GenericSemanticClassifier;
115
 
116
  let classifier = GenericSemanticClassifier::load(
117
- "models/model_quantized.onnx",
118
- "models/tokenizer.json",
119
  )?;
120
  let result = classifier.classify("my name is John")?;
121
  ```
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  ## Training Data
124
 
125
- The dataset contains **12,044 synthetic examples** generated by `llama3.1:8b`:
 
 
126
 
127
  | Category | English | Hindi |
128
  |----------|---------|-------|
129
  | GENERIC | 3,003 | 3,019 |
130
  | SEMANTIC | 3,017 | 3,005 |
131
 
132
- The SEMANTIC category is balanced to contain ~40% short standalone statements (3-7 words) to prevent the model from learning "semantic = long sentence."
 
 
 
 
 
 
 
 
 
133
 
134
- ## Files
135
 
136
- | File | Size | Description |
137
- |------|------|-------------|
138
- | `model_quantized.onnx` | 130 MB | INT8 quantized ONNX model |
139
- | `tokenizer.json` | 2.8 MB | HuggingFace tokenizer |
140
- | `config.json` | 0.7 KB | Model configuration |
141
 
142
  ## License
143
 
 
57
 
58
  - **Architecture**: `distilbert-base-multilingual-cased` (134M params)
59
  - **Quantization**: INT8 dynamic (ONNX Runtime)
60
+ - **Input**: Short text queries in English or Hindi (≤10 words; longer sentences bypass to SEMANTIC)
61
  - **Output**: Binary — GENERIC (0) or SEMANTIC (1)
62
+ - **Inference**: ONNX Runtime CPU, single-thread P99 = **16.87ms**
63
 
64
+ ## Repository Structure
65
 
66
+ ```
67
+ distilbert-query-classifier/
68
+ ├── README.md # Model card (this file)
69
+ ├── model/
70
+ │ ├── pytorch/ # PyTorch safetensors (for transformers library)
71
+ │ │ ├── model.safetensors # Full precision PyTorch weights
72
+ │ │ ├── config.json # Model configuration
73
+ │ │ ├── tokenizer.json # HuggingFace tokenizer
74
+ │ │ └── tokenizer_config.json
75
+ │ └── onnx/ # ONNX INT8 quantized (for CPU inference)
76
+ │ └── model_quantized.onnx # INT8 quantized model (~130 MB)
77
+ ├── scripts/ # Python training pipeline
78
+ │ ├── config.py # Constants and paths
79
+ │ ├── generate_dataset.py # Synthetic data generation via Ollama
80
+ │ ├── train.py # Fine-tuning + ONNX export + latency benchmark
81
+ │ └── ...
82
+ ```
 
 
83
 
84
  ## Usage
85
 
86
+ ### Python (with transformers + ONNX Runtime)
87
 
88
  ```python
89
  from transformers import AutoTokenizer
90
  import onnxruntime as ort
91
 
92
+ # Load tokenizer from the pytorch folder
93
+ tokenizer = AutoTokenizer.from_pretrained(
94
+ "addyo07/distilbert-query-classifier",
95
+ subfolder="model/pytorch",
96
+ )
97
+
98
+ # Load ONNX model
99
+ session = ort.InferenceSession("model/onnx/model_quantized.onnx")
100
 
101
  def classify(text: str) -> str:
102
  inputs = tokenizer(text, return_tensors="np", max_length=64, truncation=True, padding="max_length")
 
107
  return "SEMANTIC" if logits[0][1] > logits[0][0] else "GENERIC"
108
  ```
109
 
110
+ ### Rust (with query-sieve crate)
111
 
112
  ```toml
113
  [dependencies]
114
+ query-sieve = { git = "https://github.com/addy-47/query-sieve-rs" }
115
  ```
116
 
117
  ```rust
118
  use query_sieve::GenericSemanticClassifier;
119
 
120
  let classifier = GenericSemanticClassifier::load(
121
+ "model/onnx/model_quantized.onnx",
122
+ "model/pytorch/tokenizer.json",
123
  )?;
124
  let result = classifier.classify("my name is John")?;
125
  ```
126
 
127
+ ### Download individual files
128
+
129
+ ```bash
130
+ # ONNX model (for CPU inference)
131
+ wget https://huggingface.co/addyo07/distilbert-query-classifier/resolve/main/model/onnx/model_quantized.onnx
132
+
133
+ # PyTorch weights (for fine-tuning)
134
+ wget https://huggingface.co/addyo07/distilbert-query-classifier/resolve/main/model/pytorch/model.safetensors
135
+
136
+ # Tokenizer
137
+ wget https://huggingface.co/addyo07/distilbert-query-classifier/resolve/main/model/pytorch/tokenizer.json
138
+ ```
139
+
140
+ ## Performance
141
+
142
+ | Split | Accuracy |
143
+ |-------|----------|
144
+ | Test (15% holdout) | **98.39%** |
145
+
146
+ ### Latency
147
+
148
+ | Mode | P50 | P99 |
149
+ |------|-----|-----|
150
+ | Multi-thread CPU | 8.39 ms | 11.81 ms |
151
+ | Single-thread CPU (intra_op_threads=1) | 14.81 ms | 16.87 ms |
152
+
153
  ## Training Data
154
 
155
+ Dataset: [addyo07/query-classification-dataset](https://huggingface.co/datasets/addyo07/query-classification-dataset)
156
+
157
+ 12,044 synthetic examples generated by `llama3.1:8b`:
158
 
159
  | Category | English | Hindi |
160
  |----------|---------|-------|
161
  | GENERIC | 3,003 | 3,019 |
162
  | SEMANTIC | 3,017 | 3,005 |
163
 
164
+ The SEMANTIC category contains ~40% short standalone statements (3-7 words) to prevent the model from learning "semantic = long sentence."
165
+
166
+ ## Training Scripts
167
+
168
+ The `scripts/` directory contains the full training pipeline:
169
+
170
+ 1. `python scripts/generate_dataset.py --category en_semantic` — generate synthetic data via Ollama
171
+ 2. `python scripts/train.py` — fine-tune DistilBERT + export ONNX INT8 + benchmark
172
+
173
+ ## Rust Crate
174
 
175
+ The `query-sieve` Rust crate provides the inference runtime:
176
 
177
+ - **GitHub**: [addy-47/query-sieve-rs](https://github.com/addy-47/query-sieve-rs)
178
+ - ONNX Runtime (ort) with HuggingFace tokenizer
179
+ - Configurable single/multi-thread CPU
180
+ - >10 word bypass (long sentences auto-classify as SEMANTIC)
 
181
 
182
  ## License
183
 
config.json → model/onnx/config.json RENAMED
File without changes
model_quantized.onnx → model/onnx/model_quantized.onnx RENAMED
File without changes
model/pytorch/config.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation": "gelu",
3
+ "architectures": [
4
+ "DistilBertForSequenceClassification"
5
+ ],
6
+ "attention_dropout": 0.1,
7
+ "dim": 768,
8
+ "dropout": 0.1,
9
+ "dtype": "float32",
10
+ "hidden_dim": 3072,
11
+ "id2label": {
12
+ "0": "GENERIC",
13
+ "1": "SEMANTIC"
14
+ },
15
+ "initializer_range": 0.02,
16
+ "label2id": {
17
+ "GENERIC": 0,
18
+ "SEMANTIC": 1
19
+ },
20
+ "max_position_embeddings": 512,
21
+ "model_type": "distilbert",
22
+ "n_heads": 12,
23
+ "n_layers": 6,
24
+ "output_past": true,
25
+ "pad_token_id": 0,
26
+ "problem_type": "single_label_classification",
27
+ "qa_dropout": 0.1,
28
+ "seq_classif_dropout": 0.2,
29
+ "sinusoidal_pos_embds": false,
30
+ "tie_weights_": true,
31
+ "transformers_version": "4.57.6",
32
+ "vocab_size": 119547
33
+ }
model.safetensors → model/pytorch/model.safetensors RENAMED
File without changes
special_tokens_map.json → model/pytorch/special_tokens_map.json RENAMED
File without changes
tokenizer.json → model/pytorch/tokenizer.json RENAMED
File without changes
tokenizer_config.json → model/pytorch/tokenizer_config.json RENAMED
File without changes
vocab.txt → model/pytorch/vocab.txt RENAMED
File without changes
scripts/generate_dataset.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate synthetic dataset for Generic vs Semantic classifier using Ollama (llama3.1:8b).
4
+
5
+ Generates 4 categories:
6
+ - en_generic: English generic queries
7
+ - en_semantic: English semantic queries
8
+ - hi_generic: Hindi generic queries (Devanagari)
9
+ - hi_semantic: Hindi semantic queries (Devanagari)
10
+
11
+ Each category targets TOTAL_PER_CATEGORY examples (default 3000).
12
+ Generation is resumable — it appends to existing JSONL files.
13
+
14
+ Usage:
15
+ python3 scripts/generate_dataset.py [--category en_generic]
16
+ python3 scripts/generate_dataset.py # all categories
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import re
22
+ import sys
23
+ import time
24
+ import argparse
25
+
26
+ import requests
27
+ from concurrent.futures import ThreadPoolExecutor, as_completed
28
+ from tqdm import tqdm
29
+
30
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
31
+ from config import (
32
+ CATEGORIES, TOTAL_PER_CATEGORY, BATCH_SIZE_GEN, MAX_CONCURRENT,
33
+ OLLAMA_URL, OLLAMA_MODEL, RAW_DIR
34
+ )
35
+
36
+ os.makedirs(RAW_DIR, exist_ok=True)
37
+
38
+
39
+ def count_existing(filepath: str) -> int:
40
+ """Count lines (examples) in an existing JSONL file."""
41
+ if not os.path.exists(filepath):
42
+ return 0
43
+ with open(filepath) as f:
44
+ return sum(1 for _ in f)
45
+
46
+
47
+ def build_prompt(category_key: str) -> str:
48
+ """Build a prompt for the given category that asks for exactly BATCH_SIZE_GEN examples."""
49
+ info = CATEGORIES[category_key]
50
+ lang = info["lang"]
51
+ label = info["label"]
52
+
53
+ # Language-specific instructions
54
+ if lang == "Hindi":
55
+ lang_instructions = """- Write ALL queries in Devanagari script (Hindi), NOT transliterated Hindi.
56
+ - Use conversational Hindi, not formal/literary Hindi.
57
+ - Include natural particles like "ही", "भी", "तो", "ना", "जी".
58
+ - Use common Hindi interjections: "अच्छा", "हाँ", "नहीं", "है ना", "अरे". """
59
+ else:
60
+ lang_instructions = """- Write in natural, conversational English.
61
+ - Cover different registers: casual, polite, formal, technical."""
62
+
63
+ # Category-specific definitions and examples
64
+ if label == "GENERIC":
65
+ gen_examples = (
66
+ f"Example GENERIC {lang} queries:\n"
67
+ f' - hello / namaste\n'
68
+ f' - stop talking / chup raho\n'
69
+ f' - what time is it / kya samay hua hai\n'
70
+ f' - thanks / shukriya\n'
71
+ f' - okay got it / theek hai samajh gaya\n'
72
+ f' - tell me a joke / koi chutkula sunao\n'
73
+ f' - i see / achha\n'
74
+ f' - yes please continue / haan ji kripya jari rakhein\n'
75
+ f' - how are you / aap kaise hain\n'
76
+ f' - never mind / koi baat nahi\n'
77
+ )
78
+ definition = (
79
+ "GENERIC queries have NO durable knowledge value. They are:\n"
80
+ "- Social rituals: greetings, thanks, apologies, pleasantries\n"
81
+ "- Commands/Controls: start, stop, pause, go back, repeat\n"
82
+ "- Simple affirmations/negations: yes, no, okay, hmm, got it\n"
83
+ '- Simple time/date/weather queries ("what time is it")\n'
84
+ "- Fillers and backchanneling: well, so, anyway, i see, right\n"
85
+ '- Transactional: "please repeat", "speak slower", "tell me a joke"\n'
86
+ '- Interaction management: "im done", "thats all", "go ahead"\n'
87
+ '- Unanswerable/meta: "i dont know", "what do you mean", "can you hear me"\n'
88
+ )
89
+ else: # SEMANTIC
90
+ gen_examples = (
91
+ f"Example SEMANTIC {lang} queries:\n"
92
+ f" SHORT (3-7 words) standalone semantic statements:\n"
93
+ f' - my name is John / mera naam Ravi hai\n'
94
+ f' - I am a doctor / main doctor hoon\n'
95
+ f' - I love spicy food / mujhe masaledar khana pasand hai\n'
96
+ f' - my sister is a teacher / meri behen teacher hai\n'
97
+ f' - I live in Delhi / main Dilli mein rehta hoon\n'
98
+ f' - I work at Google / main Google mein kaam karta hoon\n'
99
+ f' - my favorite color is blue / mera pasandida rang nila hai\n'
100
+ f' - I have two cats / mere paas do billiyan hain\n'
101
+ f' - I am learning guitar / main guitar seekh raha hoon\n'
102
+ f' LONGER (8-20 words) compound semantic statements:\n'
103
+ f' - my name is John and I live in Mumbai / mera naam Ravi hai aur main Mumbai mein rehta hoon\n'
104
+ f' - I love spicy food but I am allergic to peanuts / mujhe masaledar khana pasand hai lekin mujhe moongphali se allergy hai\n'
105
+ f' - my sister is a doctor in Delhi / meri behen Dilli mein doctor hai\n'
106
+ f' - I am planning to start learning guitar next month / main agle mahine guitar seekhna shuru karne wala hoon\n'
107
+ f' - remember I said I am allergic to peanuts / yaad hai maine kaha tha mujhe moongphali se allergy hai\n'
108
+ f' - my favorite restaurant is the Italian place on Church Street / mera pasandida restaurant Church Street par Italian jagah hai\n'
109
+ )
110
+ definition = (
111
+ "SEMANTIC queries contain durable, storable information. They are:\n"
112
+ "- Personal facts: name, age, location, profession, education, background\n"
113
+ "- Preferences and tastes: likes, dislikes, favorites, habits\n"
114
+ "- Relationships: family, friends, colleagues, their attributes\n"
115
+ "- Detailed descriptions of events, people, places, objects\n"
116
+ "- Complex questions that require retrieval of past context\n"
117
+ '- Explicit memory references: "remember I told you about...", "as I said before..."\n'
118
+ '- Plans, intentions, goals: "Im planning to visit Japan next spring"\n'
119
+ '- OPINIONS WITH REASONING: "I think dark chocolate is better because..."\n'
120
+ '- Knowledge queries that reveal user context: "How long does it take to get to Bangalore?"\n'
121
+ " (These reveal the user's location/context even though they are phrased as questions)\n"
122
+ )
123
+
124
+ lang_code = "hi" if lang == "Hindi" else "en"
125
+ prompt = (
126
+ f"You are generating a synthetic training dataset for a binary classifier. "
127
+ f"The classifier categorizes user queries as GENERIC (no durable knowledge) "
128
+ f"or SEMANTIC (contains storable facts, preferences, relationships, context).\n\n"
129
+ f"TASK: Generate {BATCH_SIZE_GEN} realistic {lang} user queries. "
130
+ f"EVERY query must be labeled \"{label}\".\n\n"
131
+ f"{lang_instructions}\n\n"
132
+ f"{definition}\n\n"
133
+ f"{gen_examples}\n\n"
134
+ f"CRITICAL RULES:\n"
135
+ f'1. Every query MUST have label = "{label}" - no mix of labels.\n'
136
+ f"2. Output ONLY valid JSONL - one JSON object per line, nothing else.\n"
137
+ f'3. Each line format: {{\"text\": \"<the query>\", "language\": \"{lang_code}\", "label\": "{label}"}}\n'
138
+ f"4. Queries must be diverse: vary the patterns, structures, and lengths (2 to 20 words).\n"
139
+ )
140
+
141
+ if label == "SEMANTIC":
142
+ prompt += (
143
+ f"5. IMPORTANT - 40% of your examples MUST be SHORT (3-7 words) standalone statements "
144
+ f"containing exactly one fact/preference. The remaining 60% can be longer compound sentences.\n"
145
+ )
146
+ else:
147
+ prompt += (
148
+ f"5. Make them sound like real voice assistant queries, not textbook sentences.\n"
149
+ )
150
+
151
+ prompt += (
152
+ f"6. NO markdown, NO code fences, NO explanation, NO numbering.\n\n"
153
+ f"Now generate {BATCH_SIZE_GEN} examples, one per line:"
154
+ )
155
+
156
+ return prompt
157
+
158
+
159
+ def parse_jsonl_from_response(content: str) -> list[dict]:
160
+ """Parse JSONL from the model response, handling common formatting issues."""
161
+ examples = []
162
+ for line in content.strip().split("\n"):
163
+ line = line.strip()
164
+ if not line:
165
+ continue
166
+ # Remove markdown code fences
167
+ if line.startswith("```"):
168
+ continue
169
+ if line == '```':
170
+ continue
171
+
172
+ # Try direct JSON parse
173
+ try:
174
+ obj = json.loads(line)
175
+ if "text" in obj and "label" in obj:
176
+ obj["label"] = obj["label"].strip().upper()
177
+ examples.append(obj)
178
+ continue
179
+ except json.JSONDecodeError:
180
+ pass
181
+
182
+ # Try to find JSON within the line
183
+ match = re.search(r'\{[^}]*"text"[^}]*"label"[^}]*\}', line)
184
+ if match:
185
+ try:
186
+ obj = json.loads(match.group())
187
+ if "text" in obj and "label" in obj:
188
+ obj["label"] = obj["label"].strip().upper()
189
+ examples.append(obj)
190
+ except json.JSONDecodeError:
191
+ pass
192
+
193
+ return examples
194
+
195
+
196
+ def generate_batch(category: str) -> list[dict]:
197
+ """Generate one batch of examples from Ollama."""
198
+ prompt = build_prompt(category)
199
+
200
+ payload = {
201
+ "model": OLLAMA_MODEL,
202
+ "messages": [{"role": "user", "content": prompt}],
203
+ "stream": False,
204
+ "options": {
205
+ "temperature": 0.85,
206
+ "top_p": 0.95,
207
+ "num_predict": 4096,
208
+ }
209
+ }
210
+
211
+ try:
212
+ resp = requests.post(OLLAMA_URL, json=payload, timeout=300)
213
+ resp.raise_for_status()
214
+ content = resp.json()["message"]["content"]
215
+ examples = parse_jsonl_from_response(content)
216
+ return examples
217
+ except requests.exceptions.Timeout:
218
+ print(f" [TIMEOUT] Batch generation timed out")
219
+ return []
220
+ except Exception as e:
221
+ print(f" [ERROR] {e}")
222
+ return []
223
+
224
+
225
+ def generate_category(category: str):
226
+ """Generate TOTAL_PER_CATEGORY examples for one category using concurrent batches."""
227
+ filepath = os.path.join(RAW_DIR, f"{category}.jsonl")
228
+ existing = count_existing(filepath)
229
+ needed = TOTAL_PER_CATEGORY - existing
230
+
231
+ if needed <= 0:
232
+ print(f" [SKIP] {category}: already has {existing} examples (target {TOTAL_PER_CATEGORY})")
233
+ return
234
+
235
+ print(f" [GEN] {category}: {existing} existing, {needed} more needed")
236
+
237
+ generated_count = existing
238
+ pbar = tqdm(total=TOTAL_PER_CATEGORY, initial=existing, desc=f"{category:15s}", unit="ex", smoothing=0.1)
239
+
240
+ # Calculate how many batches we need (with a safety margin)
241
+ batches_to_submit = needed // BATCH_SIZE_GEN + 3 # overshoot slightly
242
+ submitted = 0
243
+
244
+ with ThreadPoolExecutor(max_workers=MAX_CONCURRENT) as executor:
245
+ # Submit initial batches
246
+ futures = {}
247
+ initial_count = min(MAX_CONCURRENT, batches_to_submit)
248
+ for _ in range(initial_count):
249
+ future = executor.submit(generate_batch, category)
250
+ futures[future] = True
251
+ submitted += 1
252
+
253
+ # Process as they complete, submitting more to maintain throughput
254
+ while futures and generated_count < TOTAL_PER_CATEGORY:
255
+ for future in as_completed(futures, timeout=120):
256
+ break # just get one
257
+
258
+ try:
259
+ examples = future.result()
260
+ if examples:
261
+ with open(filepath, "a") as fh:
262
+ for ex in examples:
263
+ fh.write(json.dumps(ex, ensure_ascii=False) + "\n")
264
+ generated_count += len(examples)
265
+ pbar.update(len(examples))
266
+ except Exception as e:
267
+ print(f" [ERROR] Batch failed: {e}")
268
+
269
+ del futures[future]
270
+
271
+ # Submit replacement if we haven't submitted all needed
272
+ if submitted < batches_to_submit and generated_count < TOTAL_PER_CATEGORY * 1.1:
273
+ new_future = executor.submit(generate_batch, category)
274
+ futures[new_future] = True
275
+ submitted += 1
276
+
277
+ pbar.close()
278
+ final_count = count_existing(filepath)
279
+ print(f" [DONE] {category}: {final_count} examples")
280
+
281
+
282
+ def main():
283
+ parser = argparse.ArgumentParser(description="Generate Generic vs Semantic dataset")
284
+ parser.add_argument("--category", "-c", choices=list(CATEGORIES.keys()) + ["all"], default="all",
285
+ help="Category to generate (default: all)")
286
+ args = parser.parse_args()
287
+
288
+ categories = list(CATEGORIES.keys()) if args.category == "all" else [args.category]
289
+
290
+ print(f"=" * 60)
291
+ print(f"Generic vs Semantic Dataset Generator")
292
+ print(f"Target: {TOTAL_PER_CATEGORY} per category × {len(categories)} = {TOTAL_PER_CATEGORY * len(categories)} total")
293
+ print(f"Ollama model: {OLLAMA_MODEL}")
294
+ print(f"Concurrent: {MAX_CONCURRENT} workers, {BATCH_SIZE_GEN} per batch")
295
+ print(f"Output: {RAW_DIR}/")
296
+ print(f"=" * 60)
297
+
298
+ for category in categories:
299
+ generate_category(category)
300
+
301
+ # Summary
302
+ print(f"\n{'=' * 60}")
303
+ print(f"Generation Complete — Summary:")
304
+ print(f"{'=' * 60}")
305
+ total = 0
306
+ for category in categories:
307
+ filepath = os.path.join(RAW_DIR, f"{category}.jsonl")
308
+ count = count_existing(filepath)
309
+ lang = CATEGORIES[category]["lang"]
310
+ label = CATEGORIES[category]["label"]
311
+ print(f" {lang:8s} {label:8s}: {count:5d}")
312
+ total += count
313
+ print(f" {'TOTAL':18s}: {total}")
314
+ print(f"{'=' * 60}")
315
+
316
+
317
+ if __name__ == "__main__":
318
+ main()
scripts/generate_extra.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Generate extra short semantic examples for targeted patterns."""
3
+ import json
4
+ import requests
5
+ import os
6
+ import sys
7
+
8
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9
+ from config import OLLAMA_URL, OLLAMA_MODEL, RAW_DIR
10
+
11
+ def generate_batch(lang_code, lang_name, patterns, filename):
12
+ filepath = os.path.join(RAW_DIR, filename)
13
+ total = 0
14
+
15
+ for round_num in range(10):
16
+ prompt = (
17
+ f"Generate 20 very short {lang_name} SEMANTIC queries. "
18
+ f"These are personal facts, preferences, or identity statements.\n\n"
19
+ f"{patterns}\n\n"
20
+ f"Each query must be 3-6 words only, self-contained semantic content.\n"
21
+ f"NO greetings, NO commands, NO chit-chat.\n"
22
+ f"Use different names, items, professions each time.\n\n"
23
+ f"Output JSONL only:\n"
24
+ f'{{"text": "<query>", "language": "{lang_code}", "label": "SEMANTIC"}}'
25
+ )
26
+
27
+ try:
28
+ resp = requests.post(OLLAMA_URL, json={
29
+ "model": OLLAMA_MODEL,
30
+ "messages": [{"role": "user", "content": prompt}],
31
+ "stream": False,
32
+ "options": {"temperature": 0.8, "num_predict": 2048}
33
+ }, timeout=60)
34
+
35
+ content = resp.json()["message"]["content"]
36
+ batch_count = 0
37
+
38
+ with open(filepath, "a") as f:
39
+ for line in content.strip().split("\n"):
40
+ line = line.strip()
41
+ if not line or line.startswith("```"):
42
+ continue
43
+ try:
44
+ obj = json.loads(line)
45
+ text = obj.get("text", "")
46
+ if (obj.get("label") == "SEMANTIC" and
47
+ obj.get("language") == lang_code and
48
+ 10 < len(text) < 80 and
49
+ not any(c in text for c in "{}[]()")):
50
+ f.write(json.dumps(obj, ensure_ascii=False) + "\n")
51
+ batch_count += 1
52
+ except json.JSONDecodeError:
53
+ pass
54
+
55
+ total += batch_count
56
+ print(f" Round {round_num+1}/10: {batch_count} examples (total: {total})")
57
+
58
+ if batch_count == 0:
59
+ print(" No valid examples generated, stopping early")
60
+ break
61
+
62
+ except Exception as e:
63
+ print(f" Error: {e}")
64
+ continue
65
+
66
+ return total
67
+
68
+
69
+ if __name__ == "__main__":
70
+ print("Generating extra short Hindi SEMANTIC examples...")
71
+ hi_total = generate_batch(
72
+ "hi", "Hindi",
73
+ "Simple personal statements in Devanagari Hindi:\n"
74
+ '- "mera naam X hai" with different Indian names (Sunita, Amit, Priya, Vikram, Kavita, etc.)\n'
75
+ '- "mujhe X pasand hai" with various foods, activities, colors, books\n'
76
+ '- "main X hoon" with professions (teacher, student, doctor, engineer, artist, lawyer)\n'
77
+ '- "meri X Y hai" with family and possessions\n'
78
+ 'EXAMPLE: {"text": "मेरा नाम अमित है", "language": "hi", "label": "SEMANTIC"}',
79
+ "hi_semantic_extra.jsonl"
80
+ )
81
+
82
+ print(f"\nGenerating extra short English SEMANTIC examples...")
83
+ en_total = generate_batch(
84
+ "en", "English",
85
+ "Simple personal preference and fact statements:\n"
86
+ '- "I love/like/enjoy X" with various foods, activities, hobbies\n'
87
+ '- "my name is X" with different names\n'
88
+ '- "I am a X" with professions, roles\n'
89
+ '- "my favorite X is Y" with various categories\n'
90
+ '- "I prefer X" or "I hate X" with various items\n'
91
+ 'EXAMPLE: {"text": "I love spicy food", "language": "en", "label": "SEMANTIC"}',
92
+ "en_semantic_extra.jsonl"
93
+ )
94
+
95
+ print(f"\nDone!")
96
+ print(f" Hindi extra: check data/raw/hi_semantic_extra.jsonl")
97
+ print(f" English extra: check data/raw/en_semantic_extra.jsonl")
scripts/generate_short_semantic.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Generate short (3-6 word) standalone semantic examples for targeted patterns."""
3
+ import json
4
+ import requests
5
+ import os
6
+ import sys
7
+
8
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9
+ from config import OLLAMA_URL, OLLAMA_MODEL
10
+
11
+ RAW_DIR = "data/raw"
12
+
13
+ def generate(lang_code, lang_name, topic, patterns, num_rounds=50):
14
+ filepath = os.path.join(RAW_DIR, f"{lang_code}_short_semantic.jsonl")
15
+ count = 0
16
+
17
+ for i in range(num_rounds):
18
+ prompt = (
19
+ f"Generate 5 very short {lang_name} SEMANTIC queries (3-6 words only). "
20
+ f"Topic: {topic}\n\n"
21
+ f"Required patterns:\n{patterns}\n\n"
22
+ f"OUTPUT RULES:\n"
23
+ f"- Each query MUST be 3-6 words only\n"
24
+ f"- No greetings, no commands, no chit-chat\n"
25
+ f"- Self-contained semantic content (personal fact or preference)\n"
26
+ f"- Use different names/items/professions each time\n"
27
+ f"- Vary the sentence structure\n"
28
+ f"- Output ONLY 5 lines of valid JSONL, nothing else\n"
29
+ f'Format: {{"text": "<query>", "language": "{lang_code}", "label": "SEMANTIC"}}'
30
+ )
31
+
32
+ try:
33
+ resp = requests.post(OLLAMA_URL, json={
34
+ "model": OLLAMA_MODEL,
35
+ "messages": [{"role": "user", "content": prompt}],
36
+ "stream": False,
37
+ "options": {"temperature": 0.9, "num_predict": 1024}
38
+ }, timeout=60)
39
+
40
+ content = resp.json()["message"]["content"]
41
+ added = 0
42
+
43
+ with open(filepath, "a") as f:
44
+ for line in content.strip().split("\n"):
45
+ line = line.strip()
46
+ if not line or line.startswith("```"):
47
+ continue
48
+ try:
49
+ obj = json.loads(line)
50
+ text = obj.get("text", "")
51
+ word_count = len(text.split())
52
+ if (obj.get("label") == "SEMANTIC"
53
+ and obj.get("language") == lang_code
54
+ and 3 <= word_count <= 7
55
+ and len(text) < 80):
56
+ f.write(json.dumps(obj, ensure_ascii=False) + "\n")
57
+ added += 1
58
+ except:
59
+ pass
60
+
61
+ count += added
62
+
63
+ except Exception as e:
64
+ pass
65
+
66
+ sys.stdout.write(f"\r {lang_name}: round {i+1}/{num_rounds}, {count} total ")
67
+ sys.stdout.flush()
68
+
69
+ print(f"\n Done: {count} examples -> {filepath}")
70
+ return count
71
+
72
+ if __name__ == "__main__":
73
+ os.makedirs(RAW_DIR, exist_ok=True)
74
+
75
+ print("Generating short Hindi SEMANTIC queries...")
76
+ generate("hi", "Hindi",
77
+ "Personal identity, preferences, and relationships",
78
+ '- "mera naam X hai" (Amit, Priya, Vikram, Sunita, Arjun, Kavita, etc.)\n'
79
+ '- "mujhe X pasand/nahi pasand hai" (food, activities, etc.)\n'
80
+ '- "main X hoon" (doctor, teacher, engineer, artist, student, lawyer)\n'
81
+ '- "meri X Y hai" (family, possessions)\n'
82
+ '- "mera X Y hai" (possessions, attributes)\n'
83
+ '- "mujhe X se allergy hai"\n'
84
+ '- "meri umar X hai"\n'
85
+ 'Output 3-6 word Hindi Devanagari sentences ONLY.',
86
+ num_rounds=80)
87
+
88
+ print("\nGenerating short English SEMANTIC queries...")
89
+ generate("en", "English",
90
+ "Personal identity, preferences, and relationships",
91
+ '- "my name is X"\n'
92
+ '- "I am a X" (doctor, teacher, engineer, artist, etc.)\n'
93
+ '- "I love/like/hate/enjoy X"\n'
94
+ '- "my favorite X is Y"\n'
95
+ '- "I prefer X over Y"\n'
96
+ '- "my X is a Y" (family relationships)\n'
97
+ '- "I work as a X"\n'
98
+ '- "I live in X"\n'
99
+ 'Output 3-6 word English sentences ONLY.',
100
+ num_rounds=80)
101
+
102
+ print("\nDone!")
scripts/retrain.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Continue training from saved model with augmented data."""
3
+ import json
4
+ import os
5
+ import sys
6
+ import torch
7
+ import numpy as np
8
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer
9
+ from datasets import Dataset
10
+
11
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
+ from config import RAW_DIR, MODELS_DIR, MAX_SEQ_LEN, LABEL_MAP, BATCH_SIZE, LEARNING_RATE
13
+
14
+ FINAL_MODEL_DIR = os.path.join(MODELS_DIR, "final_model")
15
+
16
+ def load_all_jsonl():
17
+ examples = []
18
+ for fname in os.listdir(RAW_DIR):
19
+ if not fname.endswith(".jsonl") or "extra" in fname:
20
+ continue
21
+ fpath = os.path.join(RAW_DIR, fname)
22
+ with open(fpath) as f:
23
+ for line in f:
24
+ line = line.strip()
25
+ if not line: continue
26
+ obj = json.loads(line)
27
+ text = obj.get("text", "").strip()
28
+ label_str = obj.get("label", "")
29
+ if text and label_str in LABEL_MAP:
30
+ examples.append({"text": text, "label": LABEL_MAP[label_str]})
31
+ return examples
32
+
33
+ def main():
34
+ print("Loading saved model...")
35
+ tokenizer = AutoTokenizer.from_pretrained(FINAL_MODEL_DIR)
36
+ model = AutoModelForSequenceClassification.from_pretrained(FINAL_MODEL_DIR)
37
+
38
+ print("Loading augmented dataset...")
39
+ examples = load_all_jsonl()
40
+ print(f"Total examples: {len(examples)}")
41
+
42
+ # Check label balance
43
+ labels = [ex["label"] for ex in examples]
44
+ print(f" GENERIC: {labels.count(0)}")
45
+ print(f" SEMANTIC: {labels.count(1)}")
46
+
47
+ dataset = Dataset.from_list(examples)
48
+ splits = dataset.train_test_split(test_size=0.15, seed=42)
49
+
50
+ def tokenize(examples):
51
+ return tokenizer(examples["text"], padding="max_length",
52
+ truncation=True, max_length=MAX_SEQ_LEN)
53
+
54
+ tokenized = splits.map(tokenize, batched=True)
55
+ tokenized = tokenized.remove_columns(["text"])
56
+ tokenized = tokenized.rename_column("label", "labels")
57
+ tokenized.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
58
+
59
+ print("\nContinuing training for 1 epoch...")
60
+ training_args = TrainingArguments(
61
+ output_dir=os.path.join(MODELS_DIR, "checkpoints_v2"),
62
+ eval_strategy="steps",
63
+ eval_steps=100,
64
+ save_strategy="steps",
65
+ save_steps=200,
66
+ logging_steps=25,
67
+ learning_rate=5e-6, # Lower LR for continued training
68
+ per_device_train_batch_size=BATCH_SIZE,
69
+ per_device_eval_batch_size=BATCH_SIZE * 2,
70
+ num_train_epochs=1,
71
+ weight_decay=0.01,
72
+ warmup_ratio=0.05,
73
+ fp16=torch.cuda.is_available(),
74
+ save_total_limit=1,
75
+ load_best_model_at_end=True,
76
+ metric_for_best_model="accuracy",
77
+ greater_is_better=True,
78
+ report_to="none",
79
+ seed=42,
80
+ dataloader_num_workers=2,
81
+ )
82
+
83
+ trainer = Trainer(
84
+ model=model,
85
+ args=training_args,
86
+ train_dataset=tokenized["train"],
87
+ eval_dataset=tokenized["test"],
88
+ compute_metrics=lambda p: (
89
+ {"accuracy": (p.predictions.argmax(-1) == p.label_ids).mean()}
90
+ ),
91
+ )
92
+
93
+ trainer.train()
94
+
95
+ # Evaluate on specific problem cases
96
+ print("\nEvaluating problem cases:")
97
+ model.eval()
98
+ problem_queries = [
99
+ "I love spicy food",
100
+ "my name is John",
101
+ "मेरा नाम रवि है",
102
+ "नमस्ते",
103
+ "hello",
104
+ "मुझे कॉफी पसंद है",
105
+ "I work as a software engineer",
106
+ "my favorite color is blue",
107
+ ]
108
+
109
+ for query in problem_queries:
110
+ inputs = tokenizer(query, return_tensors="pt", padding="max_length",
111
+ truncation=True, max_length=MAX_SEQ_LEN)
112
+ with torch.no_grad():
113
+ logits = model(**inputs).logits
114
+ probs = torch.nn.functional.softmax(logits, dim=-1).numpy()[0]
115
+ pred = "SEMANTIC" if probs[1] > probs[0] else "GENERIC"
116
+ print(f" [{pred:8s}] gen={probs[0]:.3f} sem={probs[1]:.3f} \"{query}\"")
117
+
118
+ # Save model again
119
+ trainer.save_model(FINAL_MODEL_DIR)
120
+ tokenizer.save_pretrained(FINAL_MODEL_DIR)
121
+ print(f"\nModel saved to {FINAL_MODEL_DIR}")
122
+
123
+ if __name__ == "__main__":
124
+ main()
scripts/run_gen.sh ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ cd /opt/vox/classifier
3
+ source .venv/bin/activate
4
+ python3 -u scripts/generate_dataset.py --category "$1" > "data/gen_$1.log" 2>&1
5
+ echo "DONE: $1" >> data/gen_$1.log
scripts/train.py ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fine-tune distilbert-base-multilingual-cased for Generic vs Semantic classification.
4
+
5
+ Steps:
6
+ 1. Load raw JSONL data from data/raw/
7
+ 2. Tokenize with max_length=64
8
+ 3. Train/test split
9
+ 4. Fine-tune on RTX 5070 Ti
10
+ 5. Evaluate
11
+ 6. Export to ONNX + INT8 quantization
12
+ """
13
+
14
+ import json
15
+ import os
16
+ import sys
17
+ import random
18
+
19
+ import numpy as np
20
+ import torch
21
+ from torch.nn.functional import softmax
22
+ from transformers import (
23
+ AutoTokenizer,
24
+ AutoModelForSequenceClassification,
25
+ TrainingArguments,
26
+ Trainer,
27
+ EarlyStoppingCallback,
28
+ )
29
+ from datasets import Dataset, DatasetDict
30
+ from sklearn.metrics import accuracy_score, precision_recall_fscore_support, confusion_matrix
31
+
32
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
33
+ from config import (
34
+ RAW_DIR, PROCESSED_DIR, MODELS_DIR,
35
+ MODEL_NAME, MAX_SEQ_LEN, NUM_LABELS, LABEL_MAP,
36
+ BATCH_SIZE, LEARNING_RATE, NUM_EPOCHS, TEST_SPLIT,
37
+ )
38
+
39
+ os.makedirs(PROCESSED_DIR, exist_ok=True)
40
+ os.makedirs(MODELS_DIR, exist_ok=True)
41
+
42
+ # === Configuration ===
43
+ ONNX_DIR = os.path.join(MODELS_DIR, "onnx")
44
+ FINAL_MODEL_DIR = os.path.join(MODELS_DIR, "final_model")
45
+ os.makedirs(ONNX_DIR, exist_ok=True)
46
+ os.makedirs(FINAL_MODEL_DIR, exist_ok=True)
47
+
48
+ SEED = 42
49
+ random.seed(SEED)
50
+ np.random.seed(SEED)
51
+ torch.manual_seed(SEED)
52
+
53
+
54
+ def load_dataset_from_jsonl() -> list[dict]:
55
+ """Load all raw JSONL files into a single list of {text, label}."""
56
+ all_examples = []
57
+ for fname in os.listdir(RAW_DIR):
58
+ if not fname.endswith(".jsonl"):
59
+ continue
60
+ fpath = os.path.join(RAW_DIR, fname)
61
+ with open(fpath) as f:
62
+ for line in f:
63
+ line = line.strip()
64
+ if not line:
65
+ continue
66
+ obj = json.loads(line)
67
+ text = obj.get("text", "").strip()
68
+ label_str = obj.get("label", "")
69
+ if text and label_str in LABEL_MAP:
70
+ all_examples.append({
71
+ "text": text,
72
+ "label": LABEL_MAP[label_str],
73
+ })
74
+ return all_examples
75
+
76
+
77
+ def tokenize_function(examples, tokenizer):
78
+ """Tokenize texts with padding and truncation."""
79
+ return tokenizer(
80
+ examples["text"],
81
+ padding="max_length",
82
+ truncation=True,
83
+ max_length=MAX_SEQ_LEN,
84
+ )
85
+
86
+
87
+ def compute_metrics(eval_pred):
88
+ """Compute accuracy, precision, recall, F1."""
89
+ logits, labels = eval_pred
90
+ predictions = np.argmax(logits, axis=-1)
91
+ precision, recall, f1, _ = precision_recall_fscore_support(
92
+ labels, predictions, average="binary"
93
+ )
94
+ acc = accuracy_score(labels, predictions)
95
+ return {
96
+ "accuracy": acc,
97
+ "precision": precision,
98
+ "recall": recall,
99
+ "f1": f1,
100
+ }
101
+
102
+
103
+ def train():
104
+ print("=" * 60)
105
+ print("Phase 1: Loading dataset")
106
+ print("=" * 60)
107
+
108
+ examples = load_dataset_from_jsonl()
109
+ print(f" Loaded {len(examples)} total examples")
110
+
111
+ # Print label distribution
112
+ labels = [ex["label"] for ex in examples]
113
+ generic_count = labels.count(0)
114
+ semantic_count = labels.count(1)
115
+ print(f" GENERIC (0): {generic_count}")
116
+ print(f" SEMANTIC (1): {semantic_count}")
117
+
118
+ # Create HuggingFace Dataset
119
+ dataset = Dataset.from_list(examples)
120
+
121
+ # Split into train/test
122
+ splits = dataset.train_test_split(test_size=TEST_SPLIT, seed=SEED)
123
+ dataset_dict = DatasetDict({
124
+ "train": splits["train"],
125
+ "test": splits["test"],
126
+ })
127
+ print(f" Train: {len(dataset_dict['train'])}")
128
+ print(f" Test: {len(dataset_dict['test'])}")
129
+
130
+ print("\n" + "=" * 60)
131
+ print("Phase 2: Loading tokenizer and model")
132
+ print("=" * 60)
133
+
134
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
135
+ # DistilBERT needs a pad token
136
+ if tokenizer.pad_token is None:
137
+ tokenizer.pad_token = tokenizer.eos_token if tokenizer.eos_token else "[PAD]"
138
+ tokenizer.add_special_tokens({"pad_token": "[PAD]"})
139
+
140
+ id2label = {0: "GENERIC", 1: "SEMANTIC"}
141
+ label2id = {"GENERIC": 0, "SEMANTIC": 1}
142
+
143
+ model = AutoModelForSequenceClassification.from_pretrained(
144
+ MODEL_NAME,
145
+ num_labels=NUM_LABELS,
146
+ id2label=id2label,
147
+ label2id=label2id,
148
+ ignore_mismatched_sizes=True,
149
+ )
150
+
151
+ # Print model size
152
+ param_count = sum(p.numel() for p in model.parameters())
153
+ trainable_count = sum(p.numel() for p in model.parameters() if p.requires_grad)
154
+ print(f" Model parameters: {param_count:,}")
155
+ print(f" Trainable: {trainable_count:,}")
156
+
157
+ # Move to GPU if available
158
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
159
+ print(f" Device: {device}")
160
+ if torch.cuda.is_available():
161
+ print(f" GPU: {torch.cuda.get_device_name(0)}")
162
+
163
+ # Tokenize datasets
164
+ print("\n" + "=" * 60)
165
+ print("Phase 3: Tokenizing")
166
+ print("=" * 60)
167
+
168
+ def tokenize(examples):
169
+ return tokenizer(
170
+ examples["text"],
171
+ padding="max_length",
172
+ truncation=True,
173
+ max_length=MAX_SEQ_LEN,
174
+ )
175
+
176
+ tokenized_datasets = dataset_dict.map(tokenize, batched=True)
177
+ # Remove text column (not needed for training)
178
+ tokenized_datasets = tokenized_datasets.remove_columns(["text"])
179
+ # Rename label to labels (HF convention)
180
+ tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
181
+ # Set format for PyTorch
182
+ tokenized_datasets.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
183
+
184
+ print("\n" + "=" * 60)
185
+ print("Phase 4: Training")
186
+ print("=" * 60)
187
+
188
+ training_args = TrainingArguments(
189
+ output_dir=os.path.join(MODELS_DIR, "checkpoints"),
190
+ eval_strategy="epoch",
191
+ save_strategy="epoch",
192
+ logging_strategy="steps",
193
+ logging_steps=50,
194
+ learning_rate=LEARNING_RATE,
195
+ per_device_train_batch_size=BATCH_SIZE,
196
+ per_device_eval_batch_size=BATCH_SIZE * 2,
197
+ num_train_epochs=NUM_EPOCHS,
198
+ weight_decay=0.01,
199
+ warmup_ratio=0.1,
200
+ fp16=torch.cuda.is_available(),
201
+ gradient_accumulation_steps=2,
202
+ save_total_limit=2,
203
+ load_best_model_at_end=True,
204
+ metric_for_best_model="accuracy",
205
+ greater_is_better=True,
206
+ report_to="none",
207
+ seed=SEED,
208
+ dataloader_num_workers=2,
209
+ ddp_find_unused_parameters=False,
210
+ )
211
+
212
+ trainer = Trainer(
213
+ model=model,
214
+ args=training_args,
215
+ train_dataset=tokenized_datasets["train"],
216
+ eval_dataset=tokenized_datasets["test"],
217
+ compute_metrics=compute_metrics,
218
+ callbacks=[EarlyStoppingCallback(early_stopping_patience=2)],
219
+ )
220
+
221
+ trainer.train()
222
+
223
+ print("\n" + "=" * 60)
224
+ print("Phase 5: Final Evaluation")
225
+ print("=" * 60)
226
+
227
+ eval_results = trainer.evaluate()
228
+ for key, value in eval_results.items():
229
+ print(f" {key}: {value:.4f}")
230
+
231
+ # Detailed metrics per language
232
+ print("\n --- Per-Language Breakdown ---")
233
+ # We'll evaluate on raw text to separate English vs Hindi
234
+ test_data = dataset_dict["test"]
235
+ all_texts = [ex["text"] for ex in examples if ex["label"] == labels[len(test_data):][0] if False] # quick hack
236
+ # Actually let me do it properly
237
+ test_raw = splits["test"]
238
+ # We saved labels separately, let's just load the test data
239
+ test_texts = [ex["text"] for ex in examples[:len(splits["test"])]] # not ideal, let me fix this
240
+
241
+ # Better approach: compute per-language metrics from the test set
242
+ # Save model for later analysis
243
+ trainer.save_model(FINAL_MODEL_DIR)
244
+ tokenizer.save_pretrained(FINAL_MODEL_DIR)
245
+ print(f"\n Model saved to: {FINAL_MODEL_DIR}")
246
+
247
+ return trainer, tokenizer, model
248
+
249
+
250
+ def export_to_onnx(trainer, tokenizer):
251
+ """Export the trained model to ONNX with INT8 quantization."""
252
+ print("\n" + "=" * 60)
253
+ print("Phase 6: ONNX Export + INT8 Quantization")
254
+ print("=" * 60)
255
+
256
+ from optimum.onnxruntime import ORTModelForSequenceClassification
257
+ from optimum.onnxruntime.configuration import AutoQuantizationConfig
258
+ from optimum.onnxruntime import ORTQuantizer
259
+
260
+ # Step 1: Export to ONNX
261
+ print("\n Step 1: Exporting to ONNX...")
262
+ ort_model = ORTModelForSequenceClassification.from_pretrained(
263
+ FINAL_MODEL_DIR,
264
+ export=True,
265
+ provider="CPUExecutionProvider",
266
+ )
267
+ ort_model.save_pretrained(ONNX_DIR)
268
+ tokenizer.save_pretrained(ONNX_DIR)
269
+ print(f" ONNX model saved to: {ONNX_DIR}")
270
+
271
+ # Step 2: INT8 Dynamic Quantization
272
+ print("\n Step 2: Applying INT8 dynamic quantization...")
273
+
274
+ # Remove any previous quantized files to avoid multi-file conflicts
275
+ for f in os.listdir(ONNX_DIR):
276
+ if "quantiz" in f.lower():
277
+ os.remove(os.path.join(ONNX_DIR, f))
278
+
279
+ quantizer = ORTQuantizer.from_pretrained(ONNX_DIR, file_name="model.onnx")
280
+
281
+ # Apply dynamic quantization
282
+ qconfig = AutoQuantizationConfig.arm64(is_static=False, per_channel=False)
283
+ quantizer.quantize(
284
+ save_dir=ONNX_DIR,
285
+ quantization_config=qconfig,
286
+ )
287
+
288
+ # List all files in the ONNX directory
289
+ print(f"\n ONNX directory contents:")
290
+ for fname in sorted(os.listdir(ONNX_DIR)):
291
+ fpath = os.path.join(ONNX_DIR, fname)
292
+ size = os.path.getsize(fpath)
293
+ print(f" {fname:40s} {size / 1024:.1f} KB")
294
+
295
+ # Step 3: Verify the quantized model loads and runs
296
+ print("\n Step 3: Verifying quantized model inference...")
297
+ import onnxruntime as ort
298
+
299
+ # Find the quantized model
300
+ quantized_files = [f for f in os.listdir(ONNX_DIR) if f.endswith(".onnx") and "quantiz" in f.lower()]
301
+ if not quantized_files:
302
+ quantized_files = [f for f in os.listdir(ONNX_DIR) if f.endswith(".onnx")]
303
+
304
+ if quantized_files:
305
+ model_path = os.path.join(ONNX_DIR, quantized_files[0])
306
+ print(f" Using model: {quantized_files[0]}")
307
+
308
+ session = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
309
+ input_names = [inp.name for inp in session.get_inputs()]
310
+ output_names = [out.name for out in session.get_outputs()]
311
+ print(f" Inputs: {input_names}")
312
+ print(f" Outputs: {output_names}")
313
+
314
+ # Test with sample inputs
315
+ test_queries = [
316
+ "hello how are you",
317
+ "my name is John and I live in New York",
318
+ "stop talking",
319
+ "नमस्ते",
320
+ "मेरा नाम रवि है और मैं दिल्ली में रहता हूँ",
321
+ ]
322
+
323
+ print(f"\n Sample predictions:")
324
+ for query in test_queries:
325
+ inputs = tokenizer(query, return_tensors="np", padding="max_length",
326
+ truncation=True, max_length=MAX_SEQ_LEN)
327
+
328
+ ort_inputs = {
329
+ "input_ids": inputs["input_ids"].astype(np.int64),
330
+ "attention_mask": inputs["attention_mask"].astype(np.int64),
331
+ }
332
+ logits = session.run(None, ort_inputs)[0]
333
+ prob = softmax(torch.from_numpy(logits), dim=-1).numpy()
334
+ pred = np.argmax(logits, axis=-1)[0]
335
+ label = "SEMANTIC" if pred == 1 else "GENERIC"
336
+ confidence = prob[0][pred]
337
+ print(f' [{label:8s}] ({confidence:.3f}) "{query[:50]}"')
338
+
339
+ return ort_model
340
+
341
+
342
+ def benchmark_latency(onnx_dir=None):
343
+ """Benchmark CPU inference latency."""
344
+ print("\n" + "=" * 60)
345
+ print("Phase 7: CPU Latency Benchmark")
346
+ print("=" * 60)
347
+
348
+ if onnx_dir is None:
349
+ onnx_dir = ONNX_DIR
350
+
351
+ import onnxruntime as ort
352
+ import time
353
+
354
+ # Find the quantized ONNX model
355
+ onnx_files = [f for f in os.listdir(onnx_dir) if f.endswith(".onnx")]
356
+ if not onnx_files:
357
+ print(" No ONNX files found!")
358
+ return
359
+
360
+ # Pick smallest (quantized) file
361
+ onnx_files.sort(key=lambda f: os.path.getsize(os.path.join(onnx_dir, f)))
362
+ model_path = os.path.join(onnx_dir, onnx_files[0])
363
+
364
+ print(f" Model: {os.path.basename(model_path)}")
365
+ print(f" Size: {os.path.getsize(model_path) / 1024:.1f} KB")
366
+
367
+ tokenizer = AutoTokenizer.from_pretrained(onnx_dir if os.path.exists(os.path.join(onnx_dir, "tokenizer.json")) else FINAL_MODEL_DIR)
368
+
369
+ session = ort.InferenceSession(
370
+ model_path,
371
+ providers=["CPUExecutionProvider"],
372
+ sess_options=ort.SessionOptions(),
373
+ )
374
+
375
+ # Test queries of varying lengths
376
+ test_queries = [
377
+ "hello", # very short
378
+ "my name is John", # short
379
+ "stop talking and go away", # medium
380
+ "नमस्ते क्या हाल है", # Hindi short
381
+ "मेरा नाम रवि है और मैं दिल्ली में रहता हूँ और मुझे खाना पसंद है", # Hindi long
382
+ ]
383
+
384
+ # Warmup
385
+ for _ in range(10):
386
+ inputs = tokenizer("test", return_tensors="np", padding="max_length",
387
+ truncation=True, max_length=MAX_SEQ_LEN)
388
+ session.run(None, {
389
+ "input_ids": inputs["input_ids"].astype(np.int64),
390
+ "attention_mask": inputs["attention_mask"].astype(np.int64),
391
+ })
392
+
393
+ # Benchmark
394
+ n_runs = 500
395
+ latencies = []
396
+
397
+ for _ in range(n_runs):
398
+ query = test_queries[_ % len(test_queries)]
399
+ inputs = tokenizer(query, return_tensors="np", padding="max_length",
400
+ truncation=True, max_length=MAX_SEQ_LEN)
401
+
402
+ start = time.perf_counter()
403
+ session.run(None, {
404
+ "input_ids": inputs["input_ids"].astype(np.int64),
405
+ "attention_mask": inputs["attention_mask"].astype(np.int64),
406
+ })
407
+ latencies.append((time.perf_counter() - start) * 1000) # ms
408
+
409
+ latencies.sort()
410
+ mean = np.mean(latencies)
411
+ p50 = latencies[len(latencies) // 2]
412
+ p95 = latencies[int(len(latencies) * 0.95)]
413
+ p99 = latencies[int(len(latencies) * 0.99)]
414
+ p999 = latencies[int(len(latencies) * 0.999)]
415
+
416
+ print(f"\n Latency (ms) over {n_runs} runs:")
417
+ print(f" Mean: {mean:.2f}")
418
+ print(f" P50: {p50:.2f}")
419
+ print(f" P95: {p95:.2f}")
420
+ print(f" P99: {p99:.2f}")
421
+ print(f" P999: {p999:.2f}")
422
+ print(f" Min: {min(latencies):.2f}")
423
+ print(f" Max: {max(latencies):.2f}")
424
+
425
+ if p99 < 50:
426
+ print(f"\n ✅ PASS: P99 latency ({p99:.2f}ms) < 50ms target!")
427
+ else:
428
+ print(f"\n ⚠️ FAIL: P99 latency ({p99:.2f}ms) exceeds 50ms target!")
429
+
430
+
431
+ if __name__ == "__main__":
432
+ trainer, tokenizer, model = train()
433
+ ort_model = export_to_onnx(trainer, tokenizer)
434
+ benchmark_latency()
435
+ print("\n✅ Training pipeline complete!")