File size: 5,255 Bytes
dad80ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Benchmark the zero-shot classifier against a small labelled set.

Run from the project root:

    python scripts/bench.py                       # current MODEL_NAME
    python scripts/bench.py MoritzLaurer/deberta-v3-xsmall-zeroshot-v1.1-all-33

Prints accuracy for a sweep of LEXICAL_WEIGHT values and hypothesis templates,
so config changes are made on evidence rather than vibes. The cases are the
English text as it would arrive *after* translation, since that is what the
model actually sees.

The weight sweep runs the model ONCE per case and re-blends the cached scores:
LEXICAL_WEIGHT only affects the blend, never the model, so re-running inference
per weight would just burn time for identical numbers.
"""
from __future__ import annotations

import os
import sys
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")

from app import classifier                      # noqa: E402
from app.config import DEFAULT_LABELS           # noqa: E402

LABELS = [{"name": n, "description": d} for n, d in DEFAULT_LABELS]

CASES: list[tuple[str, str]] = [
    ("There is no water in our village for three days", "Water Supply"),
    ("The water coming from the tap is muddy and smells bad", "Water Supply"),
    ("A main pipe has burst and water is flooding the street", "Water Supply"),
    ("Our area has been without electricity for three days", "Electricity"),
    ("The transformer near our house is damaged and sparking", "Electricity"),
    ("Voltage keeps fluctuating and our appliances are getting damaged", "Electricity"),
    ("There is a huge pothole on the main road", "Road & Transport"),
    ("The traffic signal at the junction is not working", "Road & Transport"),
    ("The pavement is broken and people cannot walk safely", "Road & Transport"),
    ("Garbage has not been collected in our locality for a week", "Waste & Sanitation"),
    ("The drain is blocked and sewage is overflowing on the street", "Waste & Sanitation"),
    ("The public bin is overflowing and smells terrible", "Waste & Sanitation"),
    ("Mosquitoes are breeding in the stagnant water near the school", "Public Health"),
    ("Stray dogs are attacking children in our neighbourhood", "Public Health"),
    ("Construction noise starts at 5am every day and nobody can sleep", "Noise & Pollution"),
    ("A factory is releasing thick black smoke into the air", "Noise & Pollution"),
    ("Someone is putting up an illegal building without permission", "Building & Property"),
    ("The old building next door is cracked and looks unsafe", "Building & Property"),
]

CANDIDATES = [l["name"] for l in LABELS]


def model_scores(template: str) -> tuple[list[list[tuple[str, float]]], int]:
    """Raw model scores for every case under one hypothesis template."""
    out, latencies = [], []
    for text, _ in CASES:
        started = time.perf_counter()
        r = classifier._pipe(
            text, candidate_labels=CANDIDATES,
            hypothesis_template=template, multi_label=False,
        )
        latencies.append((time.perf_counter() - started) * 1000)
        out.append(list(zip(r["labels"], [float(s) for s in r["scores"]])))
    return out, int(sum(latencies) / len(latencies))


def score(cached, weight, avg_ms, title) -> tuple[int, list[str]]:
    hits, misses = 0, []
    for (text, expected), raw in zip(CASES, cached):
        blended = classifier._blend(list(raw), text, LABELS, weight)
        blended.sort(key=lambda p: p[1], reverse=True)
        got, conf = blended[0]
        if got == expected:
            hits += 1
        else:
            misses.append(f"      {expected:<20} -> {got:<20} ({conf:.2f})  {text[:46]}")
    print(f"  {title:<44} {hits:>2}/{len(CASES)}  ({100 * hits / len(CASES):3.0f}%)  ~{avg_ms}ms")
    return hits, misses


def main() -> int:
    if len(sys.argv) > 1:
        classifier.MODEL_NAME = sys.argv[1]
        classifier._state["model"] = sys.argv[1]

    started = time.time()
    state = classifier.load_model(force=True)
    if state["status"] != "ready":
        print("MODEL FAILED TO LOAD:\n", state["error"][:500])
        return 1
    print(f"\nmodel: {classifier._state['model']}   (loaded in {time.time() - started:.0f}s)\n")

    default_template = "This complaint is about {}."
    cached, avg_ms = model_scores(default_template)

    print("LEXICAL_WEIGHT sweep  (model run once, blend re-applied)")
    best = (-1, 0.0, [])
    for weight in (0.0, 0.15, 0.25, 0.3, 0.4, 0.5, 0.6):
        hits, misses = score(cached, weight, avg_ms, f"lexical_weight = {weight}")
        if hits > best[0]:
            best = (hits, weight, misses)

    print(f"\n  best: lexical_weight={best[1]}  ->  {best[0]}/{len(CASES)}")
    if best[2]:
        print("  remaining misses:")
        print("\n".join(best[2]))

    print("\nhypothesis template sweep")
    score(cached, best[1], avg_ms, f"{default_template!r}")
    for template in (
        "This is a complaint about {}.",
        "The problem is {}.",
        "This text is about {}.",
    ):
        c, ms = model_scores(template)
        score(c, best[1], ms, f"{template!r}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())