oc_chat
Loading...
Searching...
No Matches
knn_classifier Namespace Reference

Classes

class  HashEmbedder
class  SentenceTransformerEmbedder
class  LMStudioEmbedder
class  Index
class  FitCell
class  Fit

Functions

Any _numpy ()
list[dict[str, str]] read_jsonl (Path path)
list[dict[str, str]] read_cases (Path path)
bytes _npy_header (int rows, int dim)
bytes _flat_float32 (Any matrix, int rows, int dim)
Path write_npy (Path path, Any matrix)
tuple[int, int, array] read_npy (Path path)
Any _normalise_rows (Any matrix)
Path build_index (Path corpus_path, Any embedder, Path index_dir, int|None subset=None, int seed=SUBSET_SEED)
Index load_index (Path index_dir, Any embedder=None)
list[float] index_row (Index index, int position)
Any _similarities (Index index, Sequence[float] query)
list[tuple[float, str]] top_neighbours (Index index, Sequence[float] query, int k)
dict[str, float] class_sums (Iterable[tuple[float, str]] neighbours)
dict[str, float] probabilities (dict[str, float] sums, float temperature, bool mask_absent=True)
tuple[str, bool] verdict_of (dict[str, float] probs)
list[float] embed_query (Index index, str command)
dict[str, float] classify (str command, Index index, int k=DEFAULT_K, float temperature=DEFAULT_TEMPERATURE, bool mask_absent=True)
list[list[tuple[float, str]]] _neighbour_batches (Index index, Any vectors, int k)
list[float] _normalise_one (Sequence[float] vector)
Fit fit (Path validation_path, Index index, Sequence[int] k_grid=DEFAULT_K_GRID, Sequence[float] temperature_grid=DEFAULT_TEMPERATURE_GRID, Sequence[bool] maskings=(True, False))
dict[str, Any] evaluate (Path cases_path, Index index, int k=DEFAULT_K, float temperature=DEFAULT_TEMPERATURE, bool mask_absent=True, str model_name="knn", Fit|None fitted=None)
Path write_result (dict[str, Any] payload, Path out_path)
str render_fit_table (Fit fitted, bool|None mask_absent=None)
str render_masking_table (Fit fitted)
Any _make_embedder (str kind)
argparse.ArgumentParser _build_parser ()
str _leg_tag (argparse.Namespace args)
int main (list[str]|None argv=None)

Variables

tuple LABELS = ("allow", "confirm", "deny")
str TIE_BREAK = "deny"
tuple DEFAULT_K_GRID = (1, 5, 15, 51)
tuple DEFAULT_TEMPERATURE_GRID
int DEFAULT_K = 15
float DEFAULT_TEMPERATURE = 0.1
int SUBSET_ROWS = 1000
int SUBSET_SEED = 289
str JINA_MODEL_ID = "jinaai/jina-embeddings-v2-base-code"
int MAX_SEQ_LENGTH = 512
int THREADS = 8
str LMSTUDIO_URL = "http://127.0.0.1:1234/v1/embeddings"
str LMSTUDIO_MODEL = "text-embedding-qwen3-embedding-0.6b"
float LMSTUDIO_TIMEOUT = 30.0
int BUILD_BATCH = 128
int FIT_BATCH = 256
str NPY_MAGIC = b"\x93NUMPY"
int NPY_ALIGNMENT = 64
int NPY_PREAMBLE = 10
tuple HASH_NGRAM_SIZES = (1, 2, 3, 4)
Any NUMPY = _numpy()

Detailed Description

Embed the gate-labelled corpus once, then vote with the nearest neighbours.

Purpose
-------
This is the cheapest shape on this bench that learns the argv policy from
labels rather than from prose, and the only one that needs no training loop at
all: embed every corpus command once, embed the query, and read the verdict off
the nearest neighbours' labels. A decision costs one encoder forward pass and
one matrix-vector product, nothing per call, and the whole model is rebuilt by
re-embedding when the gate changes.

It exists to bound plan 290 from below. An encoder that is fine-tuned on this
corpus has to beat what the same encoder already achieves with no training at
all, or the training was not worth its cost; and both have to beat plan 288's
standard-library token floor, which decides in 0.07 ms.

