xavier-fuentes commited on
Commit
3ef75e6
·
verified ·
1 Parent(s): 6b7e476

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +30 -19
  2. app.py +94 -7
  3. requirements.txt +4 -3
README.md CHANGED
@@ -1,35 +1,46 @@
1
  ---
2
  title: Zero-Shot Text Classifier
3
  emoji: 🏷️
4
- colorFrom: indigo
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 5.14.0
8
  app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
  # Zero-Shot Text Classifier
13
 
14
- This Hugging Face Space performs zero-shot text classification with:
15
 
16
- - Model: `facebook/bart-large-mnli`
17
- - Task: classify input text against user-defined labels
18
- - Modes: single-label and multi-label
19
- - Extras: preset label sets, inference timing, long-text truncation handling
20
 
21
- ## How to use
 
 
 
22
 
23
- 1. Enter text in **Text to Classify**.
24
- 2. Enter labels in **Candidate Labels** as comma-separated values.
25
- 3. Toggle **Multi-label mode** when needed.
26
- 4. Click **Classify** to see score bars and a summary.
27
 
28
- ## Presets included
 
 
 
29
 
30
- - Sentiment: positive, negative, neutral
31
- - Topic: tech, business, sports, science, politics
32
- - Intent: question, statement, request, complaint
33
- - Tone: formal, casual, urgent, friendly
34
 
