Skip to main content

Dynamic prototype memory classifier library

Project description

cliving

cliving est une librairie de classification incrémentale par mémoire dynamique.

Elle ne calcule pas les embeddings. Elle prend des vecteurs numériques en entrée et maintient une mémoire de prototypes supervisés qui évolue au fil du temps.

L'objectif est d'avoir un classifieur :

  • adaptable immédiatement après correction utilisateur
  • explicable
  • peu coûteux à l'inférence
  • sans retrain global

Idée générale

cliving fonctionne comme un système de classes vivantes.

Chaque classe n'est pas représentée par un unique centre figé. Elle possède une ou plusieurs zones locales :

  • une zone = un prototype
  • un prototype = un centre + une covariance diagonale régularisée + une confiance + un historique local

Quand un nouvel embedding arrive :

  • il est comparé à tous les prototypes
  • chaque prototype produit un score local
  • les scores sont agrégés par classe
  • la meilleure classe gagne, sauf si le cas est trop ambigu ou trop peu confiant

Quand un utilisateur corrige :

  • si l'embedding ressemble à un prototype existant de la bonne classe, ce prototype est mis à jour
  • sinon un nouveau prototype est créé

Quand on lance la maintenance :

  • les prototypes trop vieux sont affaiblis
  • certains prototypes proches peuvent fusionner
  • certains prototypes trop diffus peuvent être coupés
  • certains prototypes faibles peuvent être supprimés

Positionnement dans une stack ML

cliving se place après le calcul des embeddings.

Pipeline typique :

  1. préparer les transactions
  2. construire un embedding dense par transaction
  3. appeler fit(X, y) une première fois
  4. appeler predict(x) pour classer
  5. appeler fix(x, y_true) lorsqu'un utilisateur corrige
  6. appeler clean() périodiquement
  7. appeler save() / load() pour persister la mémoire

API publique

La classe principale est cliving.Cliving.

Construction

Signature :

Cliving(
    *,
    abstain_label: str = "review_needed",
    acceptance_score: float | None = None,
    conflict_score: float | None = None,
    low_confidence_score: float | None = None,
    ambiguity_margin: float | None = None,
)

Paramètres principaux :

  • abstain_label : classe renvoyée quand la prédiction doit être revue
  • acceptance_score : seuil minimal pour rattacher une correction à un prototype existant
  • conflict_score : seuil à partir duquel un prototype concurrent est considéré comme conflit local
  • low_confidence_score : seuil minimal de confiance pour accepter une classe
  • ambiguity_margin : marge minimale entre top-1 et top-2

Exemple :

from cliving import Cliving

model = Cliving(
    abstain_label="review_needed",
    acceptance_score=-7.0,
    conflict_score=-6.4,
    low_confidence_score=-2.2,
    ambiguity_margin=1.0,
)

Méthodes principales

fit

fit(
    X: np.ndarray | list[list[float]],
    y: np.ndarray | list[object],
) -> Cliving

Initialise complètement la mémoire à partir d'un jeu d'embeddings et de labels.

predict

predict(
    X: np.ndarray | list[float] | list[list[float]],
) -> PredictionResult | list[PredictionResult]

Retourne directement un résultat riche :

  • si X est un seul embedding 1D : un PredictionResult
  • si X est un batch 2D : une list[PredictionResult]

Les champs principaux de PredictionResult sont :

  • predicted_class
  • class_scores
  • best_prototype_id
  • best_prototype_class
  • best_prototype_score
  • margin
  • ambiguous
  • low_confidence
  • review_needed

fix

fix(
    embedding: np.ndarray | list[float],
    class_id: str,
    metadata: dict[str, object] | None = None,
) -> UpdateEvent

Applique une correction utilisateur sur un embedding unique.

Le metadata est optionnel et sert au debug / audit local des prototypes.

clean

clean() -> dict[str, int]

Lance manuellement la maintenance et retourne un résumé du type :

{"merged": 1, "split_created": 0, "pruned": 2}

summary

summary() -> dict[str, list[dict[str, object]]]

Retourne un résumé léger de la mémoire par classe et prototype.

save

save(path: str | Path) -> Path

Sauvegarde l'instance via pickle.

load

load(path: str | Path) -> Cliving

Recharge une instance précédemment sauvegardée.

Exemple minimal

import numpy as np

from cliving import Cliving

X = np.array(
    [
        [0.2, 1.1, -0.4],
        [0.1, 1.0, -0.3],
        [4.0, -0.2, 0.7],
    ],
    dtype=float,
)
y = np.array(["groceries", "groceries", "salary"], dtype=object)

model = Cliving(
    abstain_label="review_needed",
    acceptance_score=-7.0,
    conflict_score=-6.4,
    low_confidence_score=-2.2,
    ambiguity_margin=1.0,
)

model.fit(X, y)

result = model.predict([0.15, 1.05, -0.35])
print(result.predicted_class)
print(result.class_scores)

event = model.fix([0.18, 1.07, -0.33], "groceries")
stats = model.clean()

Construire des embeddings simples pour une transaction