The vote is a softmax over PER-CLASS SIMILARITY SUMS across the top-k cosine
neighbours, divided by a temperature. Two readings of that formula differ in
what happens to a class no neighbour voted for. This module's default masks it
out -- an empty sum is no forecast, not a zero one -- because the alternative
gives a class with no votes ``exp(0)`` of the mass, which at k=1 makes the
nearest-neighbour forecast 1/(1+2exp(-1/T)) rather than the exact 1.0 that a
1-NN forecast actually is. Both readings are implemented and both are swept by
:func:`fit`, so the choice is measured on validation Brier rather than asserted
(see ``mask_absent``).

Ties break toward ``deny``. A vote that cannot separate two classes grants the
least permission, not the most, which matters because the query that produces
an exactly tied vote is the degenerate one -- a command whose embedding is the
zero vector, so every similarity is 0.0 and every present class sums to 0.0.

Run the index build and the evaluation under the SCRATCH HARNESS venv (torch
and sentence-transformers live there, and this bench adds no dependency to any
``pyproject.toml``), from the oc_chat subproject root::

    python bench/argv_policy/baselines/knn_classifier.py --stage all

Run the tests under the PROJECT venv, where :class:`HashEmbedder` stands in for
the real encoder so the witnesses need no model, no GPU and no network.

Responsibilities
----------------
- :class:`HashEmbedder` / :class:`SentenceTransformerEmbedder` /
  :class:`LMStudioEmbedder` -- the three embedding routes.
- :func:`write_npy` / :func:`read_npy` -- a standard-library ``.npy`` codec, so
  an index written here is readable with or without numpy.
- :func:`build_index` / :func:`load_index` -- the index on disk.
- :func:`top_neighbours` / :func:`class_sums` / :func:`probabilities` /
  :func:`verdict_of` / :func:`classify` -- one decision.
- :func:`fit` -- k and the temperature from validation Brier alone.
- :func:`evaluate` / :func:`write_result` -- a case set to a plan-288 result
  file.
- :func:`main` -- the CLI: build, fit, evaluate.

Diagnostics
-----------
Domain: OC_CHAT.MAIN
Levels:
    L2 - index and evaluation lifecycle (build start and finish, files written)
    L3 - per-batch, per-fit-cell and per-case-set detail
    L4 - module load

Contracts
---------
- Deterministic given an embedder: the corpus, the pinned subset seed and the
  (k, temperature) pair fix every number this module reports.
- Read-only with respect to the corpus, the case sets and the scorer.
- Index arrays are megabytes and rebuildable, so :func:`build_index` writes a
  ``.gitignore`` of ``*`` into its own output directory. Nothing under it is
  ever committed, and no model weights are either.
- numpy is used when it is importable and a pure-Python path is used when it is
  not; the on-disk format and every number are identical either way. The
  fallback is for the witnesses, not for a 27000-row index.