35
- Built by [Xavier Fuentes](https://huggingface.co/xavier-fuentes) @ [AI Enablement Academy](https://enablement.academy) | [Buy me a coffee ☕](https://ko-fi.com/xavierfuentes)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Zero-Shot Text Classifier
3
  emoji: 🏷️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: "5.33.0"
8
  app_file: app.py
9
  pinned: false
10
+ license: apache-2.0
11
+ short_description: Classify text into any custom categories with Qwen3-0.6B
12
  ---
13
 
14
  # Zero-Shot Text Classifier
15
 
16
+ Classify any text into your own custom categories using **Qwen3-0.6B** with zero-shot instruction prompting.
17
 
18
+ ## Features
 
 
 
19
 
20
+ - **Custom labels**: Define any categories you want
21
+ - **Multi-label mode**: Allow multiple labels to apply simultaneously
22
+ - **Preset label sets**: Quick-start with Sentiment, Topic, Intent, or Tone presets
23
+ - **Fast inference**: ~200ms on GPU via ZeroGPU
24
 
25
+ ## Why Qwen3 over BART-MNLI?
 
 
 
26
 
27
+ - Qwen3-0.6B is smaller (0.6B vs 0.4B) but more capable due to modern architecture
28
+ - Handles multi-label classification natively via instruction prompting
29
+ - Supports structured JSON output for downstream integration
30
+ - Better accuracy on diverse classification tasks (not limited to NLI-style inference)
31
 
32
+ ## API Usage
 
 
 
33
 
34
+ ```python
35
+ from gradio_client import Client
36
+
37
+ client = Client("xavier-fuentes/text-classifier")
38
+ result = client.predict(
39
+ text="The product quality is amazing but shipping was slow",
40
+ candidate_labels="positive, negative, mixed",
41
+ multi_label=False,
42
+ api_name="/run_classification"
43
+ )
44
+ ```
45
+
46
+ Built by [Xavier Fuentes](https://huggingface.co/xavier-fuentes) @ [AI Enablement Academy](https://enablement.academy)
app.py CHANGED
@@ -1,14 +1,21 @@
 
1
  import time
2
  from typing import List, Tuple
3
 
4
  import gradio as gr
5
  import spaces
6
- from transformers import pipeline
 
7
 
8
- MODEL_ID = "facebook/bart-large-mnli"
9
  MAX_TEXT_CHARS = 4000
10
 
11
- classifier = pipeline("zero-shot-classification", model=MODEL_ID)
 
 
 
 
 
12
 
13
  PRESET_LABELS = {
14
  "Sentiment": "positive, negative, neutral",
@@ -69,6 +76,52 @@ def apply_preset(preset_name: str) -> str:
69
  return PRESET_LABELS.get(preset_name, "")
70
 
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  @spaces.GPU
73
  def run_classification(text: str, candidate_labels: str, multi_label: bool):
74
  clean_text, was_truncated = truncate_text(text)
@@ -79,11 +132,43 @@ def run_classification(text: str, candidate_labels: str, multi_label: bool):
79
  if len(labels) < 2:
80
  raise gr.Error("Please provide at least 2 labels, separated by commas.")
81
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  start = time.perf_counter()
83
- result = classifier(clean_text, candidate_labels=labels, multi_label=multi_label)
 
 
 
 
 
 
 
 
 
 
 
84
  elapsed = time.perf_counter() - start
85
 
86
- sorted_pairs = sorted(zip(result["labels"], result["scores"]), key=lambda x: x[1], reverse=True)
 
 
 
 
 
 
 
 
 
87
  sorted_labels = [x[0] for x in sorted_pairs]
88
  sorted_scores = [x[1] for x in sorted_pairs]
89
 
@@ -99,8 +184,9 @@ def run_classification(text: str, candidate_labels: str, multi_label: bool):
99
 
100
  summary = (
101
  f"Top prediction: {top_label} ({top_score:.2f}%). "
 
102
  f"Mode: {'multi-label' if multi_label else 'single-label'}. "
103
- f"Inference time: {elapsed:.3f} seconds.{truncation_note}"
104
  )
105
 
106
  return chart_html, summary
@@ -109,7 +195,8 @@ def run_classification(text: str, candidate_labels: str, multi_label: bool):
109
  with gr.Blocks(theme=gr.themes.Soft(), title="Zero-Shot Text Classifier") as demo:
110
  gr.Markdown("# Zero-Shot Text Classifier")
111
  gr.Markdown(
112
- "Classify any text into custom labels using `facebook/bart-large-mnli` with zero-shot inference."
 
113
  )
114
 
115
  with gr.Row():
 
1
+ import json
2
  import time
3
  from typing import List, Tuple
4
 
5
  import gradio as gr
6
  import spaces
7
+ import torch
8
+ from transformers import AutoModelForCausalLM, AutoTokenizer
9
 
10
+ MODEL_ID = "Qwen/Qwen3-0.6B"
11
  MAX_TEXT_CHARS = 4000
12
 
13
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
14
+ model = AutoModelForCausalLM.from_pretrained(
15
+ MODEL_ID,
16
+ torch_dtype=torch.float16,
17
+ device_map="auto",
18
+ )
19
 
20
  PRESET_LABELS = {
21
  "Sentiment": "positive, negative, neutral",
 
76
  return PRESET_LABELS.get(preset_name, "")
77
 
78
 
79
+ def build_classification_prompt(text: str, labels: List[str], multi_label: bool) -> str:
80
+ labels_str = ", ".join(f'"{l}"' for l in labels)
81
+ mode_instruction = (
82
+ "Multiple labels can apply simultaneously. For each label, assign a confidence score between 0 and 1."
83
+ if multi_label
84
+ else "Choose the single best label. Assign confidence scores that sum to 1."
85
+ )
86
+
87
+ return (
88
+ f"Classify the following text into these categories: {labels_str}\n\n"
89
+ f"{mode_instruction}\n\n"
90
+ f"Text: \"{text}\"\n\n"
91
+ f"Respond with ONLY a JSON object mapping each label to its confidence score. "
92
+ f"Example: {{{', '.join(f'\"{l}\": 0.5' for l in labels[:2])}}}\n"
93
+ f"JSON:"
94
+ )
95
+
96
+
97
+ def parse_scores(output: str, labels: List[str]) -> dict:
98
+ """Extract label scores from model output, with fallback parsing."""
99
+ # Try to find JSON in the output
100
+ output = output.strip()
101
+
102
+ # Find the first { and last }
103
+ start = output.find("{")
104
+ end = output.rfind("}")
105
+ if start != -1 and end != -1 and end > start:
106
+ json_str = output[start : end + 1]
107
+ try:
108
+ parsed = json.loads(json_str)
109
+ scores = {}
110
+ for label in labels:
111
+ # Try exact match, then case-insensitive
112
+ if label in parsed:
113
+ scores[label] = float(parsed[label])
114
+ else:
115
+ lower_map = {k.lower(): v for k, v in parsed.items()}
116
+ scores[label] = float(lower_map.get(label.lower(), 0.0))
117
+ return scores
118
+ except (json.JSONDecodeError, ValueError):
119
+ pass
120
+
121
+ # Fallback: equal scores
122
+ return {label: 1.0 / len(labels) for label in labels}
123
+
124
+
125
  @spaces.GPU
126
  def run_classification(text: str, candidate_labels: str, multi_label: bool):
127
  clean_text, was_truncated = truncate_text(text)
 
132
  if len(labels) < 2:
133
  raise gr.Error("Please provide at least 2 labels, separated by commas.")
134
 
135
+ prompt = build_classification_prompt(clean_text, labels, multi_label)
136
+
137
+ messages = [
138
+ {"role": "system", "content": "You are a precise text classifier. Respond only with valid JSON."},
139
+ {"role": "user", "content": prompt},
140
+ ]
141
+
142
+ input_text = tokenizer.apply_chat_template(
143
+ messages, tokenize=False, add_generation_prompt=True,
144
+ enable_thinking=False,
145
+ )
146
+
147
  start = time.perf_counter()
148
+
149
+ inputs = tokenizer(input_text, return_tensors="pt").to(model.device)
150
+ with torch.no_grad():
151
+ outputs = model.generate(
152
+ **inputs,
153
+ max_new_tokens=256,
154
+ temperature=0.1,
155
+ do_sample=True,
156
+ top_p=0.9,
157
+ )
158
+
159
+ generated = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
160
  elapsed = time.perf_counter() - start
161
 
162
+ scores = parse_scores(generated, labels)
163
+
164
+ # Normalize scores
165
+ total = sum(scores.values())
166
+ if total > 0:
167
+ scores = {k: v / total for k, v in scores.items()}
168
+ else:
169
+ scores = {k: 1.0 / len(labels) for k in labels}
170
+
171
+ sorted_pairs = sorted(scores.items(), key=lambda x: x[1], reverse=True)
172
  sorted_labels = [x[0] for x in sorted_pairs]
173
  sorted_scores = [x[1] for x in sorted_pairs]
174
 
 
184
 
185
  summary = (
186
  f"Top prediction: {top_label} ({top_score:.2f}%). "
187
+ f"Model: Qwen3-0.6B. "
188
  f"Mode: {'multi-label' if multi_label else 'single-label'}. "
189
+ f"Inference time: {elapsed:.3f}s.{truncation_note}"
190
  )
191
 
192
  return chart_html, summary
 
195
  with gr.Blocks(theme=gr.themes.Soft(), title="Zero-Shot Text Classifier") as demo:
196
  gr.Markdown("# Zero-Shot Text Classifier")
197
  gr.Markdown(
198
+ "Classify any text into custom labels using **Qwen3-0.6B** with zero-shot instruction prompting. "
199
+ "No fine-tuning needed: define your own categories and classify instantly."
200
  )
201
 
202
  with gr.Row():
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
- gradio
2
- transformers
3
- torch
4
  accelerate
 
 
1
+ gradio>=4.0
2
+ transformers>=4.45.0
3
+ torch>=2.0
4
  accelerate
5
+ spaces