cliving ne calcule pas les embeddings, mais un pipeline sklearn simple suffit souvent très bien.

Supposons une transaction avec :

  • label : texte court
  • amount : numérique
  • support : catégorielle, par exemple cheque, carte, virement

Exemple de pipeline simple :

import numpy as np
import pandas as pd

from sklearn.compose import ColumnTransformer
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer, OneHotEncoder, StandardScaler

from cliving import Cliving


def amount_features(frame: pd.DataFrame) -> np.ndarray:
    amount = pd.to_numeric(frame.iloc[:, 0], errors="coerce").fillna(0.0).to_numpy(dtype=float)
    return np.column_stack(
        [
            amount,
            np.abs(amount),
            np.log1p(np.abs(amount)),
            np.sign(amount),
        ]
    )


transactions = pd.DataFrame(
    [
        {"label": "carrefour city paris", "amount": -12.4, "support": "carte"},
        {"label": "monoprix courses", "amount": -48.2, "support": "carte"},
        {"label": "virement salaire acme", "amount": 2450.0, "support": "virement"},
        {"label": "cheque remboursement", "amount": 120.0, "support": "cheque"},
    ]
)

y = np.array(["groceries", "groceries", "salary", "refund"], dtype=object)

preprocess = ColumnTransformer(
    transformers=[
        (
            "text",
            HashingVectorizer(
                analyzer="char_wb",
                ngram_range=(3, 5),
                n_features=2**12,
                alternate_sign=False,
                lowercase=True,
                norm="l2",
            ),
            "label",
        ),
        ("support", OneHotEncoder(handle_unknown="ignore"), ["support"]),
        ("amount", FunctionTransformer(amount_features, validate=False), ["amount"]),
    ],
    remainder="drop",
)

embedding_pipeline = Pipeline(
    steps=[
        ("features", preprocess),
        ("svd", TruncatedSVD(n_components=16, random_state=0)),
        ("scale", StandardScaler()),
    ]
)

X = embedding_pipeline.fit_transform(transactions)

model = Cliving(
    abstain_label="review_needed",
    acceptance_score=-7.0,
    conflict_score=-6.4,
    low_confidence_score=-2.2,
    ambiguity_margin=1.0,
)
model.fit(X, y)

new_tx = pd.DataFrame(
    [
        {"label": "carrefour market", "amount": -16.3, "support": "carte"}
    ]
)

z = embedding_pipeline.transform(new_tx)[0]
result = model.predict(z)
print(result.predicted_class)

Pourquoi ce pipeline est adapté :

  • le texte capture la similarité locale des libellés
  • la variable catégorielle sépare des modes transactionnels simples
  • le montant injecte un signal métier utile
  • TruncatedSVD + StandardScaler produit un espace dense compact, adapté aux prototypes et à la distance de Mahalanobis

Ce que la librairie attend en entrée

Les embeddings doivent être :

  • numériques
  • de dimension fixe
  • convertibles en numpy.ndarray

Formats usuels :

  • np.ndarray 2D pour fit
  • np.ndarray 1D pour predict ou fix sur un seul exemple
  • list[list[float]]
  • list[float]

Démo

La démo du projet utilise l’encodeur sklearn local pour produire des embeddings, mais cet encodeur ne fait pas partie de la librairie publiée.

uv run --extra demo python examples/demo_cliving.py

Scenario simulé

Le projet contient aussi une simulation plus longue pour visualiser le caractère "vivant" de la mémoire :

  • une phase d'apprentissage initial sur un historique de transactions
  • puis un run en ligne avec nouvelles transactions, confirmations positives, corrections utilisateur et clean() périodiques
  • une première phase plus exploratoire, suivie d'une phase plus stable où certains motifs reviennent souvent

Dans cette simulation, cliving crée de nouveaux prototypes quand il découvre des zones encore mal couvertes, puis consolide progressivement la mémoire via les update, les merge et les clean(). On cherche donc à visualiser à la fois la montée en complexité de la mémoire et sa tendance à converger quand le flux devient plus répétitif.

Tests

uv run --extra dev pytest

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cliving-0.1.0.tar.gz (11.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cliving-0.1.0-py3-none-any.whl (13.8 kB view details)

Uploaded Python 3

File details

Details for the file cliving-0.1.0.tar.gz.

File metadata

  • Download URL: cliving-0.1.0.tar.gz
  • Upload date:
  • Size: 11.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.9

File hashes

Hashes for cliving-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ce0f4ea0791ce5abcd562e53fddf377dbe7cdc651c3424bb258f17a95e8758e7
MD5 7e7fe3a849a92a55213eff61d5ed8956
BLAKE2b-256 1cae93b10591189dc7c03d0493ad020b36f4919ad93752043f3288e437dd307a

See more details on using hashes here.

File details

Details for the file cliving-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: cliving-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.9

File hashes

Hashes for cliving-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 551d547eac7c35a373c603216731edadf3574ffb880e2e59562c33f1750482ac
MD5 d7814d0d54268475366617505393825c
BLAKE2b-256 1f5ad38033b6ce19401328eedc47861aeb58eca08423794dce128afa698d039f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page