cacheverifier 0.3.1__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/CHANGELOG.md +13 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/PKG-INFO +23 -1
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/README.md +22 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/cacheverifier/__init__.py +1 -1
- cacheverifier-0.4.0/cacheverifier/_healthcheck/_base_models.py +62 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/cacheverifier/_healthcheck/_finetune.py +43 -9
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/cacheverifier/_healthcheck/cli.py +40 -1
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/pyproject.toml +1 -1
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/tests/test_healthcheck.py +80 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/.github/workflows/ci.yml +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/.github/workflows/publish.yml +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/.gitignore +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/LICENSE +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/cacheverifier/__main__.py +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/cacheverifier/_healthcheck/__init__.py +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/cacheverifier/client.py +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/cacheverifier/integrations/__init__.py +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/cacheverifier/integrations/gptcache.py +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/cacheverifier/py.typed +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/examples/gptcache_example.py +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/examples/quickstart.py +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/tests/test_client.py +0 -0
- {cacheverifier-0.3.1 → cacheverifier-0.4.0}/tests/test_gptcache_integration.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
`cacheverifier healthcheck --base-model` picks the model the local Health
|
|
6
|
+
Check starts from: `ms_marco` (default, unchanged), `nli`, `multilingual` or
|
|
7
|
+
`multilingual_small` — the same choices as the hosted service's `base_model`
|
|
8
|
+
— or any Hugging Face model id / local model directory. Chinese and other
|
|
9
|
+
non-English traffic should use a multilingual base; the CLI now warns when
|
|
10
|
+
most of the input is CJK text and the base is English-only. The NLI base's
|
|
11
|
+
3-class head is scored `P(entailment) - P(contradiction)` for the stock
|
|
12
|
+
baseline and re-initialized to a single logit for fine-tuning, matching the
|
|
13
|
+
hosted service. `--emit-summary` records the base model (a local directory
|
|
14
|
+
is recorded only as "local directory").
|
|
15
|
+
|
|
3
16
|
## 0.3.1
|
|
4
17
|
|
|
5
18
|
`cacheverifier healthcheck` now actually fine-tunes. It passed no
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cacheverifier
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Python client for the hosted CacheVerifier semantic-cache verification API
|
|
5
5
|
Project-URL: Homepage, https://www.cacheverifier.com
|
|
6
6
|
Project-URL: Documentation, https://www.cacheverifier.com/docs
|
|
@@ -177,6 +177,28 @@ Nothing is sent anywhere — the base model downloads once from Hugging Face, th
|
|
|
177
177
|
fully offline. `--emit-summary` writes an aggregate-only JSON file (AUCs, counts, rates —
|
|
178
178
|
no query or answer text) that's safe to share for a human read.
|
|
179
179
|
|
|
180
|
+
**Choosing the base model.** `--base-model` takes the same values as the hosted
|
|
181
|
+
service's `base_model` fine-tune parameter, so local and hosted numbers stay comparable:
|
|
182
|
+
|
|
183
|
+
| `--base-model` | model | use for |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `ms_marco` (default) | `cross-encoder/ms-marco-MiniLM-L6-v2` | English traffic |
|
|
186
|
+
| `nli` | `cross-encoder/nli-MiniLM2-L6-H768` | English; entailment pretraining, stronger on negation / entity swaps |
|
|
187
|
+
| `multilingual` | `BAAI/bge-reranker-base` | Chinese and other non-English traffic (~6 GB RAM to fine-tune) |
|
|
188
|
+
| `multilingual_small` | `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | same, smaller (~3 GB RAM) |
|
|
189
|
+
|
|
190
|
+
The English bases read most Chinese characters as unknown tokens, so on Chinese
|
|
191
|
+
traffic their result doesn't mean anything — the CLI warns when it sees mostly CJK text
|
|
192
|
+
on an English base. You can also pass any Hugging Face model id or a local model
|
|
193
|
+
directory. If Hugging Face is unreachable from your network, set `HF_ENDPOINT` to a
|
|
194
|
+
mirror, or download the model elsewhere and pass its directory:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
cacheverifier healthcheck traffic.jsonl --base-model multilingual
|
|
198
|
+
HF_ENDPOINT=https://hf-mirror.com cacheverifier healthcheck traffic.jsonl --base-model multilingual_small
|
|
199
|
+
cacheverifier healthcheck traffic.jsonl --base-model ./models/bge-reranker-base
|
|
200
|
+
```
|
|
201
|
+
|
|
180
202
|
```
|
|
181
203
|
results
|
|
182
204
|
------------------------------------------------------------------
|
|
@@ -145,6 +145,28 @@ Nothing is sent anywhere — the base model downloads once from Hugging Face, th
|
|
|
145
145
|
fully offline. `--emit-summary` writes an aggregate-only JSON file (AUCs, counts, rates —
|
|
146
146
|
no query or answer text) that's safe to share for a human read.
|
|
147
147
|
|
|
148
|
+
**Choosing the base model.** `--base-model` takes the same values as the hosted
|
|
149
|
+
service's `base_model` fine-tune parameter, so local and hosted numbers stay comparable:
|
|
150
|
+
|
|
151
|
+
| `--base-model` | model | use for |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `ms_marco` (default) | `cross-encoder/ms-marco-MiniLM-L6-v2` | English traffic |
|
|
154
|
+
| `nli` | `cross-encoder/nli-MiniLM2-L6-H768` | English; entailment pretraining, stronger on negation / entity swaps |
|
|
155
|
+
| `multilingual` | `BAAI/bge-reranker-base` | Chinese and other non-English traffic (~6 GB RAM to fine-tune) |
|
|
156
|
+
| `multilingual_small` | `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | same, smaller (~3 GB RAM) |
|
|
157
|
+
|
|
158
|
+
The English bases read most Chinese characters as unknown tokens, so on Chinese
|
|
159
|
+
traffic their result doesn't mean anything — the CLI warns when it sees mostly CJK text
|
|
160
|
+
on an English base. You can also pass any Hugging Face model id or a local model
|
|
161
|
+
directory. If Hugging Face is unreachable from your network, set `HF_ENDPOINT` to a
|
|
162
|
+
mirror, or download the model elsewhere and pass its directory:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cacheverifier healthcheck traffic.jsonl --base-model multilingual
|
|
166
|
+
HF_ENDPOINT=https://hf-mirror.com cacheverifier healthcheck traffic.jsonl --base-model multilingual_small
|
|
167
|
+
cacheverifier healthcheck traffic.jsonl --base-model ./models/bge-reranker-base
|
|
168
|
+
```
|
|
169
|
+
|
|
148
170
|
```
|
|
149
171
|
results
|
|
150
172
|
------------------------------------------------------------------
|
|
@@ -18,5 +18,5 @@ Research behind it: https://github.com/imxinchengyou/CacheVerifier
|
|
|
18
18
|
|
|
19
19
|
from cacheverifier.client import CacheVerifier, CacheVerifierError, VerifyResult
|
|
20
20
|
|
|
21
|
-
__version__ = "0.
|
|
21
|
+
__version__ = "0.4.0"
|
|
22
22
|
__all__ = ["CacheVerifier", "CacheVerifierError", "VerifyResult", "__version__"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Base-model choices for `cacheverifier healthcheck --base-model`.
|
|
2
|
+
|
|
3
|
+
Kept free of torch / sentence-transformers so `cacheverifier healthcheck
|
|
4
|
+
--help` can list the choices without the `healthcheck` extra installed.
|
|
5
|
+
The aliases and model ids match the hosted service's `base_model` fine-tune
|
|
6
|
+
parameter, so a local run is comparable to `POST /v1/finetune/dry-run`
|
|
7
|
+
with the same value.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
DEFAULT_BASE_MODEL = "cross-encoder/ms-marco-MiniLM-L6-v2"
|
|
16
|
+
"""The stock off-the-shelf verifier every tenant starts on -- the same base
|
|
17
|
+
model the hosted service fine-tunes from. English only."""
|
|
18
|
+
|
|
19
|
+
BASE_MODEL_CHOICES: dict[str, str] = {
|
|
20
|
+
"ms_marco": DEFAULT_BASE_MODEL,
|
|
21
|
+
# SNLI+MultiNLI entailment pretraining, same size as ms_marco. Ships a
|
|
22
|
+
# 3-class head; see _finetune for how it is scored and fine-tuned.
|
|
23
|
+
"nli": "cross-encoder/nli-MiniLM2-L6-H768",
|
|
24
|
+
# Non-English (e.g. Chinese) traffic: the English bases turn most
|
|
25
|
+
# Chinese characters into [UNK] at tokenization.
|
|
26
|
+
"multilingual": "BAAI/bge-reranker-base",
|
|
27
|
+
"multilingual_small": "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ENGLISH_ONLY_BASE_MODELS = frozenset({BASE_MODEL_CHOICES["ms_marco"], BASE_MODEL_CHOICES["nli"]})
|
|
31
|
+
|
|
32
|
+
_CJK_RE = re.compile(r"[-ヿ㐀-䶿一-鿿가-]")
|
|
33
|
+
|
|
34
|
+
CJK_WARNING_RATIO = 0.3
|
|
35
|
+
"""Share of non-whitespace characters that are CJK above which an
|
|
36
|
+
English-only base model gets a warning."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_base_model(value: str) -> str:
|
|
40
|
+
"""An alias from `BASE_MODEL_CHOICES`, a Hugging Face model id
|
|
41
|
+
("org/name"), or a local model directory (e.g. one downloaded from a
|
|
42
|
+
mirror or ModelScope). Raises ValueError for anything else, so a typo'd
|
|
43
|
+
alias fails immediately instead of after a failed download."""
|
|
44
|
+
if value in BASE_MODEL_CHOICES:
|
|
45
|
+
return BASE_MODEL_CHOICES[value]
|
|
46
|
+
# `Path("")` is the current directory -- never treat an empty value as
|
|
47
|
+
# a local model.
|
|
48
|
+
if value.strip() and Path(value).expanduser().is_dir():
|
|
49
|
+
return str(Path(value).expanduser())
|
|
50
|
+
if "/" in value and not value.startswith("/"):
|
|
51
|
+
return value
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"unknown base model {value!r}: use one of {', '.join(BASE_MODEL_CHOICES)}, "
|
|
54
|
+
"a Hugging Face model id (org/name), or a local model directory"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cjk_ratio(texts: list[str]) -> float:
|
|
59
|
+
chars = "".join("".join(t.split()) for t in texts)
|
|
60
|
+
if not chars:
|
|
61
|
+
return 0.0
|
|
62
|
+
return len(_CJK_RE.findall(chars)) / len(chars)
|
|
@@ -24,15 +24,17 @@ import torch
|
|
|
24
24
|
from sentence_transformers import CrossEncoder, InputExample
|
|
25
25
|
from torch.utils.data import DataLoader
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
from cacheverifier._healthcheck._base_models import DEFAULT_BASE_MODEL
|
|
28
|
+
|
|
29
|
+
# The four third-party imports above are from the `healthcheck` extra. This module is
|
|
28
30
|
# only ever imported from inside `cacheverifier._healthcheck.cli.run()`,
|
|
29
31
|
# which catches the ImportError and points the user at
|
|
30
32
|
# `pip install "cacheverifier[healthcheck]"` -- so a plain
|
|
31
33
|
# `import cacheverifier` / `cacheverifier --help` never pays this cost.
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
"""
|
|
35
|
-
|
|
35
|
+
NLI_LABEL_ORDER = ("contradiction", "entailment", "neutral")
|
|
36
|
+
"""`cross-encoder/nli-*` models' class order, used when the loaded model
|
|
37
|
+
doesn't expose an `id2label` naming them."""
|
|
36
38
|
|
|
37
39
|
MAX_SEQUENCE_LENGTH = 128
|
|
38
40
|
"""Token cap applied identically at train and score time. Matches the
|
|
@@ -185,6 +187,28 @@ def _diagnose_ceiling(scores: np.ndarray, labels: np.ndarray) -> str | None:
|
|
|
185
187
|
return "still_improvable"
|
|
186
188
|
|
|
187
189
|
|
|
190
|
+
def _nli_column_indices(model: Any) -> tuple[int, int]:
|
|
191
|
+
"""(entailment_idx, contradiction_idx) for a 3-class NLI cross-encoder."""
|
|
192
|
+
id2label = getattr(getattr(model, "config", None), "id2label", None)
|
|
193
|
+
if isinstance(id2label, dict) and len(id2label) == 3:
|
|
194
|
+
lut = {str(v).lower(): int(k) for k, v in id2label.items()}
|
|
195
|
+
if "entailment" in lut and "contradiction" in lut:
|
|
196
|
+
return lut["entailment"], lut["contradiction"]
|
|
197
|
+
return NLI_LABEL_ORDER.index("entailment"), NLI_LABEL_ORDER.index("contradiction")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def score_pairs(model: Any, pairs: list[tuple[str, str]], batch_size: int = 32) -> np.ndarray:
|
|
201
|
+
"""Higher means "more likely an acceptable answer". A 1-logit model
|
|
202
|
+
(ms-marco, the multilingual rerankers, anything fine-tuned here) returns
|
|
203
|
+
its raw logit; a 3-class NLI model returns P(entailment) - P(contradiction)
|
|
204
|
+
-- the same scoring the hosted service uses for the stock NLI baseline."""
|
|
205
|
+
if int(model.config.num_labels) == 3:
|
|
206
|
+
ent, contra = _nli_column_indices(model)
|
|
207
|
+
probs = np.asarray(model.predict(pairs, batch_size=batch_size, show_progress_bar=False, apply_softmax=True))
|
|
208
|
+
return probs[:, ent] - probs[:, contra]
|
|
209
|
+
return np.asarray(model.predict(pairs, batch_size=batch_size, show_progress_bar=False))
|
|
210
|
+
|
|
211
|
+
|
|
188
212
|
def run_healthcheck(
|
|
189
213
|
examples: list[GrayZoneExample],
|
|
190
214
|
output_dir: str,
|
|
@@ -221,11 +245,21 @@ def run_healthcheck(
|
|
|
221
245
|
|
|
222
246
|
baseline = CrossEncoder(base_model, device=device, max_length=MAX_SEQUENCE_LENGTH)
|
|
223
247
|
test_pairs_scored = [smart_truncate_pair(q, a, baseline.tokenizer) for q, a in test_pairs]
|
|
224
|
-
baseline_scores =
|
|
248
|
+
baseline_scores = score_pairs(baseline, test_pairs_scored)
|
|
225
249
|
auc_baseline = roc_auc(baseline_scores, test_labels)
|
|
226
250
|
del baseline
|
|
227
251
|
|
|
228
|
-
|
|
252
|
+
# num_labels=1 forces a single-logit head, matching the hosted service: a
|
|
253
|
+
# no-op for 1-logit bases, a deliberate head reinitialization (encoder
|
|
254
|
+
# body kept) for the 3-class NLI base. ignore_mismatched_sizes lets that
|
|
255
|
+
# 3->1 reinit happen instead of raising.
|
|
256
|
+
tuned = CrossEncoder(
|
|
257
|
+
base_model,
|
|
258
|
+
num_labels=1,
|
|
259
|
+
device=device,
|
|
260
|
+
max_length=MAX_SEQUENCE_LENGTH,
|
|
261
|
+
automodel_args={"ignore_mismatched_sizes": True},
|
|
262
|
+
)
|
|
229
263
|
train_examples = [
|
|
230
264
|
InputExample(
|
|
231
265
|
texts=list(smart_truncate_pair(e.query, e.candidate_answer, tuned.tokenizer)),
|
|
@@ -246,10 +280,10 @@ def run_healthcheck(
|
|
|
246
280
|
tuned.save(output_dir)
|
|
247
281
|
|
|
248
282
|
calib_scored = [smart_truncate_pair(q, a, tuned.tokenizer) for q, a in calib_pairs]
|
|
249
|
-
calib_scores =
|
|
283
|
+
calib_scores = score_pairs(tuned, calib_scored)
|
|
250
284
|
threshold = select_threshold(calib_scores, calib_labels)
|
|
251
285
|
|
|
252
|
-
tuned_scores =
|
|
286
|
+
tuned_scores = score_pairs(tuned, test_pairs_scored)
|
|
253
287
|
auc_tuned = roc_auc(tuned_scores, test_labels)
|
|
254
288
|
ceiling_status = _diagnose_ceiling(tuned_scores, test_labels)
|
|
255
289
|
|
|
@@ -259,7 +293,7 @@ def run_healthcheck(
|
|
|
259
293
|
if threshold is not None:
|
|
260
294
|
threshold_hit_rate, threshold_error_rate = _hit_and_error_rate(tuned_scores, test_labels, threshold)
|
|
261
295
|
train_pairs_scored = [smart_truncate_pair(e.query, e.candidate_answer, tuned.tokenizer) for e in train_rows]
|
|
262
|
-
train_scores =
|
|
296
|
+
train_scores = score_pairs(tuned, train_pairs_scored)
|
|
263
297
|
train_labels = np.array([1 if e.was_correct else 0 for e in train_rows])
|
|
264
298
|
train_pred = (train_scores >= threshold).astype(int)
|
|
265
299
|
train_label_disagreement_rate = float(np.mean(train_pred != train_labels))
|
|
@@ -13,6 +13,14 @@ import sys
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
+
from cacheverifier._healthcheck._base_models import (
|
|
17
|
+
BASE_MODEL_CHOICES,
|
|
18
|
+
CJK_WARNING_RATIO,
|
|
19
|
+
ENGLISH_ONLY_BASE_MODELS,
|
|
20
|
+
cjk_ratio,
|
|
21
|
+
resolve_base_model,
|
|
22
|
+
)
|
|
23
|
+
|
|
16
24
|
_MISSING_EXTRA_HINT = (
|
|
17
25
|
"the healthcheck extra is not installed -- run:\n"
|
|
18
26
|
' pip install "cacheverifier[healthcheck]"\n'
|
|
@@ -51,6 +59,17 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
51
59
|
default=3,
|
|
52
60
|
help="fine-tuning epochs (default: 3, matching the hosted service)",
|
|
53
61
|
)
|
|
62
|
+
p.add_argument(
|
|
63
|
+
"--base-model",
|
|
64
|
+
default="ms_marco",
|
|
65
|
+
metavar="MODEL",
|
|
66
|
+
help=(
|
|
67
|
+
f"model to start from: {', '.join(BASE_MODEL_CHOICES)} (same choices as the hosted "
|
|
68
|
+
"service's base_model), a Hugging Face model id, or a local model directory. "
|
|
69
|
+
"Default ms_marco is English-only -- use multilingual or multilingual_small for "
|
|
70
|
+
"Chinese or other non-English traffic (they need several GB of RAM to fine-tune)"
|
|
71
|
+
),
|
|
72
|
+
)
|
|
54
73
|
p.add_argument(
|
|
55
74
|
"--keep-model",
|
|
56
75
|
action="store_true",
|
|
@@ -125,6 +144,12 @@ def run(args: argparse.Namespace) -> int:
|
|
|
125
144
|
print(f"error: {e}", file=sys.stderr)
|
|
126
145
|
return 2
|
|
127
146
|
|
|
147
|
+
try:
|
|
148
|
+
base_model = resolve_base_model(args.base_model)
|
|
149
|
+
except ValueError as e:
|
|
150
|
+
print(f"error: {e}", file=sys.stderr)
|
|
151
|
+
return 2
|
|
152
|
+
|
|
128
153
|
n = len(examples)
|
|
129
154
|
print(f"\nCacheVerifier local Health Check -- {input_path}")
|
|
130
155
|
print("=" * 66)
|
|
@@ -133,6 +158,17 @@ def run(args: argparse.Namespace) -> int:
|
|
|
133
158
|
if n_stale:
|
|
134
159
|
print(f"stale, excluded: {n_stale}")
|
|
135
160
|
|
|
161
|
+
print(f"base model: {base_model}")
|
|
162
|
+
if base_model in ENGLISH_ONLY_BASE_MODELS:
|
|
163
|
+
ratio = cjk_ratio([t for e in examples for t in (e.query, e.candidate_answer)])
|
|
164
|
+
if ratio > CJK_WARNING_RATIO:
|
|
165
|
+
print(
|
|
166
|
+
f"\nWarning: {ratio:.0%} of your text is Chinese/Japanese/Korean, but {base_model} is an "
|
|
167
|
+
"English-only model\n (it reads most CJK characters as unknown tokens), so this "
|
|
168
|
+
"result won't reflect your\n traffic. Re-run with --base-model multilingual "
|
|
169
|
+
"(or multilingual_small)."
|
|
170
|
+
)
|
|
171
|
+
|
|
136
172
|
if n < _finetune.MIN_TRAIN_EXAMPLES:
|
|
137
173
|
print(f"\nNeed at least {_finetune.MIN_TRAIN_EXAMPLES} non-stale rows to run; have {n}.", file=sys.stderr)
|
|
138
174
|
return 2
|
|
@@ -149,7 +185,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
149
185
|
print(f"\nfine-tuning locally (nothing sent) -> {out_dir}")
|
|
150
186
|
print("this takes a few minutes on CPU; the base model downloads once on first run...\n")
|
|
151
187
|
|
|
152
|
-
result = _finetune.run_healthcheck(examples, out_dir, base_model=
|
|
188
|
+
result = _finetune.run_healthcheck(examples, out_dir, base_model=base_model, epochs=args.epochs)
|
|
153
189
|
|
|
154
190
|
print("results")
|
|
155
191
|
print("-" * 66)
|
|
@@ -175,6 +211,9 @@ def run(args: argparse.Namespace) -> int:
|
|
|
175
211
|
|
|
176
212
|
if args.emit_summary is not None:
|
|
177
213
|
summary = {
|
|
214
|
+
# A local directory's path can carry a username -- this file is
|
|
215
|
+
# meant to be shareable, so record only that it was local.
|
|
216
|
+
"base_model": "local directory" if Path(base_model).is_dir() else base_model,
|
|
178
217
|
"n_rows_read": len(rows),
|
|
179
218
|
"n_usable": n,
|
|
180
219
|
"n_stale_excluded": n_stale,
|
|
@@ -123,3 +123,83 @@ def test_run_healthcheck_warms_up_over_a_tenth_of_its_steps(tmp_path, monkeypatc
|
|
|
123
123
|
assert seen["epochs"] == _finetune.TRAIN_EPOCHS == 3
|
|
124
124
|
expected = max(1, int(_finetune.TRAIN_WARMUP_FRACTION * seen["steps_per_epoch"] * seen["epochs"]))
|
|
125
125
|
assert seen["warmup_steps"] == expected
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestBaseModel:
|
|
129
|
+
def test_aliases_match_the_hosted_service(self):
|
|
130
|
+
from cacheverifier._healthcheck._base_models import BASE_MODEL_CHOICES, resolve_base_model
|
|
131
|
+
|
|
132
|
+
assert set(BASE_MODEL_CHOICES) == {"ms_marco", "nli", "multilingual", "multilingual_small"}
|
|
133
|
+
assert resolve_base_model("ms_marco") == "cross-encoder/ms-marco-MiniLM-L6-v2"
|
|
134
|
+
assert resolve_base_model("multilingual") == "BAAI/bge-reranker-base"
|
|
135
|
+
|
|
136
|
+
def test_hf_id_and_local_dir_pass_through(self, tmp_path):
|
|
137
|
+
from cacheverifier._healthcheck._base_models import resolve_base_model
|
|
138
|
+
|
|
139
|
+
assert resolve_base_model("someorg/some-reranker") == "someorg/some-reranker"
|
|
140
|
+
assert resolve_base_model(str(tmp_path)) == str(tmp_path)
|
|
141
|
+
|
|
142
|
+
@pytest.mark.parametrize("bad", ["multilingal", "/no/such/model/dir", ""])
|
|
143
|
+
def test_typos_and_missing_paths_are_rejected(self, bad):
|
|
144
|
+
from cacheverifier._healthcheck._base_models import resolve_base_model
|
|
145
|
+
|
|
146
|
+
with pytest.raises(ValueError, match="unknown base model"):
|
|
147
|
+
resolve_base_model(bad)
|
|
148
|
+
|
|
149
|
+
def test_cjk_ratio(self):
|
|
150
|
+
from cacheverifier._healthcheck._base_models import cjk_ratio
|
|
151
|
+
|
|
152
|
+
assert cjk_ratio(["how do I cancel"]) == 0.0
|
|
153
|
+
assert cjk_ratio(["怎么取消订阅"]) == 1.0
|
|
154
|
+
assert cjk_ratio(["怎么取消 subscription"]) == 4 / 16
|
|
155
|
+
assert cjk_ratio([]) == 0.0
|
|
156
|
+
|
|
157
|
+
def test_help_lists_base_model_choices(self, capsys):
|
|
158
|
+
with pytest.raises(SystemExit):
|
|
159
|
+
main(["healthcheck", "--help"])
|
|
160
|
+
out = capsys.readouterr().out
|
|
161
|
+
assert "--base-model" in out and "multilingual_small" in out
|
|
162
|
+
|
|
163
|
+
@pytest.mark.skipif(not HAS_TORCH, reason="run() imports the healthcheck extra first")
|
|
164
|
+
def test_unknown_base_model_is_an_input_error(self, capsys, tmp_path):
|
|
165
|
+
f = tmp_path / "t.jsonl"
|
|
166
|
+
f.write_text(json.dumps({"query": "q", "candidate_answer": "a", "was_correct": True}) + "\n")
|
|
167
|
+
assert main(["healthcheck", str(f), "--base-model", "multilingal"]) == 2
|
|
168
|
+
assert "unknown base model" in capsys.readouterr().err
|
|
169
|
+
|
|
170
|
+
@pytest.mark.skipif(not HAS_TORCH, reason="run() imports the healthcheck extra first")
|
|
171
|
+
def test_chinese_data_on_english_model_warns(self, capsys, tmp_path):
|
|
172
|
+
# Below MIN_TRAIN_EXAMPLES, so run() stops before any download/training.
|
|
173
|
+
f = tmp_path / "t.jsonl"
|
|
174
|
+
f.write_text(
|
|
175
|
+
"\n".join(
|
|
176
|
+
json.dumps({"query": "怎么取消自动续费", "candidate_answer": "进入设置取消", "was_correct": True}, ensure_ascii=False)
|
|
177
|
+
for _ in range(3)
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
assert main(["healthcheck", str(f)]) == 2
|
|
181
|
+
out = capsys.readouterr().out
|
|
182
|
+
assert "English-only" in out and "--base-model multilingual" in out
|
|
183
|
+
|
|
184
|
+
assert main(["healthcheck", str(f), "--base-model", "multilingual_small"]) == 2
|
|
185
|
+
assert "English-only" not in capsys.readouterr().out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.skipif(not HAS_TORCH, reason="needs the healthcheck extra (torch, sentence-transformers)")
|
|
189
|
+
def test_run_healthcheck_nli_base_reinitializes_to_single_logit(tmp_path):
|
|
190
|
+
"""The NLI base ships a 3-class head: the stock baseline is scored
|
|
191
|
+
P(entailment) - P(contradiction), and fine-tuning reinitializes it to the
|
|
192
|
+
same 1-logit head as every other base (matching the hosted service)."""
|
|
193
|
+
from sentence_transformers import CrossEncoder
|
|
194
|
+
|
|
195
|
+
from cacheverifier._healthcheck._base_models import BASE_MODEL_CHOICES
|
|
196
|
+
from cacheverifier._healthcheck._finetune import GrayZoneExample, run_healthcheck
|
|
197
|
+
|
|
198
|
+
examples = [
|
|
199
|
+
GrayZoneExample("how do I cancel my plan", "Settings > Billing > Cancel." if i % 2 == 0 else "Settings > Pause.", i % 2 == 0)
|
|
200
|
+
for i in range(24)
|
|
201
|
+
]
|
|
202
|
+
result = run_healthcheck(examples, str(tmp_path / "model"), base_model=BASE_MODEL_CHOICES["nli"], epochs=1)
|
|
203
|
+
assert 0.0 <= result.auc_baseline <= 1.0
|
|
204
|
+
assert 0.0 <= result.auc_tuned <= 1.0
|
|
205
|
+
assert CrossEncoder(str(tmp_path / "model")).config.num_labels == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|