- Console output is ASCII only; commands are printed through :func:`ascii`.
- Standard library only at import time. torch, sentence-transformers and numpy
  are imported lazily, inside the routes that need them.

Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- numpy (lazy; fast matrix maths and .npy loading when importable, with a pure-Python fallback)
- sentence_transformers (lazy; the SentenceTransformer encoder behind the required embedding route)
- torch (lazy; pins the encoder's CPU thread count)

Function Documentation

◆ _build_parser()

argparse.ArgumentParser knn_classifier._build_parser ( )
protected
Construct the CLI argument parser.

Definition at line 979 of file knn_classifier.py.

Here is the caller graph for this function:

◆ _flat_float32()

bytes knn_classifier._flat_float32 ( Any matrix,
int rows,
int dim )
protected
Flatten *matrix* to little-endian float32 bytes.

Two paths for one format: numpy when the caller handed over an array, and
:mod:`array` when it handed over lists. The bytes are identical.

Definition at line 354 of file knn_classifier.py.

Here is the caller graph for this function:

◆ _leg_tag()

str knn_classifier._leg_tag ( argparse.Namespace args)
protected
The short name identifying one embedder-and-corpus-size leg.

Definition at line 1013 of file knn_classifier.py.

Here is the caller graph for this function:

◆ _make_embedder()

Any knn_classifier._make_embedder ( str kind)
protected
Build the embedder the CLI was asked for.

Definition at line 967 of file knn_classifier.py.

Here is the caller graph for this function:

◆ _neighbour_batches()

list[list[tuple[float, str]]] knn_classifier._neighbour_batches ( Index index,
Any vectors,
int k )
protected
Top-*k* neighbours for a batch of query vectors.

With numpy this is one matmul per batch rather than one per query, which is
the difference between a fit that takes seconds and one that takes an hour.

Definition at line 723 of file knn_classifier.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _normalise_one()

list[float] knn_classifier._normalise_one ( Sequence[float] vector)
protected
One vector scaled to unit length, or left alone when it is all zero.

Definition at line 749 of file knn_classifier.py.

Here is the caller graph for this function:

◆ _normalise_rows()

Any knn_classifier._normalise_rows ( Any matrix)
protected
Scale every row to unit length, leaving an all-zero row alone.

Cosine similarity is a dot product only when both sides are unit-norm, so
this is done once at build time and never per query. A zero row cannot be
normalised and is kept as it is; it produces a similarity of 0.0 against
everything, which is the degenerate case the tie rule handles.

Definition at line 438 of file knn_classifier.py.

Here is the caller graph for this function:

◆ _npy_header()

bytes knn_classifier._npy_header ( int rows,
int dim )
protected
The padded version-1.0 header describing a C-order float32 matrix.

Definition at line 346 of file knn_classifier.py.

Here is the caller graph for this function:

◆ _numpy()

Any knn_classifier._numpy ( )
protected
Return numpy when it is importable, else ``None``.

The witnesses run in the project venv, which has neither numpy nor torch;
the index builds and the bench evaluations run in the scratch harness venv,
which has both. One module serves both, so the import is a question rather
than a requirement.

Definition at line 180 of file knn_classifier.py.

◆ _similarities()

Any knn_classifier._similarities ( Index index,
Sequence[float] query )
protected
Cosine similarity of *query* against every indexed row.

A dot product, because both sides are unit-norm: the index rows were
normalised at build time and the query is normalised by :func:`classify`.

Definition at line 563 of file knn_classifier.py.

Here is the caller graph for this function:

◆ build_index()

Path knn_classifier.build_index ( Path corpus_path,
Any embedder,
Path index_dir,
int | None subset = None,
int seed = SUBSET_SEED )
Embed a corpus split once and write the index to *index_dir*.

Three files: ``vectors.npy`` (row-normalised float32), ``index.json`` (the
commands, the labels and how the index was built) and a ``.gitignore`` of
``*``, because the array is megabytes of rebuildable floats and must not be
committable by accident.

Definition at line 459 of file knn_classifier.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ class_sums()

dict[str, float] knn_classifier.class_sums ( Iterable[tuple[float, str]] neighbours)
Sum the similarities of the neighbours belonging to each class.

A class absent from the result is a class no neighbour voted for, which is
not the same thing as a class whose votes summed to zero. Membership, not
the sign of the sum, is what :func:`probabilities` masks on.

Definition at line 602 of file knn_classifier.py.

Here is the caller graph for this function:

◆ classify()

dict[str, float] knn_classifier.classify ( str command,
Index index,
int k = DEFAULT_K,
float temperature = DEFAULT_TEMPERATURE,
bool mask_absent = True )
The forecast for one command: embed, search, vote.

This is the whole decision, and it is what the latency column measures:
one encoder forward pass at batch size one plus one matrix-vector product,
with no file read and no model load inside it.

Definition at line 669 of file knn_classifier.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ embed_query()

list[float] knn_classifier.embed_query ( Index index,
str command )
Embed one command with the index's own encoder, normalised.

Definition at line 656 of file knn_classifier.py.

Here is the caller graph for this function:

◆ evaluate()

dict[str, Any] knn_classifier.evaluate ( Path cases_path,
Index index,
int k = DEFAULT_K,
float temperature = DEFAULT_TEMPERATURE,
bool mask_absent = True,
str model_name = "knn",
Fit | None fitted = None )
Decide every case in *cases_path* and build a plan-288 result payload.

P1 asks for a result FILE; producing the payload and writing it are split
so the witnesses can validate the schema without touching disk and so one
evaluation can be scored twice. :func:`write_result` is the other half.

``condition`` is ``"corpus"`` for every file this module writes (P2): this
classifier is never given the policy text, so the policy/nopolicy axis the
prompted legs use does not exist for it. The case set therefore lives in
*model_name* and in the provenance block, not in the condition column.

Definition at line 824 of file knn_classifier.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ fit()

Fit knn_classifier.fit ( Path validation_path,
Index index,
Sequence[int] k_grid = DEFAULT_K_GRID,
Sequence[float] temperature_grid = DEFAULT_TEMPERATURE_GRID,
Sequence[bool] maskings = (True, False) )
Choose ``k`` and the temperature on the validation split, by Brier.

P3: never on a case set. The neighbours are searched ONCE at the largest
``k`` in the grid and every cell reads a prefix of that list, so the sweep
costs one pass over the validation split rather than one per cell.

Definition at line 759 of file knn_classifier.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ index_row()

list[float] knn_classifier.index_row ( Index index,
int position )
One row of the index as a plain list, whichever backend holds it.

Definition at line 554 of file knn_classifier.py.

◆ load_index()

Index knn_classifier.load_index ( Path index_dir,
Any embedder = None )
Load an index from disk, optionally with a live embedder for queries.

Definition at line 525 of file knn_classifier.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ main()

int knn_classifier.main ( list[str] | None argv = None)
CLI entry point: build the index, fit on validation, score both sets.

Definition at line 1023 of file knn_classifier.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ probabilities()

dict[str, float] knn_classifier.probabilities ( dict[str, float] sums,
float temperature,
bool mask_absent = True )
Softmax the per-class similarity sums over a temperature.

With ``mask_absent`` (the default, and the reading in the module docstring)
a class no neighbour voted for receives no mass, so a 1-NN forecast is
exactly one-hot. Without it, every class enters the softmax with its sum --
zero when absent -- which is the other reading of P1's formula and the arm
:func:`fit` sweeps beside the first.

Definition at line 616 of file knn_classifier.py.

Here is the caller graph for this function:

◆ read_cases()

list[dict[str, str]] knn_classifier.read_cases ( Path path)
Read one of the bench's hand-authored case sets.

Definition at line 207 of file knn_classifier.py.

Here is the caller graph for this function:

◆ read_jsonl()

list[dict[str, str]] knn_classifier.read_jsonl ( Path path)
Read a JSON-lines corpus split into a list of rows.

Definition at line 200 of file knn_classifier.py.

Here is the caller graph for this function:

◆ read_npy()

tuple[int, int, array] knn_classifier.read_npy ( Path path)
Read a float32 ``.npy`` file into ``(rows, dim, flat values)``.

Definition at line 396 of file knn_classifier.py.

Here is the caller graph for this function:

◆ render_fit_table()

str knn_classifier.render_fit_table ( Fit fitted,
bool | None mask_absent = None )
The validation-Brier table W2 requires in the report, as markdown.

Pass *mask_absent* to render one arm of the sweep; omit it for the whole
grid. :func:`render_masking_table` is the compact comparison of the two.

Definition at line 919 of file knn_classifier.py.

Here is the caller graph for this function:

◆ render_masking_table()

str knn_classifier.render_masking_table ( Fit fitted)
Best cell per ``k`` under each reading of P1's softmax, as markdown.

This is the table that settles the masking question empirically rather
than by argument: the same fit rule applied to both arms, side by side.

Definition at line 942 of file knn_classifier.py.

Here is the caller graph for this function:

◆ top_neighbours()

list[tuple[float, str]] knn_classifier.top_neighbours ( Index index,
Sequence[float] query,
int k )
The *k* most similar indexed rows as ``(similarity, label)`` pairs.

Definition at line 582 of file knn_classifier.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ verdict_of()

tuple[str, bool] knn_classifier.verdict_of ( dict[str, float] probs)
The verdict a forecast implies, and whether it came from a tie.

Ties break toward :data:`TIE_BREAK`. The caller gets the flag as well as
the verdict because plan 289 P3 requires the report to say how often the
rule fired: a tie rule nobody counts is a rule nobody can audit.

Definition at line 641 of file knn_classifier.py.

Here is the caller graph for this function:

◆ write_npy()

Path knn_classifier.write_npy ( Path path,
Any matrix )
Write *matrix* as a C-order float32 ``.npy`` file.

Hand-rolled because the project venv has no numpy and the witnesses must
still be able to round-trip an index; the harness venv reads the very same
bytes with :func:`numpy.load`.

Definition at line 375 of file knn_classifier.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ write_result()

Path knn_classifier.write_result ( dict[str, Any] payload,
Path out_path )
Write one result payload where :mod:`score` can find it.

Definition at line 906 of file knn_classifier.py.

Here is the caller graph for this function:

Variable Documentation

◆ BUILD_BATCH

int knn_classifier.BUILD_BATCH = 128

Definition at line 162 of file knn_classifier.py.

◆ DEFAULT_K

int knn_classifier.DEFAULT_K = 15

Definition at line 136 of file knn_classifier.py.

◆ DEFAULT_K_GRID

tuple knn_classifier.DEFAULT_K_GRID = (1, 5, 15, 51)

Definition at line 122 of file knn_classifier.py.

◆ DEFAULT_TEMPERATURE

float knn_classifier.DEFAULT_TEMPERATURE = 0.1

Definition at line 137 of file knn_classifier.py.

◆ DEFAULT_TEMPERATURE_GRID

tuple knn_classifier.DEFAULT_TEMPERATURE_GRID
Initial value:
1= (
2 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0,
3)

Definition at line 130 of file knn_classifier.py.

◆ FIT_BATCH

int knn_classifier.FIT_BATCH = 256

Definition at line 166 of file knn_classifier.py.

◆ HASH_NGRAM_SIZES

tuple knn_classifier.HASH_NGRAM_SIZES = (1, 2, 3, 4)

Definition at line 177 of file knn_classifier.py.

◆ JINA_MODEL_ID

str knn_classifier.JINA_MODEL_ID = "jinaai/jina-embeddings-v2-base-code"

Definition at line 145 of file knn_classifier.py.

◆ LABELS

tuple knn_classifier.LABELS = ("allow", "confirm", "deny")

Definition at line 114 of file knn_classifier.py.

◆ LMSTUDIO_MODEL

str knn_classifier.LMSTUDIO_MODEL = "text-embedding-qwen3-embedding-0.6b"

Definition at line 158 of file knn_classifier.py.

◆ LMSTUDIO_TIMEOUT

float knn_classifier.LMSTUDIO_TIMEOUT = 30.0

Definition at line 159 of file knn_classifier.py.

◆ LMSTUDIO_URL

str knn_classifier.LMSTUDIO_URL = "http://127.0.0.1:1234/v1/embeddings"

Definition at line 157 of file knn_classifier.py.

◆ MAX_SEQ_LENGTH

int knn_classifier.MAX_SEQ_LENGTH = 512

Definition at line 150 of file knn_classifier.py.

◆ NPY_ALIGNMENT

int knn_classifier.NPY_ALIGNMENT = 64

Definition at line 172 of file knn_classifier.py.

◆ NPY_MAGIC

str knn_classifier.NPY_MAGIC = b"\x93NUMPY"

Definition at line 171 of file knn_classifier.py.

◆ NPY_PREAMBLE

int knn_classifier.NPY_PREAMBLE = 10

Definition at line 173 of file knn_classifier.py.

◆ NUMPY

Any knn_classifier.NUMPY = _numpy()

Definition at line 197 of file knn_classifier.py.

◆ SUBSET_ROWS

int knn_classifier.SUBSET_ROWS = 1000

Definition at line 141 of file knn_classifier.py.

◆ SUBSET_SEED

int knn_classifier.SUBSET_SEED = 289

Definition at line 142 of file knn_classifier.py.

◆ THREADS

int knn_classifier.THREADS = 8

Definition at line 154 of file knn_classifier.py.

◆ TIE_BREAK

str knn_classifier.TIE_BREAK = "deny"

Definition at line 118 of file knn_classifier.py.