Files
cluster-knaster/core/src/knaster/classifier.py
T

51 lines
2.0 KiB
Python

"""Stufe 3: Entscheidungen direkt auf Embeddings (Nearest Centroid mit Konfidenz)."""
from __future__ import annotations
import json
import math
from collections import defaultdict
from pathlib import Path
from .embeddings import Vector, cosine
class EmbeddingClassifier:
def __init__(self, centroids: dict[str, Vector], counts: dict[str, int] | None = None):
if not centroids:
raise ValueError("Classifier braucht mindestens eine Klasse")
self.centroids = centroids
self.counts = counts or {}
@classmethod
def train(cls, examples: list[tuple[Vector, str]]) -> EmbeddingClassifier:
sums: dict[str, Vector] = {}
counts: dict[str, int] = defaultdict(int)
for vec, label in examples:
if label not in sums:
sums[label] = [0.0] * len(vec)
sums[label] = [a + b for a, b in zip(sums[label], vec, strict=True)]
counts[label] += 1
centroids = {}
for label, total in sums.items():
norm = math.sqrt(sum(v * v for v in total)) or 1.0
centroids[label] = [v / norm for v in total]
return cls(centroids, dict(counts))
def predict(self, vec: Vector) -> tuple[str, float]:
"""Liefert (Label, Konfidenz). Konfidenz = Softmax-Abstand der besten zwei Klassen (0..1)."""
scores = sorted(((cosine(vec, c), label) for label, c in self.centroids.items()), reverse=True)
best_score, best_label = scores[0]
if len(scores) == 1:
return best_label, max(0.0, best_score)
exps = [math.exp(s * 20) for s, _ in scores] # Temperatur 0.05
return best_label, exps[0] / sum(exps)
def save(self, path: str | Path) -> None:
Path(path).write_text(json.dumps({"centroids": self.centroids, "counts": self.counts}))
@classmethod
def load(cls, path: str | Path) -> EmbeddingClassifier:
data = json.loads(Path(path).read_text())
return cls(data["centroids"], data.get("counts"))